← understory gallery index ↖

making note / 22

a site that keeps its own field record.

These are the design notes for Understory: a fictional soil research collective whose homepage treats the network belowground as both subject and interface.

the concept

Understory needed the patience of fieldwork, not the slickness of an ag-tech dashboard. The page starts on spore white like a specimen tray, then moves through dark field-floor sections and a weathered-grey ledger. The copy is intentionally particular: named places, methods, and observations make the collective feel accountable to an actual season.

The hero stays in view for an extended first scroll. That makes scrolling feel less like navigation and more like supplying time to a small, living system. The network is not a background decoration; it is the page’s first piece of evidence.

The visual language is a field notebook with dirt beneath its fingernails: quiet, legible, and just irregular enough to feel grown.

the signature technique

The canvas begins with one seed node. Every growth step selects an active tip, pulls it weakly toward the closest nutrient point, pushes it away from nearby hyphae, and adds a small deterministic turn. At intervals it forks. If a growing tip gets close enough to an older, unrelated node, it creates an ochre fusion link and retires.

Growth is one-way state: the nodes and links array is never cleared. Scroll depth increases the target number of links, while a small visible-time allowance means the drawing is already alive before anyone moves. The canvas paints from that stored record on resize, rather than restarting the simulation.

function growOne() {
  if (!state.tips.length || state.links.length >= 470) return false;
  const tipIndex = Math.floor(pseudo(state.links.length * 1.9) * state.tips.length);
  const tip = state.tips[tipIndex];
  const origin = state.nodes[tip.node];
  const direction = fieldDirection(tip, origin);
  const segment = .022 + pseudo(state.links.length + 9) * .012;
  let candidate = {
    x: origin.x + direction.x * segment,
    y: origin.y + direction.y * segment,
    parent: tip.node, depth: tip.depth + 1, born: performance.now()
  };
  if (candidate.x < .045 || candidate.x > .955 || candidate.y < .07 || candidate.y > .94) {
    tip.angle += Math.PI * (.66 + pseudo(state.links.length) * .2);
    return false;
  }
  const encounter = nearestNode(candidate, tip.node);
  if (tip.depth > 3 && encounter.distance < .027 &&
      encounter.index !== origin.parent) {
    state.links.push({
      from: tip.node, to: encounter.index, fusion: true, born: performance.now()
    });
    state.tips.splice(tipIndex, 1);
    return true;
  }
  const newIndex = state.nodes.push(candidate) - 1;
  state.links.push({
    from: tip.node, to: newIndex, fusion: false, born: candidate.born
  });
  tip.node = newIndex;
  tip.depth += 1;
  tip.angle = Math.atan2(direction.y, direction.x) + direction.turn;
  const shouldBranch = tip.depth > 3 && tip.depth % 3 === 0 &&
    state.tips.length < 12 && pseudo(state.links.length * 3.7) > .43;
  if (shouldBranch) {
    const fork = .44 + pseudo(state.links.length * 8) * .35;
    state.tips.push({
      node: newIndex,
      angle: tip.angle - fork,
      depth: tip.depth,
      strain: (pseudo(state.links.length) - .5) * .8
    });
    tip.angle += fork * .72;
  }
  return true;
}

palette + type

The four palette values stay close to a real forest-floor register: a warm paper, mycelium grey, dark decomposed matter, and one mineral ochre for new growth and decisions.

spore white#F0EDE4
mycelium grey#C4BCA8
forest floor#2E2A20
ochre#C08A2E

Inclusive Sans carries the lowercase-heavy voice and keeps labels plainspoken. Source Serif 4 appears where the site needs a slower, more reflective note: field measures, case-study names, and the small quote.

Inclusive Sans / 400–700soil has a memory
Source Serif 4 / 500–600listen at the root

iteration passes

  • Pass 1 — correctness
    • Guarded the canvas boot against a missing canvas, frame, or 2D context; added resize and motion-query fallbacks for older browsers.
    • Changed the growth clock to count only active, visible time, so a hidden tab cannot resume with a large catch-up jump. The render loop now stops when the tab or canvas is hidden; reduced motion renders a static 54-link record with no running loop.
    • Allowed up to four growth steps per frame when scroll advances far ahead, keeping scroll-fed branching visible rather than slowly catching up. The node and link record still never resets.
    • Moved the narrow layout breakpoint to 820px, reduced the small-screen hero type clamp, and added wrapping/min-width guards to prevent clipped text and horizontal overflow.
    • Raised low-contrast ledger, case-study, and ochre-panel labels to passing foreground colours; made focus outlines use the already-contrasting foreground colour.
    • Made the wordmark, guide navigation, footer links, and return links at least 44px tall, and moved the desktop field caption inside the clipped canvas frame so it remains visible.
    • Known verification limitation: no in-app browser was available during this pass, so the 390px, 768px, and 1440px checks were source-level and parser-based rather than live screenshots.
  • Pass 2 — design depth
    • Weak moment: the network was contained too politely. The hero now gives the canvas an intentional leftward invasion and cuts a root-shaped notch into the text field, so the live organism visibly enters the editorial space without compromising the copy.
    • Weak moment: the work and method sections repeated a safe two-column cadence. The three studies now form an asymmetric field-record composition—one long vertical observation beside two smaller parcels—while the method steps overlap a notched field panel rather than simply sitting alongside it.
    • Weak moment: display type and spacing had too little sectional contrast. A five-step spatial scale now drives the major intervals; the ledger and work headings step up in size and tracking, and the hero’s three deliberate lines prevent accidental headline orphans. Button transitions now use a slow, soil-like custom curve rather than default ease.
  • Pass 3 — complexify
    • Weak moment: after the hero, visitors could only read. Added the hand-read soil core after the case studies. Each of its four keyboard-accessible strata opens a particular observation—from surface litter to a blue-grey clay seam—while a cursor probe gives the specimen a quiet physical response. Its deliberate initial state explains what to do; the last selected depth remains visible as a held reading.
    • Weak moment: transitions between the pale editorial sections did not yet carry the site’s field language. The core arrives as a dark, framed specimen against sediment-grey ground, with shallow horizontal marks at its upper edge. It makes the move from projects to practice feel like putting a spade into the page, rather than changing to another content block.
    • Finished share metadata for both pages, kept the interaction motion-optional, and re-read the field copy to make the new moment observational rather than decorative. The canvas remains one-way state; the core adds a second, smaller kind of attention without competing with it.

The field record is more useful in motion. Return to the site and grow the network with the first section’s scroll.

open understory →