【腾讯Bugly干货】canvas粒子引擎手把手教学,教你惊艳领导和用户

移动开发
好吧,说是“粒子引擎”还是大言不惭而标题党了,离真正的粒子引擎还有点远。废话少说,先看[demo],扫描后点击屏幕有惊喜哦...

 

[[165460]]

前言

好吧,说是“粒子引擎”还是大言不惭而标题党了,离真正的粒子引擎还有点远。废话少说,先看[demo],扫描后点击屏幕有惊喜哦... 

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

世界观

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

世界(World)

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

发射器(Launcher)

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

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

粒子(Grain)

最小基本单位,就是每一个骚动的个体。每一个个体都拥有自己的位置、大小、寿命、是否受到同名度的影响等属性,这样才能在canvas上每时每刻准确描绘出他们的形态。

粒子绘制主逻辑

上面就是粒子绘制的主要逻辑。

我们先来看看世界需要什么。

创造一个世界

不知道为什么我理所当然得会想到世界应该有重力加速度。但是光有重力加速度不能表现出很多花样,于是这里我给他增加了另外两种影响因素:热气和风。重力加速度和热气他们的方向是垂直的,风影响方向是水平的,有了这三个东西,我们就能让粒子动得很风骚了。

一些状态(比如粒子的存亡)的维护需要有时间标志,那么我们把时间也加入到世界里吧,这样方便后期做时间暂停、逆流的效果。

  1. define(function(require, exports, module) { 
  2.    var Util = require('./Util'); 
  3.    var Launcher = require('./Launcher'); 
  4.  
  5.    /** 
  6.     * 世界构造函数 
  7.     * @param config 
  8.     *          backgroundImage     背景图片 
  9.     *          canvas              canvas引用 
  10.     *          context             canvas的context 
  11.     * 
  12.     *          time                世界时间 
  13.     * 
  14.     *          gravity             重力加速度 
  15.     * 
  16.     *          heat                热力 
  17.     *          heatEnable          热力开关 
  18.     *          minHeat             随机最小热力 
  19.     *          maxHeat             随机最大热力 
  20.     * 
  21.     *          wind                风力 
  22.     *          windEnable          风力开关 
  23.     *          minWind             随机最小风力 
  24.     *          maxWind             随机最大风力 
  25.     * 
  26.     *          timeProgress        时间进步单位,用于控制时间速度 
  27.     *          launchers           属于这个世界的发射器队列 
  28.     * @constructor 
  29.     */ 
  30.     function World(config){ 
  31.     //太长了,略去细节 
  32.     } 
  33.     World.prototype.updateStatus = function(){}; 
  34.     World.prototype.timeTick = function(){}; 
  35.     World.prototype.createLauncher = function(config){}; 
  36.     World.prototype.drawBackground = function(){}; 
  37.     module.exports = World
  38.  }); 

大家都知道,画动画就是不断得重画,所以我们需要暴露出一个方法,提供给外部循环调用:

  1. /** 
  2.   * 循环触发函数 
  3.   * 在满足条件的时候触发 
  4.   * 比如RequestAnimationFrame回调,或者setTimeout回调之后循环触发的 
  5.   * 用于维持World的生命 
  6.   */ 
  7.  
  8. World.prototype.timeTick = function(){ 
  9.  
  10.     //更新世界各种状态 
  11.     this.updateStatus(); 
  12.  
  13.     this.context.clearRect(0,0,this.canvas.width,this.canvas.height); 
  14.     this.drawBackground(); 
  15.  
  16.     //触发所有发射器的循环调用函数 
  17.     for(var i = 0;i<this.launchers.length;i++){ 
  18.        this.launchers[i].updateLauncherStatus(); 
  19.        this.launchers[i].createGrain(1); 
  20.        this.launchers[i].paintGrain(); 
  21.     } 
  22.  }; 

这个 timeTick 方法在外部循环调用时,每次都做着这几件事:

  1. 更新世界状态
  2. 清空画布重新绘制背景
  3. 轮询全世界所有发射器,并更新它们的状态,创建新的粒子,绘制粒子

