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

主要实现如下的功能:

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

实际效果: drawboard

功能点包括:

  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{
//增加凹顶点与凸顶点的比例属性size
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;
}
}
}
// 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的支持度差的浏览器会出现的问题。