使用canvas进行开发项目,我们离不开各种线段,曲线,图形,但每次都必须用代码一步一步去实现,显得非常麻烦。有没有一种类似于PS,CAD之类的可视化工具,绘制出基本的图形,然后输出代码。之后我们就可以在这个生成的图形场景的基础上去实现功能,那将是多么的美妙的事啊。话不多说,我们来实现一个图形编辑器吧😂。
主要实现如下的功能:
直线(实线、虚线)
贝塞尔曲线(2次,3次)
多边形(三角形、矩形、任意边形)
多角星(3角星、4角星、5角星…)
圆形、椭圆
实际效果: drawboard
功能点包括:
所有的图形都可以拖拽位置,直线和曲线需要拖拽中点(黄色圆点),其他图形只需要把鼠标放于图形内部拖拽即可;
所有的图形只要把鼠标放于中心点或图形内部,然后按delete键即可删除;
线段可以实现拉伸减少长度,旋转角度;
贝塞尔曲线可以通过拖拽控制点实现任意形状的变化;
多边形可以拖拽控制点控制多边形的旋转角度和大小变化,所有顶点都可以拖拽;
多角星除了多边形的功能外,拖拽第二控制点可以实现图形的饱满程度;
是否填充图形,是否显示控制线,是否显示背景格;
生成代码。
使用方式:
选中工具栏中的图形选项,是否填充,颜色等,然后在画板拖动鼠标,同时选中的工具栏中的选项复位,此时为绘图模式;
完成绘制图形后,可以对图形进行拖拽位置,变换顶点,旋转等,此时为修改模式;
然后再选中工具栏选项,再次绘制,如此类推;
可以消除控制线和背景格,查看效果,然后可以点击生成代码,复制代码即可。
该项目用到的知识点包括:
ES6面向对象
html5标签,布局
基本的三角函数
canvas部分有:坐标变换,渐变,混合模式,线条和图形的绘制。
工具栏
首先我们实现如图所示的工具栏,也就是基本的html/css,使用了flex布局,同时使用了html5的color, range, number标签,其它都是普通的html和css代码。主要注意的地方就是如下用纯css实现选择效果
.wrap [type=radio] { position : absolute; top : 0 ; left : 0 ; width : 100% ; height : 100% ; z-index : 99 ; opacity : 0 ; cursor : pointer; } .wrap [type=radio] :checked ~.label { background : hsl (200 , 100% , 40% ); color : hsl (0 , 0% , 100% ) }
其中多边形边数选择范围控制为:3-20,当然我们也可以扩大为无限大的边数,但实际应用到的情况比较少。多角星情况类型,范围控制为3~20。
然后对线条粗细,描边颜色,填充颜色显示信息,也就是onchang事件触发时获取value值,再显示出来。显示鼠标当前的位置功能也非常简单,在此也略过不表。
图形基类 开始实现画板的功能,第一步,实现图形基类,这个是最重要的部分。因为不管是线条,多边形都会继承该类。 注意:isPointInPath非常有用,就是这个api实现鼠标是否选中的功能了,它的原理就是调用上下文context绘制路径,然后向isPointInPath传递位置(x,y)信息,该api会返回这个点是否在绘制路径上,相当于绘制的是隐形的路径进行判断点是否在该路径或图形内部,这也是我要把绘制路径和渲染的功能分离开的原因。
具体的功能还是直接看代码吧
class Graph { constructor (pos){ this .x=pos.x; this .y=pos.y; this .points=[]; this .sides=5 ; this .stars=5 ; this .lineWidth=1 ; this .strokeStyle='#f00' ; this .fillStyle='#f00' ; this .isFill=false ; } initUpdate(start,end){ this .points[1 ]=end; this .x=(start.x+end.x)/2 ; this .y=(start.y+end.y)/2 ; } update(i,pos){ if (i==9999 ){ var that=this , x1=pos.x-this .x, y1=pos.y-this .y; this .points.forEach((p,i )=> { that.points[i]={x :p.x+x1, y :p.y+y1 }; }); this .x=Math .round(pos.x); this .y=Math .round(pos.y); } else { this .points[i]=pos; var x=0 ,y=0 ; this .points.forEach(p => { x+=p.x; y+=p.y; }); this .x=Math .round(x/this .points.length); this .y=Math .round(y/this .points.length); } } createPath(ctx){ ctx.beginPath(); this .points.forEach((p,i )=> { ctx[i==0 ?'moveTo' :'lineTo' ](p.x,p.y); }); ctx.closePath(); } isInPath(ctx,pos){ for (var i=0 ,point,len=this .points.length;i<len;i++){ point=this .points[i]; ctx.beginPath(); ctx.arc(point.x,point.y,5 ,0 ,Math .PI*2 ,false ); if (ctx.isPointInPath(pos.x,pos.y)){ return i; } } this .createPath(ctx); if (ctx.isPointInPath(pos.x,pos.y)){ return 9999 ; } return -1 } drawController(ctx){ this .drawPoints(ctx); this .drawCenter(ctx); } drawPoints(){ ctx.save(); ctx.lineWidth=2 ; ctx.strokeStyle='#999' ; this .points.forEach(p => { ctx.beginPath(); ctx.arc(p.x,p.y,5 ,0 ,Math .PI*2 ,false ); ctx.stroke(); }); ctx.restore(); } drawCenter(ctx){ ctx.save(); ctx.lineWidth=1 ; ctx.strokeStyle='hsla(60,100%,45%,1)' ; ctx.fillStyle='hsla(60,100%,50%,1)' ; ctx.beginPath(); ctx.arc(this .x,this .y,5 ,0 ,Math .PI*2 ,false ); ctx.stroke(); ctx.fill(); ctx.restore(); } draw(ctx){ ctx.save(); ctx.lineWidth=this .lineWidth; ctx.strokeStyle=this .strokeStyle; ctx.fillStyle=this .fillStyle; this .createPath(ctx); ctx.stroke(); if (this .isFill){ ctx.fill(); } ctx.restore(); } createCode(){ var codes=['// ' +this .name]; codes.push('ctx.save();' ); codes.push('ctx.lineWidth=' +this .lineWidth); codes.push('ctx.strokeStyle=\'' +this .strokeStyle+'\';' ); if (this .isFill){ codes.push('ctx.fillStyle=\'' +this .fillStyle+'\';' ); } codes.push('ctx.beginPath();' ); codes.push('ctx.translate(' +this .x+',' +this .y+');' ) this .points.forEach((p,i )=> { if (i==0 ){ codes.push('ctx.moveTo(' +(p.x-this .x)+',' +(p.y-this .y)+');' ); } else { codes.push('ctx.lineTo(' +(p.x-this .x)+',' +(p.y-this .y)+');' ); } }); codes.push('ctx.closePath();' ); codes.push('ctx.stroke();' ); if (this .isFill){ codes.push('ctx.fill();' ); } codes.push('ctx.restore();' ); return codes.join('\n' ); } }
直线
实现直线功能相当简单,继承基类,只需要重写draw和createCode方法,拖拽和变换等功能都已经在基类实现了。
class Line extends Graph { constructor (pos){ super (pos); this .points=[pos,pos]; this .name='直线' } createPath(ctx){ ctx.beginPath(); ctx.arc(this .x,this .y,5 ,0 ,Math .PI*2 ,false ); } draw(ctx){ ctx.save(); ctx.lineWidth=this .lineWidth; ctx.strokeStyle=this .strokeStyle; ctx.beginPath(); this .points.forEach((p,i )=> { if (i==0 ){ ctx.moveTo(p.x,p.y); } else { ctx.lineTo(p.x,p.y); } }); ctx.closePath(); ctx.stroke(); ctx.restore(); } createCode(){ var codes=['// ' +this .name]; codes.push('ctx.lineWidth=' +this .lineWidth); codes.push('ctx.strokeStyle=\'' +this .strokeStyle+'\';' ); codes.push('ctx.beginPath();' ); this .points.forEach((p,i )=> { if (i==0 ){ codes.push('ctx.moveTo(' +p.x+',' +p.y+');' ); } else { codes.push('ctx.lineTo(' +p.x+',' +p.y+');' ); } }); codes.push('ctx.closePath();' ); codes.push('ctx.stroke();' ); return codes.join('\n' ); } }
还有就是虚线功能了,其实就是先绘制一段直线,然后空出一段空间,接着再绘制一段直线,如此类推。小伙伴可以思考一下怎么实现,这个和直线所涉及的知识点相同,代码就略过了。
贝塞尔曲线
接着就是贝塞尔曲线的绘制了,首先继承直线类,曲线比直线不同的是除了起始点和结束点,它还多出了控制点,2次贝塞尔曲线有一个控制点,3次贝塞尔曲线则有两个控制点。所以对应初始化拖拽,顶点绘制的方法必须重写,以下是3次贝塞尔曲线的代码。
class Bezier extends Line { constructor (pos){ super (pos); this .points=[pos,pos,pos,pos]; this .name='三次贝塞尔曲线' } initUpdate(start,end){ var a=Math .round(Math .sqrt(Math .pow(end.x-start.x,2 )+Math .pow(end.y-start.y,2 )))/2 , x1=start.x+(end.x-start.x)/2 , y1=start.y-a, y2=end.y+a; this .points[1 ]={x :end.x,y :end.y}; this .points[2 ]={x :x1,y :y1<0 ?0 :y1}; this .points[3 ]={x :start.x,y :end.y}; this .points[3 ]={x :x1,y :y2>H?H:y2}; this .x=(start.x+end.x)/2 ; this .y=(start.y+end.y)/2 ; } drawPoints(ctx){ ctx.lineWidth=0.5 ; ctx.strokeStyle='#00f' ; ctx.beginPath(); ctx.moveTo(this .points[0 ].x, this .points[0 ].y); ctx.lineTo(this .points[2 ].x, this .points[2 ].y); ctx.moveTo(this .points[1 ].x, this .points[1 ].y); ctx.lineTo(this .points[3 ].x, this .points[3 ].y); ctx.stroke(); this .points.forEach(function (point,i ) { ctx.beginPath(); ctx.arc(point.x,point.y,5 ,0 ,Math .PI*2 ,false ); ctx.stroke(); }); } draw(){ ctx.save(); ctx.lineWidth=this .lineWidth; ctx.strokeStyle=this .strokeStyle; ctx.beginPath(); ctx.moveTo(this .points[0 ].x, this .points[0 ].y); ctx.bezierCurveTo(this .points[2 ].x,this .points[2 ].y,this .points[3 ].x,this .points[3 ].y,this .points[1 ].x,this .points[1 ].y); ctx.stroke(); ctx.restore(); } createCode(){ var codes=['// ' +this .name]; codes.push('ctx.lineWidth=' +this .lineWidth); codes.push('ctx.strokeStyle=\'' +this .strokeStyle+'\';' ); codes.push('ctx.beginPath();' ); codes.push(`ctx.moveTo(${this .points[0 ].x} ,${this .points[0 ].y} );` ); codes.push(`ctx.bezierCurveTo(${this .points[2 ].x} ,${this .points[2 ].y} ,${this .points[3 ].x} ,${this .points[3 ].y} ,${this .points[1 ].x} ,${this .points[1 ].y} );` ); codes.push('ctx.stroke();' ); return codes.join('\n' ); } }
至于贝塞尔2次曲线功能类似,同时也更加简单,代码也略过。
多边形
实现任意条边的多边形,大家思考一下都会知道如何实现,平均角度=360度/边数,不是吗?
在知道中点和第一个顶点的情况下,第n个顶点与中点的角度 = n*平均角度;然后记录下每个顶点的位置,然后依次绘制每个顶点的连线即可。这里用到了二维旋转的公式,也就是绕图形的中点,旋转一定的角度。
既然我们已经记录了每个顶点的位置,当拖动对应的顶点后修改该顶点位置,重新绘制,就可以伸缩成任意的图案。
难点是拖拽控制线,实现旋转多边形角度,和扩大缩小多边形。等比例扩大缩小每个顶点与中点的距离即可实现等比例缩放多边形,记录第一个顶点与中点的角度变化即可实现旋转功能,这里用到反正切Math.atan2(y,x)求角度;具体实现看如下代码。
class Polygon extends Graph { constructor (pos){ super (pos); this .cPoints=[]; } get name(){ return this .sides+'边形' ; } createPoints(start,end){ var x1 = end.x - start.x, y1 = end.y - start.y, angle=0 ; this .points=[]; for (var i=0 ;i<this .sides;i++){ angle=2 *Math .PI/this .sides*i; var sin=Math .sin(angle), cos=Math .cos(angle), newX = x1*cos - y1*sin, newY = y1*cos + x1*sin; this .points.push({ x:Math .round(start.x + newX), y:Math .round(start.y + newY) }); } } createControlPoint(start,end,len){ var x1 = end.x - start.x, y1 = end.y - start.y, angle=Math .atan2(y1,x1), c=Math .round(Math .sqrt(x1*x1+y1*y1)), l=c+(!len?0 :c/len), x2 =l * Math .cos(angle) + start.x, y2 =l * Math .sin(angle) + start.y; return {x :x2,y :y2}; } initUpdate(start,end){ this .createPoints(start,end); this .cPoints[0 ]=this .createControlPoint(start,end,3 ); } update(i,pos){ if (i==10000 ){ var point=this .createControlPoint({x :this .x,y :this .y},pos,-4 ); this .cPoints[0 ]=pos; this .createPoints({x :this .x,y :this .y},point); } else if (i==9999 ){ var that=this , x1=pos.x-this .x, y1=pos.y-this .y; this .points.forEach((p,i )=> { that.points[i]={x :p.x+x1, y :p.y+y1 }; }); this .cPoints.forEach((p,i )=> { that.cPoints[i]={x :p.x+x1,y :p.y+y1}; }); this .x=Math .round(pos.x); this .y=Math .round(pos.y); } else { this .points[i]=pos; var x=0 ,y=0 ; this .points.forEach(p => { x+=p.x; y+=p.y; }); this .x=Math .round(x/this .points.length); this .y=Math .round(y/this .points.length); } } createCPath(ctx){ this .cPoints.forEach(p => { ctx.beginPath(); ctx.arc(p.x,p.y,6 ,0 ,Math .PI*2 ,false ); }); } isInPath(ctx,pos){ var index=super .isInPath(ctx,pos); if (index>-1 ) return index; this .createCPath(ctx); for (var i=0 ,len=this .cPoints.length;i<len;i++){ var p=this .cPoints[i]; ctx.beginPath(); ctx.arc(p.x,p.y,6 ,0 ,Math .PI*2 ,false ); if (ctx.isPointInPath(pos.x,pos.y)){ return 10000 +i;break ; } } return -1 } drawCPoints(ctx){ ctx.save(); ctx.lineWidth=1 ; ctx.strokeStyle='hsla(0,0%,50%,1)' ; ctx.fillStyle='hsla(0,100%,60%,1)' ; this .cPoints.forEach(p => { ctx.beginPath(); ctx.moveTo(this .x,this .y); ctx.lineTo(p.x,p.y); ctx.stroke(); ctx.beginPath(); ctx.arc(p.x,p.y,6 ,0 ,Math .PI*2 ,false ); ctx.stroke(); ctx.fill(); }); ctx.restore(); } drawController(ctx){ this .drawPoints(ctx); this .drawCPoints(ctx); this .drawCenter(ctx); } }
多角星
仔细思考一下,多角星其实就是2*n边形,不过它是凹多边形而已,于是我们在之前凸多边形基础上去实现。相比于多边形,我们还要在此基础上增加第二控制点,实现凹点与凸点的比值变化,通俗点就是多角星的胖瘦度。
class Star extends Polygon { constructor (pos){ super (pos); this .cPoints=[]; this .size=0.5 ; } get name() { return this .stars+'角星' } createPoints(start,end){ var x1 = end.x - start.x, y1 = end.y - start.y, x2 =x1*this .size, y2 =y1*this .size, angle=0 , angle2=0 ; this .points=[]; for (var i=0 ;i<this .stars;i++){ angle=2 *Math .PI/this .stars*i; angle2=angle+Math .PI/this .stars; var sin=Math .sin(angle), cos=Math .cos(angle), newX = x1*cos - y1*sin, newY = y1*cos + x1*sin, sin2=Math .sin(angle2), cos2=Math .cos(angle2), newX2 = x2*cos2 - y2*sin2, newY2 = y2*cos2 + x2*sin2; this .points.push({ x:Math .round(start.x + newX), y:Math .round(start.y + newY) }); this .points.push({ x:Math .round(start.x + newX2), y:Math .round(start.y + newY2) }); } } initUpdate(start,end){ this .createPoints(start,end); this .cPoints[0 ]=this .createControlPoint(start,end,3 ); this .cPoints[1 ]=this .createControlPoint(start,this .points[1 ],3 ); } update(i,pos){ if (i==10000 ){ var ang=Math .PI/this .stars, angle=Math .atan2(pos.y-this .y,pos.x-this .x), sin=Math .sin(ang+angle), cos=Math .cos(ang+angle), a=Math .sqrt(Math .pow(pos.x-this .x,2 )+Math .pow(pos.y-this .y,2 )); this .cPoints[1 ]={ x:(a*this .size+10 )*cos+this .x, y:(a*this .size+10 )*sin+this .y }; var point=this .createControlPoint({x :this .x,y :this .y},pos,-4 ); this .cPoints[0 ]=pos; this .createPoints({x :this .x,y :this .y},point); } else if (i==10001 ){ var x1 = this .points[1 ].x - this .x, y1 = this .points[1 ].y - this .y, angle=Math .atan2(y1,x1), a=Math .sqrt(Math .pow(pos.x-this .x,2 )+Math .pow(pos.y-this .y,2 )), b=Math .sqrt(Math .pow(this .points[0 ].x-this .x,2 )+Math .pow(this .points[0 ].y-this .y,2 )); var x=a*Math .cos(angle), y=a*Math .sin(angle); this .size=(a-20 )/b; this .cPoints[1 ]={x :this .x+x, y :this .y+y }; this .createPoints({x :this .x,y :this .y},this .points[0 ]); } else { super .update(i,pos); } } }
三角形,矩形
这两个图形就是特别的多边形而已,功能非常简单,而且只需要继承图形基类Graph
class Triangle extends Graph { constructor (pos){ super (pos); this .points=[pos,pos,pos]; this .name='三角形' ; } initUpdate(start,end){ var x1=Math .round(start.x), y1=Math .round(start.y), x2=Math .round(end.x), y2=Math .round(end.y); this .points[0 ]={x :x1,y :y1}; this .points[1 ]={x :x1,y :y2}; this .points[2 ]={x :x2,y :y2}; this .x=Math .round((x1*2 +x2)/3 ); this .y=Math .round((y2*2 +y1)/3 ); } } class Rect extends Graph { constructor (pos){ super (pos); this .points=[pos,pos,pos,pos]; this .name='矩形' ; } initUpdate(start,end){ var x1=Math .round(start.x), y1=Math .round(start.y), x2=Math .round(end.x), y2=Math .round(end.y); this .points[0 ]={x :x1,y :y1}; this .points[1 ]={x :x2,y :y1}; this .points[2 ]={x :x2,y :y2}; this .points[3 ]={x :x1,y :y2}; this .x=Math .round((x1+x2)/2 ); this .y=Math .round((y1+y2)/2 ); } }
圆形,椭圆
绘制圆形比较简单,只需要知道中点和半径,即可绘制,代码在此省略。
椭圆的绘制才是比较麻烦的,canvas并没有提供相关的api,我这里参考了网上的例子,是使用4条三次贝塞尔曲线首尾相接来实现的,椭圆有两个控制点,分别可以拖拽实现椭圆的压扁程度。这里只展示部分的代码,其他和多边形类似:
initUpdate(start,end){ this .points[0 ]=end; this .a=Math .round(Math .sqrt(Math .pow(this .points[0 ].x-start.x,2 )+Math .pow(this .points[0 ].y-start.y,2 ))); this .b=this .a/2 ; this .angle = Math .atan2(this .points[0 ].y-this .y,this .points[0 ].x-this .x); this .rotateA(); } update(i,pos){ if (i==9999 ){ var that=this , x1=pos.x-this .x, y1=pos.y-this .y; this .points.forEach((p,i )=> { that.points[i]={x :p.x+x1, y :p.y+y1 }; }); this .x=pos.x; this .y=pos.y; } else { this .points[i]=pos; if (i==0 ){ this .a=Math .round(Math .sqrt(Math .pow(this .points[0 ].x-this .x,2 )+Math .pow(this .points[0 ].y-this .y,2 ))); this .angle = Math .atan2(this .points[0 ].y-this .y,this .points[0 ].x-this .x); this .rotateA(); } else if (i==1 ){ this .b=Math .round(Math .sqrt(Math .pow(this .points[1 ].x-this .x,2 )+Math .pow(this .points[1 ].y-this .y,2 ))); this .angle = Math .PI/2 +Math .atan2(this .points[1 ].y-this .y,this .points[1 ].x-this .x); this .rotateB(); } } } createPath(ctx){ var k = .5522848 , x=0 , y=0 , a=this .a, b=this .b, ox = a * k, oy = b * k; ctx.beginPath(); ctx.moveTo(x - a, y); ctx.bezierCurveTo(x - a, y - oy, x - ox, y - b, x, y - b); ctx.bezierCurveTo(x + ox, y - b, x + a, y - oy, x + a, y); ctx.bezierCurveTo(x + a, y + oy, x + ox, y + b, x, y + b); ctx.bezierCurveTo(x - ox, y + b, x - a, y + oy, x - a, y); ctx.closePath(); }
事件部分 绘图的主体部分已经完成,接下来就是定义相关的事件了,首先mousedown的时候记录下第一个坐标mouseStart,这个点是绘制直线和曲线的起始点,同时也是多边形和多角星的中点;
然后再定义mousemove事件,记录下第二个坐标mouseEnd,这个是绘制直线和曲线的结束点,同时也是多边形和多角星的第一个顶点;
当然这中间还要区分绘制模式和修改模式,绘制模式下,根据类型从对象工厂获取对应的对象,然后设置对象的属性,完成初始化之后就把图形对象放入图形列表shapes中。列表中的图形对象就可以作为后续修改模式进行应用动画。
如果是修改模式的话,首先是遍历shapes中所有的图形对象,并依次调用isInPath方法,看看当前的鼠标位置是否在该图形上,并判断是在中点或图形内部,还是某个顶点上。而具体的判断逻辑已经控制反转在图形对象内部,外部并不需要知道其实现原理。如果鼠标落在了某个图形对象上,则在鼠标移动时实时更新该图形对应的位置,顶点,控制点,并同步动画渲染该图形。
删除功能的实现,就是按下delete键时,遍历shapes中所有的图形对象,并依次调用isInPath方法,鼠标如果在该对象上面,直接在shapes数组上splice(i,1),然后重写渲染就ok。
生成代码功能一样,遍历shapes,依次调用createCode方法获取该图形生成的代码字符串,然后将所有值合并赋予textarea的value。
这里要理解的是,只要启动了对应的模式,改变了图形的某部分,背景和对应所有的图形都要重新绘制一遍,当然这也是canvas这种比较底层的绘图api实现动画的方式了。
function factory (type,pos ) { switch (type){ case 'line' : return new Line(pos); case 'dash' : return new Dash(pos); case 'quadratic' : return new Quadratic(pos); case 'bezier' : return new Bezier(pos); case 'triangle' : return new Triangle(pos); case 'rect' : return new Rect(pos); case 'round' : return new Round(pos); case 'polygon' : return new Polygon(pos); case 'star' : return new Star(pos); case 'ellipse' : return new Ellipse(pos); default :return new Line(pos); } } canvas.addEventListener('mousedown' ,function (e ) { mouseStart=WindowToCanvas(canvas,e.clientX,e.clientY); env=getEnv(); activeShape=null ; if (drawing){ activeShape = factory(env.type,mouseStart); activeShape.lineWidth = env.lineWidth; activeShape.strokeStyle = env.strokeStyle; activeShape.fillStyle = env.fillStyle; activeShape.isFill = env.isFill; activeShape.sides = env.sides; activeShape.stars = env.stars; shapes.push(activeShape); index=-1 ; drawGraph(); } else { for (var i=0 ,len=shapes.length;i<len;i++){ if ((index=shapes[i].isInPath(ctx,mouseStart))>-1 ){ canvas.style.cursor='crosshair' ; activeShape=shapes[i];break ; } } } canvas.addEventListener('mousemove' ,mouseMove,false ); canvas.addEventListener('mouseup' ,mouseUp,false ); },false ); function mouseMove (e ) { mouseEnd=WindowToCanvas(canvas,e.clientX,e.clientY); if (activeShape){ if (index>-1 ){ activeShape.update(index,mouseEnd); } else { activeShape.initUpdate(mouseStart,mouseEnd); } drawBG(); if (env.guid){drawGuidewires(mouseEnd.x,mouseEnd.y); } drawGraph(); } } function mouseUp (e ) { canvas.style.cursor='pointer' ; if (activeShape){ drawBG(); drawGraph(); resetDrawType(); } canvas.removeEventListener('mousemove' ,mouseMove,false ); canvas.removeEventListener('mouseup' ,mouseUp,false ); } document .body.onkeydown=function (e ) { if (e.keyCode==8 ){ for (var i=0 ,len=shapes.length;i<len;i++){ if (shapes[i].isInPath(ctx,currPos)>-1 ){ shapes.splice(i--,1 ); drawBG(); drawGraph(); break ; } } } }; function drawBG ( ) { ctx.clearRect(0 ,0 ,W,H); if (getEnv().grid){DrawGrid(ctx,'lightGray' ,10 ,10 ); } } function drawGuidewires (x,y ) { ctx.save(); ctx.strokeStyle='rgba(0,0,230,0.4)' ; ctx.lineWidth=0.5 ; ctx.beginPath(); ctx.moveTo(x+0.5 ,0 ); ctx.lineTo(x+0.5 ,ctx.canvas.height); ctx.stroke(); ctx.beginPath(); ctx.moveTo(0 ,y+0.5 ); ctx.lineTo(ctx.canvas.width,y+0.5 ); ctx.stroke(); ctx.restore(); } function drawGraph ( ) { var showControl=getEnv().control; shapes.forEach(shape => { shape.draw(ctx); if (showControl){ shape.drawController(ctx); } }); }
最后 功能全部完成,当然里面有很多的细节,可以查看源代码,这里有待进一步完善的是修改功能,比如调整边框宽度,改变边框颜色和填充颜色。 还有就是本人是在mac平台的chrome下玩canvas,因此不保证其他对es6,canvas的支持度差的浏览器会出现的问题。