Fractals with HTML 5 Canvas

The HTML 5 canvas element provides a capable 2D raster API to web developers. It’s been implemented in the major browsers for some time now and good tutorials like this and this should get you up to speed quickly.

In this post we’re going to use the canvas API to generate some "fractal" images. i.e., images consisting of a basic element repeated with regular scaling and positioning to form a larger pattern. Some javascript experience would be helpful but the brief code examples should be readily comprehensible to most developers. Here’s the web page that will serve as the basis for our exploration:

<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd"> 
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
<head>
    <meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
    <title>Canvas Fractals</title>
    <link rel="shortcut icon" href="favicon.ico" type="image/x-icon" />
    <style type="text/css" media="all" xml:space="preserve"> 
    /*<![CDATA[*/
        body {
            background: #000;
        }
        canvas {
            background: #fff;
        }
    /*]]>*/
    </style>
    <script type="application/javascript">
    /*<![CDATA[*/

//
// prepare the canvas for subsequent drawing
//
// canvas: the html5 canvas element
// ctx: the canvas 2d drawing context
// config: object with configuration properties
//
var init = function(canvas, ctx, config) {
    if (config.width) {
        canvas.width = config.width;
    }
    if (config.height) {
        canvas.height = config.height;
    }
    if (config.background) {
        ctx.fillStyle = config.background;
    }
    ctx.fillRect(0, 0, canvas.width, canvas.height);
    ctx.translate(canvas.width * config.tx, canvas.height * config.ty);
};

//
// draw one instance of the basic shape
//
// ctx: the canvas 2d drawing context
// depth: recursion level
// config: object with configuration properties
//
var draw = function(ctx, depth, config) {
    ctx.fillStyle = config.color;
    ctx.fillRect(-config.w/2, 0, config.w, config.h);
};

//
// setup the canvas context transformation for the next level of recursion
//
// ctx: the canvas 2d drawing context
// depth: recursion level
// config: object with configuration properties
//
var transform = function(ctx, depth, config) {
    ctx.translate(-config.w/2, 0);
    ctx.scale(config.ratio, config.ratio);
    ctx.translate(-config.w/2, -config.h);
};

//
// direct the overall recursion of the drawing
//
// ctx: the canvas 2d drawing context
// depth: recursion level
// depthMax: recursion level limit
// config: object with configuration properties
//
var recurse = function(ctx, depth, depthMax, config) {
    if (depth >= depthMax) {
        return;
    }
    
    draw(ctx, depth, config);

    ctx.save();
    transform(ctx, depth, config);
    recurse(ctx, depth + 1, depthMax, config);
    ctx.restore();
};

//
// create recursive image on the canvas
//
// config: object with configuration properties
//
var create = function(config) {
    var canvas = document.getElementsByTagName("canvas")[0];
    var ctx = canvas.getContext("2d");

    init(canvas, ctx, config);

    recurse(ctx, 0, config.depth, config);
};

//
// execute after page load
//
window.onload = function(ev) {
    var config = {
        background: "rgb(59, 185, 255)",
        width: 600,     // canvas width
        height: 500,    // canvas height
        tx: 0.5,        // x origin translation as a fraction of width
        ty: 0.8,        // y origin translation as a fraction of width

        depth: 10,      // recursion limit
        ratio: 1/Math.sqrt(2),     // scale for each step

        w: 100,         // properties of basis image
        h: 100,
        color: "green",
    };

    create(config);
};

    /*]]>*/
    </script>
</head>
<body>
    <div id="container"> 
        <canvas id="canvas" width="512" height="512">
        </canvas>
    </div> 
</body>
</html>

To begin with the end in mind, this page creates the following image when loaded:

The flow of control is straightforward:

  • The window.onload() event handler (line 104) is called after the page loads.
  • The create() function (line 92) performs some setup including calling
  • The init() function (line 27) initializes the image.
  • The recurse() function (line 74) directs the recursive image contruction by calling
  • The draw() function (line 48) draws the basic shape and
  • The transform() function (line 60) prepares the drawing context for the next level of recursion.

A few variables deserve special mention:

  • The "config" javascript object literal declared on line 105 contains a set of properties that are passed down to all subsequent functions. It offers a one stop shop for tweaking the image generation process. For example, the init() function uses the config argument properties to set the size and background color of the canvas.
  • "ctx" assigned in line 94 is the canvas 2D context instance which provides the actual canvas API. This variable is also passed down to all subsequent functions.

The workhorse of this design is the recurse() function which calls itself repeatedly up to a limit defined by the "depth" property in the config object. In each call recurse() calls draw() to draw the basic shape and then transform() to change the coordinate system in preparation for the next call to recurse(). The ctx.save() and ctx.restore() calls on lines 81 and 84, respectively, use the stack of coordinate settings to restore the coordinate system to its line 81 state after line 84. These calls aren’t strictly necessary for a single line of recursion but we’ll keep them there in anticipation of our next refinement.

