用Canvas製作可以根據手勢擺動的樹

用Canvas製作可以根據手勢擺動的樹

根據工作的需要,製作一個擺動的樹做爲頁面的背景。爲了增加頁面的交互性,我又爲背景中的樹增加了鼠標(觸控)事件,使他能夠根據鼠標(觸控)做出相應的動作,當手指做上下或者左右滑動的時候樹會跟着擺動。先看看最終效果。
最終效果

Step1.完成HTML頁面,新建一個Tree類

完成HTML頁面後新建一個Tree類用來記錄樹的各個屬性。其中x,y爲樹根部的座標值,branchLen,branchWidth分別是樹枝的長度與寬度,depth爲樹枝的層數,canvas用來接頁面中的canvas元素(默認是ID爲canvas的元素)。


function Tree(x,y,branchLen,branchWidth,depth,canvas){
    this.canvas = canvas || document.getElementById('canvas');
    this.ctx = this.canvas.getContext('2d');
    this.x = x||0;
    this.y = y||0;
    this.branchLen = branchLen||0;
    this.branchWidth = branchWidth||0;
    var depth = depth || 5;
}


點擊查看歷史代碼

Step2.添加drawRoot方法,用來繪製樹幹

首先在drawRoot中畫第一個枝幹。drawRoot的參數意義同上。並且在Tree類的構造函數中運行drawRoot並把Tree接受到的參數傳入。最後new一個Tree類,使樹根位於屏幕的底部正中心,樹枝長100px,樹枝寬度爲8px,樹枝層數爲8層(暫時用不上)。var atree = new Tree(canvas.width/2-4,canvas.height,100,8,8,canvas);

在drawRoot中我們需要用lineTo()畫出樹枝。樹枝的起始的座標值(x,y)已經給出,結束的座標值(toX,toY)需要進行計算。第一個畫的是樹幹,由於樹幹垂直於地面所以結束座標toX等於初始座標x,而結束座標toY等於初始y減去樹幹長度branchLen(注意座標的0,0點在canvas的左上角)。var toX = x;var toY = y-branchLen;



function Tree(x,y,branchLen,branchWidth,depth,canvas){
    this.canvas = canvas || document.getElementById('canvas');
    this.ctx = this.canvas.getContext('2d');
    this.x = x||0;
    this.y = y||0;
    this.branchLen = branchLen||0;
    this.branchWidth = branchWidth||0;
    var depth = depth || 5;
    this.drawRoot(this.x,this.y,this.branchLen,this.branchWidth);
}
Tree.prototype.drawRoot = function(x,y,branchLen,branchWidth){
    var toX = x;
    var toY = y-branchLen;
    this.ctx.save();
    this.ctx.strokeStyle="rgba(37, 141, 194, 0.93)";
    this.ctx.beginPath();
    this.ctx.lineCap = "butt";
    this.ctx.lineJoin="round";
    this.ctx.lineWidth = branchWidth;
    this.ctx.moveTo(x,y);
    this.ctx.lineTo(toX,toY);
    this.ctx.closePath();
    this.ctx.stroke();
    this.ctx.restore();
}
var atree = new Tree(canvas.width/2-4,canvas.height,100,8,8,canvas);


運行代碼:


效果圖
點擊查看歷史代碼

Step3.添加drawBranch方法,用來繪製樹枝

drawBranch同樣是根據初始與結束座標畫出一條直線代表樹枝。與樹幹不同的是樹枝不再是垂直與地面而是與樹幹保持一定的角度,而且樹枝的初始值是樹幹的結束點(toX,toY)。所以在drawBranch中我們加入新參數angle用來表示樹枝與樹幹的垂直夾角α,這樣就可以根據α算出toX與toY。請看圖。
效果圖
這樣我們在畫完樹幹後再分別畫兩個不同角度的樹枝,一個是30°一個-30°。並將傳給樹枝的寬度branchWidth減小一個像素,使其與樹幹粗細不同。


