canvas動畫包教不包會:緩動與彈動

2018-06-19 15:24 更新
      到目前為止,《canvas動畫包教不包會》系列已經(jīng)進(jìn)行了7章,主要講解了用戶交互與基礎(chǔ)動畫,從本章開始,將進(jìn)入高級動畫篇。這一章主要講解緩動(比例速度)和彈動(比例加速度)。

1、比例運(yùn)動
比例運(yùn)動 是指運(yùn)動與距離成比例的運(yùn)動。

緩動彈動都是比例運(yùn)動,兩者關(guān)系緊密,都是將對象從已有位置移動到目標(biāo)位置的方法。緩動是指物體滑動到目標(biāo)點就停下來了。彈動是指物體來回地反彈一會兒,最終停在目標(biāo)點的運(yùn)動。

兩者的共同點
  • 有一個目標(biāo)點
  • 確定物體到目標(biāo)點的距離
  • 運(yùn)動與距離是成正比的----距離越遠(yuǎn),運(yùn)動的程度越大

兩者的不同點
  • 運(yùn)動和距離成正比的方式不一樣。緩動是指 速度 距離 成正比(物體離目標(biāo)越遠(yuǎn),物體運(yùn)動的速度越快,當(dāng)物體運(yùn)動到很接近目標(biāo)點時,物體幾乎就停下來了);而彈動是指 加速度 距離 成正比(物體離目標(biāo)點越遠(yuǎn),加速度就快速增大,當(dāng)物體很接近目標(biāo)點時,加速度變得很小,但它還是在加速;當(dāng)它越過目標(biāo)點之后,隨著距離的變大,反向加速度也隨之變大,就會把它拉回了,最終在摩擦力的作用下停住。)

2、緩動
緩動的類型不止一種,我們可以“緩入”(ease in)到一個位置,也可以從一個位置“緩出”(ease out)。

在現(xiàn)實生活中,相信大家都坐過公交(自動過濾土豪),在寬敞的馬路上時,公交會高速前進(jìn),特別是車少的道路,司機(jī)會開的盡可能快(限速之內(nèi)),當(dāng)快要達(dá)到一個站點時,司機(jī)就會適當(dāng)?shù)臏p速。當(dāng)公交還有幾米就要停下來的時候,速度已經(jīng)很慢很慢了。這就是一種緩動。

如何實現(xiàn)緩動呢?
一般來說,我們會如下處理:
  • 為運(yùn)動確定一個小于1且大于0的小數(shù)作為比例系數(shù)(easing)
  • 確定目標(biāo)點
  • 計算物體與目標(biāo)點的距離
  • 計算速度,速度=距離 * 比例系數(shù)
  • 用當(dāng)前位置加上速度來計算新的位置
  • 不斷重復(fù)第3步到第5步,直到物體到達(dá)目標(biāo)點

緩動的整個過程并不復(fù)雜,我們需要知道距離(物體與目標(biāo)點(target)之間,變化值)、比例系數(shù)(easing,速度除以距離)。

dx = targetX - ball.x;

dy = targetY - ball.y;


easing = vx / dx;  =>   vx = dx * easing;

easing = vy / dy;  =>   vy = dy * easing;

根據(jù)《速度與加速度》那一章的公式:

ball.x += vx;  =>  ball.x += dx*easing;  =>  ball.x += (targetX - ball.x) * easing;

ball.y += vy;  => ball.y += dy*easing; => ball.y += (targetY - ball.y) * easing;

最終緩動公式:

ball.x += (targetX - ball.x) * easing;

ball.y += (targetY - ball.y) * easing;

來看看例子:

關(guān)鍵代碼:

var easing = 0.05;   

var targetX = canvas.width - 10;   

var targetY = canvas.height - 10;

在上面的例子中,我們將比例系數(shù)設(shè)為0.05,用變量easing表示,然后在循環(huán)中調(diào)用下面的代碼:

ball.x += (targetX- ball.x)*easing; //每次循環(huán)中調(diào)用

這樣簡單的處理,就能實現(xiàn)剎車模式,這就是緩動的一種效果,你可以改變easing看看。


