canvas图形编辑器

  原文地址:http://jeffzhong.space/2017/11/02/drawboard/
  使用canvas进行开发项目,我们离不开各种线段,曲线,图形,但每次都必须用代码一步一步去实现,显得非常麻烦。有没有一种类似于PS,CAD之类的可视化工具,绘制出基本的图形,然后输出代码。之后我们就可以在这个生成的图形场景的基础上去实现功能,那将是多么的美妙的事啊。话不多说,我们来实现一个图形编辑器吧?。

主要实现如下的功能:

  1. 直线(实线、虚线)
  2. 贝塞尔曲线(2次,3次)
  3. 多边形(三角形、矩形、任意边形)
  4. 多角星(3角星、4角星、5角星...)
  5. 圆形、椭圆

实际效果: drawboard(推荐在chrome或safari下运行)

功能点包括:

  1. 所有的图形都可以拖拽位置,直线和曲线需要拖拽中点(黄色圆点),其他图形只需要把鼠标放于图形内部拖拽即可;
  2. 所有的图形只要把鼠标放于中心点或图形内部,然后按delete键即可删除;
  3. 线段可以实现拉伸减少长度,旋转角度;
  4. 贝塞尔曲线可以通过拖拽控制点实现任意形状的变化;
  5. 多边形可以拖拽控制点控制多边形的旋转角度和大小变化,所有顶点都可以拖拽;
  6. 多角星除了多边形的功能外,拖拽第二控制点可以实现图形的饱满程度;
  7. 是否填充图形,是否显示控制线,是否显示背景格;
  8. 生成代码。

使用方式:

  1. 选中工具栏中的图形选项,是否填充,颜色等,然后在画板拖动鼠标,同时选中的工具栏中的选项复位,此时为绘图模式;
  2. 完成绘制图形后,可以对图形进行拖拽位置,变换顶点,旋转等,此时为修改模式;
  3. 然后再选中工具栏选项,再次绘制,如此类推;
  4. 可以消除控制线和背景格,查看效果,然后可以点击生成代码,复制代码即可。

该项目用到的知识点包括:

  1. ES6面向对象
  2. html5标签,布局
  3. 基本的三角函数
  4. 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{/* 覆盖radio */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 ');')//translate到中心点,方便使用this.points.forEach((p,i)=>{if(i==0){codes.push('ctx.moveTo(' (p.x-this.x) ',' (p.y-this.y) ');');// codes.push('ctx.moveTo(' (p.x) ',' (p.y) ');');} else {codes.push('ctx.lineTo(' (p.x-this.x) ',' (p.y-this.y) ');');// codes.push('ctx.lineTo(' (p.x) ',' (p.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{//增加凹顶点与凸顶点的比例属性sizeconstructor(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;}}}// saveImageData();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的支持度差的浏览器会出现的问题。


更多专业前端知识,请上 【猿2048】www.mk2048.com

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

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

相关文章

Sacrilege –自定义SWT滚动条

SWT是本机OS小部件之上的薄抽象层。 如果您打算将应用程序与OS外观很好地集成在一起&#xff0c;那将是一件非常好的事情。 但是&#xff0c;作为一种折衷方案&#xff0c;这种方法大大限制了样式功能。 特别是&#xff0c;我感觉到本机SW​​T滚动条通常会干扰更精细的视图布…

CSS3盒模型温故

CSS有一种基础设计模式叫盒模型&#xff0c;定义了Web页面中的元素是如何看做盒子来解析的。每一个盒子有不同的展示界面&#xff0c;下面就来介绍盒模型&#xff0c;主要有一下几种盒模型&#xff1a;inline、inline-block、block、table、absolute position、float。浏览器把…

SSL与WildFly 8和Undertow

我一直在研究WildFly 8的一些安全性主题&#xff0c;偶然发现了一些配置文档&#xff0c;这些文档没有很好地记录。 其中之一是新Web子系统Undertow的TLS / SSL配置。 有许多关于较旧的Web子系统的文档&#xff0c;并且确实仍然可以使用&#xff0c;但是这里是使用新方法进行配…

CSS属性选择器温故-4

1.属性选择器就是通过元素属性来找到元素 2.属性选择器语法 CSS3遵循了惯用的编码规则&#xff0c;通配符的使用提高了样式表的书写效率&#xff0c;也使CSS3的属性选择器更符合编码习惯 3.浏览器兼容性 CSS选择器总结&#xff1a;CSS选择器和jQuery的选择器非常相似&#xff…

Python多篇新闻自动采集

昨天用python写了一个天气预报采集&#xff0c;今天趁着兴头写个新闻采集的。 目标是&#xff0c;将腾讯新闻主页上所有新闻爬取下来&#xff0c;获得每一篇新闻的名称、时间、来源以及正文。 接下来分解目标&#xff0c;一步一步地做。 步骤1&#xff1a;将主页上所有链接爬取…

使用navicat premium将数据库从Oracle迁移到SQL Server,或从Oracle迁移到MySQL

有时候我们有迁移数据库的需求&#xff0c;例如从Oracle迁移到SQL Server&#xff0c;或者从MySQL迁移到Oracle。 很多江湖好汉一时不知如何手工操作&#xff0c;所幸的是Navicat提供了迁移的自动化操作界面。 当然&#xff0c;Navicat的数据库迁移无法做到完美&#xff0c;一些…

