GODRICH
#css-animation#button#loader#transition#scroll#dashboard#ui#design-trends

Mandelbrot Zoom, 9 Ways (Escape-Time, No Library)

Iterate z = z² + c per pixel and count escapes — the Mandelbrot cardioid appears, and every zoom level hides new mini-mandelbrots forever.

Auto-plays · click a tile to jump to its section · all nine in one zip

From Escape Time to the Landing Screen: Nine Layers of the Mandelbrot Set

The nine scenes climb one axis: what each one keeps from the iteration. 01 keeps only the escape count — the base image, with the palette cycling so the staircase stays visible instead of hidden. 02 keeps the count but moves the viewport exponentially, diving toward a cusp. 03 keeps the formula and swaps which side is fixed, connecting the set to its Julia cousins live. 04 throws the count out and keeps the orbit's geometry instead. 05 keeps the orbit itself, as accumulated light. 06 changes one absolute value in the formula and grows a different creature altogether. 07 hands the viewport to the pointer. 08 repairs the one artifact the integer count leaves behind. 09 assembles a dive, a veil, and typography into a product's first screen. It is the same per-pixel field computation that lights a sphere you find by marching a ray until it crosses the surface — except there the loop walks a ray through space and here it walks one complex number through squarings, and the reward runs the other way: the ray marcher converges on an answer, while a fractal dive never finishes; it only deepens.

Code per item

Each snippet below is lifted from that item's own script tag — constant names intact, comments translated, identifiers untouched. Paste one next to a canvas element and it draws the same picture the panel above it renders.

01Escape-time basics — square, add, and color by the count

Every pixel becomes one complex number c, the loop squares and adds until the magnitude passes 2, and the step count at that moment picks the color — the cardioid and its bulb are simply where the count never finishes, painted at 32.008% cumulative area with a 30.799% peak frame and 24.9 average intensity under a 2.0s palette cycle. The viewport is the classic overview at center −0.75, 0 with span 2.9, and the integer count is exactly what makes the bands a staircase — the flaw item 08 exists to erase.

Escape time basics — iterate z = z² + c per pixel and color by escape count
Open in a new tab

01 · Escape time basics — iterate z = z² + c per pixel and color by escape count — Measured area 32.008% · moving frames 23/23Original by GODRICH · SCSS and React ship in the zip · See the password

CanvasRenderingContext2DcreateImageDatarequestAnimationFrame

Four real operations and one comparison per step

        var cr = CX - SPAN / 2 + i * dx;
        // start from z = 0 and repeat z = z² + c — the step count n at escape is the color index
        var zr = 0, zi = 0, n = 0;
        while (n < MAXIT && zr * zr + zi * zi <= 4) {
          var t = zr * zr - zi * zi + cr;
          zi = 2 * zr * zi + ci;
          zr = t;
          n++;
        }
        var r, g, b;
        if (n >= MAXIT) {
          r = C_SET[0]; g = C_SET[1]; b = C_SET[2];
        } else {
          // palette cycle — adding phase to n sweeps the 5-color band through the set
          var u = (n / MAXIT) * 4 + phase;
          var seg = u - Math.floor(u / 4) * 4;
          var a = PAL[Math.floor(seg) % 4], bb = PAL[(Math.floor(seg) + 1) % 4];
          var f = seg - Math.floor(seg);
          r = a[0] + (bb[0] - a[0]) * f;
          g = a[1] + (bb[1] - a[1]) * f;
          b = a[2] + (bb[2] - a[2]) * f;
        }
        px[k++] = r; px[k++] = g; px[k++] = b; px[k++] = 255;
      }

02Deep-zoom dive — magnifying toward a cusp target

The magnification climbs to x25 over each 2-second loop while the center glides toward the cusp target −0.743643887037, 0.13182590421, and the iteration ceiling rises from 96 to 500 in step with the dive so the fine outlines never starve — 34.847% painted, 31.349% peak, 73.4 average intensity, the second brightest of the nine. It is the heart of any mandelbrot set generator aimed at a chosen target: zoom and ceiling rise together, or not at all.

Infinite deep zoom — an automatic dive toward a chosen target
Open in a new tab

02 · Infinite deep zoom — an automatic dive toward a chosen target — Measured area 34.847% · moving frames 23/23Original by GODRICH · SCSS and React ship in the zip · See the password

createImageDatarequestAnimationFrameMath.log

The exponential dive and its inverse

  // zoom state at progress t (0~1) — exponential dive: scale = 2.9 / k, k = 25^t.
  // Math.log(k)/Math.log(ZOOM2S) recovers t from k (the inverse), used for the label check.
  // the center approaches the target with t so an off-screen target never jumps into view.
  function stateAt(t) {
    var k = Math.pow(ZOOM2S, t);
    var progress = Math.log(k) / Math.log(ZOOM2S);   // magnification → progress inverse (= t, verification)
    var cx = -0.75 + (TX + 0.75) * t;
    var cy = (TY) * t;
    var maxit = Math.round(MAXIT0 + (MAXIT1 - MAXIT0) * progress);
    return { cx: cx, cy: cy, scale: 2.9 / k, maxit: maxit, k: k };
  }

03Julia set link — one pointer, two sets, one formula

The left canvas renders the Mandelbrot at span 2.9 while the right renders the Julia set fractal for whatever c the pointer — or the automatic boundary cruise on a 2s period — selects, both computed simultaneously at 150px each, which is why the painted area lands at 12.885% with a 7.357% peak, the lowest of the nine and a structural property of the two-canvas split rather than a defect. Average intensity holds at 73.3 because both sets re-render every frame.

Julia set link — pointer picks c on the Mandelbrot, a second canvas morphs
Open in a new tab

03 · Julia set link — pointer picks c on the Mandelbrot, a second canvas morphs — Measured area 12.885% · moving frames 23/23Original by GODRICH · SCSS and React ship in the zip · See the password

CanvasRenderingContext2DcreateImageDatapointermove

One formula with the fixed side swapped

    var span = 3.0;                     // Julia viewport width — the z plane at ±1.5
    var dx = span / iw, dy = span / iw;
    var oy0 = -(ih * dy) / 2;
    var k = 0;
    for (var j = 0; j < ih; j++) {
      var zi0 = oy0 + j * dy;
      for (var i = 0; i < iw; i++) {
        var zr0 = -span / 2 + i * dx;
        var zr = zr0, zi = zi0, n = 0;
        while (n < MAXIT_J && zr * zr + zi * zi <= 4) {
          var t = zr * zr - zi * zi + cr;
          zi = 2 * zr * zi + ci;
          zr = t; n++;
        }
        var c = colorOf(n, MAXIT_J);
        px[k++] = c[0]; px[k++] = c[1]; px[k++] = c[2]; px[k++] = 255;
      }
    }

04Orbit traps — coloring by what the orbit brushed

Instead of the escape count, the loop tracks the closest approach of each orbit to the real axis, the imaginary axis, and a circle of radius 0.62, and colors by that minimum — 32.008% painted, exactly tied with item 01 because the viewport matches, with a 28.188% peak and 28.5 intensity as the three traps cycle on a 2s period. Brushed spots glow in a pastel gold accent.

Orbit traps — coloring by what the iteration path brushes against
Open in a new tab

04 · Orbit traps — coloring by what the iteration path brushes against — Measured area 32.008% · moving frames 23/23Original by GODRICH · SCSS and React ship in the zip · See the password

createImageDataMath.hypotrequestAnimationFrame

