美文网首页Hybrid开发WEB前端程序开发Web前端之路
【高级系列】Canvas绘制性能专题

【高级系列】Canvas绘制性能专题

作者: Kevin_Junbaozi | 来源:发表于2018-05-05 00:12 被阅读36次

    总原则:

            在移动APP中,因为手机硬件性能有限,其实不宜做太多特效,应该往简洁突出重点的方向考虑。

    1 性能建议

    英文引文地址:

    http://www.html5rocks.com/en/tutorials/canvas/performance/

    提高HTML5 canvas性能的几种方法!

    http://blog.csdn.net/zyz511919766/article/details/7401792

    1.1 预渲染

    1.PRE-RENDER TO AN OFF-SCREEN CANVAS

            预渲染即在一个或者多个临时的不会在屏幕上显示的canvas中渲染临时的图像,然后再把这些不可见的canvas作为图像渲染到可见的canvas中。对于计算机图形学比较熟悉的朋友应该都知道,这个技术也被称做display list

            没有预渲染的情况:

    // canvas, context are defined

    function render() {

        drawMario(context);

        requestAnimationFrame(render);

    }

            预渲染的情况:

    var m_canvas = document.createElement('canvas');

    m_canvas.width = 64;

    m_canvas.height = 64;

    var m_context = m_canvas.getContext(‘2d’);

    drawMario(m_context);

    function render() {

        context.drawImage(m_canvas, 0, 0);

        requestAnimationFrame(render);

    }  

            关于requestAnimationFrame的使用方法将在后续部分做详细的讲述。当渲染操作(例如上例中的drawmario)开销很大时该方法将非常有效。其中很耗资源的文本渲染操作就是一个很好的例子。

            要确保临时的canvas恰好适应你准备渲染的图片的大小,否则过大的canvas会导致我们获取的性能提升被将一个较大的画布复制到另外一个画布的操作带来的性能损失所抵消掉。

            上述的测试用例中紧凑的canvas相当的小:

    can2.width = 100;

    can2.height = 40;

            如下宽松的canvas将导致糟糕的性能:

    can3.width = 300;  

    1.2 多条指令一次载入

    2.BATCH CANVAS CALLS TOGETHER

            因为绘图是一个代价昂贵的操作,因此,用一个长的指令集载入将绘图状态机载入,然后再一股脑的全部写入到video缓冲区。这样会会更佳有效率。

            例如,当需要画对条线条时先创建一条包含所有线条的路经然后用一个draw调用将比分别单独的画每一条线条要高效的多:

    for (var i = 0; i < points.length - 1; i++) {

      var p1 = points[i];

      var p2 = points[i+1];

      context.beginPath();

      context.moveTo(p1.x, p1.y);

      context.lineTo(p2.x, p2.y);

      context.stroke();

    }

            通过绘制一个包含多条线条的路径我们可以获得更好的性能:

    context.beginPath();

    for (var i = 0; i < points.length - 1; i++) {

      var p1 = points[i];

      var p2 = points[i+1];

      context.moveTo(p1.x, p1.y);

      context.lineTo(p2.x, p2.y);

    }

    context.stroke();

            这个方法也适用于HTML5 canvas。比如,当我们画一条复杂的路径时,将所有的点放到路径中会比分别单独的绘制各个部分要高效的多(jsperf):

            然而,需要注意的是,对于canvas来说存在一个重要的例外情况:若欲绘制的对象的部件中含有小的边界框(例如,垂直的线条或者水平的线条),那么单独的渲染这些线条或许会更加有效(jsperf

    1.3 避免不必要的状态切换

    3.AVOID UNNECESSARY CANVAS STATE CHANGES

            HTML5 canvas元素是在一个状态机之上实现的。状态机可以跟踪诸如fill、stroke-style以及组成当前路径的previous points等等。在试图优化绘图性能时,我们往往将注意力只放在图形渲染上。实际上,操纵状态机也会导致性能上的开销。

            例如,如果你使用多种填充色来渲染一个场景,按照不同的颜色分别渲染要比通过canvas上的布局来进行渲染要更加节省资源。为了渲染一副条纹的图案,你可以这样渲染:用一种颜色渲染一条线条,然后改变颜色,渲染下一条线条,如此反复:

    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);

    }  

    1.4 只重绘变化部分而不是全部重绘

    4.RENDER SCREEN DIFFERENCES ONLY, NOT THE WHOLE  NEW STATE

            在屏幕上绘制较少的东西要比绘制大量的东西节省资源。重绘时如果只有少量的差异你可以通过仅仅重绘差异部分来获得显著的性能提升。换句话说,不要在重绘前清除整个画布。:

    context.fillRect(0, 0, canvas.width, canvas.height);

            跟踪已绘制部分的边界框,仅仅清理这个边界之内的东西:

    context.fillRect(last.x, last.y, last.width, last.height);  

            如果您对计算机图形学比较熟悉,你或许应该知道这项技术也叫做“redraw technique”,这项技术会保存前一个渲染操作的边界框,下一次绘制前仅仅清理这一部分的内容。这项技术也适用于基于像素的渲染环境。这篇名为JavaScript NIntendo emulator tallk的文章说明了这一点。

    1.5 使用多图层绘制复杂场景

    5.USE MUTIPLE LAYERED CANVASES FOR COMPLEX SCENES

            我们前边提到过,绘制一副较大的图片代价是很高昂的因此我们应尽可能的避免。除了前边讲到的利用另外得不可见的canvas进行预渲染外,我们也可以叠在一起的多层canvas。利用前景的透明度,我们可以在渲染时依靠GPU整合不同的alpha值。你可以像如下这么设置,两个绝对定位的canvas一个在另一个的上边:

            相对于仅仅有一个canvas的情况来讲,这个方法的优势在于,当我们需要绘制或者清理前景canvas时,我们不需要每次都修改背景canvas。如果你的游戏或者多媒体应用可以分成前景和背景这样的情况,那么请考虑分别渲染前景和背景来获取显著的性能提升。

            你可以用相较慢的速度(相对于前景)来渲染背景,这样便可利用人眼的一些视觉特性达到一定程度的立体感,这样会更吸引用户的眼球。比如,你可以在每一帧中渲染前景而仅仅每N帧才渲染背景。

            注意,这个方法也可以推广到包含更多canvas曾的复合canvas。如果你的应用利用更多的曾会运行的更好时请利用这种方法。

    1.6 减少使用阴影效果

    6.AVOID SHADOWBLUR

            跟其他很多绘图环境一样,HTML5 canvas允许开发者对绘图基元使用阴影效果,然而,这项操作是相当耗费资源的。

    context.shadowOffsetX = 5;

    context.shadowOffsetY = 5;

    context.shadowBlur = 4;

    context.shadowColor = 'rgba(255, 0, 0, 0.5)';

    context.fillRect(20, 20, 150, 100);  

    1.7 熟悉多种重绘方法

    7.KNOW VARIOUS WAYS TO CLEAR THE CANVAS

            因为HTML5 canvas 是一种即时模式immediate mode)的绘图范式(drawing paradigm),因此场景在每一帧都必需重绘。正因为此,清楚canvas的操作对于 HTML5 应用或者游戏来说有着根本的重要性。

            正如在 避免 canvas 状态变化的一节中提到的,清楚整个canvas的操作往往是不可取的。如果你必须这样做的话有两种方法可供选择:调用

    context.clearRect(0, 0, width, height)

            或者使用 canvas特定的一个技巧

    canvas.width = canvas.width

            在书写本文的时候,cleaRect方法普遍优越于重置canvas宽度的方法。但是,在某些情况下,在Chrome14中使用重置canvas宽度的技巧要比clearRect方法快很多(jsperf):

            请谨慎使用这一技巧,因为它很大程度上依赖于底层的canvas实现,因此很容易发生变化,欲了解更多信息请参见 Simon Sarris 的关于清除画布的文章

    1.8 减少浮点坐标绘制

    8. AVOID FLOATING POINT COORDINATES

            HTML5 canvas 支持子像素渲染(sub-pixel rendering),而且没有办法关闭这一功能。如果你绘制非整数坐标他会自动使用抗锯齿失真以使边缘平滑。以下是相应的视觉效果(参见Seb Lee-Delisle的关于子像素画布性能的文章

            如果平滑的精灵并非您期望的效果,那么使用 Math.floor方法或者Math.round方法将你的浮点坐标转换成整数坐标将大大提高运行速度(jsperf):

            为使浮点坐标抓换为整数坐标你可以使用许多聪明的技巧,其中性能最优越的方法莫过于将数值加0.5然后对所得结果进行移位运算以消除小数部分。

    // With a bitwise or.

    rounded = (0.5 + somenum) | 0;

    // A double bitwise not.

    rounded = ~~ (0.5 + somenum);

    // Finally, a left bitwise shift.

    rounded = (0.5 + somenum) << 0;

            两种方法性能对比如下(jsperf):

    1.9 尽量使用requeatAnimationFrame方法执行动画

    9.OPTIMIZE YOUR ANIMATIONS WITH ‘REQUESTANIMATIONFRAME’

            相对较新的 requeatAnimationFrame API是在浏览器中实现交互式应用的推荐标准。与传统的以固定频率命令浏览器进行渲染不同,该方法可以更友善的对待浏览器,它会在浏览器可用的时候使其来渲染。这样带来的另外一个好处是当页面不可见的时候,它会很聪明的停止渲染。

            requestAnimationFrame调用的目标是以60帧每秒的速度来调用,但是他并不能保证做到。所以你要跟踪从上一次调用导线在共花了多长时间。这看起来可能如下所示:

    var x = 100;

    var y = 100;

    var lastRender = new Date();

    function render() {

        var delta = newDate() - lastRender;

        x += delta;

        y += delta;

        context.fillRect(x, y, W, H);

        requestAnimationFrame(render);

    }

    render(); 

            注意requestAnimationFrame不仅仅适用于canvas 还适用于诸如WebGL的渲染技术。

            在书写本文时,这个API仅仅适用于Chrome,Safari以及Firefox,所以你应该使用这一代码片段

    1.10 职责分离

            与渲染无关的计算交给worker,复杂的计算交给引擎(自己写,或者用开源的),比如3D、物理 。缓存load好的图片,canvas上画canvas,而不是画image。

    2 图层优化

    2.1 多层半透明优化处理

    2.1.1 范例1——模拟波浪性能优化

    2.1.1.1 绘制机制

            在最近这个项目中,有一个模拟波浪的特效,绘制原理是用多层半透明Canvas进行叠加:

        1、全局用三层半透明Canvas叠加,各层透明度分别为0.5、0.6、0.8;

        2、每层Canvas中利用滤镜功能截取上边沿图形,而截取的高度则是利用正弦函数结合振幅值与频率进行绘制,再通过一定刷新频率将绘图刷新以及平移,以形成动态效果;

        3、各层的振幅与频率不同,但刷新频率一致,故各层叠加在一起后即形成三道波浪图形;

        结语:

            这样做出来的效果比较逼真,但是性能损坏很大,在iPhone4s上,因为屏幕渲染开销太大,已经导致界面响应事件失效了。因为屏幕绘制时,每个像素点上的颜色计算,需要集合三层Canvas的透明度来计算,非常损耗CPU性能。

    2.1.1.2 代码

    2.1.1.2.1 HTML代码:

    2.1.1.2.2 CSS代码

    .control_panel_wrap .filter_info_wrap .controlPanel_animation_wrap .waves_wrap #waves1

    {

        position: absolute;

        top: 100px;

       filter: alpha(Opacity=80);

        opacity:.8

    }

    .control_panel_wrap .filter_info_wrap .controlPanel_animation_wrap .waves_wrap #waves2

    {

        position: absolute;

        top:100px;

       filter: alpha(Opacity=70);

        opacity:.7

    }

    .control_panel_wrap .filter_info_wrap .controlPanel_animation_wrap .waves_wrap #waves3

    {

        position: absolute;

        top:100px;

       filter: alpha(Opacity=50);

        opacity:.5

    }

    2.1.1.2.3 JS代码:

    function createWave(canvasId, amplitude, frequency) {

        if (typeof(Humble)== 'undefined') window.Humble = {};

        Humble.Trig = {};

        Humble.Trig.init =init;

        var canvas, context, height, width, xAxis, yAxis, draw;

        //设置振幅和频率

        var amplitude = amplitude || 10;

        var frequency = frequency || 0.2;

        /**

         * Init function.

         *

         * Initialize variables and begin the animation.

         */

        function init() {

          canvas = document.getElementById(canvasId);

          canvas.width = 320;

          canvas.height = 120;//320

          context = canvas.getContext("2d");

          height = canvas.height;

          width = canvas.width;

          xAxis =Math.floor(height/2);

          yAxis = 0;

          context.save();

          draw();

        }

        /**

         * Draw animation function.

         *

         * This function draws one frame of the animation, waits 20ms, and then calls

         * itself again.

         */

        draw = function (){

          // Clear the canvas

         context.clearRect(0, 0, width, height);

          // Set styles for animated graphics

          var waveGradient= context.createLinearGradient(0, 0, 0, canvas.height);

          waveGradient.addColorStop(0, 'rgba(255,255,255,1)');

          waveGradient.addColorStop(1, 'rgba(255,255,255,0)');

          context.save();

          context.strokeStyle = waveGradient;

          context.fillStyle = waveGradient;

          context.lineWidth = 1;

          // Draw the sine curve at time draw.t, as well as the circle.

          context.beginPath();

          drawSine(draw.t);

          context.stroke();

           // Restore original styles

          context.restore();

          // Update the time and draw again

          draw.seconds = draw.seconds - .007;

          draw.t = draw.seconds*Math.PI;

          setTimeout(draw,35);

        };

        draw.seconds = 0;

        draw.t = 0;

        /**

         * Function to draw sine

         *

         * The sine curve is drawn in 10px segments starting at the origin.

         */

        function drawSine(t) {

          // Set the initial x and y, starting at 0,0 and translating to the origin on

          // the canvas.

          var x = t;

          var y = Math.sin(x);

         context.moveTo(yAxis, amplitude*y+xAxis);

          // Loop to draw segments

          for (i = yAxis; i <= width; i += 10) {

             x =t+(-yAxis+i)/amplitude*frequency;

             y =Math.sin(x);

            context.lineTo(i, amplitude*y+xAxis);

          }

          context.lineTo(canvas.width, canvas.height);

          context.lineTo(0, canvas.height);

    //     context.stroke();

           context.fill();

        }

        Humble.Trig.init()

      }

       //测试到底是哪个效果好计算资源

      //生成三条波浪,比较耗资源,至少有50%的几率造成事件阻塞

      createWave('waves1');

      createWave('waves2',15, 1/11);

      createWave('waves3',20, 1/4);

    2.1.1.3 优化措施1——减少不变部分范围:

            特效其实只是要求上边沿有滤镜效果,变动部分相对较少,下半部分都是不会变动,故考虑缩减Canvas高度,将下半部分分离出来做纯色出来或者只贴一层半透明Canvas,透明度值直接计算出来。

            本APP中简单处理,将原来320*320尺寸的Canvas缩减为320*120,再将Canvas下移200px,直接缩减叠加层范围,较少渲染计算范围,提高APP性能。

    2.1.1.4 优化措施2——直接计算半透明度

    性能优化思路:

            三个半透明层叠加,是否可以在一层中处理,对于不同坐标值,直接计算出颜色值,然后渲染,变量包括时间t、横坐标x,计算分支判断依据是纵坐标y。

    3 参考链接

    Immediate mode vs. retained mode.

    Other HTML5 Rocks canvas articles.

    The Canvas section ofDive into HTML5.

    JSPerf lets developers create JS performance tests.

    Browserscope stores browser performance data.

    JSPref view, which renders JSPerf tests as charts.

    Simon's blog post on clearing the canvas.

    Sebastian's blog post on sub-pixel rendering performance.

    Paul's blog post on using the requestAnimationFrame.

    Ben's talk about optimizing a JS NES emulator.

    相关文章

      网友评论

        本文标题:【高级系列】Canvas绘制性能专题

        本文链接:https://www.haomeiwen.com/subject/bmajrftx.html