【问题标题】:Strange results when pre-rendering vs rendering in real-time Canvas预渲染与实时 Canvas 渲染时的奇怪结果
【发布时间】:2019-02-19 10:49:17
【问题描述】:

我有一种方法可以在使用requestAnimationFrame 进行下一次重绘之前在Canvas 内渲染一个矩形矩阵。我正在努力实现最佳性能。我解决这个问题的第一个方法是在Canvas 中实时创建矩形。

  render(display: PanelDisplay): void {
    const ctx = this.parameters.canva.getContext("2d");
    const widthEachBit = Math.floor(this.parameters.canva.width / display[0].length);
    const heightEachBit = Math.floor(this.parameters.canva.height / display.length);

    ctx.lineWidth = 1;
    ctx.strokeStyle = this.parameters.colorStroke;

    for(var i = 0; i < display.length; i++) {
      for(var j = 0; j < display[i].length; j++) {
        const x = j*widthEachBit;
        const y = i*heightEachBit;
        ctx.beginPath();
        ctx.fillStyle = display[i][j] == 1 ? this.parameters.colorBitOn : this.parameters.colorBitOff;
        ctx.rect(x, y, widthEachBit, heightEachBit);
        ctx.fill();
        ctx.stroke();
      }
    }
  }

这样做会导致 3k 元素矩阵的性能平庸:

  • 铬:20-30 fps
  • 火狐:40 fps

作为第二种方法,我决定预渲染这两个矩形并使用drawImageCanvas 上渲染它们:

  render(display: PanelDisplay): void {
    const ctx = this.parameters.canva.getContext("2d");
    const widthEachBit = Math.floor(this.parameters.canva.width / display[0].length);
    const heightEachBit = Math.floor(this.parameters.canva.height / display.length);

    // Render the different canvas once before instead of recalculating every loop
    const prerenderedBitOn = this._prerenderBit(this._prerenderedOn, widthEachBit, heightEachBit, this.parameters.colorBitOn);
    const prerenderedBitOff = this._prerenderBit(this._prerenderedOff, widthEachBit, heightEachBit, this.parameters.colorBitOff);

    for(var i = 0; i < display.length; i++) {
      for(var j = 0; j < display[i].length; j++) {
        const x = j*widthEachBit;
        const y = i*heightEachBit;
        ctx.drawImage(display[i][j] == 1 ? prerenderedBitOn : prerenderedBitOff, x, y);
      }
    }
  }

  private _prerenderBit(canvas: HTMLCanvasElement, widthEachBit: number, heightEachBit: number, color: string) {
    canvas.width = widthEachBit;
    canvas.height = heightEachBit;
    const ctx = canvas.getContext('2d');

    ctx.beginPath();
    ctx.fillStyle = color;
    ctx.rect(0, 0, widthEachBit, heightEachBit);
    ctx.fill();
    ctx.lineWidth = 1;
    ctx.strokeStyle = this.parameters.colorStroke;
    ctx.stroke();

    return canvas;
  }

这样做的结果我在 Firefox 中得到了更好的结果,而在 Chrome 中得到了最差的结果:

  • 铬:10 fps
  • 火狐:50 fps

我不太确定我应该如何解释这些结果。作为第三种方法,我正在考虑预先创建 n Canvas 其中 n 是矩阵的大小,并且仅在下一次重绘之前更新需要更新的那些。在此之前,我想听听您的意见,为什么我在一个浏览器上预渲染得到更好的结果,而在另一个浏览器上预渲染得到最差的结果。我还希望获得任何反馈以获得更好的性能。如有必要,我可以提供性能堆栈跟踪。