Three distances and one running minimum

  function trapDist(zr, zi, kind, best) {
    var d;
    if (kind === 0) d = Math.abs(zi);                    // closeness to the real axis
    else if (kind === 1) d = Math.abs(zr);               // closeness to the imaginary axis
    else d = Math.abs(Math.hypot(zr, zi) - 0.62);        // closeness to the circle of radius 0.62
    return d < best ? d : best;
  }

        var best = 1e9;                     // this pixel orbit's closest trap distance
        while (n < MAXIT && zr * zr + zi * zi <= 4) {
          var t = zr * zr - zi * zi + cr;
          zi = 2 * zr * zi + ci;
          zr = t;
          best = trapDist(zr, zi, kind, best);
          n++;
        }

05Buddhabrot — accumulating only the escaping orbits

Every step seeds 260 orbits from a fixed mulberry32 stream — 30 steps per second — discards the ones that never escape, and adds one visit to every pixel the survivors cross, with brightness log-scaled to saturate at 30 visits and a decay of 0.94 fading the oldest trails — 45.133% painted, 41.835% peak, 34.4 intensity. The 2s loop resets the accumulator so the figure surfaces again from black.

Buddhabrot — accumulating escaping orbits until a ghost figure emerges
Open in a new tab

05 · Buddhabrot — accumulating escaping orbits until a ghost figure emerges — Measured area 45.133% · moving frames 23/23Original by GODRICH · SCSS and React ship in the zip · See the password

createImageDataglobalAlphaMath.random

Judge first, then walk again with a trail

    var cr = RX0 + rnd() * (RX1 - RX0);
    var ci = RY0 + rnd() * (RY1 - RY0);
    var zr = 0, zi = 0, n = 0;
    // first pass judges divergence with no trail recorded — only escapers walk again
    while (n < MAXIT && zr * zr + zi * zi <= 4) {
      var t = zr * zr - zi * zi + cr;
      zi = 2 * zr * zi + ci;
      zr = t; n++;
    }
    if (n >= MAXIT) return;                    // set interior — leaves no trail (the Buddhabrot condition)
    zr = 0; zi = 0;                            // start over, this time leaving the trail
    var iw = ov.width, ih = ov.height;
    for (var m = 0; m < n; m++) {
      // standard Buddhabrot view — the Mandelbrot rotated 90° into a vertically symmetric figure
      var px = (zi - RX0) / (RX1 - RX0) * iw;  // imaginary axis horizontal (rotation)
      var py = (zr - RY0) / (RY1 - RY0) * ih;  // real axis vertical
      var ix = px | 0, iy = py | 0;
      if (ix >= 0 && ix < iw && iy >= 0 && iy < ih) {
        acc[iy * iw + ix] += 1;                // visit weight — a longer orbit leaves more light
      }
      var t2 = zr * zr - zi * zi + cr;
      zi = 2 * zr * zi + ci;
      zr = t2;
    }

06Burning Ship — one absolute value, a different creature

One absolute value wrapped around both parts before squaring — z ← |z|² + c — and the cardioid vanishes: a ship spewing flames fills the frame at 45.881% cumulative area and a 44.984% peak, both the largest of the nine, at 35.9 intensity. The viewport frames the hull at center −1.755, −0.04 with span 1.35, and a breathing micro-zoom of ±4.5% keeps all 128 iterations alive every frame.

Burning Ship variant — one absolute value turns the set into a ship ablaze
Open in a new tab

06 · Burning Ship variant — one absolute value turns the set into a ship ablaze — Measured area 45.881% · moving frames 23/23Original by GODRICH · SCSS and React ship in the zip · See the password

createImageDataMath.absrequestAnimationFrame

The one-line formula change

        while (n < MAXIT && zr * zr + zi * zi <= 4) {
          // ── the Burning Ship's one line — absolute values before squaring ──
          var ar = Math.abs(zr), ai = Math.abs(zi);
          if (POWER === 2) {
            var t = ar * ar - ai * ai + cr;
            zi = 2 * ar * ai + ci;
          } else {
            // cubic variant — (|z|)³ by de Moivre's triple angle
            var t = ar * ar * ar - 3 * ar * ai * ai + cr;
            zi = 3 * ar * ar * ai - ai * ai * ai + ci;
          }
          zr = t;
          n++;
        }

07Pointer zoom and pan — wheel and drag exploring

The wheel pulls the point under the pointer toward screen center, dragging pans, and the iteration ceiling scales with the logarithm of the magnification from 96 up to 400 — the control layer every mandelbrot zoom website is built on, measured at 41.972% painted, 36.078% peak, and 87.3 average intensity, the brightest of the nine. A deterministic 4-act preview replays zoom, pan, deeper zoom, and return on the same 2-second clock alongside the manual controls.

Pointer zoom and pan — an explorer sucked into the point you click
Open in a new tab

07 · Pointer zoom and pan — an explorer sucked into the point you click — Measured area 41.972% · moving frames 23/23Original by GODRICH · SCSS and React ship in the zip · See the password

pointerdownpointermovewheel

The point under the pointer stays put

  function zoomAt(px_, py_, factor) {
    // correct the center so the complex point under (px_, py_) stays on screen after the zoom
    var p = toPlane(px_, py_);
    view.scale /= factor;
    view.cx = p.x + (view.cx - p.x) / factor;
    view.cy = p.y + (view.cy - p.y) / factor;
  }
  function panBy(dxPx, dyPx) {
    var dx = view.scale / W;
    view.cx -= dxPx * dx;
    view.cy += dyPx * dx;
  }

08Smooth iteration count — erasing the staircase bands

Subtracting a double logarithm of the magnitude at escape — ν = n + 1 − log₂(log|z|) — turns the integer staircase into a continuous gradient, shown against the banded original side by side, both at 150px per panel over a 64-iteration palette period: 31.206% painted, 28.339% peak, 25.7 intensity. The viewport is the coastline between cardioid and bulb at center −0.65, −0.42 with span 0.55, where the bands are loudest.

Smooth iteration count — erasing bands into flowing gradients
Open in a new tab

08 · Smooth iteration count — erasing bands into flowing gradients — Measured area 31.206% · moving frames 23/23Original by GODRICH · SCSS and React ship in the zip · See the password

createImageDataMath.logcreateLinearGradient

One formula subtracts the staircase

        while (n < MAXIT && (zz = zr * zr + zi * zi) <= 4) {
          var t = zr * zr - zi * zi + cr;
          zi = 2 * zr * zi + ci;
          zr = t; n++;
        }
        var c;
        if (n >= MAXIT) {
          c = C_SET;
        } else {
          var nu;
          if (smooth) {
            // ── the smooth formula — a double log of |z| right after escape ──
            var mod = Math.sqrt(zz);
            nu = n + 1 - Math.log(Math.log(mod)) / Math.LN2;
          } else {
            nu = n;                       // the original — the cause of the staircase
          }
          c = palAt((nu / PERIOD) * 4 + phase);
        }

09Fractal hero scene — a front door over a diving backdrop

The item-02 dive slowed to x3 per 2 seconds becomes a backdrop, and over it a radial vignette breathes from alpha 0.42 to 0.62 on the same 2-second period, carrying typography and a pulsing star — a breathing-guidance hero at 27.910% painted, a 5.805% peak (expected for a slow background dive under a darkening veil), and 48.7 intensity. The iteration ceiling rises from 90 to 260 as the dive deepens.

Fractal hero scene — a front-door stage layered with type and starlight
Open in a new tab

09 · Fractal hero scene — a front-door stage layered with type and starlight — Measured area 27.91% · moving frames 23/23Original by GODRICH · SCSS and React ship in the zip · See the password

createRadialGradientcreateImageDatarequestAnimationFrame

