【騰訊Bugly干貨】canvas粒子引擎手把手教學(xué),教你驚艷領(lǐng)導(dǎo)和用戶
前言
好吧,說是“粒子引擎”還是大言不慚而標(biāo)題黨了,離真正的粒子引擎還有點(diǎn)遠(yuǎn)。廢話少說,先看[demo],掃描后點(diǎn)擊屏幕有驚喜哦...
本文將教會(huì)你做一個(gè)簡(jiǎn)單的canvas粒子制造器(下稱引擎)。
世界觀
這個(gè)簡(jiǎn)單的引擎里需要有三種元素:世界(World)、發(fā)射器(Launcher)、粒子(Grain)。總得來說就是:發(fā)射器存在于世界之中,發(fā)射器制造粒子,世界和發(fā)射器都會(huì)影響粒子的狀態(tài),每個(gè)粒子在經(jīng)過世界和發(fā)射器的影響之后,計(jì)算出下一刻的位置,把自己畫出來。
世界(World)
所謂“世界”,就是全局影響那些存在于這這個(gè)“世界”的粒子的環(huán)境。一個(gè)粒子如果選擇存在于這個(gè)“世界”里,那么這個(gè)粒子將會(huì)受到這個(gè)“世界”的影響。
發(fā)射器(Launcher)
用來發(fā)射粒子的單位。他們能控制粒子生成的粒子的各種屬性。作為粒子們的爹媽,發(fā)射器能夠控制粒子的出生屬性:出生的位置、出生的大小、壽命、是否受到“World”的影響、是否受到"Launcher"本身的影響等等……
除此之外,發(fā)射器本身還要把自己生出來的已經(jīng)死去的粒子清掃掉。
粒子(Grain)
最小基本單位,就是每一個(gè)騷動(dòng)的個(gè)體。每一個(gè)個(gè)體都擁有自己的位置、大小、壽命、是否受到同名度的影響等屬性,這樣才能在canvas上每時(shí)每刻準(zhǔn)確描繪出他們的形態(tài)。
粒子繪制主邏輯
上面就是粒子繪制的主要邏輯。
我們先來看看世界需要什么。
創(chuàng)造一個(gè)世界
不知道為什么我理所當(dāng)然得會(huì)想到世界應(yīng)該有重力加速度。但是光有重力加速度不能表現(xiàn)出很多花樣,于是這里我給他增加了另外兩種影響因素:熱氣和風(fēng)。重力加速度和熱氣他們的方向是垂直的,風(fēng)影響方向是水平的,有了這三個(gè)東西,我們就能讓粒子動(dòng)得很風(fēng)騷了。
一些狀態(tài)(比如粒子的存亡)的維護(hù)需要有時(shí)間標(biāo)志,那么我們把時(shí)間也加入到世界里吧,這樣方便后期做時(shí)間暫停、逆流的效果。
- define(function(require, exports, module) {
- var Util = require('./Util');
- var Launcher = require('./Launcher');
- /**
- * 世界構(gòu)造函數(shù)
- * @param config
- * backgroundImage 背景圖片
- * canvas canvas引用
- * context canvas的context
- *
- * time 世界時(shí)間
- *
- * gravity 重力加速度
- *
- * heat 熱力
- * heatEnable 熱力開關(guān)
- * minHeat 隨機(jī)最小熱力
- * maxHeat 隨機(jī)最大熱力
- *
- * wind 風(fēng)力
- * windEnable 風(fēng)力開關(guān)
- * minWind 隨機(jī)最小風(fēng)力
- * maxWind 隨機(jī)最大風(fēng)力
- *
- * timeProgress 時(shí)間進(jìn)步單位,用于控制時(shí)間速度
- * launchers 屬于這個(gè)世界的發(fā)射器隊(duì)列
- * @constructor
- */
- function World(config){
- //太長了,略去細(xì)節(jié)
- }
- World.prototype.updateStatus = function(){};
- World.prototype.timeTick = function(){};
- World.prototype.createLauncher = function(config){};
- World.prototype.drawBackground = function(){};
- module.exports = World;
- });
大家都知道,畫動(dòng)畫就是不斷得重畫,所以我們需要暴露出一個(gè)方法,提供給外部循環(huán)調(diào)用:
- /**
- * 循環(huán)觸發(fā)函數(shù)
- * 在滿足條件的時(shí)候觸發(fā)
- * 比如RequestAnimationFrame回調(diào),或者setTimeout回調(diào)之后循環(huán)觸發(fā)的
- * 用于維持World的生命
- */
- World.prototype.timeTick = function(){
- //更新世界各種狀態(tài)
- this.updateStatus();
- this.context.clearRect(0,0,this.canvas.width,this.canvas.height);
- this.drawBackground();
- //觸發(fā)所有發(fā)射器的循環(huán)調(diào)用函數(shù)
- for(var i = 0;i<this.launchers.length;i++){
- this.launchers[i].updateLauncherStatus();
- this.launchers[i].createGrain(1);
- this.launchers[i].paintGrain();
- }
- };
這個(gè) timeTick 方法在外部循環(huán)調(diào)用時(shí),每次都做著這幾件事:
- 更新世界狀態(tài)
- 清空畫布重新繪制背景
- 輪詢?nèi)澜缢邪l(fā)射器,并更新它們的狀態(tài),創(chuàng)建新的粒子,繪制粒子
那么,世界的狀態(tài)到底有哪些要更新?
顯然,每一次都要讓時(shí)間往前增加一點(diǎn)是容易想到的。其次,為了讓粒子盡可能動(dòng)得風(fēng)騷,我們讓風(fēng)和熱力的狀態(tài)都保持不穩(wěn)定——每一陣風(fēng)和每一陣熱浪,都是你意識(shí)不到的~
- World.prototype.updateStatus = function(){
- this.time+=this.timeProgress;
- this.wind = Util.randomFloat(this.minWind,this.maxWind);
- this.heat = Util.randomFloat(this.minHeat,this.maxHeat);
- };
世界造出來了,我們還得讓世界能造粒子發(fā)射器呀,要不然怎么造粒子呢~
- World.prototype.createLauncher = function(config){
- var _launcher = new Launcher(config);
- this.launchers.push(_launcher);
- };
好了,做為上帝,我們已經(jīng)把世界打造得差不多了,接下來就是捏造各種各樣的生靈了。
捏出第一個(gè)生物:發(fā)射器
發(fā)射器是世界上的第一種生物,依靠發(fā)射器才能繁衍出千奇百怪的粒子。那么發(fā)射器需要具備什么特征呢?
首先,它是屬于哪個(gè)世界的得搞清楚(因?yàn)檫@個(gè)世界可能不止一個(gè)世界)。
其次,就是發(fā)射器本身的狀態(tài):位置、自身體系內(nèi)的風(fēng)力、熱力,可以說:發(fā)射器就是一個(gè)世界里的小世界。
最后就是描述一下他的“基因”了,發(fā)射器的基因會(huì)影響到他們的后代(粒子)。我們賦予發(fā)射器越多的“基因”,那么他們的后代就會(huì)有更多的生物特征。具體看下面的良心注釋代碼吧~
- define(function (require, exports, module) {
- var Util = require('./Util');
- var Grain = require('./Grain');
- /**
- * 發(fā)射器構(gòu)造函數(shù)
- * @param config
- * id 身份標(biāo)識(shí)用于后續(xù)可視化編輯器的維護(hù)
- * world 這個(gè)launcher的宿主
- *
- * grainImage 粒子圖片
- * grainList 粒子隊(duì)列
- * grainLife 產(chǎn)生的粒子的生命
- * grainLifeRange 粒子生命波動(dòng)范圍
- * maxAliveCount 最大存活粒子數(shù)量
- *
- * x 發(fā)射器位置x
- * y 發(fā)射器位置y
- * rangeX 發(fā)射器位置x波動(dòng)范圍
- * rangeY 發(fā)射器位置y波動(dòng)范圍
- *
- * sizeX 粒子橫向大小
- * sizeY 粒子縱向大小
- * sizeRange 粒子大小波動(dòng)范圍
- *
- * mass 粒子質(zhì)量(暫時(shí)沒什么用)
- * massRange 粒子質(zhì)量波動(dòng)范圍
- *
- * heat 發(fā)射器自身體系的熱氣
- * heatEnable 發(fā)射器自身體系的熱氣生效開關(guān)
- * minHeat 隨機(jī)熱氣最小值
- * maxHeat 隨機(jī)熱氣最小值
- *
- * wind 發(fā)射器自身體系的風(fēng)力
- * windEnable 發(fā)射器自身體系的風(fēng)力生效開關(guān)
- * minWind 隨機(jī)風(fēng)力最小值
- * maxWind 隨機(jī)風(fēng)力最小值
- *
- * grainInfluencedByWorldWind 粒子受到世界風(fēng)力影響開關(guān)
- * grainInfluencedByWorldHeat 粒子受到世界熱氣影響開關(guān)
- * grainInfluencedByWorldGravity 粒子受到世界重力影響開關(guān)
- *
- * grainInfluencedByLauncherWind 粒子受到發(fā)射器風(fēng)力影響開關(guān)
- * grainInfluencedByLauncherHeat 粒子受到發(fā)射器熱氣影響開關(guān)
- *
- * @constructor
- */
- function Launcher(config) {
- //太長了,略去細(xì)節(jié)
- }
- Launcher.prototype.updateLauncherStatus = function () {};
- Launcher.prototype.swipeDeadGrain = function (grain_id) {};
- Launcher.prototype.createGrain = function (count) {};
- Launcher.prototype.paintGrain = function () {};
- module.exports = Launcher;
- });
發(fā)射器要負(fù)責(zé)生孩子啊,怎么生呢:
- Launcher.prototype.createGrain = function (count) {
- if (count + this.grainList.length <= this.maxAliveCount) {
- //新建了count個(gè)加上舊的還沒達(dá)到最大數(shù)額限制
- } else if (this.grainList.length >= this.maxAliveCount &&
- count + this.grainList.length > this.maxAliveCount) {
- //光是舊的粒子數(shù)量還沒能達(dá)到最大限制
- //新建了count個(gè)加上舊的超過了最大數(shù)額限制
- 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);
- }
- };
生完孩子,孩子死掉了還得打掃……(好悲傷,怪內(nèi)存不夠用咯)
- Launcher.prototype.swipeDeadGrain = function (grain_id) {
- for (var i = 0; i < this.grainList.length; i++) {
- if (grain_id == this.grainList[i].id) {
- thisthis.grainList = this.grainList.remove(i);//remove是自己定義的一個(gè)Array方法
- this.createGrain(1);
- break;
- }
- }
- };
生完孩子,還得把孩子放出來玩:
- Launcher.prototype.paintGrain = function () {
- for (var i = 0; i < this.grainList.length; i++) {
- this.grainList[i].paint();
- }
- };
自己的內(nèi)部小世界也不要忘了維護(hù)呀~(跟外面的大世界差不多)
- 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);
- }
- };
好了,至此,我們完成了世界上第一種生物的打造,接下來就是他們的后代了(呼呼,上帝好累)
子子孫孫,無窮盡也
出來吧,小的們,你們才是世界的主角!
作為世界的主角,粒子們擁有各種自身的狀態(tài):位置、速度、大小、壽命長度、出生時(shí)間當(dāng)然必不可少
- define(function (require, exports, module) {
- var Util = require('./Util');
- /**
- * 粒子構(gòu)造函數(shù)
- * @param config
- * id 唯一標(biāo)識(shí)
- * world 世界宿主
- * launcher 發(fā)射器宿主
- *
- * x 位置x
- * y 位置y
- * vx 水平速度
- * vy 垂直速度
- *
- * sizeX 橫向大小
- * sizeY 縱向大小
- *
- * mass 質(zhì)量
- * life 生命長度
- * birthTime 出生時(shí)間
- *
- * color_r
- * color_g
- * color_b
- * alpha 透明度
- * initAlpha 初始化時(shí)的透明度
- *
- * influencedByWorldWind
- * influencedByWorldHeat
- * influencedByWorldGravity
- * influencedByLauncherWind
- * influencedByLauncherHeat
- *
- * @constructor
- */
- function Grain(config) {
- //太長了,略去細(xì)節(jié)
- }
- Grain.prototype.isDead = function () {};
- Grain.prototype.calculate = function () {};
- Grain.prototype.paint = function () {};
- module.exports = Grain;
- });
粒子們需要知道自己的下一刻是怎樣子的,這樣才能把自己在世界展現(xiàn)出來。對(duì)于運(yùn)動(dòng)狀態(tài),當(dāng)然都是初中物理的知識(shí)了:-)
- Grain.prototype.calculate = function () {
- //計(jì)算位置
- 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;
- thisthis.alpha = this.initAlpha * (1 - (this.world.time - this.birthTime) / this.life);
- //TODO 計(jì)算顏色 和 其他
- };
粒子們?cè)趺粗雷约核懒藳]?
- Grain.prototype.isDead = function () {
- return Math.abs(this.world.time - this.birthTime)>this.life;
- };
粒子們又該以怎樣的姿態(tài)把自己展現(xiàn)出來?
- Grain.prototype.paint = function () {
- if (this.isDead()) {
- this.launcher.swipeDeadGrain(this.id);
- } else {
- this.calculate();
- this.world.context.save();
- this.world.context.globalCompositeOperation = 'lighter';
- thisthis.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();
- }
- };
嗟乎。
后續(xù)
后續(xù)希望能夠通過這個(gè)雛形,進(jìn)行擴(kuò)展,再造一個(gè)可視化編輯器供大家使用。
對(duì)了,代碼都在這:https://github.com/jation/CanvasGrain