【Canvas系列】通过离屏渲染提高 Canvas 书写性能

前言

上一节 通过上下分层优化 Canvas 书写性能 优化了 Canvas 的书写性能,接下来我们通过离屏渲染的方式,进一步优化 Canvas 的书写性能。

基本思路

在书写的过程中,每绘制一笔都需要不断地调用 Canvas 的 API,重新渲染整个 Canvas,这样就会导致性能的浪费。

而离屏渲染则是将 绘制内容存储到离屏的 Canvas 中,相当于一个缓冲区,然后将需要绘制的画面在离屏的 Canvas 缓冲好,最后将离屏的 Canvas 转化成图片,渲染到屏幕上,这样就可以达到优化性能的目的。

实现

创建离屏 Canvas

思路如下: 基于上一节的基础,我们改写 render 函数,如果是离屏渲染的话,将绘制的内容存储到离屏的 Canvas 中,然后将离屏的 Canvas 缓存起来,下次绘制的时候,如果命中缓存的话,就直接使用缓存的 Canvas,从而达到优化性能的目的。

操作如下:

  • 1 在执行 render 函数之前,先判断是否存在缓存的 Canvas,如果存在的话,就直接使用缓存的 Canvas
  • 2 如果命中缓存,使用离屏 Canvas 转化成图片进行绘制
  • 3 如果不存在缓存的 Canvas,就创建一个离屏的 Canvas,然后将绘制的内容存储到离屏的 Canvas 中,最后将离屏的 Canvas 缓存起来
js 复制代码
        const elementWithCanvasCache = new WeakMap(); // 用于存储离屏 Canvas 的缓存
        const generateOffScreenCanvas = (points) => {
            const padding = 20; // 避免笔记被 Canvas 
            const canvas = document.createElement('canvas'); // 创建一个离屏 Canvas
            const ctxContent = canvas.getContext('2d');
            
            // TODO 绘制的内容存储
            // ....

            // 将离屏 Canvas 缓存起来
            elementWithCanvasCache.set(points, {
                canvas,
            });
            return canvas;
        }

        /**
         * 绘制函数
         * @param {*} ctx - canvas 尺寸
         * @param {*} points - 鼠标移动的点集
         * @return 返回一个 canvas 元素
         */
        function render(ctx, points, isOffScreen = false) {
            /*
              判断是否存在缓存元素,存在的话使用缓存元素,绘制
            */
            if (isOffScreen && elementWithCanvasCache.has(points)) {
                const { canvas, x, y, width, height } = elementWithCanvasCache.get(points);
                ctx.save();
                ctx.scale(1 / dpr, 1 / dpr);
                ctx.drawImage(
                    canvas,
                    x,
                    y,
                    canvas.width,
                    canvas.height
                );
                ctx.restore();
                console.log(`命中了🎯`)
                return;
            }

            ctx.strokeStyle = 'red'; // 设置线条颜色
            ctx.lineWidth = 6; // 设置线条宽度
            ctx.lineJoin = 'round'; // 设置线条连接处的样式
            ctx.lineCap = 'round'; // 设置线条末端的样式

            /*
            beginPath() 是 Canvas 2D API 中的一个方法,用于开始一个新的路径。当你想创建一个新的路径时,你需要调用这个方法。
            例如,你可能会这样使用它:
                context.beginPath();
                context.moveTo(50, 50);
                context.lineTo(200, 50);
                context.stroke();
                在这个例子中,beginPath() 开始一个新的路径,moveTo(50, 50) 将路径的起点移动到 (50, 50),lineTo(200, 50) 添加一条从当前位置到 (200, 50) 的线,
                最后 stroke() 方法绘制出路径。
                其中 context 是你的 canvas 上下文。
            */
            ctx.beginPath(); // 开始绘制

            ctx.moveTo(points[0].x, points[0].y); // 将画笔移动到起始点

            for (let i = 1; i < points.length; i++) {
                // 取终点,将上一个点作为控制点,平滑过渡
                const cx = (points[i].x + points[i - 1].x) / 2;
                const cy = (points[i].y + points[i - 1].y) / 2;
                ctx.quadraticCurveTo(points[i - 1].x, points[i - 1].y, cx, cy);
            }

            ctx.stroke(); // 绘制路径

            if (isOffScreen) {
                generateOffScreenCanvas(points);
            }
        }

数据的切换

这里是离屏 Canvas 的难点,即如何将绘制的坐标转化到对应的离屏 Canvas 中,这里笔者通过计算坐标的偏移量,将坐标转化到离屏 Canvas 中。

即关键是 generateOffScreenCanvas 函数如何实现将绘制的内容存储到离屏的 Canvas 中。