上面的例子中的目標(biāo)點是canvas邊界,其實,目標(biāo)點是可以 變動 的,因為我們每次都會重新計算距離,所以只須在播放每一幀的時候知道目標(biāo)點的位置,然后就可以計算距離和速度了。比如:將鼠標(biāo)位置(mouse.x和mouse.y)作為目標(biāo)點,你可以試試,會發(fā)現(xiàn)鼠標(biāo)里的越遠(yuǎn),小球就運(yùn)動的越快。


這里還有一個關(guān)鍵性問題:何時停止緩動


不是到達(dá)目標(biāo)點就停止緩動嗎?估計這是你看到這的第一想法,你還可能立即想到下面判斷公式:

if(ball.x === targetX && ball.y === targetY){

  //到達(dá)目標(biāo)點

}

這是理論上的判斷,但是從數(shù)學(xué)的角度來看,下面的公式永遠(yuǎn)不會相等:

(ball.x + (targetX - ball.x) * easing) !== targetX

這是為什么呢?

這就涉及了 芝諾餑論 ,簡單的理解是這樣:為了把一個物體從A點移到B點,就必須把它先移到到A和B的中間點C,然后再移到C和B的中間點,然后再折半,不斷地重復(fù)下去,每次移到到物體到距離目標(biāo)點的一半,這樣就會進(jìn)入無窮循環(huán)下去,物體永遠(yuǎn)不會到達(dá)目標(biāo)點。


我們來看看數(shù)學(xué)例子:物體從0的位置,要將它移到100,比例系數(shù)easing設(shè)為0.,5,然后將它每次移動距離的一半,過程如下:

  • 從原點開始,在第一幀后,它移到到50
  • 在第二幀后,移動到75
  • 在第三幀后,移動到87.5
  • 就這樣循環(huán)下去,物體位置變化是93.75、96.875等,經(jīng)過20幀后,它的位置是99.999809265

看到?jīng)]有,它會離目標(biāo)點越來越近,可是理論上是永遠(yuǎn)不會到達(dá)目標(biāo)點的,所以上面的判斷公式是永遠(yuǎn)不會返回true的。


但畢竟肉眼是無法分辨這么精確的位置變化的,有時候當(dāng)ball.x 等于99的時候,我們在canvas上看就已經(jīng)是到達(dá)終點了,所以這就產(chǎn)生了一個問題:多近才是足夠近呢?


這就需要我們?nèi)藶榈闹付ㄒ粋€特定值,判斷物體到目標(biāo)點的距離是否小于特定值,如果小于特定值,那我們就認(rèn)為它到達(dá)終點了。

/*二維坐標(biāo)*/

distance = Math.sqrt(dx * dx + dy * dy);


/*一維坐標(biāo)*/

distance = Math.abs(dx)


if(distance < 1){

  console.log('到達(dá)終點');

  cancelAnimationFrame(requestID);

}

一般采取是否小于1來判斷是否到達(dá)目標(biāo)點,是為了停止動畫,避免資源的浪費(fèi)。


在tool.js工具類中,我們已經(jīng)封裝了停止 requestAnimaitonFrame 動畫的方法,就是 cancelRequestAnimationFrame ,參數(shù)是requestID。

var cancelAnimationFrame = function() {   

  return window.cancelAnimationFrame || window.webkitCancelAnimationFrame || window.mozCancelAnimationFrame || function(id) {   

    clearTimeout(id);   

  };  

}();

當(dāng)然,緩動并不僅僅適用于運(yùn)動,它還可以應(yīng)用很多屬性:


(1)旋轉(zhuǎn)

定義起始角度:

var rotation = 0;

var targetRotation = 360;

然后緩動:

rotation += (targetRotation - rotation) * easing;

object.rotation = rotation * Math.PI / 180;

別忘了弧度與角度的轉(zhuǎn)換。


(2)透明度

設(shè)置起始透明度

var alpha = 0;

var targetAlpha = 1;

設(shè)置緩動:

alpha += (targetAlpha - alpha) * easing;

object.color = 'rgba(255,0,0,' + alpha + ')';


2、彈動

