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

Magic Eye Stereograms: 9 Depth Map Demos

Read each pixel's brightness as depth, shift it a few pixels along the row, and a solid shape hides inside flat noise.

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

From a Hemisphere Formula to Pointer Sculpting: Widening Magic Eye Depth

The nine scenes share one pipeline and differ only in what feeds it. 01 is the pipeline itself, bare: a breathing disc drives the depth and a circle becomes a ball. 02 swaps the noise for a repeating tile and makes the disparity visible as seam misalignment. 03 opens the big door — stamp any image on an offscreen canvas, read its luminance, and that luminance is the depth map. 04 pushes that door onto terrain synthesized from gradients, 05 turns depth into a landscape you travel through by wheel and drag, 06 answers the people who cannot see stereograms at all, 07 hands the depth map to your pointer as clay, 08 runs a time-driven wave through it, and 09 assembles a wordmark, a slow wave, and a vignette into a front door. Every scene here moved on 23 of 24 sampled frames, and painted area sits above 31% on every item — a magic eye is a screen where noise blankets the whole field, so the items separate on peak frame (how much re-weaving happens within one frame) and on brightness. Item 02 shines brightest at 155.2 average intensity; 06 hits the highest peak frame at 52.87%.

# Item Technique Painted area Peak frame Avg intensity
01 Random-dot basics breathing disc, MAXSHIFT 10px, 2s cycle 31.427% 20.921% 69.7
02 Pattern tiling TILE 100px, steps 4/8/12/16px 52.806% 12.343% 155.2
03 Hiding text fillText SOLID 220px, depth 14px 31.934% 19.192% 85.8
04 Photo relief synthesized photo, brightness as height 50.087% 31.937% 71.8
05 Scroll landscape 3 sine octaves 0.45/0.30/0.18, cruise 90px/s 51.918% 36.326% 71.8
06 How-to-see guide guide dots parting to 100px, solution map 52.87% 52.87% 66.0
07 Pointer sculpting brush 26px, lift +0.8, press -0.5, 6s decay 34.71% 13.015% 76.0
08 Living bulge diagonal sine, wavelength 130px, amplitude 12px 31.176% 22.983% 71.1
09 Logo hero scene GODRICH 96px, wave 150px, vignette 18% 52.539% 31.517% 69.9

Code per item

Every fence below is copied from that item's own source file — same constants, same identifier names — with comments and on-screen phrases rendered in English. One function arrives split across two consecutive fences, the 06 guide cuts off where the solution overlay begins, and the zip carries the nine full files plus a React port of each.

01Random-dot basics — a sphere silently surfacing on the first screen

A hemispheric disc swells and settles on a 2s cycle while the copy distance follows its depth — 31.427% painted area, a 20.921% peak frame, 69.7 average intensity. The grain is a coordinate-only hash, one sample per pixel, five colors.

Random-dot basics — a sphere silently surfacing on the first screen
Open in a new tab

01 · Random-dot basics — a sphere silently surfacing on the first screen — Measured area 31.427% · moving frames 23/23Original by GODRICH · SCSS and React ship in the zip · See the password

ImageDataUint8ClampedArrayputImageData

The hash that must not remember time

  function baseIdx(x, y) {
    var h = (Math.imul(x + 11, 374761393) + Math.imul(y + 7, 668265263)) >>> 0;
    h ^= h >>> 13;
    h = Math.imul(h, 1274126177) >>> 0;
    h ^= h >>> 16;
    return ((h % PAL.length) + PAL.length) % PAL.length;   // ^ folds to ToInt32, so wrap negatives back in
  }

The disc, breathing inside its own cycle

  function depthAt(x, y, t) {
    var ph = t / CYCLE * Math.PI * 2;
    var cx = W * 0.5, cy = H * 0.5 + Math.sin(ph) * H * 0.07;
    var rad = Math.min(W, H) * 0.74;
    var dx = (x - cx) / rad, dy = (y - cy) / rad;
    var d2 = dx * dx + dy * dy;
    if (d2 >= 1) return 0;
    var bulge = 0.16 + 0.84 * (Math.sin(ph - Math.PI / 2) * .5 + .5);
    return Math.sqrt(1 - d2) * bulge;   // hemisphere cross-section × breathing
  }

