前端canvas项目实战——简历制作网站(四)——右侧属性栏(线条端点样式)

目录

  • 前言
  • 一、效果展示
  • 二、实现步骤
    • 1. 实现线条和端点的组装模块
    • 2. 修复一个fabric自身的bug
    • 3. 实现属性栏中的编辑模块
    • 4. 把UI操作和画布更新连接起来
  • 三、Show u the code
  • 后记

前言

上一篇博文中,我们扩充了线条对象(fabric.Line)的属性列表,使用户可以修改画布中选中的线条的宽度和样式(实线、虚线、点线等)。

这篇博文是《前端canvas项目实战——简历制作网站》付费专栏系列博文的第四篇——右侧属性栏(线条端点样式),主要的内容有:

  1. 针对线条对象: 扩充属性列表,使用户可以为画布中选中的线条增加或修改端点样式。

如有需要,可以:

  • 点击这里,返回第一篇《前端canvas项目实战——简历制作网站(一)——左侧工具栏》
  • 点击这里,返回上一篇《前端canvas项目实战——简历制作网站(三)——右侧属性栏(线条宽度&样式)》

一、效果展示

  • 动手体验
    CodeSandbox会自动对代码进行编译,并提供地址以供体验代码效果
    由于CSDN的链接跳转有问题,会导致页面无法工作,请复制以下链接在浏览器打开:
    https://qgt4m4.csb.app/

  • 动态效果演示

  • 本节之后,我们的简历能做成什么样子
    本节所做的改动是线条的扩展能力,对我们目标中的简历没有影响。

二、实现步骤

为线条添加端点并不是fabric.Line自带的基础能力,要实现这样的功能,我们需要用到fabric.Group。即通过let pointedLine = new fabric.Group([线条, 左端点, 右端点])这样的形式让多个对象构成一个。这样用户所做的拖拽、缩放、旋转等操作就可以直接施加在整个组上,对其中的所有对象同时生效。

1. 实现线条和端点的组装模块

基于上述目的,我们新建一个代码模块complex-line.js。这里的代码会在用户为线条添加第一个端点时创建一个Group,在为线条去除最后一个端点时将Group恢复为Line

