zoukankan      html  css  js  c++  java
  • 【Bugly干货分享】一起用 HTML5 Canvas 做一个简单又骚气的粒子引擎

    Bugly 技术干货系列内容主要涉及移动开发方向,是由 Bugly 邀请腾讯内部各位技术大咖,通过日常工作经验的总结以及感悟撰写而成,内容均属原创,转载请标明出处。

    前言

    好吧,说是“粒子引擎”还是大言不惭而标题党了,离真正的粒子引擎还有点远。废话少说,先看[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();
        }
    };
    

    嗟乎。

    后续

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

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

    如果你觉得内容意犹未尽,如果你想了解更多相关信息,请扫描以下二维码,关注我们的公众账号,可以获取更多技术类干货,还有精彩活动与你分享~

                                                         

     

    腾讯 Bugly是一款专为移动开发者打造的质量监控工具,帮助开发者快速,便捷的定位线上应用崩溃的情况以及解决方案。智能合并功能帮助开发同学把每天上报的数千条 Crash 根据根因合并分类,每日日报会列出影响用户数最多的崩溃,精准定位功能帮助开发同学定位到出问题的代码行,实时上报可以在发布后快速的了解应用的质量情况,适配最新的 iOS, Android 官方操作系统,鹅厂的工程师都在使用,快来加入我们吧!

  • 相关阅读:
    软测试-计算机组成原理、系统和网络安全机构
    POJ 2044 Weather Forecast
    Cocos2d-x 3.x 头像选择,本地相册图片+图片编辑(Android、IOS双平台)
    Spring-----1、Spring一个简短的引论
    捕android程序崩溃日志
    java 正则表达式例子, 查找字符串
    java中Pattern.compile函数的相关解释
    java JdbcTemplate源码
    eclipse 常用快捷键整理
    java 正则表达式去除标点符号
  • 原文地址:https://www.cnblogs.com/bugly/p/5403671.html
Copyright © 2011-2022 走看看