vr = 0.1; //角度增量
angle = 0;
radius = 100;
centerX = 0;
centerY = 0;
object.x = centerX + Math.cos(angle) * radius;
object.y = centerY + Math.sin(angle) * radius;
angle += vr;
var dx = ball.x - centerX;
var dy = ball.y - centerY;
var angle = Math.atan2(dy,dx);
var radius = Math.sqrt(dx * dx + dy * dy);
(x,y)
圍繞著一個(gè)點(diǎn)(x2,y2)
旋轉(zhuǎn),而我們只知道物體的坐標(biāo)和點(diǎn)的坐標(biāo),那如何計(jì)算旋轉(zhuǎn)后物體的坐標(biāo)呢?下面有一個(gè)很適合這種場景的公式:x1 = (x - x2) * cos(rotation) - (y - y2) * sin(rotation);
y1 = (y - y2) * cos(rotation) + (x - x2) * sin(rotation);
(x-x2)
、(y-y2)
是物體相對(duì)于旋轉(zhuǎn)點(diǎn)的坐標(biāo),rotation
是旋轉(zhuǎn)角度(旋轉(zhuǎn)量,指當(dāng)前角度和旋轉(zhuǎn)后的角度的差值),x1
、y1
是物體旋轉(zhuǎn)后的位置坐標(biāo)。/*物體當(dāng)前的坐標(biāo)*/
x = radius * cos(angle);
y = radius * sin(angle);
/*物體旋轉(zhuǎn)rotation后的坐標(biāo)*/
x1 = radius * cos(angle + rotation);
y1 = radius * sin(angle + rotation);
cos(a + b) = cos(a) * cos(b) - sin(a) * sin(b);
sin(a + b) = sin(a) * cos(b) + cos(a) * sin(b);
x1 = radius * cos(angle) * cos(rotation) - radius * sin(angle) *sin(rotation);
y1 = radius * sin(angle) * cos(rotation) + radius * cos(angle) * sin(rotation);
x1 = x * cos(rotation) - y * sin(rotation);
y1 = y * cos(rotation) + x * sin(rotation);
注意:這里的x、y是相對(duì)于旋轉(zhuǎn)點(diǎn)的x、y坐標(biāo),也就是上面的(x-x2)、(y-y2),而不是相對(duì)于坐標(biāo)系的坐標(biāo)。
使用這個(gè)公式,我們不需要知道起始角度和旋轉(zhuǎn)后的角度,只需要知道旋轉(zhuǎn)角度即可。
(1)旋轉(zhuǎn)單個(gè)物體
有了公式,當(dāng)然要實(shí)踐一下,我們先來試試旋轉(zhuǎn)單個(gè)物體
這里的vr依舊是0.05
,然后計(jì)算這個(gè)角度的正弦和余弦值,然后根據(jù)小球相對(duì)于中心點(diǎn)的位置計(jì)算出x1、y1,接著利用公式計(jì)算出小球旋轉(zhuǎn)后的坐標(biāo)。
sin = Math.sin(angle);
cos = Math.cos(angle);
var x1 = ball.x - centerX;
var y1 = ball.y - centerY;
ball.x = centerX + (x1 * cos - y1 * sin);
ball.y = centerY + (y1 * cos + x1 * sin);
還是要強(qiáng)制一句,這個(gè)公式傳入的x、y是物體相對(duì)于旋轉(zhuǎn)點(diǎn)的坐標(biāo),不是旋轉(zhuǎn)點(diǎn)的坐標(biāo),也不是物體的坐標(biāo)。
你可能會(huì)疑惑,這不是跟第一個(gè)例子的效果一樣嗎?為什么要用這個(gè)公式呢?不要急,接著看下面的旋轉(zhuǎn)多個(gè)物體,看完后你就會(huì)明白這條公式的好處了。
(2)旋轉(zhuǎn)多個(gè)物體
假如要旋轉(zhuǎn)多個(gè)物體,我們將小球保存在變量balles的數(shù)組中,旋轉(zhuǎn)代碼如下:
balles.forEach(function(ball){
var dx = ball.x - centerX;
var dy = ball.y - centerY;
var angle = Math.atan2(dy,dx);
var dist = Math.sqrt(dx * dx + dy * dy);
angle += vr;
ball.x = centerX + Math.cos(angle) * dist;
ball.y = centerY + Math.sin(angle) * dist;
});
使用高級(jí)坐標(biāo)旋轉(zhuǎn)是這樣的:
var cos = Math.cos(vr);
var sin = Math.sin(vr);
balles.forEach(function(ball){
var x1 = ball.x - centerX;
var y1 = ball.y - centerY;
var x2 = x1 * cos - y1 * sin;
var y2 = y2 * cos + x1 * sin;
ball.x = centerX + x2;
ball.y = centerY + y2;
});
我們來對(duì)比一下這兩種方式,在第一種方式中,每次循環(huán)都調(diào)用了4次Math函數(shù),也就是說,旋轉(zhuǎn)每一個(gè)小球都要調(diào)用4次Math函數(shù),而第二種方式,只調(diào)用了兩次Math函數(shù),而且都位于循環(huán)之外,不管增加多少小球,它們都只會(huì)執(zhí)行一次。
實(shí)例:
我們用鼠標(biāo)來控制多個(gè)球的旋轉(zhuǎn)速度,如果鼠標(biāo)位置在canvas的中央,那么它們都靜止不動(dòng),如果鼠標(biāo)向左移動(dòng),這些小球就沿逆時(shí)針方向旋轉(zhuǎn),如果向右移動(dòng),小球就沿順時(shí)針方法越轉(zhuǎn)越快。
2、斜面反彈
前面我們學(xué)習(xí)了如何讓物體反彈,不過都是基于垂直或水平的反彈面,如果是一個(gè)斜面,我們該如何反彈呢?
處理斜面反彈,我們要做的是:旋轉(zhuǎn)整個(gè)系統(tǒng)使反彈面水平,然后做反彈,最后再旋轉(zhuǎn)回來,這意味著反彈面、物體的坐標(biāo)位置和速度向量都發(fā)生了旋轉(zhuǎn)。
圖1是小球撞向斜面,向量箭頭表示小球的方向
圖2中,整個(gè)場景旋轉(zhuǎn)了,反彈面處于水平位置,就像前面碰撞示例中的底部障礙一樣。在這里,速度向量也隨著整個(gè)場景向右旋轉(zhuǎn)了。
圖3中,我們就可以實(shí)現(xiàn)反彈了,也就是改變y軸上的速度
圖4中,就是整個(gè)場景旋轉(zhuǎn)回到最初的角度。
什么,你還看不明白,那我再給你畫個(gè)圖吧:
斜面和小球的旋轉(zhuǎn)都是相對(duì)于(x,y)。
經(jīng)歷了上圖,你應(yīng)該明白,如果還不明白,請(qǐng)自己畫圖看看,畫出每一步。
2.1 旋轉(zhuǎn)起來
為了斜面反彈的真實(shí)性,我們需要?jiǎng)?chuàng)建一個(gè)斜面,在canvas中,我們只需畫一條斜線,這樣我們就可以看到小球在哪里反彈了。
相信畫直線對(duì)你來說不難,下面創(chuàng)建一個(gè)Line類:
function Line(x1, y1, x2, y2) {
this.x = 0;
this.y = 0;
this.x1 = (x1 === undefined) ? 0 : x1;
this.y1 = (y1 === undefined) ? 0 : y1;
this.x2 = (x2 === undefined) ? 0 : x2;
this.y2 = (y2 === undefined) ? 0 : y2;
this.rotation = 0;
this.scaleX = 1;
this.scaleY = 1;
this.lineWidth = 1;
};
/*繪制直線*/
Line.prototype.draw = function(context) {
context.save();
context.translate(this.x, this.y); //平移
context.rotate(this.rotation); // 旋轉(zhuǎn)
context.scale(this.scaleX, this.scaleY);
context.lineWidth = this.lineWidth;
context.beginPath();
context.moveTo(this.x1, this.y1);
context.lineTo(this.x2, this.y2);
context.closePath();
context.stroke();
context.restore();
};
先看實(shí)例(點(diǎn)擊一下按鈕看看):
在上面的例子中,我創(chuàng)建的小球是隨機(jī)位置的,不過都位于斜線的上方。
一開始,我們首先聲明ball、line、gravity和bounce,然后初始化ball和line的位置,接著計(jì)算直線旋轉(zhuǎn)角度的cos和sin值
line = new Line(0, 0, 300, 0);
line.x = 50;
line.y = 200;
line.rotation = (10 * Math.PI / 180); //設(shè)置線的傾斜角度
cos = Math.cos(line.rotation);
sin = Math.sin(line.rotation);
接下來,用小球的位置減去直線的位置(50,100),就會(huì)得到小球相對(duì)于直線的位置:
var x1 = ball.x - line.x;
var y1 = ball.y - line.y;
完成了上面這些,我們現(xiàn)在可以開始旋轉(zhuǎn),獲取旋轉(zhuǎn)后的位置和速度:
var x2 = x1 * cos + y1 * sin;
var y2 = y1 * cos - x1 * sin;
如果你夠仔細(xì),可能你也發(fā)現(xiàn)了,這里的代碼好像和坐標(biāo)旋轉(zhuǎn)公式有點(diǎn)區(qū)別:
x1 = x * cos(rotation) - y * sin(rotation);
y1 = y * cos(rotation) + x * sin(rotation);
加號(hào)變減號(hào),減號(hào)變加號(hào)了,寫錯(cuò)了嗎?其實(shí)沒有,這是因?yàn)楝F(xiàn)在直線的斜度是10,那要將它旋轉(zhuǎn)成水平的話,就不是旋轉(zhuǎn)10,而是-10才對(duì):
sin(-10) = - sin(10)
cos(-10) = cos(10)
當(dāng)你旋轉(zhuǎn)后獲得相對(duì)于直線的坐標(biāo)和速度后,你就可以使用位置x2
、y2
和速度vx1
、vy1
來執(zhí)行反彈了,根據(jù)什么來判斷球碰撞直線呢?用y2
,因?yàn)榇藭r(shí)y2
是相對(duì)直線的位置的,所以“底邊”就是line自己,也就是0,還要考慮小球的大小,需要判斷y2
是否大于0-ball.radius
:
if(y2 > -ball.radius) {
y2 = -ball.radius;
vy1 *= bounce;
};
最后,你還要將整個(gè)系統(tǒng)旋轉(zhuǎn)歸位,計(jì)算原始角度的正余弦值:
x1 = x2 * cos - y2 * sin;
y1 = y2 * cos + x2 * sin;
求得ball實(shí)例的絕對(duì)位置:
ball.x = line.x + x1;
ball.y = line.y + y1;
2.2 優(yōu)化代碼
在上面的例子中,有些代碼在反彈之前是沒必要執(zhí)行的,所以我們可以將它們放到if語句中:
if(y2 > -ball.radius) {
var x2 = x1 * cos + y1 * sin;
var vx1 = ball.vx * cos + ball.vy * sin;
var vy1 = ball.vy * cos - ball.vx * sin;
y2 = -ball.radius;
vy1 *= bounce;
//旋轉(zhuǎn)回來,計(jì)算坐標(biāo)和速度
x1 = x2 * cos - y2 * sin;
y1 = y2 * cos + x2 * sin;
ball.vx = vx1 * cos - vy1 * sin;
ball.vy = vy1 * cos + vx1 * sin;
ball.x = line.x + x1;
ball.y = line.y + y1;
};
2.3 修復(fù)“不從邊緣落下”的問題
如果你試過上面的例子,現(xiàn)在你也看到了,即使小球到了直線的邊緣,它還是會(huì)沿著直線方向滾動(dòng),這不科學(xué),原因在于我們是模擬,并不是真實(shí)的碰撞,小球并不知道線的起點(diǎn)和終點(diǎn)在哪里。
2.3.1 碰撞檢測
在前面的碰撞檢測中,我們介紹過一個(gè)方法tool.intersects()
,可用來檢測直線的邊界框是否與小球的邊界框重疊。
當(dāng)然,我們還需要獲得直線的邊界框,這里給Line類添加一個(gè)方法getBound:
Line.prototype.getBound = function() {
if(this.rotation === 0) {
var minX = Math.min(this.x1, this.x2);
var minY = Math.min(this.y1, this.y2);
var maxX = Math.max(this.x1, this.x2);
var maxY = Math.max(this.y1, this.y2);
return {
x: this.x + minX,
y: this.y + minY,
width: maxX - minX,
height: maxY - minY
};
} else {
//基于坐標(biāo)系原點(diǎn)旋轉(zhuǎn)
var sin = Math.sin(this.rotation);
var cos = Math.cos(this.rotation);
var x1r = cos * this.x1 + sin * this.y1;
var x2r = cos * this.x2 + sin * this.y2;
var y1r = cos * this.y1 + sin * this.x1;
var y2r = cos * this.y2 + sin * this.x2;
return {
x: this.x + Math.min(x1r, x2r),
y: this.y + Math.min(y1r, y2r),
width: Math.max(x1r, x2r) - Math.min(x1r, x2r),
height: Math.max(y1r, y2r) - Math.min(y1r, y2r)
};
}
};
返回一個(gè)包含有x
、y
、width
和height
屬性的矩形對(duì)象。
使用如下:
if(tool.intersects(ball.getBound(), line.getBound()){
}
還有一個(gè)更精確的方法。
2.3.2 邊界檢查
var bounds = line.getBound();
if(ball.x + ball.radius > bounds.x && ball.x - ball.radius <bounds.x + bounds.width){
//執(zhí)行反彈
}
如上代碼所示,如果小球的邊界框小于bounds.x
(左邊緣),或者大于bounds.x+bounds.width
(右邊緣),就說明它已經(jīng)從線段上掉落了。
注意:因?yàn)樾∏虻膱A心是中心點(diǎn),左邊框和上邊框就是圓心位置減去小球的半徑,有邊框和下邊框就是圓心位置加上小球的半徑。
2.4 多個(gè)斜面反彈
要實(shí)現(xiàn)多個(gè)斜面反彈其實(shí)也不難,只需要?jiǎng)?chuàng)建多個(gè)斜面并循環(huán)即可。
實(shí)例:
上面的例子中,我們已經(jīng)實(shí)現(xiàn)了多個(gè)斜面反彈,可似乎有一個(gè)問題,當(dāng)小球從第二個(gè)斜面掉落時(shí),并沒有掉落到第三個(gè)斜面上,而是在半空中就反彈回去了,這是為什么呢?下面我們就來修復(fù)這個(gè)問題。
2.5 修復(fù)“線下”的問題
在上面的檢測碰撞時(shí),首先要判斷小球是否在直線附近,然后進(jìn)行坐標(biāo)旋轉(zhuǎn),得到旋轉(zhuǎn)后的位置和速度,接著,判斷小球旋轉(zhuǎn)后的縱坐標(biāo)y2是否越過了直線,如果超過了,則執(zhí)行反彈。
if(y2 > -ball.radius){}
上面的代碼也是導(dǎo)致2.4中例子沒有掉落到下面的原因,因?yàn)楫?dāng)小球從第二個(gè)斜面掉落下,卻是落到了第一個(gè)斜面的下面,也就會(huì)觸發(fā)第一個(gè)斜面和小球的反彈,這不是我們想要的,如何解決呢?先看下圖:
左邊小球在y軸上的速度大于它與直線的相對(duì)距離,這表示它剛剛從直線上穿越下來;右邊小球的速度向量小于它與直線的相對(duì)距離,這表示,它在這一幀和上一幀都位于線下,因此它此時(shí)只是在線下運(yùn)動(dòng),所以我們需要的是在小球穿過直線的那一瞬間才執(zhí)行反彈。
也就是:比較vy1
和y2
,僅當(dāng)vy1
大于y2
時(shí)才執(zhí)行反彈:
if(y2 > -ball.radius && y2 < vy1) {}
看看修復(fù)后的例子:
總結(jié)
這一章,我們介紹了坐標(biāo)旋轉(zhuǎn)和斜面反彈,其中不遺余力的分析了坐標(biāo)旋轉(zhuǎn)公式,并且修復(fù)了“不從邊緣落下”和“線下”兩個(gè)問題,一定要掌握坐標(biāo)旋轉(zhuǎn),后面我們還將多處用到。
下一章:撞球物理
附錄
重要公式:
(1)坐標(biāo)旋轉(zhuǎn)
x1 = x * Math.cos(rotation) - y * Math.sin(rotation);
y1 = y * Math.cos(rotation) + x * Math.sin(rotation);
(2)反向坐標(biāo)旋轉(zhuǎn)
x1 = x * Math.cos(rotation) + y * Math.sin(rotation);
y1 = y * Math.cos(rotation) - x * Math.sin(rotation);
更多建議: