Strange results when pre-rendering vs rendering in real-time Canvas

杀马特。学长 韩版系。学妹 提交于 2020-01-25 08:08:19

问题


I have a method which renders a matrix of rectangles within a Canvas before the next repaint using requestAnimationFrame. I'm trying to achieve maximum performances. My first approach to this problem was to create rectangles in real-time within the 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();
      }
    }
  }

Doing so results in mediocre performance for a matrix of 3k elements:

  • Chrome: 20-30 fps
  • Firefox: 40 fps

As a second approach, I decided to pre-render the two rectangles and use drawImage to render them on the 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);

    // 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;
  }

Doing so results I get better results in Firefox, and worst ones in Chrome:

  • Chrome: 10 fps
  • Firefox: 50 fps

I'm not quite sure how I'm suppose to interpret these results. As a third approach, I'm thinking of pre-creating n Canvas where n is the size of the matrix and only updating the ones that need to before the next repaint. Before that, I would like to get your input on why I'm getting better results pre-rendering on one browser and worst results pre-rendering on the other. I would also like any feedback to get better performance. I can provide Performance stack traces if necessary.


回答1:


The different results may be caused by different implementations of the API, or maybe even different settings that you have set-up on your browser, which make one prefer GPU acceleration over CPU computation, or one handles better grapich memory than the other or what else.

But anyway, if I understand you code correctly, you can get better than these two options.

I can think of two main ways, that you'll have to test.

The first one is to render one big rect the size of the whole matrix in one color, then loop over all the cells the other color, and compose them in a single sub path, so that you call fill() only at the end of this sub-path composition, once.

Finally you'd draw the grid over all this (grid which could be either a simple cross pattern, or pre-rendered on an offscreen canvas, or once again a single sub-path).

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>

An other entirely different approach you could take, would be to manipulate an ImageData directly. Set it the size of your matrix (cellSize would be 1), put it on your canvas, and then finally just redraw it scaled up, and draw the grid over.

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);
      }
    }
}
<canvas id="canvas"></canvas>


来源:https://stackoverflow.com/questions/52335088/strange-results-when-pre-rendering-vs-rendering-in-real-time-canvas

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!