Tree.prototype.drawRoot = function(x,y,branchLen,branchWidth){
    var toX = x;
    var toY = y-branchLen;
    this.ctx.save();
    this.ctx.strokeStyle="rgba(37, 141, 194, 0.93)";
    this.ctx.beginPath();
    this.ctx.lineCap = "butt";
    this.ctx.lineJoin="round";
    this.ctx.lineWidth = branchWidth;
    this.ctx.moveTo(x,y);
    this.ctx.lineTo(toX,toY);
    this.ctx.closePath();
    this.ctx.stroke();
    this.ctx.restore();
    this.drawBranch(toX,toY,branchLen,branchWidth-1,30);
    this.drawBranch(toX,toY,branchLen,branchWidth-1,-30);
}
Tree.prototype.drawBranch = function(x,y,branchLen,branchWidth,angle){
    var angle = angle || 0;
    var radian = (90-angle)*(Math.PI/180);
    var toX = x+Math.cos(radian)*branchLen;
    var toY = y-Math.sin(radian)*branchLen;
    this.ctx.save();
    this.ctx.strokeStyle="rgba(37, 141, 194, 0.93)";
    this.ctx.beginPath();
    this.ctx.lineCap = "butt";
    this.ctx.lineJoin="round";
    this.ctx.lineWidth = branchWidth;
    this.ctx.moveTo(x,y);
    this.ctx.lineTo(toX,toY);
    this.ctx.closePath();
    this.ctx.stroke();
    this.ctx.restore();
}


運行代碼:


效果圖
點擊查看歷史代碼

Step4.修改drawBranch函數,重複畫樹枝

在drawBranch函數的最後再次調用兩次drawBranch


this.drawBranch(toX,toY,branchLen,branchWidth-1,angle+30);
this.drawBranch(toX,toY,branchLen,branchWidth-1,angle-30);


使其調用自己完成遞歸,注意這裏傳入的角度是在之前的角度的基礎上在增加或者減少30度。


爲了使遞歸停下來我們需要一個停止條件,就是之前一直沒有用到的depth參數。我們在每次畫下一層之前使其減1表示已經完成了一層樹枝的繪製,直至depth減小到0表示繪製完所有的層數。


function Tree(x,y,branchLen,branchWidth,depth,canvas){
    this.canvas = canvas || document.getElementById('canvas');
    this.ctx = this.canvas.getContext('2d');
    this.x = x||0;
    this.y = y||0;
    this.branchLen = branchLen||0;
    this.branchWidth = branchWidth||0;
    var depth = depth || 5;
    this.drawRoot(this.x,this.y,this.branchLen,this.branchWidth,depth);
}
Tree.prototype.drawRoot = function(x,y,branchLen,branchWidth,depth){
    var toX = x;
    var toY = y-branchLen;
    var depth = depth||5;
    this.ctx.save();
    this.ctx.strokeStyle="rgba(37, 141, 194, 0.93)";
    this.ctx.beginPath();
    this.ctx.lineCap = "butt";
    this.ctx.lineJoin="round";
    this.ctx.lineWidth = branchWidth;
    this.ctx.moveTo(x,y);
    this.ctx.lineTo(toX,toY);
    this.ctx.closePath();
    this.ctx.stroke();
    this.ctx.restore();
    depth--;
    if(depth>0){
      this.drawBranch(toX,toY,branchLen,branchWidth-1,30,depth);
      this.drawBranch(toX,toY,branchLen,branchWidth-1,-30,depth);
    }
}
Tree.prototype.drawBranch = function(x,y,branchLen,branchWidth,angle,depth){
    var angle = angle || 0;
    var radian = (90-angle)*(Math.PI/180);
    var toX = x+Math.cos(radian)*branchLen;
    var toY = y-Math.sin(radian)*branchLen;
    this.ctx.save();
    this.ctx.strokeStyle="rgba(37, 141, 194, 0.93)";
    this.ctx.beginPath();
    this.ctx.lineCap = "butt";
    this.ctx.lineJoin="round";
    this.ctx.lineWidth = branchWidth;
    this.ctx.moveTo(x,y);
    this.ctx.lineTo(toX,toY);
    this.ctx.closePath();
    this.ctx.stroke();
    this.ctx.restore();
    depth--;
    if(depth>0){
      this.drawBranch(toX,toY,branchLen,branchWidth-1,angle+30,depth);
      this.drawBranch(toX,toY,branchLen,branchWidth-1,angle-30,depth);
    }
}


運行代碼:


效果圖
由於樹之間角度過大,而且所有樹枝長度都相等,看起來並不像一棵樹。所以我們需要在Tree的構造函數中加入幾個參數用來調整樹的姿態。


function Tree(x,y,branchLen,branchWidth,depth,canvas){
    ......
    this.branchLenFactor = 0.8;
    this.rootLenFactor = 1.2;
    this.branchAngle = 20;
    ......
}


branchLenFactor:畫每一層樹枝的時候乘在branchLen上面,用來控制樹枝長度。rootLenFactor:畫樹根的時候乘在branchLen上面,用來控制樹根長度。branchAngle: 用來控制樹枝之間的角度。



Tree.prototype.drawRoot = function(x,y,branchLen,branchWidth,depth){
    var toX = x;
    var toY = y-branchLen*this.rootLenFactor;
    var depth = depth||5;
    this.ctx.save();
    this.ctx.strokeStyle="rgba(37, 141, 194, 0.93)";
    this.ctx.beginPath();
    this.ctx.lineCap = "butt";
    this.ctx.lineJoin="round";
    this.ctx.lineWidth = branchWidth;
    this.ctx.moveTo(x,y);
    this.ctx.lineTo(toX,toY);
    this.ctx.closePath();
    this.ctx.stroke();
    this.ctx.restore();
    depth--;
    if(depth>0){
      this.drawBranch(toX,toY,branchLen*this.branchLenFactor,branchWidth-1,this.branchAngle,depth);
      this.drawBranch(toX,toY,branchLen*this.branchLenFactor,branchWidth-1,-this.branchAngle,depth);
    }
  }
  Tree.prototype.drawBranch = function(x,y,branchLen,branchWidth,angle,depth){
    var angle = angle || 0;
    var radian = (90-angle)*(Math.PI/180);
    var toX = x+Math.cos(radian)*branchLen;
    var toY = y-Math.sin(radian)*branchLen;
    this.ctx.save();
    this.ctx.strokeStyle="rgba(37, 141, 194, 0.93)";
    this.ctx.beginPath();
    this.ctx.lineCap = "butt";
    this.ctx.lineJoin="round";
    this.ctx.lineWidth = branchWidth;
    this.ctx.moveTo(x,y);
    this.ctx.lineTo(toX,toY);
    this.ctx.closePath();
    this.ctx.stroke();
    this.ctx.restore();
    depth--;
    if(depth>0){
      this.drawBranch(toX,toY,branchLen*this.branchLenFactor,branchWidth-1,angle+this.branchAngle,depth);
      this.drawBranch(toX,toY,branchLen*this.branchLenFactor,branchWidth-1,angle-this.branchAngle,depth);
    }
  }


運行代碼:


效果圖
點擊查看歷史代碼

Step5.使樹枝晃動起來

爲了使樹枝有搖晃的效果,我們只需要改變樹枝之間的角度branchAngle就可以了。我需要在Tree的構造函數中增加三個新屬性:oBranchAngle用來記錄初始角度;branchAngleFactor用來控制角度隨時間變化的變化量;swingAngle:隨時間增加用來記錄搖動的角度。
同時修改下drawRoot函數使其不用接受參數。調用更加方便。


function Tree(x,y,branchLen,branchWidth,depth,canvas){
    ......
    this.branchAngle = 20;
    this.oBranchAngle = this.branchAngle;
    this.branchAngleFactor = 5;
    this.swingAngle = 0;
    ......
    this.drawRoot();
}

Tree.prototype.drawRoot = function(){
    var x = this.x,y=this.y,branchLen = this.branchLen,depth = this.depth,branchWidth = this.branchWidth;
    var toX = x;
    var toY = y-branchLen*this.rootLenFactor;
    var depth = depth||5;
    this.ctx.save();
    this.ctx.strokeStyle="rgba(37, 141, 194, 0.93)";
    this.ctx.beginPath();
    this.ctx.lineCap = "butt";
    this.ctx.lineJoin="round";
    this.ctx.lineWidth = this.branchWidth;
    this.ctx.moveTo(x,y);
    this.ctx.lineTo(toX,toY);
    this.ctx.closePath();
    this.ctx.stroke();
    this.ctx.restore();
    depth--;
    if(depth>0){
      this.drawBranch(toX,toY,branchLen*this.branchLenFactor,branchWidth-1,this.branchAngle,depth);
      this.drawBranch(toX,toY,branchLen*this.branchLenFactor,branchWidth-1,-this.branchAngle,depth);
    }
  }


增加循環函數,在循環函數中重繪整個樹,並且每次重繪都要修改branchAngle值,使大樹搖動起來。atree.swingAngle++;使搖動角度隨時間變化。這裏使用Math.sin(atree.swingAngle*(Math.PI/180))可以獲得一個-1至1之間的連續變化值。atree.branchAngle = Math.sin(atree.swingAngle*(Math.PI/180))*atree.branchAngleFactor+atree.oBranchAngle;乘以係數並加在原角度上。



