The specimen for this foundry isn't a picture of type — it is the type, rebuilt every frame from forty thousand points of ink. Here's how a glyph becomes matter: how it's sampled, how it morphs, and how your cursor gets to carve it.
A type foundry where the specimen is the particle system. Instead of a static poster, the hero letter is a live cloud that samples its own outline, settles into a glyph, reflows into the next, and yields under the cursor so you can carve a hollow in it — then heals when you lift your hand. The palette is ivory paper, ink particles, and a single vermilion accent, with a real specimen sheet (weights, glyph set, optical scale) set in the family below.
The constraint that shaped everything: the letter must read crisply AS a letter from a single static frame. That pushed every decision toward a tight resting state — dense sampling, high damping, a clean counter.
There is no font-parsing library here. The trick is to let the browser do the hard part: render the glyph to an offscreen 2D canvas, then walk its pixels and emit a particle target wherever a pixel is opaque. A stride of two pixels controls density; a little per-cell jitter keeps the edges from looking gridded.
// render the glyph big and heavy on an offscreen canvas
octx.font = `900 ${Math.round(SAMPLE_H*0.82)}px "Fraunces", serif`;
octx.fillText(ch, SAMPLE_W/2, SAMPLE_H/2);
// opaque pixels -> world-space targets, with cell jitter
const data = octx.getImageData(0,0,SAMPLE_W,SAMPLE_H).data;
for (let y=0; y<SAMPLE_H; y+=step)
for (let x=0; x<SAMPLE_W; x+=step) {
if (data[(y*SAMPLE_W+x)*4+3] > 128) { // alpha test
const wx = (x/SAMPLE_W - 0.5) * PLANE_W;
const wy = -(y/SAMPLE_H - 0.5) * PLANE_W;
pts.push(wx, wy);
}
}
A fixed budget of ~42,000 particles is then mapped onto whatever number of sample
points a glyph yields (i % n), so an A and an
& both use every particle even though they have very different pixel
areas. Waiting on document.fonts.ready and re-sampling
matters — sample too early and you get Times New Roman's skeleton, not Fraunces'.
Each particle carries a random seed. When the glyph
changes, particles don't all leave at once — higher-seed particles start their
journey later, so the letterform dissolves and re-forms in a wave rather
than a hard cut. Every point runs a little critically-damped spring toward its
target; strong damping at rest (0.68) is what makes the
settled letter read as crisp ink instead of a comet field.
// staggered local progress, then a spring toward the target
const start = seeds[i] * 0.42; // late starters
const lp = easeInOut(clamp((gp-start)/(1-0.42), 0, 1));
vel[i] += (target[i] - pos[i]) * (0.06 + 0.22*lp);
vel[i] *= morphing ? 0.80 : 0.68; // crisp at rest
pos[i] += vel[i];
To carve the letter you need the mouse in the same space as the particles. The pointer is unprojected through the camera onto the z = 0 glyph plane with a raycaster; particles within a radius of that world point get pushed away — and crucially, off the plane in z, so the hole you carve has real depth. The same spring that formed the letter pulls them back the instant you leave: the healing is free.
// screen -> NDC -> ray -> intersection with the glyph plane
raycaster.setFromCamera(ndc, camera);
raycaster.ray.intersectPlane(glyphPlane, cursorWorld);
// push nearby particles out, and lift them off z=0
const f = 1 - d / REPEL_R;
vel[ix] += (rx/d) * f * REPEL_STR;
vel[ix+2] += f*f * REPEL_STR * 1.8 * dir; // carve depth
A click adds a one-shot ripple that decays over a handful of frames, shoving grains outward from the click point as the next glyph is cast.
Built the way the whole showcase was: screenshot, critique like a hostile art director, fix, repeat.
It read, but it drifted. The first cast was legible but soft at the edges and often caught mid-morph in a static shot. I tightened the resting spring and cut the depth jitter so the settled letter hugs its outline.
Comet trails and a mobile collision.
Low damping left long motion streaks; on a phone the full-height glyph sat right on
top of the hero copy. Raising damping to 0.68 gave crisp
grains; on mobile the canvas shrinks to 60% height, the copy gets a paper scrim, and
the controls collapse to a top row. Added the click ripple as the pass upgrade.
Polish. Fixed a kicker widow and the italic side-bearing before the comma, tuned the accent-particle ratio (~7% vermilion) so the color reads without shouting, and confirmed zero console errors and a clean headless WebGL render with a static fallback letter if the context is unavailable.
document.fonts.ready and re-sample, or your specimen is the wrong typeface.