← Blog
MathCanvas APIFractalTypeScript

Drawing Fractal Mathematics in the Browser — From Mandelbrot Set to Barnsley Fern

9 min read

Go deeper on this topic

Creative Coding

Mathematical simulations, nature visualization, and generative art in the browser

Article 3 of 12 in this series.

View topic page

Use the topic hub as a map, then continue to the next article in the series. New here? Start at the home hub →

What Is the Mandelbrot Set

For a complex number c, iterate z_{n+1} = z_n² + c. The set of c values that do not diverge is the Mandelbrot set. Infinite complexity emerges from a simple formula.

Escape Time Algorithm

Each pixel is mapped to a point on the complex plane, and the color is determined by the number of iterations until divergence:

function mandelbrot(
  cx: number,
  cy: number,
  maxIter: number,
): number {
  let zx = 0
  let zy = 0

  for (let i = 0; i < maxIter; i++) {
    const zx2 = zx * zx
    const zy2 = zy * zy

    if (zx2 + zy2 > 4) return i

    zy = 2 * zx * zy + cy
    zx = zx2 - zy2 + cx
  }

  return maxIter
}

zx² + zy² > 4 is the divergence test. It is mathematically proven that once |z| > 2, divergence is guaranteed. Pre-computing zx2 and zy2 is an optimization that eliminates two multiplications.

Pixel-to-Complex-Plane Mapping

Mapping (px, py) on the Canvas API to (cx, cy) on the complex plane:

function pixelToComplex(
  px: number,
  py: number,
  width: number,
  height: number,
  centerX: number,
  centerY: number,
  zoom: number,
) {
  const cx = centerX + (px - width / 2) / (width * zoom)
  const cy = centerY + (py - height / 2) / (height * zoom)
  return { cx, cy }
}

At zoom=1, the full picture is visible; at zoom=1000+, fine structures appear. The same patterns scale down infinitely.

Color Mapping

Mapping iteration counts to hues creates beautiful gradients:

function iterToColor(iter: number, maxIter: number): [number, number, number] {
  if (iter === maxIter) return [0, 0, 0]

  const t = iter / maxIter
  const r = Math.floor(9 * (1 - t) * t * t * t * 255)
  const g = Math.floor(15 * (1 - t) * (1 - t) * t * t * 255)
  const b = Math.floor(8.5 * (1 - t) * (1 - t) * (1 - t) * t * 255)
  return [r, g, b]
}

Color scheme based on Bernstein polynomials. Points inside the set (iter === maxIter) are colored black.

Barnsley Fern — IFS

Iterated Function Systems (IFS) generate self-similar shapes by probabilistically applying affine transformations:

type AffineTransform = {
  a: number; b: number; c: number; d: number
  e: number; f: number
  prob: number
}

const FERN: AffineTransform[] = [
  { a: 0,    b: 0,    c: 0,    d: 0.16, e: 0,   f: 0,    prob: 0.01 },
  { a: 0.85, b: 0.04, c: -0.04, d: 0.85, e: 0,   f: 1.6,  prob: 0.85 },
  { a: 0.2,  b: -0.26, c: 0.23, d: 0.22, e: 0,   f: 1.6,  prob: 0.07 },
  { a: -0.15, b: 0.28, c: 0.26, d: 0.24, e: 0,   f: 0.44, prob: 0.07 },
]

function barnsleyFern(iterations: number): [number, number][] {
  const points: [number, number][] = []
  let x = 0
  let y = 0

  for (let i = 0; i < iterations; i++) {
    const r = Math.random()
    let cumProb = 0
    for (const t of FERN) {
      cumProb += t.prob
      if (r <= cumProb) {
        const nx = t.a * x + t.b * y + t.e
        const ny = t.c * x + t.d * y + t.f
        x = nx
        y = ny
        break
      }
    }
    points.push([x, y])
  }

  return points
}

Just four affine transformations and their probabilities produce a surprisingly realistic fern leaf.

Recursive Drawing with FractalTree

FractalTree uses recursion to branch:

function drawBranch(
  ctx: CanvasRenderingContext2D,
  x: number,
  y: number,
  length: number,
  angle: number,
  depth: number,
) {
  if (depth <= 0 || length < 2) return

  const endX = x + Math.cos(angle) * length
  const endY = y + Math.sin(angle) * length

  ctx.beginPath()
  ctx.moveTo(x, y)
  ctx.lineTo(endX, endY)
  ctx.lineWidth = depth * 0.8
  ctx.strokeStyle = `hsl(30, \${40 + depth * 5}%, \${20 + depth * 3}%)`
  ctx.stroke()

  const branchAngle = Math.PI / 6
  drawBranch(ctx, endX, endY, length * 0.7, angle - branchAngle, depth - 1)
  drawBranch(ctx, endX, endY, length * 0.7, angle + branchAngle, depth - 1)
}

At depth 10, there are 1024 branch tips. Beyond depth 12, Canvas draw calls become heavy, so 10-12 is the practical limit.

Sierpinski Triangle via Chaos Game

The Sierpinski triangle can be drawn using the Chaos Game. Randomly pick one of 3 vertices and plot a point at the midpoint between the current position and the chosen vertex:

function sierpinski(
  ctx: CanvasRenderingContext2D,
  vertices: [number, number][],
  iterations: number,
) {
  let x = vertices[0][0]
  let y = vertices[0][1]

  for (let i = 0; i < iterations; i++) {
    const v = vertices[Math.floor(Math.random() * 3)]
    x = (x + v[0]) / 2
    y = (y + v[1]) / 2
    ctx.fillRect(x, y, 1, 1)
  }
}

Randomness producing order. The essence of fractals — "a stochastic algorithm generating a deterministic shape" — is captured here.

Summary: Tools for Fractal Drawing

For a deeper exploration of the relationship between fractals and mathematics, the books on the tool shelf serve as a great starting point. The experience of "seeing" equations is a unique pleasure of browser-based implementation. Fractal drawing is also a popular theme in Processing and p5.js.