Skip to content

Rendering the Mandelbrot Set on an HTML Canvas

Published: at 01:30 AM

Rendering the Mandelbrot Set on an HTML Canvas

The Mandelbrot set is a captivating fractal that exhibits intricate patterns and infinite complexity. Rendering it on an HTML canvas provides a visual exploration of this mathematical wonder. This article explains the core concepts and the process of mapping coordinates to the complex plane to generate the Mandelbrot set.

Understanding the Mandelbrot Set

The Mandelbrot set is defined by the iterative equation:

z_{n+1} = z_n^2 + c

where:

The set consists of all complex numbers c for which the sequence z_n remains bounded (does not diverge to infinity) as n increases.

Imaginary and Complex Numbers

The Complex Plane

The complex plane is a graphical representation of complex numbers. The horizontal axis (x-axis) represents the real part, and the vertical axis (y-axis) represents the imaginary part. Each point in the complex plane corresponds to a complex number.

Mapping Coordinates to the Complex Plane

To render the Mandelbrot set on an HTML canvas, we need to map the pixel coordinates of the canvas to the complex plane. Here’s how:

  1. Canvas Coordinates: The canvas has a coordinate system where (0, 0) is the top-left corner, and (width, height) is the bottom-right corner.
  2. Complex Plane Range: We define a rectangular region in the complex plane that we want to visualize. For example, we might choose a range from -2 to 1 on the real axis and -1.5 to 1.5 on the imaginary axis.
  3. Mapping Function: We create a mapping function that converts canvas coordinates (x, y) to complex coordinates (real, imaginary). This involves scaling and translating the canvas coordinates to fit within the desired complex plane range.
function mapToComplex(
  x,
  y,
  canvasWidth,
  canvasHeight,
  minReal,
  maxReal,
  minImag,
  maxImag
) {
  const real = minReal + (x / canvasWidth) * (maxReal - minReal);
  const imag = maxImag - (y / canvasHeight) * (maxImag - minImag); // Note the inversion of Y axis.
  return { real, imag };
}
  1. Mandelbrot Iteration: For each pixel on the canvas, we:

    • Map the pixel coordinates to a complex number c.
    • Iterate the Mandelbrot equation z_{n+1} = z_n^2 + c for a certain number of iterations.
    • If the magnitude of z exceeds a threshold (e.g., 2), we consider the sequence to be unbounded, and the point c is outside the Mandelbrot set.
    • The number of iterations before divergence determines the color of the pixel.
  2. Coloring: Pixels within the Mandelbrot set are typically colored black. Pixels outside the set are colored based on the number of iterations it took for the sequence to diverge.

Mandelbrot Formula in Detail

When calculating z^2 + c for a complex number z = a + bi, we break it down:

Rendering on Canvas

By iterating through each pixel, mapping it to the complex plane, and applying the Mandelbrot formula, you can generate a visual representation of the Mandelbrot set on an HTML canvas. JavaScript and the HTML canvas element make this visual representation possible.

Below is the code with comments to guide.

And here’s the demo; it has a few controls to tweak the parameters. Please note, while setting higher iterations does allow higher quality especially when zoomed in deep, it might slow down your browser or freeze it.

