亲宝软件园·资讯

展开

canvas性能优化总结

方帅 人气:0

canvas的主要功能就是用来绘制内容,有时候为了给用户流畅的视觉感受,需要绘制的频率要求很高,这样对绘制的性能就有要求,那么怎么才能写出高性能的绘制代码呢。

尽可能少调用api

例如我们绘制一段线条,如果用如下代码的话,每移动一次就stroke一次:

1      for (var i = 0; i < points.length - 1; i++) {
2           var p1 = points[i];
3           var p2 = points[i + 1];
4           context.beginPath();
5           context.moveTo(p1.x, p1.y);
6           context.lineTo(p2.x, p2.y);
7           context.stroke();
8       } 

优化后代码如下,这样beginPah和stroke就少调用了n次。

1       context.beginPath();
2       for (var i = 0; i < points.length - 1; i++) {
3           var p1 = points[i];
4           var p2 = points[i + 1];
5           context.moveTo(p1.x, p1.y);
6           context.lineTo(p2.x, p2.y);
7       }
8       context.stroke();

尽量少改变CANVAS状态机

我们可以改变 context 的若干状态,而几乎所有的渲染操作,最终的效果与 context 本身的状态有关系。例如当对context.lineWidth赋值的话,开销远远大于对一个普通对象赋值的开销。

Canvas 上下文不是一个普通的对象,当调用了 context.lineWidth = 5 时,浏览器会需要立刻地做渲染上下文环境的工作,这样你下次调用诸如 stroke 或 strokeRect 等 API 时,画出来的线就正好是 5 个像素宽了。其实这也是浏览器自身的一种优化,否则如果等到stroke调用时再临时准备渲染环境,会更加影响正常绘制情况下的性能。

下面对比优化前后的代码:

      for (var i = 0; i < STRIPES; i++) {
          context.fillStyle = (i % 2 ? COLOR1 : COLOR2);
          context.fillRect(i * GAP, 0, GAP, 480);
      } 
      context.fillStyle = COLOR1;
      for (var i = 0; i < STRIPES / 2; i++) {
          context.fillRect((i * 2) * GAP, 0, GAP, 480);
      }
      context.fillStyle = COLOR2;
      for (var i = 0; i < STRIPES / 2; i++) {
          context.fillRect((i * 2 + 1) * GAP, 0, GAP, 480);
      }

上面两段代码,对fillStyle的调用时机做了改变,提高了性能。

分层canvas

绘制场景复杂的情况下,一般采用多个canvas,可依据绘制内容的频率高低来划分。

如游戏中的背景绘制频率低可以放在一层canvas上,上面的小人等绘制频率高放在一层canvas上,两层canvas的叠加效果达到完整效果。

如下图中绘制过程中的圆形在一层canvas上,不断清除不断绘制,而下面的已经绘制出来的笔迹内容放在另外一层canvas上,不需要清除重绘。

 

 

  

离屏canvas

也叫作预渲染,在离屏canvas上绘制好一整块图形,绘制好后在放到视图canvas中,适合每一帧画图运算复杂的图形。

比如我们有时候为了尽可能少的请求网络资源,会用到精灵图,这样在绘制精灵图某一块内容时,需要利用绘图api的裁剪。

实际发现,使用 drawImage 绘制一张大尺寸图片到较小画布区域上,比起绘制一张和绘制区域尺寸一样大的图片的情形,开销要大一些。可以认为,两者相差的开销正是「裁剪」这一个操作的开销。下面三种绘制方式,性能开销依次增加。

// 将image放到目标canvas指定位置,大小按照原图大小渲染
void ctx.drawImage(image, dx, dy); 
// 将image放到目标canvas指定位置,指定宽高渲染
void ctx.drawImage(image, dx, dy, dWidth, dHeight);
// 将image裁剪之后放到目标canvas指定位置,指定宽高渲染
void ctx.drawImage(image, sx, sy, sWidth, sHeight, dx, dy, dWidth, dHeight);

而离屏渲染就可以让我们先把图片裁剪成想要的尺寸内容保存起来,等到真正绘制的时候就可以使用第一种写法简单的把图片绘制出来。