The per-row copy shift, whole

  function draw(t) {
    var d = img.data, x, y, di;
    var col = new Int32Array(W);   // this row's palette indices
    for (y = 0; y < H; y++) {
      for (x = 0; x < W; x++) {
        var s = Math.round(depthAt(x, y, t) * MAXSHIFT);
        if (s <= 0) {
          col[x] = baseIdx(x, y);                    // background — base grain untouched
        } else {
          var from = x - s;
          col[x] = from >= 0 ? col[from] : baseIdx(from + W, y);  // copy from the left (wrap past the edge)
        }
      }
      for (x = 0; x < W; x++) {
        var c = PAL[col[x]];
        di = (y * W + x) * 4;
        d[di] = c[0]; d[di + 1] = c[1]; d[di + 2] = c[2]; d[di + 3] = 255;
      }
    }
    ctx.putImageData(img, 0, 0);
  }

02Pattern tiling — the repeat width is the angle your eyes converge at

Trapezoid-stripe tiles 100px wide replace the noise, and the depth is a 4-step staircase (4, 8, 12, 16px) whose bands sweep on a 2s cycle while the tile itself streams upward at 28px/s — the brightest screen of the set at 155.2 average intensity, 52.806% painted, a 12.343% peak.

Pattern tiling — the repeat width is the angle your eyes converge at
Open in a new tab

02 · Pattern tiling — the repeat width is the angle your eyes converge at — Measured area 52.806% · moving frames 23/23Original by GODRICH · SCSS and React ship in the zip · See the password

ImageDatarequestAnimationFrame

A tile pattern infinite in both directions

  function tileColor(px, py) {
    var tx = ((px % TILE) + TILE) % TILE, ty = ((py % TILE) + TILE) % TILE;
    var diag = (tx + ty) % TILE;                       // diagonal coordinate
    if (diag < 14 || diag > TILE - 14) return C2;      // thick diagonal band
    var dx = tx - TILE * 0.5, dy = ty - TILE * 0.5;
    var rr = Math.sqrt(dx * dx + dy * dy);
    if (rr < 9) return C2;                             // center dot
    if (rr < 15) return C0;                            // dot rim
    return (diag % 50 < 4) ? C1 : C0;                  // field + thin secondary line
  }

The staircase, and why seams lock or stand proud

  function depthAt(x, y, t) {
    var band = (y * 4 + Math.floor(x / (W / 4)));      // diagonal staircase index
    var scan = (t / CYCLE) % 1;                        // 0..1 sweep phase
    var appear = ((band % 8) + 8) % 8 / 8;             // stagger each band's entrance time
    var lift = Math.max(0, Math.sin((scan - appear) * Math.PI * 2 - Math.PI / 2) * .5 + .5);
    return lift * lift;
  }

  function draw(t) {
    var d = img.data, x, y, di;
    var flowY = Math.floor(t * 28);   // pattern drift — the tile streams down (integer pixels)
    for (y = 0; y < H; y++) {
      var rowY = y + flowY;
      for (x = 0; x < W; x++) {
        var s = shiftFor(x, rowY, t);
        var src = x - s;
        // the tile grid repeats forever, so negatives pass straight through — tileColor wraps them
        var c = tileColor(src, rowY);
        di = (y * W + x) * 4;
        d[di] = c[0]; d[di + 1] = c[1]; d[di + 2] = c[2]; d[di + 3] = 255;
      }
    }
    ctx.putImageData(img, 0, 0);
  }

03Hiding text — fillText stamped straight into the depth map

The word SOLID goes down in 220px black-weight type on an offscreen canvas, getImageData reads it back, and the luminance becomes depth — 31.934% painted, a 19.192% peak, 85.8 average intensity.

Hiding text — fillText stamped straight into the depth map
Open in a new tab

