Drawing Mandelbrot Fractals With HTML5 Canvas And JavaScript
In this tutorial we will be creating a demo application that draws Mandelbrot fractals on an HTML5 Canvas using JavaScript. For every pixel on the screen, we need to calculate the color based on the fractal properties. We will need to draw individual pixels to the Canvas. My tutorial Drawing Pixels With HTML5 Canvas And JavaScript explains how to draw pixels on a Canvas, instead of drawing regular primitives likes squares and circles. We implement an interactive demo that draws the Mandelbrot fractal and allows the user to pan and zoom around the screen.
Click here to go directly to the end of this article and view the demo.
How To Draw The Fractal
For every pixel on the screen, we need to make repeated calculations, also called iterations, to determine if the value belongs to the Mandelbrot set or not. Based on how many iterations are needed, we can define a color. Some calculations might take an infinite amount of time to finish, so we must limit the number of iterations that are allowed per pixel. Lets call this limit maxiterations. If we allow a higher number of iterations, our images will have more detail, but the time it takes to calculate the image will be higher as well. In any case, we will have to make a trade-off.
Here is the first part of a method to draw the Mandelbrot fractal on a canvas. We get the canvas and create an ImageData object, as explained in my tutorial Drawing Pixels With HTML5 Canvas And JavaScript. This allows us to draw pixels on the canvas. For every pixel in our canvas, we apply an iterate function and we define a maxiterations parameter. The iterate function calculates and draws an appropriate color to the ImageData object, which will be drawn to the canvas at a later time.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 | // Get the canvas and context var canvas = document.getElementById("viewport"); var context = canvas.getContext("2d"); // Width and height of the image var imagew = canvas.width; var imageh = canvas.height; // Image Data (RGBA) var imagedata = context.createImageData(imagew, imageh); // The maximum number of iterations per pixel var maxiterations = 250; // (...) // Generate the fractal image function generateImage() { // Iterate over the pixels for (var y=0; y<imageh; y++) { for (var x=0; x<imagew; x++) { iterate(x, y, maxiterations); } } } |
The iterate function implements the Escape time algorithm to draw the fractal. It iterates until maxiterations is reached or until a certain escape-condition is met. The result of this process gives us the number of iterations that were reached. This number is an integer value between 0 and maxiterations. If the number of iterations is equal to maxiterations, we draw a black pixel. If the number of iterations is smaller than maxiterations, we draw a color from a specified palette. The color doesn’t follow from the Mandelbrot set itself, it is determined by the creator of the palette. A more faithful representation of the fractal would be a grayscale image, but a colorful image is nicer to look at.
Here is the iterate function. The number of iterations is calculated for the specified pixel. The number is converted to a color value from a pre-generated palette. Finally, the color value of the pixel is copied into the pixel array of the ImageData object. Generating the palette array is explained in the next chapter.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 | // Pan and zoom parameters var offsetx = -imagew/2; var offsety = -imageh/2; var panx = -100; var pany = 0; var zoom = 150; // (..) // Calculate the color of a specific pixel function iterate(x, y, maxiterations) { // Convert the screen coordinate to a fractal coordinate var x0 = (x + offsetx + panx) / zoom; var y0 = (y + offsety + pany) / zoom; // Iteration variables var a = 0; var b = 0; var rx = 0; var ry = 0; // Iterate var iterations = 0; while (iterations < maxiterations && (rx * rx + ry * ry <= 4)) { rx = a * a - b * b + x0; ry = 2 * a * b + y0; // Next iteration a = rx; b = ry; iterations++; } // Get palette color based on the number of iterations var color; if (iterations == maxiterations) { color = { r:0, g:0, b:0}; // Black } else { var index = Math.floor((iterations / (maxiterations-1)) * 255); color = palette[index]; } // Apply the color var pixelindex = (y * imagew + x) * 4; imagedata.data[pixelindex] = color.r; imagedata.data[pixelindex+1] = color.g; imagedata.data[pixelindex+2] = color.b; imagedata.data[pixelindex+3] = 255; } |
Generating A Palette
We are going to generate a palette that is used in the iterate function above. The palette we are generating is an array of 256 colors. I have decided to make a palette with red and yellow colors, but you can choose any colors you want. We start with a darker red color and increase the color values to create a gradient. Below you can see an image of the palette.
Here is the code to generate the palette.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 | // Palette array of 256 colors var palette = []; // (...) // Generate palette function generatePalette() { // Calculate a gradient var roffset = 24; var goffset = 16; var boffset = 0; for (var i=0; i<256; i++) { palette[i] = { r:roffset, g:goffset, b:boffset}; if (i < 64) { roffset += 3; } else if (i<128) { goffset += 3; } else if (i<192) { boffset += 3; } } } |
Mandelbrot Fractal Example
Zoom the fractal by clicking on a part of the fractal. Holding Control while zooming will use the zoom-out function. Holding Shift while zooming will use the pan function.
The full source code is available on GitHub.