那么,世界的状态到底有哪些要更新?

显然,每一次都要让时间往前增加一点是容易想到的。其次,为了让粒子尽可能动得风骚,我们让风和热力的状态都保持不稳定——每一阵风和每一阵热浪,都是你意识不到的~

  1. World.prototype.updateStatus = function(){ 
  2.     this.time+=this.timeProgress; 
  3.     this.wind = Util.randomFloat(this.minWind,this.maxWind); 
  4.     this.heat = Util.randomFloat(this.minHeat,this.maxHeat); 
  5. }; 

世界造出来了,我们还得让世界能造粒子发射器呀,要不然怎么造粒子呢~

  1. World.prototype.createLauncher = function(config){ 
  2.     var _launcher = new Launcher(config); 
  3.     this.launchers.push(_launcher); 
  4. }; 

好了,做为上帝,我们已经把世界打造得差不多了,接下来就是捏造各种各样的生灵了。

捏出第一个生物:发射器

发射器是世界上的第一种生物,依靠发射器才能繁衍出千奇百怪的粒子。那么发射器需要具备什么特征呢?

首先,它是属于哪个世界的得搞清楚(因为这个世界可能不止一个世界)。

其次,就是发射器本身的状态:位置、自身体系内的风力、热力,可以说:发射器就是一个世界里的小世界。

最后就是描述一下他的“基因”了,发射器的基因会影响到他们的后代(粒子)。我们赋予发射器越多的“基因”,那么他们的后代就会有更多的生物特征。具体看下面的良心注释代码吧~

  1. define(function (require, exports, module) { 
  2.    var Util = require('./Util'); 
  3.    var Grain = require('./Grain'); 
  4.  
  5.    /** 
  6.     * 发射器构造函数 
  7.     * @param config 
  8.     *          id              身份标识用于后续可视化编辑器的维护 
  9.     *          world           这个launcher的宿主 
  10.     * 
  11.     *          grainImage      粒子图片 
  12.     *          grainList       粒子队列 
  13.     *          grainLife       产生的粒子的生命 
  14.     *          grainLifeRange  粒子生命波动范围 
  15.     *          maxAliveCount   最大存活粒子数量 
  16.     * 
  17.     *          x               发射器位置x 
  18.     *          y               发射器位置y 
  19.     *          rangeX          发射器位置x波动范围 
  20.     *          rangeY          发射器位置y波动范围 
  21.     * 
  22.     *          sizeX           粒子横向大小 
  23.     *          sizeY           粒子纵向大小 
  24.     *          sizeRange       粒子大小波动范围 
  25.     * 
  26.     *          mass            粒子质量(暂时没什么用) 
  27.     *          massRange       粒子质量波动范围 
  28.     * 
  29.     *          heat            发射器自身体系的热气 
  30.     *          heatEnable      发射器自身体系的热气生效开关 
  31.     *          minHeat         随机热气最小值 
  32.     *          maxHeat         随机热气最小值 
  33.     * 
  34.     *          wind            发射器自身体系的风力 
  35.     *          windEnable      发射器自身体系的风力生效开关 
  36.     *          minWind         随机风力最小值 
  37.     *          maxWind         随机风力最小值 
  38.     * 
  39.     *          grainInfluencedByWorldWind      粒子受到世界风力影响开关 
  40.     *          grainInfluencedByWorldHeat      粒子受到世界热气影响开关 
  41.     *          grainInfluencedByWorldGravity   粒子受到世界重力影响开关 
  42.     * 
  43.     *          grainInfluencedByLauncherWind   粒子受到发射器风力影响开关 
  44.     *          grainInfluencedByLauncherHeat   粒子受到发射器热气影响开关 
  45.     * 
  46.     * @constructor 
  47.     */ 
  48.  
  49.    function Launcher(config) { 
  50.        //太长了,略去细节 
  51.    } 
  52.  
  53.    Launcher.prototype.updateLauncherStatus = function () {}; 
  54.    Launcher.prototype.swipeDeadGrain = function (grain_id) {}; 
  55.    Launcher.prototype.createGrain = function (count) {}; 
  56.    Launcher.prototype.paintGrain = function () {}; 
  57.  
  58.    module.exports = Launcher
  59.  
  60. }); 