前面提到過,在彈動中,物體的 加速度 與它到目標(biāo)點的 距離 成正比。


現(xiàn)實中的彈動例子:在橡皮筋的一頭系上一個小球(懸空,靜止時的點就是目標(biāo)點),另一頭固定起來。當(dāng)我們用力(力足夠大)去拉小球然后松開,我們會看到小球反復(fù)的上下運(yùn)動幾次后,速度逐漸慢下來,停在目標(biāo)點上。(沒玩過橡皮筋的,可以去實踐一下)


2.1 一維坐標(biāo)上的彈動

實現(xiàn)彈動的代碼和緩動類似,只不過將速度換成了加速度(spring)。

var spring = 0.1;

var targetX = canvas.width / 2;

var vx = 0;

計算小球到目標(biāo)點的距離:

var dx = targetX - ball.x;

計算加速度,與距離是成比例的:

var ax = dx * spring;

將加速度加在速度上,然后添加到小球的位置上:

vx += ax;

ball.x += vx;

我們先模擬一下整個彈動過程,假設(shè)小球的x是0,vx也是0,目標(biāo)點的x是100,spring變量的值為0.1:

  • 用距離(100)乘以spring,得到10,將它加在vx上,vx變?yōu)?0,把vx加在小球的位置上,小球的x為10
  • 下一幀,距離(100-10)為90,加速度為90乘以0.1,等于9,加在vx上,vx就變?yōu)?9,小球的x變?yōu)榱?9
  • 再下一幀,距離是71,加速度是7.1,vx是26.1,小球的x為55.1


重復(fù)幾次后,隨著小球一幀一幀的靠近目標(biāo),加速度變得越來越小,速度越來越快,雖然增加的幅度在減小,但還是在增加。


當(dāng)小球越過了目標(biāo)點,到底了x軸上的117點時,與目標(biāo)點的距離是-17(100-117)了,也就是加速度會是-1.7,當(dāng)速度加上這個加速度時,小球就會減速運(yùn)動。


這就是彈動的過程。


看看實例(目標(biāo)點定在canvas的中心點,相當(dāng)于將球從中心點拉到左邊,然后松開):



上面的例子中,小球是不是有種被彈簧拉扯的效果,但是,由于小球的擺動幅度不變,它現(xiàn)在貌似停不下來,這不科學(xué),現(xiàn)實中,它的擺動幅度應(yīng)該是越來越?。ㄓ捎谧枇Γ瑥梽拥脑絹碓铰钡酵O聛?,所以為了更真實,我們應(yīng)該給它添加一個摩擦力friction:

var friction = 0.95;

然后改變速度:

vx += ax;

vx *= friction;

ball.x += vx;

當(dāng)小球停止時,我們就不需去執(zhí)行動畫了,所以我們還需要判斷是否停止:

if(Math.abs(vx) < 0.001){

  vx += ax;  

  vx *= friction;  

  ball.x += vx;

};

注意:當(dāng)你的初始速度vx為0時,這樣是無法進(jìn)入彈動的,對我來說,我會加入一個變量判斷是否開始彈動:

var isBegin = false;

if(!isBegin || Math.abs(vx) < 0.001){

  vx += ax;     

  vx *= friction;     

  ball.x += vx;

  isBegin = true;

};


2.2 二維坐標(biāo)上的彈動

二維坐標(biāo)上的彈動與一維坐標(biāo)上的彈動并沒有大區(qū)別,只不過前者多了y軸上的彈動。

初始化變量:

var vx = 0;   

var ax = 0;   

var vy = 0;   

var ay = 0;

var dx = 0;

var dy = 0;

設(shè)置x、y軸上的彈動:

if(Math.abs(vx) > 0.001){

  dx = targetX - ball.x;

  ax = dx * spring;   

  vx += ax;   

  vx *= friction;   

  ball.x += vx;

  dy = targetY - ball.y;   

  ay = dy * spring;   

  vy += ay;   

  vy *= friction;   

  ball.y += vy;   

};

例子(將canvas的中心點作為目標(biāo)點,相當(dāng)于一開始將球從中心點拉到左上角,然后松開):



