前言最近空闲时间比较多,想做一些小工具玩玩。我选择了几个方案,最后决定做一个canvasbaseddrawingboard。第一个版本已经完成,主要功能如下。画笔(动态宽度设置、颜色设置)橡皮擦回缩、反回缩、清除画板、保存画板拖拽多层预览当前效果如下预览地址:https://lhrun.github.io/paint-board/repo:https://github.com/LHRUN/paint-board欢迎star??Paintboard设计从创建一个canvas画布类开始,这里处理画布上的所有操作和数据,比如初始化,渲染,拖动画布,etc.classPaintBoard{canvas:HTMLCanvasElementcontext:CanvasRenderingContext2D...constructor(canvas:HTMLCanvasElement){}//初始化画布initCanvas(){}//renderrender(){}//dragdrag(){}...}然后在canvas类的基础上,根据当前的操作,创建相应的canvas元素,比如画笔、橡皮擦等,基本类型如下//...constructor(type:string,layer:number){this.type=typethis.layer=layer//...}//...}最后会根据渲染逻辑,封装一些通用的逻辑来改变最终在画布上的显示,比如撤回、反撤回、图层操作等对于画笔要实现画笔效果,首先在鼠标按下时创建一个画笔元素,然后在构造函数中接受基础宽度和颜色,初始化鼠标移动记录和线宽记录,然后记录鼠标移动的坐标当鼠标移动时,以反映如果鼠标移动快,线宽会变窄;如果鼠标移动缓慢,线宽会恢复正常。为了这个效果,我会计算当前的移动速度,然后根据速度计算线宽classFreeLineextendsCanvasElement{...constructor(color:string,width:number,layer:number){this.positions=[]//鼠标移动位置记录this.lineWidths=[0]//线宽记录this.color=color//当前绘制的线颜色this.maxWidth=width//最大线widththis.minWidth=width/2//最小线宽this.lastLineWidth=width//最后绘制的线宽}}记录鼠标位置和当前线宽interfaceMousePosition{x:numbery:number}addPosition(position:MousePosition){this.positions.push(position)//处理当前线宽if(this.positions.length>1){constmouseSpeed=this._computedSpeed(this.positions[this.positions.length-2],this.positions[this.positions.length-1])constlineWidth=this._computedLineWidth(mouseSpeed)this.lineWidths.push(lineWidth)}}/***计算移动速度*@paramstart起点*@paramend终点*/_computedSpeed(start:MousePosition,end:MousePosition){//获取距离constmoveDistance=getDistance(start,end)constcurTime=Date.now()//获取移动距离vementintervallastMoveTime:最后一次鼠标移动时间constmoveTime=curTime-this.lastMoveTime//计算速度constmouseSpeed=moveDistance/moveTime//更新上次移动时间this.lastMoveTime=curTimereturnmouseSpeed}/***计算画笔宽度*@paramspeed鼠标移动速度*/_computedLineWidth(speed:number){letlineWidth=0constminWidth=this.minWidthconstmaxWidth=this.maxWidthif(speed>=this.maxSpeed){lineWidth=minWidth}elseif(speed<=this.minSpeed){lineWidth=maxWidth}else{lineWidth=maxWidth-(speed/this.maxSpeed)*}maxWidth}lineWidth=lineWidth*(1/3)+this.lastLineWidth*(2/3)this.lastLineWidth=lineWidthreturnlineWidth}保存坐标后渲染就是遍历所有坐标functionfreeLineRender(context:CanvasRenderingContext2D,instance:FreeLine){context.save()context.lineCap='round'context.lineJoin='round'context.strokeStyle=instance.colorfor(leti=1;ivoid,instance:CleanLine){for(leti=0;ivoid,cleanWidth:number){const{x:x1,y:y1}=startconst{x:x2,y:y2}=end//获取鼠标起点和终点之间矩形区域的端点constasin=cleanWidth*Math.sin(Math.atan((y2-y1)/(x2-x1)))constacos=cleanWidth*Math.cos(Math.atan((y2-y1)/(x2-x1)))constx3=x1+asinconsty3=y1-acosconstx4=x1-asinconsty4=y1+acosconstx5=x2+asinconsty5=y2-acosconstx6=x2-asinconsty6=y2+acos//清除结束弧context.save()context.beginPath()context.arc(x2,y2,cleanWidth,0,2*Math.PI)context.clip()cleanCanvas()context.restore()//清除矩形区域context.save()context.beginPath()context.moveTo(x3,y3)context.lineTo(x5,y5)context.lineTo(x6,y6)context.lineTo(x4,y4)context.closePath()context.clip()cleanCanvas()context.restore()}撤回和反召回实现撤回,反召回是将每个元素的渲染数据存储在画布上,通过改变控制变量来限制渲染元素的遍历,使效果可以实现退出。在画板初始化的时候创建一个历史类,然后创建缓存和步骤数据。提现和反提现时,只需要修改步骤即可。classHistory{cacheQueue:T[]step:numberconstructor(cacheQueue:T[]){this.cacheQueue=cacheQueuethis.step=cacheQueue.length-1}//添加数据add(data:T){//如果在回滚期间添加数据,则删除临时数据if(this.step!==this.cacheQueue.length-1){this.cacheQueue.length=this.step+1}this.cacheQueue.push(data)this.step=this.cacheQueue.length-1}//遍历缓存队列each(cb?:(ele:T,i:number)=>void){for(leti=0;i<=this.step;i++){cb?.(this.cacheQueue[i],i)}}//返回undo(){if(this.step>=0){this.step--returnthis.cacheQueue[this.step]}}//forwardredo(){if(this.step{this.context.save()//render....this.context,resore()})//缓存数据this.cache()}}拖动画布拖动画布的实现是计算鼠标移动的距离,根据距离改变画布的原点位置,实现拖动的效果。functiondrag(position:MousePosition){constmousePosition={x:position.x-this.canvasRect.left,y:position.y-this.canvasRect.top}if(this.originPosition.x&&this.originPosition.y){consttranslteX=mousePosition.x-this.originPosition.xconsttranslteY=mousePosition.y-this.originPosition.ythis.context.translate(translteX,translteY)this.originTranslate={x:translteX+this.originTranslate.x,y:translteY+this.originTranslate.y}this.render()}this.originPosition=mousePosition}Multi-layer实现多层需要处理以下几个地方。在画板初始化时创建图层类。所有层数据和层逻辑都在这里。然后给画布上的元素添加图层属性,使用来判断画板属于哪一层,渲染函数改为按照图层顺序渲染。拖动或隐藏图层需要重新渲染。删除层并删除对应的缓存层元素interfaceILayer{id:number//layeridtitle:string//图层名称show:boolean//图层显示状态}/***layer*/classLayer{stack:ILayer[]//图层数据current:number//当前图层render:()=>void//画板渲染事件constructor(render:()=>void,initData?:Layer){const{stack=[{id:1,title:'item1',show:true}],id=1,current=1}=initData||复制代码{}this.stack=stackthis.id=idthis.current=currentthis.render=render}...}classPaintBoard{//按层排序sortOnLayer(){this.history.sort((a,b)=>{return(this.layer.stack.findIndex(({id})=>id===b?.layer)-this.layer.stack.findIndex(({id})=>id===a?.layer))})}//渲染函数只渲染图层显示状态的元素render(){constshowLayerIds=newSet(this.layer.stack.reduce((acc,cur)=>{returncur.show?[...acc,cur.id]:acc},[]))this.history.each((ele)=>{如果(ele?.layer&&showLayerIds.has(ele.layer)){...}}}}综上所述,本文主要是分享一些主要逻辑,还有一些兼容性问题和一些UI交互,所以我不会't描述这个画板写作大概花了一个星期的时间才写下来,还有很多功能没有写出来。过段时间有空再继续写,进一步优化。现在还有一些优化问题没有写出来。比如笔刷宽度的显示还有问题。origin位置和一些初始化设计不是很好,但是写完这个sketchbook还是蛮有成就感的。参考资料HTML5实现橡皮擦的擦除效果我做了一个在线白板!