Library / Teardown

Thirteen blades,
drawn in code.

The lens on this page closes from f/1.1 to f/16 as you scroll. The film model asked to show that drew nine blades and opened the iris again halfway through, so the iris is drawn in code over a still photograph instead. Here is the geometry, what it costs, and how the film got it wrong.

13Blades
1.3 msPer redraw, 4K
0Blade shapes stored

CAIVOR is a lens maker that does not exist, and the Velurne 50 / 1.1 is its only lens. The page is made of films and stills generated in Google Flow, with one exception: in the second section, where you scroll the aperture ring from f/1.1 to f/16, the thirteen blades closing over the glass are not in the photograph. They are drawn on a canvas laid exactly on top of it.

That was not the plan. It is what happened after the film model was asked to do it.

Close it yourself

This is the same geometry — the same stops, the same opening rule, the same blades and the same turn — drawn over a plain housing instead of the photograph. The blade count is the one thing added, so you can see what the film model got wrong.

The same geometry, live13-gon
f/2.8
15 %Light in
2.7Stops closed
18.7°Turned
39.3 %Opening radius
2.9 %Off round
96.2 %Of a circle

Try 9 blades at f/2.8, then 13. Both are polygons; one of them reads as a circle. That difference is most of this page.

1 · A still, measured

The plate is the first frame of the rejected film: the iris fully open, before anything went wrong. Its centre and radius were measured in the image's own pixels, and the canvas is sized to the image and scaled with it, so the drawn blades stay on the glass at any window size.

the photograph is the coordinate system
// measured on the 1920 × 1080 still: the iris centre and its fully open radius
const IRIS = { x: 1410, y: 532, r: 326 };
const SCALE = canvas.width / 1920;            // the canvas is 3840 × 2160, laid over the still

function coverPlate(zoom) {                   // one box holds the still and the canvas,
  const k = Math.max(innerWidth / 1920, innerHeight / 1080) * zoom;   // so they scale together
  plate.style.width = 1920 * k + 'px';
  plate.style.height = 1080 * k + 'px';
}

2 · The opening follows the f-number

The light a lens admits falls with the square of its f-number, so the opening's area has to as well. For a regular polygon that means the radius goes as 1/f — nothing else is needed.

f-number → opening
const r = R * 1.1 / f;                        // circumradius: area ∝ 1 / f²
if (r >= R * 0.995) return;                   // wide open: the photograph's own iris shows

const V = [];                                 // the opening is a regular 13-gon
for (let i = 0; i < 13; i++) {
  const a = spin + i * 2 * Math.PI / 13;
  V.push([cx + r * Math.cos(a), cy + r * Math.sin(a)]);
}
light.textContent = Math.round(100 * (1.1 / f) ** 2) + '%';

At f/16 the radius is 6.9 % of the open one and the lens passes 0.47 % of the light it passes wide open — 7.7 stops less. At f/1.1 nothing is drawn at all: the canvas stays empty and the photograph's own open iris is what you see, so the hand-off is invisible.

3 · The ring moves in stops

The engraved marks on an aperture ring are a stop apart and evenly spaced, so each click halves the light. Scrolling moves the ring, not the f-number.

scroll → ring → f-number
const FSTOPS = [1.1, 1.4, 2, 2.8, 4, 5.6, 8, 11, 16];

const s = smooth(0.12, 0.88, progress);       // the section's scroll, eased at both ends
const idx = s * (FSTOPS.length - 1);          // position along the engraved marks
const lo = Math.floor(idx), hi = Math.min(FSTOPS.length - 1, lo + 1), t = idx - lo;
const f = FSTOPS[lo] * Math.pow(FSTOPS[hi] / FSTOPS[lo], t);   // geometric between marks

Between two marks the f-number is interpolated geometrically rather than linearly, so the light changes at the same rate all the way across a click. The one uneven gap is the first: f/1.1 to f/1.4 is only 0.7 of a stop, as it is on real fast lenses.

4 · Thirteen edges make thirteen blades

No blade shape is stored anywhere. Each edge of the opening is extended past its end vertex, and that extension is the leading edge of one blade. The thirteen pieces tile the ring around the opening with no gap and no overlap, at every size.

