Pushing pixels with the HTML5 canvas.

Having seen a HTML5 and javascript canvas demo that my colleague Emil Åström had put together I made a mental note to try my hand at it myself as soon as time would allow. What intrigued me the most was the frame rates he was able to achieve when performing quite a lot of calculations for every pixel. This is a truly impressive leap for JavaScript even though the performance you could expect when writing this sort of code closer to the hardware (e.g. in Assembler) would of course completely wipe the floor with it.

My experimental application is a cosine interpolation between points on a grid “overlaying” pixels of the canvas. The interpolated elevation is translated into a color by interpolating between three colors. It is worth noting that this code was written for readability and understanding rather than with performance in mind.

I have posted the source to GitHub and you may directly preview the page here.

Canvas Plasma

The goal I set myself was to get the demo up and running even in full screen with reasonable performance. To this end I decided to render the plasma to a smaller canvas off screen and then paste it to the larger canvas for each frame.

To do so requires a rather elaborate setup.

// Set up our rendering canvas, context and image data.
var sw = 240;
var sh = 240;
var renderCanvas = createCanvas(sw, sh);
var renderContext = renderCanvas.getContext("2d");
var renderImageData = renderContext.createImageData(sw, sh);

// Retrieving our target canvas, and context.
var targetCanvas = document.getElementById("plasmaCanvas");
resizeTargetCanvas(targetCanvas);
var targetContext = targetCanvas.getContext("2d");

// Scale between the render and target canvas sizes.
targetContext.scale(targetCanvas.width/renderCanvas.width, 
targetCanvas.height/renderCanvas.height);

Once this is done it is basically just a matter of pasting
each frame onto the target canvas in the render loop.

// Write image data to render canvas.
renderContext.putImageData(renderImageData, 0, 0);
// Draw render canvas onto the target canvas.
targetContext.drawImage(renderCanvas,0, 0);

To keep it all straight in my head I decided that a few helper “classes” were necessary. The most central of them is the one dealing with the grid data and its animation. The others are a simple color class for interpolation and a grid point holding a value and an animation direction (up or down).

function grid(w, h, from, to, step)
{
    this.w = w;
    this.h = h;
    this.from = from;
    this.to = to;
    this.step = step;
    this.data = new Array(w);
    for (var i = 0; i < this.data.length; i++) {
        this.data[i] = new Array(h);
        for (var j = 0; j < this.data[i].length; j++)
        {
            this.data[i][j] = 
                new gridPoint(Math.random(), 
                    Math.random() < 0.5);
        }
    }
    this.animate = function()
    {
        for (var i = 0; i < this.w; i++) {
            for (var j = 0; j < this.h; j++)
            {
                var v = this.data[i][j].v;
                var d = this.data[i][j].d;
                var rs = Math.random() * this.step;
                v = d == true ? v + rs : v - rs;
                if (v > this.to)
                {
                    v = this.to;
                    d = false;
                }
                if (v < this.from)
                {
                    v = this.from;
                    d = true;
                }
                this.data[i][j].v = v;
                this.data[i][j].d = d;
            }
        } 
    }
}

function color(r, g, b, a) {
    this.r = r;
    this.g = g;
    this.b = b;
    this.a = a;
}

function gridPoint(v, d) {
    this.v = v;
    this.d = d;
}

The interpolation over the grid onto the canvas pixel data is encapsulated in the following functions. A nice writeup on cosine interpolation may be found here.

function gridInterpolate(x, y, grid)
{
    // How much space between grid 
    // points in unit coordinates (0-1).
    var gsx =  1 / (grid.w - 1);            
    var gsy =  1 / (grid.h - 1); 
    // Top/left grid point for grid 
    // quadrant that this point belongs to.
    var ox = Math.floor(x / gsx);
    var oy = Math.floor(y / gsy);
    // Elevations for all grid 
    // points in quadrant.
    var ga = grid.data[ox][oy].v;
    var gb = grid.data[ox + 1][oy].v;
    var gc = grid.data[ox][oy + 1].v;
    var gd = grid.data[ox + 1][oy + 1].v;
    // Unit coordinate within grid.
    var gx = (x - (ox * gsx)) / gsx;
    var gy = (y - (oy * gsy)) / gsy;
    // Interpolate for elevations along 
    // top, left, bottom, and right.
    var zt = cosineInterpolate(ga, gb, gx);
    var zl = cosineInterpolate(ga, gc, gy);
    var zb = cosineInterpolate(gc, gd, gx);
    var zr = cosineInterpolate(gb, gd, gy);
    // Interpolate exact elevation for 
    // the point.
    return (cosineInterpolate(zl, zr, gx) + 
    cosineInterpolate(zt, zb, gy)) / 2;  
}

function cosineInterpolate(p1, p2, v)
{
    var ft = v * Math.PI;
    var f = (1 - Math.cos(ft))*0.5;
    return p1 * (1 - f) + p2 * f;
}

Notable observations which I have taken from this experience is that Google Chrome basically wipes the floor with IE9/10 in terms of framerate. Using Google Chrome on my Apple MacBook Pro (Late 2011) I get 90+ frames per second in full screen running this, while IE barely reaches 15 frames per second. The performance seems to vary considerably depending on hardware as running it on my work machine (Lenovo T410S) results in more flickering and lower frame rates.

In summary this was an extremely fun little side project and I am sure to be continuing to experiment with the HTML5 canvas in the future.

Leave a comment

This site uses Akismet to reduce spam. Learn how your comment data is processed.