【Bugly乾貨分享】一塊兒用 HTML5 Canvas 作一個簡單又騷氣的粒子引擎

Bugly 技術乾貨系列內容主要涉及移動開發方向,是由 Bugly 邀請騰訊內部各位技術大咖,經過平常工做經驗的總結以及感悟撰寫而成,內容均屬原創,轉載請標明出處。vue

前言

好吧,說是「粒子引擎」仍是大言不慚而標題黨了,離真正的粒子引擎還有點遠。廢話少說,先看[demo],掃描後點擊屏幕有驚喜哦…
git

本文將教會你作一個簡單的canvas粒子製造器(下稱引擎)。github

世界觀

這個簡單的引擎裏須要有三種元素:世界(World)、發射器(Launcher)、粒子(Grain)。總得來講就是:發射器存在於世界之中,發射器製造粒子,世界和發射器都會影響粒子的狀態,每一個粒子在通過世界和發射器的影響以後,計算出下一刻的位置,把本身畫出來。canvas

世界(World)

所謂「世界」,就是全局影響那些存在於這這個「世界」的粒子的環境。一個粒子若是選擇存在於這個「世界」裏,那麼這個粒子將會受到這個「世界」的影響。markdown

發射器(Launcher)

用來發射粒子的單位。他們能控制粒子生成的粒子的各類屬性。做爲粒子們的爹媽,發射器可以控制粒子的出生屬性:出生的位置、出生的大小、壽命、是否受到「World」的影響、是否受到」Launcher」自己的影響等等……app

除此以外,發射器自己還要把本身生出來的已經死去的粒子清掃掉。dom

粒子(Grain)

最小基本單位,就是每個騷動的個體。每個個體都擁有本身的位置、大小、壽命、是否受到同名度的影響等屬性,這樣才能在canvas上每時每刻準確描繪出他們的形態。編輯器

粒子繪製主邏輯

上面就是粒子繪製的主要邏輯。函數

咱們先來看看世界須要什麼。工具

創造一個世界

不知道爲何我理所固然得會想到世界應該有重力加速度。可是光有重力加速度不能表現出不少花樣,因而這裏我給他增長了另外兩種影響因素:熱氣和風。重力加速度和熱氣他們的方向是垂直的,風影響方向是水平的,有了這三個東西,咱們就能讓粒子動得很風騷了。

一些狀態(好比粒子的存亡)的維護須要有時間標誌,那麼咱們把時間也加入到世界裏吧,這樣方便後期作時間暫停、逆流的效果。

define(function(require, exports, module) {
   var Util = require('./Util');
   var Launcher = require('./Launcher');

   /**
    * 世界構造函數
    * @param config
    *          backgroundImage     背景圖片
    *          canvas              canvas引用
    *          context             canvas的context
    *
    *          time                世界時間
    *
    *          gravity             重力加速度
    *
    *          heat                熱力
    *          heatEnable          熱力開關
    *          minHeat             隨機最小熱力
    *          maxHeat             隨機最大熱力
    *
    *          wind                風力
    *          windEnable          風力開關
    *          minWind             隨機最小風力
    *          maxWind             隨機最大風力
    *
    *          timeProgress        時間進步單位,用於控制時間速度
    *          launchers           屬於這個世界的發射器隊列
    * @constructor
    */
    function World(config){
    //太長了,略去細節
    }
    World.prototype.updateStatus = function(){};
    World.prototype.timeTick = function(){};
    World.prototype.createLauncher = function(config){};
    World.prototype.drawBackground = function(){};
    module.exports = World;
 });

你們都知道,畫動畫就是不斷得重畫,因此咱們須要暴露出一個方法,提供給外部循環調用:

/**
  * 循環觸發函數
  * 在知足條件的時候觸發
  * 好比RequestAnimationFrame回調,或者setTimeout回調以後循環觸發的
  * 用於維持World的生命
  */

World.prototype.timeTick = function(){

    //更新世界各類狀態
    this.updateStatus();

    this.context.clearRect(0,0,this.canvas.width,this.canvas.height);
    this.drawBackground();

    //觸發全部發射器的循環調用函數
    for(var i = 0;i<this.launchers.length;i++){
       this.launchers[i].updateLauncherStatus();
       this.launchers[i].createGrain(1);
       this.launchers[i].paintGrain();
    }
 };

這個 timeTick 方法在外部循環調用時,每次都作着這幾件事:

  1. 更新世界狀態
  2. 清空畫布從新繪製背景
  3. 輪詢全世界全部發射器,並更新它們的狀態,建立新的粒子,繪製粒子

那麼,世界的狀態到底有哪些要更新?

顯然,每一次都要讓時間往前增長一點是容易想到的。其次,爲了讓粒子儘量動得風騷,咱們讓風和熱力的狀態都保持不穩定——每一陣風和每一陣熱浪,都是你意識不到的~

