A university-press book that came alive
The brief asked for an interactive field guide to fungi: a cream/forest/ochre naturalist palette, hand-drawn-feel mushroom illustrations, organically morphing shapes, a "mycelial network" canvas that spreads as you scroll, and spore-print dividers. Fraunces for display, Karla for text.
The organizing idea was restraint with one quiet wonder underneath it. A field guide is a calm, print-first object — generous margins, careful captions, the confidence to leave space. So the page reads mostly like a beautiful book. The wonder is what the book can't show on paper: the living network below the forest floor. Everything animated had to earn its place against that stillness.
To make the concept feel real rather than decorative, I invented a plausible brand — The Mycological Understory Society, publishing "Vol. IV · Understory Papers" — and wrote every word as copy, not filler: six full species accounts with habitat, season, spore print, and an edibility verdict that occasionally warns you off ("whether it wants to feed you or fell you").
Where the choices were made
Typography as the illustration frame
Fraunces carries the whole voice. Its optical-size and SOFT/WONK axes let display headings feel drawn and slightly wonky — botanical, not corporate — while text-size settings stay legible. Karla, a humanist grotesque, keeps the body warm and quiet so the serif can be the character.
A committed palette
Everything runs on CSS custom properties: cream paper, deep-forest ink, ochre and rust for the fungi, gold for the network glow. A multiply-blended SVG grain sits over the whole page so the "paper" never reads as flat screen-white. The dark network section inverts the palette — forest field, gold hyphae — so the one moment of spectacle is set apart from the calm reading around it.
Illustration over stock
Every mushroom is a hand-authored SVG. The trick to making vector shapes feel drawn is to refuse geometric perfection: wobble the outlines, add a faint pen-stroke pass over the fill, and layer real anatomy — a gill fringe under the cap, warts, a bulbous volva, honeycomb pits, concentric bracket bands.
// faint pen-line pass reused across every illustration
const pen = 'stroke="#2b2417" stroke-opacity=".55" fill="none"';
// gill fringe under the cap — reads as a real amanita
<path d="M52 96 C70 106 130 106 148 96 L142 110 …Z" fill="#f0e5c9"/>
<g stroke="#cbb98d">${Array.from({length:13}).map((_,i)=>{
const x = 60 + i*6.4; // evenly-spaced blades
return `<path d="M${x} 101 l1 10"/>`;
}).join('')}</g>
The three things worth stealing
1 · Mycelium that grows by branching
The centrepiece is a canvas "wood wide web." The first version was a mistake worth admitting (see the passes below); the version that shipped is a recursive hyphal-growth algorithm. A handful of hub "trees" each send out roots, and every segment curves via a perpendicular control point, then spawns one to three thinner children at a spread angle. Segments carry an order (their generation) so that on scroll the web can grow outward from the trunks, front by front.
function grow(x, y, ang, len, depth, ord){
if (depth <= 0 || len < 9) return;
const ex = x + Math.cos(ang)*len, ey = y + Math.sin(ang)*len;
// curved control point, offset perpendicular to the run
const perp = ang + Math.PI/2, bend = rnd(-len*.32, len*.32);
segments.push({ x0:x, y0:y, x1:ex, y1:ey, order:ord, grow:0,
cx: (x+ex)/2 + Math.cos(perp)*bend,
cy: (y+ey)/2 + Math.sin(perp)*bend });
// branch into 1–3 thinner, shorter children
const kids = depth > 3 ? (Math.random()<.75?2:3) : (Math.random()<.6?2:1);
for (let k=0; k<kids; k++)
grow(ex, ey, ang + rnd(.35,.95)*(k%2?1:-1), len*rnd(.62,.82), depth-1, ord+1);
}
Scroll position sets a growth front; each segment eases its own grow value toward that front and is drawn as a partial quadratic curve, with a bright mote at its advancing tip. Line width tapers with generation, so trunks are bold and the finest threads are hairlines — which is exactly how mycelium looks.
2 · Blobs that morph like living tissue
The soft background shapes aren't static SVGs. Eight points on a circle each breathe on their own sine offset, and the path is rebuilt every frame through their midpoints so the curve stays smooth — an organic wobble instead of a rigid ellipse.
for (let i=0; i<n; i++){
const ang = (i/n)*Math.PI*2;
const rr = base + Math.sin(ts/speed + seeds[i]) * amp; // each point breathes
pts.push([ cx + Math.cos(ang)*rr, cy + Math.sin(ang)*rr ]);
}
// rebuild the path through the midpoints for a smooth closed curve
el.setAttribute('d', buildSmoothPath(pts));
3 · Spore prints made of maths
The section dividers are real spore prints: a few hundred dots sprayed from a centre with a gaussian-ish falloff (dense and dark at the core, sparse at the edges), flattened vertically the way a cap deposits them on paper. Same function, different accent colour per divider.
<canvas>. The only external requests are the two Google Fonts.What's real, and where it came from
- Illustrations — six species, one anatomy plate, four lifecycle icons, and the brandmark, all authored by hand as SVG path data. No stock, no AI image generation, no
assets/folder needed. - The network & hero motes — generated live in
<canvas>withrequestAnimationFrame, paused ondocument.hidden. - Paper grain — an inline
feTurbulenceSVG data-URI, multiply-blended. No texture image. - Type — Fraunces & Karla via Google Fonts (preconnect +
display=swap). This build note also loads JetBrains Mono for the code blocks. - Copy — written for this build. The species facts are broadly accurate naturalist detail; treat the guide as design, not foraging advice.
What critique found, and what changed
Each pass followed the same loop: screenshot top, middle, and bottom at both 1440×900 and 390×844, read every frame like a hostile art director, fix everything, then add one deliberate complexity upgrade.
The network was the problem
Charm and legibility
Atmosphere and life
A checklist to build one like it
- Commit to a palette in custom properties and lay a multiply-blended noise grain over everything — screen-white is the giveaway of an unfinished page.
- Draw your illustrations in code and wobble them. A faint shared pen-stroke pass plus one layer of real anatomy turns flat vectors into something that reads as hand-drawn.
- Make one hero interaction, and make it structural. A recursive branching system with an order per element lets you grow it on scroll for almost free.
- Tie animation to the reader. Ease values toward a scroll-derived target; pause every loop on
document.hidden; honourprefers-reduced-motion. - Write real copy. Invent a brand, give it a voice, and never ship lorem ipsum.
- Screenshot and critique in passes. Read your own renders harshly at 1440 and 390; fix, then add one deliberate upgrade each round.
That's the whole thing. The source is a single self-contained index.html — no build step, no framework. Go back to the guide →