function loop(time){
    ctx.clearRect(0,0,canvas.width,canvas.height);
    atree.branchAngle = Math.sin(atree.swingAngle*(Math.PI/180))*atree.branchAngleFactor+atree.oBranchAngle;
    atree.drawRoot()
    requestAnimFrame(loop);
  }
  loop(0);


運行代碼:


效果圖
點擊查看歷史代碼

Step6.添加手勢

這裏爲了省事只添加了touch事件,mouse事件與touch事件的處理方法大體一致。
首先爲Tree新加一個屬性swingSwitch = true用來表示大樹是否擺動。當手指觸控到屏幕的時候擺動停止,離開屏幕的時候擺動繼續。
添加strengthX,strengthY兩個屬性;分別表示樹在x軸與y軸因受到的力而移動的距離。
添加strengthXFactor,strengthYFactor;分別用來表示再一次滑動中x軸與y軸移動的最大距離。


function Tree(x,y,branchLen,branchWidth,depth,canvas){
    ......
    this.swingSwitch = true;
    ......
    this.strengthX = 0;
    this.strengthY = 0;
    ......
  }
//記錄觸控開始時的信息
var touchStart = {x:0,y:0,strengthX:0,strengthY:0};
document.addEventListener('touchstart',function(e){
    //讓樹停止擺動
    atree.swingSwitch = false;
    touchStart.x = e.touches[0].clientX;
    touchStart.y = e.touches[0].clientY;
    //記錄觸控開始時,原strength的值
    touchStart.strengthX = atree.strengthX;
    touchStart.strengthY = atree.strengthY;
});
document.addEventListener('touchmove',function(e){
    //阻止瀏覽器默認動作
    e.preventDefault();
    //(touchStart.x-e.touches[0].clientX)/canvas.width可以根據滑動距離獲得一個0-1的值
    atree.strengthX = touchStart.strengthX-(touchStart.x-e.touches[0].clientX)/canvas.width*atree.strengthXFactor;
    atree.strengthY = touchStart.strengthY-(touchStart.y-e.touches[0].clientY)/canvas.height*atree.strengthYFactor;
});
document.addEventListener('touchend',function(e){
    //恢復擺動
    atree.swingSwitch = true;
});


修改drawBranch將strength的變化添加到角度與toX,toY的計算中,詳情見註釋。


Tree.prototype.drawBranch = function(x,y,branchLen,branchWidth,angle,depth){

    var angle = angle || 0;
    //用strengthX乘以(depth/this.depth)使得樹枝末梢對角度的變化不敏感
    angle += this.strengthX*(depth/this.depth)/this.strengthXFactor*this.branchAngle;
    var radian = (90-angle)*(Math.PI/180);
    //用strengthX乘以(1-depth/this.depth)使得樹枝末梢對角度的變化敏感
    var toX = x+Math.cos(radian)*branchLen+this.strengthX*(1-depth/this.depth);
    var toY = y-Math.sin(radian)*branchLen+this.strengthY*(1-depth/this.depth);
    ......
}


在動畫循環中添加恢復代碼,使strengthX,strengthY恢復爲0,並增加swingSwitch的判斷。



function loop(time){
    ......
    //當swingSwitch開啓時開始擺動
    if(atree.swingSwitch){
        //將strength恢復到0
      if(atree.strengthX >0){
        atree.strengthX -= 1;
      }
      if(atree.strengthX <0){
        atree.strengthX += 1;
      }
      if(atree.strengthY >0){
        atree.strengthY -= 1;
      }
      if(atree.strengthY <0){
        atree.strengthY += 1;
      }
      atree.swingAngle++;
      atree.branchAngle = Math.sin(atree.swingAngle*(Math.PI/180))*atree.branchAngleFactor+atree.oBranchAngle;
    }
    ......
}
loop(0);


運行代碼:


效果圖
點擊查看歷史代碼

Step7.添加緩動效果

Step6中的恢復strengthX,strengthY的代碼過於簡單,動畫勻速恢復到0,顯得過於突兀。比較真實的情況應該是由快變慢的恢復,所以我們要爲恢復代碼加上緩動。首先在Tree中添加recoverStartTime = 0用來記錄恢復開始的時間,在手指離開屏幕的時候(touchend)將其賦爲0,同時用oStrengthX,oStrengthY記錄下來strengthX與strengthY的目標值。