the pinwheel
// blade i is bounded by edge i, by edge i's own extension past V[i+1] (its leading edge),
// and by edge i−1's extension past V[i] (the previous blade's leading edge, lying over it)
for (let i = 0; i < N; i++) {
  const v0 = V[i], v1 = V[(i + 1) % N], d = D[i], dp = D[(i + N - 1) % N];
  const far1 = [v1[0] + d[0] * S, v1[1] + d[1] * S];
  const far0 = [v0[0] + dp[0] * S, v0[1] + dp[1] * S];
  g.save();
  g.beginPath();
  g.moveTo(...v0); g.lineTo(...v1); g.lineTo(...far1); g.lineTo(...far0);
  g.closePath();
  g.clip();                                   // everything drawn now lands on blade i only
  shadeBlade(i);
  g.restore();
}

As the opening shrinks, the pieces change shape the way the blades of a real iris appear to from the front, because they are the same construction: straight blades pivoting around a polygon.

5 · Light, shadow and a turn

Thirteen identical dark shapes would read as a pattern, not as metal. Each blade is lit by which way its edge faces a key light at the upper left, darkened where the blade above it casts a shadow, and given a thin warm catch-light along its leading edge.

shading and spin
const nx = d[1], ny = -d[0];                             // the edge's outward normal
const lit = 0.5 + 0.5 * (-0.7 * nx - 0.7 * ny);         // 1 when it faces the key light
gr.addColorStop(0, `rgb(${40 + 70 * lit},${34 + 52 * lit},${29 + 36 * lit})`);   // blued steel

drawIris(f, -0.35 * Math.log(f / 1.1));                 // the opening turns as it closes

The turn is logarithmic in the f-number, so it is the same 7° for every stop and 53.7° by f/16. A real iris turns as it closes; without it the polygon only shrinks, and the eye reads that as a zoom rather than as blades moving.

What it costs

PartSizeNote
The iris section's code7.5 KBblades, ring, readouts and the scroll mapping; no dependencies
The still plate264 KBone JPEG, the film's first frame
The canvas3840 × 216033 MB of pixels, the same on every screen
One redraw1.0–1.5 msat 4K in Chrome, M1 Pro, from f/1.4 to f/16

The blades are only redrawn when the f-number has actually moved — by more than 0.001 — so a reader who stops scrolling costs no drawing at all. The one fixed cost is the canvas: it is sized once for a 4K capture, 8.3 million pixels on every screen. That is less wasteful than it sounds. The photograph is scaled to cover the window, so on a portrait phone it is shown about 1 500 CSS pixels wide and the canvas lands close to one canvas pixel per device pixel.

What went wrong

The first version of this section was a film. Veo 3.1 was given the still of the open iris as its first frame and a closed iris as its last, and asked for the blades to close over eight seconds. One hundred credits later the clip was checked frame by frame:

  • the iris closed by 2.7 s, opened again between 3 and 5 s, then closed a second time — scrubbed by scroll, the aperture would have breathed;
  • the opening had about nine sides, counted on a crop at 3.4 s. The lens is specified with thirteen.

Earlier in the project an assistant model, asked to check a generated still, had reported its highlights as thirteen-sided. Counted by hand they were nine or ten — and it called a thirteen-sided polygon hendecagonal, which is eleven.

Nine against thirteen sounds like a detail. It is not: a nine-gon sits 6.0 % inside its circle and covers 92 % of it; a thirteen-gon sits 2.9 % inside and covers 96 %. The toy shows the difference: one reads as a polygon, the other as round with small corners, which is exactly what a thirteen-blade iris is built for.

The lesson worth keeping

A film model does not hold a count or a direction of motion, and a model asked to check it will agree with the prompt. Count it yourself. Where the number is the point — blades, elements, teeth, digits — draw that part in code and let the photograph carry everything else.

See it running

The iris is the second section of the page. Scroll slowly through it: the ruler under the lens is the ring, and the number beside it is computed, not engraved.

Open CAIVOR ↗

Sources

The area of a regular N-gon of circumradius r is ½·N·r²·sin(2π/N), and its edges sit 1 − cos(π/N) inside the circle. Exposure through a lens falls as 1/N² in the f-number. The Velurne 50 / 1.1 and CAIVOR are fictional; the plate was generated in Google Flow.