原文地址:https://edwardzhong.github.io/2017/11/02/drawboard/
使用canvas進行開發專案,我們離不開各種線段,曲線,圖形,但每次都必須用程式碼一步一步去實現,顯得非常麻煩。有沒有一種類似於PS,CAD之類的視覺化工具,繪製出基本的圖形,然後輸出程式碼。之後我們就可以在這個生成的圖形場景的基礎上去實現功能,那將是多麼的美妙的事啊。話不多說,我們來實現一個圖形編輯器吧?。
主要實現如下的功能:
- 直線(實線、虛線)
- 貝塞爾曲線(2次,3次)
- 多邊形(三角形、矩形、任意邊形)
- 多角星(3角星、4角星、5角星...)
- 圓形、橢圓
實際效果: drawboard(推薦在chrome或safari下執行)
功能點包括:
- 所有的圖形都可以拖拽位置,直線和曲線需要拖拽中點(黃色圓點),其他圖形只需要把滑鼠放於圖形內部拖拽即可;
- 所有的圖形只要把滑鼠放於中心點或圖形內部,然後按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{/* 覆蓋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的支援度差的瀏覽器會出現的問題。