Library / Teardown

The scroll
is the dimmer.

A lighting page with one control. Scroll down and the luminaire dims, the light warms, and every figure on the page — candela, lux, the size of the pool, what the wall returns to your eye — is recomputed from one specification. Here is the model, what it costs, and what we would design differently next time.

3.2 KBOf photometry
0.07 µsPer sample
1Specification

VESPER is a lighting house that does not exist, making a ceiling luminaire that does not exist either. The idea behind the page was simple and, I still think, right: you cannot photograph a light, only what it lands on. So the page has no product shot at the top. It has a plaster room with one luminaire in it, and scrolling is the dimmer.

Every figure on that page comes from one specification object. None of them was typed in.

Dim it yourself

The same model, cut down to what is worth reading. Drag the dimmer down and watch two things the eye does not expect: the light falls much faster than the control, and it goes orange as it goes.

The same photometry, 40 linesFull
100%
1,150 lmFlux
100%Of full output
2,700 KColour
5,784 cdPeak intensity
738 lxOn the floor
98.6 cd/m²Wall, ρ 0.42

Half the setting gives a quarter of the light. That is not a bug in the toy — it is step 5.

1 · One specification in

Everything starts from the numbers a lighting designer would find on a data sheet. The page never stores a result, only these, and derives the rest every frame.

the whole input
const SPEC = {
  flux: 1150,        // lm, at the top of the throw
  watts: 13.1,       // W
  beamFWHM: 24,      // deg, full width at half peak intensity
  cutoff: 40,        // deg, past this the louvre shows metal, not source
  height: 2.80,      // m, aperture to floor
  kHigh: 2700, kLow: 1800,
};

1150 lm from 13.1 W is 87.8 lm/W — a believable figure for a warm-white architectural downlight, which matters: a spec that is off by a factor of three makes every number after it quietly absurd.

2 · The beam is a Gaussian

A rotationally symmetric beam is modelled as a Gaussian in the angle off the axis. The width comes from the beam angle, which lighting defines at half of the peak intensity. Integrate over the sphere and the peak falls out.

flux → candela
const sigma = (fwhm) => (fwhm * RAD) / 2.35482;      // FWHM → standard deviation

// ∫ I₀·e^(−θ²/2σ²) over the sphere ≈ I₀·2πσ² for a narrow beam
const peakIntensity = (flux) => flux / (2 * Math.PI * sigma(24) ** 2);

function intensityAt(deg, flux) {
  if (Math.abs(deg) > SPEC.cutoff) return 0;           // the louvre cuts it
  const t = deg * RAD;
  return peakIntensity(flux) * Math.exp(-(t * t) / (2 * sigma(24) ** 2));
}

1150 lm in a 24° beam is a peak of 5 784 cd. Check the model against its own definition: at 12° — half the beam angle — the intensity is exactly 0.500 of the peak. If that ratio is not one half, the Gaussian is wrong and nothing downstream is worth printing.

3 · Candela is not what you see on the floor

Intensity is a property of the luminaire. What lands on a surface falls off with the square of the distance, and the lit circle is where the intensity has dropped to half.

candela → lux
const illuminance  = (flux, h = 2.80) => peakIntensity(flux) / (h * h);
const poolDiameter = (h = 2.80) => 2 * h * Math.tan((24 / 2) * RAD);

At 2.80 m that is 738 lx on the axis and a pool 1.19 m across. And because the model emits nothing past the 40° cut-off, the page could honestly print UGR < 10: there is no source visible from a normal seated angle to glare you. That line on the page is a consequence, not a claim.

4 · Lux is not what you see either

The eye reads luminance — what the surface sends back. For a matte surface of reflectance ρ under E lux, that is ρE/π. The same light on three different walls is three different rooms.

lux → what the eye gets
const luminance = (E, rho) => (rho * E) / Math.PI;     // cd/m², a matte surface

const E = illuminance(1150);   // 737.8 lx — use the model's value, not the rounded one
luminance(E, 0.72);   // pale plaster  → 169.1 cd/m²
luminance(E, 0.42);   // warm stone    →  98.6 cd/m²
luminance(E, 0.28);   // dark oak      →  65.8 cd/m²

This is the whole argument of the original page in one function: the luminaire did not change between those three rooms, only what it landed on.

5 · The dimmer is a square law

A linear dimmer feels wrong, because perceived brightness is not linear in flux. Driving the output as the square of the setting makes the bottom of the travel usable, and a warm-dim luminaire rides the Planckian locus down at the same time, like a filament does.

setting → lumens → kelvin
const fluxAt   = (d) => SPEC.flux * d * d;                       // square law
const kelvinAt = (d) => 1800 + (2700 - 1800) * Math.pow(d, 0.55); // warm dim

fluxAt(0.5);   kelvinAt(0.5);   //  288 lm — a quarter of full — at 2 415 K
fluxAt(0.1);   kelvinAt(0.1);   //   12 lm at 2 054 K
                kelvinAt(0);     //          1 800 K, the last glow

The colour then goes through the same Planckian-locus conversion CALASTRA uses for sunlight (Kim et al., 1999), so a dimmed lamp and a low sun are drawn by one function.

What it costs

PartSizeNote
The whole model (js/photometry.js)3.2 KBone spec, eight functions
The page and its script43 KB24 KB of HTML, 18.7 KB of main.js
The films and stills7.8 MBfour clips (6.3 MB) and six stills and posters (1.6 MB)

A full sample — flux, colour, candela, lux and one luminance — measured at 0.074 µs on an M1 Pro: about 216 000 complete recomputations inside one 16 ms frame. The physics is, again, the cheapest thing on the page; the 7.8 MB of media is what a visitor waits for.

What we would do differently

The model above is sound. What it taught is about the page around it: a correct computation is only half of an experience, and these are the things we would design differently next time.

  • Give every section its own look. Each chapter was a plaster room with a pool of warm light in it — consistent, but close enough in tone that the sections blur together at scrolling speed. Changing the surface, the scale or the time of day from room to room would have given each one an identity.
  • One film, one place. A clip that appears in two sections spends the reader's attention twice on the same thing.
  • Let the product be seen. "You cannot photograph a light, only what it lands on" is a good premise, and it can still show the luminaire itself clearly somewhere on the page.
  • Build towards a moment. A dimmer that only goes down has nowhere to arrive. A page like this wants a turn — the light coming back up, or the room it was designed for revealed at the end.

One thing was caught and fixed along the way: the first hero film only brightened, and it read as one lamp going off and another coming on rather than one lamp dimming. The replacement take moves the pool of light across the wall with the camera locked, so there is visibly one source.

The lesson worth keeping

A correct model gives a page its integrity, not its interest. Design the page to be worth looking at first; then make every number on it true.

A smaller one, for anyone capturing pages to video: a CSS box-shadow blur on a small dot was the single element that did not rasterise identically across two compositing histories, and it broke a byte-identical loop seam. Replacing it with a radial gradient fixed it.

Sources

Gaussian beam and FWHM: the standard definition of a luminaire's beam angle at half peak intensity. Planckian locus: Kim et al., Design of advanced color temperature control system for HDTV applications (1999). Luminance of a Lambertian surface: L = ρE/π.