Build note · Generative Assets
SHADOWPLAY is a fictional wayang kulit troupe that hands the lamp to the audience. The page has one job: let you carry the only light and watch the paper figures throw long, angle-correct shadows that lengthen and swing as you move it. Every shadow is projected live from your cursor — nothing is filmed, filtered, or pre-baked.
In a real wayang theatre a single oil lamp — the blencong — hangs above the puppeteer and throws the puppets' shadows onto a stretched cotton screen, the kelir. This page gives that lamp to you. The stage sits still and faithful until your hand moves; then one light travels with your cursor and the whole cast responds together — shadows leaning away from you like spokes from a hub, the unmistakable signature of a single point source.
The brief for this wave is second person: the page is inert until the viewer drives it. Move away and the lamp eases back overhead, the shadows gather under their figures, and the theatre goes quiet.
Argued entirely from a lamp behind cloth: a warm lit screen, near-black shade, teak hardware, and one flame accent reserved for light and line — never for small text, which takes a dark burnt-sienna ink so it always clears 4.5:1 on the linen.
Playfair Display is the theatre poster — a high-contrast Didone with the drama a shadow play wants, set huge and italic for the wordmark. Work Sans handles every functional register (labels, body, the readout) in a calm humanist grotesque, so the serif never has to do a job it is bad at.
The one act of typographic bravery: the hero wordmark casts its own raking shadow, generated from the same lamp as the puppets — a stack of eight text-shadow steps whose direction and reach update as you move your cursor. The title belongs to the same light as the stage.
Every figure is one SVG silhouette, drawn twice: once as the lit puppet (teak), once behind it as its shadow (shade). Both instances <use> a single <symbol>, so the cut holes — the tooled tatah perforations — line up exactly, and the shadow shows light through them just like real hide.
The shadow is not a blur or a drop-shadow filter. It is a genuine homothety about the lamp. For a lamp at height Λ and a figure on layer height h, the projection scale is t = Λ / (Λ − h); the shadow copy is scaled by t and translated by (t − 1) · (figureCorner − lamp), so every point lands exactly where a straight line from the lamp through the figure meets the screen. The lamp's height Λ is your cursor's vertical position: near the top it is high and shadows hug their figures; near the footlights it is low, Λ − h shrinks, and shadows stretch toward the wings. Longer throws also blur and fade, a cheap honest penumbra.
On every pointermove the code re-reads the lamp, recomputes each t, and writes a fresh translate + scale onto the shadow — one transform per figure, per frame, eased with a small rAF lerp. Grab a puppet and the lamp freezes so you can watch the throw while you place it; release and the light returns to your hand. Under reduced-motion the flame stops guttering and the easing is dropped, but your lamp still commands the shadows — the interaction is the content, so calming it to a dead page would defeat the room.
#8A4B18 after checking eyebrows against the linen — the raw #E4913A measured ~1.9:1 and failed; the burnt-sienna clears ~6:1.#1E1A24 so it reads as backlit hide, not a black sticker, and tuned the resting opacity so overlapping shadows do not muddy into one slab..reveal so the load choreography staggers cleanly instead of popping.document.hidden and when the lamp settles, and confirmed reduced-motion drops all autonomous motion while keeping the pointer-driven shadows alive.