The draw() function utilizes the usual 2D APIs to draw lines, ovals, rectangles, bitmap images etc. More interesting is the transform() function which manipulates the coordinate system using the ctx.scale(), ctx.translate(), and ctx.rotate() methods. A call to transform() will almost always call ctx.scale() to reduce the size of the next level of drawing. Those in the know can even assign a 3×3 transformation matrix with the ctx.setTransform() method.

Our first transform() implementation scales the coordinate system down by a factor of .71 and moves the origin towards the northwest. This causes each successive rectangle to be drawn to the northwest of its predecessor.

Doubling Down

Our next example produces this image:

How did we do it? By introducing another recursive call in the recurse() function along with another transform2() function. (We also changed the config.ratio property to produce a nicer looking "tree".) Here’s the code:

var transform2 = function(ctx, depth, config) {
    ctx.translate(config.w/2, 0);
    ctx.scale(config.ratio, config.ratio);
    ctx.translate(config.w/2, -config.h);
};

var recurse = function(ctx, depth, depthMax, config) {
    if (depth >= depthMax) {
        return;
    }

    draw(ctx, depth, config);

    ctx.save();
    transform(ctx, depth, config);
    recurse(ctx, depth + 1, depthMax, config);
    ctx.restore();

    ctx.save();
    transform2(ctx, depth, config);
    recurse(ctx, depth + 1, depthMax, config);
    ctx.restore();
};

Circling Back

Minor changes to the draw() and transform() functions result in this image. Note that we alternate between two colors at each level of recursion.

var draw = function(ctx, depth, config) {
    ctx.fillStyle = depth % 2 == 0 ? config.colorA : config.colorB;
    ctx.beginPath();
    ctx.arc(0, 0, config.w, 0, 2 * Math.PI, true);
    ctx.closePath();
    ctx.fill();
};

var transform = function(ctx, depth, config) {
    ctx.translate(-config.w/2, 0);
    ctx.scale(config.ratio, config.ratio);
};

var transform2 = function(ctx, depth, config) {
    ctx.translate(config.w/2, 0);
    ctx.scale(config.ratio, config.ratio);
};

Triangulating

In the next example we use the ctx.rotate() method in the transform() functions along with a recursion-depth dependent color to produce the following image. I should mention that the config object is also getting some new and updated properties in each example.

var draw = function(ctx, depth, config) {
    ctx.fillStyle = "rgb(0, " + (255 - (15 * depth)) + ", " + (100 + (15 * depth)) + ")";
    ctx.beginPath();
    ctx.moveTo(-config.w/2, config.h/2);
    ctx.lineTo(0, -config.h/2);
    ctx.lineTo(config.w/2, config.h/2);
    ctx.closePath();
    ctx.fill();
};

var transform = function(ctx, depth, config) {
    ctx.translate(0, -config.h/2);
    ctx.rotate(-Math.PI/5);
    ctx.scale(config.ratio, config.ratio);
    ctx.translate(-config.w/2, -config.h/2);
};

var transform2 = function(ctx, depth, config) {
    ctx.translate(0, -config.h/2);
    ctx.rotate(Math.PI/5);
    ctx.scale(config.ratio, config.ratio);
    ctx.translate(config.w/2, -config.h/2);
};

Keep Tri-ing

Adding a third recursive call to recurse…

var draw = function(ctx, depth, config) {
    var d = depth * 12;
    ctx.fillStyle = "orange";
    ctx.beginPath();
    ctx.arc(0, 0, config.w, 0, 2 * Math.PI, true);
    ctx.closePath();
    ctx.fill();
    ctx.fillStyle = "black";
    ctx.beginPath();
    ctx.arc(0, 0, config.w/5, 0, 2 * Math.PI, true);
    ctx.closePath();
    ctx.fill();
};

var transform = function(ctx, depth, config) {
    ctx.translate(config.w, 0);
    ctx.scale(config.ratio, config.ratio);
    ctx.translate(config.w, 0);
};

var transform2 = function(ctx, depth, config) {
    ctx.rotate(-Math.PI*2/3);
    ctx.translate(config.w, 0);
    ctx.scale(config.ratio, config.ratio);
    ctx.translate(config.w, 0);
};

var transform3 = function(ctx, depth, config) {
    ctx.rotate(Math.PI*2/3);
    ctx.translate(config.w, 0);
    ctx.scale(config.ratio, config.ratio);
    ctx.translate(config.w, 0);
};

Closing Thoughts

It should be obvious by now that we’ve only scratched the surface of the image generation possibilities with canvas. I hope some enterprising readers will post their own creative variations in the comments section below. Note that in firefox you can right click on the canvas element to pop up a menu that will allow you to save the current canvas bitmap as a .png file. Here’s the zipped code pile for your perusal:

fractal.zip

Note that our approach can be further generalized. For example, the config object could be given an array of transform() functions to drive the recursion in recurse().

This entry was posted in Technical. Bookmark the permalink.

Leave a Reply