发射器要负责生孩子啊,怎么生呢:

  1. Launcher.prototype.createGrain = function (count) { 
  2.        if (count + this.grainList.length <= this.maxAliveCount) { 
  3.            //新建了count个加上旧的还没达到最大数额限制 
  4.        } else if (this.grainList.length >= this.maxAliveCount && 
  5.            count + this.grainList.length > this.maxAliveCount) { 
  6.            //光是旧的粒子数量还没能达到最大限制 
  7.            //新建了count个加上旧的超过了最大数额限制 
  8.            count = this.maxAliveCount - this.grainList.length; 
  9.        } else { 
  10.            count = 0
  11.        } 
  12.        for (var i = 0; i < count; i++) { 
  13.            var _rd = Util.randomFloat(0, Math.PI * 2); 
  14.            var _grain = new Grain({/*粒子配置*/}); 
  15.            this.grainList.push(_grain); 
  16.        } 
  17.    }; 

生完孩子,孩子死掉了还得打扫……(好悲伤,怪内存不够用咯)

  1. Launcher.prototype.swipeDeadGrain = function (grain_id) { 
  2.     for (var i = 0; i < this.grainList.length; i++) { 
  3.         if (grain_id == this.grainList[i].id) { 
  4.             thisthis.grainList = this.grainList.remove(i);//remove是自己定义的一个Array方法 
  5.             this.createGrain(1); 
  6.             break; 
  7.         } 
  8.     } 
  9. }; 

生完孩子,还得把孩子放出来玩:

  1. Launcher.prototype.paintGrain = function () { 
  2.     for (var i = 0; i < this.grainList.length; i++) { 
  3.         this.grainList[i].paint(); 
  4.     } 
  5. }; 

自己的内部小世界也不要忘了维护呀~(跟外面的大世界差不多)

  1. Launcher.prototype.updateLauncherStatus = function () { 
  2.     if (this.grainInfluencedByLauncherWind) { 
  3.         this.wind = Util.randomFloat(this.minWind, this.maxWind); 
  4.     } 
  5.     if(this.grainInfluencedByLauncherHeat){ 
  6.         this.heat = Util.randomFloat(this.minHeat, this.maxHeat); 
  7.     } 
  8. }; 

好了,至此,我们完成了世界上第一种生物的打造,接下来就是他们的后代了(呼呼,上帝好累)

子子孙孙,无穷尽也

出来吧,小的们,你们才是世界的主角!

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

  1. define(function (require, exports, module) { 
  2.     var Util = require('./Util'); 
  3.  
  4.     /** 
  5.      * 粒子构造函数 
  6.      * @param config 
  7.      *          id              唯一标识 
  8.      *          world           世界宿主 
  9.      *          launcher        发射器宿主 
  10.      * 
  11.      *          x               位置x 
  12.      *          y               位置y 
  13.      *          vx              水平速度 
  14.      *          vy              垂直速度 
  15.      * 
  16.      *          sizeX           横向大小 
  17.      *          sizeY           纵向大小 
  18.      * 
  19.      *          mass            质量 
  20.      *          life            生命长度 
  21.      *          birthTime       出生时间 
  22.      * 
  23.      *          color_r 
  24.      *          color_g 
  25.      *          color_b 
  26.      *          alpha           透明度 
  27.      *          initAlpha       初始化时的透明度 
  28.      * 
  29.      *          influencedByWorldWind 
  30.      *          influencedByWorldHeat 
  31.      *          influencedByWorldGravity 
  32.      *          influencedByLauncherWind 
  33.      *          influencedByLauncherHeat 
  34.      * 
  35.      * @constructor 
  36.      */ 
  37.     function Grain(config) { 
  38.         //太长了,略去细节 
  39.     } 
  40.  
  41.     Grain.prototype.isDead = function () {}; 
  42.     Grain.prototype.calculate = function () {}; 
  43.     Grain.prototype.paint = function () {}; 
  44.     module.exports = Grain
  45. }); 

