Drawing Mandelbrot Set on HTML5 Graphics Canvas

Mandelbrot set is a mathematical set of points whose boundary is a distinctive and easily recognizable two-dimensional fractal shape. The Mandelbrot set is defined over the complex number plane. The definition of the Mandelbrot set is simple but the structure of the set is really complex and beautiful.

Mandelbrot Set Overview

Mandelbrot set is defined mathematically as the set of values of c in the complex plane for which the orbit of 0 under iteration of the complex quadratic polynomial zn+1 = zn2 + c remains bounded. That is, a complex number c is part of the Mandelbrot set if, when starting with z0 = 0 and applying the iteration repeatedly, the absolute value of zn remains bounded however large n gets.

Below is the structure of the Mandelbrot set that is generated using our application.

Mandelbrot Set

Mandelbrot Set

HTML 5 Canvas Overview

HTML5 is the latest version of the standard HTML. It comes with new cool element, Canvas that allows you to draw directly and manipulate every single pixels in the canvas. A canvas is a rectangular area on an HTML page, and it is specified with the <canvas> element. HTML5 currently is under development and not all browser support it yet.

We’ll not cover all the definition of cool function in the canvas. There are documentation of those over the web. We only give an attention to several functions that will be used in our tutorial.

Every canvas element has a graphics context that will be used to modify the pixel on the canvas. We’ll use 2d context. To get the context, use getContext function of the canvas object.

var canvas = document.getElementById('thecanvas');
var ctx = canvas.getContext('2d');

The above code assumes that we have defined a canvas element which id thecanvas.

<canvas id="thecanvas" width="500" height="500"></canvas>

Our requirement is to get all the pixels data of the canvas and modify the color of the pixels. To get all pixels data, use getImageData function of the context. The function has 4 arguments x,y,width and height. x and y arguments define the square top left position from which the pixels will be taken. width and height define the dimension of the square.

var imageData = ctx.getImageData(0, 0, 100, 100);
var pixels = imageData.data;

The variable pixels above contains 100×100=10.000 pixels that is save in 1 dimension array. 1 pixels have 4 elements, red,green,blue and alpha component. So pixels variable has 10.000×4 = 40.000 elements. 1 element pixel is an integer from 0 to 255.

We now can modify the pixels data. After the pixels have been modified, we must put the pixels back to canvas in order to our modification takes effect.

ctx.putImageData(imageData, 0, 0);

Drawing Mandelbrot Set on HTML 5 Canvas

Javascript doesn’t provides built in complex number data type. So, we have to define our complex number data type. For simplicity of the tutorial, we will represent any complex number in javascript object with two properties. The x property represents the real part and y property represents the imaginary part.

//define complex data type
var Complex = function(x, y) {
   this.x = x; // real part
   this.y = y; // imaginary part
};

//define new complex number 1+2i
var complex = new Complex(1,2);

We have now a simple complex number data type for our application. Before we start to code, remember from above explanation. The mandelbrot definition requires to use two operation on complex number. We’ll not define all number operations in complex number, but we’ll create only two operations, multiplication and addition that will be used in generating mandelbrot set.

//define two complex number
var a = 1, b = 2, c = 3, d = 4;
var complex1 = new Complex(a,b);
var complex1 = new Complex(c,d);

//add complex1 and complex1 will result
var addition = new Complex(a+c, b+d);

//multiply complex1 and complex 2 will result
var multiplication = new Complex(a*c - b*d, b*c + a*d);


The Mandelbrot Set Drawing Algorithm

We’ll use different approach from mandelbrot set definition to visualize the set. Rather than finding all complex numbers c which is bounded, our goal is to colorize all of the pixels in the canvas based on number of iterations. This will result great visualization for mandelbrot set.

