Shadowplay

Build note · Generative Assets

How the theatre
was lit

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.

01 Concept

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.

02 Palette

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.

Screen linen
#EFE6D2
The lit kelir. Warm, slightly uneven cotton — the page's ground and the surface every shadow lands on.
Shadow
#1E1A24
Cast shade, faintly violet-black like real backlit hide. Never pure black — that would read as a sticker, not a shadow.
Teak
#6B4A2E
The proscenium frame, the control rods (gapit), and the lit face of every puppet. The theatre's woodwork.
Flame
#E4913A
The lamp, its halo, and hairline rules. Accent for light only; its text sibling is burnt-sienna #8A4B18.

03 Type

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.

Shadowplay
Playfair Display 900 · italic 700 — display & headings
Work Sans 400–700 — labels, body, controls, the diagram. Uppercase and letterspaced for eyebrows; the flame accent is demoted to a darker ink whenever it carries words.

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.

04 The signature — projected shadows

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.

05 Iteration log

Pass 1 · Craft

Spacing, contrast, and honest shade

  • Split the flame accent into a light token (lamp, rules) and a text ink #8A4B18 after checking eyebrows against the linen — the raw #E4913A measured ~1.9:1 and failed; the burnt-sienna clears ~6:1.
  • Set the shadow off pure black to #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.
  • Tightened the section rhythm to a single clamp-based scale and aligned every .reveal so the load choreography staggers cleanly instead of popping.
Pass 2 · Depth

Making the light do more than one thing

  • Tied the hero wordmark's shadow to the same lamp — a second-read moment most people notice only on the return visit, when they realise the title moved with their cursor the whole time.
  • Added the perforation glow: when the lamp comes close, each figure's tatah holes warm toward flame, as if the light were shining through the hide.
  • Planted the easter egg — drag the Kayon to centre-stage and the old dalang's stage direction, tancep kayon (“the tree is planted; the scene rests”), whispers in.
  • Coupled shadow blur and opacity to throw length so long shadows soften and fade — a free, physically-true penumbra.
Pass 3 · Hardening

375px, keyboard, and the quiet room

  • Verified the layout at 375 / 768 / 1440: the stage keeps its 16∶9 frame, the cast-row role text drops below the name, and nothing crosses the viewport edge.
  • Gave the stage arrow-key control of the lamp so keyboard-only visitors can still drive the shadows, and a visible focus ring throughout.
  • Capped the projection scale and DPR-independent transforms, paused the rAF loop on document.hidden and when the lamp settles, and confirmed reduced-motion drops all autonomous motion while keeping the pointer-driven shadows alive.
  • Added a no-JS scene note and a reveal failsafe so content is never trapped at opacity zero.