Mandelbrot Set

A real-time WebGL fractal explorer. For every pixel c, the sequence z ↦ z² + c starts at z = 0 and is iterated to see whether it escapes or stays bounded forever. Drag to pan, scroll to zoom, or click anywhere on the boundary to preview the Julia set it produces.

drag to pan · scroll to zoom · click to pick a point

Notable locations:

Preview the Julia set for a point

Click anywhere on the fractal above to pick a value of c. The mini map shows the Julia set for that c — the boundary is connected exactly when c lies inside the Mandelbrot set (the dark region); points picked from the wispy filaments outside it produce dust-like, disconnected Julia sets.

Open this point in the full Julia Set explorer →

z₀ = 0, zn+1 = z_n² + c

The Mandelbrot set, named after Benoît Mandelbrot, is the set of complex numbers c for which this sequence never escapes to infinity. Each pixel here is colored by an escape-time count — how many iterations it survives before crossing the bailout radius — smoothed to avoid banding. Points that never escape (within the iteration budget) are rendered as the dark background: that's the set itself.

Learn more on Wikipedia →

Understanding the Mandelbrot Set

The Mandelbrot set is a fractal — an infinite pattern that looks similar at every scale. Let's walk through the mathematical ideas step by step, starting from the basics.

Step 1: What are complex numbers?

A complex number combines two parts: a real part and an imaginary part. We write it as a + bi, where a is real and i is the imaginary unit. What's i? The letter i is defined as the square root of −1. In normal math, you can't square a number and get −1 (negative). But mathematicians invented i to solve equations that would otherwise be impossible. Think of it as a number on a separate axis, perpendicular to the real number line. Visualizing complex numbers: We can plot them on a 2D plane. The horizontal axis is the real part, the vertical axis is the imaginary part. So 3 + 2i is a point at coordinates (3, 2).

Step 2: The iteration formula

To calculate the Mandelbrot set, we test each point c using this formula:

