canvas動(dòng)畫包教不包會(huì):緩動(dòng)與彈動(dòng)

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

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

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

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

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

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

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

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

緩動(dòng)的整個(gè)過程并不復(fù)雜,我們需要知道距離(物體與目標(biāo)點(diǎn)(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;

最終緩動(dòng)公式:

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)用

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


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


這里還有一個(gè)關(guān)鍵性問題:何時(shí)停止緩動(dòng)


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

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

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

}

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

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

這是為什么呢?

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


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

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

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


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


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

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

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


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

distance = Math.abs(dx)


if(distance < 1){

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

  cancelAnimationFrame(requestID);

}

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


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

var cancelAnimationFrame = function() {   

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

    clearTimeout(id);   

  };  

}();

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


(1)旋轉(zhuǎn)

定義起始角度:

var rotation = 0;

var targetRotation = 360;

然后緩動(dòng):

rotation += (targetRotation - rotation) * easing;

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

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


(2)透明度

設(shè)置起始透明度

var alpha = 0;

var targetAlpha = 1;

設(shè)置緩動(dòng):

alpha += (targetAlpha - alpha) * easing;

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


2、彈動(dòng)

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


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


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

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

var spring = 0.1;

var targetX = canvas.width / 2;

var vx = 0;

計(jì)算小球到目標(biāo)點(diǎn)的距離:

var dx = targetX - ball.x;

計(jì)算加速度,與距離是成比例的:

var ax = dx * spring;

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

vx += ax;

ball.x += vx;

我們先模擬一下整個(gè)彈動(dòng)過程,假設(shè)小球的x是0,vx也是0,目標(biāo)點(diǎn)的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)點(diǎn),到底了x軸上的117點(diǎn)時(shí),與目標(biāo)點(diǎn)的距離是-17(100-117)了,也就是加速度會(huì)是-1.7,當(dāng)速度加上這個(gè)加速度時(shí),小球就會(huì)減速運(yùn)動(dòng)。


這就是彈動(dòng)的過程。


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



上面的例子中,小球是不是有種被彈簧拉扯的效果,但是,由于小球的擺動(dòng)幅度不變,它現(xiàn)在貌似停不下來,這不科學(xué),現(xiàn)實(shí)中,它的擺動(dòng)幅度應(yīng)該是越來越小(由于阻力),彈動(dòng)的越來越慢,直到停下來,所以為了更真實(shí),我們應(yīng)該給它添加一個(gè)摩擦力friction:

var friction = 0.95;

然后改變速度:

vx += ax;

vx *= friction;

ball.x += vx;

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

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

  vx += ax;  

  vx *= friction;  

  ball.x += vx;

};

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

var isBegin = false;

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

  vx += ax;     

  vx *= friction;     

  ball.x += vx;

  isBegin = true;

};


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

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

初始化變量:

var vx = 0;   

var ax = 0;   

var vy = 0;   

var ay = 0;

var dx = 0;

var dy = 0;

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

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的中心點(diǎn)作為目標(biāo)點(diǎn),相當(dāng)于一開始將球從中心點(diǎn)拉到左上角,然后松開):



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


2.3  向移動(dòng)的目標(biāo)點(diǎn)彈動(dòng)

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

dx = targetX - ball.x;

dy = targetY - ball.y;


/*改成如下*/

dx = mouse.x - ball.x;  

dy = mouse.y - ball.y;


2.4 繪制彈簧

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

ctx.beginPath();

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

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

ctx.stroke();

實(shí)例:



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

var gravity = 2;


vy += gravity;

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


2.5 鏈?zhǔn)綇梽?dòng)

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


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


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


我還給球添加了重力:

ball.vy += gravity;

運(yùn)動(dòng)結(jié)束時(shí),四個(gè)球會(huì)連成一串。


2.6 目標(biāo)偏移量

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


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


假設(shè)上面的圖中連接球和固定點(diǎn)是金屬?gòu)椈桑敲辞蚴怯肋h(yuǎn)都到不了固定點(diǎn)的位置的,因?yàn)閺椈墒怯畜w積的,會(huì)把球擋住,而且一旦彈簧收縮到它正常的長(zhǎng)度,它就不會(huì)對(duì)小球施加拉力了,所以,真正的目標(biāo)點(diǎn),其實(shí)是彈簧處于松弛(拉伸)狀態(tài)時(shí),系著小球那一端的那個(gè)點(diǎn)(這個(gè)點(diǎn)是變化的)。


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


其實(shí),從我上面的圖你就應(yīng)該想到,要用三角函數(shù),因?yàn)槲覀冎狼虻奈恢谩⒐潭c(diǎn)的位置,那我們就可以獲得球與固定點(diǎn)之間的夾角 θ ,當(dāng)然,我們還需要定義一個(gè)彈簧的長(zhǎng)度(springLength),比如:100。


計(jì)算目標(biāo)點(diǎn)的代碼如下:

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;


又到了例子時(shí)刻(以canvas的中心點(diǎn)為固定點(diǎn),彈簧長(zhǎng)度為100,小球可拖動(dòng)):


試過上面例子了嗎?我們?cè)賮砜纯瓷厦娴膱D:


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


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


例子的部分代碼:

dx = ballA.x - mouse.x;   

dy = ballA.y - mouse.y;   

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

//計(jì)算目標(biāo)點(diǎn)坐標(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 用彈簧連接多個(gè)物體

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


上例子:


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


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

if(!ball0_dragging) {   

  springTo(ball0, ball1);   

};   

if(!ball1_dragging) {   

  springTo(ball1, ball0);   

};


下面讓我們加入第三個(gè)球ball2:



總結(jié)

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

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


附錄


重要公式:


(1)簡(jiǎn)單緩動(dòng)

dx = targetX - object.x;

dy = targetY - object.y;

vx = dx * easing;

vy = dy * easing;

object.x += vx;

object.y += vy;

可精簡(jiǎn):

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

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

object.x += vx;

object.y += vy;

再精簡(jiǎn):

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

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


(2)簡(jiǎn)單彈動(dòng)

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

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

vx += ax;

vy += ay;

vx *= friction;

vy *= friction;

object.x += vx;

object.y += vy;

可精簡(jiǎn):

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

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

vx *= friction;

vy *= friction;

object.x += vx;

object.y += vy;

再精簡(jiǎn):

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

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

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

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


(3)有偏移的彈動(dòng)

dx = object.x - fixedX;

dy = object.y - fixedY;

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

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


下一章:碰撞檢測(cè)


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

掃描二維碼

下載編程獅App

公眾號(hào)
微信公眾號(hào)

編程獅公眾號(hào)