HTML5 キャンバスを使用して単純なパーティクル エンジン コード例を実装する

黄舟
リリース: 2017-03-22 15:26:37
オリジナル
2190 人が閲覧しました

前書き

まあ、これを「粒子エンジン」と呼ぶのはまだ大げさで、ちょっとした見出しですが、実際の粒子エンジンからはまだ少し遠いです。早速、最初にデモを見てみましょう

この記事では、簡単なキャンバスパーティクルメーカー(以下、エンジン)の作成方法を説明します。

ワールドビュー

このシンプルなエンジンには、ワールド、ランチャー、グレインの 3 つの要素が必要です。要約すると、ワールドにはエミッターが存在し、エミッターはパーティクルを作成します。ワールドとエミッターはパーティクルの 状態 に影響を与えます。各パーティクルはワールドとエミッターの影響を受けた次の瞬間にその位置を計算します。自分を描いて。

世界

いわゆる「世界」とは、この「世界」に存在する粒子にグローバルに影響を与える環境です。粒子がこの「世界」に存在することを選択した場合、この粒子はこの「世界」の影響を受けることになります。

ランチャー

パーティクルを放出するために使用されるユニット。パーティクルによって生成されるパーティクルのさまざまな プロパティ を制御できます。パーティクルの親として、エミッターはパーティクルの誕生属性を制御できます: 誕生位置、誕生サイズ、寿命、「ワールド」の影響を受けるかどうか、「ランチャー」自体の影響を受けるかどうかなど...

例外さらに、エミッター自体が生成した死んだパーティクルをクリーンアップする必要があります。

粒子(粒子)

最小の基本単位は、あらゆる乱流個人です。それぞれの個体は位置、大きさ、寿命、同じ名前の影響を受けているかどうかなどを持ち、その姿を常にキャンバス上に正確に描き出すことができる。

パーティクル描画のメインロジック

HTML5 キャンバスを使用して単純なパーティクル エンジン コード例を実装する

以上がパーティクル描画のメインロジックです。

まず世界が何を必要としているかを見てみましょう。

世界を作ろう

世界には重力加速度があるべきだとなぜ自然に考えるのかわかりません。しかし、重力加速度だけでは多くのトリックを示すことはできません。そこで、ここでは、他の 2 つの影響要素、 を追加しました。重力加速度と熱の方向は垂直方向、風の影響は水平方向、この 3 つを使用すると、粒子を非常にコケティッシュに動かすことができます。

一部の状態 (パーティクルの生存など) の維持には時間マーカーが必要なので、時間の一時停止と後での逆流の効果を容易にするために、ワールドに時間を追加しましょう。

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();
    }
};
ログイン後にコピー

今回の Tick メソッドは、外部ループによって呼び出されるたびにこれらのことを実行します :

  1. Update world状態

  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();
    }
};
ログイン後にコピー

自分の小さな内なる世界を維持することを忘れないでください〜(似ています)外の大きな世界へ)

rreee

さて、これで終わりです 世界で最初の生き物が創造されたら、次はその子孫です (フフ、神は疲れたね)

子孫よ、無限に

来てねさあ、小さな子供たちよ、あなたたちは世界の主人公です!

作为世界的主角,粒子们拥有各种自身的状态:位置、速度、大小、寿命长度、出生时间当然必不可少

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 キャンバスを使用して単純なパーティクル エンジン コード例を実装するの詳細内容です。詳細については、PHP 中国語 Web サイトの他の関連記事を参照してください。

関連ラベル:
ソース:php.cn
このウェブサイトの声明
この記事の内容はネチズンが自主的に寄稿したものであり、著作権は原著者に帰属します。このサイトは、それに相当する法的責任を負いません。盗作または侵害の疑いのあるコンテンツを見つけた場合は、admin@php.cn までご連絡ください。
人気のチュートリアル
詳細>
最新のダウンロード
詳細>
ウェブエフェクト
公式サイト
サイト素材
フロントエンドテンプレート