← Available Light

How this was lit

Four lighting booths, zero photographs — every frame shaded per pixel in plain JavaScript.

Available Light is one HTML file. The four "booths" are canvases running the same little renderer; only their preset lamp positions differ. Nothing is WebGL — the point was to show that a lighting lesson fits in a few dozen lines of arithmetic.

The renderer

Each booth is a 640×520 ImageData loop. The subject is an analytic sphere: for every pixel inside its disc the surface normal is known outright (nz = √(1−x²−y²)), so shading is a dot product with the lamp vector plus a Blinn-ish highlight (pow(n·h, 42)) and a fixed fill term of 0.055 — that fill is what the contrast-ratio readout divides against. A warm "wrap" is added along the terminator, because tungsten does that. The cast shadow is an ellipse displaced opposite the lamp, faded by elevation; the wall hotspot brightens when the lamp crosses behind the subject, which is what makes the rim study read.

The lamp is the interface

There is no slider. Pointer drags map horizontal offset to azimuth (±178°) and vertical offset to elevation (−8° to 77°), and the frame re-renders on every move — a full-frame CPU repaint is ~330k pixels, comfortably under a millisecond budget on anything modern. Each study's Reset button restores its canonical position, and a live classifier names whatever you have built (Rembrandt / Loop / Split / Butterfly / Rim / Flat (mugshot)), so wandering off the recipe is itself the lesson.

Type & palette

Access

The booths are pointer-driven but the page reads completely without them; every study's copy states what its preset does. Reveals respect prefers-reduced-motion; renders only happen on interaction, so an idle page costs nothing.

Deployment

npx wrangler pages deploy set1-d --project-name=set1-d

Static deploy to Cloudflare Pages. Three self-critique passes at 1440px and 390px preceded the ship; the log lives in NOTES.md.

Designed and built end-to-end by Claude Fable 5.