上面的例子依舊是一個直線彈動,你可以試試將vx或vy的初始值增大一點,設(shè)為50,會有意想不到的動畫。


2.3  向移動的目標(biāo)點彈動

在緩動中也說過,目標(biāo)點不一定是固定,而對于彈動也一樣,目標(biāo)點可以是移動的,只需在每一幀改變目標(biāo)點的坐標(biāo)值即可,比如:鼠標(biāo)坐標(biāo)是目標(biāo)點:

dx = targetX - ball.x;

dy = targetY - ball.y;


/*改成如下*/

dx = mouse.x - ball.x;  

dy = mouse.y - ball.y;


2.4 繪制彈簧

在上面的幾個例子中,雖然有了彈簧的效果,可是始終還是沒看到橡皮筋所在,所以我們有必要來將橡皮筋繪畫出來:

ctx.beginPath();

ctx.moveTo(ball.x,ball.y);

ctx.lineTo(mouse.x,mouse.y);

ctx.stroke();

實例:



為了更真實,你還可以加上重力加速度:

var gravity = 2;


vy += gravity;

注意:在物理學(xué)中,重力是一個常數(shù),只由你所在星球的質(zhì)量來決定的。理論上,應(yīng)該保持gravity值不變,比如0.5,然后給物體增加一個mass(質(zhì)量)屬性,比如10,然后用mass乘以gravity得到5(依舊用gravity變量表示)。


2.5 鏈?zhǔn)綇梽?/b>

鏈?zhǔn)竭\(yùn)動是指物體A以物體B為目標(biāo)點,物體B又以物體C為目標(biāo)點,諸如此類的運(yùn)動。


看看例子,然后再來分析:


在上面的例子中,我們創(chuàng)建了四個球,每個球都有自己的屬性 vx 和 vy ,初始為0。在動畫函數(shù) animation 里,我們使用Array.forEach()方法來繪制每一個球,然后連線。在 connect 方法中,你可以看到第一個球的目標(biāo)點是鼠標(biāo)位置,剩余的球都是以上一個球(balles[i-1])的坐標(biāo)位置為目標(biāo)點來彈動。


我還給球添加了重力:

ball.vy += gravity;

運(yùn)動結(jié)束時,四個球會連成一串。


2.6 目標(biāo)偏移量

在上面的所有例子中,我們使用的都是模擬橡皮筋,如果我們模擬的是一個彈性金屬材料制作的彈簧會怎樣呢?是不是球還可以這樣自由的運(yùn)動呢?


答案是否定,在現(xiàn)實中,你無法讓物體頂著彈簧從一頭運(yùn)動到另一頭,還不明白?看下圖:


假設(shè)上面的圖中連接球和固定點是金屬彈簧,那么球是永遠(yuǎn)都到不了固定點的位置的,因為彈簧是有體積的,會把球擋住,而且一旦彈簧收縮到它正常的長度,它就不會對小球施加拉力了,所以,真正的目標(biāo)點,其實是彈簧處于松弛(拉伸)狀態(tài)時,系著小球那一端的那個點(這個點是變化的)。


那如何確定目標(biāo)點呢?


其實,從我上面的圖你就應(yīng)該想到,要用三角函數(shù),因為我們知道球的位置、固定點的位置,那我們就可以獲得球與固定點之間的夾角 θ ,當(dāng)然,我們還需要定義一個彈簧的長度(springLength),比如:100。


計算目標(biāo)點的代碼如下:

dx = ball.x - fixedX;

dy = ball.y -fixedY;

angle = Math.atan2(dy,dx);

targetX = fixedX + Math.cos(angle) * springLength;

targetY = fixedY + Math.sin(angle) * springLength;


又到了例子時刻(以canvas的中心點為固定點,彈簧長度為100,小球可拖動):


試過上面例子了嗎?我們再來看看上面的圖:


圖中的A點相當(dāng)于例子中的固定點(也就是canvas的中心點),B點是彈簧(無壓縮無拉伸)正常情況下的位置(也是彈動的目標(biāo)點),C點就是你拖動小球然后松開鼠標(biāo)的位置,那么AB之間的距離就是彈簧的長度100,而BC之間的距離就是小球彈動的距離了,同時,基于直角三角形,我們也很容易求得 θ 的值。


