Showing posts with label Canvas. Show all posts
Showing posts with label Canvas. Show all posts

February 11, 2014

HTML5 Canvas: Offscreen Rendering

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

December 19, 2013

HTML5 Canvas: State Stacking

Stacking states with Canvas

Introduction

This is the first post of a series about HTML5 Canvas. In this article I will explain the meaning of context.save() and context.restore(). Simply said, these methods are reponsible for stacking the contexts states. But what does it mean?

What are states in Canvas?

First, it is necessary to know, what states are. The simplified answer is: Everything that does not draw!
The Canvas API provides a set of methods which can be distinguished between drawing methods and auxiliary methods. A good part (not all) of these auxiliary methods are used to define the appearance of drawn areas, and/or paths. For example, strokeStyle, fillStyle, globalAlpha, lineWidth, shadowOffsetX, shadowOffsetY, shadowBlur, shadowColor, font, textAlign, textBaseline etc. are all methods which modify states. Also considered as a state is the transformation matrix, which is modified by the methods translate, rotate, scale, setTransform. Another kind of state is a defined clipping region, modified by clip; Everything modified by these methods are states, which can be stacked.

What can I do with stacking?

Obviously, it is easy to recover already set states by simply popping it from the stack, because sometimes it is quite cumbersome to define a proper state. This also keeps your code cleaner. Stacking can even improve runtime performance, as demonstrated here at JsPerf. Another important advantages is the "isolatation" of state dependent operations. In the next paragraph I'll explain this concept more precisely.

Isolation of state dependent operations

Using state stacking you can isolate some and group other state operations quite easily. Imagine a car whose wheels shall rotate, while the car is moving forward. You can isolate the rotation of the wheels during their painting by stacking the "translation matrix" and apply the rotation. Afterwards, you restore the "translation matrix" and paint the next frame. The following snippet demonstrate this principle by transforming texts. Here is the visual result.
And here comes the code.
function main(){
    var context = document.getElementById('myCanvas').getContext('2d');
    
    var painter = new Painter(context);
    
    painter.setFillColor(255,0,0,1);
    painter.drawText("Text 1", 50);    
    painter.pushState();    
    painter.rotate(320, 100, 45);
    painter.setFillColor(0,0,255,1);
    painter.drawText("Text 2", 100); 
    painter.popState();
    painter.drawText("Text 3", 150);                
}
function Painter(ctx){
    var context = ctx;
    var DEG2RAD = Math.PI/180.0;
    var center = {};
        
    var init = function(ctx){
        context = ctx;
        center[0] = context.canvas.width/2;
        center[1] = context.canvas.height/2;
    };
            
    this.pushState = function(){
        context.save();
    };
    
    this.popState = function(){
        context.restore();
    };
    
    this.rotate = function(posX, posY, angle){
        context.translate(posX, posY);
        context.rotate(angle * DEG2RAD);
        context.translate(-posX, -posY);
    };
    
    this.setFillColor = function(r,g,b,a){
        context.fillStyle = "rgba(" + r + "," + g + "," + b + "," + a +")";
    };
    
    this.drawText = function(text, ypos){        
        context.save();
        context.font = "30px Arial";
        context.textAlign = "center";
        context.fillText(text, center[0], ypos);
        context.restore();              
    };
        
    init(ctx);
}