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.
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.
// 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.
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.
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.
// 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.
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
| Part | Size | Note |
|---|---|---|
| The iris section's code | 7.5 KB | blades, ring, readouts and the scroll mapping; no dependencies |
| The still plate | 264 KB | one JPEG, the film's first frame |
| The canvas | 3840 × 2160 | 33 MB of pixels, the same on every screen |
| One redraw | 1.0–1.5 ms | at 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.
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.
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.