03 · Hiding text — fillText stamped straight into the depth map — Measured area 31.934% · moving frames 23/23Original by GODRICH · SCSS and React ship in the zip · See the password

fillTextgetImageDataImageData

The word stamped, and luminance read back

    srcCtx.fillStyle = '#fff';
    srcCtx.textAlign = 'center';
    srcCtx.textBaseline = 'middle';
    var fs = Math.min(FSIZE, W * 0.46);
    srcCtx.font = '900 ' + fs + 'px "Arial Black", "Segoe UI", sans-serif';
    srcCtx.fillText(WORD, W * 0.5, H * 0.58);
    var sd = srcCtx.getImageData(0, 0, W, H).data;

Luminance to depth, with the edges softened

    // luminance → depth. Averaging with the 1px neighbors blunts the edge drop (no visible kink)
    depthBuf = new Float32Array(W * H);
    var x, y, v, sum, n;
    for (y = 0; y < H; y++) {
      for (x = 0; x < W; x++) {
        v = sd[(y * W + x) * 4] / 255;
        sum = v; n = 1;
        if (x > 0)     { sum += sd[(y * W + x - 1) * 4] / 255; n++; }
        if (x < W - 1) { sum += sd[(y * W + x + 1) * 4] / 255; n++; }
        depthBuf[y * W + x] = sum / n;
      }
    }

The same row shift, driven by a picture

  function draw(t) {
    var d = img.data, x, y, di;
    var ph = t / CYCLE * Math.PI * 2;
    var appear = Math.sin(ph - Math.PI / 2) * .5 + .5;
    var amp = 0.12 + 0.88 * appear;   // hold a 12% floor — the loop stays connected
    var bob = Math.round(Math.sin(ph) * H * 0.09);   // the word bobs — per-row depth drifts
    var col = new Int32Array(W);
    for (y = 0; y < H; y++) {
      var sy = y - bob;
      var rowOff = (sy >= 0 && sy < H ? sy : y) * W;
      for (x = 0; x < W; x++) {
        var s = Math.round(depthBuf[rowOff + x] * amp * MAXSHIFT);
        if (s <= 0) {
          col[x] = baseIdx(x, y);
        } else {
          var from = x - s;
          col[x] = from >= 0 ? col[from] : baseIdx(from + W, y);
        }
      }

Emptying the row buffer into the frame

      for (x = 0; x < W; x++) {
        var c = PAL[col[x]];
        di = (y * W + x) * 4;
        d[di] = c[0]; d[di + 1] = c[1]; d[di + 2] = c[2]; d[di + 3] = 255;
      }
    }
    ctx.putImageData(img, 0, 0);
  }

04Photo relief — reading brightness as terrain height and lifting it

No external image: the photo is synthesized on canvas — a sky gradient, a light patch orbiting horizontally on a 2s period, three hills shaded toward the light — and the raster's brightness feeds the shift directly (50.087% painted, a 31.937% peak, 71.8 intensity).

Photo relief — reading brightness as terrain height and lifting it
Open in a new tab

04 · Photo relief — reading brightness as terrain height and lifting it — Measured area 50.087% · moving frames 23/23Original by GODRICH · SCSS and React ship in the zip · See the password

ImagegetImageDataImageData

Painting the photo the depth will come from

    // sky — vertical gradient (brightness falls top to bottom = terrain falling away behind)
    var sky = phCtx.createLinearGradient(0, 0, 0, H);
    sky.addColorStop(0, '#e8e2d4');
    sky.addColorStop(0.7, '#c9c0ae');
    sky.addColorStop(1, '#b3a894');
    phCtx.fillStyle = sky;
    phCtx.fillRect(0, 0, W, H);

    // light patch — the brightest point of the sky (reads as the nearest peak)
    var glow = phCtx.createRadialGradient(lx, ly, 2, lx, ly, W * 0.16);
    glow.addColorStop(0, '#ffffff');
    glow.addColorStop(1, 'rgba(255,255,255,0)');
    phCtx.fillStyle = glow;
    phCtx.fillRect(0, 0, W, H);