World.prototype.updateStatus = function(){
    this.time+=this.timeProgress;
    this.wind = Util.randomFloat(this.minWind,this.maxWind);
    this.heat = Util.randomFloat(this.minHeat,this.maxHeat);
};

世界造出來了,咱們還得讓世界能造粒子發射器呀,要否則怎麼造粒子呢~

World.prototype.createLauncher = function(config){
    var _launcher = new Launcher(config);
    this.launchers.push(_launcher);
};

好了,作爲上帝,咱們已經把世界打造得差很少了,接下來就是捏造各類各樣的生靈了。

捏出第一個生物:發射器

發射器是世界上的第一種生物,依靠發射器才能繁衍出千奇百怪的粒子。那麼發射器須要具有什麼特徵呢?

首先,它是屬於哪一個世界的得搞清楚(由於這個世界可能不止一個世界)。

其次,就是發射器自己的狀態:位置、自身體系內的風力、熱力,能夠說:發射器就是一個世界裏的小世界。

最後就是描述一下他的「基因」了,發射器的基因會影響到他們的後代(粒子)。咱們賦予發射器越多的「基因」,那麼他們的後代就會有更多的生物特徵。具體看下面的良心註釋代碼吧~

define(function (require, exports, module) {
   var Util = require('./Util');
   var Grain = require('./Grain');

   /**
    * 發射器構造函數
    * @param config
    *          id              身份標識用於後續可視化編輯器的維護
    *          world           這個launcher的宿主
    *
    *          grainImage      粒子圖片
    *          grainList       粒子隊列
    *          grainLife       產生的粒子的生命
    *          grainLifeRange  粒子生命波動範圍
    *          maxAliveCount   最大存活粒子數量
    *
    *          x               發射器位置x
    *          y               發射器位置y
    *          rangeX          發射器位置x波動範圍
    *          rangeY          發射器位置y波動範圍
    *
    *          sizeX           粒子橫向大小
    *          sizeY           粒子縱向大小
    *          sizeRange       粒子大小波動範圍
    *
    *          mass            粒子質量(暫時沒什麼用)
    *          massRange       粒子質量波動範圍
    *
    *          heat            發射器自身體系的熱氣
    *          heatEnable      發射器自身體系的熱氣生效開關
    *          minHeat         隨機熱氣最小值
    *          maxHeat         隨機熱氣最小值
    *
    *          wind            發射器自身體系的風力
    *          windEnable      發射器自身體系的風力生效開關
    *          minWind         隨機風力最小值
    *          maxWind         隨機風力最小值
    *
    *          grainInfluencedByWorldWind      粒子受到世界風力影響開關
    *          grainInfluencedByWorldHeat      粒子受到世界熱氣影響開關
    *          grainInfluencedByWorldGravity   粒子受到世界重力影響開關
    *
    *          grainInfluencedByLauncherWind   粒子受到發射器風力影響開關
    *          grainInfluencedByLauncherHeat   粒子受到發射器熱氣影響開關
    *
    * @constructor
    */

   function Launcher(config) {
       //太長了,略去細節
   }

   Launcher.prototype.updateLauncherStatus = function () {};
   Launcher.prototype.swipeDeadGrain = function (grain_id) {};
   Launcher.prototype.createGrain = function (count) {};
   Launcher.prototype.paintGrain = function () {};

   module.exports = Launcher;

});

發射器要負責生孩子啊,怎麼生呢:

Launcher.prototype.createGrain = function (count) {
       if (count + this.grainList.length <= this.maxAliveCount) {
           //新建了count個加上舊的還沒達到最大數額限制
       } else if (this.grainList.length >= this.maxAliveCount &&
           count + this.grainList.length > this.maxAliveCount) {
           //光是舊的粒子數量還沒能達到最大限制
           //新建了count個加上舊的超過了最大數額限制
           count = this.maxAliveCount - this.grainList.length;
       } else {
           count = 0;
       }
       for (var i = 0; i < count; i++) {
           var _rd = Util.randomFloat(0, Math.PI * 2);
           var _grain = new Grain({/*粒子配置*/});
           this.grainList.push(_grain);
       }
   };

生完孩子,孩子死掉了還得打掃……(好悲傷,怪內存不夠用咯)

Launcher.prototype.swipeDeadGrain = function (grain_id) {
    for (var i = 0; i < this.grainList.length; i++) {
        if (grain_id == this.grainList[i].id) {
            this.grainList = this.grainList.remove(i);//remove是本身定義的一個Array方法
            this.createGrain(1);
            break;
        }
    }
};

生完孩子,還得把孩子放出來玩:

Launcher.prototype.paintGrain = function () {
    for (var i = 0; i < this.grainList.length; i++) {
        this.grainList[i].paint();
    }
};