粒子们需要知道自己的下一刻是怎样子的,这样才能把自己在世界展现出来。对于运动状态,当然都是初中物理的知识了:-)

  1. Grain.prototype.calculate = function () { 
  2.     //计算位置 
  3.     if (this.influencedByWorldGravity) { 
  4.         this.vy += this.world.gravity+Util.randomFloat(0,0.3*this.world.gravity); 
  5.     } 
  6.     if (this.influencedByWorldHeat && this.world.heatEnable) { 
  7.         this.vy -this.world.heat+Util.randomFloat(0,0.3*this.world.heat); 
  8.     } 
  9.     if (this.influencedByLauncherHeat && this.launcher.heatEnable) { 
  10.         this.vy -this.launcher.heat+Util.randomFloat(0,0.3*this.launcher.heat); 
  11.      } 
  12.      if (this.influencedByWorldWind && this.world.windEnable) { 
  13.          this.vx += this.world.wind+Util.randomFloat(0,0.3*this.world.wind); 
  14.      } 
  15.      if (this.influencedByLauncherWind && this.launcher.windEnable) { 
  16.         this.vx += this.launcher.wind+Util.randomFloat(0,0.3*this.launcher.wind); 
  17.     } 
  18.     this.y += this.vy; 
  19.     this.x += this.vx; 
  20.     thisthis.alpha = this.initAlpha * (1 - (this.world.time - this.birthTime) / this.life); 
  21.  
  22.     //TODO 计算颜色 和 其他 
  23.  
  24. }; 

粒子们怎么知道自己死了没?

  1. Grain.prototype.isDead = function () { 
  2.     return Math.abs(this.world.time - this.birthTime)>this.life; 
  3. }; 

粒子们又该以怎样的姿态把自己展现出来?

  1. Grain.prototype.paint = function () { 
  2.     if (this.isDead()) { 
  3.         this.launcher.swipeDeadGrain(this.id); 
  4.     } else { 
  5.         this.calculate(); 
  6.         this.world.context.save(); 
  7.         this.world.context.globalCompositeOperation = 'lighter'
  8.         thisthis.world.context.globalAlpha = this.alpha; 
  9.         this.world.context.drawImage(this.launcher.grainImage, this.x-(this.sizeX)/2, this.y-(this.sizeY)/2, this.sizeX, this.sizeY); 
  10.         this.world.context.restore(); 
  11.     } 
  12. }; 

嗟乎。

后续

后续希望能够通过这个雏形,进行扩展,再造一个可视化编辑器供大家使用。

对了,代码都在这:https://github.com/jation/CanvasGrain

 

责任编辑:倪明 来源: 腾讯Bugly
相关推荐

2016-04-27 09:49:16

用户模型产品总结

2017-12-01 05:01:35

WiFi干扰无线网络

2011-01-10 14:41:26

2011-05-03 15:59:00

黑盒打印机

2021-07-14 09:00:00

JavaFX开发应用

2021-05-10 06:48:11

Python腾讯招聘

2023-06-05 13:07:38

2011-02-22 13:46:27

微软SQL.NET

2021-02-26 11:54:38

MyBatis 插件接口

2021-12-28 08:38:26

Linux 中断唤醒系统Linux 系统

2022-12-07 08:42:35

2022-07-27 08:16:22

搜索引擎Lucene

2023-04-26 12:46:43

DockerSpringKubernetes

2022-01-08 20:04:20

拦截系统调用

2022-03-14 14:47:21

HarmonyOS操作系统鸿蒙

2022-01-29 21:54:58

电商用户数据

2011-02-22 14:36:40

ASP.NETmsdnC#

2020-10-28 14:03:22

NLP自然语言分词

2021-09-30 18:27:38

数据仓库ETL

2020-07-09 08:59:52

if else模板Service
点赞
收藏

51CTO技术栈公众号