canvas动画实战与性能优化

插播一篇关于 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~

posted @ 2020-12-13 13:57  Sunmus  阅读(407)  评论(0编辑  收藏  举报