An initial rendering of the Mandelbrot set using canvas
Building on information presented in A brief overview of the Mandelbrot set, we describe here a basic method for rendering the Mandelbrot set using canvas.
Before presenting an algorithm for drawing the Mandelbrot set, we first create a JavaScript "class" for manipulating complex numbers. As indicated in A brief overview of the Mandelbrot set, the only operations needed are the ability to square, add, and take the absolute value of complex numbers.
<!DOCTYPE html>
<html>
<head>
<meta content="text/html; charset=utf-8" http-equiv="Content-Type">
<title>Useful Complex Operations</title>
<style>
body {
text-align: center;
}
</style>
</head>
<body>
<h1>Useful Complex Operations</h1>
<script>
function Complex(x, y) {
// Constructs the complex number x + yi.
this.x = x || 0; // Default to 0 if this parameter is undefined.
this.y = y || 0;
} // Complex
Complex.prototype.toString = function() {
// Returns a string representing this complex number in the form "x + yi".
return this.y >= 0 ? this.x + " + " + this.y + "i" : this.x + " - " + (-this.y) + "i";
} // toString
Complex.prototype.modulus = function() {
// Returns a real number equal to the absolute value of this complex number.
return Math.sqrt(this.x*this.x + this.y*this.y);
} // modulus
Complex.prototype.add = function(z) {
// Returns a complex number equal to the sum of the given complex number and this complex number.
return new Complex(this.x + z.x, this.y + z.y);
} // sum
Complex.prototype.square = function() {
// Returns a complex number equal to the square of this complex number.
var x = this.x*this.x - this.y*this.y;
var y = 2*this.x*this.y;
return new Complex(x, y);
} // square
var z1 = new Complex(0.2, 0.4);
var z2 = new Complex(-0.6, 0.5);
var z3 = z1.add(z2.square()); // Square z2 and add z1 to it.
alert(z3.toString()); // Print the complex result in x + yi form.
alert(z3.modulus()); // Print absolute value of z3.
</script>
</body>
</html>
In the Useful complex operations example, we have:
- z₁ = 0.2 + 0.4i
- z₂ = -0.6 + 0.5i
- z₃ = (z₂)² + z₁ = (-0.6 + 0.5i)² + (0.2 + 0.4i) = 0.31 - 0.2i
- |z₃| = z3.modulus() ≈ 0.3689
With the complex JavaScript "class" in place, we present a simple algorithm for drawing the Mandelbrot set:
Divide a 4 x 4 square, centered at the complex origin, into a large number of grid points (not shown):
Treat each grid point as a c value.
For each such c value, check whether |zₙ| escapes within a reasonable number of iterations (under zₙ₊₁ = zₙ + c; where z₀ = 0).
If not, color c black (black indicates that c is a member of the Mandelbrot set). This assumes we're drawing the Mandelbrot set on a white background.
Implementing this algorithm in JavaScript produces an image similar to this:
In this image, both red coordinate axes have a length of 4. That is, a circle of radius 2, centered at the origin of the complex plane, would exactly contain these axes. As stated previously, the Mandelbrot set lies entirely within a circle of radius 2, and this fact is leveraged in the code used to create this image, as shown next:
<!DOCTYPE html>
<html>
<head>
<meta content="text/html; charset=utf-8" http-equiv="Content-Type">
<title>Mandelbrot 1</title>
<style>
body {
text-align: center;
}
canvas {
border: 1px black solid;
}
</style>
</head>
<body>
<h1>Mandelbrot 1</h1>
<p>This example demonstrates a basic algorithm for drawing the Mandelbrot set using complex plane coordinates.</p>
<canvas width="600" height="600">Canvas not supported - upgrade your browser</canvas>
<script>
var CPS = 2; // CPS stands for "complex plane square". That is, we are examining a 2*CPS by 2*CPS square region of the complex plane such that this square (or box) is centered at the complex plane's origin.
var MAX_ITERATIONS = 300; // Increase to improve detection of complex c values that belong to the Mandelbrot set.
var DELTA = 0.008; // Decreasing this value increases the number of "pixels" on the canvas, thereby increasing the size of the rendering but without losing image resolution.
function Complex(x, y) {
// Constructs the complex number x + yi. If any parameter is undefined, 0 is used instead.
this.x = x || 0;
this.y = y || 0;
} // Complex
Complex.prototype.toString = function() {
// Returns a string representing this complex number in the form "x + yi".
return this.y >= 0 ? this.x + " + " + this.y + "i" : this.x + " - " + (-this.y) + "i";
} // toString
Complex.prototype.modulus = function() {
// Returns a real number equal to the absolute value of this complex number.
return Math.sqrt(this.x*this.x + this.y*this.y);
} // modulus
Complex.prototype.add = function(z) {
// Returns a complex number equal to the sum of the given complex number and this complex number.
return new Complex(this.x + z.x, this.y + z.y);
} // sum
Complex.prototype.square = function() {
// Returns a complex number equal to the square of this complex number.
var x = this.x*this.x - this.y*this.y;
var y = 2*this.x*this.y;
return new Complex(x, y);
} // square
var globals = {}; // Store all would-be-global-variables in one handy global object.
globals.canvas = document.getElementsByTagName('canvas')[0];
globals.canvas.ctx = globals.canvas.getContext('2d');
globals.canvas.ctx.fillStyle = "black";
initializeCoordinateSystem();
drawMandelbrotSet();
drawCoordinateAxes();
function initializeCoordinateSystem() {
var ctx = globals.canvas.ctx;
ctx.translate(globals.canvas.width / 2, globals.canvas.height / 2); // Move the canvas's coordinate system to the center of the canvas.
ctx.scale(1/DELTA, -1/DELTA); // Flip the y-axis to produce a standard Cartesian coordinate system and scale the canvas coordinate system to match the region of the complex plane under consideration.
} // initializeCoordinateSystem
function drawMandelbrotSet() {
var ctx = globals.canvas.ctx;
for (var Re = -CPS; Re <= CPS; Re = Re + DELTA) { // Represents the Re-axis. Re represents the real part of a complex c value.
next_c_value: // "continue next_c_value;" is equivalent to an old school GOTO statement (which can be very handy in deeply nested loops).
for (var Im = -CPS; Im <= CPS; Im = Im + DELTA) { // Represents the Im-axis. Im represents the imaginary part of a complex c value.
var z = new Complex(0, 0); // Represents Zo (where "o" indicates subscript 0).
var c = new Complex(Re, Im); // Represents a complex c value, which either does or does not belong to the Mandelbrot set, as determined in the next FOR loop.
for (var iterationCount = 1; iterationCount <= MAX_ITERATIONS; iterationCount++) {
z = c.add( z.square() ); // Performs Zn+1 = (Zn)^2 + c
if (z.modulus() > 2) {
continue next_c_value; // The complex c value is not part of the Mandelbrot set, so immediately check the next one.
} // if
} // for
// Assert: z.modulus() <= 2, therefore the complex c value is probably a member of the Mandelbrot set - increase MAX_ITERATIONS to improve this determination.
ctx.fillRect(Re, Im, DELTA, DELTA); // This c value is probably part of the Mandelbrot set, so color this pixel black. A "pixel" for the canvas is a DELTA x DELTA black square.
} // for
} // for
} // drawMandelbrotSet
function drawCoordinateAxes() {
/*
Draws coordinate axes that are exactly as long as the (square) complex plane region under consideration.
*/
var ctx = globals.canvas.ctx;
ctx.lineWidth = DELTA;
ctx.strokeStyle = "red";
// Draw the x-axis:
ctx.beginPath();
ctx.moveTo(CPS, 0);
ctx.lineTo(-CPS, 0);
ctx.stroke();
// Draw the y-axis:
ctx.beginPath();
ctx.moveTo(0, CPS);
ctx.lineTo(0, -CPS);
ctx.stroke();
} // drawCoordinateAxes
</script>
</body>
</html>
The key functions to examine are initializeCoordinateSystem
and drawMandelbrotSet
.
initializeCoordinateSystem
As the name implies, initializeCoordinateSystem
initializes the coordinate system used in the 600 pixel by 600 pixel canvas. By default, the point (0, 0) is always in the upper-left corner of the canvas. To use our familiar Cartesian coordinate system, we must translate this origin to the center of the canvas as follows:
ctx.translate(globals.canvas.width / 2, globals.canvas.height / 2);
We now have the origin of the coordinate system centered but the positive y-axis is pointing down (increasing) when it should be pointing up (increasing). To correct this, we can use the scale method as follows:
ctx.scale(1, -1);
This has the effect of flipping the y-axis (while leaving the x-axis unchanged), resulting in a traditional Cartesian coordinate system.
drawMandelbrotSet
The scale method can also be used to increase or decrease the size of a rendered image (the larger the scaling factor, the larger the rendered image becomes). We use this method in the example code as follows:
ctx.scale(1/DELTA, -1/DELTA);
If DELTA
is small (say 0.008) and also represents the size of a canvas pixel, a normally tiny image is significantly increased in size in that ctx.scale(1/DELTA, -1/DELTA)
becomes ctx.scale(125, -125)
. This becomes apparent when we examine drawMandelbrotSet
:
drawMandelbrotSet
function drawMandelbrotSet() {
var ctx = globals.canvas.ctx;
for (var Re = -CPS; Re <= CPS; Re = Re + DELTA) { // Represents the Re-axis. Re represents the real part of a complex c value.
next_c_value: // "continue next_c_value;" is equivalent to an old school GOTO statement (which can be very handy in deeply nested loops).
for (var Im = -CPS; Im <= CPS; Im = Im + DELTA) { // Represents the Im-axis. Im represents the imaginary part of a complex c value.
var z = new Complex(0, 0); // Represents Zo (where "o" indicates subscript 0).
var c = new Complex(Re, Im); // Represents a complex c value, which either does or does not belong to the Mandelbrot set, as determined in the next FOR loop.
for (var iterationCount = 1; iterationCount <= MAX_ITERATIONS; iterationCount++) {
z = c.add( z.square() ); // Performs Zn+1 = (Zn)^2 + c
if (z.modulus() > 2) {
continue next_c_value; // The complex c value is not part of the Mandelbrot set, so immediately check the next one.
} // if
} // for
// Assert: z.modulus() <= 2, therefore the complex c value is probably a member of the Mandelbrot set - increase MAX_ITERATIONS to improve this determination.
ctx.fillRect(Re, Im, DELTA, DELTA); // This c value is probably part of the Mandelbrot set, so color this pixel black. A "pixel" for the canvas is a DELTA x DELTA black square.
} // for
} // for
} // drawMandelbrotSet
In that CPS
equals 2, we’re examining each grid point in the following square (grid points not shown):
The number of grid points within this square is determined by DELTA
. As DELTA
decreases, the number of grid points increases:
for (var Re = -CPS; Re <= CPS; Re = Re + DELTA) {
…
for (var Im = -CPS; Im <= CPS; Im = Im + DELTA) {
…
}
}
We next set z₀ to 0 + 0i = 0 as required and create c based on which grid point (Re, Im) we’re currently examining:
var z = new Complex(0, 0);
var c = new Complex(Re, Im);
We can now determine if c is (more than likely) in the Mandelbrot set by calculating |zₙ| (within a reasonable number of iterations) and checking if |zₙ| is greater than 2:
for (var iterationCount = 1; iterationCount <= MAX_ITERATIONS; iterationCount++) {
z = c.add( z.square() ); // Performs Zn+1 = (Zn)^2 + c
if (z.modulus() > 2) {
continue next_c_value; // The complex c value is not part of the Mandelbrot set, so immediately check the next one.
} // if
} // for
Why |zₙ| > 2? Because, and as described in Basic properties, if the absolute value of zₙ ever becomes larger than 2, the sequence will always escape to infinity (indicating that c is not part of the Mandelbrot set).
If, after MAX_ITERATIONS
, the absolute value of zₙ is less than or equal to 2 (z.modulus() <= 2
), the c value associated with zₙ is more than likely a member of the Mandelbrot set. Thus, we color c’s grid point black:
ctx.fillRect(cx, cy, DELTA, DELTA);
Lastly, we draw two red coordinate axes by calling drawCoordinateAxes
.
One of the most egregious issues with Mandelbrot 1 is that we cannot zoom in to examine the finer details of the Mandelbrot set. In order to facilitate this necessary feature, we must first determine how to map a canvas screen point (in pixels) to the complex plane, which is discussed in Mapping screen coordinates to the complex plane.