视频1 视频21 视频41 视频61 视频文章1 视频文章21 视频文章41 视频文章61 推荐1 推荐3 推荐5 推荐7 推荐9 推荐11 推荐13 推荐15 推荐17 推荐19 推荐21 推荐23 推荐25 推荐27 推荐29 推荐31 推荐33 推荐35 推荐37 推荐39 推荐41 推荐43 推荐45 推荐47 推荐49 关键词1 关键词101 关键词201 关键词301 关键词401 关键词501 关键词601 关键词701 关键词801 关键词901 关键词1001 关键词1101 关键词1201 关键词1301 关键词1401 关键词1501 关键词1601 关键词1701 关键词1801 关键词1901 视频扩展1 视频扩展6 视频扩展11 视频扩展16 文章1 文章201 文章401 文章601 文章801 文章1001 资讯1 资讯501 资讯1001 资讯1501 标签1 标签501 标签1001 关键词1 关键词501 关键词1001 关键词1501 专题2001
使用HTML5canvas实现一个简单的粒子引擎代码实例
2020-11-27 15:10:54 责编:小采
文档


前言

好吧,说是“粒子引擎”还是大言不惭而标题党了,离真正的粒子引擎还有点远。废话少说,先看demo

本文将教会你做一个简单的canvas粒子制造器(下称引擎)。

世界观

这个简单的引擎里需要有三种元素:世界(World)、发射器(Launcher)、粒子(Grain)。总得来说就是:发射器存在于世界之中,发射器制造粒子,世界和发射器都会影响粒子的状态,每个粒子在经过世界和发射器的影响之后,计算出下一刻的位置,把自己画出来。

世界(World)

所谓“世界”,就是全局影响那些存在于这这个“世界”的粒子的环境。一个粒子如果选择存在于这个“世界”里,那么这个粒子将会受到这个“世界”的影响。

发射器(Launcher)

用来发射粒子的单位。他们能控制粒子生成的粒子的各种属性。作为粒子们的爹妈,发射器能够控制粒子的出生属性:出生的位置、出生的大小、寿命、是否受到“World”的影响、是否受到"Launcher"本身的影响等等……

除此之外,发射器本身还要把自己生出来的已经死去的粒子清扫掉。

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

嗟乎。

下载本文
显示全文
专题