首頁 > web前端 > H5教程 > 使用HTML5 canvas實作一個簡單的粒子引擎程式碼實例

使用HTML5 canvas實作一個簡單的粒子引擎程式碼實例

黄舟
發布: 2017-03-22 15:26:37
原創
2219 人瀏覽過

前言

好吧,說是「粒子引擎」還是大言不慚而標題黨了,離真正的粒子引擎還有點遠。廢話少說,先看demo

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

世界觀

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

世界(World)

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

發射器(Launcher)

用來發射粒子的單位。他們能控製粒子產生的粒子的各種屬性。身為粒子們的爹媽,發射器能夠控製粒子的出生屬性:出生的位置、出生的大小、壽命、是否受到「World」的影響、是否受到"Launcher"本身的影響等等……

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

助詞(Grain)

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

粒子繪製主邏輯

使用HTML5 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(&#39;./Util&#39;);
    var Grain = require(&#39;./Grain&#39;);

    /**
     * 发射器构造函数
     * @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(&#39;./Util&#39;);

    /**
     * 粒子构造函数
     * @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 = &#39;lighter&#39;;
        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();
    }
};
登入後複製

嗟乎。

以上是使用HTML5 canvas實作一個簡單的粒子引擎程式碼實例的詳細內容。更多資訊請關注PHP中文網其他相關文章!

來源:php.cn
本網站聲明
本文內容由網友自願投稿,版權歸原作者所有。本站不承擔相應的法律責任。如發現涉嫌抄襲或侵權的內容,請聯絡admin@php.cn
熱門教學
更多>
最新下載
更多>
網站特效
網站源碼
網站素材
前端模板