Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles / web / HTML5

Generating Mandelbrot fractals using HTML5

4.84/5 (26 votes)
24 Dec 2011CPOL6 min read 70.7K   1.6K  
HTML5 offers a new cool element - Canvas. This is a replacement for SVG. You can draw on it with JavaScript, so we can draw fractals too.

View live demo

Image 1

Introduction

HTML5 offers a new cool element - Canvas. This is a replacement for SVG. You can draw on it with JavaScript. So in theory, we can draw fractals too? Yes, we can. This article is about drawing Mandelbrot with the HTML5 Canvas element. Most browsers support HTML5, Internet Explorer 9 too, but IE8 and lower don't support Canvas.

In order to draw, we have to get the context of the Canvas: canvas.getContext('2d'). The parameter "2d" is the name of the context. Most browsers support only 2D context, but Chrome has experimental WebGL for 3D context. Next, with the context element, we can draw and get or set image data. In this solution of Mandelbrot, we will use getting and setting image data, because we will draw pixel by pixel and it is faster than micro-rectangles.

Background

I learned 2D context interface from this reference.

Script Structure

The script is divided into two parts. There are formulas and a viewer. Formulas are single functions to calculate single pixels. Their inputs are coordinates (as complex from my library) and outputs are numbers between 0 and 1 (number of iterations divided by maximal iterations). The viewer part runs a specified formula for all coordinates in the Canvas and then represents them with a special palette. The viewer handles zooming too. Thanks to such a construction, it is extensible. We can simply add new formulas, for example, Julia.

Mandelbrot Formula

Mandelbrot requires complex numbers. I wrote a simple class in JavaScript for these numbers. You can download it from the top of the article. So add the complex class to the page:

HTML
<script type="text/javascript" src="complex.js"></script>

Now we can define the function for Mandelbrot:

JavaScript
function mandelbrot(xy) {
}

xy will be a complex number. This function will be a formula. The algorithm for generating it is iterated. We will recalculate the z complex number in each iteration. Its start value is xy. The loop continues until the absolute value of z is less than the bailout. The bailout usually equals 4. In the center of the Mandelbrot, the number of iterations will be infinite. So we have to define the maximal iterations to avoid an infinite loop. Let's define z in the function and i for counting iterations:

JavaScript
var z = new Complex(xy.x, xy.y);

Now we have to define maximal iterations, bailout, and other numbers for recalculating z. Default offset and size will be declared too. They will be after the function.

JavaScript
// Code of function ends.

mandelbrot.maxIter = 32; // Maximal iterations
// Power of fractal. For normal Mandelbrot it will be (2,0).
mandelbrot.power = new Complex(2.0, 0.0);
mandelbrot.bailout = 4.0; // Bailout value.
mandelbrot.offset = new Complex(-3.0, -2.0); // Default offset of fractal.
mandelbrot.size = new Complex(0.25, 0.25); // Default size of fractal.

Now we can define the while loop of the formula:

JavaScript
while (i < mandelbrot.maxIter && z.abs() <= mandelbrot.bailout) {
    z = z.pow(mandelbrot.power).add(xy); // Recalculating z.
    i++; // Iterations + 1.
}

Now we can calculate the iterations. The return value of the function must be between 0 and 1. One is the maximal return value, and the max value of iterations is maxIter. So the return value will be i divided by maximal iterations. I apply a smoothing algorithm too.

JavaScript
if (i < mandelbrot.maxIter) {
    // Smoothing algorithm.
    i -= Math.log(Math.log(z.abs())) / Math.log(mandelbrot.power.abs());
    return i / mandelbrot.maxIter;
}
else
    return 0.0;

The function with its values should be like this:

JavaScript
function mandelbrot(xy) {
    var z = new Complex(xy.x, xy.y);
    var i = 0;

    while (i < mandelbrot.maxIter && z.abs() <= mandelbrot.bailout) {
        z = z.pow(mandelbrot.power).add(xy);
        i++;
    }

    if (i < mandelbrot.maxIter) {
        i -= Math.log(Math.log(z.abs())) / Math.log(mandelbrot.power.abs());
        return i / mandelbrot.maxIter;
    }
    else
        return 0.0;
}
mandelbrot.maxIter = 32;
mandelbrot.power = new Complex(2.0, 0.0);
mandelbrot.bailout = 4.0;
mandelbrot.offset = new Complex(-3.0, -2.0);
mandelbrot.size = new Complex(0.25, 0.25);

The Viewer

First, let's create the body of the page:

HTML
<h1>Mandelbrot</h1>
<div id="di" style="position: relative;">
    <canvas id="cv" width="580" height="580" 
            style="border-style: solid; border-width: 1px;"
            onmousedown="canvas_onmousedown(event);">
        Your browser doesn't support canvas.
    </canvas>
</div>

Div is reserved for future use. Inside div, you can see a Canvas named cv. Now let's define the generateFractal function.

JavaScript
function generateFractal(formula, resetSize) {
}

The formula will be the function like our Mandelbrot formula. We will pass the formula function - not its value - to generateFractal. If resetSize is true, then offset and size variables will be set to default of fractal. Now add the Mandelbrot generation when the page is loaded.

HTML
onload="generateFractal(mandelbrot);"

Define the following global variables:

JavaScript
var lastFormula; 	// Here we will store actual formula.
var tim; 		// In the future we will store here timeout for generating
         		// lines - it simulates asynchronous thread.
var offset = new Complex(-3.0, -2.0); 	// Offset of fractal view.
var size = new Complex(0.25, 0.25); 		// Scale of fractal view.

When the first line is computed, we will set the timeout (1ms) for computing the second line. This 1ms is for refreshing the Canvas. tim will be the ID of the current timeout. First, we have to stop the previous calculation if it isn't completed. Then we have to set the new formula to lastFormula.

JavaScript
clearTimeout(tim);
lastFormula = formula;

Then we have to reset optionally offset and size:

JavaScript
if (resetSize) {
    offset = new Complex(formula.offset.x, formula.offset.y);
    size = new Complex(formula.size.x, formula.size.y);
} 

Then we have to get the Canvas and its width and height.

JavaScript
var w = cv.width;
var h = cv.height;

Now we have to get the context and the image data of the Canvas. Define y too. It will be the number of actually rendered lines.

JavaScript
var g = cv.getContext("2d"); // Image context.
var img = g.getImageData(0, 0, w, h); // Image data.
var pix = img.data; // Table of canvas image data.

Now declare the function drawLine in generateFractal.

JavaScript
function drawLine() {
}

Now if y < h (height of the Canvas is greater than 0):

JavaScript
if (y < h)
tim = setTimeout(drawLine, 1);

The nested function drawLine will compute a single line of fractal, will set the rendered part of the fractal to the Canvas, and then will set the timeout to it for the next line. Let's write this code. But first, we need the palette and a function for plotting the pixel. I will not explain how the palette works. Here is the function for getting the color from the formula's return value:

JavaScript
function getColor(i) {
    var k = 1.0 / 3.0;
    var k2 = 2.0 / 3.0;
    var cr = 0.0;
    var cg = 0.0;
    var cb = 0.0;
    if (i >= k2) {
        cr = i - k2;
        cg = (k - 1) - cr;
    }
    else if (i >= k) {
        cg = i - k;
        cb = (k - 1) - cg;
    }
    else {
        cb = i;
    }
    var r = parseInt(cr * 3 * 255);
    var g = parseInt(cg * 3 * 255);
    var b = parseInt(cb * 3 * 255);
    return [r, g, b];
}

Put it in the generateFractal function. Now let's define the function for drawing a pixel. It must have access to pix - the table of pixels. So put it inside the function for generating the fractal.

JavaScript
function drawPixel(x, y, i) {
    var c = getColor(i);
    var off = 4 * (y * w + x);
    pix[off] = c[0];
    pix[off + 1] = c[1];
    pix[off + 2] = c[2];
    pix[off + 3] = 255;
}

The first line gets the color by the formula's return value. In the second line, we calculate the starting position of the pixel to plot. Each pixel allocates 4 bytes (R, G, B, A) in the array. In the last 4 lines, it sets the bytes of the pixel. The last byte is 255, because the image isn't translucent (and this channel is alpha). Now you can write the drawLine function.

First, define the loop:

JavaScript
for (var x = 0; x < w; x++) {
    var c = formula(new Complex(x / w / size.x + 
                    offset.x, y / h / size.y + offset.y));
    drawPixel(x, y, c);
}

In the variable c, we have calculated the value of the formula. It passes the scaled and translated position to the formula. Next, it sets the specified pixel in the Canvas to the returned value. When we have computed the line, we have to update the Canvas:

JavaScript
g.putImageData(img, 0, 0);

Now we have to set the timeout for computing the next line (if the number of the next line is less than the height of the Canvas).

JavaScript
if (++y < h)
tim = setTimeout(drawLine, 1);

The basic viewer is ready.

Julia Formula

Julia formula is very similar to Mandelbrot. There is one change: in the loop don't add pixel, but add seed. Seed is a point. It can be random or selected from Mandelbrot.

JavaScript
function julia(xy) {
    var z = new Complex(xy.x, xy.y);
    var i = 0;

    while (i < julia.maxIter && z.abs() <= julia.bailout) {
        z = z.pow(julia.power).add(julia.seed);
        i++;
    }

    if (i < julia.maxIter) {
        i -= Math.log(Math.log(z.abs())) / Math.log(julia.power.abs());
        return i / julia.maxIter;
    }
    else
        return 0.0;
}
julia.maxIter = 32;
julia.power = new Complex(2.0, 0.0);
julia.bailout = 4.0;
julia.offset = new Complex(-2.0, -2.0);
julia.size = new Complex(0.25, 0.25);
julia.seed = new Complex(0.0, 0.0);

Other Functionalities in the Demo

I have implemented zooming into a fractal too. It handles mouse events of the Canvas and the body. When you press a mouse button, it saves the first position, and when you hold a button, it saves the second position. It calculates the new offset and scale of the fractal. Additionally, when you move the mouse, it displays a preview - a red rectangle (div with absolute position).

In demo, there is switching too: you can choose point on Mandelbrot and this generates Julia fractal.

In order to save a fractal, I get the data URL of the Canvas and set it to an image. Then the user can save the image to disc. The data URL is the path with the data of the image as hex.

You can see the details of these functions in the source code.

Conclusion

The HTML5 Canvas is a cool element for drawing dynamically using JavaScript. We can draw fractals with it too.

History

  • 2011-12-22 - Added Julia

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)