实现思路:

  • 1 获取到绘制图形的最小的点和最大的点,从而计算出宽高
  • 2 获取最小的点坐标,从而计算出相对于离屏 Canvas 的坐标集合
  • 3 将点绘制到离屏 Canvas 中
  • 4 将离屏 Canvas 缓存起来
  • 5 将离屏 Canvas 转化成图片进行绘制
js 复制代码
    const getBoundsFromPoints = (points) => {
        let minX = Infinity;
        let minY = Infinity;
        let maxX = -Infinity;
        let maxY = -Infinity;
        for (const { x, y } of points) {
            minX = Math.min(minX, x);
            minY = Math.min(minY, y);
            maxX = Math.max(maxX, x);
            maxY = Math.max(maxY, y);
        }
        return [minX, minY, maxX, maxY];
    };

      /*
      1 获取当前元素的坐标,相对于离屏 Canvas 的坐标
      2 获取 Canvas 的宽高
      */
      const getElementAbsoluteCoords = (points) => {
          const [minX, minY, maxX, maxY] = getBoundsFromPoints(points);
          const width = maxX - minX;
          const height = maxY - minY;
          return {
              minX,
              minY,
              width,
              height,
              points: points.map(({ x, y }) => ({ x: Math.round(x - minX), y: Math.round(y - minY) })) // 获取当前元素的坐标,相对于离屏 Canvas 的坐标
          };
      };

    const generateOffScreenCanvas = (points) => {
          const canvas = document.createElement('canvas'); // 创建一个离屏 Canvas
          const ctxContent = canvas.getContext('2d');
          ctxContent.save();
          // 获取最小的点和最大的点
          const { minX, minY, width: realWidth, height: realHeight, points: realPoints } = getElementAbsoluteCoords(points);

          console.log(`realWidth---->`, realWidth, Math.floor(minX));
          console.log(`realHeight---->`, realHeight, Math.floor(minY));
          canvas.width = realWidth * dpr + padding * 2;
          canvas.height = realHeight * dpr + padding * 2;
          canvas.style.width = realWidth + "px";
          canvas.style.height = realHeight + "px";
          ctxContent.translate(padding, padding); // 将坐标轴原点移动到(20, 20)
          ctxContent.scale(dpr, dpr);
          ctxContent.strokeStyle = 'red'; // 设置线条颜色
          ctxContent.lineWidth = 6; // 设置线条宽度
          ctxContent.lineJoin = 'round'; // 设置线条连接处的样式
          ctxContent.lineCap = 'round'; // 设置线条末端的样式
          ctxContent.moveTo(realPoints[0].x, realPoints[0].y); // 将画笔移动到起始点
          for (let i = 1; i < realPoints.length; i++) {
              // 取终点,将上一个点作为控制点,平滑过渡
              const cx = (realPoints[i].x + realPoints[i - 1].x) / 2;
              const cy = (realPoints[i].y + realPoints[i - 1].y) / 2;
              ctxContent.quadraticCurveTo(realPoints[i - 1].x, realPoints[i - 1].y, cx, cy);
          }

          ctxContent.stroke(); // 画线
          ctxContent.restore();
          console.log(`canvas--->`, canvas.toDataURL());
          elementWithCanvasCache.set(points, {
              canvas,
              realPoints,
              x: minX * dpr - padding,
              y: minY * dpr - padding
          });
      }

实现的效果

具体代码

传送门

相关推荐
SameX4 分钟前
初识 HarmonyOS Next 的分布式管理:设备发现与认证
前端·harmonyos
M_emory_30 分钟前
解决 git clone 出现:Failed to connect to 127.0.0.1 port 1080: Connection refused 错误
前端·vue.js·git
Ciito34 分钟前
vue项目使用eslint+prettier管理项目格式化
前端·javascript·vue.js
成都被卷死的程序员1 小时前
响应式网页设计--html
前端·html
fighting ~1 小时前
react17安装html-react-parser运行报错记录
javascript·react.js·html
老码沉思录1 小时前
React Native 全栈开发实战班 - 列表与滚动视图
javascript·react native·react.js
abments1 小时前
JavaScript逆向爬虫教程-------基础篇之常用的编码与加密介绍(python和js实现)
javascript·爬虫·python
mon_star°1 小时前
将答题成绩排行榜数据通过前端生成excel的方式实现导出下载功能
前端·excel
Zrf21913184551 小时前
前端笔试中oj算法题的解法模版
前端·readline·oj算法
老码沉思录2 小时前
React Native 全栈开发实战班 - 状态管理入门(Context API)
javascript·react native·react.js