多次复制 canvas:克隆 canvas 还是复制图像数据?

Duplicating a canvas many times: clone the canvas or copy the image data?

我的一个界面元素正在使用 HTML5 <canvas> 元素和关联的 JavaScript API 呈现。此元素在同一屏幕上的多个位置以及整个应用程序的多个屏幕上使用。在需要的地方显示它的最有效方法是什么?

我的第一个想法是绘制母版 canvas,然后我将其克隆并插入页面中需要的位置。大师 canvas 可能是这样的:

var master = $('<canvas>').attr({
      width: 100,
      height: 100
    }),
    c = master[0],
    ctx = c.getContext("2d");

    ctx.fillStyle = "#FF0000";
    ctx.fillRect(0, 0, 150, 75);

假设我想在这些 div 容器中复制 canvas:

<div class="square-container" id="square_header"></div>
...
<div class="square-container" id="square_dataTable"></div>
...
<div class="square-container" id="square_gallery"></div>
....

当页面加载时,我将执行此操作以将重复的 canvas 元素插入到每个容器中:

$(document).ready(function() {
    $('.square-container').each(function() {
        master.clone().appendTo($(this));
    });
}); 

在 canvas 上呈现的内容将比本示例中使用的简单正方形更复杂,但最终仍将只是静态图像。不过,有可能有几十张不同的图片,每张图片每页都被克隆了几十次。

我想到的另一种方法是使用 toDataURL() 方法创建图像并将其设置为适当的图像来源:

var master = $('<canvas>').attr({
    width: 100,
    height: 100
}),
    c = master[0],
    ctx = c.getContext("2d");

    ctx.fillStyle = "#FF0000";
    ctx.fillRect(0,0,150,75);

var square = c.toDataURL('image/png'); 

我会在必要时添加图片标签:

<img src="" id="square_header" class="square" alt="" />
...
<img src="" id="square_dataTable1" class="square" alt="" />
...
<img src="" id="square_gallery" class="square" alt="" />
....

然后将他们所有的 SRC 设置为新创建的图像:

$(document).ready(function() {
    $('img.square').attr('src', square);
});

对我来说,它看起来就像一个中的六个,另一个中的六个。但是我想知道一种方法是否被认为比另一种方法更好?如果 <canvas> 上呈现的内容更复杂,一种方式会比另一种方式更有效吗?

本着同样的精神,当我需要在后续页面上使用该元素时,最好是在每个页面上执行所有 javascript(从上面认为最好的解决方案)还是保存值CANVAS_ELEMENT.toDataURL() 在 cookie 中,然后在后续页面上使用它是否更有效?

克隆 canvas 将复制其尺寸和样式,但不会复制其图像数据。您可以通过在上下文中调用 drawImage 来复制图像数据。要将 originalCanvas 的内容绘制到 duplicateCanvas 上,请写:

duplicateCanvas.getContext('2d').drawImage(originalCanvas, 0, 0);

作为演示,以下代码片段生成四个 canvases:

  • 原作canvas,上面画了一个小场景

  • 仅通过调用 cloneNode 制作的副本

  • 通过调用 cloneNodedrawImage

  • 制作的副本
  • 通过创建新图像并将其源设置为数据 URI 制作的副本

function message(s) {
  document.getElementById('message').innerHTML += s + '<br />';
}

function timeIt(action, description, initializer) {
  var totalTime = 0,
      initializer = initializer || function () {};
  initializer();
  var startTime = performance.now();
  action();
  var elapsed = performance.now() - startTime;
  message('<span class="time"><span class="number">' +
      Math.round(elapsed * 1000) + ' &mu;s</span></span> ' + description);
}

function makeCanvas() {
  var canvas = document.createElement('canvas'),
      context = canvas.getContext('2d');
  canvas.width = 100;
  canvas.height = 100;
  timeIt(function () {
    context.fillStyle = '#a63d3d';
    context.fillRect(10, 10, 80, 40);   // Paint a small scene.
    context.fillStyle = '#3b618c';
    context.beginPath();
    context.arc(60, 60, 25, 0, 2*Math.PI);
    context.closePath();
    context.fill();
  }, '(millionths of a second) to draw original scene', function () {
    context.clearRect(0, 0, canvas.width, canvas.height);
  });
  return canvas;
}

// copyCanvas returns a canvas containing the same image as the given canvas.
function copyCanvas(original) {
  var copy;
  timeIt(function () {
    copy = original.cloneNode();  // Copy the canvas dimensions.
    copy.getContext('2d').drawImage(original, 0, 0);  // Copy the image.
  }, 'to copy canvas with cloneNode and drawImage');
  return copy;
}

// imageFromStorage extracts the image data from a canvas, stores the image data
// in a browser session, then retrieves the image data from the session and
// makes a new image element out of it. We measure the total time to retrieve
// the data and make the image.
function imageFromStorage(original) {
  var image,
      dataURI = original.toDataURL();
  timeIt(function () {
    image = document.createElement('img');
    image.src = dataURI;
  }, 'to make image from a dataURI');
  return image;
}

function pageLoad() {
  var target = document.getElementById('canvases'),
      containers = {},  // We'll put the canvases inside divs.
      names = ['original', 'cloneNode', 'drawImage', 'dataURI'];
  for (var i = 0; i < names.length; ++i) {
    var name = names[i],  // Use the name as an ID and a visible header.
        container = document.createElement('div'),
        header = document.createElement('div');
    container.className = 'container';
    header.className = 'header';
    header.innerHTML = container.id = name;
    container.appendChild(header);
    target.appendChild(container);
    containers[name] = container;  // The canvas container is ready.
  }
  var canvas = makeCanvas();
  containers.original.appendChild(canvas);  // Original canvas.
  containers.cloneNode.appendChild(canvas.cloneNode());  // cloneNode
  containers.drawImage.appendChild(copyCanvas(canvas));  // cloneNode + drawImage
  containers.dataURI.appendChild(imageFromStorage(canvas));  // localStorage
}

pageLoad();
body {
  font-family: sans-serif;
}
.header {
  font-size: 18px;
}
.container {
  margin: 10px;
  display: inline-block;
}
canvas, img {
  border: 1px solid #eee;
}
#message {
  color: #666;
  font-size: 16px;
  line-height: 28px;
}
#message .time {
  display: inline-block;
  text-align: right;
  width: 100px;
}
#message .number {
  font-weight: bold;
  padding: 1px 3px;
  color: #222;
  background: #efedd4;
}
<div id="canvases"></div>

<div id="message"></div>

如果您调用 toDataURL 将图像数据复制到字符串中以供其他页面使用,请不要将该字符串放入 cookie 中。 Cookie 旨在存储少量数据。相反,使用 HTML5 Web Storage API to store the image data in the browser. Alternatively, if the image doesn't change between user sessions, you can render it to a PNG image on a server and use the Cache-Control header to encourage the browser to cache the image file for fast retrieval.

当涉及到客户端图像渲染的性能时,重新绘制场景可能比将字符串化的图像数据绘制到 canvas 上更快。解码字符串和绘制像素是一项相对昂贵的操作。要确定在每个页面上重绘场景是否有意义,您可以使用 performance.now 为绘图操作计时,如代码片段所示。