【帶着canvas去流浪(8)】碰撞

示例代碼託管在:http://www.github.com/dashnowords/blogshtml

博客園地址:《大史住在大前端》原創博文目錄前端

華爲雲社區地址:【你要的前端打怪升級指南】java

通過前面章節相對枯燥的練習,相信你已經可以上手canvas的原生API了,那麼從這一節開始,咱們就開始接觸點好玩的東西——動畫。git

一. canvas的能力

若是你覺得canvas只能繪製圖表那真的就圖樣圖森破了,且不談webgl的繪圖上下文,單就2d空間的畫筆就能夠作不少有意思的事情,好比實現一些酷炫的動畫效果,好比作一些物理仿真,圖片濾鏡,直播彈幕,甚至作遊戲開發等等,畫面的變化大多依賴於canvas提供的像素操做能力,而動效幾乎都是靠canvas在短期內逐幀繪製而造成的,和電影的原理是同樣的。github

咱們知道javascript中和時間控制有關的函數setTimeout( ) 以及setInterval( )最終執行時的時間點並不許確,由於在事件隊列中會被其餘異步任務影響甚至直接阻塞,那麼在不斷重複的繪製中,就有可能會出現卡頓或者忽快忽慢;另外一方面,假設咱們使用的電腦顯示屏刷新率爲60幀/秒,也就是大約16.7ms重繪一次,那麼即時咱們在16.7ms時間內執行了不少次計算和繪製命令,實際上最終呈現出的也只是最後一次結果,就比如對一段很密集的數據進行了隔點採樣,輕則浪費性能,重則會在畫面呈現時出現跳幀。爲了配合顯示器刷新,咱們可使用另外一個方法——requestAnimationFrame(fn),這是javascript中專門用來繪製逐幀動畫的,它會配合顯示器的刷新頻率進行必要的圖像更新,節省沒必要要的性能浪費。web

二. 動畫框架

canvas上實現基本的動畫,能夠遵循一個基本的編程框架:編程

function step(){
    /**
    *在每一幀中要執行的邏輯
    *......
    */
    requestAnimationFrame(step);
}

step();//啓動執行

你沒看錯,這就是canvas動畫最核心的一段代碼,step()函數會在每一個繪圖週期內重複執行。那麼每一幀中須要作哪些工做呢?canvas

咱們將canvas想象成一個舞臺stage,每個須要繪製在畫布上的元素被稱爲精靈,不管它們擁有怎樣的屬性,它們都具有update( )paint( )兩個基本方法,前者用於在每一幀中計算更新精靈的參數屬性,後者用於將這個精靈對象繪製在畫布上。那麼step函數在每一幀中所執行的邏輯就變得明朗了,對畫布進行必要的擦除,接着更新每個精靈的狀態(多是位置,顏色等等),而後將其繪製在畫布上。數組

好比如今要在畫布上表現一段太陽東昇西落得動畫,對應的僞代碼就是下面這個樣子的:

let stage = [];
stage.push(background, tree, cloud, sun);

function step(){
    cleanStage();//對畫布進行必要擦除
    background.update();//更新土地的屬性
    tree.update();//更新樹的屬性
    cloud.update();//更新雲的屬性
    sun.update();//更新太陽的屬性(屬性中必然包含着太陽的位置數據)
    background.paint();//繪製土地
    tree.paint();//繪製樹
    cloud.paint();//繪製雲
    sun.paint();//繪製太陽
    requestAnimationFrame(step);
}

若是你理解了上面的過程,那麼接下來咱們對上述代碼進行一些抽象和改寫:

//創建舞臺及添加元素的代碼
let stage = [];
stage.push(background, tree, cloud, sun....);

//逐幀動畫代碼
function step(){
    cleanStage();
    stage.map(sprite=>{
        sprite,update();
        sprite.paint(ctx);
    });
    requestAnimationFrame(step);
}

每個精靈對象都須要實現本身的update( )paint( )方法來描述本身的參數如何變化,以及如何在每一幀中被繪製,被添加進stage數組的都是精靈的實例,通常會將canvas繪圖上下文傳入paint(context)方法,這樣就能夠將精靈繪製在指定的畫布上。上面的範式只是一個簡陋的核心模型,可是已經足夠說明canvas動畫的本質。

三. 在canvas中模擬碰撞

如今咱們就經過一個碰撞仿真的例子來學習canvas動畫以及基本的物理仿真分析,示例雖然精簡,但包含了canvas動效最核心的精靈動畫和碰撞檢測主題。爲了方便二維向量操做並隱藏各類數學計算的細節,咱們直接使用一個已經定義好的Vector2類,其中封裝了不少向量的基本操做,都是初高中數學的知識,若是你已經記不太清楚,能夠找一些有關的資料複習一下。

3.1定義小球的屬性

將每個小球視爲一個精靈,咱們須要爲它增長一些基本屬性以便在每一幀中可以將其繪製出來。經過位置,半徑和顏色信息,就可以繪製出小球;經過速度信息,就能夠計算小球的位置變化,以便在繪製下一幀時使用。

class Ball{
    constructor(x,y,id){
        this.pos = new Vector2(x,y);//初始化小球的位置
        this.id = id;
        this.color = '';//繪製的顏色
        this.r = 20;//小球半徑,爲方便演示,此處使用給定值
        this.velocity = null;//小球的速度
    }
}

3.2 生成新的小球

爲了增長演示效果,咱們使用一個定時函數來隨機生成小球,每次生成時爲其賦予一個顏色,並給定一個隨機的初始速度。

//爲全局balls數組增長一個新的小球,初始位置爲(50,30),
function addBall() {
   let ball = new Ball(50,30,balls.length);
       ball.color = colorPalette[parseInt(steps / 100,10) % 10];
       ball.velocity = new Vector2(5*Math.random(), 5 * Math.random());
       balls.push(ball);
}