A vignette that breathes on the same clock

  function draw(t) {
    var k = Math.pow(ZOOM2S, t);
    var cx = -0.75 + (TX + 0.75) * t;
    var cy = TY * t;
    var maxit = Math.round(MAXIT0 + (MAXIT1 - MAXIT0) * t);
    var ctx = cv.getContext('2d');
    ctx.drawImage(renderFractal(cx, cy, 2.9 / k, maxit), 0, 0, W, H);
    // radial vignette — transparent at the center, ink toward the edges; depth rides the breath
    var breath = 0.5 + 0.5 * Math.sin(t * Math.PI * 2);      // 0~1 — one breath every 2 seconds
    var vg = ctx.createRadialGradient(W / 2, H / 2, Math.min(W, H) * 0.18, W / 2, H / 2, Math.max(W, H) * 0.72);
    vg.addColorStop(0, 'rgba(' + C_INK[0] + ',' + C_INK[1] + ',' + C_INK[2] + ',0)');
    vg.addColorStop(1, 'rgba(' + C_INK[0] + ',' + C_INK[1] + ',' + C_INK[2] + ',' + (0.42 + breath * 0.20).toFixed(3) + ')');
    ctx.fillStyle = vg;
    ctx.fillRect(0, 0, W, H);
  }

A fixed iteration ceiling is where the deep zoom breaks

Full-resolution loops collapse the frame rate

The stage is 480×300 and a single pixel can demand up to five hundred complex iterations, so looping at stage resolution means tens of millions of multiplications per frame. Every item instead renders into an internal canvas between 150 and 260 pixels wide and lets CSS scale it up — under a third of the pixel count — and that alone is what holds these panels at 60fps.

A square root inside the divergence test

The escape check compares zr² + zi² against 4, which is the radius-2 test squared, and needs no root. Calling Math.sqrt inside the loop adds millions of calls that decide nothing, and only item 08 wants the magnitude at all — once, after the loop has already finished.

Scaling the canvas is not zooming

A CSS transform stretch enlarges the same pixels; new mini-mandelbrots appear only when the span shrinks and the complex plane is remapped and recomputed. A real zoom divides the span, corrects the center so the target holds still, and pays the full pixel-loop price again — exactly what zoomAt in item 07 does with its divide-and-recenter arithmetic.

A fixed ceiling starves the deep zoom

Hold the ceiling at 96 while magnifying and the boundary's fine structure goes dark — points that escape at step 300 get painted as interior, outlines disconnect, and the dive looks like it is dissolving rather than deepening. The logarithmic ceiling raise in items 02 and 07 is not polish; without it, the deep zoom is simply wrong.

Dropping these nine into a real page — where each one lands

A landing page that needs a full-bleed intro reaches for item 09 first — the hero scene already frames typography and a starfield over the dive, so it drops in as a background section with no extra CSS. A portfolio or a lab page built for visitors who like to poke at things does better with item 07 instead of the passive item 02 deep zoom: the same divide-and-recenter math now answers a click, so the zoom doubles as a scroll hook.

A math or algorithms write-up is the case for item 03 — the pointer choosing c on one canvas rewrites the Julia set on the canvas beside it, so the relationship between a parameter and its picture reads off the screen without another paragraph of explanation. All three ship as one self-contained file, so none of them need a build step to sit inside an iframe or a static section.

Why does every zoom level reveal new shapes? — escape time and self-similarity

The escape count is a clock, not a color

Far from the set, orbits blow past the radius in a handful of steps; near the boundary, the count climbs toward the ceiling; inside, it never answers at all. The count therefore measures how long a point resists escaping — a distance told in time instead of in space — and the palette only makes that number visible. This is why the bands crowd together near the edge: the count changes fastest exactly where membership itself can no longer be decided at any finite ceiling.

Mini-mandelbrots are not copies

Zooming reveals small versions of the whole set, but each hangs from the boundary by its own filament — antennae, spirals, seahorse tails — and no two neighborhoods repeat exactly. Self-similarity here means structure at every scale, not repetition of the same picture, which is why the dive in item 02 can cycle forever and still surprise: the copy at depth 10⁻⁶ sits on a different decoration than the copy at 10⁻³. The inexhaustibility is a property of this one iteration, not a rendering trick.

