最近在做canvas粒子动画效果的研究,发现当粒子数量达到一定等级的时候,动画效果会变慢变卡。搜索了一下解决办法,发现离屏渲染是推荐最多的解决办法,那本文就利用离屏渲染实现一个动画效果来对比性能的提升。
概念
查阅了一下资料,概述一下离屏渲染的概念,相当于在屏幕渲染的时候开辟一个缓冲区,将当前需要加载的动画事先在缓冲区渲染完成之后,再显示到屏幕上。
非离屏渲染
非离屏渲染就是不建立缓冲区,直接在屏幕上逐个进行绘制,需要重复利用canvas的api。当粒子数量到达一定等级时,性能上会受到较大影响。
实现
先创建一个雪花粒子的类,构造相关的属性,定义一个名为snowArray
的数组,将每个粒子都存入该数组中。count
为雪花的数量。
class DrawSnow {
constructor(count) {
this.canvas = document.getElementById('canvas');
this.content = this.canvas.getContext('2d')
this.width = this.canvas.width = 1200;
this.height = this.canvas.height = 1000;
this.r = 2.5;
this.timer = null;
this.snowArray= [];
this.count = count;
this.useOffCanvas = false; // 是否使用离屏渲染
this.init()
}
}
init()
函数初始化雪花粒子,根据粒子的数量,重复渲染生成随机的位置,并存入数组中。初始化完成之后,开始绘制粒子。并执行动画函数animate()
。
init() {
let OffScreen = '';
if (this.useOffCanvas) {
OffScreen = new OffScreen();
}
for (let i = 0; i < this.count; i++) {
let x = this.width * Math.random();
let y = this.height * Math.random();
this.snowArray.push({
x: x,
y: y
});
this.draw(x, y);
}
this.animate();
}
animate()
函数实现了动画的循环,在一次动画执行完成之后,通过window.requestAnimationFrame
来实现重复效果。根据存储在snowArray[]
中的粒子信息,反复进行绘制。
animate() {
this.content.clearRect(0, 0, this.width, this.height);
for (let i in this.snowArray) {
let snow = this.snowArray[i];
snow.y += 2;
if (snow.y >= this.height + 10) {
snow.y = Math.random() * 50;
}
this.draw(snow.x, snow.y);
}
this.timer = requestAnimationFrame(() => {
this.animate();
});
}
效果
完成以上的步骤之后,来看一下在浏览器中的效果
小雪
中雪
大雪
性能分析
上述动图中,右上角为chrome自带的性能分析工具,点击开发者工具performance面板,按快捷键cmd + shift + p
然后输入show rendering
(打开实时查看帧率的面板),可以看到实时的帧率变化。
performance面板的使用在之前有介绍,指路:十分钟上手chrome性能分析面板。
小雪、中雪、大雪需要绘制的粒子分别是80、200、7000个粒子,当粒子数量较少时,动画效果比较顺畅,维持在60FPS左右,当数量增加到7000个时,动画开始卡顿,帧数快速下降。因为录屏工具对实际帧数会产生影响,上述动图可作为参考,实际帧数参考下图:
离屏渲染
实现
原理
创建缓冲区,需要额外创建一个canvas画布,将缓冲的画面现在该canvas上绘制好,在通过drawImage()的方式将该画布渲染到屏幕显示的画布上。
代码
首先实现离屏渲染的粒子构造方法,构造完成之后进行绘制,move()
将绘制好的画布通过drawImage
方法在屏幕上展示。
// 粒子类
class OffScreen {
constructor() {
this.canvas = document.createElement('canvas');
this.r = 2.5;
this.width = this.canvas.width = 5;
this.height = this.canvas.height = 5;
this.ctx = this.canvas.getContext('2d');
this.x = this.width * Math.random();
this.y = this.height * Math.random();
this.create();
}
// 创建粒子
create() {
this.ctx.save();
this.ctx.fillStyle = 'rgba(255,255,255)';
this.ctx.beginPath();
this.ctx.arc(this.x, this.y, 2.5, 0, 2 * Math.PI, false);
this.ctx.closePath();
this.ctx.fill();
this.ctx.restore();
}
// 绘制粒子
move(ctx, x, y) {
ctx.drawImage(this.canvas, x, y);
}
}
初始化粒子时,判断是否是离屏渲染模式,离屏模式下构造一个离屏粒子,先在画布中画出,当遍历粒子数组时,通过animate()
中执行OffScreen
类中的move()
方法,将粒子展示出来,类似复制黏贴的操作。
class DrawSnow {
constructor(count,useOffCanvas) {
......
this.useOffCanvas = useOffCanvas; // 是否使用离屏渲染
this.init();
}
init() {
let offScreen = '';
if (this.useOffCanvas) {
offScreen = new OffScreen();
}
for (let i = 0; i < this.count; i++) {
let x = this.width * Math.random();
let y = this.height * Math.random();
if (this.useOffCanvas) {
this.snowArray.push({
instance: offScreen,
x: x,
y: y
});
} else {
this.snowArray.push({
x: x,
y: y
});
this.draw(x, y);
}
}
this.animate();
}
animate() {
this.content.clearRect(0, 0, this.width, this.height);
for (let i in this.snowArray) {
let snow = this.snowArray[i];
snow.y += 2;
if (snow.y >= this.height + 10) {
snow.y = Math.random() * 50;
}
if (this.useOffCanvas) {
snow.instance.move(this.content, snow.x, snow.y);
} else {
this.draw(snow.x, snow.y);
}
}
this.timer = requestAnimationFrame(() => {
this.animate();
});
}
}
效果
小雪
中雪
大雪
性能分析
和非离屏渲染进行对比,发现当粒子数量不多时,差距并不明显,当粒子数量达到7000时,有了明显差距。
在上述动图中,离屏渲染下,大雪动画的帧率达到平均23FPS,录屏工具会对性能产生影响,实际的性能如下图:
相比非离屏模式下帧率提升了一倍。
如何选择使用离屏渲染
上述例子中,使用离屏渲染确实提升了动画运行的帧率,但不是任何时候都适用离屏渲染。
基于上面这个例子,衍生实现另一个效果,即改变雪花粒子的样式,随机选择粒子的大小位置和透明度,使画面更有层次感。
案例效果
先观察一下两种模式下的实现效果以及帧率,离屏渲染的帧率反而更低,与之前的结果完全相反。
原因
新的粒子是随机生成大小位置和透明度,如果通过之前的方式去构建离屏粒子,那么每个粒子的属性都将相同,无法实现随机效果。在本例中,需要通过循环,将不同的参数传递给构造函数,相当于多次调用了构造函数的canvas api。与非离屏渲染模式相比,还增加了创建缓冲区,从缓冲区绘制到屏幕上的性能消耗,所以帧率相比非离屏模式,反而更低。
而在之前的例子中,粒子的大小、颜色、透明度都相同,不需要重复构造,所以只调用了一次构造函数,也只调用了一次绘制的canvas api。
相关代码
观察下方代码,结合上文中在离屏模式下的构造方式,可以发现,本例中循环构造了新的粒子,也就不断调用了api,并没有降低性能的消耗。
init() {
let offScreen = '';
for (let i = 0; i < this.count; i++) {
let x = this.width*Math.random();
let y = this.height*Math.random();
let alpha = (Math.floor(Math.random() * 10) + 1) / 10 / 2;
let color = "rgba(255,255,255," + alpha + ")";
let r = Math.random() * 2 + 1;
if (this.useOffCanvas) {
// 循环构造新的粒子
offScreen = new OffScreen();
this.snowArray.push({
instance: offScreen,
x: x,
y: y,
color: color,
r:r
});
} else {
this.snowArray.push({
x: x,
y: y,
color: color,
r:r
});
this.draw(x,y,color,r);
}
}
this.animate();
}
FPS
FPS是图像领域中的定义,是指画面每秒传输帧数,通俗来讲就是指动画或视频的画面数。
理论上说,FPS 越高,动画会越流畅,目前大多数设备的屏幕刷新率为 60 次/秒,所以通常来讲 FPS 为 60 frame/s 时动画效果最好。
当不同的帧率下,动画的视觉效果如何呢?
- 帧率能够达到 50 ~ 60 FPS 的动画将会相当流畅,让人倍感舒适;
- 帧率在 30 ~ 50 FPS 之间的动画,因各人敏感程度不同,舒适度因人而异;
- 帧率在 30 FPS 以下的动画,让人感觉到明显的卡顿和不适感; 帧率波动很大的动画,亦会使人感觉到卡顿。
所以流畅的动画,帧率需要达到30fps往上。
具体分析可以参考: 【前端性能】Web 动画帧率(FPS)计算
总结
离屏渲染在动画优化上非常多人推荐,但也不是任何情况下都可以利用,离屏渲染首先需要构造一个缓冲区,再将缓冲区中的画面展示到显示屏上,这两个过程也需要消耗性能。
例如上文中第二个例子,并没有减少对api的调用,反而离屏的过程增加了性能的消耗,这种情况就不合适采用这种方式。离屏渲染也需要选择合理的使用场景。