插播一篇关于 canvas
动画及性能优化的文章,为我们可以更好的进入到 webgl
的世界奠定基础。
本篇文章的内容可能会稍难理解,还希望大家有问题及时提出。闲话我们就不多说了,开始今天的正题吧。
1. 动画实战
首先介绍一下我们要实现的动画内容: 夜空中的流星源码
。
今天就来跟大家详细分享一下如何进行编写 canvas
动画以及如何进行优化。
1.1 搭建页面
canvas
的页面组成是非常简单的。如下所示:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>canvas动画和性能优化实战</title>
<!-- 引入页面的样式 -->
<link rel="stylesheet" href="./style/index.css">
</head>
<body>
<canvas id="shooting-star">您的浏览器不支持canvas标签,请升级版本或选择其他浏览器</canvas>
</body>
</html>
<!-- 引入页面的功能 -->
<script src="./js/index.js"></script>
创建一个 html
文件,并且引入 css 和 js
文件。
这里有一个问题,之前我们跟大家分享的时候说,canvas
标签的宽和高要在html中进行设置。但是为了适配我们的屏幕,就得用js
来设置画布的宽和高。
1.2 具体实现
1. 首先创建一个流星的类。并添加一个启动的方法
class ShootingStar{
// 构造方法
constructor() {}
// 启动的方法
start() {}
}
// 实例化一个对象,并启动
new ShootingStar().start();
2. 获取到 canvas
对象和绘图上下文,并设置 canvas
的宽和高
注意:这里设置宽高是因为画布会出现模糊的现象。
// 构造方法
constructor() {
// 获取canvas对象
this.ctx = document.getElementById('id名称');
// 获取绘图上下文
this.c = this.ctx.getContext('2d');
// 获取当前页面的宽高
this.maxW = document.body.offsetWidth;
this.maxH = document.body.offsetHeight;
// 定义一个数组,用来存放夜空中星,之后的开发中会用到
this.skyList = [];
}
// 初始化画布的宽和高
initCanvasSize() {
this.ctx.width = this.maxW;
this.ctx.height = this.maxH;
}
// 在起始方法中调用初始化函数
start() {
this.initCanvasSize();
}
3. 绘制背景
这里我们没有引入图片,是模拟了一个星空的效果,略微简陋。
// 绘制背景
drawBackground() {
// 夜空中星的数量
const maxCount = 500;
// 创建一个黑色的纯色背景
this.c.fillRect(0, 0, this.ctx.width, this.ctx.height);
// 创建随机坐标。在当前页面内随机
for (let i = 0; i < maxCount; i++) {
const r = ~~(Math.random() * 3);
const x = ~~(Math.random() * this.maxW);
const y = ~~(Math.random() * this.maxH);
// 将随机坐标推入数组中
this.skyList.push({
x,y,r
})
}
}
4. 开启流星模式
// 初始化背景,并重新开始绘制流星
initBackground() {
this.c.beginPath();
this.c.fillStyle = 'rgba(0,0,0,0.2)';
// 清空当前画布
this.c.rect(0,0,this.maxW, this.maxH);
this.c.fill();
// 重新绘制背景
this.drawStarList();
// 重新开始绘制流星
this.startShootingStar();
}
// 绘制流星
drawStar(x, y) {
this.drawStarList()
this.c.beginPath();
this.c.fillStyle = 'white';
this.c.arc(x,y,2,0,Math.PI * 2);
this.c.fill();
}
// 添加拖尾效果
drawCover() {
this.c.beginPath();
this.c.fillStyle = 'rgba(0,0,0,0.06)';
this.c.rect(0,0,this.ctx.width,this.ctx.height);
this.c.fill();
}
// 开启流星模式
startShootingStar() {
// 设置x和y的运动速度
let x = ~~(Math.random() * this.maxW);
let y = 4;
// 设置流星的颜色。
this.c.fillStyle = 'darkorange';
// 获取一个流星滑落的最大距离。到这个距离之后消失
const clearY = Math.random() * this.maxH;
// 绘制函数。
const draw = () => {
x -= 1;
y += 4;
// 如果当前滑落的距离大于最大距离,初始化当前背景。
if (y >= clearY) {
this.initBackground();
return;
}
// 绘制流星,传入当前的 x、y 坐标
this.drawStar(x, y);
// 此函数用来使流星有拖尾效果。
this.drawCover();
// 使用此函数实现动画效果
requestAnimationFrame(draw);
}
draw()
}
到这里动画实战部分的内容就分享完了,有兴趣的同学可以查看下源码,也可以试着自己实现以下。
2. 性能优化
2.1 使用计算代替频繁的渲染
绘制过程中,通常会使用计算来代替频繁的画布渲染。原理类似于 dom回流
。因为 canvas
也是属于 dom 的部分,过多的操作会影响性能。当然,如果你使用一个特别消耗实现的算法的话就另当别论了。
2.2 使用 requestAnimationFrame
很多情况下,我们都习惯于使用 setInterval、setTimeout
来实现页面中的动画。也有很多小伙伴发现这种实现会出现丢帧的现象。原因有二:
-
setInterval、setTimeout
依赖于浏览器的异步循环,因而我们设定的动画执行时间可能并不是真正的动画执行时间,可能这个时候的js引擎
正在执行其他代码,所以就会出现丢帧的情况。 - 刷新频率收屏幕分辨率和屏幕尺寸影响。不同设备的屏幕刷新率可能不同。
针对以上两点内容,我们使用 requestAnimationFrame
来优化动画实现。相对于前者,它有两点明显的优势
- 由系统来决定回调的执行时机,在执行时浏览器会优化方法的调用。
- 按帧进行重绘,通过
requestAnimationFrame
调用回调函数引起的页面重绘或回流的时间间 隔和显示器的刷新时间间隔相同。所以requestAnimationFrame
不需要像setTimeout
那样传递时间间隔,而是浏览器通过系统获取并使用显示器刷新频率
2.3 离屏渲染
离屏渲染可以理解为创建一个备用canvas
但是不显示到页面上,执行预渲染操作。此项操作用到了drawImage
这个方法,此方法第一个参数可以接受一个图片对象或者是一个canvas
对象。
具体实现:
将需要操作的内容在离屏的canvas
上先处理好,之后再使用drawImage
方法放入到显示层。
2.4 分层画布
也是使用多个canvas
的方式,将静态的内容和需要频繁计算的内容分开渲染,越复杂的场景越适合使用此方法。具体实现如下
示意图:
通过这种方式可以将我们的需求拆分为多个模块。将需要频繁绘制的内容拆分出来,从而减少性能的消耗。
性能优化方式主要是一些日常的注意点和拆分方式,并不是万能的。
在canvas
的动画实现中,算法也占据了很大一部分,比如canvas
中的粒子操作,动辄就是成千上万的像素点,算法的使用不当可能会带来很大的问题。
好了,今天的分享就到这里了,临时插播的一条内容。接下来会分享关于 webgl
的内容,版兴趣的同学不要错过哟。Bye~