Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

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

One of my interface elements is being rendered using the HTML5 <canvas> element and associated JavaScript API. This element is used in several places on the same screen and on multiple screens throughout the app. What is the most efficient way to display this everywhere it's required?

My first idea is to draw to a master canvas, which I then clone and insert where needed in the page. The master canvas might be something like:

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

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

Let's say I want to duplicate the canvas in these div containers:

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

When the page loads, I'll do this to insert a duplicate canvas element into each container:

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

The content being rendered on the canvas is going to be more complex than the simple square used in this example but will still end up being just a static image. It is possible, though, that there could be dozens of different images each cloned dozens of times per page.

The other approach I had in mind was to create an image using the toDataURL() method and set that as the appropriate images' sources:

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'); 

I would add image tags where necessary:

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

And then set all of their SRCs to that newly created image:

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

To me, it pretty much looks like six of one, half dozen of the other. But I'm wondering if one way is considered better practice than the other? If the content being rendered on the <canvas> were more complex, would one way be more efficient than the other?

In that same spirit, when I need to use that element on subsequent pages, is it best to execute all the javascript (from whatever solution is deemed best above) on each page or would saving the value of CANVAS_ELEMENT.toDataURL() in a cookie and then using that on subsequent pages be any more efficient?

like image 889
Daveh0 Avatar asked May 08 '15 05:05

Daveh0


1 Answers

Cloning a canvas will duplicate its dimensions and styling, but not its image data. You can copy the image data by calling drawImage on the context. To paint the contents of originalCanvas onto duplicateCanvas, write:

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

As a demonstration, the following snippet generates four canvases:

  • an original canvas with a small scene painted onto it

  • a copy made by calling cloneNode only

  • a copy made by calling cloneNode and drawImage

  • a copy made by creating a new image and setting its source to the data 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>

If you call toDataURL to copy the image data into a string for use in other pages, don't put the string into a cookie. Cookies are meant to store small amounts of data. Instead, use the 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.

When it comes to the performance of client-side image rendering, it may be faster to draw the scene anew than to paint the stringified image data onto the canvas. Decoding the string and painting the pixels is a relatively expensive operation. To find out if it makes sense to redraw the scene on each page, you can time your drawing operations with performance.now, as demonstrated in the snippet.

like image 57
Michael Laszlo Avatar answered Oct 28 '22 10:10

Michael Laszlo