Step by Step Mandelbrot Set Drawing

  1. Define user interface. Our application will has simple user interface. It’ll contains color scheme selection box, a button and the canvas.
    Mandelbrot Generator Simple User Interface

    Mandelbrot Generator Simple User Interface


    Below is HTML tags that defines our simple user interface.

    <html>
    <head>
    <title>Drawing Mandelbrot set on HTML5 graphics canvas</title>
    </head>
    <body onload="setupCanvas();">
        <div style="margin:0 auto;width:505px;padding-top:10px;">
            <div>Color Scheme:
            <select id="scheme">
                <option value="1" selected>Color Scheme 1</option>
                <option value="2">Color Scheme 2</option>
                <option value="3">Color Scheme 3</option>
                <option value="4">Color Scheme 4</option>
                <option value="5">Color Scheme 5</option>
                <option value="6">Color Scheme 6</option>
            </select>
            <input type="button" value="Render" id="render_btn" onclick="render()"/><br/>
            Scroll your mouse on the canvas to zoomin/zoomout
            </div>
            <canvas style="border:1px solid;" id="thecanvas" width="500" height="500"></canvas>
        </div>
    </body>
    </html>
    

    After the html page is loaded, immediately we call javascript function setupCanvas. This function will checks whether the browser support html5 canvas then get canvas’s drawing context and save it as global variable ctx. We’ll use the context to modify each pixels on the canvas.

  2. Define setupCanvas function.
         
    var setupCanvas = function() {
        canvas = document.getElementById('thecanvas');
        setupMouseListener();
    
        if (!canvas.getContext) {
            alert("I'm sorry, seems your browser not support HTML 5, try another browser.");
            return;
        }
           
        // get drawing context
        ctx = canvas.getContext('2d');
    };   
    
  3. Define render function.
    In this function we get the selected color scheme in the selection box. Save the canvas size as global variables then define the scaling ratio from canvas coordinats to mandelbrot coordinats.

    var render = function() {
        if (!canvas.getContext) return;
        
        //get color scheme
        var a = document.getElementById('scheme');
        colorScheme = parseInt((a.value || a.options[a.selectedIndex].value));
        
        //save canvas size into variable
        cw = canvas.width;
        ch = canvas.height;
        
        //define scaling ratio from canvas coordinat to mandlebort coordinat
        scaleX = (mx[1] - mx[0])/cw;
        scaleY = (my[1] - my[0])/ch;
        
        drawMandlebrot();
    };
    
  4. Define the drawMandelbrot function.
    In this function, we iterate to each pixels in the canvas and find the iteration for each pixels then color the pixel based on number iterations.

    
    var drawMandlebrot = function() {
        if(isDrawing) return;
        
        isDrawing = true;
        
        var imageData = ctx.getImageData(0, 0, cw, ch);
        pixels = imageData.data;
        
        for(var x = 0; x < cw; x++) {
            for(var y = 0; y < ch; y++) {
                iteration = 0;
                // scale canvas coordinat to mandlebrot complex coordinat
                var x0 = scaleX*x + mx[0], y0 = scaleY*y + my[0];
                // define complex number c
                var c = new Complex(x0, y0), z = new Complex(0, 0);
                
                while(iteration < maxIteration) {
                    // check if z still inbound
                    if(z.x*z.x + z.y*z.y >= 2*2) {
                        break;
                    }
                    
                    // multiply z and z
                    z = new Complex(z.x*z.x - z.y*z.y, z.y*z.x + z.x*z.y);
                    // add z and c
                    z = new Complex(z.x+c.x, z.y+c.y);
                    // inrease iteration
                    iteration++;
                }
                
                // colorize current pixel
                setPixelColor(x, y, iteration);
            }
        }
        
        ctx.putImageData(imageData, 0, 0);
        isDrawing = false;
    };        
    
  5. Define setPixelColor
    This function will color the current pixels and get the color from getColor function. getColor function will generate a color based on number of iteration and selected color scheme.

    var getColor = function(i) {            
        i /= maxIteration;
        var cr = 0.0;
        var cg = 0.0;
        var cb = 0.0;
        
        switch(colorScheme) {
            case 1:
                if (i >= 0.66) cr = i;
                else if (i >= 0.33) cg = i;
                else cb = i;
                break;
            case 2:
                if (i >= 0.66) cr = i;
                else if (i >= 0.33) cb = i;
                else cg = i;
                break;
            case 3:
                if (i >= 0.66) cb = i;
                else if (i >= 0.33) cg = i;
                else cr = i;
                break;
            case 4:
                if (i >= 0.66) cg = i;
                else if (i >= 0.33) cr = i;
                else cb = i;
                break;
            case 5:
                if (i >= 0.66) cg = i;
                else if (i >= 0.33) cb = i;
                else cr = i;
                break;
            case 6:
                if (i >= 0.66) cb = i;
                else if (i >= 0.33) cr = i;
                else cg = i;
                break;
        };            
        
        var r = parseInt(cr * 3 * maxColor);
        var g = parseInt(cg * 3 * maxColor);
        var b = parseInt(cb * 3 * maxColor);
        return [r, g, b];
    };
    var setPixelColor = function(x, y, i) {
        var c = getColor(i);
        var off = 4 * (y * cw + x);
        pixels[off] = c[0]; //red
        pixels[off + 1] = c[1]; //green
        pixels[off + 2] = c[2]; //blue
        pixels[off + 3] = 255; //alpha
    };
    
  6. Finally, add the zoom capability.
    var mouseWheelHandler = function(e) {
        // cross-browser wheel delta
        var e = window.event || e; // old IE support
        // get delta scroll
        var delta = Math.max(-1, Math.min(1, (e.wheelDelta || -e.detail)));
        
        //get mouse position
        var mousePos = getMousePos(e);
        
        // scale canvas coordinat to mandlebrot complex coordinat
        var x0 = scaleX*mousePos.x + mx[0], y0 = scaleY*mousePos.y + my[0];
        
        //scale the mandelbrot coordinat size
        var scale = 1 - delta*zoomScale;
        var sizeX = (mx[1]-mx[0])*scale;
        var sizeY = (my[1]-my[0])*scale;
        
        // redefine mandelbrot boundaries
        mx = [x0-sizeX/2, x0+sizeX/2];
        my = [y0-sizeY/2, y0+sizeY/2];
        render();
    };        
    var getMousePos = function(evt) {
        var rect = canvas.getBoundingClientRect();
        return {
            x: evt.clientX - rect.left,
            y: evt.clientY - rect.top
        };
    };
    

