内容简介:导语:在日常的开发过程中,我们会常常会用到简单地说,粒子系统是一些粒子的集合,通过指定发射源(即每个粒子的起始位置)发射粒子流(即粒子的动画效果)。
导语:在日常的开发过程中,我们会常常会用到 canvas
来制作一些动画特效,其中有一个动画种类,需要我们生成一定数量,形状类似且行为基本一致的粒子,通过这些粒子的运动,来展现动画效果,比如: 下雨
, 闪烁的星空
。。。此类效果统一可称为粒子系统动画。
简单地说,粒子系统是一些粒子的集合,通过指定发射源(即每个粒子的起始位置)发射粒子流(即粒子的动画效果)。
本文具体示例及完整代码见 :
本文目录:
1. 粒子系统的共性
首先我们观察一个简单的粒子动画效果,如下图:
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta http-equiv="X-UA-Compatible" content="ie=edge"> <title>Document</title> </head> <body> <canvas id="example"></canvas> </body> <script> var cvs = document.getElementById('example'); var ctx = cvs.getContext('2d'); var width = 400; var height = 400; cvs.width = 400; cvs.height = 400; var particle = []; var lineAnimation; function createItem(amount) { for (let i = 0; i < amount; i++) { particle.push({ posX: Math.round(Math.random() * width), posY: Math.round(Math.random() * height), r: 4, color: Math.random() < 0.5 ? '#d63e3e' : '#23409b' }); } draw(); }; function draw() { ctx.clearRect(0, 0, width, height); particle.map((item, index) => { ctx.beginPath(); ctx.arc(item.posX, item.posY, item.r, 0, 2 * Math.PI); ctx.fillStyle = item.color; ctx.fill(); //画实心圆 ctx.closePath(); item.posY = item.posY + 2; if (item.posY > height) { item.posX = Math.round(Math.random() * width); item.posY = Math.round(Math.random() * height); }; }) lineAnimation = requestAnimationFrame(draw); } function stop() { cancelAnimationFrame(lineAnimation); } createItem(100); </script> </html> 复制代码
分析下上述代码,我们可以总结出粒子系统的一些特性:
1. 创建 canvas
画布。
2. 初始化粒子(创建粒子形状,确定粒子的起始位置)。
3. 绘制粒子到画布。
4. 定义粒子的运动方式(即粒子的运动动画)。
5. 控制动画的播放与暂停。
6. 清除画布。
既然粒子系统有这么多的通用性, 为什么我们不能把其中通用的地方抽离出来,建立一个粒子系统呢?
开始搭建一个粒子系统(基于es6)
根据上一部分总结出的共性,我们可以写出一个粒子系统的大概组成代码:
const STATUS_RUN = 'run'; const STATUS_STOP = 'stop'; //粒子系统基类 class Particle { //1. 创建 `canvas` 画布 constructor(idName, width, height, options) { this.canvas = document.getElementById(`${idName}`); this.ctx = this.canvas.getContext('2d'); //canvas执行上下文 this.timer = null; //动画运行定时器,采用requestAnimationFrame this.status = STATUS_STOP; //动画执行状态 默认为stop this.options = options || {}; //配置(粒子数量,速度等) this.canvas.width = width; this.canvas.height = height; this.width = width; this.height = height; this.init(); }; //2. 初始化粒子 init() { }; //3. 绘制粒子到画布 draw() { let self = this; let { ctx, width, height } = this; ctx.clearRect(0, 0, width, height); this.moveFunc(ctx, width, height); this.timer = requestAnimationFrame(() => { self.draw(); }); }; //4. 定义粒子的运动方式 moveFunc() { }; //5. 控制动画的播放与暂停。 run() { if (this.status !== STATUS_RUN) { this.status = STATUS_RUN; this.draw(); } }; stop() { this.status = STATUS_STOP; cancelAnimationFrame(this.timer); }; //6. 清除画布 clear() { this.stop(); this.ctx.clearRect(0, 0, this.width, this.height); }; }; export { Particle } 复制代码
我们通过这个方法改写下最开始的例子:
import { Particle } from "../lib/particleI.js"; class exampleMove extends Particle { //2. 初始化粒子 init() { this.particle = []; let amount = this.options.amount; let { width, height } = this; for (let i = 0; i < amount; i++) { this.particle.push({ posX: Math.round(Math.random() * width), posY: Math.round(Math.random() * height), r: 4, color: Math.random() < 0.5 ? '#d63e3e' : '#23409b' }); } }; //4. 定义粒子的运动方式 moveFunc(ctx, width, height) { this.particle.map(item => { item.posY = item.posY + 2; if (item.posY > height) { item.posX = Math.round(Math.random() * width); item.posY = Math.round(Math.random() * height); }; this.createParticle(ctx, item.posX, item.posY, item.r, item.color); }); }; //粒子形状 createParticle(ctx, x, y, r, color) { ctx.fillStyle = color; ctx.beginPath(); ctx.arc(x, y, r, 0, 2 * Math.PI); ctx.closePath(); ctx.fill(); }; //4. 定义粒子的运动方式 moveFunc(ctx, width, height) { this.particle.map(item => { item.posY = item.posY + 2; if (item.posY > height) { item.posX = Math.round(Math.random() * width); item.posY = Math.round(Math.random() * height); }; this.createParticle(ctx, item.posX, item.posY, item.r, item.color); }); }; } 复制代码
新建实例,让粒子系统运动:
var example = new exampleMove('example', 400, 400, { speed: 3, amount: 8 }); example.run(); 复制代码
写到这里, 一个小小的粒子系统就搭建完成了,我们看下总结看下:
关于粒子系统的这些共性:
1. 创建 canvas
画布。(基类完成)
2. 初始化粒子(创建粒子形状,确定粒子的起始位置)。
3. 绘制粒子到画布(基类完成)。
4. 定义粒子的运动方式(即粒子的运动动画)。
5. 控制动画的播放与暂停(基类完成)。
6. 清除画布(基类完成)。
由于每个人的粒子动画的展现方式有所不同,所以 2、4
两点需要,自己继承进行修改。
文章到此你以为就完了嘛?
我们把刚才搭建的粒子系统的数量提高到 6000
个看一下:
帧率在30左右非常低!!!一般帧率应该要保持在 60
,否则动画会出现卡顿感!!!
ps:关于性能分析,可以看我之前的一篇总结: 兄dei,听说你动画很卡?
那我们改咋办呢?老铁?
现在就让我们进入第三部分加入离屏渲染优化你的粒子系统
加入离屏渲染优化你的粒子系统
在开始之前,我们是不是要分析一下,为什么我们的粒子动画到达一定数量以后会卡!!
根据 chrome
性能分析工具,观察下图:
不难看出每一帧大部分的时间消耗都在 canvas Api
的调用中。
如何解决这个问题?
看似一个 ctx.fillStyle = '#f00'
整的跟 var a = '#f00'
差不多似的,实际的消耗是远远大约简单的变量赋值的,如下代码:
var cvs = document.getElementById('example'); var ctx = cvs.getContext('2d'); var timeStart = (new Date()).getTime(); var count; for (var i = 0; i < Math.pow(10, 7); i++) { // ctx.fillStyle = '#f00'; count = '#f00'; }; var timeEnd = (new Date()).getTime(); console.log('during:::', timeEnd - timeStart); 复制代码
所以我们解决问题的关键就是要尽可能减少调用渲染相关 API 的次数。
这时就需要用到我们的离屏渲染机制啦!!!
所谓离屏渲染,其实就是为了避开每一帧频繁的调用 渲染相关 API 的次数
,那么该如何避开呢?
离屏渲染原理
我们为每个粒子单独创建一个 canvas
画布,把粒子先在画布中画出。
如下代码(完整代码 canvas粒子系统 ):
// 离屏粒子类(这里的画布大小尽量和粒子大小保持一致,画布太大也会消耗性能); class offScreenItem { constructor(width, height, create) { this.canvas = document.createElement('canvas'); this.width = this.canvas.width = width * 2; this.height = this.canvas.height = height * 2; this.ctx = this.canvas.getContext('2d'); //在画布上绘制粒子 create(this.ctx, this.width, this.height); }; // 移动粒子(使用 drawImage 方法,通过改变粒子canvas画布的位置,达到运动的效果) move(ctx, x, y) { if (this.canvas.width && this.canvas.height) { ctx.drawImage(this.canvas, x, y); } } } 复制代码
我们来看下,开启离屏渲染后的性能如何?
同样是 6000
个粒子,但是帧率已经几乎回到了 60
, 开森!!!。
注意:
在创建离屏粒子实例时,一定要按种类创建,比如,上图中,实际上我只有红蓝两种圆,所以只要实例化两次就好,千万不要每一个粒子都实例化一次,会十分消耗内存,还不如没开启离屏渲染的时候。
关于粒子系统源码使用说明:
import { Particle, offScreenItem } from "../lib/particle.js"; class exampleMove extends Particle { //粒子形状绘制 createParticle(ctx, x, y, r, color) { //todo... }; //粒子如何运动 moveFunc(ctx, width, height) { //todo... }; //离屏粒子初始化位置 createOffScreenInstance(width, height, amount) { //todo... }; //正常粒子初始化位置 createNormalInstance(width, height, amount) { //todo... } } /** * @param {[String]} id [canvas画布的id] * @param {[Number]} width [canvas画布的宽] * @param {[Number]} height [canvas画布的高] * @param {[Object]} option [粒子系统的配置{speed: 3, amount: 800}] * @param {[Boolean]} offScreen [是否采用离屏渲染] * */ var example = new exampleMove(id, width, height, option, offScreen); //运动 example.run(); //停止 example.stop(); //清理画布 example.clear(); 复制代码
以上所述就是小编给大家介绍的《看你骨骼惊奇,这里有一套canvas粒子动画方案了解一下?》,希望对大家有所帮助,如果大家有任何疑问请给我留言,小编会及时回复大家的。在此也非常感谢大家对 码农网 的支持!
猜你喜欢:- 骨骼数目和骨骼层级数目的美术规范
- 动画骨骼节点批处理
- 动态骨骼Dynamic Bone优化
- 纯 CSS 实现简单骨骼动画
- CSharpGL(50)使用Assimp加载骨骼动画
- 闲鱼Flutter互动引擎系列——骨骼动画篇
本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们。