View live demo
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:
<script type="text/javascript" src="complex.js"></script>
Now we can define the function for Mandelbrot:
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:
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.
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);
Now we can define the while
loop of the formula:
while (i < mandelbrot.maxIter && z.abs() <= mandelbrot.bailout) {
z = z.pow(mandelbrot.power).add(xy);
i++;
}
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.
if (i < mandelbrot.maxIter) {
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:
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:
<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.
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.
onload="generateFractal(mandelbrot);"
Define the following global variables:
var lastFormula;
var tim;
var offset = new Complex(-3.0, -2.0);
var size = new Complex(0.25, 0.25);
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
.
clearTimeout(tim);
lastFormula = formula;
Then we have to reset optionally offset and size:
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.
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.
var g = cv.getContext("2d");
var img = g.getImageData(0, 0, w, h);
var pix = img.data;
Now declare the function drawLine
in generateFractal
.
function drawLine() {
}
Now if y < h
(height of the Canvas
is greater than 0):
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:
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.
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:
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
:
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
).
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.
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