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,有这样的场景,游戏开发中,前景内容需要变化较快如每秒60帧,而背景可能速度较慢如每秒10帧,这样便可利用人眼的一些视觉特性达到一定程度的立体感(远远看山水的效果),这样会更吸引用户的眼球。
离屏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 局部刷新的方案:
- 清除指定区域的颜色,并设置 clip
- 所有同这个区域相交的图形重新绘制
要实现局部渲染时,需要考虑的两个因素是:
- 单次刷新时影响的范围最小
- 刷新的图形不会影响其他图形的正确绘制
清除画布内容(不建议参考)
我目前只是使用了clearRect(),没有做个实验对照。
请谨慎使用这一技巧,因为它很大程度上依赖于底层的canvas实现,因此很容易发生变化。
context.fillRect()//颜色填充 context.clearRect(0, 0, w, h) canvas.width = canvas.width; // 一种画布专用的技巧
避免使用阴影
减少使用 shadowBlur 效果,阴影渲染的性能开销通常比较高
context.shadowOffsetX = 5; context.shadowOffsetY = 5; context.shadowBlur = 4; context.shadowColor = 'rgba(255, 0, 0, 0.5)'; context.fillRect(20, 20, 150, 100);
坐标值尽量使用整数
避免使用浮点数坐标,使用非整数的坐标绘制内容,系统会自动使用抗锯齿功能,尝试对线条进行平滑处理,这又是一种性能消耗。
可以调用 Math.round 四舍五入取整,或者floor向上ceil向下取整,trunc直接丢弃小数位。对应的
当然性能最优越的方法莫过于将数值加0.5然后对所得结果进行移位运算以消除小数部分。
1 rounded = (0.5 + somenum) | 0; 2 rounded = ~~ (0.5 + somenum); 3 rounded = (0.5 + somenum) << 0;
避免大量计算造成阻塞
所谓「阻塞」,可以理解为不间断运行时间超过 16ms 的 JavaScript 代码,导致页面卡顿,丢帧,或者失去响应,这种问题能很快被用户察觉到,造成很差的交互体验。
所以我们要把与渲染无关的大量计算交给worker。大量计算可能造成渲染不流畅,但绝对不能让用户操作卡顿失去响应。
像下图的效果,需要计算大量函数曲线上的点来绘制成曲线,我们移动的时候可以看到计算新点坐标值的过程是有延迟的,但是并不会让用户鼠标拖拽卡顿失效,渲染的过程再跟随鼠标移动。
总结
以上便是总结到的提升绘制效率的几点建议!具体采用哪种需要在实际项目里面根据情况来定,如果你知道这几种方式至少不会大脑空白了!
还有几点开发过程需要注意的:
- 尽可能使用计算代替canvas渲染
- 减少改变 context 的状态,如果要改变请赋值正确的类型,减少浏览器的尝试