On a bone-colored terrazzo ground, thousands of circles appear and grow until they touch, never overlapping, while cobalt, vermilion, ink and teal circles crowd into the letterforms and stone ones fill around them, until the word FULL stands out of a fully packed field.
Written and rendered by AI CSS Animations Oct 19, 2026
Packing problems ask how to fit shapes into a space without overlap, and circles are the classic case: Apollonius of Perga studied circles tangent to three others in the third century BC, and the Apollonian gasket fills a circle with ever smaller tangent circles. Creative coders popularized a simpler growth version in which circles appear at random and grow until they touch a neighbor, producing dense, organic fields with a natural mix of sizes. Filling a letterform or picture with one family of colors and its surroundings with another makes an image appear from pure geometry, much as dot-plate designs do. The technique is common in posters, data art and generative typography.
Make a 10-second generative circle packing: thousands of circles appear and grow until they touch, none ever overlapping, until the field is packed and the word FULL stands out in saturated inks among quiet stone-colored circles.
<inputs>
If I don't give you something, use the default and don't ask.
- Word: FULL
- Studio: Halfmoon Type Lab (fictional).
- Caption: "Every circle grew until it touched another."
- Length: 10 seconds.
</inputs>
<direction>
- Palette, a terrazzo poster: ground bone #EFE7D8; field circles in quiet stone tones #E4DACA, #D6CCBA, #C9CFCB, #DCD2C1 and #CFC5B3; letter circles in saturated inks: cobalt #2747C9, vermilion #DE3F28, ink #1D1E2C and teal #0E7C7B; type #1D1E2C, secondary #6E675C. Every letter color is at least 2.5 times darker in relative luminance than every field color, so the word reads by lightness alone, for color-blind viewers too; hue only adds life.
- Layout: the circle field is the rectangle x 80–1840, y 80–930; the strip below holds the type.
- The mask: FULL in Dela Gothic One 400, rendered once into an offscreen canvas at stage size, scaled to fit a 1560 × 560 box centered at (960, 505). From it build a signed distance field with an exact Euclidean distance transform (Felzenszwalb–Huttenlocher, two passes), positive inside the letters.
- The packing, simulated once at load at a fixed step of 1/120 s from t = 0.3 s to 7.6 s with a seeded PRNG:
- each step, make K(t) = round(min(60, 2·e^{0.62(t − 0.3)})) spawn attempts: a uniform random point in the field; reject it if |sdf| < 4.5 (too close to a letter edge) or if it lies within rⱼ + 4.5 of any existing circle j; otherwise start a circle there with r = 2 on the side sign(sdf) tells you;
- every growing circle grows by 110px/s × step and stops at the step it would reach any of: its side's maximum radius (30 inside letters, 46 in the field), |sdf(center)| − 2.5 (the letter edge, so the letterforms come out crisp from both sides), the field rectangle's edge, or another circle (center distance < rᵢ + rⱼ + 2.5); when two growing circles touch, both stop;
- find neighbors with a uniform 64px grid. Store each circle's birth time, stop time and final radius; between them r grows linearly, so r(t) is closed form. Expect roughly 3,000–3,500 circles, about a third of them inside the letters, covering about 75–80% of the field.
- Colors: each circle takes a seeded color from its side's palette (letters: cobalt 30%, vermilion 30%, ink 20%, teal 20%; field: the five stones equally).
- The look: flat discs with a 1px rim 10% darker, no outlines, no shading. A new circle fades in over 0.08 s from r = 2. When a circle stops it settles: its drawn radius dips to 96% and returns over 0.2 s, never exceeding its final radius, so nothing ever overlaps.
- Type, in the strip: the studio in Inter Tight 600, 20px, tracked 0.18em, uppercase, secondary, at (80, 978); the caption in Inter Tight 500 italic, 26px, #1D1E2C, at (80, 1014). Right-aligned at x 1840, two counters in Inter Tight 500, 26px, tabular figures, with 20px uppercase labels: "Circles 3,412" and "Covered 78%" (circle area ÷ field area), updating every frame.
- Easing: text rises 10px as it fades in over 0.6 s on cubic-bezier(0.22, 1, 0.36, 1). Circles move only by growing.
- Premium: thousands of circles that truly never overlap, letters built only from circles yet crisp at the edges, a fill that accelerates and then crawls into the last gaps, a palette where the word reads by lightness. Cheap: dots on a grid, overlapping blobs, a font set on top of the dots.
Banned: overlapping circles, the word drawn as type, outlines, gradients, drop shadows, glow, red-green-only contrast, circles leaving the field, anything flashing more than 3 times per second.
</direction>
<structure>
- 0.0–0.3s: the bare bone ground; the studio line rises in.
- 0.3–2.4s: the first circles appear across the field and grow; the big ones reach their limits; no word yet, only a scatter of stone and the odd ink circle.
- 2.4–4.8s: spawning accelerates; ink circles crowd the letterforms and stone circles the space around them until FULL emerges by color and lightness, its edges crisp; the counters rise in at 2.6 s and climb.
- 4.8–6.8s: smaller and smaller circles fill the remaining gaps as growth slows to a crawl. The 6.0 s frame: FULL clearly legible in a nearly packed field.
- 6.8–7.6s: the last tiny circles find room and stop; the counters stop.
- 7.6–8.4s: the caption rises in.
- 8.4–10.0s: hold, completely still.
For other lengths, scale the spawn rate's growth and keep the final crawl; keep at least 1 s of hold.
</structure>
<build>
- One self-contained index.html (inline CSS and JS), a 1920×1080 (16:9) stage scaled to fit the window, designed for 60fps. No image, video, texture or model files: every shape and texture is generated in code.
- window.seek(t), t in seconds, computes every visual from t alone: no CSS transitions or CSS animations, no setTimeout/setInterval, no state carried between frames, randomness only from a seeded PRNG. Anything cumulative is solved analytically from t or precomputed once at load at a fixed step. Past the end, seek clamps to the last frame.
- Without ?render in the URL, drive seek from requestAnimationFrame so it plays in a browser (loop after a 1s end hold); with ?render, don't start that loop.
- Google Fonts via <link> are fine; await document.fonts.load() for each face before defining window.seek.
- Engine: Canvas 2D, repainting every frame from scratch, the backing store sized from CSS size × devicePixelRatio. Canvas won't fetch fonts itself, so the awaited loads matter, and the mask is rendered only after Dela Gothic One has loaded.
- The distance field and the whole packing are computed once at load into typed arrays (keep load under 5 s); seek(t) draws each circle at its closed-form radius for t.
</build>
<check>
Before showing me, step through one frame per second (t = 0…10): screenshot each and look at it. Fix overlaps, clipped or cramped text, and anything off-style or Banned. Also test at load that no two final circles overlap (center distance ≥ rᵢ + rⱼ + 2.5) and no circle crosses a letter edge, and log the circle count and coverage. Check FULL reads at a glance in the 6.0 s frame and the final frame, also in grayscale, letter edges look crisp at 1280×720, nothing grows after 7.6 s, and the counters match what's drawn. Confirm seek(10) and seek(11) match, so the final frame holds.
</check> Ours, and free to use under CC0: copy it, change it, ship it. No credit needed.
A jazz-tempo film title sequence in flat vermilion, mustard, bone and black: hand-cut paper shards jab in on the beat,…
A Swiss-style event poster that builds itself on a visible 12-column grid: grotesque type rises out of baseline masks,…
A primary-colored circle, square and triangle roll, drop and turn into place on a black bar, assembling a constructed B…
A Constructivist poster in motion: red bars, a wedge piercing a black circle, halftone fields and condensed uppercase t…
More like this on the motion design inspiration board: Kinetic typography inspirationMotion graphics inspiration