canvas圖形編輯器

原文地址:canvas圖形編輯器
使用canvas進行開發項目,我們離不開各種線段,曲線,圖形,但每次都必須用代碼一步一步去實現,顯得非常麻煩。有沒有一種類似于PS,CAD之類的可視化工具,繪制出基本的圖形,然后輸出代碼。之后我們就可以在這個生成的圖形場景的基礎上去實現功能,那將是多么的美妙的事啊。話不多說,我們來實現一個圖形編輯器吧??。

主要實現如下的功能:

  1. 直線(實線、虛線)
  2. 貝塞爾曲線(2次,3次)
  3. 多邊形(三角形、矩形、任意邊形)
  4. 多角星(3角星、4角星、5角星...)
  5. 圓形、橢圓

實際效果: drawboard

drawboard

功能點包括:

  1. 所有的圖形都可以拖拽位置,直線和曲線需要拖拽中點(黃色圓點),其他圖形只需要把鼠標放于圖形內部拖拽即可;
  2. 所有的圖形只要把鼠標放于中心點或圖形內部,然后按delete鍵即可刪除;
  3. 線段可以實現拉伸減少長度,旋轉角度;
  4. 貝塞爾曲線可以通過拖拽控制點實現任意形狀的變化;
  5. 多邊形可以拖拽控制點控制多邊形的旋轉角度和大小變化,所有頂點都可以拖拽;
  6. 多角星除了多邊形的功能外,拖拽第二控制點可以實現圖形的飽滿程度;
  7. 是否填充圖形,是否顯示控制線,是否顯示背景格;
  8. 生成代碼。

使用方式:

  1. 選中工具欄中的圖形選項,是否填充,顏色等,然后在畫板拖動鼠標,同時選中的工具欄中的選項復位,此時為繪圖模式;
  2. 完成繪制圖形后,可以對圖形進行拖拽位置,變換頂點,旋轉等,此時為修改模式;
  3. 然后再選中工具欄選項,再次繪制,如此類推;
  4. 可以消除控制線和背景格,查看效果,然后可以點擊生成代碼,復制代碼即可。

該項目用到的知識點包括:

  1. ES6面向對象
  2. html5標簽,布局
  3. 基本的三角函數
  4. canvas部分有:坐標變換,漸變,混合模式,線條和圖形的繪制。

工具欄

drawboard tools

首先我們實現如圖所示的工具欄,也就是基本的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');
    }
  }

直線

it

實現直線功能相當簡單,繼承基類,只需要重寫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');
    }
  }

還有就是虛線功能了,其實就是先繪制一段直線,然后空出一段空間,接著再繪制一段直線,如此類推。小伙伴可以思考一下怎么實現,這個和直線所涉及的知識點相同,代碼就略過了。

貝塞爾曲線

it

接著就是貝塞爾曲線的繪制了,首先繼承直線類,曲線比直線不同的是除了起始點和結束點,它還多出了控制點,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次曲線功能類似,同時也更加簡單,代碼也略過。

多邊形

it

實現任意條邊的多邊形,大家思考一下都會知道如何實現,平均角度=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);
    }
  }

多角星

it

仔細思考一下,多角星其實就是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);
      }
    }

  }

三角形,矩形

it

這兩個圖形就是特別的多邊形而已,功能非常簡單,而且只需要繼承圖形基類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);
    }
  }

圓形,橢圓

it

繪制圓形比較簡單,只需要知道中點和半徑,即可繪制,代碼在此省略。
橢圓的繪制才是比較麻煩的,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的支持度差的瀏覽器會出現的問題。

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 228,156評論 6 531
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,401評論 3 415
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 176,069評論 0 373
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,873評論 1 309
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,635評論 6 408
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,128評論 1 323
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,203評論 3 441
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,365評論 0 288
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 48,881評論 1 334
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,733評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,935評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,475評論 5 358
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,172評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,582評論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,821評論 1 282
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,595評論 3 390
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,908評論 2 372

推薦閱讀更多精彩內容