這篇是學習和回顧canvas系列筆記的第五篇,完整筆記詳見:canvas核心技術。javascript
在上一篇canvas核心技術-如何實現簡單的動畫筆記中,咱們詳細學習瞭如何進行canvas座標系的平移,縮放,旋轉等操做來實現一些比較簡單和單一的動畫。可是在實際動畫中,影響一個動畫的因素是不少的,好比一個小球自由落體運動,咱們不只要考慮小球的初始速度和初始方向,還要考慮重力加速度,空氣阻力等外界因素。這一篇筆記,咱們會詳細學習複雜動畫的相關知識。css
咱們理解的動畫,應該是在一段時間內,物體的某些屬性,好比顏色,大小,位置,透明度等,發生改變。判斷動畫流程度的單位是動畫刷新的速率,在瀏覽器中通常是瀏覽器的幀速率。幀速率越大,動畫就越流暢。在現代瀏覽器中,咱們通常是使用requestAnimationFrame
來執行動畫。java
let raf = null;
let lastFrame = 0;
//動畫
function animate(frame) {
// todo:這裏能夠執行一些動畫更新
console.log(frame)
raf = requestAnimationFrame(animate);
lastFrame = frame;
}
function start() {
// 一些初始化的操做
init();
// 執行動畫
animate(performance.now());
}
function stop() {
cancelAnimationFrame(raf);
}
複製代碼
通常大體的結構就是這樣的,經過requestAnimationFrame
不斷地在瀏覽器下一幀中執行animate
,且animate
函數會接受一個當前幀開始執行的時間戳的參數。若是想中斷當前進行的動畫,只須要調用cancelAnimationFrame
,那麼在下一幀中就不會執行animate
函數了。上一幀執行的時間,能夠用frame - lastFrame
,而後再根據這個差值就能夠計算出當前動畫的幀速率了,以下,css3
let fps = 0;
let lastCalculateFpsTime = 0;
function calculateFps(frame) {
if (lastFrame && (fps === 0 || frame - lastCalculateFpsTime > 1000)) {
fps = 1000 / (frame - lastFrame);
lastCalculateFpsTime = frame;
}
}
//動畫
function animate(frame) {
// todo:這裏能夠執行一些動畫更新
calculateFps(frame);
raf = requestAnimationFrame(animate);
lastFrame = frame;
}
複製代碼
在計算fps時,咱們是用1s除以上一幀執行的時間,因爲frame的單位是毫秒,因此是用1000除的。上面咱們還作了一個優化,就是每1s纔會去計算一次fps,而不是每幀都去計算,由於每幀都去計算,意義不大,且增長了額外的計算。git
在繪製動畫時,咱們必須按照基於時間的方式來設計,而不是當前瀏覽器的幀速率。由於,不一樣瀏覽器會有不一樣的幀速率,同一瀏覽器在不一樣的GPU負載下幀速率也可能會不一樣,因此咱們的動畫必須是基於時間的,這樣才能保證一樣的速度在相同的時間內,動畫變化的是一致的。好比,咱們在考慮小球垂直下落時,必須設置小球的下落的速度v,而後再根據公式,得出小球在當前時間段內的移動距離,計算出當前幀內的座標。github
/* 初始化 */
private init() {
this.fps = 0;
this.lastFrameTime = 0;
this.speed = 5; // 設置小球初速速度爲:5m/s
this.distance = 50; //設置小球距離地面高度爲:50m
let pixel = this.height - this.padding * 2;
if (this.distance <= 0) {
this.pixelPerMiter = 0;
} else {
this.pixelPerMiter = (this.height - this.padding * 2) / this.distance;
}
}
複製代碼
在上面代碼中,咱們在初始化的時候,設定了小球的初始速度爲,小球距離地面的高度爲
,以及計算出了物理高度與像素高度的比值pixelPerMiter,這個值在後面計算小球的座標時是有用到的。canvas
/* 更新 */
private update() {
if (this.fps) {
this.ball.update(1000 / this.fps); //更新小球
}
}
複製代碼
而後,在每幀更新小球位置時,咱們將上一幀進過的時間值傳遞給ball.update
。瀏覽器
/* 移動 */
static move(ball: Ball, elapsed: number) {
//小球是靜止狀態,不更新
if (ball.isStill) {
return;
}
let { currentSpeed } = ball;
let t = elapsed / 1000; //elapsed是毫秒, 而速度單位是m/s,因此要除1000
let distance = ball.currentSpeed * t;
if (ball.offset + distance > ball.verticalHeight) {
////若是小球是否已經超過實際高度,則落到地面了
ball.isStill = true;
ball.currentSpeed = 0;
ball.offset = ball.verticalHeight;
} else {
ball.offset += distance;
}
}
複製代碼
再根據公式計算出上一幀時間裏,小球下落的距離distance,累加每一幀下落的距離,則能夠獲得當前小球下落的總距離,若是小球下落的總距離大於了小球距離地面的實際高度,則表示小球落到地面了,就中止小球下落。函數
/* 繪製小球 */
public render(ctx: CanvasRenderingContext2D) {
let { x, y, radius, offset, pixelPerMiter } = this;
ctx.save();
ctx.translate(x, y + offset * pixelPerMiter); //offset * pixelPerMiter獲得下落的像素
ctx.beginPath();
ctx.arc(0, 0, radius, 0, Math.PI * 2, false);
ctx.fill();
ctx.stroke();
ctx.restore();
}
複製代碼
最後在繪製小球時,先要根據下落的實際高度offset
和前面計算獲得的實際高度與像素高度的比值,來獲得小球在屏幕上下落的像素值()。post
上面就是咱們在寫小球自由落體時的大體思路,重點是設置小球的初始下落速度,以及在每一幀裏計算出小球下落的距離,最後根據實際高度與像素高度比,計算出小球在屏幕上下落的像素高度。這個過程當中,咱們尚未考慮重力加速度和空氣阻力等物理因素,下面,咱們就來考慮物理因素對動畫的影響。
爲了使動畫或者遊戲表現的更加真實,一般須要考慮真實世界中物理因素的影響,好比咱們繼續考慮小球自由落體運動,真實世界中,小球自由落體運動會收到重力加速度,空氣阻力,空氣流向,反彈等的影響,從而改變小球下落的速度。
/* 建立小球 */
private createBall() {
let { width, height, padding, speed, radius, pixelPerMiter, distance } = this;
this.ball = new Ball(width / 2, padding - radius, radius, { verticalHeight: distance, pixelPerMiter, useGravity: true });
this.ball.setSpeed(speed);
this.ball.addBehavior(Ball.move);
}
複製代碼
在建立小球時,咱們給了一個參數userGravity
來表示是否使用重力加速度,這裏咱們設置爲true
,同時咱們也傳遞了小球的初始座標和半徑,以及初始速度等。
const GRAVITY = 9.8; //重力加速度9.8m/s
/* 移動 */
static move(ball: Ball, elapsed: number) {
// ...
//若是應用了重力加速度,則更新速度
if (ball.useGravity) {
ball.currentSpeed += GRAVITY * t;
}
// ...
}
複製代碼
而後在更新小球時,咱們增長了對小球當前速度的計算,根據公式 計算出上一幀的速度,這樣,隨着時間,小球的速度其實是不斷增長的,小球下落的會愈來愈快。
前面咱們在處理小球落到地面時,只是單純的讓小球停在地面上。可是在實際生活中,咱們下落的小球,碰到地面後,都會反彈必定高度,而後又下落,直到小球靜止在地面上。爲了更加真實模擬小球下落,咱們來考慮反彈物理因素。
//建立小球
this.ball = new Ball(width / 2, padding - radius, radius, { verticalHeight: distance, pixelPerMiter, useGravity: true, useRebound: true });
複製代碼
在建立小球時,咱們傳遞了useRebound:true
,表示當前小球應用了反彈效果,在更新小球時,須要判斷小球在落地時,將當前速度方向反向,且大小減爲原來的0.6倍,這個0.6係數只是一個經驗值,在具體遊戲中,能夠調整,達到想要的效果。係數越大,反彈越高。
/* 移動 */
static move(ball: Ball, elapsed: number) {
//小球是靜止狀態,不更新
if (ball.isStill) {
return;
}
let { currentSpeed } = ball;
let t = elapsed / 1000; //elapsed是毫秒, 而速度單位是m/s,因此要除1000
//更新速度
if (ball.useGravity) {
ball.currentSpeed += GRAVITY * t;
}
let distance = ball.currentSpeed * t;
if (ball.offset + distance > ball.verticalHeight) {
//落到地面了
//使用反彈效果
if (ball.useRebound) {
ball.offset = ball.verticalHeight;
ball.currentSpeed = -ball.currentSpeed * 0.6; //速度方向取反,大小乘0.6
if ((distance * ball.pixelPerMiter) / t < 1) {
//當前移動距離小於1px,應該靜止了,
ball.isStill = true;
ball.currentSpeed = 0;
}
} else {
ball.isStill = true;
ball.currentSpeed = 0;
ball.offset = ball.verticalHeight;
}
} else {
ball.offset += distance;
}
}
}
複製代碼
在應用反彈效果時,咱們判斷當前速度在1s內在下落位移小於1px時,就將小球中止,這樣,防止小球在反彈距離很小很小時,進行沒必要要的計算。
至於其餘物理因素,好比風向,阻力等,咱們就不具體討論了,具體思路跟上面同樣,先進行物理建模,而後在更新過程當中根據物理公式計算受影響的屬性,最後再根據屬性值來繪製。
動畫是持續一段時間的,咱們能夠事先給定具體的持續時間值,讓動畫在這段時間內持續執行,就像css3中animation-duration
,而後經過扭曲時間軸,可讓動畫執行非線形運動,好比咱們常見緩入效果,緩出效果,緩入緩出效果等。
時間軸扭曲,是經過一系列對應的緩動函數,根據當前的時間完成比率compeletePercent,計算獲得一個扭曲後的值effectPercent,最後根據這2個值獲得扭曲後的時間值elapsed
線性函數,
static linear() {
return function(percent: number) {
return percent;
};
}
複製代碼
緩入函數,
static easeIn(strength: number = 1) {
return function(percent: number) {
return Math.pow(percent, strength * 2);
};
}
複製代碼
緩出函數,
static easeOut(strength: number = 1) {
return function(percent: number) {
return 1 - Math.pow(1 - percent, strength * 2);
};
}
複製代碼
緩入緩出函數,
static easeInOut() {
return function(percent: number) {
return percent - Math.sin(percent * Math.PI * 2) / (2 * Math.PI);
};
}
複製代碼
更復雜的緩動函數還有彈簧效果,貝塞爾曲線等,詳細能夠參見EasingFunctions。
本篇筆記主要討論了在canvas中如何實現複雜的動畫效果,從一個小球的自由落地運動爲示例,咱們在計算小球的下落距離時,是以時間維度來計算,而不是當前瀏覽器的幀速率,由於幀速率不是一個恆定可靠的值,它會使小球的運動變得不明確。當咱們以時間爲計算值時,小球在一樣時間內下落的距離值,咱們是能夠計算出來的,是一個準確不受幀速率影響的值。爲了使小球下落的更加真實,咱們又考慮了影響小球下落的物理因素,好比重力加速度,反彈效果等。在製做其餘一些非線性運動的動畫時,咱們可使用常見的緩動函數,好比,緩入,緩出等,它們的本質都是經過扭曲時間軸,使得當前的運動受時間因素影響。
在製做canvas遊戲時,基本都會運用到動畫,有物體運動,那麼必定會發生碰撞,好比上面咱們的小球下落,就會發生小球與地面的碰撞,咱們進行了簡單的碰撞檢測。下一篇筆記,咱們詳細討論,如何在canvas中進行碰撞檢測。