书评:Mockito Essentials

Sujoy Acharya的Mockito Essentials副标题&#xff08; Packt Publishing &#xff0c;2014年10月&#xff09;是&#xff1a;“实用指南&#xff0c;可帮助您使用Mockito进行单元测试并开始运行。” Mockito Essentials中的前言和七章涵盖大约190个实质性页面。 前言 在序言中…

http网站转换成https网站

https&#xff0c;https的本地测试环境搭建,asp.net结合https的代码实现,http网站转换成https网站之后遇到的问题 一&#xff1a;什么是https SSL&#xff08;Security Socket Layer&#xff09;全称是加密套接字协议层&#xff0c;它位于HTTP协议层和TCP协议层之间&#x…

EAGER的获取是代码的味道

介绍 休眠获取策略确实可以使几乎没有爬网的应用程序和响应速度很快的应用程序有所不同。 在这篇文章中&#xff0c;我将解释为什么您应该选择基于查询的获取而不是全局获取计划。 取得101 Hibernate定义了四种关联检索策略 &#xff1a; 提取策略 描述 加入 原始SELECT语…

基于S2SH的电子商务网站系统性能优化

对于经常逛网页的人来说最受不了的事情就是访问的网页加载太慢&#xff0c;除去网络的原因网站的系统性能对加载的快慢非常重要&#xff0c; 网上有个统计&#xff1a; 每慢500ms Google访问量降低20% 每慢400ms Yahoo 访问量降低5-9% 每慢100ms Amazon销售额降低1% 对于商务网…

ExtJs的Reader

ExtJs的Reader Reader : 主要用于将proxy数据代理读取的数据按照不同的规则进行解析,讲解析好的数据保存到Modle中 结构图 Ext.data.reader.Reader 读取器的根类 Ext.data.reader.Json JSON格式的读取器 Ext.data.reader.Array 扩展JSON的Array读取器 Ext.data.reader.Xml XML格…

Java Lambdas和低延迟

总览 有关在Java和低延迟中使用Lambda的主要问题是&#xff1a; 它们会产生垃圾吗&#xff0c;您能做些什么吗&#xff1f; 背景 我正在开发一个支持不同有线协议的库。 这样的想法是&#xff0c;您可以描述要写入/读取的数据&#xff0c;并且有线协议确定它是否使用带有JSon或…

Java中的线程本地存储

开发人员中鲜为人知的功能之一是线程本地存储。 这个想法很简单&#xff0c;并且在需要数据的情况下很有用。 如果我们有两个线程&#xff0c;则它们引用相同的全局变量&#xff0c;但我们希望它们具有彼此独立初始化的单独值。 大多数主要的编程语言都有该概念的实现。 例如&…

多个退货单

我曾经听说过&#xff0c;过去人们为使方法具有单个出口点而奋斗。 我知道这是一种过时的方法&#xff0c;从未认为它特别值得注意。 但是最近&#xff0c;我与一些仍坚持该想法的开发人员进行了联系&#xff08;最后一次是在这里 &#xff09;&#xff0c;这让我开始思考。 因…

GO 语言编程 windows 环境搭建

参考 : http://blog.csdn.net/love_se/article/details/7754274 首先是安装Go&#xff0c;这里有很详细的安装说明&#xff0c;http://code.google.com/p/golang-china/wiki/Install 或者http://golang.org/doc/install 下面我们在window下面安装&#xff0c;google有提供win安…

机打发票打印管理

最近公司也从手写发票换成了机打发票&#xff0c;便应财务的要求做了这么一个简单的发票管理及打印系统&#xff0c;程序并不复杂。 使用C#&#xff08;2.0&#xff09; Access&#xff08;97-2003版&#xff09;/WinForm形式 系统菜单中有企业基本信息设置&#xff0c;见图4…

序列化的概念

讨论了为什么Optional不可序列化以及如何处理&#xff08;即将推出&#xff09;之后&#xff0c;让我们仔细看看序列化。 总览 这篇文章介绍了序列化的一些关键概念。 它尝试精简地执行此操作&#xff0c;而不会涉及太多细节&#xff0c;包括将建议降至最低。 它没有叙述&…

Java飞行记录器(JFR)

JFR是Java分析器&#xff0c;它使您可以研究代码的运行时特征。 通常&#xff0c;您将使用探查器来确定代码的哪些部分导致大量内存分配或导致消耗过多的CPU。 有很多产品在那里。 过去&#xff0c;我使用过YourKit&#xff0c;OptimizeIt&#xff0c;JProfiler&#xff0c;Ne…

图像识别SLIC、Haralick texture features(自备)

SLIC 简单线性迭代聚类(SLIC ),它采用k-means聚类方法来有效地生成超像素。 SLIC超像素分割详解&#xff08;一&#xff09;&#xff08;二&#xff09;&#xff08;三&#xff09;_超像素分割 样本-CSDN博客 超像素分割 & SLIC算法 & 使用示例_slic分割算法matlab-C…

浏览器中的JavaFX

浏览器中的JavaFX屏幕截图 最近&#xff0c;Carl Dea和我启动了一个新项目&#xff0c;将JavaFX 8引入浏览器。 今天&#xff0c;我想介绍我们创建的前两个概念验证&#xff0c;以查看该想法是否完全可行。 对于不耐烦的人&#xff0c;这里是到PoC的链接。 但请注意&#xff0…