The ceiling is the resolution of depth

MAXIT decides the finest structure a render can resolve: magnify without raising the ceiling and the boundary smears into interior color, because a point needing six hundred steps to escape gets declared non-escaping at five hundred. Items 02, 07, and 09 tie the ceiling to the magnification — 96 to 500 on the dive, 96 to 400 on the explorer, 90 to 260 on the hero — and the cost is honest, since iteration depth is precisely where the computation lives.

The same family as every computed field on this site

Every scene here recomputes a full field from scratch each frame — per pixel, per orbit, per visit — the same premise as a plate where stripes and spots grow out of two spreading chemicals and a galaxy pair tearing tidal tails out of each other. What differs is what the field stores: chemical concentrations, gravitational acceleration, or the escape count of one complex iteration. A fractal generator is the purest member of the family because the field and the picture are the same thing.

One constant swapped three ways, with reduced-motion built in

One constant, three different worlds

Name Change Feel
Iteration ceiling item 02 loop end 500 → 2,000 outlines reconnect at depth and the dive visibly slows
Palette period item 08 PERIOD 64 → 16 colors cycle densely and the cardioid's interior sharpens
The formula item 06 z = |z|² + c → z = z³ + c symmetry turns 2-fold to 3-fold — a different creature

Reduced motion still shows a finished frame

Every item checks prefers-reduced-motion before its loop starts, and for those visitors the loop never runs: one frame renders at a fixed phase — item 01 freezes at 0.6, item 09 at 0.4 — and the canvas gains an is-still class, so the still page shows a complete cardioid, a complete ship, a complete hero rather than a blank panel.

Two panels that want a pointer, seven that run alone

Items 03 and 07 respond to pointer input — 03 reads pointermove over the Mandelbrot canvas to pick c, 07 takes wheel, drag, and pointermove — and 07's wheel handler calls preventDefault so the page never scrolls under a zoom — while the other seven run on requestAnimationFrame alone, and every canvas host carries an aria-label and a tabindex so the whole grid is reachable from the keyboard. Every snippet above is the exact source running the panel over it, and m5rczssw opens the same nine files with their HTML and CSS inside this page's zip.

Questions before you paste this mandelbrot zoom code

Can one panel become a full mandelbrot set explorer

Yes — item 07 is that layer. Wheel zoom at a factor of 1.18 toward the point under the pointer, drag to pan, an iteration ceiling that rises to 400 with depth, and a live magnification readout; the scripted preview walks the same view state those public zoomAt and panBy calls edit, so the automatic and manual paths describe one explorer rather than two.

Why does the Julia item paint the smallest area of the nine

Because its screen is two canvases, not one. Both the Mandelbrot and the Julia render simultaneously at 150px internal resolution each, so the pixels that change color per frame sit well under half the stage — 12.885% cumulative against 45.881% for the Burning Ship. The number is the honest price of watching the two sets connect live, and removing a canvas to inflate it would delete the item's entire point.

How deep can the dive actually go

The loops here cycle x25 and x3 per 2 seconds with ceilings of 500 and 260, which is hero-scene depth. The escape-time method itself continues to any magnification, but a 64-bit double carries roughly 15 to 16 significant decimal digits, so past about 10¹⁴ the pixel-to-pixel step in cr falls below the smallest representable increment and adjacent pixels collapse to identical values — the practical floor for zooming without extended-precision arithmetic.

Sources: MDN — CanvasRenderingContext2D · MDN — ImageData · MDN — Window.requestAnimationFrame() · MDN — prefers-reduced-motion · Wikipedia — Mandelbrot set · Wikipedia — Escape-time algorithm · Wikipedia — Julia set

Enter the archive password

The password is inside this article. You will find it as you read.