import { fabric } from "fabric";const pointPathsMap = {arrow1: "M 0 0 L 10 5 L 0 10 z",...
};const pointPathsList = [{ name: "无", fill: null },{ name: "arrow1", fill: "black" },{ name: "arrow1", fill: "transparent" },...
];const newPoint = (line, pointType, isStartPoint) => {let lineACoords = line.calcACoords();let angle = line.angle,left = (lineACoords.tr.x + lineACoords.br.x) / 2,top = (lineACoords.tr.y + lineACoords.br.y) / 2;if (isStartPoint) {angle = (angle + 180) % 360;left = (lineACoords.tl.x + lineACoords.bl.x) / 2;top = (lineACoords.tl.y + lineACoords.bl.y) / 2;}let scale = (line.strokeWidth + 7) * 0.125;let pointParams = pointPathsList[pointType];let point = new fabric.Path(pointPathsMap[pointParams.name], {left,top,originX: "center",originY: "center",angle,fill: pointParams.fill === "transparent" ? "transparent" : line.stroke,stroke: line.stroke,strokeWidth: line.strokeWidth,scaleX: scale,scaleY: scale,});// 调整端点位置,避免线条总长被改变let factor = isStartPoint ? -1 : 1;let offsetLeft = point.width / 2;point.set({left: left + offsetLeft * factor,top,});return point;
};const operateLineWithAngle = (fn) => {return function (...args) {let line = args[0];let angle = line.angle;line.set({ angle: 0 });line = fn(args) || line;line.set({ angle });return line;};
};const handleScalingGroup = operateLineWithAngle((args) => {let [group, startPointType, endPointType] = args;let groupObjects = group.getObjects();for (let i = 1; i < groupObjects.length; i++) {group.remove(groupObjects[i]);}if (startPointType && 0 !== startPointType) {group.add(newPoint(groupObjects[0], startPointType, true));}if (endPointType && 0 !== endPointType) {group.add(newPoint(groupObjects[0], endPointType, false));}group.addWithUpdate();group.setCoords();
});const refreshLine = operateLineWithAngle((args) => {let [line, startPointType, endPointType] = args;let groupArray;if (line.type === "line") {groupArray = [line];} else {line.getObjects()[0].clone((newLine) => {groupArray = [newLine];});}if (startPointType && 0 !== startPointType) {groupArray.push(newPoint(groupArray[0], startPointType, true));}if (endPointType && 0 !== endPointType) {groupArray.push(newPoint(groupArray[0], endPointType, false));}if (groupArray.length > 1) {line = new fabric.Group(groupArray, { startPointType, endPointType });line.on("scaling", (event) => {let { original, target, corner } = event.transform;let { left, top } = original;let aCoordsBefore = target.calcACoords();// 让两个端点保持不被拉伸和压缩handleScalingGroup(target, startPointType, endPointType);if (corner === "ml") {let aCoordsAfter = target.calcACoords();let offsetX = aCoordsAfter.tr.x - aCoordsBefore.tr.x;let offsetY = aCoordsAfter.tr.y - aCoordsBefore.tr.y;left = target.left - offsetX;top = target.top - offsetY;}target.set({ left, top });});} else {line = groupArray[0];}return line;
});const assembleLine = (line, startPointType, endPointType) => {let oldCenterPoint = line.getCenterPoint();line = refreshLine(line, startPointType, endPointType);let newCenterPoint = line.getCenterPoint();// 计算 left 和 top 的位移const deltaX = newCenterPoint.x - oldCenterPoint.x;const deltaY = newCenterPoint.y - oldCenterPoint.y;// 以中心点作为变换前后的基准line.set({top: line.top - deltaY,left: line.left - deltaX,});return line;
};export { assembleLine, pointPathsMap, pointPathsList };

可见,这个模块的功能比较复杂。我们将其拆分为6个部分进行讲解:

  • pointPathsMappointPathsList: 由于我们通过svg来绘制线条的端点,因此前者以字典的形式存储各种图形的path信息;后者以列表的形式存储各种端点类型的属性信息。
  • newPoint方法: 该方法用于为传入的线条生成指定的端点。通过fabric.Path来绘制自定义的图形,即为线条的端点。
  • operateLineWithAngle方法: 一个增强方法,参数是另一个方法。其流程为:
    • 执行传入的方法fn之前记录线条当前的角度angle,并把angle设为0
    • 执行方法fn
    • 在执行方法fn之后再把angle设置回之前记录的值。
    • 这样做的好处是: 避免了各种情况下计算/更新线条和端点相对位置时,需要时刻考虑angle不为0带来的偏移量,即对各种sin/cos三角函数的计算。
  • handleScalingGroup方法: 经过了上述增强的方法。用于用户缩放Group时,重新绘制端点,避免端点跟随着一起被缩放。
  • refreshLine方法: 经过了上述增强的方法。用于在用户想要对线条增删端点时刷新线条的样式。区分GroupLine两种状态。如果当前要为Line增加端点,就会返回一个Group;如果当前要为Group去掉最后一个端点,就会返回一个Line
  • assembleLine方法: 主方法。用于为线条增删端点,同时消除坐标上的偏移。暴露出去供属性编辑模块调用。

2. 修复一个fabric自身的bug

请先设想,有两条长度相同、位置相同的线条上下重叠。上面的线条为绿色,下面的线条为黑色。当我们把上面的那条线的宽度strokeWidth提高到5,预期它可以在水平和竖直两个方向上都保持居中。但实际表现如下图所示:

--->

可以看到,线条加粗后会在右和下两个方向上有误差偏移。 因此,在wrap-line.js中添加一下代码进行修正:

fabric.Line.prototype._adjustPosition = function (originalCenter) {// 获取变化后的中心点坐标const newCenter = this.getCenterPoint();// 计算 left 和 top 的位移const deltaX = newCenter.x - originalCenter.x;const deltaY = newCenter.y - originalCenter.y;// 更新 left 和 top 值this.set({left: this.left - deltaX,top: this.top - deltaY,});
};fabric.Line.prototype.set = (function (fn) {return function (key, value) {// 获取变化前的中心点坐标const originalCenter = this.getCenterPoint();// 调用父类的 set 方法const result = fn.call(this, key, value);// 如果 key 是一个对象,检查 strokeWidth 是否在其中if (typeof key === "object" && "strokeWidth" in key) {this._adjustPosition(originalCenter);}// 如果 key 是字符串,检查是否等于 'strokeWidth'if (typeof key === "string" && key === "strokeWidth") {this._adjustPosition(originalCenter);}return result;};
})(fabric.Line.prototype.set);

共分为两个部分:

  • fabric.Line的原型添加一个_adjustPosition方法,用于调整线条的位置
  • 重新封装fabric.Line原型的set方法,当调用set方法编辑线条的宽度strokeWidth后,调用_adjustPosition方法修正位置

经过调整后,再次加粗线条,位置就正确了:

--->

3. 实现属性栏中的编辑模块

我们继续在object-props.js中添加以下代码:

  const LinePointWrapperTemplate = (props) => {const { title, optionViews, handleChange, pointType } = props;return (<div className="property-row"><span className="property-title">{title}</span><div className="property-container"><Select value={pointType} bordered={false} style={{width:"100%"}} onChange={handleChange}>{optionViews}</Select></div></div>);};const LineStartPointWrapper = (props) => {const newProps = {title: "始端样式",handleChange: (newValue) => {handleChange("pointsType", [newValue, endPointType]);},optionViews: pointPathsList.map((pointParams, index) => {let menuItem;if (0 === index) {menuItem = <span></span>;} else {menuItem = (<svg width="26" height="11" viewBox="-1 -0.5 25 11"><path d={pointPathsMap[pointParams.name]} fill={pointParams.fill}stroke="black" strokeWidth="1" transform="rotate(180 5 5)" /><line x1="11" y1="5" x2="25" y2="5" stroke="black" strokeWidth="1" /></svg>);}return (<Option className="property-stroke-width" value={index} title={pointParams.name}key={`line-point-${pointParams.name}-${pointParams.fill}`}><div className="property-line-point">{menuItem}</div></Option>);}),pointType: startPointType,};return <LinePointWrapperTemplate {...newProps} key={props.key} />;};const LineEndPointWrapper = (props) => {···};

显而易见,代码分为3个部分:

  • LinePointWrapperTemplate: 端点属性模块的模板。由于线条两端可以添加不同的端点,所以需要两个编辑模块。而这两个编辑模块大部分的内容都是相同的,因此提取出一个模板模块,复用代码。
  • LineStartPointWrapper: 线条始端的编辑模块,调用了上述模板。
  • LineEndPointWrapper: 线条末端的编辑模块,调用了上述模板,与LineStartPointWrapper类似,此处省略代码。

两个属性编辑模块实现的效果:

4. 把UI操作和画布更新连接起来

在前面的博文中,我没有可以提及这个部分,现在有必要讲一讲了,真正去更新画布中对象属性的是下面这个方法:

const updateProperty = (object, key, newValue) => {let objectList;if (object.type === "group") {objectList = [object.getObjects()[0]];} else {objectList = [object];}let { canvas } = store.getState();for (let i = 0; i < objectList.length; i++) {let _object = objectList[i];if (typeof newValue !== "object" || key === "strokeDashArray") {_object.set(key, newValue);} else if (key === "pointsType") {let newLine = assembleLine(object, newValue[0], newValue[1]);canvas.remove(object);canvas.add(newLine);canvas.setActiveObject(newLine);} else {_object.set(key, newValue[_object.id][key]);}}canvas.renderAll();}

代码逻辑很简单:

  • 遇到普通属性,就直接使用set方法对对象进行更新
  • 遇到pointsType线条端点类型:
    • 调用前文中提到的assembleLine方法重新创建一个新的线条对象
    • 从画布中移除旧的线条对象
    • 将新的线条对象添加到画布中
    • 调用画布的renderAll方法重新渲染画布

三、Show u the code

按照惯例,本节的完整代码我也托管在了CodeSandbox中,点击前往,查看完整代码


后记

本节看似不复杂的功能,却实实在在耗费了我近一个月的空闲时间。基础的实现并不难,难在解决各种各样的bug,包括自己写的,还有fabric框架自身的。

我写代码注重细节,而这个功能又是要全面考虑缩放、旋转等情况下Group中的几个子对象相对位置是否正确。所以用时良久,呕心沥血。

这里在修正各种位移偏差时得到一个经验: 如果一个operation()方法会使操作的对象产生不必要的位移,可以先选定一个调用A()前后坐标都不应该变化的点P

  let oldP = line.getP();operation(line);let newP = line.getP();// 计算位移偏差let offsetX = newP.x - oldP.x;let offsetY = newP.y - oldP.y;// 消除位移偏差line.set({left: line.left - offsetX,top: line.top - offsetY})

这种方法在我的实现中多次用到,有兴趣的小伙伴可以翻翻前文中列出的代码。这样做可以消除原本需要考虑的sin/cos等三角函数计算,屡试不爽!

如有需要,可以:

  • 点击这里,返回第一篇《前端canvas项目实战——简历制作网站(一)——左侧工具栏》
  • 点击这里,返回上一篇《前端canvas项目实战——简历制作网站(三)——右侧属性栏(线条宽度&样式)》

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.rhkb.cn/news/261203.html

如若内容造成侵权/违法违规/事实不符,请联系长河编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

构建企业数据安全的根基:深入解析数据安全治理能力评估与实践框架

随着数字化转型深入各行各业&#xff0c;数据安全已成为企业不可或缺的重要议题。在这一背景下&#xff0c;有效的数据安全治理框架成为确保企业数据安全的基石。 一、数据安全治理框架 中国互联网协会于 2021 年发布 T/SC-0011-2021《数据安全治理能力评估方法》&#xff0c…

windows10|音视频剪辑|FFMPEG录屏和网络推流源初步的生成

前言&#xff1a; FFMPEG的功能强大是毋庸置疑的&#xff0c;那么录屏的需求大家在某些时候大家可能是非常需要的&#xff0c;例如&#xff0c;现有的项目需要演示&#xff0c;因此录制一段演示视频&#xff1b;亦或者做内容分发直播的&#xff0c;比如游戏主播&#xff0c;需…

MySQL 学习记录 1

原文&#xff1a;https://blog.iyatt.com/?p12631 1 前言 去年年初报考 3 月的计算机二级&#xff08;C 语言&#xff09;【https://blog.iyatt.com/?p9266 】考过了&#xff0c;这次打算报考 3 月的计算机三级&#xff08;数据库&#xff09;。数据库这一块&#xff0c;很…

Kubernetes(K8s)的基础概念

K8s的概念 K8S 的全称为 Kubernetes (K12345678S) &#xff08;简化全称&#xff09; Kubernetes 是一个可移植、可扩展的开源平台&#xff0c;用于 管理容器化工作负载和服务&#xff0c;有助于声明式配置和自动化。它拥有庞大且快速发展的生态系统。Kubernetes 服务、支持和…

SQL防止注入工具类,可能用于SQL注入的字符有哪些

SQL注入是一种攻击技术&#xff0c;攻击者试图通过在输入中注入恶意的SQL代码来干扰应用程序的数据库查询。为了防止SQL注入&#xff0c;你需要了解可能用于注入的一些常见字符和技术。以下是一些常见的SQL注入字符和技术&#xff1a; 单引号 ​&#xff1a; 攻击者可能会尝试…

【前端工程化面试题】webpack proxy的工作原理,为什么能解决跨域问题

在 webpack 的配置文件 webpack.config.js 中有一个配置项 devServer 里面有一个属性是 proxy&#xff0c;这里面可以配置代理服务器&#xff0c;解决跨域问题&#xff0c;请参考官网。 一般来说 webpack 的代理就是说的开发服务器 webpack-dev-server。 其实不光是 webpack 其…

线阵相机之帧超时

1 帧超时的效果 在帧超时时间内相机若未采集完一张图像所需的行数&#xff0c;则相机会直接完成这张图像的采集&#xff0c;并自动将缺失行数补黑出图&#xff0c;机制有以下几种选择&#xff1a; 1. 丢弃整张补黑的图像 2. 保留补黑部分出图 3.丢弃补黑部分出图

爬虫知识--02

免费代理池搭建 # 代理有免费和收费代理 # 代理有http代理和https代理 # 匿名度&#xff1a; 高匿&#xff1a;隐藏访问者ip 透明&#xff1a;服务端能拿到访问者ip 作为后端&#xff0c;如何拿到使用代理人的ip 请求头中&#xff1a;x-forwor…

⭐北邮复试刷题106. 从中序与后序遍历序列构造二叉树__递归分治 (力扣每日一题)

106. 从中序与后序遍历序列构造二叉树 给定两个整数数组 inorder 和 postorder &#xff0c;其中 inorder 是二叉树的中序遍历&#xff0c; postorder 是同一棵树的后序遍历&#xff0c;请你构造并返回这颗 二叉树 。 示例 1: 输入&#xff1a;inorder [9,3,15,20,7], postor…

人工智能深度学习

目录 人工智能 深度学习 机器学习 神经网络 机器学习的范围 模式识别 数据挖掘 统计学习 计算机视觉 语音识别 自然语言处理 机器学习的方法 回归算法 神经网络 SVM&#xff08;支持向量机&#xff09; 聚类算法 降维算法 推荐算法 其他 机器学习的分类 机器…

基于vue的个性化推荐餐饮系统Springboot

项目&#xff1a;基于vue的个性化推荐餐饮系统Springboot 摘要 现代信息化社会下的数据管理对活动的重要性越来越为明显&#xff0c;人们出门可以通过网络进行交流、信息咨询、查询等操作。网络化生活对人们通过网上购物也有了非常大的考验&#xff0c;通过网上进行点餐的人也…

C# Winfrom实现的肺炎全国疫情实时信息图

运行结果&#xff1a; using System; using System.Drawing; using System.Text; using NSoup; using NSoup.Nodes; using System.IO; using System.Net; using System.Text.RegularExpressions; using System.Windows.Forms;namespace Pneumonia {public partial class MainFo…

C#开发AGV地图编辑软件

C#自己开发AGV地图编辑软件&#xff1a; 1、自由添加和删除站点、停车位、小车、运行路径。 2、编辑得地图以XML文件保存。 3、导入编辑好地图的XML文件。 4、程序都是源码&#xff0c;可以直接在此基础上进行二次开发。 下载链接&#xff1a;https://download.csdn.net/d…

【Pytorch深度学习开发实践学习】B站刘二大人课程笔记整理lecture04反向传播

lecture04反向传播 课程网址 Pytorch深度学习实践 部分课件内容&#xff1a; import torchx_data [1.0,2.0,3.0] y_data [2.0,4.0,6.0] w torch.tensor([1.0]) w.requires_grad Truedef forward(x):return x*wdef loss(x,y):y_pred forward(x)return (y_pred-y)**2…

19个Web前端交互式3D JavaScript框架和库

JavaScript &#xff08;JS&#xff09; 是一种轻量级的解释&#xff08;或即时编译&#xff09;编程语言&#xff0c;是世界上最流行的编程语言。JavaScript 是一种基于原型的多范式、单线程的动态语言&#xff0c;支持面向对象、命令式和声明式&#xff08;例如函数式编程&am…

Spring最新核心高频面试题(持续更新)

1 什么是Spring框架 Spring框架是一个开源的Java应用程序开发框架&#xff0c;它提供了很多工具和功能&#xff0c;可以帮助开发者更快地构建企业级应用程序。通过使用Spring框架&#xff0c;开发者可以更加轻松地开发Java应用程序&#xff0c;并且可以更加灵活地组织和管理应…

OpenAI全新发布文生视频模型:Sora!

OpenAI官网原文链接&#xff1a;https://openai.com/research/video-generation-models-as-world-simulators#fn-20 我们探索视频数据生成模型的大规模训练。具体来说&#xff0c;我们在可变持续时间、分辨率和宽高比的视频和图像上联合训练文本条件扩散模型。我们利用对视频和…

【Vuforia+Unity】AR03-圆柱体物体识别

1.创建数据库模型 这个是让我们把生活中类似圆柱体和圆锥体的物体进行AR识别所选择的模型 Bottom Diameter:底部直径 Top Diameter:顶部直径 Side Length:圆柱侧面长度 请注意&#xff0c;您不必上传所有三个部分的图片&#xff0c;但您需要先为侧面曲面关联一个图像&#…

HarmonyOS—@Observed装饰器和@ObjectLink嵌套类对象属性变化

Observed装饰器和ObjectLink装饰器&#xff1a;嵌套类对象属性变化 概述 ObjectLink和Observed类装饰器用于在涉及嵌套对象或数组的场景中进行双向数据同步&#xff1a; 被Observed装饰的类&#xff0c;可以被观察到属性的变化&#xff1b;子组件中ObjectLink装饰器装饰的状…

动态内存管理(下)

动态内存管理&#xff08;上&#xff09;-CSDN博客&#xff08;malloc&#xff0c; realloc&#xff0c; calloc&#xff0c; free函数的用法以及注意事项等知识点&#xff09; 动态内存管理&#xff08;中&#xff09;-CSDN博客&#xff08;常见的内存出错问题) -----------…