まあ、これを「粒子エンジン」と呼ぶのはまだ大げさで、ちょっとした見出しですが、実際の粒子エンジンからはまだ少し遠いです。早速、最初にデモを見てみましょう
この記事では、簡単なキャンバスパーティクルメーカー(以下、エンジン)の作成方法を説明します。
このシンプルなエンジンには、ワールド、ランチャー、グレインの 3 つの要素が必要です。要約すると、ワールドにはエミッターが存在し、エミッターはパーティクルを作成します。ワールドとエミッターはパーティクルの 状態 に影響を与えます。各パーティクルはワールドとエミッターの影響を受けた次の瞬間にその位置を計算します。自分を描いて。
いわゆる「世界」とは、この「世界」に存在する粒子にグローバルに影響を与える環境です。粒子がこの「世界」に存在することを選択した場合、この粒子はこの「世界」の影響を受けることになります。
パーティクルを放出するために使用されるユニット。パーティクルによって生成されるパーティクルのさまざまな プロパティ を制御できます。パーティクルの親として、エミッターはパーティクルの誕生属性を制御できます: 誕生位置、誕生サイズ、寿命、「ワールド」の影響を受けるかどうか、「ランチャー」自体の影響を受けるかどうかなど...
例外さらに、エミッター自体が生成した死んだパーティクルをクリーンアップする必要があります。
最小の基本単位は、あらゆる乱流個人です。それぞれの個体は位置、大きさ、寿命、同じ名前の影響を受けているかどうかなどを持ち、その姿を常にキャンバス上に正確に描き出すことができる。
以上がパーティクル描画のメインロジックです。
まず世界が何を必要としているかを見てみましょう。
世界には重力加速度があるべきだとなぜ自然に考えるのかわかりません。しかし、重力加速度だけでは多くのトリックを示すことはできません。そこで、ここでは、他の 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 メソッドは、外部ループによって呼び出されるたびにこれらのことを実行します :
Update world状態
キャンバスをクリアして背景を再描画
世界中のすべてのエミッターをポーリングしてステータスを更新し、新しいパーティクルを作成し、パーティクルを描画します
それで、世界 どのステータスを更新する必要があるでしょうか?
明らかに、毎回時間を少しずつ進めることを考えるのは簡単です。第二に、粒子をできるだけコケティッシュに動かすために、風と熱の状態を不安定に保ちます - すべての突風とすべての熱波は、あなたが認識していないものです〜
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(); } };
自分の小さな内なる世界を維持することを忘れないでください〜(似ています)外の大きな世界へ)
rreeeさて、これで終わりです 世界で最初の生き物が創造されたら、次はその子孫です (フフ、神は疲れたね)
来てねさあ、小さな子供たちよ、あなたたちは世界の主人公です!
作为世界的主角,粒子们拥有各种自身的状态:位置、速度、大小、寿命长度、出生时间当然必不可少
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(); } };
嗟乎。
以上がHTML5 キャンバスを使用して単純なパーティクル エンジン コード例を実装するの詳細内容です。詳細については、PHP 中国語 Web サイトの他の関連記事を参照してください。