Offscreen Rendering
Introduction
This post is part of a series about HTML5 Canvas. In this article I talk about "offscreen rendering" and an useful application for this technique: The Spritebuffer. The two important methods for offscreen rendering are context.getImageData() and context.putImageData().
What means offscreen rendering?
As the name says, it is about rendering content somewhere, but the screen. That "somewhere" means the memory. So, we are able to render graphics to the memory. Another term often used for this technique is "pre-rendering". In general, this technique is used to improve the visual experience (by reactivity and runtime performance), as rendering to memory is much faster than rendering to screen; especially when using context.drawImage() as shown at JsPerf.
How can I apply offscreen rendering?
A visual rendering applies for canvas elements which are embedded in the DOM only. Canvas elements that are not linked into the DOM can be used for offscreen rendering, as its content won't be visualized onto the screen. Therefore, to realize offscreen painting, simply create a canvas element dynamically without embedding it into the DOM. The following code demonstrates this simple technique.<html> <head> <script type="application/javascript"> function main(){ // here we create an OFFSCREEN canvas var offscreenCanvas = document.createElement('canvas'); offscreenCanvas.width = 300px; offscreenCanvas.height = 300px; var context = offscreenCanvas.getContext('2d'); // draw something into the OFFSCREEN context context.fillRect(10,10,290,290); // ... } </script> </head> <body onload='main()'> </body> </html>
Ok, I painted something into an offscreen canvas. And now?
Now, it gets interesting. We have painted something into an offscreen canvas and we want to use its content. The canvas API provides two functions to copy and paste image data from one canvas into another. While context.getImageData() fetches a rectangular area from a canvas, context.putImageData() pastes image data into a context. So, it is quite straightforward to copy image data from the offscreen canvas into the visual 'onscreen' canvas.<html> <head> <script type="application/javascript"> function createOffscreenCanvas(){ // here we create an OFFSCREEN canvas var offscreenCanvas = document.createElement('canvas'); offscreenCanvas.width = 300px; offscreenCanvas.height = 300px; var context = offscreenCanvas.getContext('2d'); // draw something into the OFFSCREEN context context.fillRect(10,10,280,280); // ... } function copyIntoOnscreenCanvas(offscreenCanvas){ var onscreenContext = document.getElementById('onscreen').getContext('2d'); var offscreenContext = offscreenCanvas.getContext('2d'); // cut the drawn rectangle var image = offscreenCanvas.getImageData(10,10,280,280); // copy into visual canvas at different position onscreenContext.putImageData(image, 0, 0); } function main(){ copyIntoOnscreenCanvas(createOffscreenCanvas()); } </script> </head> <body onload='main()'> <!-- this is the visual canvas --> <canvas id='onscreen' style='width:300px; height:300px'></canvas> </body> </html>
Applying transformations on image data
Unfortunately, the image data pasted with putImageData() cannot be transformed with rotate(), scale(), translate(), setTransform(). The methods getImageData() and putImageData() are raw pixel operations for which no context state apply. To make the image data state aware, it is necessary to draw it into another (offscreen) canvas. This new canvas can be rendered with drawImageData(). Yes, you read correctly: drawImageData() also accepts a canvas object as argument. The following code illustrates the copying technique.function main(){ loadImage("image.png"); } function loadImage(imgFile, onload){ var img = new Image(); img.onload = function() { exec(img); }; img.src = imgFile; } function exec(imgData){ // create offscreen image var imageCanvas = createOffscreenCanvas(imgData.width,imgData.height); var imageContext = imageCanvas.getContext('2d'); imageContext.drawImage(imgData, 0,0); // copy a part from image to subimage var w = 32; var h = 32; var subImageData = imageContext.getImageData(16,16,w,h); var subImage = createOffscreenCanvas(w,h); var subImageContext = subImage.getContext('2d'); subImageContext.putImageData(subImageData,0,0); paintScene(subImage); }
function createOffscreenCanvas(width,height){ var canvas = document.createElement('canvas'); canvas.width = width; canvas.height = height; return canvas; } function paintScene(subImage){ var onscreenContext = document.getElementById('onscreen').getContext('2d'); // it is possible to use subImage as stamp onscreenContext.drawImage(subImage,32,64); onscreenContext.drawImage(subImage,96,64); // apply transformation onscreenContext.save(); onscreenContext.translate(80,80); onscreenContext.rotate(45 * (Math.PI/180)); onscreenContext.translate(-80,-80); onscreenContext.drawImage(subImage,64,64); onscreenContext.restore(); }The rendered and visualized result may look like this.
Use case: Spritebuffer
A common use case for offscreen rendering is a spritebuffer. A sprite is a (movable) graphic object that is drawn over another graphics, e.g. background. Sprites are usually bundled in one or more images. The specific sprite is copied from that bundle to the canvas. As we want our sprite to be transformable we need to make our graphic object become a canvas first. The following code shows the implementation of a spritebuffer and its usage. The 'class' is ready to use and you may feel free to copy and reuse it.var DEG2RAD = Math.PI/180.0; function drawScene(imgSprite){ var spriteBuffer = new SpriteBuffer(imgSprite); var context = document.getElementById('myCanvas').getContext('2d'); var sprite = spriteBuffer.getSprite(0,0,32,32); context.rotate(45 * DEG2RAD); context.translate(100,100); context.drawImage(sprite, 0, 0); }; function loadImage(imageUrl, onload){ var image = new Image(); image.onload = function(){ onload(image); }; this.image.src = imageUrl; }; function main(){ loadImage('./img/sprites.png', drawScene); };
function SpriteBuffer(spriteImage) { var imageBuffer; var initImageBuffer = function(spriteImage){ var canvas = document.createElement('canvas'); canvas.width = spriteImage.width; canvas.height = spriteImage.height; imageBuffer = canvas.getContext('2d'); imageBuffer.drawImage(spriteImage, 0, 0); }; this.getSprite = function(x,y,w,h){ var imgData = imageBuffer.getImageData(x,y,w,h); var sprite = document.createElement("canvas"); sprite.width=w; sprite.height=h; sprite.getContext('2d').putImageData(imgData, 0,0); return sprite; }; initImageBuffer(spriteImage); };