【问题讨论】:

    标签: javascript typescript google-chrome firefox canvas


    【解决方案1】:

    不同的结果可能是由于 API 的不同实现,甚至可能是您在浏览器上设置的不同设置,这使得其中一个更喜欢 GPU 加速而不是 CPU 计算,或者一个处理比另一个处理更好的图形内存或者别的什么。

    但无论如何,如果我正确理解你的代码,你可以比这两个选项更好。

    我可以想到两种主要方法,您必须进行测试。

    第一个是用一种颜色渲染整个矩阵大小的一个大矩形,然后循环使用另一种颜色的所有单元格,并将它们组合在一个子路径中,这样你就可以调用fill()这个子路径组合的结束,一次。

    最后,您将在所有这些上绘制网格(网格可以是简单的十字图案,或者在屏幕外画布上预渲染,或者再次是单个子路径)。

    const W = 50;
    const H = 50;
    const cellSize = 10;
    const grid_color = 'black';
    var grid_mode = 'inline';
    
    
    const ctx = canvas.getContext('2d');
    const matrix = [];
    
    canvas.width = W * cellSize;
    canvas.height = H * cellSize;
    
    for (let i = 0; i < H; i++) {
      let row = [];
      matrix.push(row);
      for (let j = 0; j < W; j++) {
        row.push(Math.random() > 0.5 ? 0 : 1);
      }
    }
    
    const grid_pattern = generateGridPattern();
    const grid_img = generateGridImage();
    
    draw();
    
    function draw() {
      shuffle();
    
      // first draw all our green rects ;)
      ctx.fillStyle = 'green';
      ctx.fillRect(0, 0, canvas.width, canvas.height);
    
      // now draw all the red ones
      ctx.fillStyle = 'red';
      ctx.beginPath(); // single sub-path declaration
      for (let i = 0; i < H; i++) {
        for (let j = 0; j < W; j++) {
          // only if a red cell
          if (matrix[i][j])
            ctx.rect(i * cellSize, j * cellSize, cellSize, cellSize);
        }
      }
      ctx.fill(); // single fill operation
      drawGrid();
    
      requestAnimationFrame(draw);
    
    }
    
    function shuffle() {
      let r = Math.floor(Math.random() * H);
      for (let i = r; i < r + Math.floor(Math.random() * (H - r)); i++) {
        let r = Math.floor(Math.random() * W);
        for (let j = r; j < r + Math.floor(Math.random() * (W - r)); j++) {
          matrix[i][j] = +!matrix[i][j];
        }
      }
    }
    
    function drawGrid() {
      if (grid_mode === 'pattern') {
        ctx.fillStyle = grid_pattern;
        ctx.beginPath();
        ctx.rect(0, 0, canvas.width, canvas.height);
        ctx.translate(-cellSize / 2, -cellSize / 2);
        ctx.fill();
        ctx.setTransform(1, 0, 0, 1, 0, 0);
      } else if (grid_mode === 'image') {
        ctx.drawImage(grid_img, 0, 0);
      } else {
        ctx.strokeStyle = grid_color;
        ctx.beginPath();
        for (let i = 0; i <= cellSize * H; i += cellSize) {
          ctx.moveTo(0, i);
          ctx.lineTo(cellSize * W, i);
          for (let j = 0; j <= cellSize * W; j += cellSize) {
            ctx.moveTo(j, 0);
            ctx.lineTo(j, cellSize * H);
          }
        }
        ctx.stroke();
      }
    }
    
    function generateGridPattern() {
      const ctx = Object.assign(
        document.createElement('canvas'), {
          width: cellSize,
          height: cellSize
        }
      ).getContext('2d');
      // make a cross
      ctx.beginPath();
      ctx.moveTo(cellSize / 2, 0);
      ctx.lineTo(cellSize / 2, cellSize);
      ctx.moveTo(0, cellSize / 2);
      ctx.lineTo(cellSize, cellSize / 2);
    
      ctx.strokeStyle = grid_color;
      ctx.lineWidth = 2;
      ctx.stroke();
    
      return ctx.createPattern(ctx.canvas, 'repeat');
    }
    
    function generateGridImage() {
      grid_mode = 'inline';
      drawGrid();
      const buf = canvas.cloneNode(true);
      buf.getContext('2d').drawImage(canvas, 0, 0);
      ctx.clearRect(0, 0, canvas.width, canvas.height);
      return buf;
    }
    field.onchange = e => {
      grid_mode = document.querySelector('input:checked').value;
    }
    <fieldset id="field">
      <legend>Draw grid using:</legend>
      <label><input name="grid" type="radio" value="inline" checked>inline</label>
      <label><input name="grid" type="radio" value="pattern">pattern</label>
      <label><input name="grid" type="radio" value="image">image</label>
    </fieldset>
    
    <canvas id="canvas"></canvas>

    您可以采取的另一种完全不同的方法是直接操作 ImageData。 将其设置为矩阵的大小(cellSize 为 1),将其放在画布上,然后按比例重新绘制它,然后绘制网格。

    ctx.putImageData(smallImageData, 0,0);
    ctx.imageSmoothingEnabled = false;
    ctx.drawImage(ctx.canvas, 0, 0, ctx.canvas.width, ctx.canvas.height);
    drawgrid();
    

    const W = 50;
    const H = 50;
    const cellSize = 10;
    const grid_color = 'black';
    
    canvas.width = W * cellSize;
    canvas.height = H * cellSize;
    
    const ctx = canvas.getContext('2d');
    // we'll do the matrix operations directly on an imageData
    const imgData = ctx.createImageData(W, H);
    const matrix = new Uint32Array(imgData.data.buffer);
    
    const red = 0xFF0000FF;
    const green = 0xFF008000;
    
    for (let i = 0; i < H*W; i++) {
        matrix[i] = (Math.random() > 0.5 ? green : red);
    }
    
    prepareGrid();
    ctx.imageSmoothingEnabled = false;
    draw();
    
    function draw() {
      shuffle();
      // put our update ImageData
      ctx.putImageData(imgData, 0, 0);
      // scale its result
      ctx.drawImage(ctx.canvas,
        0,0,W,H,
        0,0,canvas.width,canvas.height
      );
      // draw the grid which is already drawn in memory
      ctx.stroke();
      requestAnimationFrame(draw);
    }
    function shuffle() {
      // here 'matrix' is actually the data of our ImageData
      // beware it is a 1D array, so we need to normalize the coords
      let r = Math.floor(Math.random() * H);
      for (let i = r; i < r + Math.floor(Math.random() * (H - r)); i++) {
        let r = Math.floor(Math.random() * W);
        for (let j = r; j < r + Math.floor(Math.random() * (W - r)); j++) {
          matrix[i*W + j] = matrix[i*W + j] === red ? green : red;
        }
      }
    }
    function prepareGrid() {
      // we draw it only once in memory
      // 'draw()' will then just have to call ctx.stroke()
        ctx.strokeStyle = grid_color;
        ctx.beginPath();
        for (let i = 0; i <= cellSize * H; i += cellSize) {
          ctx.moveTo(0, i);
          ctx.lineTo(cellSize * W, i);
          for (let j = 0; j <= cellSize * W; j += cellSize) {
            ctx.moveTo(j, 0);
            ctx.lineTo(j, cellSize * H);
          }
        }
    }
    &lt;canvas id="canvas"&gt;&lt;/canvas&gt;

    【讨论】:

    • 绝妙的答案!这两个建议都是有希望的,我会给他们一个机会。我会在接下来的几周内报告结果。谢谢!
    • 我没有在我的问题中包含矩阵不一定会为每个位生成正方形,因为我认为它不会那么相关。我错了。为每个位绘制的形状在名为drawBit(..) 的抽象方法的实现中定义。我最终使用了您第一个建议的变体。我循环遍历矩阵两次。第一次,我画出每个位,然后调用.fill().stroke()。第二次,我在位上绘制每个,然后调用.fill().stroke()。将对.fill() 的调用从 n 减少到 2 解决了所有性能问题。
    • 在每个位的形状为正方形的情况下,您的两种解决方案都比我的要好。
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2017-02-17
    • 1970-01-01
    • 2021-07-31
    相关资源
    最近更新 更多