Drawing Fractal Mathematics in the Browser — From Mandelbrot Set to Barnsley Fern
Go deeper on this topic
Creative Coding
Mathematical simulations, nature visualization, and generative art in the browser
Article 3 of 12 in this series.
Next reads
Generating Natural Terrain with Perlin Noise — Why Randomness Can Be Beautiful
Step-by-step implementation of natural terrain generation with Perlin noise, from the mathematics of gradient noise to octave composition and 3D mesh rendering on Canvas.
Visualizing Wave Equations in the Browser — Interference, Diffraction, and Resonance
Implementing 1D and 2D wave equations with finite difference methods and visualizing interference, diffraction, and resonance patterns on Canvas. Rendering techniques for WaveInterference, WaveEquation, and ChladniPlate.
The N-Body Problem in the Browser — Implementing Gravitational Simulation
Implementation details of gravitational calculations, vector operations, integration methods, and orbit rendering used in the NBody, SolarSystem, and GravityWell components.
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.