性能篇29 # 怎么给Canvas绘制加速?

Posted 凯小默

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了性能篇29 # 怎么给Canvas绘制加速?相关的知识,希望对你有一定的参考价值。

说明

【跟月影学可视化】学习笔记。

方法一:优化 Canvas 指令

例子:实现一些位置随机的多边形,并且不断刷新这些图形的形状和位置

<!DOCTYPE html>
<html lang="en">
    <head>
        <meta charset="UTF-8" />
        <meta http-equiv="X-UA-Compatible" content="IE=edge" />
        <meta name="viewport" content="width=device-width, initial-scale=1.0" />
        <title>优化 Canvas 指令</title>
        <style>
            canvas 
                border: 1px dashed #fa8072;
            
        </style>
    </head>
    <body>
        <canvas width="500" height="500"></canvas>
        <script>
            const canvas = document.querySelector("canvas");
            const ctx = canvas.getContext("2d");

            // 创建正多边形,返回顶点
            function regularShape(x, y, r, edges = 3) 
                const points = [];
                const delta = (2 * Math.PI) / edges;
                for (let i = 0; i < edges; i++) 
                    const theta = i * delta;
                    points.push([
                        x + r * Math.sin(theta),
                        y + r * Math.cos(theta),
                    ]);
                
                return points;
            

            // 根据顶点绘制图形
            function drawShape(context, points) 
                context.fillStyle = "red";
                context.strokeStyle = "black";
                context.lineWidth = 2;
                context.beginPath();
                context.moveTo(...points[0]);
                for (let i = 1; i < points.length; i++) 
                    context.lineTo(...points[i]);
                
                context.closePath();
                context.stroke();
                context.fill();
            

            // 多边形类型,包括正三角形、正四边形、正五边形、正六边形和正100边形、正500边形
            const shapeTypes = [3, 4, 5, 6, 100, 500];
            const COUNT = 1000;

            // 执行绘制
            function draw() 
                ctx.clearRect(0, 0, canvas.width, canvas.height);
                for (let i = 0; i < COUNT; i++) 
                    const type =
                        shapeTypes[
                            Math.floor(Math.random() * shapeTypes.length)
                        ];
                    const points = regularShape(
                        Math.random() * canvas.width,
                        Math.random() * canvas.height,
                        10,
                        type
                    );
                    drawShape(ctx, points);
                
                requestAnimationFrame(draw);
            

            draw();
        </script>
    </body>
</html>

我们f12查看帧率,效果如下:Google Chrome浏览器怎么开启查看帧率功能?

对于一个 500 边形来说,它的顶点数量非常多,所以 Canvas 需要执行的绘图指令也会非常多,那绘制很多个 500 边形自然会造成性能问题。

下面减少绘制 500 边形的绘图指令的数量:用 -1 代替正 500 边形,如果type小于0表名多边形是正500边形,用 arc 指令来画圆

<!DOCTYPE html>
<html lang="en">
    <head>
        <meta charset="UTF-8" />
        <meta http-equiv="X-UA-Compatible" content="IE=edge" />
        <meta name="viewport" content="width=device-width, initial-scale=1.0" />
        <title>优化 Canvas 指令</title>
        <style>
            canvas 
                border: 1px dashed #fa8072;
            
        </style>
    </head>
    <body>
        <canvas width="500" height="500"></canvas>
        <script>
            const canvas = document.querySelector("canvas");
            const ctx = canvas.getContext("2d");

            // 创建正多边形,返回顶点
            function regularShape(x, y, r, edges = 3) 
                const points = [];
                const delta = (2 * Math.PI) / edges;
                for (let i = 0; i < edges; i++) 
                    const theta = i * delta;
                    points.push([
                        x + r * Math.sin(theta),
                        y + r * Math.cos(theta),
                    ]);
                
                return points;
            

            // 根据顶点绘制图形
            function drawShape(context, points) 
                context.fillStyle = "red";
                context.strokeStyle = "black";
                context.lineWidth = 2;
                context.beginPath();
                context.moveTo(...points[0]);
                for (let i = 1; i < points.length; i++) 
                    context.lineTo(...points[i]);
                
                context.closePath();
                context.stroke();
                context.fill();
            

            // 多边形类型,包括正三角形、正四边形、正五边形、正六边形和正100边形以及正500边形
            // 用 -1 代替正 500 边形
            const shapeTypes = [3, 4, 5, 6, 100, -1];
            const COUNT = 1000;
            const TAU = Math.PI * 2;

            // 执行绘制
            function draw() 
                ctx.clearRect(0, 0, canvas.width, canvas.height);
                for (let i = 0; i < COUNT; i++) 
                    const type =
                        shapeTypes[
                            Math.floor(Math.random() * shapeTypes.length)
                        ];
                    const x = Math.random() * canvas.width;
                    const y = Math.random() * canvas.height;
                    // 如果type小于0表名多边形是正500边形
                    if(type > 0) 
                        // 画正多边形
                        const points = regularShape(x, y, 10, type);
                        drawShape(ctx, points);
                     else 
                        // 画圆
                        ctx.beginPath();
                        // 绘制正多边形,否则用 arc 指令来画圆
                        ctx.arc(x, y, 10, 0, TAU);
                        ctx.stroke();
                        ctx.fill();
                    
                
                requestAnimationFrame(draw);
            

            draw();
        </script>
    </body>