Final Source Code

<html>
<head>
    <title>Drawing Mandelbrot set on HTML5 graphics canvas</title>
    <script type="text/javascript">
        var canvas, ctx;
        var iteration, maxIteration = 100, maxColor = 255;
        var cw, ch, scaleX, scaleY;
        var zoomScale = 0.3;
        
        // color schemes
        var colorScheme = 1;        
        
        //define mandlebort boundaries
        var mx = [-2,2], my = [-2,2];
        // hold all pixels image data
        var pixels;        
        var isDrawing = false;
        
        //define complex data type
        var Complex = function(x, y) {            
            this.x = x; // real part
            this.y = y; // imaginary part
        };        
        var setupCanvas = function() {
            canvas = document.getElementById('thecanvas');
            setupMouseListener();

            if (!canvas.getContext) {
                alert("I'm sorry, seems your browser not support HTML 5, try another browser.");
                return;
            }
               
            // get drawing context
            ctx = canvas.getContext('2d');
            
            render();
        };        
        var render = function() {
            if (!canvas.getContext) return;
            
            //get color scheme
            var a = document.getElementById('scheme');
            colorScheme = parseInt((a.value || a.options[a.selectedIndex].value));
            
            //save canvas size into variable
            cw = canvas.width;
            ch = canvas.height;
            
            //define scaling ratio from canvas coordinat to mandlebort coordinat
            scaleX = (mx[1] - mx[0])/cw;
            scaleY = (my[1] - my[0])/ch;
            
            drawMandlebrot();
        };
        var drawMandlebrot = function() {
            if(isDrawing) return;
            
            isDrawing = true;
            
            var imageData = ctx.getImageData(0, 0, cw, ch);
            pixels = imageData.data;
            
            for(var x = 0; x < cw; x++) {
                for(var y = 0; y < ch; y++) {
                    iteration = 0;
                    // scale canvas coordinat to mandlebrot complex coordinat
                    var x0 = scaleX*x + mx[0], y0 = scaleY*y + my[0];
                    // define complex number c
                    var c = new Complex(x0, y0), z = new Complex(0, 0);
                    
                    while(iteration < maxIteration) {
                        // check if z still inbound
                        if(z.x*z.x + z.y*z.y >= 2*2) {
                            break;
                        }
                        
                        // multiply z and z
                        z = new Complex(z.x*z.x - z.y*z.y, z.y*z.x + z.x*z.y);
                        // add z and c
                        z = new Complex(z.x+c.x, z.y+c.y);
                        // inrease iteration
                        iteration++;
                    }
                    
                    // colorize current pixel
                    setPixelColor(x, y, iteration);
                }
            }
            
            ctx.putImageData(imageData, 0, 0);
            isDrawing = false;
        };        
        
        var getColor = function(i) {            
            i /= maxIteration;
            var cr = 0.0;
            var cg = 0.0;
            var cb = 0.0;
            
            switch(colorScheme) {
                case 1:
                    if (i >= 0.66) cr = i;
                    else if (i >= 0.33) cg = i;
                    else cb = i;
                    break;
                case 2:
                    if (i >= 0.66) cr = i;
                    else if (i >= 0.33) cb = i;
                    else cg = i;
                    break;
                case 3:
                    if (i >= 0.66) cb = i;
                    else if (i >= 0.33) cg = i;
                    else cr = i;
                    break;
                case 4:
                    if (i >= 0.66) cg = i;
                    else if (i >= 0.33) cr = i;
                    else cb = i;
                    break;
                case 5:
                    if (i >= 0.66) cg = i;
                    else if (i >= 0.33) cb = i;
                    else cr = i;
                    break;
                case 6:
                    if (i >= 0.66) cb = i;
                    else if (i >= 0.33) cr = i;
                    else cg = i;
                    break;
            };            
            
            var r = parseInt(cr * 3 * maxColor);
            var g = parseInt(cg * 3 * maxColor);
            var b = parseInt(cb * 3 * maxColor);
            return [r, g, b];
        };
        var setPixelColor = function(x, y, i) {
            var c = getColor(i);
            var off = 4 * (y * cw + x);
            pixels[off] = c[0];
            pixels[off + 1] = c[1];
            pixels[off + 2] = c[2];
            pixels[off + 3] = 255;
        };
        var setupMouseListener = function() {
            if (canvas.addEventListener) {
                // IE9, Chrome, Safari, Opera
                canvas.addEventListener("mousewheel", mouseWheelHandler, false);
                // Firefox
                canvas.addEventListener("DOMMouseScroll", mouseWheelHandler, false);
            }
            // IE 6/7/8
            else canvas.attachEvent("onmousewheel", mouseWheelHandler);
        };
        var mouseWheelHandler = function(e) {
            // cross-browser wheel delta
            var e = window.event || e; // old IE support
            // get delta scroll
            var delta = Math.max(-1, Math.min(1, (e.wheelDelta || -e.detail)));
            
            //get mouse position
            var mousePos = getMousePos(e);
            
            // scale canvas coordinat to mandlebrot complex coordinat
            var x0 = scaleX*mousePos.x + mx[0], y0 = scaleY*mousePos.y + my[0];
            
            //scale the mandelbrot coordinat size
            var scale = 1 - delta*zoomScale;
            var sizeX = (mx[1]-mx[0])*scale;
            var sizeY = (my[1]-my[0])*scale;
            
            // redefine mandelbrot boundaries
            mx = [x0-sizeX/2, x0+sizeX/2];
            my = [y0-sizeY/2, y0+sizeY/2];
            render();
        };        
        var getMousePos = function(evt) {
            var rect = canvas.getBoundingClientRect();
            return {
                x: evt.clientX - rect.left,
                y: evt.clientY - rect.top
            };
        };
    </script>
</head>
<body onload="setupCanvas();">
    <div style="margin:0 auto;width:505px;padding-top:10px;">
        <div>Color Scheme:
        <select id="scheme">
            <option value="1" selected>Color Scheme 1</option>
            <option value="2">Color Scheme 2</option>
            <option value="3">Color Scheme 3</option>
            <option value="4">Color Scheme 4</option>
            <option value="5">Color Scheme 5</option>
            <option value="6">Color Scheme 6</option>
        </select>
        <input type="button" value="Render" id="render_btn" onclick="render()"/><br/>
        Scroll your mouse on the canvas to zoomin/zoomout
        </div>
        <canvas style="border:1px solid;" id="thecanvas" width="500" height="500"></canvas>
    </div>
</body>
</html>

Related Posts:

Javascript