溫馨提示×

溫馨提示×

您好,登錄后才能下訂單哦!

密碼登錄×
登錄注冊×
其他方式登錄
點擊 登錄注冊 即表示同意《億速云用戶服務(wù)條款》

HTML5 canvas怎么做一個簡單又騷氣的粒子引擎

發(fā)布時間:2021-10-12 15:58:16 來源:億速云 閱讀:129 作者:柒染 欄目:云計算

本篇文章為大家展示了HTML5 canvas怎么做一個簡單又騷氣的粒子引擎,內(nèi)容簡明扼要并且容易理解,絕對能使你眼前一亮,通過這篇文章的詳細介紹希望你能有所收獲。

好吧,說是“粒子引擎“還是大言不慚而標題黨了,離真正的粒子引擎還有點遠。
廢話少說,先看[demo]
教會你做一個簡單的canvas粒子制造器(下稱引擎)。

一.世界觀

這個簡單的引擎里需要有三種元素:世界(World)、發(fā)射器(Launcher)、粒子(Grain)。總得來說就是:發(fā)射器存在于世界之中,發(fā)射器制造粒子,世界和發(fā)射器都會影響粒子的狀態(tài),每個粒子在經(jīng)過世界和發(fā)射器的影響之后,計算出下一刻的位置,把自己畫出來。

1.世界(World)

所謂“世界”,就是全局影響那些存在于這這個“世界”的粒子的環(huán)境。一個粒子如果選擇存在于這個“世界”里,那么這個粒子將會受到這個“世界”的影響。

2.發(fā)射器(Launcher)

用來發(fā)射粒子的單位。他們能控制粒子生成的粒子的各種屬性。作為粒子們的爹媽,發(fā)射器能夠控制粒子的出生屬性:出生的位置、出生的大小、壽命、是否受到“World”的影響、是否受到"Launcher"本身的影響等等……
除此之外,發(fā)射器本身還要把自己生出來的已經(jīng)死去的粒子清掃掉。

3.粒子(Grain)

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

二.粒子繪制主邏輯


上面就是粒子繪制的主要邏輯。

我們先來看看世界需要什么。

三.創(chuàng)造一個世界

不知道為什么我理所當然得會想到世界應該有重力加速度。但是光有重力加速度不能表現(xiàn)出很多花樣,于是這里我給他增加了另外兩種影響因素:熱氣和風。重力加速度和熱氣他們的方向是垂直的,風影響方向是水平的,有了這三個東西,我們就能讓粒子動得很風騷了。

一些狀態(tài)(比如粒子的存亡)的維護需要有時間標志,那么我們把時間也加入到世界里吧,這樣方便后期做時間暫停、逆流的效果。

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

    /**
     * 世界構(gòu)造函數(shù)
     * @param config
     *          backgroundImage     背景圖片
     *          canvas              canvas引用
     *          context             canvas的context
     *
     *          time                世界時間
     *
     *          gravity             重力加速度
     *
     *          heat                熱力
     *          heatEnable          熱力開關(guān)
     *          minHeat             隨機最小熱力
     *          maxHeat             隨機最大熱力
     *
     *          wind                風力
     *          windEnable          風力開關(guān)
     *          minWind             隨機最小風力
     *          maxWind             隨機最大風力
     *
     *          timeProgress        時間進步單位,用于控制時間速度
     *          launchers           屬于這個世界的發(fā)射器隊列
     * @constructor
     */
    function World(config){
        //太長了,略去細節(jié)
    }
    World.prototype.updateStatus = function(){};
    World.prototype.timeTick = function(){};
    World.prototype.createLauncher = function(config){};
    World.prototype.drawBackground = function(){};
    module.exports = World;
});
大家都知道,畫動畫就是不斷得重畫,所以我們需要暴露出一個方法,提供給外部循環(huán)調(diào)用:

/**
 * 循環(huán)觸發(fā)函數(shù)
 * 在滿足條件的時候觸發(fā)
 * 比如RequestAnimationFrame回調(diào),或者setTimeout回調(diào)之后循環(huán)觸發(fā)的
 * 用于維持World的生命
 */

World.prototype.timeTick = function(){

    //更新世界各種狀態(tài)
    this.updateStatus();

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

    //觸發(fā)所有發(fā)射器的循環(huán)調(diào)用函數(shù)
    for(var i = 0;i<this.launchers.length;i++){
        this.launchers[i].updateLauncherStatus();
        this.launchers[i].createGrain(1);
        this.launchers[i].paintGrain();
    }
};
這個timeTick方法在外部循環(huán)調(diào)用時,每次都做著這幾件事:

更新世界狀態(tài)
清空畫布重新繪制背景
輪詢?nèi)澜缢邪l(fā)射器,并更新它們的狀態(tài),創(chuàng)建新的粒子,繪制粒子
那么,世界的狀態(tài)到底有哪些要更新?
顯然,每一次都要讓時間往前增加一點是容易想到的。其次,為了讓粒子盡可能動得風騷,我們讓風和熱力的狀態(tài)都保持不穩(wěn)定——每一陣風和每一陣熱浪,都是你意識不到的~

World.prototype.updateStatus = function(){
    this.time+=this.timeProgress;
    this.wind = Util.randomFloat(this.minWind,this.maxWind);
    this.heat = Util.randomFloat(this.minHeat,this.maxHeat);
};
世界造出來了,我們還得讓世界能造粒子發(fā)射器呀,要不然怎么造粒子呢~

World.prototype.createLauncher = function(config){
    var _launcher = new Launcher(config);
    this.launchers.push(_launcher);
};
好了,做為上帝,我們已經(jīng)把世界打造得差不多了,接下來就是捏造各種各樣的生靈了。

四.捏出第一個生物:發(fā)射器

發(fā)射器是世界上的第一種生物,依靠發(fā)射器才能繁衍出千奇百怪的粒子。那么發(fā)射器需要具備什么特征呢?

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

其次,就是發(fā)射器本身的狀態(tài):位置、自身體系內(nèi)的風力、熱力,可以說:發(fā)射器就是一個世界里的小世界。

最后就是描述一下他的“基因”了,發(fā)射器的基因會影響到他們的后代(粒子)。我們賦予發(fā)射器越多的“基因”,那么他們的后代就會有更多的生物特征。具體看下面的良心注釋代碼吧~

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

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

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

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

    module.exports = Launcher;

});
發(fā)射器要負責生孩子啊,怎么生呢:

Launcher.prototype.createGrain = function (count) {
        if (count + this.grainList.length <= this.maxAliveCount) {
            //新建了count個加上舊的還沒達到最大數(shù)額限制
        } else if (this.grainList.length >= this.maxAliveCount &&
            count + this.grainList.length > this.maxAliveCount) {
            //光是舊的粒子數(shù)量還沒能達到最大限制
            //新建了count個加上舊的超過了最大數(shù)額限制
            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);
        }
    };
生完孩子,孩子死掉了還得打掃……(好悲傷,怪內(nèi)存不夠用咯)

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();
    }
};
自己的內(nèi)部小世界也不要忘了維護呀~(跟外面的大世界差不多)

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);
    }
};
好了,至此,我們完成了世界上第一種生物的打造,接下來就是他們的后代了(呼呼,上帝好累)

子子孫孫,無窮盡也
出來吧,小的們,你們才是世界的主角!

作為世界的主角,粒子們擁有各種自身的狀態(tài):位置、速度、大小、壽命長度、出生時間當然必不可少

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

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

    Grain.prototype.isDead = function () {};
    Grain.prototype.calculate = function () {};
    Grain.prototype.paint = function () {};
    module.exports = Grain;
});
粒子們需要知道自己的下一刻是怎樣子的,這樣才能把自己在世界展現(xiàn)出來。對于運動狀態(tài),當然都是初中物理的知識了:-)

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;
};
粒子們又該以怎樣的姿態(tài)把自己展現(xiàn)出來?

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();
    }
};

上述內(nèi)容就是HTML5 canvas怎么做一個簡單又騷氣的粒子引擎,你們學到知識或技能了嗎?如果還想學到更多技能或者豐富自己的知識儲備,歡迎關(guān)注億速云行業(yè)資訊頻道。

向AI問一下細節(jié)

免責聲明:本站發(fā)布的內(nèi)容(圖片、視頻和文字)以原創(chuàng)、轉(zhuǎn)載和分享為主,文章觀點不代表本網(wǎng)站立場,如果涉及侵權(quán)請聯(lián)系站長郵箱:is@yisu.com進行舉報,并提供相關(guān)證據(jù),一經(jīng)查實,將立刻刪除涉嫌侵權(quán)內(nèi)容。

AI