One photo becomes a scene that hides a second, corrupted truth. This is a free, open workflow — generate your own images with any AI tool, drop in a zero-dependency script, and ship it. Everything below explains the trick; you're touching it right now.
There is no shader magic in the reveal itself. It's two images of the same scene, generated to be pixel-aligned, stacked on top of each other. A CSS mask-image radial gradient cuts a soft hole in the top one, and JavaScript lerps that hole toward your cursor. Alignment is the entire illusion — drag the handle and watch every object hold its position while the world changes.
Layer 2 is generated from layer 1 (image-to-image, strength ~0.6) with a prompt that opens with a "layout law": the output must be pixel-identical in composition. Every ordinary object was seeded into layer 1 on purpose, so it has a secret to become — notes turn to ciphertext, windows grow targeting brackets, the router becomes blinking data nodes.
Everything above the base is compositor-cheap garnish: only opacity, transform, clip-path and the mask variables ever animate, so the whole thing runs smoothly on a phone. Hover the legend.
Explicit z-index on every layer is not optional — see bug 01 below.
The rule that holds the whole thing together: each derived asset is generated from the locked previous stage, never from the original photo. Regenerate a stage and everything downstream must be regenerated too — so iterate each stage until you love it before spending a single run on the next.
One portrait. Passed only as the identity reference for stage 1 — nothing downstream ever sees it again.
Text-to-image, ultra-wide lens, chiaroscuro in your site's palette — and deliberately seeded with "secret" objects. Demand grime: AI defaults to showroom polish.
Image-to-image from the locked scene + a style reference. Prompt opens with the layout law; bans glow; describes terminal structure, never skulls.
Re-dither the broadcast for shimmer frames (or animate it with video gen). Feed the scene to a depth estimator for the parallax map.
A static glitch image reads as a filter. The fix costs two extra generations: ask the model to regenerate the broadcast "as the next frame of the same transmission" — identical composition, re-rolled dot pattern, new tear positions. Flipping opacity between the frames makes the dots crawl like a CRT re-dithering. This demo is doing it right now, from the portrait run of this workflow.
Verify alignment before accepting a variant: blend it with the base at 50% opacity — you should see one clean image with only the tears ghosted. Double edges mean it drifted; re-roll. In the demo: slow flicks while idle, rapid ~60ms flips during glitch bursts, and the terminal text "scrolls" between frames so the flicker reads as a live session.
Feed the scene to a free depth estimator (Depth Anything, MiDaS), get a grayscale map — bright is near. A ~60-line WebGL shader offsets the photo's UVs by depth × cursor offset, and the subject genuinely shifts against the background. Move your cursor over this portrait.
float d = texture2D(uD, v).r; // depth
float gx = dD/dx, gy = dD/dy; // depth gradient
float edge = clamp(1. - 5.*length(vec2(gx,gy)), 0., 1.);
vec2 off = uO * (d - .5) * edge; // near moves more…
gl_FragColor = texture2D(uT, v + off); // …silhouettes stay pinned
The edge term is the difference between magic and mess: without it, depth boundaries smear background pixels across silhouettes in ugly streaks. It zeroes the offset wherever the depth gradient is steep.
An accelerated WebGL canvas can paint above later masked siblings in Chromium, regardless of DOM order. Symptom: "the reveal stopped working" while the ring and particles still draw — it's rendering underneath the canvas. Fix: explicit z-index on every layer.
If your resize handler touches GL state, the initial resize() call must run after the let gl declarations execute — hoisted functions can reach a let before it initializes. Symptom: Cannot access 'gl' before initialization and a dead page.
A WebGL canvas with alpha:false composites as solid black before anything is drawn — covering your fallback image, permanently if the shader fails. Keep it display:none until textures are loaded and the first frame is safe.
Wrap the GL draw in try/catch inside your rAF. A lost context should quietly degrade the parallax — not freeze the lens, the particles, the HUD, and everything else living in the same loop.
Ordered by wow-per-effort. Stop wherever the ratio stops making sense — step 3 alone is already a hero people will screenshot.