經過前面的文章,我們已經能夠在canvas畫布上畫出各種炫酷的圖形和畫面,但是這些畫面都是禁止的,怎么樣才能讓他們動起來呢?
- 如何繪制基本圖形可以參考:canvas基本圖形繪制
- 如何對基本圖形移動旋轉縮放可以參考:canvas圖形變換
- 如何設置基本圖形顏色和樣式可以參考:canvas樣式和顏色
- 如何使用外部圖片以及圖形組合可以參考:canvas使用圖片,圖形組合以及裁剪
- canvas如何保存和加載圖像可以參考:canvas圖像保存
- canvas系列教程可以參考:canvas
動畫的基本步驟
我們知道,動畫是一幀一幀的畫面不斷反映實現的,人的眼睛看到一幅畫或一個物體后,在0.34秒內不會消失。利用這一原理,在一幅畫還沒有消失前播放下一幅畫,就會給人造成一種流暢的視覺變化效果。在canvas中,就是在繪制完當前畫面之后,快速的繪制下一個畫面。步驟如下:
- 清空canvas。
- 除非接下來要畫的內容會完全充滿 canvas (例如背景圖),否則你需要清空所有畫布上的內容。最簡單的做法就是用
clearRect方法。
- 除非接下來要畫的內容會完全充滿 canvas (例如背景圖),否則你需要清空所有畫布上的內容。最簡單的做法就是用
- 保存canvas狀態。
- 如果你要改變一些會改變 canvas 狀態的設置(樣式,變形之類的),又要在每畫一幀之時都是原始狀態的話,你需要先保存一下。
- 繪制動畫圖形(animated shapes)。
- 這一步才是重繪動畫幀。
- 恢復 canvas 狀態。
- 如果已經保存了 canvas 的狀態,可以先恢復它,然后重繪下一幀。
操縱動畫
在 canvas 上繪制內容是用 canvas 提供的或者自定義的方法,而通常,我們僅僅在腳本執行結束后才能看見結果,比如說,在 for 循環里面做完成動畫是不太可能的。
因此,為了實現動畫,我們需要一些可以定時執行重繪的方法。window對象提供了下面的方法實現定時動畫:
setInterval(function, delay)當設定好間隔時間后,function會定期執行setTimeout(function, delay)在設定好的時間之后執行函數requestAnimationFrame(callback)告訴瀏覽器你希望執行一個動畫,並在重繪之前,請求瀏覽器執行一個特定的函數來更新動畫。
如果你並不需要與用戶互動,你可以使用setInterval()方法,它就可以定期執行指定代碼。如果我們需要做一個游戲,我們可以使用鍵盤或者鼠標事件配合上setTimeout()方法來實現。通過設置事件監聽,我們可以捕捉用戶的交互,並執行相應的動作。
window.requestAnimationFrame()這個方法提供了更加平緩並更加有效率的方式來執行動畫,當系統准備好了重繪條件的時候,才調用繪制動畫幀。一般每秒鍾回調函數執行60次,也有可能會被降低。
在使用window.requestAnimationFrame()方法的過程中,我推薦使用下面的兼容性方法來代替:
window.requestAnimationFrame = (function(){
return window.requestAnimationFrame ||
window.webkitRequestAnimationFrame ||
window.mozRequestAnimationFrame ||
window.oRequestAnimationFrame ||
window.msRequestAnimationFrame ||
function (callback) {
window.setTimeout(callback, 1000 / 60);
};
})();
canvas動畫實例-模擬小球自由落體運動
上面介紹了canvas動畫的基本概念,接下來我們將會在canvas中實現小球下落的動畫。小球的完整代碼再本文結尾。點擊可跳轉到結尾。
繪制小球
首先需要在canvas上繪制一個小球。
var ctx = document.getElementById('canvas').getContext('2d');
if (!ctx) {
console.log('您的瀏覽器不支持canvas');
// 可以拋出異常強制結束JS執行
throw new Error("Do not support canvas");
}
var ball = {
x: 100, // 小球的x坐標
y: 100, // 小球的y坐標
radius: 25, // 小球半徑
color: 'cyan', // 小球顏色
draw: function() { // 繪制小球的函數
ctx.beginPath();
ctx.arc(this.x, this.y, this.radius, 0, Math.PI * 2, true);
ctx.closePath();
ctx.fillStyle = this.color;
ctx.fill();
},
clear: function() { // 清除小球區域的函數
ctx.clearRect(this.x - this.radius,
this.y - this.radius,
this.radius * 2,
this.radius * 2);
}
}
ball.draw(); // 繪制小球
添加運動描述
繪制了小球之后,要添加動畫,還需要為小球添加速率矢量進行移動。另外,速度也是變化的量,對於只有落體運動,還有豎直方向的重力加速度,所以還需要為小球加上加速度。
var ball = {
x: 100, // 小球的x坐標
y: 100, // 小球的y坐標
vx: 0, // 小球水平方向速度
vy: 0, // 小球豎直方向速度
ax: 0, // 小球水平方向加速度
ay: 0, // 小球豎直方向加速度
dt: 1, // 兩幀之間的時間為1個單位時間
radius: 25, // 小球半徑
color: 'cyan', // 小球顏色
s: function(v, a, t) {
// 勻加速直線運動的位移公式:s=vt+1/2at^2
return v * t + (1 / 2.0) * a * t * t;
},
dx: function() {
// 計算水平方向的位移
return this.s(this.vx, this.ax, this.dt);
},
dy: function() {
// 計算豎直方向的位置
return this.s(this.vy, this.ay, this.dt);
},
next: function() {
// 計算小球下一時刻的位移
this.x += this.dx();
this.y += this.dy();
// 計算小球下一時刻的速度:v_t = v_0 + a*t
this.vx = this.vx + this.ax * this.dt;
this.vy = this.vy + this.ay * this.dt;
this.boundary(0, canvas.width, canvas.height, 0);
},
};
假設每一幀之間的時間是單位時間,那么根據當前小球的位置速度和加速度,我們就可以計算下一幀的小球的位置和速度,此時清空上一幀的canvas,再繪制下一幀,即可實現動畫效果。
var animate; // 記錄動畫
ball.draw();
// 繪制一幀
function draw() {
// 1:清空畫布
ctx.clearRect(0, 0, canvas.width, canvas.height);
// 2:繪制小球
ball.draw();
// 3:計算小球的下一個狀態
ball.next();
// 4:進入下一幀
animate = window.requestAnimationFrame(draw);
}
邊界處理
若沒有任何的碰撞檢測,我們的小球很快就會超出畫布。我們需要檢查小球的 x 和 y 位置是否已經超出畫布的尺寸以及是否需要將速度矢量反轉。
boundary: function(top, right, bottom, left) {
// 檢測小球下一幀是否出界,出界則補正
if (this.y > bottom) { // 下邊界越界
this.vy = -this.vy; // 速度反向
} else if (this.y < top) {
this.vy = -this.vy;
} else if (ball.x > right) {
this.vx = -this.vx; // 速度反向
} else if (ball.x < left) {
this.vx = -this.vx;
}
}
添加拖尾效果
為了使得小球運動更加逼真,可以添加拖尾效果。使用clearRect函數清除前一幀動畫時,若用一個半透明的fillRect函數取代之,就可輕松制作長尾效果。
ctx.fillStyle = 'rgba(255,255,255,0.3)';
ctx.fillRect(0,0,canvas.width,canvas.height);
移動鼠標到canvas內可讓小球動起來!
遺留問題和優化
在實際的生活中,小球碰撞到地面反彈的時候,反彈的高度會越來越低,因為碰撞地面損失了一部分速度。
boundary: function(top, right, bottom, left) {
// 檢測小球下一幀是否出界,出界則補正
if (this.y > bottom) { // 下邊界越界
this.vy = -this.vy; // 速度反向
this.vy = 0.9 * this.vy; // 速度損失
} else if (this.y < top) {
this.vy = -this.vy;
} else if (ball.x > right) {
this.vx = -this.vx; // 速度反向
} else if (ball.x < left) {
this.vx = -this.vx;
}
}
上面這種方式會偶爾使得小球無法反彈。
在碰撞地面的時候,小球的反彈之后的速度和位移,准確值需要根據嚴格的勻加速公式以及損失之后的速度來計算。
邊界檢查時上述方法是檢查圓心和邊界的位置,更好的方式是檢查圓周和邊界的距離。
源碼可以以及效果可以參考這兒:本文實例
上述所有方式的源代碼如下:
```html