// 在离屏 canvas 上绘制
var offscreencanvas = document.createElement('canvas');
// 宽高赋值为想要的图片尺寸
offscreencanvas.width = dWidth;
offscreencanvas.height = dHeight;
// 裁剪
offscreencanvas.getContext('2d').drawImage(image, sx, sy, sWidth, sHeight, dx, dy, dWidth, dHeight);
// 在视图canvas中绘制
viewcontext.drawImage(canvas, x, y);

有时候,游戏对象是多次调用 drawImage 绘制而成,或者根本不是图片,而是使用路径绘制出的矢量形状,那么离屏绘制还能帮你把这些操作简化为一次 drawImage 调用。

组合图形组合了多个图形将它们绘制存放到离屏canvas中,下次未变化的时候直接绘制一次离屏canvas。

裁剪

Canvas (大小一般小于等于屏幕宽高)只是整个大场景下的一个「可视窗口」,如果我们在每一帧中,都把全部内容画出来,势必就会有很多东西画到 Canvas 外面去了,同样调用了绘制 API,但是并没有任何效果。

那么视口外的内容是不需要绘制的,但如果绘制对性能影响有多少呢?进行这样一个实验,绘制一张 320x180 的图片 104 次,当每次都绘制在 Canvas 内部时,消耗了 40ms,而每次都绘制在 Canvas 外时,仅消耗了 8ms。虽然绘制在canvas外时,消耗的时间较短。

但考虑到计算的开销与绘制的开销相差 2~3 个数量级,所以一般情况下通过计算来过滤掉哪些画布外的对象,仍然是很有必要的。

局部重绘

由于 Canvas 的绘制方式是画笔式的,在 Canvas 上绘图时每调用一次 API 就会在画布上进行绘制,一旦绘制就成为画布的一部分。绘制图形时并没有对象保存下来,一旦图形需要更新,需要清除整个画布重新绘制。

如下图仅对红边框的平行四边形做改变,如果每次重绘整个画布内容就不太合适

 Canvas 局部刷新的方案:

  1. 清除指定区域的颜色,并设置 clip
  2. 所有同这个区域相交的图形重新绘制

要实现局部渲染时,需要考虑的两个因素是:

  • 单次刷新时影响的范围最小
  • 刷新的图形不会影响其他图形的正确绘制

清除画布内容

有地方说这三种方法性能依次提高,我目前只是使用了clearRect(),没有做个实验对照。 

context.fillRect()//颜色填充
context.clearRect(0, 0, w, h)
canvas.width = canvas.width; // 一种画布专用的技巧

坐标值尽量使用整数

避免使用浮点数坐标,使用非整数的坐标绘制内容,系统会自动使用抗锯齿功能,尝试对线条进行平滑处理,这又是一种性能消耗。

可以调用 Math.round 四舍五入取整。

避免大量计算造成阻塞

所谓「阻塞」,可以理解为不间断运行时间超过 16ms 的 JavaScript 代码,导致页面卡顿,丢帧,或者失去响应,这种问题能很快被用户察觉到,造成很差的交互体验。

 

 所以我们要把与渲染无关的大量计算交给worker。大量计算可能造成渲染不流畅,但绝对不能让用户操作卡顿失去响应。

像下图的效果,需要计算大量函数曲线上的点来绘制成曲线,我们移动的时候可以看到计算新点坐标值的过程是有延迟的,但是并不会让用户鼠标拖拽卡顿失效,渲染的过程再跟随鼠标移动。

 

总结

以上便是总结到的提升绘制效率的几点建议!具体采用哪种需要在实际项目里面根据情况来定,如果你知道这几种方式至少不会大脑空白了!

还有几点开发过程需要注意的:

  • 尽可能使用计算代替canvas渲染
  • 减少改变 context 的状态,如果要改变请赋值正确的类型,减少浏览器的尝试
  • 减少使用 shadowBlur 效果,阴影渲染的性能开销通常比较高

加载全部内容

相关教程
猜你喜欢
用户评论