我們還定義了一個 getBound() 方法,傳入球?qū)ο螅祷匾粋€矩形對象,也就是球的矩形邊界。


例子的部分代碼:

dx = ballA.x - mouse.x;   

dy = ballA.y - mouse.y;   

angle = Math.atan2(dy, dx); // 獲取鼠標(biāo)與球之間的夾角θ   

//計算目標(biāo)點坐標(biāo)   

targetX = mouse.x + Math.cos(angle) * springLength;   

targetY = mouse.y + Math.sin(angle) * springLength;   

ballA.vx += (targetX - ballA.x) * spring;   

ballA.vy += (targetY - ballA.y) * spring;   

ballA.vx *= friction;   

ballA.vy *= friction;   

ballA.x += ballA.vx;   

ballA.y += ballA.vy;


2.7 用彈簧連接多個物體

我們還可以用彈簧連接多個物體,先從連接兩個物體開始,讓它們互相向?qū)Ψ綇梽?,移動其中一個,另一個就要跟隨彈動過去:


上例子:


在上面的例子中,我們創(chuàng)建了兩個Ball實例 ball0 和 ball1 ,都是可拖動的,ball0向ball1彈動,ball1向ball0彈動,而且它們之間有一定的偏移量,兩者用彈簧連接。


 springTo() 方法接受兩個參數(shù),第一個參數(shù)是移動物體,第二個參數(shù)是目標(biāo)點。還要引入兩個變量: ball0_dragging 和 ball1_dragging ,作為是否拖動小球的標(biāo)志。

if(!ball0_dragging) {   

  springTo(ball0, ball1);   

};   

if(!ball1_dragging) {   

  springTo(ball1, ball0);   

};


下面讓我們加入第三個球ball2:



總結(jié)

本章主要介紹了兩個比例運(yùn)動:緩動彈動

  • 緩動是指 速度 與 距離 成正比(物體離目標(biāo)越遠(yuǎn),物體運(yùn)動的速度越快,當(dāng)物體運(yùn)動到很接近目標(biāo)點時,物體幾乎就停下來了);
  • 彈動是指 加速度 與 距離 成正比(物體離目標(biāo)點越遠(yuǎn),加速度就快速增大,當(dāng)物體很接近目標(biāo)點時,加速度變得很小,但它還是在加速;當(dāng)它越過目標(biāo)點之后,隨著距離的變大,反向加速度也隨之變大,就會把它拉回了,最終在摩擦力的作用下停住。)


附錄


重要公式:


(1)簡單緩動

dx = targetX - object.x;

dy = targetY - object.y;

vx = dx * easing;

vy = dy * easing;

object.x += vx;

object.y += vy;

可精簡:

vx = (targetX - object.x) * easing;

vy = (targetY - object.y) * easing;

object.x += vx;

object.y += vy;

再精簡:

object.x += (targetX - object.x) * easing;

object.y += (targetY - object.y) * easing;


(2)簡單彈動

ax = (targetX - object.x) * spring;

ay = (targetY - object.y) * spring;

vx += ax;

vy += ay;

vx *= friction;

vy *= friction;

object.x += vx;

object.y += vy;

可精簡:

vx += (targetX - object.x) * spring;

vy += (targetY - object.y) * spring;

vx *= friction;

vy *= friction;

object.x += vx;

object.y += vy;

再精簡:

vx += (targetX - object.x) * spring;

vy += (targetY - object.y) * spring;

object.x += (vx *= friction);

object.y += (vy *= friction);


(3)有偏移的彈動

dx = object.x - fixedX;

dy = object.y - fixedY;

targetX = fixedX + Math.cos(angle) * springLength;

targetY = fixedY + Math.sin(angle) * springLength;


下一章:碰撞檢測


以上內(nèi)容是否對您有幫助:
在線筆記
App下載
App下載

掃描二維碼

下載編程獅App

公眾號
微信公眾號

編程獅公眾號