摘要:废话少说,先看,扫描后点击屏幕有惊喜哦本文将教会你做一个简单的粒子制造器下称引擎。发射器用来发射粒子的单位。重力加速度和热气他们的方向是垂直的,风影响方向是水平的,有了这三个东西,我们就能让粒子动得很风骚了。
前言腾讯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这个 timeTick 方法在外部循环调用时,每次都做着这几件事:
更新世界状态
清空画布重新绘制背景
轮询全世界所有发射器,并更新它们的状态,创建新的粒子,绘制粒子
那么,世界的状态到底有哪些要更新?
显然,每一次都要让时间往前增加一点是容易想到的。其次,为了让粒子尽可能动得风骚,我们让风和热力的状态都保持不稳定——每一阵风和每一阵热浪,都是你意识不到的~
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
如果你觉得内容意犹未尽,如果你想了解更多相关信息,请扫描以下二维码,关注我们的公众账号,可以获取更多技术类干货,还有精彩活动与你分享~
文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。
转载请注明本文地址:https://www.ucloud.cn/yun/59905.html
摘要:左边的边栏线则是使用的来画出来的,根据层级相应做一些调整,辅以的的效果就可以呈现出不错的移动效果。 这个插件根据选定的目录内容中的 h1, h2, h3, h4, h5, h6 标签来自动生成目录插入到选定的目录容器中,并且提供一个漂亮的样式效果 监听内容区滚动 点击跳转功能 兼容性:IE10+ (由于使用了 node.classList) 地址戳这里 github地址,欢迎sta...
摘要:模拟飞机航班线路动画一款基于的飞机航班线路模拟动画,它模拟了许多航班在不同目的地的起飞降落数量。跳动加载动画可调节参数这是一款基于的跳动加载动画,它的另一个特点是可以动态调节动画参数。 showImg(https://segmentfault.com/img/bVblze6?w=900&h=383); HTML5 动画在Canvas 上得到了充分的发挥,我们 VIP 视频也分享过很多相...
摘要:在最后,推荐给大家几个相关的库,增加使用时的便捷。周刊筛选的每篇内容,是作者的独到见解,踩坑总结和经验分享。每周二更新,欢迎关注或者订阅。 showImg(https://sfault-image.b0.upaiyun.com/382/562/382562537-5874b30ad612b); SegmentFault 曾举办过一个社区官方的比赛「30 行 js 你能做出什么?」,产生...
摘要:由于需要用户使用鼠标点击滑动释放鼠标等操作来实现绘画,所以我们也必须要使用鼠标的几个基本的监听事件。函数名其中为时间间隔。接下来我们可是做出以下特殊的画笔,比如说钢笔效果,边缘会比较笔触比较重的。 在HTML5备受期待和瞩目的今天,越来越多的人已经感受到它带来的无限魅力与震撼力,许多的技术人员、设计者、互联网爱好者们纷纷加入了HTML5的研究与设计中。 首先我先为大家介绍一下一个功能...
摘要:用做了个字体的粒子系统动画,可设置的参数改变动画效果,截图如下预览地址此文重讲思路,为方便解释,部分代码有做修改,此外因为部分代码太长,所以做的是截取,完整代码请看地址有兴趣的同学可以自己试试调参数尝试出不同的动画,滑稽脸的三大基本组件相机 用Three.js做了个字体的粒子系统动画,可设置speedX=speedY=speedZ=1000的参数改变动画效果,截图如下: showImg...
阅读 1825·2021-11-19 09:40
阅读 3070·2021-10-13 09:40
阅读 1806·2021-09-28 09:36
阅读 2067·2021-09-22 10:02
阅读 2589·2019-08-30 14:00
阅读 1821·2019-08-29 15:31
阅读 2779·2019-08-29 15:11
阅读 2786·2019-08-29 13:04