Brightness straight into the shift

  function draw(t) {
    paintPhoto(t);
    var ph = phCtx.getImageData(0, 0, W, H).data;
    var d = img.data, x, y, di;
    var col = new Int32Array(W);
    for (y = 0; y < H; y++) {
      var rowOff = y * W;
      for (x = 0; x < W; x++) {
        var lum = ph[(rowOff + x) * 4];   // brightness 0..255
        var s = Math.round(lum / 255 * MAXSHIFT);
        if (s <= 0) {
          col[x] = baseIdx(x, y);
        } else {
          var from = x - s;
          col[x] = from >= 0 ? col[from] : baseIdx(from + W, y);
        }
      }
      for (x = 0; x < W; x++) {
        var c = PAL[col[x]];
        di = (y * W + x) * 4;
        d[di] = c[0]; d[di + 1] = c[1]; d[di + 2] = c[2]; d[di + 3] = 255;
      }
    }
    ctx.putImageData(img, 0, 0);
  }

05Scroll landscape — mountain ranges unveiling as you roll

Terrain height is a moving ramp of three sines weighted 0.45, 0.30, and 0.18, sampled at x plus a travel offset — wheel (0.6x) and horizontal drag (1.2x) move you on top of an auto-cruise of 90px/s that runs every frame, so the loop never breaks (51.918% painted, a 36.326% peak, 71.8 intensity).

Scroll landscape — mountain ranges unveiling as you roll
Open in a new tab

05 · Scroll landscape — mountain ranges unveiling as you roll — Measured area 51.918% · moving frames 23/23Original by GODRICH · SCSS and React ship in the zip · See the password

scrollImageDatarequestAnimationFrame

Three octaves of ramp

  // terrain — a moving ramp of 3 octaves. wx = world coordinate (x plus the travel offset)
  function terrain(wx, y) {
    var v = 0;
    v += Math.sin(wx * 0.011 + y * 0.004) * 0.45;
    v += Math.sin(wx * 0.031 + 1.7 + y * 0.010) * 0.30;
    v += Math.sin(wx * 0.074 + 4.2) * 0.18;
    v += (Math.cos(wx * 0.005 + y * 0.006) * 0.5 + 0.5) * 0.35;
    var front = y / H;                       // the bottom of the screen (foreground) rises higher
    var h01 = Math.max(0, Math.min(1, v * 0.5 + 0.5)) * (0.35 + 0.65 * front * front);
    return h01;
  }

Wheel, drag, and the cruise that never stops

  var travel = 0, hover = false, dragX = null;
  fieldEl.addEventListener('wheel', function (ev) {
    ev.preventDefault();
    travel += (ev.deltaY + ev.deltaX) * 0.6;
    hover = true;
  }, { passive: false });
  fieldEl.addEventListener('pointerdown', function (ev) {
    dragX = ev.clientX; hover = true;
    ev.preventDefault();
  });
  window.addEventListener('pointermove', function (ev) {
    if (dragX === null) return;
    travel -= (ev.clientX - dragX) * 1.2;
    dragX = ev.clientX;
  });
  window.addEventListener('pointerup', function () { dragX = null; });

06How-to-see guide — guide dots and a self-solve overlay

The accessibility item: two dots part slowly to 100px — exactly the disparity your eyes must relax to — training the parallel gaze until they overlap, and a button overlays the depth map itself — a donut whose inner hole sits low at 0.22 — as a grayscale wash (52.87% painted and a 52.87% peak frame the instant the map lights, 66.0 intensity).

How-to-see guide — guide dots and a self-solve overlay
Open in a new tab

06 · How-to-see guide — guide dots and a self-solve overlay — Measured area 52.87% · moving frames 23/23Original by GODRICH · SCSS and React ship in the zip · See the password

requestAnimationFramefillRectMath.sin

