• canvas性能优化总结


    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 局部刷新的方案:

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

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

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

    清除画布内容(不建议参考)

    我目前只是使用了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 的状态,如果要改变请赋值正确的类型,减少浏览器的尝试
  • 相关阅读:
    Vuex ~ 初识
    Vue 2.0 生命周期-钩子函数理解
    vue利用watch侦听对象具体的属性 ~ 巧用计算属性computed做中间层
    Elements in iteration expect to have 'v-bind:key' directives.' 提示错误如何解决?
    微信小程序-如何自定义导航栏(navigationStyle)?
    微信小程序~触摸相关事件(拖拽操作、手势识别、多点触控)
    [Java] Collections
    [Java] Map / HashMap
    [Data Structure] 红黑树( Red-Black Tree )
    [Data Structure] 二叉搜索树(Binary Search Tree)
  • 原文地址:https://www.cnblogs.com/fangsmile/p/14721283.html
Copyright © 2020-2023  润新知