Странные результаты при предварительном рендеринге против рендеринга в режиме реального времени Canvas - PullRequest
0 голосов
/ 14 сентября 2018

У меня есть метод, который визуализирует матрицу прямоугольников в пределах Canvas до следующего перерисовки с использованием requestAnimationFrame. Я пытаюсь добиться максимальной производительности. Моим первым подходом к этой проблеме было создание прямоугольников в режиме реального времени в 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 элементов:

  • Chrome: 20-30 кадров в секунду
  • Firefox: 40 кадров в секунду

В качестве второго подхода я решил предварительно визуализировать два прямоугольника и использовать drawImage для их рендеринга на 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;
  }

При таких результатах я получаю лучшие результаты в Firefox, а худшие в Chrome:

  • Chrome: 10 кадров в секунду
  • Firefox: 50 кадров в секунду

Я не совсем уверен, как я должен интерпретировать эти результаты. В качестве третьего подхода я имею в виду предварительное создание n Canvas, где n - размер матрицы, и обновление только тех, которые необходимы перед следующей перерисовкой. Перед этим я хотел бы узнать, почему я получаю лучшие результаты перед рендерингом в одном браузере, а худшие - перед рендерингом в другом. Я также хотел бы получить любую обратную связь, чтобы получить лучшую производительность. При необходимости я могу предоставить трассировку стека производительности.

1 Ответ

0 голосов
/ 15 сентября 2018

Разные результаты могут быть вызваны разными реализациями 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);
      }
    }
}
<canvas id="canvas"></canvas>
Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...