<canvas></canvas>
<script>
  class MandelbrotCanvas {
    constructor(
      canvasElementSelector,
      canvasWidth,
      canvasHeight,
      maxIterations,
      complexPlane
    ) {
      // Maximum number of iterations to check for divergence
      this.maxIterations = maxIterations || 1000;

      // Define the range of the complex plane you want to draw
      this.complexPlane = complexPlane;

      this.canvas = document.querySelector(canvasElementSelector);
      this.ctx = this.canvas.getContext("2d");
      this.width = canvasWidth;
      this.height = canvasHeight;
      this.canvas.width = canvasWidth;
      this.canvas.height = canvasHeight;
    }

    zoomCanvas(zoomFactor, zoomCenter) {
      // Calculate the complex plane coordinates of the zoom center
      const zoomCenterC = {
        real:
          this.complexPlane.xmin +
          (zoomCenter.x / this.width) *
            (this.complexPlane.xmax - this.complexPlane.xmin),
        imag:
          this.complexPlane.ymin +
          (zoomCenter.y / this.height) *
            (this.complexPlane.ymax - this.complexPlane.ymin),
      };

      // Update the complex plane range based on the zoom factor
      const widthRange =
        (this.complexPlane.xmax - this.complexPlane.xmin) / zoomFactor;
      const heightRange =
        (this.complexPlane.ymax - this.complexPlane.ymin) / zoomFactor;

      this.complexPlane = {
        xmin: zoomCenterC.real - widthRange / 2,
        xmax: zoomCenterC.real + widthRange / 2,
        ymin: zoomCenterC.imag - heightRange / 2,
        ymax: zoomCenterC.imag + heightRange / 2,
      };

      // update this.complexPlane in url as parameters
      const url = new URL(window.location.href);
      url.searchParams.set("xmin", this.complexPlane.xmin);
      url.searchParams.set("xmax", this.complexPlane.xmax);
      url.searchParams.set("ymin", this.complexPlane.ymin);
      url.searchParams.set("ymax", this.complexPlane.ymax);
      url.searchParams.set("iterations", this.maxIterations);
      history.replaceState({}, "", url);

      this.paint();
    }

    resizeCanvas(canvasWidth, canvasHeight) {
      this.width = canvasWidth;
      this.height = canvasHeight;
      this.canvas.width = canvasWidth;
      this.canvas.height = canvasHeight;
      this.paint();
    }

    isInMandelbrotSet(c, maxIterations) {
      // z starts at 0
      let z = { real: 0, imag: 0 };

      // Iterate up to maxIterations
      for (let i = 0; i < maxIterations; i++) {
        // Calculate z^2 + c, where z is a complex number (real and imaginary parts)
        // To square a complex number z = a + bi: (a + bi)^2 = (a^2 - b^2) + 2ab*i
        let real = z.real * z.real - z.imag * z.imag + c.real;
        let imag = 2 * z.real * z.imag + c.imag;

        // Update z to the new value (z^2 + c)
        z = { real: real, imag: imag };

        // If the magnitude of z (|z| = sqrt(real^2 + imag^2)) exceeds 2, it diverges
        if (z.real * z.real + z.imag * z.imag > 4) {
          return i; // c is not in the Mandelbrot set
        }
      }

      return maxIterations; // c is in the Mandelbrot set
    }

    paint() {
      for (let x = 0; x < this.width; x++) {
        for (let y = 0; y < this.height; y++) {
          // Map the canvas coordinates (x, y) to the complex plane (c)
          const c = {
            real:
              this.complexPlane.xmin +
              (x / this.width) *
                (this.complexPlane.xmax - this.complexPlane.xmin),
            imag:
              this.complexPlane.ymin +
              (y / this.height) *
                (this.complexPlane.ymax - this.complexPlane.ymin),
          };

          // Check if the point is in the Mandelbrot set
          const iterations = this.isInMandelbrotSet(c, this.maxIterations);
          var color = `hsl(0 50% 0%)`;
          // Color the pixel based on whether it is in the Mandelbrot set
          if (iterations !== this.maxIterations) {
            var hue = Math.floor((iterations / this.maxIterations) * 360);
            color = `hsl(${hue} 50% 50%)`;
          }

          this.ctx.fillStyle = color;
          this.ctx.fillRect(x, y, 1, 1);
        }
      }

      // Mark center on screen
      // this.ctx.fillStyle = "#f00"
      // this.ctx.fillRect(this.width / 2 - 5, this.height / 2 - 5, 10, 10);
    }
  }

  let maxIterations = 1000;
  let complexPlane = { xmin: -2.5, xmax: 1, ymin: -1, ymax: 1 };

  // fetch parameters from url
  const urlParams = new URLSearchParams(window.location.search);
  if (urlParams.has("xmin")) complexPlane.xmin = Number(urlParams.get("xmin"));
  if (urlParams.has("xmax")) complexPlane.xmax = Number(urlParams.get("xmax"));
  if (urlParams.has("ymin")) complexPlane.ymin = Number(urlParams.get("ymin"));
  if (urlParams.has("ymax")) complexPlane.ymax = Number(urlParams.get("ymax"));
  if (urlParams.has("iterations"))
    maxIterations = Number(urlParams.get("iterations"));

  let canvas = new MandelbrotCanvas(
    "canvas",
    window.innerWidth,
    window.innerHeight,
    maxIterations,
    complexPlane
  );

  window.addEventListener("resize", () => {
    canvas.resizeCanvas(window.innerWidth, window.innerHeight);
  });

  window.addEventListener("click", () => {
    canvas.zoomCanvas(5, { x: event.clientX, y: event.clientY });
  });

  canvas.paint();
</script>

Previous Post
Understanding Expires Headers for Web Caching
Next Post
Multiple Choice