## 緩動與彈動
這一章主要講解緩動(比例速度)和彈動(比例加速度)。
**1、比例運動**
比例運動 是指運動與距離成比例的運動。
緩動和彈動都是比例運動,兩者關系緊密,都是將對象從已有位置移動到目標位置的方法。緩動是指物體滑動到目標點就停下來了。彈動是指物體來回地反彈一會兒,最終停在目標點的運動。
兩者的共同點:
- 有一個目標點
- 確定物體到目標點的距離
- 運動與距離是成正比的----距離越遠,運動的程度越大
兩者的不同點:
- 運動和距離成正比的方式不一樣。緩動是指 速度 與 距離 成正比(物體離目標越遠,物體運動的速度越快,當物體運動到很接近目標點時,物體幾乎就停下來了);而彈動是指 加速度 與 距離 成正比(物體離目標點越遠,加速度就快速增大,當物體很接近目標點時,加速度變得很小,但它還是在加速;當它越過目標點之后,隨著距離的變大,反向加速度也隨之變大,就會把它拉回了,最終在摩擦力的作用下停住。)
**2、緩動**
緩動的類型不止一種,我們可以“緩入”(ease in)到一個位置,也可以從一個位置“緩出”(ease out)。
在現實生活中,相信大家都坐過公交(自動過濾土豪),在寬敞的馬路上時,公交會高速前進,特別是車少的道路,司機會開的盡可能快(限速之內),當快要達到一個站點時,司機就會適當的減速。當公交還有幾米就要停下來的時候,速度已經很慢很慢了。這就是一種緩動。
**如何實現緩動呢?**
一般來說,我們會如下處理:
- 為運動確定一個小于1且大于0的小數作為比例系數(easing)
- 確定目標點
- 計算物體與目標點的距離
- 計算速度,速度=距離 * 比例系數
- 用當前位置加上速度來計算新的位置
- 不斷重復第3步到第5步,直到物體到達目標點

緩動的整個過程并不復雜,我們需要知道距離(物體與目標點(target)之間,變化值)、比例系數(easing,速度除以距離)。
```
dx = targetX - ball.x;
dy = targetY - ball.y;
easing = vx / dx; => vx = dx * easing;
easing = vy / dy; => vy = dy * easing;
```
根據《速度與加速度》那一章的公式:
```
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;
```
實例:canvas-demo/easing.html
關鍵代碼:
```
var easing = 0.05;
var targetX = canvas.width - 10;
var targetY = canvas.height - 10;
```
在上面的例子中,我們將比例系數設為0.05,用變量easing表示,然后在循環中調用下面的代碼:
```
ball.x += (targetX- ball.x)*easing; //每次循環中調用
```
這樣簡單的處理,就能實現剎車模式,這就是緩動的一種效果,你可以改變easing看看。
上面的例子中的目標點是canvas邊界,其實,目標點是可以 變動 的,因為我們每次都會重新計算距離,所以只須在播放每一幀的時候知道目標點的位置,然后就可以計算距離和速度了。比如:將鼠標位置(mouse.x和mouse.y)作為目標點,你可以試試,會發現鼠標里的越遠,小球就運動的越快。
這里還有一個關鍵性問題:**何時停止緩動**
不是到達目標點就停止緩動嗎?估計這是你看到這的第一想法,你還可能立即想到下面判斷公式:
```
if(ball.x === targetX && ball.y === targetY){
//到達目標點
}
```
這是理論上的判斷,但是從數學的角度來看,下面的公式永遠不會相等:
```
(ball.x + (targetX - ball.x) * easing) !== targetX
```
這是為什么呢?
這就涉及了 芝諾餑論 ,簡單的理解是這樣:為了把一個物體從A點移到B點,就必須把它先移到到A和B的中間點C,然后再移到C和B的中間點,然后再折半,不斷地重復下去,每次移到到物體到距離目標點的一半,這樣就會進入無窮循環下去,物體永遠不會到達目標點。
我們來看看數學例子:物體從0的位置,要將它移到100,比例系數easing設為0.,5,然后將它每次移動距離的一半,過程如下:
- 從原點開始,在第一幀后,它移到到50
- 在第二幀后,移動到75
- 在第三幀后,移動到87.5
- 就這樣循環下去,物體位置變化是93.75、96.875等,經過20幀后,它的位置是99.999809265
看到沒有,它會離目標點越來越近,可是理論上是永遠不會到達目標點的,所以上面的判斷公式是永遠不會返回true的。
但畢竟肉眼是無法分辨這么精確的位置變化的,有時候當ball.x 等于99的時候,我們在canvas上看就已經是到達終點了,所以這就產生了一個問題:多近才是足夠近呢?
這就需要我們人為的指定一個特定值,判斷物體到目標點的距離是否小于特定值,如果小于特定值,那我們就認為它到達終點了。
```
/*二維坐標*/
distance = Math.sqrt(dx * dx + dy * dy);
/*一維坐標*/
distance = Math.abs(dx)
if(distance < 1){
console.log('到達終點');
cancelAnimationFrame(requestID);
}
```
一般采取是否小于1來判斷是否到達目標點,是為了停止動畫,避免資源的浪費。
在tool.js工具類中,我們已經封裝了停止` requestAnimaitonFrame` 動畫的方法,就是 `cancelRequestAnimationFrame` ,參數是requestID。
```
var cancelAnimationFrame = function() {
return window.cancelAnimationFrame || window.webkitCancelAnimationFrame || window.mozCancelAnimationFrame || function(id) {
clearTimeout(id);
};
}();
```
當然,緩動并不僅僅適用于運動,它還可以應用很多屬性:
**(1)旋轉**
定義起始角度:
```
var rotation = 0;
var targetRotation = 360;
```
然后緩動:
```
rotation += (targetRotation - rotation) * easing;
object.rotation = rotation * Math.PI / 180;
```
別忘了弧度與角度的轉換。
**(2)透明度**
設置起始透明度
```
var alpha = 0;
var targetAlpha = 1;
```
設置緩動:
```
alpha += (targetAlpha - alpha) * easing;
object.color = 'rgba(255,0,0,' + alpha + ')';
```
**2、彈動**
前面提到過,在彈動中,物體的 加速度 與它到目標點的 距離 成正比。
現實中的彈動例子:在橡皮筋的一頭系上一個小球(懸空,靜止時的點就是目標點),另一頭固定起來。當我們用力(力足夠大)去拉小球然后松開,我們會看到小球反復的上下運動幾次后,速度逐漸慢下來,停在目標點上。(沒玩過橡皮筋的,可以去實踐一下)
**2.1 一維坐標上的彈動**
實現彈動的代碼和緩動類似,只不過將速度換成了加速度(spring)。
```
var spring = 0.1;
var targetX = canvas.width / 2;
var vx = 0;
```
計算小球到目標點的距離:
```
var dx = targetX - ball.x;
```
計算加速度,與距離是成比例的:
```
var ax = dx * spring;
```
將加速度加在速度上,然后添加到小球的位置上:
```
vx += ax;
ball.x += vx;
```
我們先模擬一下整個彈動過程,假設小球的x是0,vx也是0,目標點的x是100,spring變量的值為0.1:
- 用距離(100)乘以spring,得到10,將它加在vx上,vx變為10,把vx加在小球的位置上,小球的x為10
- 下一幀,距離(100-10)為90,加速度為90乘以0.1,等于9,加在vx上,vx就變為19,小球的x變為了29
- 再下一幀,距離是71,加速度是7.1,vx是26.1,小球的x為55.1
重復幾次后,隨著小球一幀一幀的靠近目標,加速度變得越來越小,速度越來越快,雖然增加的幅度在減小,但還是在增加。
當小球越過了目標點,到底了x軸上的117點時,與目標點的距離是-17(100-117)了,也就是加速度會是-1.7,當速度加上這個加速度時,小球就會減速運動。
這就是彈動的過程。
看看實例(目標點定在canvas的中心點,相當于將球從中心點拉到左邊,然后松開):canvas-demo/spring.html
上面的例子中,小球是不是有種被彈簧拉扯的效果,但是,由于小球的擺動幅度不變,它現在貌似停不下來,這不科學,現實中,它的擺動幅度應該是越來越小(由于阻力),彈動的越來越慢,直到停下來,所以為了更真實,我們應該給它添加一個摩擦力friction:
```
var friction = 0.95;
```
然后改變速度:
```
vx += ax;
vx *= friction;
ball.x += vx;
```
當小球停止時,我們就不需去執行動畫了,所以我們還需要判斷是否停止:
```
if(Math.abs(vx) < 0.001){
vx += ax;
vx *= friction;
ball.x += vx;
};
```
注意:當你的初始速度vx為0時,這樣是無法進入彈動的,對我來說,我會加入一個變量判斷是否開始彈動:
```
var isBegin = false;
if(!isBegin || Math.abs(vx) < 0.001){
vx += ax;
vx *= friction;
ball.x += vx;
isBegin = true;
};
```
**2.2 二維坐標上的彈動**
二維坐標上的彈動與一維坐標上的彈動并沒有大區別,只不過前者多了y軸上的彈動。
初始化變量:
```
var vx = 0;
var ax = 0;
var vy = 0;
var ay = 0;
var dx = 0;
var dy = 0;
```
設置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的中心點作為目標點,相當于一開始將球從中心點拉到左上角,然后松開):canvas-demo/spring2.html
上面的例子依舊是一個直線彈動,你可以試試將vx或vy的初始值增大一點,設為50,會有意想不到的動畫。
**2.3 向移動的目標點彈動**
在緩動中也說過,目標點不一定是固定,而對于彈動也一樣,目標點可以是移動的,只需在每一幀改變目標點的坐標值即可,比如:鼠標坐標是目標點:
```
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();
```
實例:canvas-demo/spring3.html
為了更真實,你還可以加上重力加速度:
```
var gravity = 2;
vy += gravity;
```
注意:在物理學中,重力是一個常數,只由你所在星球的質量來決定的。理論上,應該保持gravity值不變,比如0.5,然后給物體增加一個mass(質量)屬性,比如10,然后用mass乘以gravity得到5(依舊用gravity變量表示)。
**2.5 鏈式彈動**
鏈式運動是指物體A以物體B為目標點,物體B又以物體C為目標點,諸如此類的運動。
看看例子,然后再來分析:canvas-demo/spring4.html
在上面的例子中,我們創建了四個球,每個球都有自己的屬性 vx 和 vy ,初始為0。在動畫函數 animation 里,我們使用Array.forEach()方法來繪制每一個球,然后連線。在 connect 方法中,你可以看到第一個球的目標點是鼠標位置,剩余的球都是以上一個球(balles[i-1])的坐標位置為目標點來彈動。
我還給球添加了重力:
```
ball.vy += gravity;
```
運動結束時,四個球會連成一串。
**2.6 目標偏移量**
在上面的所有例子中,我們使用的都是模擬橡皮筋,如果我們模擬的是一個彈性金屬材料制作的彈簧會怎樣呢?是不是球還可以這樣自由的運動呢?
答案是否定,在現實中,你無法讓物體頂著彈簧從一頭運動到另一頭,還不明白?看下圖:

假設上面的圖中連接球和固定點是金屬彈簧,那么球是永遠都到不了固定點的位置的,因為彈簧是有體積的,會把球擋住,而且一旦彈簧收縮到它正常的長度,它就不會對小球施加拉力了,所以,真正的目標點,其實是彈簧處于松弛(拉伸)狀態時,系著小球那一端的那個點(這個點是變化的)。
那如何確定目標點呢?
其實,從我上面的圖你就應該想到,要用三角函數,因為我們知道球的位置、固定點的位置,那我們就可以獲得球與固定點之間的夾角 θ ,當然,我們還需要定義一個彈簧的長度(springLength),比如:100。
計算目標點的代碼如下:
```
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,小球可拖動):canvas-demo/spring5.html
試過上面例子了嗎?我們再來看看上面的圖:

圖中的A點相當于例子中的固定點(也就是canvas的中心點),B點是彈簧(無壓縮無拉伸)正常情況下的位置(也是彈動的目標點),C點就是你拖動小球然后松開鼠標的位置,那么AB之間的距離就是彈簧的長度100,而BC之間的距離就是小球彈動的距離了,同時,基于直角三角形,我們也很容易求得 θ 的值。
我們還定義了一個 getBound() 方法,傳入球對象,返回一個矩形對象,也就是球的矩形邊界。
例子的部分代碼:
```
dx = ballA.x - mouse.x;
dy = ballA.y - mouse.y;
angle = Math.atan2(dy, dx); // 獲取鼠標與球之間的夾角θ
//計算目標點坐標
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 用彈簧連接多個物體**
我們還可以用彈簧連接多個物體,先從連接兩個物體開始,讓它們互相向對方彈動,移動其中一個,另一個就要跟隨彈動過去:
上例子:canvas-demo/spring6.html
在上面的例子中,我們創建了兩個Ball實例 ball0 和 ball1 ,都是可拖動的,ball0向ball1彈動,ball1向ball0彈動,而且它們之間有一定的偏移量,兩者用彈簧連接。
springTo() 方法接受兩個參數,第一個參數是移動物體,第二個參數是目標點。還要引入兩個變量: ball0_dragging 和 ball1_dragging ,作為是否拖動小球的標志。
```
if(!ball0_dragging) {
springTo(ball0, ball1);
};
if(!ball1_dragging) {
springTo(ball1, ball0);
};
```
下面讓我們加入第三個球ball2:canvas-demo/spring7.html
**總結**
本章主要介紹了兩個比例運動:緩動和彈動
緩動是指 速度 與 距離 成正比(物體離目標越遠,物體運動的速度越快,當物體運動到很接近目標點時,物體幾乎就停下來了);
彈動是指 加速度 與 距離 成正比(物體離目標點越遠,加速度就快速增大,當物體很接近目標點時,加速度變得很小,但它還是在加速;當它越過目標點之后,隨著距離的變大,反向加速度也隨之變大,就會把它拉回了,最終在摩擦力的作用下停住。)
**附錄**
**重要公式:**
(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;
```