爲了方便起見,咱們使用一個全局自增的數值變量,在step中根據條件來執行addBall()方法:

if (steps % 100 === 0 && steps < 1500) {
  addBall();
}

step每循環100次(大約1.5秒)就會多生成一個向隨機方向發射的小球,且小球的數量不能超過15個。

3.3 幀動畫繪製函數step

step函數是動畫的核心,咱們須要在其中完成重繪背景,添加小球,更新每一個小球,繪製小球這些邏輯(因爲背景是靜態的,示例中並無將其抽象爲精靈動畫)。

function step() {
    steps++;
    //重繪背景
    paintBg();
    //每隔必定時間增長一個小球
    if (steps % 100 === 0 && steps < 1500) {
      addBall();
    }
    //更新每一個小球的狀態
    balls = balls.map((ball,index,originArr)=>{
      ball.update(index,originArr);
      ball.paint();//描線但不在畫布上繪製
      return ball;
    });
    //繪製每一個小球位置
    requestAnimationFrame(step);
}

3.4 定義小球的update方法

精靈的繪製方法paint通常都只涉及canvas的基本繪圖API,並不複雜,例如本例中,只須要在小球的pos屬性記錄的位置處繪製一個封閉弧線並填充它就能夠了。精靈的update( )方法每每纔是最難編寫的部分。在這個方法中,須要完成的基本邏輯包括狀態更新和碰撞檢測。

  • 狀態更新

    狀態更新通常包括自身狀態更新和相對狀態更新。自身狀態的更新,好比你但願小球在運動過程當中顏色會有變化,就屬於自身狀態的變化,相對狀態變化通常指小球相對公共座標系或某個參照對象而發生的宏觀位置變化,好比本例中的小球位置變化。

  • 碰撞檢測

    碰撞檢測通常包括精靈是否與其餘精靈發生碰撞,並須要對碰撞後形成的影響進行仿真。

參考代碼:

/*更新狀態
因爲檢測碰撞須要知道其餘小球的位置,故此處將小球數組的引用傳入
也能夠直接以面向對象的方式來定義*/
update(index,balls){

    let nextPos;//模擬下一次落點

    //1.計算下一次落點
    nextPos = this.pos.add(this.velocity.multiply(dt)); 

    //2.判斷新位置是否碰觸邊界,若是是則邊界法向的速度反向,假設碰撞過程是無能量損失
    if (nextPos.x + this.r > rightBorder || nextPos.x < this.r) {
        this.velocity.x = -1 * this.velocity.x;//速度份量反向
        nextPos = this.pos;//取消當前幀的位置更新
    } 
    if (nextPos.y + this.r > bottomBorder || nextPos.y < this.r) {
        this.velocity.y = -1 * this.velocity.y;
        nextPos = this.pos;
    }

    //3.判斷是否與其餘小球產生碰撞,爲避免重複,每一個小球只和比本身id更大的小球作檢測
    balls.map(ball=>{
       if (ball.id > index && this.checkCollision(ball)) {
           this.handleCollision(ball);
       }
       return ball;
    });

    //4.確認更新位置
    this.pos = nextPos;      
}

3.5 碰撞檢測

規則形狀的碰撞檢測通常有某些特殊方法,例如平面內的小球,其實只須要判斷圓心的距離和兩球半徑和的大小,就能夠知道兩球是否碰撞。而當檢測物體的外觀並不規則時,碰撞檢測是成了一個很是複雜的問題,最經常使用的方法包括外接盒檢測,光線投射法和分離軸定理檢測,感興趣的小夥伴能夠自行查資料進行學習。本例中的檢測方法其實是外接盒檢測法的一種基本狀況。

//碰撞檢測
checkCollision(ball){
   return this.pos.subtract(ball.pos).length() < this.r + ball.r;
}

3.6 碰撞仿真

碰撞仿真就是利用物理知識來計算碰撞對於物體形成的影響並修改其對應參數。本例中的碰撞能夠抽象爲兩個質量相等的運動小球的非對心碰撞,且不計能量損失,通常狀況下須要使用能量守恆定理和動量守恆定理聯立方程進行求解。本例的仿真中,咱們先將小球的非對心碰撞簡化爲對心碰撞,方法是將小球的速度向量分解爲沿球心連線方向Vr以及沿圓心連線法向Vn兩個份量,而後使用兩個小球的Vr來進行對心碰撞的模擬(質量相等的剛體對心碰撞後會互換速度),接着再將碰撞後的速度與小球本身的法向速度Vn進行向量合成便可。

本例的代碼中使用了簡化的方案,只計算了沿球心連線方向的份量並進行了碰撞模擬,沒有對碰撞後的速度進行合成,但對碰撞模擬的效果影響不大。參考代碼以下:

//處理碰撞
handleCollision(ball){
    let ballToThis = this.pos.subtract(ball.pos).normalize();
    let thisToBall = ballToThis.negate();
    this.velocity = ballToThis.multiply(Math.abs(ball.velocity.length()*(ball.velocity.dot(ballToThis) / ball.velocity.length())));
    ball.velocity = thisToBall.multiply(Math.abs(this.velocity.length()*(this.velocity.dot(ballToThis) / this.velocity.length())));
}

碰撞後兩個小球的速度都發生了變化,在下一幀更新位置時就會表現出來,效果已經在本節開頭展現出了。

完整的示例代碼能夠參見附件的demo,或訪問開頭處個人github倉庫地址。

四. 下一步

有了這樣一個撞球的基本模型和示例,你能作出一個乒乓球小遊戲或是撞球小遊戲嗎?

相關文章
相關標籤/搜索