前端图形实战:从零开始开发几何画板(vue3+vite版)
时间:2023-03-13 17:52:15
科技观察
前言大家好,我是许小希,今天又是我们的博学时间。本文是100+前端几何应用案例专栏的第二篇文章。在第一篇几何在前端边界计算中的应用与原理分析中,介绍了几何在前端领域的应用。同时,我使用vue3带来了Together,我们实现了常见图形的边界计算算法,分享了如何利用几何原理和WebDom生成任意三角形:有兴趣的可以查看我在gitee中的具体代码实现:https://gitee.com/lowcode-china/euryd继续这个话题,我们进一步扩展,从零开始实现一个几何画板。你将获得vue3+vite的实用技能几何画板的基本开发思路创建、编辑、拖放、图层管理撤消和重做导入导出运用几何代数知识解决前端问题给你看成品demo,让大家更好的理解我们接下来要做的事情:技术实现我们继续沿用上一篇几何在前端边界计算中的应用和原理分析项目,因为几何画板相当于一个具有一定复杂度的独立小应用。这里我们将配置less对vite项目的支持:安装less和less-loader(推荐yarn、pnpm),在vite.config.ts中做如下配置:exportdefaultdefineConfig({plugins:[vue()],css:{preprocessorOptions:{less:{modifyVars:{hack:`true;@import(reference)"${path.resolve("src/base.less")}";`,},javascriptEnabled:true,},},},})配置完成后,我们就可以在vite项目中以less的方式编写样式代码了。modifyVars属性中的配置就是指定less全局变量地址,这样我们就可以把主题和通用样式放在这个目录下,这样在项目的任何页面都可以直接使用。好了,准备工作已经完成,我们开始下一个实现部分。1.画板搭建画板搭建主要是静态部分和交互部分。这里简单介绍一下基本结构:上图可以看出绘图板主要分为两部分:画布区域(包括记录鼠标移动坐标的文字提示)和侧边控制区域的画布我们使用实现点阵背景的css背景样式。网上教程很多,就不一一分析了。下面是直接实现的代码。你可以使用它:section.card{position:relative;高度:480px;盒子阴影:004pxrgba(0,0,0,0.1);背景图像:径向渐变(rgba(9、89、194、0.3)6%,透明0),径向渐变(#faf9f86%,透明0);背景大小:10px10px;background-position:00,2px2px;}整个画板应用的基本结构如下:如果你对这块知识感兴趣,可以参考我的实现代码,具体代码地址:https://gitee.com/lowcode-china/euryd接下来我们开始对比核心方案设计。2.创建并绘制几何图形因为是画板应用,图形的创建必须简单灵活,控制权交给用户和鼠标,所以这里实现的效果如下:用户只需选中相应的图形,用鼠标在画布上点击拖动即可创建任意大小和比例的图形。为了实现这个效果,我们需要做以下准备工作:定义图形的schema结构,根据鼠标光标的位置计算图形创建的元信息(图形id、顶点坐标、宽高)Style和其他属性)(1)定义图的schema结构和任何可视化低代码产品一样,我们都需要一个统一的、可扩展的组件schema结构,以便更好地识别组件和分发属性。对于画板应用中的图形,我们可以设计如下schhema结构:typeIShapeTypes='rect'|'圆圈'|'线';interfaceIShapeProps{id:string,type:IShapeTypes,style:{width:number,height:number,left:number,top:number,...otherStyle},isEditable:boolean,...otherSchema}只要具体配置是通用的,我们可以结合自己的业务进行配置。模式定义好后,我们只需要实现相应的图形即可。这里我们以矩形为例和大家分享实现细节。(2)根据鼠标光标的位置计算图形创建的元信息。我们都知道,要通过拖动鼠标来创建任意一个矩形,我们需要知道几个条件:鼠标按下的初始点的坐标和鼠标实时位置这两个问题,其实是可以全局实现的。基于组件设计的原子化原理,我们可以捕获并计算鼠标在画布组件中的实时位置,然后分发给其他组件消费。这样,我们也可以实现增加了记录鼠标移动坐标的文字提示的功能。在上一篇文章中,我已经介绍了如何使用vue3的组合函数来实现通用的hooks。接下来我们要做的是对useMouse得到的结果进行处理,让其他组件可用。这里我使用vue3的toRefs来实现。先来看一下代码://BaseBoard.tsx{{msg}}
x:{{x-cardOffset.x}},y:{{y-cardOffset.y}}
BaseBoard是我们的canvas组件,我们可以使用这个组件创建任意数量的canvas,并且因为vue3的组合功能支持使用defineProps来定义组件的props,所以我们可以通过它来定义组件的属性,这里暴露了两个属性:msg用于控制组件的名称外部画布。将内部鼠标监听到的事件传递给外部,使外部可以获取到内部的事件。我们在使用useMouse的时候,可以实时得到鼠标x和y的绝对坐标,然后减去画布在页面上的实际偏移量cardOffset。x,cardOffset.y,可以得到鼠标在画布中的正确坐标:这样我们就可以通过onMouseChange回调将鼠标相对于画布的坐标实时传递给父组件:const{x,y}=useMouse(window,(x,y)=>{onMouseChange&&onMouseChange.value(x-cardOffset.value.x,y-cardOffset.value.y);});同时,我们在代码中发现了defineExpose,这个api的作用是将Dataexport暴露出来供父组件使用,相当于把child传递给parent,我们可以在parent中获取暴露的值组件,这里我们暴露了canvas的dom,这样父组件就可以获取到子组件的dom有了以上前提,我们就可以创建矩形元素了。为了更好的管理画布中的元素,我们定义了一个元素集合canvasBox:typeshapeType="rect"|“圈子”|“线”;接口IBaseShapeProp{类型:shapeType;键:字符串;style:any;}constcanvasBox=ref<{[keyinshapeType]:IBaseShapeProp[]}>({rect:[],circle:[],line:[],});当用户选择一个形状并在画布上按下鼠标时,我们创建一个基本的元数据:consthandleMouseDown=(){const{x,y}=mouseAbsPos.value;如果(curShape.value){templateDot=[x,y];templateDot[2]=Date.now()+"";canvasBox.value["rect"].push({type:"rect",key:templateDot[2],style:{},});}};从上面的代码可以看出,我们会创建一个矩形的元数据,包括矩形:元素类型矩形的唯一键(方便后续快速查图)矩形的初始化样式以及我们在templateDot变量缓存鼠标的初始位置,方便后续生成矩形的完整元数据。图中我们可以看到,当鼠标拖动时,矩形是跟随鼠标实时生成的。要实现这个效果,我们需要监听鼠标的mousemove,动态更新矩形的元数据,如下:consthandleMouseChange=(x:number,y:number)=>{mouseAbsPos.value={x,y};//1.如果有选中的元素,则判断移动当前选中的元素if(curSelect.value&&templateDot.length){//something...return;}//2.否则,生成元素const[a1,b1,key]=templateDot;if(curShape.value&&templateDot.length){让dx=x-a1;让dy=y-b1;让curIndex=canvasBox.value["rect"].findIndex((v)=>v.key===key);如果(curIndex>-1){canvasBox.value["rect"][curIndex]={...canvasBox.value["rect"][curIndex],样式:{left:(dx>0?a1:x)+“px”,顶部:(dy>0?b1:y)+“px”,宽度:Math.abs(dx)+“px”,高度:Math.abs(dy)+“px”,},};}}};从代码中可以看出,我是通过实时改变矩形元素的left和top来实现的矩形是跟随鼠标实时更新的。我们也可以使用transform来达到同样的效果。有兴趣的朋友可以试试。顺便在这里展开一下。我们平时看到的拖拽框架在对组件进行多选操作时,也是采用同样的方法。多选区域通过鼠标拖拽滑动生成:有兴趣的朋友可以展开这个方案。实现更多有趣的应用场景。3、移动和编辑几何图形在上面创建元素的基础上,我们继续实现移动和编辑元素的功能。3.1MovingelementsFirstweneedtofindtheelementtobemoved,andthendynamicallychangeitsposition,becauseIsetauniquekeyforeachelement,sowhentheelementisselected,wecanfindtheelementaccordingtothekey,andonlyfor对元素进行操作://如果有选中的元素,则判断移动当前选中的元素if(curSelect.value&&templateDot.length){const[x0,y0]=templateDot;canvasBox.value["rect"]=canvasBox.value["rect"].map((v)=>{if(v.key===curSelect.value){const{left,top}=v.style;templateDot=[x,y];return{...v,style:{...v.style,left:parseFloat(left)+(x-x0)+"px",top:parseFloat(top)+(y-y0)+"px",},};}返回v;});返回;}上面代码主要是通过计算鼠标移动的位置差来改变元素的left和top值(通过缓存鼠标上一步的坐标),在mouseup时重新设置缓存变量,移动过程即可完成一次。consthandleMouseUp=(){const{x,y}=mouseAbsPos.value;if(curShape.value){//1.如果起点和终点相同,则不创建if(templateDot[0]===x&&templateDot[1]===y){canvasBox.value["rect"]=canvasBox.value["rect"].filter((v)=>v.key!==templateDot[2]);模板点=[];返回;}}//重置templateDot=[];};这里有一个细节需要注意,就是如果鼠标按下后没有拖拽(也就是点击画布的操作),其实是需要创建mousedown元素的清空和删除了,所以上面代码第一步判断。3.2编辑元素编辑元素的方式与移动元素类似。改变的是元素的静态属性。比如我们可以编辑元素的背景颜色、边框样式等。这里我以删除元素为例介绍实现过程。首先,让我们展示元素的dom结构:
x 当我们双击元素时,我们会通过key给当前选中的元素一个激活状态,此时会显示v-if的删除按钮,我们绑定删除方法handleDel:consthandleDel=(key:string)=>{canvasBox.value["rect"]=canvasBox.value["rect"].filter((v)=>v.key!==key);curSelect.value="";模板点=[];};删除元素的方法是典型的单向操作,比较简单。如果我们要改变元素的整体属性,我们需要设计一个属性面板,并实现一个表单渲染器来动态更新元素的属性,类似于H5-Dooring中的Edit面板:在后面的文章中,我会实现一个最小版本的属性编辑器来改进我们的几何画板。4、图层管理、图片导出等解决方案介绍图层管理也是编辑器的一个常用功能。有了我们之前设计的canvasBox,我们就可以轻松实现一个图层管理面板。我们只需要将存储在canvasBox中的元素数组遍历到图层面板,为其绑定操作方法即可实现图层管理常用的功能,如:显示、隐藏、快速删除、批量删除多选图层、移动、切换等元素,如H5-Dooring层管理面板中图: