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;
關(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,然后將它每次移動距離的一半,過程如下:
看到?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:
重復(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)動:緩動和彈動
附錄
重要公式:
(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;
下一章:碰撞檢測
更多建議: