canvas動(dòng)畫包教不包會(huì):坐標(biāo)旋轉(zhuǎn)和斜面反彈

2018-06-19 15:25 更新
      坐標(biāo)旋轉(zhuǎn),顧名思義,就是說圍繞著某個(gè)點(diǎn)旋轉(zhuǎn)坐標(biāo)系。這一章就來介紹一下如何實(shí)現(xiàn)坐標(biāo)旋轉(zhuǎn)和坐標(biāo)旋轉(zhuǎn)的作用。
內(nèi)容如下:
  • 坐標(biāo)旋轉(zhuǎn)
  • 斜面反彈

1、坐標(biāo)旋轉(zhuǎn)

1.1 簡單旋轉(zhuǎn)
在前面的三角函數(shù)一章中的實(shí)例“指紅針”中,我們已經(jīng)使用過坐標(biāo)旋轉(zhuǎn)技術(shù)。只需一個(gè)中心點(diǎn),一個(gè)物體,還有半徑和角度(弧度制),通過增減這個(gè)角度,然后用基本的三角函數(shù)計(jì)算位置,就能使物體圍繞著中心點(diǎn)旋轉(zhuǎn)。
初始化參數(shù):

vr = 0.1;  //角度增量

angle = 0;

radius = 100;

centerX = 0;

centerY = 0;

在動(dòng)畫循環(huán)中做下列計(jì)算:

object.x = centerX + Math.cos(angle) * radius;

object.y = centerY + Math.sin(angle) * radius;

angle += vr;

實(shí)例:
每次旋轉(zhuǎn)角度vr設(shè)置為0.05,根據(jù)上面的公式計(jì)算小球旋轉(zhuǎn)后的位置。


如果只知道物體的位置和中心點(diǎn),如何做旋轉(zhuǎn)呢?其實(shí)也不難,我們只需根據(jù)兩個(gè)點(diǎn)來計(jì)算出當(dāng)前角度和半徑即可:

var dx = ball.x - centerX;

var dy = ball.y - centerY;

var angle = Math.atan2(dy,dx);

var radius = Math.sqrt(dx * dx + dy * dy);

得到角度和半徑,我們就可以像上面那樣旋轉(zhuǎn)了。

上面的方法比較適合單個(gè)物體旋轉(zhuǎn),對(duì)于多個(gè)物體的旋轉(zhuǎn),這種方法不是很高效,當(dāng)然,我們有更好的方法。

1.2 高級(jí)坐標(biāo)旋轉(zhuǎn)
如果物體(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);

我們可以認(rèn)為(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)。

注意:這里采取的依舊是弧度制。

這條公式是不是看的有點(diǎn)糊里糊涂的,不知道怎么來的,下面我們將介紹它是如何得出的。

先看圖:

/*物體當(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);

下面又來介紹一個(gè)兩個(gè)關(guān)于三角函數(shù)的數(shù)學(xué)公式了。
兩角之和的余弦值:

cos(a + b) = cos(a) * cos(b) - sin(a) * sin(b);

兩角之和的正弦值:

sin(a + b) = sin(a) * cos(b) + cos(a) * sin(b);

基于這兩條推導(dǎo)公式,我們將x1和y1的公式展開:

x1 = radius * cos(angle) * cos(rotation) - radius * sin(angle) *sin(rotation);

y1 = radius * sin(angle) * cos(rotation) + radius * cos(angle) * sin(rotation);

最后將x、y變量代入公式,就會(huì)得到最初那條公式:

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、widthheight屬性的矩形對(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í)行反彈。


也就是:比較vy1y2,僅當(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);


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

掃描二維碼

下載編程獅App

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

編程獅公眾號(hào)