COUPON — how a live coupon-collector album, and the agonising wait for the last coupon, was made

COUPON. field notes
← View the site

A set is cheap to start and brutal to finish. COUPON runs that asymmetry in front of you and lets you watch the last one refuse to come.

COUPON is a live study of the coupon-collector problem: draw uniformly at random from sixty distinct coupons and try to complete the album. It fills fast, then stalls near-complete while duplicates pile up, because collecting all N takes on average N·Hₙ ≈ 281 draws, not sixty. A draw-count, the running expectation, and a canvas histogram of many completed runs sit alongside — the shape of the wait, recorded. Built for the Generative Assets collection as a piece about how randomness, piled deep, becomes an iron law of arithmetic.

Palette

Argued from the subject: a well-thumbed sticker album on a warm paper page, teal printer's rule for the empty slots, one hot ink for the coupons you've actually landed.

Album cream
#EBE4D4
The paper page. Warm, matte, unglossy — a collector's binder, not a screen. A faint procedural grain keeps it from reading flat.
Slot teal (line/mark)
#3A6E6A
Printer's rule for the empty slots and their ghost numbers. A mark colour only — it would fail 4.5:1 as body text, so copy never uses it.
Filled terracotta
#C25A2E
The one warm accent — a coupon you own. Reserved for filled slots, new-draw chips and the mean line, so landing a coupon reads instantly.
Ink
#24221C
Body copy and the big counters. Near-black warm brown, ~12:1 on the cream — the dedicated text token, kept apart from teal and terracotta.
Ink secondary
#57503F
Paragraph and caption ink, still ~5.5:1 on the page for comfortable long-read contrast.
Terracotta deep
#A5481D
A darkened accent used where terracotta must carry text — the filled numbers, "since last new", the completion flag.
Type

The whole page is one variable font doing two jobs. Roboto Flex is driven across its optical-size, width and weight axes so a coupon's state is legible as pure letterform — the "type" brief made literal.

Roboto Flex
Display · wordmark · album numbers · counters

One variable file, five expressions. The wordmark sits at width 140 / weight 820; an empty slot's number is thin and condensed (width 62 / weight 300); when a coupon lands it morphs to wide and heavy in a single animated transition — the collection made physical in type. The big draw-count runs tabular so it ticks in place.

Roboto Flex
The empty-slot ghost — width 62 / weight 300

Every empty slot shows its number waiting, set condensed and light so unclaimed coupons literally read thinner than the ones you hold. "Since last new" tightens its own weight as the drought lengthens — the wait rendered as tension in the letterform.

IBM Plex Mono
Labels · readouts · draw chips · axes

Every label, chip and histogram tick is monospaced, so the live readouts and the row of recent draws align like a lab tally beside the album.

Technique

A DOM album, a variable-font fill, a canvas histogram

The album is sixty plain DOM slots in a CSS grid — no canvas for the collection itself, because the signature is type. Each tick draws an integer in [0,60); a new coupon flips its slot to .filled and the number's font-variation-settings transition animates thin-condensed → wide-heavy, a small "stamp" pop with it. A duplicate just increments the tally and a mostly-dim chip joins the ticker — which is what makes the stall visceral: draw after draw lands a coupon you already own.

The draw rate is deliberately adaptive: many missing coupons pour fast; the final few are metered out slowly so you feel each one arrive. Beside the album, the running N·Hₙ expectation is printed as the target, and a canvas histogram bins the completion draws of many runs. Sixty-four full runs are simulated silently at boot, so the skewed right tail (the Gumbel finish) is already drawn; a small terracotta marker slides along the same axis showing where the live run currently sits, and drops into a bin when it completes.

At rest the album boots pre-poured to ~90% — six coupons missing, the count already past two hundred — so the stall is the first thing on screen, not a blank waiting for time to pass. DPR is capped at 2, the histogram re-sizes with its container, and the loop pauses when the tab is hidden. Reduced-motion skips the animation entirely and renders the settled 90% frame with the histogram fully drawn.

Build log

Pass 1 — Craft

First pass read as a plain progress grid, not an album — the empty and filled slots were too similar. Pushed the variable-font contrast hard (condensed-light ghost vs wide-heavy fill) so state is unmistakable, added the dashed printer's-rule border for empties and a warm terracotta tint plus soft shadow for fills. Corrected the resting draw-count, which read implausibly low for a 90%-full album, by fast-forwarding the real simulation rather than faking a number — and had that fast-forward keep its last sixteen draws so the ticker's duplicate pile is on the very first paint instead of an empty row. Tuned the cream so terracotta and teal both sit on it without vibrating, and set the grid columns to reflow cleanly at every breakpoint.

Pass 2 — Depth

Added the two details that carry the concept: the draw ticker of recent picks (mostly dim duplicates, the rare terracotta new) that makes the stall legible, and the "since last new" counter whose type weight tightens as the drought lengthens — the wait becoming physical tension in the letterform. Gave the last few missing slots a slow "wanted" pulse so the eye knows exactly which coupons are holding up the set, and a filled slot lifts like a sticker on hover — a small tactile reward for inspecting the album. Added the live marker sliding along the histogram axis, so this run's position among all past runs is visible before it lands in a bin.

Pass 3 — Hardening

Verified 375px (masthead, wordmark, six-column album, panel and histogram all within the viewport, no real overflow), the reduced-motion path (settled 90% frame, histogram drawn, every animation and transition killed), focus-visible rings, DPR cap and debounced histogram resize, and rAF pause on tab-hide. Confirmed liveness: the album pours, stalls, completes, records its run and re-pours on its own. Chanel rule — removed a redundant second background-blend layer and an unused inner-shadow overlay that the border and tint already implied.