canvas离屏渲染优化

最近在作canvas粒子动画效果的研究,发现当粒子数量达到必定等级的时候,动画效果会变慢变卡。搜索了一下解决办法,发现离屏渲染是推荐最多的解决办法,那本文就利用离屏渲染实现一个动画效果来对比性能的提高。javascript

概念

查阅了一下资料,概述一下离屏渲染的概念,至关于在屏幕渲染的时候开辟一个缓冲区,将当前须要加载的动画事先在缓冲区渲染完成以后,再显示到屏幕上。html

非离屏渲染

非离屏渲染就是不创建缓冲区,直接在屏幕上逐个进行绘制,须要重复利用canvas的api。当粒子数量到达必定等级时,性能上会受到较大影响。前端

实现

先建立一个雪花粒子的类,构造相关的属性,定义一个名为snowArray的数组,将每一个粒子都存入该数组中。count为雪花的数量。java

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()chrome

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[]中的粒子信息,反复进行绘制。canvas

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();
    });
  }
复制代码

效果

完成以上的步骤以后,来看一下在浏览器中的效果api

小雪数组

非离屏小雪

中雪浏览器

非离屏中雪

大雪dom

非离屏大雪

性能分析

上述动图中,右上角为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的调用,反而离屏的过程增长了性能的消耗,这种状况就不合适采用这种方式。离屏渲染也须要选择合理的使用场景。

相关文章
相关标签/搜索