</html>

优化完之后的效果:

方法二:使用缓存

具体做法就是将图形缓存下来,保存到离屏的 Canvas(offscreen Canvas)中,然后在绘制的时候作为图像来渲染,那就可以将绘制顶点的绘图指令变成直接通过 drawImage 指令来绘制图像,而且也不需要 fill() 方法来填充图形。

https://developer.mozilla.org/zh-CN/docs/Web/API/OffscreenCanvas

OffscreenCanvas 提供了一个可以脱离屏幕渲染的 canvas 对象。它在窗口环境和web worker环境均有效。

<!DOCTYPE html>
<html lang="en">
    <head>
        <meta charset="UTF-8" />
        <meta http-equiv="X-UA-Compatible" content="IE=edge" />
        <meta name="viewport" content="width=device-width, initial-scale=1.0" />
        <title>使用缓存</title>
        <style>
            canvas 
                border: 1px dashed #fa8072;
            
        </style>
    </head>
    <body>
        <canvas width="500" height="500"></canvas>
        <script>
            const canvas = document.querySelector("canvas");
            const ctx = canvas.getContext("2d");

            // 创建缓存的函数
            function createCache() 
                const ret = [];
                for (let i = 0; i < shapeTypes.length; i++) 
                    // 创建离屏Canvas缓存图形
                    const cacheCanvas = new OffscreenCanvas(20, 20);
                    // 将图形绘制到离屏Canvas对象上
                    const type = shapeTypes[i];
                    const context = cacheCanvas.getContext("2d");
                    context.fillStyle = "red";
                    context.strokeStyle = "black";
                    if (type > 0) 
                        const points = regularShape(10, 10, 10, type);
                        drawShape(context, points);
                     else 
                        context.beginPath();
                        context.arc(10, 10, 10, 0, TAU);
                        context.stroke();
                        context.fill();
                    
                    ret.push(cacheCanvas);
                
                // 将离屏Canvas数组(缓存对象)返回
                return ret;
            

            // 创建正多边形,返回顶点
            function regularShape(x, y, r, edges = 3) 
                const points = [];
                const delta = (2 * Math.PI) / edges;
                for (let i = 0; i < edges; i++) 
                    const theta = i * delta;
                    points.push([
                        x + r * Math.sin(theta),
                        y + r * Math.cos(theta),
                    ]);
                
                return points;
            

            // 根据顶点绘制图形
            function drawShape(context, points) 
                context.fillStyle = "red";
                context.strokeStyle = "black";
                context.lineWidth = 2;
                context.beginPath();
                context.moveTo(...points[0]);
                for (let i = 1; i < points.length; i++) 
                    context.lineTo(...points[i]);
                
                context.closePath();
                context.stroke();
                context.fill();
            

            // 多边形类型,包括正三角形、正四边形、正五边形、正六边形和正100边形以及正500边形
            const shapeTypes = [3, 4, 5, 6, 100, 500];
            const COUNT = 1000;
            const TAU = Math.PI * 2;

            // 一次性创建缓存,直接通过缓存来绘图
            const shapes = createCache();

            function draw() 
                ctx.clearRect(0, 0, canvas.width, canvas.height);
                for (let i = 0; i < COUNT; i++) 
                    const shape = shapes[Math.floor(Math.random() * shapeTypes.length)];
                    const x = Math.random() * canvas.width;
                    const y = Math.random() * canvas.height;
                    ctx.drawImage(shape, x, y);
                
                requestAnimationFrame(draw);
            

            draw();
        </script>
    </body>
</html>

开启缓存效果:

缓存的局限性

  1. 如果需要创建大量的离屏 Canvas 对象,就会对内存消耗就非常大,有可能反而降低了性能。
  2. 缓存适用于图形状态本身不变的图形元素,如果是经常发生状态改变的图形元素,起不到减少绘图指令的作用。
  3. 不使用缓存直接绘制的是矢量图,而通过缓存 drawImage 绘制出的则是位图,所以缓存绘制的图形,在清晰度上可能不是很好。

方法三:分层渲染

简单点说就是用两个 Canvas 叠在一起,将不变的元素绘制在一个 Canvas 中,变化的元素绘制在另一个 Canvas 中。

满足两个条件

  • 一是有大量静态的图形元素不需要重新绘制
  • 二是动态和静态图形元素绘制顺序是固定的,先绘制完静态元素再绘制动态元素

上面就是两个canvas,一个动的,一个静态的,我们把它们叠在一起

<!DOCTYPE html>
<html lang="en">
    <head>
        <meta charset="UTF-8" />
        <<

以上是关于性能篇29 # 怎么给Canvas绘制加速?的主要内容,如果未能解决你的问题,请参考以下文章

性能篇30 # 怎么给WebGL绘制加速?

性能篇30 # 怎么给WebGL绘制加速?

canvas绘制多边形

h5 Canvas正多边形绘制

Canvas:绘制线和填充多边形

canvas绘制直线和多边形基本操作