【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 方法在外部循环调用时,每次都做着这几件事:
- 更新世界状态
- 清空画布重新绘制背景
- 轮询全世界所有发射器,并更新它们的状态,创建新的粒子,绘制粒子
那么,世界的状态到底有哪些要更新?
显然,每一次都要让时间往前增加一点是容易想到的。其次,为了让粒子尽可能动得风骚,我们让风和热力的状态都保持不稳定——每一阵风和每一阵热浪,都是你意识不到的~
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 官方操作系统,鹅厂的工程师都在使用,快来加入我们吧!
【Bugly干货分享】一起用 HTML5 Canvas 做一个简单又骚气的粒子引擎的更多相关文章
- 一起用HTML5 canvas做一个简单又骚气的粒子引擎
前言 好吧,说是"粒子引擎"还是大言不惭而标题党了,离真正的粒子引擎还有点远.废话少说,先看demo 本文将教会你做一个简单的canvas粒子制造器(下称引擎). 世界观 这个简单 ...
- html5 canvas 实现一个简单的叮当猫头部
原文:html5 canvas 实现一个简单的叮当猫头部 html5的canvas是很强大的,今天也是温习了一下之前的基础知识,然后学着做了一个简单的小案例.虽然在这一块几乎空白,但还是乐于尝试... ...
- 【腾讯Bugly干货分享】人人都可以做深度学习应用:入门篇
导语 2016年,继虚拟现实(VR)之后,人工智能(AI)的概念全面进入大众的视野.谷歌,微软,IBM等科技巨头纷纷重点布局,AI 貌似将成为互联网的下一个风口. 很多开发同学,对人工智能非常感兴趣, ...
- 用HTML5 Canvas做一个画图板
使用HTML5可以非常简单地在canvas上实现画图应用,用支持html5的浏览器便可在下面的区域进行绘画,要看到演示效果,请确保你的浏览器支持HTML5: 功能很简单,原理其实和拖放是类似的,主要是 ...
- 怎样用HTML5 Canvas制作一个简单的游戏
原文连接: How To Make A Simple HTML5 Canvas Game 自从我制作了一些HTML5游戏(例如Crypt Run)后,我收到了很多建议,要求我写一篇关于怎样利用HTML ...
- [译]怎样用HTML5 Canvas制作一个简单的游戏
这是我翻译自LostDecadeGames主页的一篇文章,原文地址:How To Make A Simple HTML5 Canvas Game. 下面是正文: 自从我制作了一些HTML5游戏(例如C ...
- 【腾讯Bugly干货分享】跨平台 ListView 性能优化
本文来自于腾讯Bugly公众号(weixinBugly),未经作者同意,请勿转载,原文地址:https://mp.weixin.qq.com/s/FbiSLPxFdGqJ00WgpJ94yw 导语 精 ...
- 使用Multiplayer Networking做一个简单的多人游戏例子-2/3(Unity3D开发之二十六)
猴子原创,欢迎转载.转载请注明: 转载自Cocos2Der-CSDN,谢谢! 原文地址: http://blog.csdn.net/cocos2der/article/details/51007512 ...
- tkinter做一个简单的登陆页面
做一个简单的登陆页面 import tkinter wuya = tkinter.Tk() wuya.title("wuya") wuya.geometry("900x3 ...
随机推荐
- HTML超标记语言
Html超文本标记语言,负责描绘Web世界的骨架. 〇.工具 http;//www.w3cchool.com.cn 一.Tim Bemers Lee 万维网之父: Html设计者: W3C创始人: ...
- mysql备份文件注释乱码处理工具
我们有时候需要做mysql数据库的注释,可是备份出来的是乱码,怎么办呢? 本工具软件来帮你的忙. 将GBK库备份后的文本文件中夹带的UTF8乱码转换为GBK编码,就不再乱码了. http://down ...
- JavaSE基础知识总结
最近回顾了一下Java的基础知识,决定写成博客梳理一遍,主要是JavaSE部分最基础的知识,适合考前突击,学后回顾,不适合作为初学材料. 简单的列个目录吧: 一.数据类型和运算符 二.流程控制与数组 ...
- winrar命令行加压解密
加密的操作 Rar.exe a -P123 test1.rar test.txt 其中参数a表示添加文件或文件夹到压缩包中,-P参数表示是带密码的加密 Rar.exe e -P123 test1.ra ...
- 游标的使用之压缩数据库Log文件
declare @databasename nvarchar(100)--定义游标以及赋值 获取所有Online的Database Namedeclare getDataBaseCursor cur ...
- [13]APUE:KQUEUE / FreeBSD
[a] 概述 kqueue API 由两个函数(kqueue.kevent).一个辅助宏(EV_SET).一个结构体(struct kevent)构成,可以应用于 socket.FIFO.pipe.a ...
- IE WebBrowser事件触发
<= IE6: IE6 下如果iframe很多,子框架的BeforeNavigate2,DownloadBegin,DownloadComplete,DocumentComplete可能交替出现 ...
- Win7上的ASP.NET MVC3项目在Win10上运行的一个坑
先解释一下问题:我原来的电脑环境是Win7+VS2015,因为新换了个电脑环境变成Win10+VS2015了,所以就把原先的项目复制到新的机器上,那么问题来了,原先的一个项目在VS2015上打开竟然直 ...
- 对象序列化成Json字符串 及 反序列化成对象
一. public static string JsonSerializer<T>(T t) { DataContractJsonSerializer ...
- linux下的5个查找命令
在Linux中,有很多方法可以做到这一点.国外网站LinuxHaxor总结了五条命令,你可以看看自己知道几条.大多数程序员,可能经常使用其中的2到3条,对这5条命令都很熟悉的人应该是不多的. 1. f ...