function Tree(x,y,branchLen,branchWidth,depth,canvas){
    ......
    this.recoverStartTime = 0;
    ......
}
function loop(time){
    ......
    if(atree.swingSwitch){
      if(atree.strengthX > 0){
        if(atree.recoverStartTime == 0){
          atree.recoverStartTime = time;
        }
        var t = time-atree.recoverStartTime;
        //五次方的緩動
        atree.strengthX =  Math.max(atree.oStrengthX-atree.oStrengthX*((t=t/2000-1)*t*t*t*t + 1)+0,0);
      }
      if(atree.strengthX < 0){
        if(atree.recoverStartTime == 0){
          atree.recoverStartTime = time;
        }
        var t = time-atree.recoverStartTime;
        //五次方的緩動
        atree.strengthX =  Math.min(atree.oStrengthX-atree.oStrengthX*((t=t/2000-1)*t*t*t*t + 1)+0,0);
      }
      if(atree.strengthY > 0){
        if(atree.recoverStartTime == 0){
          atree.recoverStartTime = time;
        }
        var t = time-atree.recoverStartTime;
        //五次方的緩動
        atree.strengthY =  Math.max(atree.oStrengthY-atree.oStrengthY*((t=t/2000-1)*t*t*t*t + 1)+0,0);
      }
      if(atree.strengthY < 0){
        if(atree.recoverStartTime == 0){
          atree.recoverStartTime = time;
        }
        var t = time-atree.recoverStartTime;
        //五次方的緩動
        atree.strengthY =  Math.min(atree.oStrengthY-atree.oStrengthY*((t=t/2000-1)*t*t*t*t + 1)+0,0);
      }
    }
    ......
}
document.addEventListener('touchend',function(e){
  atree.recoverStartTime = 0;
  atree.oStrengthX = atree.strengthX;
  atree.oStrengthY = atree.strengthY;
  ......
});


運行代碼:


效果圖點擊查看歷史代碼

Step7.使樹幹搖動並移至屏幕左邊

修改drawRoot使樹幹也可以晃動,並修改var atree = new Tree(10,canvas.height,100,8,8,canvas);使其移至左邊。



Tree.prototype.drawRoot = function(){
    ......
    //增加strength
    var angle = 0;
    angle += this.strengthX/this.strengthXFactor*this.branchAngle;
    var radian = (90-angle)*(Math.PI/180);
    var toX = x+Math.cos(radian)*branchLen*this.rootLenFactor;
    var toY = y-Math.sin(radian)*branchLen*this.rootLenFactor;
    ......
}
var atree = new Tree(10,canvas.height,100,8,8,canvas);


運行代碼:


效果圖點擊查看歷史代碼

Step8.

將動畫循環中處理角度的部分添加到Tree的swing()中。

Tree.prototype.swing = function(time){
    this.ctx.clearRect(0,0,this.canvas.width,this.canvas.height);
    if(this.swingSwitch){
      if(this.strengthX > 0){
        if(this.recoverStartTime == 0){
          this.recoverStartTime = time;
        }
        var t = time-this.recoverStartTime;
        this.strengthX =  Math.max(this.oStrengthX-this.oStrengthX*((t=t/2000-1)*t*t*t*t + 1)+0,0);
      }
      if(this.strengthX < 0){
        if(this.recoverStartTime == 0){
          this.recoverStartTime = time;
        }
        var t = time-this.recoverStartTime;
        this.strengthX =  Math.min(this.oStrengthX-this.oStrengthX*((t=t/2000-1)*t*t*t*t + 1)+0,0);
      }
      if(this.strengthY > 0){
        if(this.recoverStartTime == 0){
          this.recoverStartTime = time;
        }
        var t = time-this.recoverStartTime;
        this.strengthY =  Math.max(this.oStrengthY-this.oStrengthY*((t=t/2000-1)*t*t*t*t + 1)+0,0);
      }
      if(this.strengthY < 0){
        if(this.recoverStartTime == 0){
          this.recoverStartTime = time;
        }
        var t = time-this.recoverStartTime;
        this.strengthY =  Math.min(this.oStrengthY-this.oStrengthY*((t=t/2000-1)*t*t*t*t + 1)+0,0);
      }
      this.swingAngle++;
      this.branchAngle = Math.sin(this.swingAngle*(Math.PI/180))*this.branchAngleFactor+this.oBranchAngle;
    }
    this.drawRoot();}var atree = new Tree(10,canvas.height,100,8,8,canvas);function loop(time){
    atree.swing(time);
    requestAnimFrame(loop);}loop(0);

運行代碼:
效果圖




查看所有代碼請去Github


如有問題或者建議請微博@UED天機。我會及時回覆


發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章