z₀ = 0
zn+1 = z_n² + c
Starting with z = 0, we repeatedly apply the same operation: square z, then add c. The fascinating question is: Does z grow larger and larger without bound (escape to infinity), or does it stay confined within a limited range? What does "escape to infinity" mean? When we square a number larger than 1, it grows. For example: 2² = 4, then 4² = 16, then 16² = 256... it explodes exponentially. If at any point |z| (the distance from zero) exceeds 2, the squaring operation guarantees it will keep growing until infinity. We call 2 the bailout radius. What does "stay bounded" mean? For some values of c, z bounces around but never gets too large. No matter how many times we iterate, it stays within a reasonable range—say, always between −2 and 2. These are the points inside the Mandelbrot set. Example: If c = 0, then z starts at 0, and z² + 0 = 0 every iteration. It never escapes. But if c = 1, then z = 0 → 0² + 1 = 1 → 1² + 1 = 2 → 2² + 1 = 5 → 5² + 1 = 26... it quickly escapes! This is why different colors represent how fast a point escapes.
z₀ = 0
zn+1 = z_n² + c
Partendo da z = 0, ripetiamo la stessa operazione: eleviamo z al quadrato, poi aggiungiamo c. La domanda affascinante è: z cresce sempre più grande senza limiti (sfugge all'infinito), o rimane confinato entro un intervallo limitato? Cosa significa "sfuggire all'infinito"? Quando eleviamo al quadrato un numero più grande di 1, cresce. Per esempio: 2² = 4, poi 4² = 16, poi 16² = 256... cresce esponenzialmente. Se ad un certo punto |z| (la distanza da zero) supera 2, il quadrato garantisce che continuerà a crescere all'infinito. Chiamiamo 2 il raggio di fuga (bailout radius). Cosa significa "rimanere limitato"? Per alcuni valori di c, z rimbalza ma non diventa mai troppo grande. Non importa quante volte iteriamo, rimane entro un intervallo ragionevole—ad esempio, sempre tra −2 e 2. Questi sono i punti dentro l'insieme di Mandelbrot. Esempio: Se c = 0, allora z parte da 0, e z² + 0 = 0 ad ogni iterazione. Non scappa mai. Ma se c = 1, allora z = 0 → 0² + 1 = 1 → 1² + 1 = 2 → 2² + 1 = 5 → 5² + 1 = 26... scappa velocemente! Questo è il motivo per cui colori diversi rappresentano la velocità con cui un punto sfugge.

Step 3: Points inside vs outside

Inside the set (dark regions): If we iterate a point c hundreds of times and it never escapes beyond radius 2, we consider it part of the Mandelbrot set. These points are drawn in dark (almost black). Outside the set (colored regions): If a point escapes, we record how many iterations it took before reaching radius 2. A point that escapes in 5 iterations gets colored differently than one that takes 50 iterations. This creates the beautiful colored bands around the set boundary. The boundary between inside and outside is infinitely detailed — no matter how much you zoom in, you'll always see more structure. That's what makes it a fractal!

Step 4: Turning escape time into color

If we just color pixels by raw iteration count, we get harsh, jagged stripes (called "banding"). To smooth this, we apply a smoothing algorithm that uses the magnitude of z when it escapes. Once we have a smooth iteration count, we cycle through a color palette — a carefully chosen gradient of colors. In this explorer, the palette transitions through deep blue, cyan, yellow, and red as the value increases. The Color density slider controls how quickly the palette cycles. A higher value means colors change faster, creating tighter bands. A lower value spreads the palette out over a wider range.

Step 5: The escape time algorithm in detail

Here's the actual process for a single pixel:

  1. Convert the pixel's screen position into a complex number (c)
  2. Set z = 0 + 0i (starting point)
  3. For each iteration (up to the max):
  4. Calculate: z = z² + c (using complex number arithmetic)
  5. If |z| > 2, the point has escaped — record the iteration count and stop
  6. Otherwise, continue to the next iteration
  7. Color the pixel based on the total iteration count

Why does zooming reveal infinite structure?

The Mandelbrot set is self-similar — smaller copies of the entire set appear within itself at smaller scales. The boundary between the set and the empty space is infinitely complex. When you zoom in, you're not just magnifying the same pattern; you're discovering entirely new patterns that were hidden at larger scales. This infinite complexity from a simple rule is the defining characteristic of fractals.

How this page works

The fractal is computed entirely on the GPU, one pixel at a time, inside a WebGL fragment shader — there's no per-pixel loop on the CPU. Here's the code broken into its main blocks.

1. Map each pixel to a point in the complex plane

The canvas is a single fullscreen quad (two triangles, four vertices). The vertex shader just forwards each corner's [-1, 1] coordinate to the fragment shader as vUv. The fragment shader then turns that into a complex number using the current pan/zoom state:

vec2 coord = uCenter + vec2(vUv.x * uAspect, vUv.y) * uScale;

2. Simulate the escape: iterate z ↦ z² + c until it flies off

For the main view, z starts at 0 and c is that pixel's coordinate — that's the Mandelbrot set. For the Julia preview it's flipped: z starts at the pixel's coordinate and c is the picked point. Both share one shader, switched by a uMode uniform. This loop runs independently for every pixel, in parallel, every frame — that's the "simulation":

for (int i = 0; i < MAX_ITERATIONS; i++) {
  if (float(i) >= uMaxIter) break;
  float x2 = z.x * z.x - z.y * z.y + c.x;
  float y2 = 2.0 * z.x * z.y + c.y;
  z = vec2(x2, y2);
  if (dot(z, z) > 4.0) break;   // escaped past radius 2
  iter += 1.0;
}

3. Turn the iteration count into a color

A raw integer count bands harshly, so it's smoothed with a logarithmic correction (smoothIter) and fed through a 5-stop gradient (palette()). Pixels that never escape within the iteration budget are painted the solid dark background instead.

4. Drive the shader from TypeScript

createFractalRenderer() compiles and links the shader once per canvas, then exposes a single draw(uniforms) call that resizes the canvas, uploads c, center, scale and the rest as uniforms, and issues one draw call. The exact same function renders both the main canvas (mode 1) and the Julia preview (mode 0) — only the uniforms passed in differ.

A requestAnimationFrame loop redraws the main canvas every frame. Pointer drag events convert pixel deltas into a complex-plane pan; the wheel handler zooms by scaling scale while re-solving center so the point under the cursor doesn't move. A click that doesn't drag converts the pointer position back into a complex number, redraws the Julia preview for it, and updates the "open in Julia Set explorer" link with that point encoded as ?re=&im= query parameters.