Off-Screen Rendering (Render to Texture) with HTML5's Canvas Element

eye catcher
Caching makes it feasible

Generally speaking: off-screen rendering allows you to cache expensive drawing operations in some sort of image, texture, or buffer. With the new Canvas API the vector drawing operations for example can be a bit taxing. Same goes for gradients or patterns (Firefox 3.x). Or well, anything that requires many drawing steps or per-pixel calculations.

If you have used any other 2D drawing API in the past, you'll probably picture this a bit more complicated than it actually needs to be. As it turns out, it's surprisingly easy with Canvas since the drawImage function can also take another Canvas as parameter. So, there is no need to construct an actual image – you already got one, kinda.

The renderToCanvas Utility Function

Since JavaScript also features first-class functions, you can use a small handy helper function like the following one, which takes another function (which handles the actual drawing) as parameter:

var renderToCanvas = function (width, height, renderFunction) {
    var buffer = document.createElement('canvas');
    buffer.width = width;
    buffer.height = height;
    renderFunction(buffer.getContext('2d'));
    return buffer;
};

And that's already the whole trick. You tell it how big the buffer is supposed to be and what to draw. You get a canvas instance back which you can just pass to drawImage like any other image.

The basic usage pattern is indeed very straightforward:

var cached = renderToCanvas(512, 512, function (ctx) {
    ctx.whatever();
});
[...]
context.drawImage(cached, 0, 0);

A Real-World Example

As I mentioned earlier, patterns can be too expensive for real-time usage. Using them once is alright, but 60 times a second is a bit too much right now. With hardware acceleration they will become very cheap, but for now we need to make it work with weaker software renderers.

One of the games I'm currently working on uses a 640x480 viewport (i.e. the on-screen Canvas is 640x480 in size). The vertically scrolling background is filled with a 256x256 star field texture. A straightforward createPattern implementation looked just fine. It just created lots of garbage – too much garbage for Firefox to handle.

After replacing it with a big pre-rendered image everything was fine. The code which generates the image looks basically like this (except that I'm using objects for the library stuff and also for media):

var starfieldFill = renderToCanvas(640, 480 + 256, function (ctx) {
    ctx.fillStyle = ctx.createPattern(starfield, 'repeat');
    ctx.fillRect(0, 0, 640, 480 + 256);
});

The generated image is as tall as the viewport plus the height of the pattern image. Once the vertical scroll offset is bigger than than the height of a cell (marked with an arrow), the wrapping takes place:

diagram
Figure 1: an infinitely scrolling 256x256 pattern in a 640x480 viewport

The required drawing code is fairly straightforward:

scrollOffset += delta / 250;
scrollOffset %= 256;

context.save();
context.translate(0, scrollOffset);
context.drawImage(starfieldFill, 0, -256);
context.restore();

The image is drawn at a Y coordinate of -256. Over time the origin (and thus the translated image) moves down. As soon as the offset exceeds 256, it's wrapped around with the handy modulo operator. From the user's point of view it appears to scroll ad infinitum.

By the way, don't bother clearing the canvas if you overdraw everything anyway.

Another Thing I Tried

You probably wonder if drawing the star field texture manually 6 times would have worked. Well, yes. Sort of. It will work fine with integer coordinates, but it won't work if you translate the origin at sub pixel steps, which is what I'm doing. Well, Chrome currently happily ignores this and snaps the coordinates to the nearest integer, but the other browsers don't do this.

The artifact you get are visible seams. The reason for that is somewhat straightforward: Each image is drawn separately. The underlying rendering code doesn't know where some pixel's color came from. So, if for example the background is white and you draw a back rectangle whose coordinates are offset by 0.5, you'll get a 50% gray at the edges (black with 50% opacity drawn onto white). If you draw another black rectangle next to the previous one, its black edge pixels whose opacity is 50% will get mixed with that 50% gray which is already there. The result is a 75% gray pixel instead of black one.

And that's how sub-pixel seams are born.

renderToCanvas is Very Handy

I just checked, I'm using renderToCanvas 8 times in that (previously mentioned) game project: For the star field background (pattern), for the playing field (pattern), four times for motion blur, and once more because I'm very lazy.

I also used it to create JPG based RGBA images. The alpha channel was either taken/created from a gray-scale JPEG, a 8bit PNG, or SVG path data. Actually, I already wrote an article covering this in detail. It's only missing a few diagrams. It should be online in two or three days, I think. The article about emulating lossy RGBA images is now available.

Comments

thanks for the post!

I'm doing a bunch of text rendering in my game: www.esdao.com and really need to be doing that for the text as it's on a map which is dragged around. Saves me having to figure it out :-)

--nahum

pass element, not context

Note that you pass the canvas element to drawImage(), not the context object of the canvas element. I got stuck there for a bit.

re: pass element, not context

My only problem was that the specs are full of stuff for implementers. I.e. it isn't regular API documentation. I completely missed the point that you can just pass a Canvas to drawImage. On my first try I used toDataURL and then created a new Image out of that. Boy, did I feel stupid the next day. ;)

the missing snippet

thanks for your renderToCanvas function! i used it to multiply 3 layers on http://www.chrispie.com/blog/html5-logo-1037/
cheers chris

Post new comment

  • Web page addresses and e-mail addresses turn into links automatically.
  • Allowed HTML tags: <a> <em> <strong> <cite> <code> <ul> <ol> <li> <dl> <dt> <dd>
  • Lines and paragraphs break automatically.

More information about formatting options