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)用
這樣簡(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)距離的一半,過程如下:
看到?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:
重復(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)
附錄
重要公式:
(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è)
更多建議: