Share via


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.

Useful complex operations

<!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:

  1. Divide a 4 x 4 square, centered at the complex origin, into a large number of grid points (not shown):

  2. Treat each grid point as a c value.

  3. For each such c value, check whether |zₙ| escapes within a reasonable number of iterations (under zₙ₊₁ = zₙ + c; where z₀ = 0).

  4. 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:

Mandelbrot 1

<!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.

Mapping screen coordinates to the complex plane