Adventures in Web 3.0: Part 5 - The HTML5 Canvas Element

This weekend I came across Prez Jordan's post on Julia sets, I then followed the trail back to his original post on the Mandelbrot set. I've always loved fractals, since I read Gleick's Chaos back in school, and I used to spend hours generating Mandelbrot images on my Amiga when I should have been learning CS at University. I did at one point try to write my own Mandelbrot generator, but quickly got bogged down in trying to manage enough code to get a basic UI in Workbench and gave up and went to the pub.

Anyway, Prez's post stirred my memories and inspired me to once again try to put together my own Mandelbrot generator. Except this time, instead of having to manage OS libraries and indirect addressing in C just to open a window, I would take advantage of the HTML5 Canvas element:

The canvas element provides scripts with a resolution-dependent bitmap canvas, which can be used for rendering graphs, game graphics, or other visual images on the fly.

Basically, canvas creates an area on your web page on which you can then draw lines, curves, images, and text with Javascript. It allows you to do some pretty crazy things, a lot of the more spectacular early HTML5 examples used it.

Adding a canvas to the page is very simple:

<canvas id="mandelbrot" width="320" height="240">A mandelbrot fractal will appear here.</canvas>

The content of the element will only appear if the user agent does not support canvas, it's similar to a noscript block. If your browser does have support, then all the content the user sees will be drawn on with Javascript. You'll notice I specified the width and height, if I left that out it would default to 300 pixels wide by 150 pixels high, which has an interesting side effect I'll discuss below, otherwise, it's much the same as any other element. It doesn't have much in the way of default styling, and it's an inline, rather than block level, element, but you can target it with CSS rules:

canvas { 
    -moz-box-shadow: rgb(0,0,0) 0px 2px 3px 3px; 
    -webkit-box-shadow: rgb(0,0,0) 0px 2px 4px; 
    box-shadow: rgb(0,0,0) 0px 2px 3px 3px;
}

So, we now have an empty rectangle with a nice drop shadow, how do we actually draw something? To draw on a canvas you need to get the context object, which in turn gives you access to all the drawing methods:

var canvas = document.getElementById('mandelbrot');
if (canvas.getContext){
    var ctx = canvas.getContext('2d');
    ctx.fillText('Hello World', 50, 50);
}

To draw a Mandelbrot we need to be able to plot single pixels across the whole element. There isn't a plot method, canvas isn't really intended for pixel by pixel manipulation, but we can pick a colour and plot a one pixel by one pixel rectangle:

var canvas = document.getElementById('mandelbrot');
if (canvas.getContext){
    var x=1, y=1;
    var ctx = canvas.getContext('2d');
    ctx.fillStyle = 'rgb(255,0,0)';
    ctx.fillRect(x,y,1,1);
}

So now you know enough about the canvas element to write a Mandelbrot generator, for the rest we can just steal code off the internet &#58;&#41; Here's what we're aiming for (warning - don't click on the link on a slow computer, it'll take several seconds to render):

An image of the Mandelbrot set, generated in the canvas element with Javascript

Going back to Prez's post, his code is in Python, but it's fairly straightforward looking and easy enough to translate into Javascript. I took the Complex number library from "9.3.6. Example: Complex Numbers" of JavaScript: The Definitive Guide, 5th Edition and then used it to reimplement the Python function:

function mandel(c) {
    var cols = ["rgb(255,0,0)", "rgb(255,165,0)", "rgb(255,255,0)", "rgb(165,255,0)", "rgb(0,255,255)", "rgb(0,165,255)", "rgb(165,0,255)", "rgb(0,0,255)"];
    var z = new Complex(0,0);
    for (var i = 0; i <=20; i++) {
        z = Complex.add(Complex.multiply(z,z), c);
        if (z.magnitude() > 2) {return cols[i % cols.length]}
    }
    return "rgb(0,0,0)";
}

The function returns a different colour based on how many iterations it takes for z to exceed 2 in magnitude when combined with the input value c in the formula z * z + c (if it doesn't exceed 2, then it is in the Mandelbrot set, and so it's black). The input value is the pixel position on our canvas element except translated into a complex number - where the x axis is the real component and the y axis the imaginary one - ctx.fillStyle = mandel(c);. Check the Wikipedia page for full details of how the Mandelbrot set works, I worked out most of the details through trial and error once I had it functional.

I mentioned the default size for a canvas element above, this is 300px by 150px. As an experiment I removed the width and height attributes from the element itself and set the size of the canvas in CSS:

canvas { 
    width: 45%;
    height: 45%;
}

You can view the results here (again, watch out if you have a slow computer). The browser renders the canvas, then scales the results to fit the CSS dimensions. So, if you want your canvas to take up a particular portion of the page (eg. half of it), you need to set the dimensions of the element with Javascript based on the pixel width of the page.

Moving on to Prez's second post, which inspired this whole adventure, where he looks at the Julia set. The Julia calculation is very similar to the Mandelbrot one, except instead of starting with z = 0 like the Mandelbrot it starts with z = t, where t is a complex number. The obvious place to get t is the existing plane of numbers on which we've drawn our Mandelbrot set. A canvas is an element like any other, so simply attach an onclick event to it and then work out the value of t from where the click event fired. Here's the Julia set for real 0.4249, i -0.2666:

An image of a Julia set, generated in the canvas element with Javascript

You can take a look at the final Canvas Mandelbrot/Julia Generator here. In performance terms, doing a calculation to render each individual pixel is a pathologically bad case for canvas - it's not something you'd normally do, you would make use of the higher level drawing controls instead. As it stands it does make an interesting performance comparator between browsers. I tested it in Firefox 3.6 (about 2 seconds to generate each image on my machine), Google Chrome 5 beta (about 1 second for each image) and Opera 10.50 (about half a second for each image), it ought to work in Safari too but I didn't try it. I did a version which used Explorer Canvas to try and see how long IE took, but it kept hitting the script timeout before it was even one third of the way through.