本身的內部小世界也不要忘了維護呀~(跟外面的大世界差很少)

Launcher.prototype.updateLauncherStatus = function () {
    if (this.grainInfluencedByLauncherWind) {
        this.wind = Util.randomFloat(this.minWind, this.maxWind);
    }
    if(this.grainInfluencedByLauncherHeat){
        this.heat = Util.randomFloat(this.minHeat, this.maxHeat);
    }
};

好了,至此,咱們完成了世界上第一種生物的打造,接下來就是他們的後代了(呼呼,上帝好累)

子子孫孫,無窮盡也

出來吧,小的們,大家纔是世界的主角!

做爲世界的主角,粒子們擁有各類自身的狀態:位置、速度、大小、壽命長度、出生時間固然必不可少

define(function (require, exports, module) {
    var Util = require('./Util');

    /**
     * 粒子構造函數
     * @param config
     *          id              惟一標識
     *          world           世界宿主
     *          launcher        發射器宿主
     *
     *          x               位置x
     *          y               位置y
     *          vx              水平速度
     *          vy              垂直速度
     *
     *          sizeX           橫向大小
     *          sizeY           縱向大小
     *
     *          mass            質量
     *          life            生命長度
     *          birthTime       出生時間
     *
     *          color_r
     *          color_g
     *          color_b
     *          alpha           透明度
     *          initAlpha       初始化時的透明度
     *
     *          influencedByWorldWind
     *          influencedByWorldHeat
     *          influencedByWorldGravity
     *          influencedByLauncherWind
     *          influencedByLauncherHeat
     *
     * @constructor
     */
    function Grain(config) {
        //太長了,略去細節
    }

    Grain.prototype.isDead = function () {};
    Grain.prototype.calculate = function () {};
    Grain.prototype.paint = function () {};
    module.exports = Grain;
});

粒子們須要知道本身的下一刻是怎樣子的,這樣才能把本身在世界展示出來。對於運動狀態,固然都是初中物理的知識了:-)

Grain.prototype.calculate = function () {
    //計算位置
    if (this.influencedByWorldGravity) {
        this.vy += this.world.gravity+Util.randomFloat(0,0.3*this.world.gravity);
    }
    if (this.influencedByWorldHeat && this.world.heatEnable) {
        this.vy -= this.world.heat+Util.randomFloat(0,0.3*this.world.heat);
    }
    if (this.influencedByLauncherHeat && this.launcher.heatEnable) {
        this.vy -= this.launcher.heat+Util.randomFloat(0,0.3*this.launcher.heat);
     }
     if (this.influencedByWorldWind && this.world.windEnable) {
         this.vx += this.world.wind+Util.randomFloat(0,0.3*this.world.wind);
     }
     if (this.influencedByLauncherWind && this.launcher.windEnable) {
        this.vx += this.launcher.wind+Util.randomFloat(0,0.3*this.launcher.wind);
    }
    this.y += this.vy;
    this.x += this.vx;
    this.alpha = this.initAlpha * (1 - (this.world.time - this.birthTime) / this.life);

    //TODO 計算顏色 和 其餘

};

粒子們怎麼知道本身死了沒?

Grain.prototype.isDead = function () {
    return Math.abs(this.world.time - this.birthTime)>this.life;
};

粒子們又該以怎樣的姿態把本身展示出來?

Grain.prototype.paint = function () {
    if (this.isDead()) {
        this.launcher.swipeDeadGrain(this.id);
    } else {
        this.calculate();
        this.world.context.save();
        this.world.context.globalCompositeOperation = 'lighter';
        this.world.context.globalAlpha = this.alpha;
        this.world.context.drawImage(this.launcher.grainImage, this.x-(this.sizeX)/2, this.y-(this.sizeY)/2, this.sizeX, this.sizeY);
        this.world.context.restore();
    }
};

嗟乎。

後續

後續但願可以經過這個雛形,進行擴展,再造一個可視化編輯器供你們使用。

對了,代碼都在這:https://github.com/jation/CanvasGrain

若是你以爲內容意猶未盡,若是你想了解更多相關信息,請掃描如下二維碼,關注咱們的公衆帳號,能夠獲取更多技術類乾貨,還有精彩活動與你分享~

                                                     

 

騰訊 Bugly是一款專爲移動開發者打造的質量監控工具,幫助開發者快速,便捷的定位線上應用崩潰的狀況以及解決方案。智能合併功能幫助開發同窗把天天上報的數千條 Crash 根據根因合併分類,每日日報會列出影響用戶數最多的崩潰,精準定位功能幫助開發同窗定位到出問題的代碼行,實時上報能夠在發佈後快速的瞭解應用的質量狀況,適配最新的 iOS, Android 官方操做系統,鵝廠的工程師都在使用,快來加入咱們吧!

相關文章
相關標籤/搜索