A donut with a low hole reads as a donut

  function depthAt(x, y, t) {
    var ph = t / CYCLE * Math.PI * 2;
    var cy = H * 0.54 + Math.sin(ph) * H * 0.08;              // vertical bob
    var cx = W * 0.5;
    var rad = Math.min(W, H) * 0.36;
    var dx = (x - cx) / rad, dy = (y - cy) / rad;
    var d2 = dx * dx + dy * dy;
    if (d2 >= 1) return 0;
    var ring = Math.sqrt(d2);
    var hole = ring < 0.36 ? 0.22 : 1;   // the inner hole sits low — in stereo it reads as a donut
    var pulse = 0.55 + 0.45 * (Math.sin(ph - Math.PI / 2) * .5 + .5);   // depth pulsing
    return Math.sqrt(1 - d2 * 0.25) * 0.92 * hole * pulse;
  }

Dots at the disparity, drawn on the capture clock

    if (root.classList.contains('is-demo')) {
      // two cue dots — slowly parting, then meeting
      var open = Math.max(0, Math.sin(ph - Math.PI / 2) * .5 + .5);
      var px = open * SEP / 2;
      ctx.fillStyle = 'rgba(' + DOT[0] + ',' + DOT[1] + ',' + DOT[2] + ',0.92)';
      ctx.beginPath(); ctx.arc(W / 2 - px, 14, 3.5, 0, 6.2832); ctx.fill();
      ctx.beginPath(); ctx.arc(W / 2 + px, 14, 3.5, 0, 6.2832); ctx.fill();

      // solution map — during the back half of the cycle (1s) it is overlaid, then erased
      var litPhase = ph % (Math.PI * 2);
      if (litPhase > Math.PI) {

07Pointer sculpting — pressing depth down and pulling it up

A Float32Array sculpt buffer where left-drag lifts (+0.8) and right-drag presses (-0.5) with a 26px brush, and every trace soaks in over 6 seconds of exponential decay (34.71% painted, a 13.015% peak, 76.0 intensity).

Pointer sculpting — pressing depth down and pulling it up
Open in a new tab

07 · Pointer sculpting — pressing depth down and pulling it up — Measured area 34.71% · moving frames 23/23Original by GODRICH · SCSS and React ship in the zip · See the password

pointermoveImageDatarequestAnimationFrame

The brush, with a conic falloff

  function brush(x0, y0, x1, y1, amount, r) {
    r = r || BRUSH / 2;
    var dist = Math.hypot(x1 - x0, y1 - y0);
    var steps = Math.max(1, Math.ceil(dist / (r * 0.4)));
    for (var i = 0; i <= steps; i++) {
      var bx = x0 + (x1 - x0) * (i / steps);
      var by = y0 + (y1 - y0) * (i / steps);
      var x0i = Math.max(0, Math.floor(bx - r)), x1i = Math.min(W - 1, Math.ceil(bx + r));
      var y0i = Math.max(0, Math.floor(by - r)), y1i = Math.min(H - 1, Math.ceil(by + r));
      for (var yy = y0i; yy <= y1i; yy++) {
        for (var xx = x0i; xx <= x1i; xx++) {
          var dx = xx - bx, dy = yy - by;
          var dd = Math.sqrt(dx * dx + dy * dy);
          if (dd >= r) continue;
          var fall = 1 - dd / r;               // conic falloff — shallow at the rim
          var idx = yy * W + xx;
          var v = sculpt[idx] + amount * fall;
          sculpt[idx] = v < 0 ? 0 : (v > 1 ? 1 : v);
        }
      }
    }
  }

Six seconds of soak, and the two drags

    // transfer decay — traces soak in over 6 seconds
    var k = Math.exp(-dt / DECAY);
    for (var i = 0; i < sculpt.length; i++) {
      if (sculpt[i] !== 0) sculpt[i] *= k;
    }

  fieldEl.addEventListener('pointerdown', function (ev) {
    ev.preventDefault();
    root.classList.remove('is-demo');   // the first touch dismisses the five-hook sweep staging
    mode = ev.button === 2 ? -1 : 1;    // right button = press
    var p = localXY(ev); lastX = p[0]; lastY = p[1];
    fieldEl.setPointerCapture(ev.pointerId);
  });

08Living bulge — a surface that breathes through depth waves

A diagonal sine of 130px wavelength crosses a central dome; the crest produces the 12px maximum shift and the trough flattens to zero, so cells with and without shift alternate down each row — 31.176% painted, a 22.983% peak, 71.1 intensity.

Living bulge — a surface that breathes through depth waves
Open in a new tab

08 · Living bulge — a surface that breathes through depth waves — Measured area 31.176% · moving frames 23/23Original by GODRICH · SCSS and React ship in the zip · See the password

ImageDataMath.sinrequestAnimationFrame

Two twisted waves over one dome

  function depthAt(x, y, t) {
    var ph = t / CYCLE * Math.PI * 2;
    // base — central dome (zero at the screen edge). The center bobs vertically, slowly.
    var cx = W * 0.5, cy = H * 0.5 + Math.sin(ph) * H * 0.06;
    var rad = Math.min(W, H) * 0.74;
    var dx = (x - cx) / rad, dy = (y - cy) / rad;
    var d2 = dx * dx + dy * dy;
    if (d2 >= 1) return 0;
    var dome = Math.sqrt(1 - d2) * 0.60;
    // waves — a diagonal sine + an orthogonal one (the wavefront twists), wavelength WAVELEN
    var wpos = (x + y * 0.6) / WAVELEN * Math.PI * 2;
    var wpos2 = (x * 0.4 - y) / (WAVELEN * 1.4) * Math.PI * 2;
    var wave = Math.sin(wpos - ph) * 0.5 + 0.5;
    var wave2 = Math.sin(wpos2 - ph * 0.7 + 2.1) * 0.5 + 0.5;
    var v = dome * (0.28 + 0.44 * wave + 0.28 * wave2);
    // breathing — the whole amplitude swells and settles at half the cycle's phase
    var breathe = 0.68 + 0.32 * Math.sin(ph * 0.5);
    return v * breathe;
  }

09Logo hero scene — a front door hiding the brand in plain sight

The wordmark GODRICH is stamped at 96px bold, luminance becomes depth, a slow 150px wavelength makes the letters swell and settle on a 2s cycle, and an 18% vignette darkens the edges — 52.539% painted, a 31.517% peak, 69.9 intensity.

Logo hero scene — a front door hiding the brand in plain sight
Open in a new tab

09 · Logo hero scene — a front door hiding the brand in plain sight — Measured area 52.539% · moving frames 23/23Original by GODRICH · SCSS and React ship in the zip · See the password

createLinearGradientImageDatarequestAnimationFrame

The mark, the wave, and a distant floor

    if (!markMap) return 0;
    var ph = t / CYCLE * Math.PI * 2;
    var breathe = 0.55 + 0.45 * Math.sin(ph + 0.8);
    // the floor — a very shallow far terrain (1~2px) laid across the whole board. Like fog behind
    // the mark it breathes, slowly re-weaving the entire noise field (the mark alone is small).
    var fx = (x / W) * Math.PI * 2;
    var fw = 0.5 + 0.5 * Math.sin(fx * 1.5 - ph * 0.5);
    var fw2 = 0.5 + 0.5 * Math.sin(fx * 0.8 + y * 0.012 - ph * 0.34 + 2.0);
    var floorD = (0.085 * fw + 0.075 * fw2) * breathe;
    // vertical bob — slowly raising the mark (the per-row sample drifts)
    var bob = Math.round(Math.sin(ph) * H * 0.08);
    var sy = y - bob;
    if (sy < 0 || sy >= H) sy = y;
    var m = markMap[sy * W + x];
    if (!m || m <= 0) return floorD;
    // the slow wave — a sine crossing the mark diagonally swells and settles the letters
    var wpos = ((x + y * 0.5) / WAVELEN) * Math.PI * 2;
    var wave = Math.sin(wpos - ph) * 0.5 + 0.5;
    var wave2 = Math.sin(((x * 0.4 - y) / (WAVELEN * 1.3)) * Math.PI * 2 - ph * 0.6 + 1.4) * 0.5 + 0.5;
    var markD = m * (0.22 + 0.42 * wave + 0.36 * wave2) * breathe;
    return markD > floorD ? markD : floorD;

The vignette that finishes the scene

    // vignette 18% — darkened edges laid over the noise plate (the scene completes)
    var vg = ctx.createLinearGradient(0, 0, 0, H);
    vg.addColorStop(0, 'rgba(12, 10, 18, ' + (VIG * 0.9).toFixed(3) + ')');
    vg.addColorStop(0.28, 'rgba(12, 10, 18, 0)');
    vg.addColorStop(0.72, 'rgba(12, 10, 18, 0)');
    vg.addColorStop(1, 'rgba(12, 10, 18, ' + (VIG * 0.9).toFixed(3) + ')');
    ctx.fillStyle = vg;
    ctx.fillRect(0, 0, W, H);

Why a solid rises from flat noise — binocular disparity and the depth map

Your eyes measure depth by comparing two pictures

Hold a finger up and close one eye, then the other: the finger jumps sideways against the background. That jump is binocular disparity — each eye gets a slightly different horizontal picture, and the brain reads the difference as distance. Near things jump far, far things barely move. Every depth cue in these scenes rides that one mechanism.

The stereogram is the same trick run backwards

Instead of showing each eye a separate picture, a stereogram prints one picture whose rows contain repeated texture at a controlled spacing. Look through the page so your eyes converge past it, and one patch of grain lands on the left retina while its twin lands on the right — the brain, finding the two identical, assigns them a distance. The depth map's job is to decide that spacing pixel by pixel: at depth 0.8 with MAXSHIFT 10, the copy comes from 8 columns to the left, and that cell fuses as nearer than its neighbor copied from 2 columns. Nothing is drawn in 3D anywhere; the solid is an agreement between the pattern and your visual cortex.

Why the noise hash takes coordinates and nothing else

Fusion requires both eyes to find the same grain. Re-roll the noise each frame and the correspondence your eyes locked onto a moment ago is gone — the sculpture dissolves before it can appear. So baseIdx hashes x and y alone, never time: the same pixel holds the same grain on every frame, the memory survives, and only the shift amount changes. This is the one rule every one of the nine items inherits unchanged.

The same family as the other computed fields on this site

Each scene here computes a full field per pixel per frame — the same premise as a sphere that appears only where a marched ray stops, a butterfly orbit that never repeats its path, and a fractal that keeps refining its outline as the dive deepens. The stereogram is the quiet member of that family: the ray marcher and the fractal compute what color a pixel is, while this computes only how far left a pixel's twin should sit — and hands the rendering to the viewer.

Three variants, and one lands straight on reduced-motion accessibility

Three knobs to turn

Name Change Feel
Bulge depth item 01 disc MAXSHIFT 10 → 22 the ball leaps to your face and shows itself even to viewers who do not know the trick; push further and the pattern tears into double vision
Pattern width item 02 tile width 100 → 140px the wider the tile, the farther the eyes must relax and the stronger the strain; narrow it and viewing becomes easy
Wave speed item 08 sine period 2s → 1s the surface that breathed like a sleeper pulses like a heartbeat, the solid's outline changing faster

Reduced motion still shows the solid

Each scene checks prefers-reduced-motion before scheduling its loop, and when the preference is set the loop never starts: the scene draws one representative frame at a fixed phase, freezes, and the root takes an is-still class. The goal is that a still user still receives one solid picture, not a blank canvas.

Two pointer items, seven that run alone

Items 05 and 07 listen to pointer events over their fields — 05 to wheel and drag for travel, 07 to left-drag lift and right-drag press; the other seven run on requestAnimationFrame alone. Every canvas host carries an aria-label and a tabindex, keeping the boards keyboard-reachable, and 06 exists precisely for viewers who cannot fuse stereograms unaided.

A MAXSHIFT above the stable range is where this breaks

Nine boards funnel every failure people hit while building this into two shapes: pushing the shift past where correspondence holds, or feeding it a source that the shift math can't survive. Both keep rendering without a single error — the pattern just stops fusing into a solid.

A MAXSHIFT above the stable range tears the pattern

Correspondence with the neighboring grain collapses as the shift grows: at 18px or more the eyes find two candidate matches and the image splits into doubled ghosts. The stable range across this set is 10 to 16px — item 01 runs 10, item 03 pushes 14, item 02's top staircase step sits at 16 — and 22px belongs to the variant scene only, where the tearing is the lesson.

Vertical shift of any size kills fusion

Stereogram disparity is horizontal only. Each row must stay an independent left-to-right one-way copy; the moment a cell pulls from the row above or below, the two eyes receive vertically mismatched grain and the solid never forms. Even the bob in items 03 and 09 works by re-sampling which row the depth comes from, never by copying pixels across rows.

Thin letterforms dissolve after the shift

If the strokes are 1 or 2px wide in the offscreen raster, the shift smears them into noise — which is why 03 stamps SOLID at up to 220px in a 900-weight face and 09 sets GODRICH at 96px bold. Short bold words surface; long thin paragraphs never do.

Item 02's staircase must stay a multiple of the tile's rhythm

Seams lock only where the discrete shift lands on the pattern's own period; step the staircase arbitrarily and the misalignment bands turn to mush. The 4/8/12/16px steps against the 100px tile keep every band readable as texture — the 16px step included, which sits at the top of the 10~16px comfort band.

Re-rolled grain and CSS-animated guides

Swap baseIdx for Math.random and every frame redraws differently — the eyes lose the pattern and nothing ever surfaces. And item 06's parting dots are drawn inside the rAF loop for the same reason a CSS animation would fail it: an animation drifting against the capture clock desynchronizes from the depth map underneath, so the guide would lie about where the fusion sits. Treat this set as a small stereogram generator — nine magic eye pictures you can retype from the fences — and keep its one law: same grain, different shift. The zip of nine source folders plus the React ports behind this post opens with 9kzb6vjj and carries every scene in full.

Where this actually gets used — a brand hero and a product backdrop

Item 09 hides a wordmark inside repeating grain, so it drops straight into a landing page's first screen: the letters stay invisible until a visitor's eyes converge, which holds attention for the few seconds fusion takes without a single line of instructional copy.

Item 04 turns a photo's brightness into a depth relief, which suits a product page or an about-page backdrop where the picture should read as texture rather than a literal photograph. Item 05 unveils a mountain range as the page scrolls, which fits neatly into a mid-page transition on a scrollytelling layout.

All three only need a different depth map underneath — the noise wash and the shift loop stay exactly as written.

Questions before you paste this magic eye stereogram code

How do I view a magic eye picture if I have never managed to see one

Start with item 06: two dots float at the top, slowly parting to exactly the disparity distance. Relax your eyes as if looking through the screen at something behind it until the two dots overlap, then hold that gaze and drop it to the board below — the donut rises. If it still fails, press the solution-map button to see the depth map itself, then try again knowing what hides there. Most people land their first fusion within ten seconds of the dots.

Does this stereogram generator need a library, WebGL, or image files

No. Each item is one self-contained HTML file — a plain canvas 2D context, a hand-rolled integer hash, getImageData reads, and putImageData writes; no CDN, no WebGL, and no external image (even item 04's photo is synthesized on canvas in the same file). Paste it into any page and it runs as-is, and the zip adds a React port of every scene for component-tree projects.

Why does the solid wobble or tear when I raise the shift

Because disparity has a comfort band. Up to about 16px the brain finds the correspondence quickly; past 18px, neighboring grain starts to offer competing matches and the image doubles. The variant that lifts item 01 to 22px stands right at that boundary deliberately — it shows the mechanism by breaking it. If a custom scene tears, lower the shift before touching anything else.

Sources: MDN — ImageData · MDN — CanvasRenderingContext2D.putImageData() · MDN — prefers-reduced-motion · Wikipedia — Autostereogram · Wikipedia — Stereopsis

Enter the archive password

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