Skip to main content

ns-ui

Crack Polygon Order

A decorative panel fill that tiles itself the way drying mud actually cracks: widely-spaced primary cracks grow first, then two more generations nucleate inside the largest surviving cells after a 900ms stress-rebuild pause each, every new crack tip T-junctioning into an existing one at ~90 degrees instead of crossing it, until the mature tiling holds, rewets to blank, and a new unrelated cycle begins.

Use when a section divider or decorative panel fill whose tessellation is built from a real generation HIERARCHY — first-generation cracks growing widely spaced, then second- and third-generation cracks nucleating inside the largest remaining cells and T-junctioning into older cracks at right angles, never crossing them, on an unbounded rest-hold-rewet-cooldown cycle; pick craze-rule instead for a thin 40px <hr>-replacement divider that draws in once via IntersectionObserver and idles (a single branching fracture line, not a full tessellation, and no generation hierarchy at all), or compare-crack-seam instead when the fracture needs to be a draggable, interactive seam splitting a before/after image comparison rather than an ambient, non-interactive panel fill.

Install

npx shadcn add https://design.helpmarq.com/r/crack-polygon-order.json

Ask AI

Point an assistant at this component's docs (llms-full.txt) with one click.

Claude, ChatGPT, Grok, and Perplexity open with the prompt already in. Gemini copies it to your clipboard first. Paste it in once the chat opens.

Source
registry/core/crack-polygon-order/component.tsx
"use client";

import { useEffect, useRef, useState } from "react";

// ---------------------------------------------------------------------------
// CrackPolygonOrder — a decorative panel that tiles itself the way a drying
// mud layer actually cracks: a first, widely-spaced generation of cracks
// nucleates and grows across the whole panel; once every primary crack has
// stopped, a 900ms "stress rebuilding" pause elapses and a second generation
// nucleates INSIDE the largest surviving cells and grows outward from a
// point in both directions; the same pause-then-nucleate step repeats once
// more for a third generation. The one rule that makes this desiccation
// cracking rather than a generic Voronoi/crack-seam fill: every new crack
// tip that comes within ~2px of an existing crack (a free surface, zero
// stress) stops there, at close to a right angle — a T-junction, never a
// crossing. Once no cell is worth splitting further the tiling holds fully
// formed, then the whole panel "rewets" (every stroke's opacity eases to 0)
// and a blank cooldown precedes the next, unrelated cycle.
//
// This is a real-time simulation, not a pre-computed path being revealed:
// a 2px-cell occupancy grid records where ink already exists, a random-walk
// tip advances against that grid at a literal 90px/s (segment length
// 6-10px, turning noise +-12deg per segment), and completed generations are
// turned into regions by a flood fill of the still-empty cells so the next
// generation can nucleate inside the biggest 60% of them. Growth is driven
// by real elapsed ms every rAF frame, not snapped to the 6-10px segment
// boundary — a tip's in-progress segment interpolates continuously
// (progressPx / segLen) so the stroke visibly glides at 60fps and only the
// completed, collision-tested segment endpoints are ever committed. Path
// geometry updates happen via direct SVG DOM writes on refs (not React
// state) so ~14 rAF-driven tips can grow every frame without a React
// re-render; React state only changes on the rare event that new <path>
// elements need to mount (once per generation, three times a cycle) or the
// panel resizes.
//
// t0 is deliberately not a fresh, empty panel: on mount the whole tick
// function is run synchronously against a fast, fixed 16ms clock for a
// random 1.5-10.5s of simulated time before the real rAF loop takes over,
// so every visit lands mid-cycle. prefers-reduced-motion runs the same
// synchronous driver all the way to the mature, fully-tiled hold frame and
// then stops for good — the single most structured frame this pattern ever
// produces, all three generations of T-junctions visible at once.
// ---------------------------------------------------------------------------

export interface CrackPolygonOrderProps {
  /** seeds the crack RNG; omit for a fresh, unrepeatable tiling every mount */
  seed?: number;
  className?: string;
}

const CELL = 2; // occupancy-grid cell size, px — matches the ~2px T-junction test
const SPEED_PX_S = 90; // crack tip advance rate
const SEG_MIN = 6;
const SEG_MAX = 10;
const TURN_NOISE = (12 * Math.PI) / 180; // +-12deg per segment
const GEN_PAUSE_MS = 900;
const HOLD_MS = 4000;
const REWET_MS = 2500;
const COOLDOWN_MS = 600;
const MIN_PRIMARY = 4;
const MAX_PRIMARY = 7; // inclusive
const LARGEST_FRACTION = 0.6;
const MIN_REGION_AREA_PX2 = 130; // too small to bother nucleating inside
const MAX_DT_MS = 50; // clamp so a paused-tab / IO resume can't lurch the sim
const FASTFWD_STEP_MS = 16;
const MAX_FASTFWD_ITERS = 4000;
const STROKE_W = 1.5;

type Phase =
  | "primary"
  | "secondary-wait"
  | "secondary"
  | "tertiary-wait"
  | "tertiary"
  | "hold"
  | "rewet"
  | "cooldown";

type Pt = { x: number; y: number };

type Tip = {
  // finalized segment endpoints — collision-tested, never move again
  committed: Pt[];
  // current heading + target length of the segment still in progress
  heading: number;
  segLen: number;
  // px already covered into that in-progress segment, advanced every frame
  // by real elapsed time (not snapped to the segment boundary) so the SVG
  // path visibly glides rather than jumping in 6-10px steps
  progressPx: number;
  done: boolean;
  recentCells: number[];
};

type CrackData = { id: number; gen: 1 | 2 | 3; a: Tip; b: Tip };
type CrackMeta = { id: number; gen: 1 | 2 | 3; initialDA: string; initialDB: string };

/** mulberry32 — small, fast, deterministic given a seed */
function mulberry32(seed: number) {
  let s = seed | 0;
  return () => {
    s = (s + 0x6d2b79f5) | 0;
    let t = Math.imul(s ^ (s >>> 15), 1 | s);
    t = (t + Math.imul(t ^ (t >>> 7), 61 | t)) ^ t;
    return ((t ^ (t >>> 14)) >>> 0) / 4294967296;
  };
}

const easeInOutCubic = (t: number) => (t < 0.5 ? 4 * t * t * t : 1 - Math.pow(-2 * t + 2, 3) / 2);

function pointsToPath(pts: Pt[]): string {
  if (pts.length === 0) return "";
  if (pts.length === 1) return `M ${pts[0].x.toFixed(1)} ${pts[0].y.toFixed(1)}`;
  return `M ${pts.map((p) => `${p.x.toFixed(1)} ${p.y.toFixed(1)}`).join(" L ")}`;
}

/** committed points plus the current in-progress segment's live, interpolated tip — this is what gets drawn every frame */
function renderPoints(tip: Tip): Pt[] {
  if (tip.done) return tip.committed;
  const from = tip.committed[tip.committed.length - 1];
  const t = tip.segLen > 0 ? tip.progressPx / tip.segLen : 0;
  const live: Pt = {
    x: from.x + Math.cos(tip.heading) * tip.segLen * t,
    y: from.y + Math.sin(tip.heading) * tip.segLen * t,
  };
  return [...tip.committed, live];
}

function makeTip(x: number, y: number, heading: number, rng: () => number): Tip {
  return {
    committed: [{ x, y }],
    heading,
    segLen: SEG_MIN + rng() * (SEG_MAX - SEG_MIN),
    progressPx: 0,
    done: false,
    recentCells: [],
  };
}

/** Occupancy grid: one Uint8Array cell per CELLxCELL px, 0 = empty. */
class Grid {
  cols: number;
  rows: number;
  cells: Uint8Array;
  constructor(w: number, h: number) {
    this.cols = Math.max(1, Math.ceil(w / CELL));
    this.rows = Math.max(1, Math.ceil(h / CELL));
    this.cells = new Uint8Array(this.cols * this.rows);
  }
  idx(cx: number, cy: number) {
    return cy * this.cols + cx;
  }
  mark(x: number, y: number, tip: Tip) {
    const cx = Math.min(this.cols - 1, Math.max(0, Math.floor(x / CELL)));
    const cy = Math.min(this.rows - 1, Math.max(0, Math.floor(y / CELL)));
    const i = this.idx(cx, cy);
    this.cells[i] = 1;
    tip.recentCells.push(i);
    if (tip.recentCells.length > 8) tip.recentCells.shift();
  }
  /** true if (x,y)'s 3x3 cell neighbourhood holds ink not owned by `tip`'s recent trail */
  collides(x: number, y: number, tip: Tip): boolean {
    const cx = Math.floor(x / CELL);
    const cy = Math.floor(y / CELL);
    for (let dy = -1; dy <= 1; dy++) {
      for (let dx = -1; dx <= 1; dx++) {
        const nx = cx + dx;
        const ny = cy + dy;
        if (nx < 0 || ny < 0 || nx >= this.cols || ny >= this.rows) continue;
        const i = this.idx(nx, ny);
        if (this.cells[i] !== 0 && !tip.recentCells.includes(i)) return true;
      }
    }
    return false;
  }
}

type Region = { cells: number[]; area: number };

function findRegions(grid: Grid): Region[] {
  const { cols, rows, cells } = grid;
  const visited = new Uint8Array(cols * rows);
  const regions: Region[] = [];
  const stack: number[] = [];
  for (let start = 0; start < cells.length; start++) {
    if (cells[start] !== 0 || visited[start]) continue;
    const regionCells: number[] = [];
    stack.length = 0;
    stack.push(start);
    visited[start] = 1;
    while (stack.length) {
      const i = stack.pop()!;
      regionCells.push(i);
      const cx = i % cols;
      const cy = (i / cols) | 0;
      const neighbours = [
        [cx - 1, cy],
        [cx + 1, cy],
        [cx, cy - 1],
        [cx, cy + 1],
      ];
      for (const [nx, ny] of neighbours) {
        if (nx < 0 || ny < 0 || nx >= cols || ny >= rows) continue;
        const ni = ny * cols + nx;
        if (visited[ni] || cells[ni] !== 0) continue;
        visited[ni] = 1;
        stack.push(ni);
      }
    }
    regions.push({ cells: regionCells, area: regionCells.length * CELL * CELL });
  }
  return regions;
}

/** picks a cell biased toward a region's interior: sample a few candidates, keep the one with the most empty neighbours */
function pickSeedCell(region: Region, grid: Grid, rng: () => number): { x: number; y: number } {
  const tries = Math.min(8, region.cells.length);
  let best = region.cells[Math.floor(rng() * region.cells.length)];
  let bestScore = -1;
  for (let t = 0; t < tries; t++) {
    const i = region.cells[Math.floor(rng() * region.cells.length)];
    const cx = i % grid.cols;
    const cy = (i / grid.cols) | 0;
    let score = 0;
    for (const [dx, dy] of [
      [-1, 0],
      [1, 0],
      [0, -1],
      [0, 1],
    ]) {
      const nx = cx + dx;
      const ny = cy + dy;
      if (nx < 0 || ny < 0 || nx >= grid.cols || ny >= grid.rows) continue;
      if (grid.cells[ny * grid.cols + nx] === 0) score++;
    }
    if (score > bestScore) {
      bestScore = score;
      best = i;
    }
  }
  const cx = best % grid.cols;
  const cy = (best / grid.cols) | 0;
  return { x: cx * CELL + CELL / 2, y: cy * CELL + CELL / 2 };
}

/**
 * advances one tip by dtMs of simulated growth against the shared grid.
 * The in-progress segment's `progressPx` moves by real elapsed distance
 * every call (never snapped straight to a segment boundary), so a caller
 * repainting every rAF frame sees the path glide continuously at
 * SPEED_PX_S rather than jump in SEG_MIN..SEG_MAX chunks every ~70-110ms.
 * A segment is only collision/edge-tested — and only then can it terminate
 * the tip at a T-junction — the instant progressPx reaches segLen.
 */
function growTip(tip: Tip, dtMs: number, grid: Grid, w: number, h: number) {
  if (tip.done) return;
  let dt = dtMs;
  let guard = 0;
  while (dt > 0 && !tip.done && guard < 40) {
    guard++;
    const remainingSegPx = tip.segLen - tip.progressPx;
    const availablePx = (SPEED_PX_S / 1000) * dt;

    if (availablePx < remainingSegPx) {
      tip.progressPx += availablePx;
      dt = 0;
      break;
    }

    // enough distance this tick to finish the in-progress segment — spend
    // only the ms it actually needed and carry the rest into the next one
    const neededMs = (remainingSegPx / SPEED_PX_S) * 1000;
    dt -= neededMs;

    const from = tip.committed[tip.committed.length - 1];
    const to = {
      x: from.x + Math.cos(tip.heading) * tip.segLen,
      y: from.y + Math.sin(tip.heading) * tip.segLen,
    };

    if (to.x < 0 || to.x > w || to.y < 0 || to.y > h) {
      const cx = Math.min(w, Math.max(0, to.x));
      const cy = Math.min(h, Math.max(0, to.y));
      tip.committed.push({ x: cx, y: cy });
      grid.mark(cx, cy, tip);
      tip.done = true;
      break;
    }

    // sample a few points along the finished segment: earliest collision
    // wins, and every clean sample gets marked so the raster has no gap
    // another crack could slip through
    let hit = false;
    const samples = 3;
    for (let s = 1; s <= samples; s++) {
      const t = s / samples;
      const px = from.x + (to.x - from.x) * t;
      const py = from.y + (to.y - from.y) * t;
      if (grid.collides(px, py, tip)) {
        tip.committed.push({ x: px, y: py });
        grid.mark(px, py, tip);
        tip.done = true;
        hit = true;
        break;
      }
      grid.mark(px, py, tip);
    }
    if (hit) break;

    tip.committed.push(to);
    tip.heading += (rngFor() - 0.5) * 2 * TURN_NOISE;
    tip.segLen = SEG_MIN + rngFor() * (SEG_MAX - SEG_MIN);
    tip.progressPx = 0;
  }
}

// a tiny module-level RNG stream (reseeded whenever the component's own
// seeded rng is (re)created) avoids threading the seeded generator through
// every tip while still keeping the whole pattern reproducible from `seed`
let sharedRng: () => number = mulberry32(1);
function rngFor() {
  return sharedRng();
}

function spawnCrack(id: number, gen: 1 | 2 | 3, x: number, y: number, rng: () => number): CrackData {
  const heading = rng() * Math.PI * 2;
  const a = makeTip(x, y, heading, rng);
  const b = makeTip(x, y, heading + Math.PI + (rng() - 0.5) * 0.6, rng);
  return { id, gen, a, b };
}

export function CrackPolygonOrder({ seed, className = "" }: CrackPolygonOrderProps) {
  const wrapRef = useRef<HTMLDivElement>(null);
  const gRef = useRef<SVGGElement>(null);
  const pathRefs = useRef<Map<number, { a: SVGPathElement | null; b: SVGPathElement | null }>>(new Map());

  const [dims, setDims] = useState({ w: 0, h: 0 });
  const [cracks, setCracks] = useState<CrackMeta[]>([]);
  const [reduced, setReduced] = useState(false);

  const gridRef = useRef<Grid | null>(null);
  const cracksMapRef = useRef<Map<number, CrackData>>(new Map());
  const cracksDirtyRef = useRef(false);
  const phaseRef = useRef<Phase>("primary");
  const phaseTimerRef = useRef(0);
  const nextIdRef = useRef(0);
  const rngRef = useRef<() => number>(mulberry32(1));
  const rafRef = useRef<number | null>(null);
  const lastTsRef = useRef<number | null>(null);
  const visibleRef = useRef(true);
  const initializedRef = useRef(false);

  useEffect(() => {
    setReduced(window.matchMedia("(prefers-reduced-motion: reduce)").matches);
  }, []);

  // -- resize: measure the panel, (re)build the grid at that size --
  useEffect(() => {
    const el = wrapRef.current;
    if (!el) return;
    let debounce: ReturnType<typeof setTimeout> | null = null;
    const ro = new ResizeObserver((entries) => {
      const entry = entries[0];
      if (!entry) return;
      const { width, height } = entry.contentRect;
      if (width < 4 || height < 4) return;
      if (debounce) clearTimeout(debounce);
      debounce = setTimeout(() => {
        const sameSize =
          initializedRef.current && Math.abs(width - dims.w) < 4 && Math.abs(height - dims.h) < 4;
        if (sameSize) return;
        setDims({ w: Math.round(width), h: Math.round(height) });
      }, 120);
    });
    ro.observe(el);
    return () => {
      ro.disconnect();
      if (debounce) clearTimeout(debounce);
    };
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, []);

  // -- visibility gate: pause the growth loop off-screen, resume in place --
  useEffect(() => {
    const el = wrapRef.current;
    if (!el) return;
    const io = new IntersectionObserver((entries) => {
      visibleRef.current = entries.some((e) => e.isIntersecting);
    });
    io.observe(el);
    return () => io.disconnect();
  }, []);

  const syncCracksToDom = () => {
    if (!cracksDirtyRef.current) return;
    cracksDirtyRef.current = false;
    const meta: CrackMeta[] = Array.from(cracksMapRef.current.values()).map((c) => ({
      id: c.id,
      gen: c.gen,
      initialDA: pointsToPath(renderPoints(c.a)),
      initialDB: pointsToPath(renderPoints(c.b)),
    }));
    setCracks(meta);
  };

  const beginGeneration = (gen: 1 | 2 | 3, w: number, h: number, rng: () => number) => {
    const grid = gridRef.current;
    if (!grid) return;
    if (gen === 1) {
      const count = MIN_PRIMARY + Math.floor(rng() * (MAX_PRIMARY - MIN_PRIMARY + 1));
      for (let i = 0; i < count; i++) {
        const x = rng() * w;
        const y = rng() * h;
        const crack = spawnCrack(nextIdRef.current++, 1, x, y, rng);
        cracksMapRef.current.set(crack.id, crack);
      }
    } else {
      const regions = findRegions(grid).filter((r) => r.area >= MIN_REGION_AREA_PX2);
      regions.sort((a, b) => b.area - a.area);
      const take = Math.ceil(regions.length * LARGEST_FRACTION);
      for (const region of regions.slice(0, take)) {
        const seedPt = pickSeedCell(region, grid, rng);
        const crack = spawnCrack(nextIdRef.current++, gen, seedPt.x, seedPt.y, rng);
        cracksMapRef.current.set(crack.id, crack);
      }
    }
    cracksDirtyRef.current = true;
  };

  const resetCycle = (w: number, h: number) => {
    gridRef.current = new Grid(w, h);
    cracksMapRef.current.clear();
    cracksDirtyRef.current = true;
    phaseRef.current = "primary";
    phaseTimerRef.current = 0;
    if (gRef.current) gRef.current.style.opacity = "1";
    beginGeneration(1, w, h, rngRef.current);
  };

  /** advances the whole simulation by dtMs: growth, phase transitions, generation spawns */
  const tick = (dtMs: number, w: number, h: number) => {
    const grid = gridRef.current;
    if (!grid) return;
    const rng = rngRef.current;
    const phase = phaseRef.current;

    const stepGen = (gen: 1 | 2 | 3) => {
      let allDone = true;
      for (const c of cracksMapRef.current.values()) {
        if (c.gen !== gen) continue;
        growTip(c.a, dtMs, grid, w, h);
        growTip(c.b, dtMs, grid, w, h);
        if (!(c.a.done && c.b.done)) allDone = false;
      }
      return allDone;
    };

    switch (phase) {
      case "primary":
        if (stepGen(1)) {
          phaseRef.current = "secondary-wait";
          phaseTimerRef.current = 0;
        }
        break;
      case "secondary-wait":
        phaseTimerRef.current += dtMs;
        if (phaseTimerRef.current >= GEN_PAUSE_MS) {
          beginGeneration(2, w, h, rng);
          phaseRef.current = "secondary";
        }
        break;
      case "secondary":
        if (stepGen(2)) {
          phaseRef.current = "tertiary-wait";
          phaseTimerRef.current = 0;
        }
        break;
      case "tertiary-wait":
        phaseTimerRef.current += dtMs;
        if (phaseTimerRef.current >= GEN_PAUSE_MS) {
          beginGeneration(3, w, h, rng);
          phaseRef.current = "tertiary";
        }
        break;
      case "tertiary":
        if (stepGen(3)) {
          phaseRef.current = "hold";
          phaseTimerRef.current = 0;
        }
        break;
      case "hold":
        phaseTimerRef.current += dtMs;
        if (phaseTimerRef.current >= HOLD_MS) {
          phaseRef.current = "rewet";
          phaseTimerRef.current = 0;
        }
        break;
      case "rewet": {
        phaseTimerRef.current += dtMs;
        const t = Math.min(1, phaseTimerRef.current / REWET_MS);
        if (gRef.current) gRef.current.style.opacity = String(1 - easeInOutCubic(t));
        if (phaseTimerRef.current >= REWET_MS) {
          gridRef.current = new Grid(w, h);
          cracksMapRef.current.clear();
          cracksDirtyRef.current = true;
          if (gRef.current) gRef.current.style.opacity = "1";
          phaseRef.current = "cooldown";
          phaseTimerRef.current = 0;
        }
        break;
      }
      case "cooldown":
        phaseTimerRef.current += dtMs;
        if (phaseTimerRef.current >= COOLDOWN_MS) {
          phaseRef.current = "primary";
          phaseTimerRef.current = 0;
          beginGeneration(1, w, h, rng);
        }
        break;
    }
  };

  // -- write per-frame growth straight to the DOM, bypassing React --
  const paintFrame = () => {
    for (const c of cracksMapRef.current.values()) {
      const refs = pathRefs.current.get(c.id);
      if (!refs) continue;
      if (refs.a) refs.a.setAttribute("d", pointsToPath(renderPoints(c.a)));
      if (refs.b) refs.b.setAttribute("d", pointsToPath(renderPoints(c.b)));
    }
  };

  // -- init + main loop: waits for a real measurement, then either freezes
  // at the mature frame (reduced motion) or fast-forwards into a random
  // mid-cycle point before handing off to a live rAF loop --
  useEffect(() => {
    if (dims.w < 4 || dims.h < 4) return;
    if (window.matchMedia("(prefers-reduced-motion: reduce)").matches) {
      rngRef.current = mulberry32(seed ?? Math.floor(Math.random() * 2 ** 31));
      sharedRng = rngRef.current;
      gridRef.current = new Grid(dims.w, dims.h);
      cracksMapRef.current.clear();
      phaseRef.current = "primary";
      phaseTimerRef.current = 0;
      beginGeneration(1, dims.w, dims.h, rngRef.current);
      let iters = 0;
      while ((phaseRef.current as Phase) !== "hold" && iters < MAX_FASTFWD_ITERS) {
        tick(FASTFWD_STEP_MS, dims.w, dims.h);
        iters++;
      }
      syncCracksToDom();
      initializedRef.current = true;
      return;
    }

    if (!initializedRef.current) {
      rngRef.current = mulberry32(seed ?? Math.floor(Math.random() * 2 ** 31));
      sharedRng = rngRef.current;
      resetCycle(dims.w, dims.h);
      const leadInMs = 1500 + rngRef.current() * 9000;
      let simulated = 0;
      let iters = 0;
      while (simulated < leadInMs && iters < MAX_FASTFWD_ITERS) {
        tick(FASTFWD_STEP_MS, dims.w, dims.h);
        simulated += FASTFWD_STEP_MS;
        iters++;
      }
      syncCracksToDom();
      paintFrame();
      initializedRef.current = true;
    } else {
      // a real resize after init: rebuild fresh at the new size, no lead-in
      resetCycle(dims.w, dims.h);
      syncCracksToDom();
      paintFrame();
    }

    const loop = (ts: number) => {
      rafRef.current = requestAnimationFrame(loop);
      if (!visibleRef.current) {
        lastTsRef.current = ts;
        return;
      }
      const last = lastTsRef.current ?? ts;
      const dt = Math.min(MAX_DT_MS, ts - last);
      lastTsRef.current = ts;
      if (dt <= 0) return;
      tick(dt, dims.w, dims.h);
      syncCracksToDom();
      paintFrame();
    };
    rafRef.current = requestAnimationFrame(loop);
    return () => {
      if (rafRef.current !== null) cancelAnimationFrame(rafRef.current);
      lastTsRef.current = null;
    };
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, [dims.w, dims.h, seed]);

  return (
    <div
      ref={wrapRef}
      data-crack-polygon-order
      aria-hidden
      className={`ns-crack-polygon-order relative h-full w-full overflow-hidden ${className}`}
    >
      <svg
        width="100%"
        height="100%"
        viewBox={`0 0 ${Math.max(1, dims.w)} ${Math.max(1, dims.h)}`}
        preserveAspectRatio="none"
        className="block"
      >
        <rect x={0} y={0} width={dims.w} height={dims.h} fill="var(--background)" />
        <g ref={gRef}>
          {cracks.map((c) => (
            <g key={c.id}>
              <path
                ref={(el) => {
                  const entry = pathRefs.current.get(c.id) ?? { a: null, b: null };
                  entry.a = el;
                  pathRefs.current.set(c.id, entry);
                }}
                d={c.initialDA}
                fill="none"
                stroke="var(--foreground)"
                strokeWidth={STROKE_W}
                strokeLinecap="round"
                strokeLinejoin="round"
                vectorEffect="non-scaling-stroke"
              />
              <path
                ref={(el) => {
                  const entry = pathRefs.current.get(c.id) ?? { a: null, b: null };
                  entry.b = el;
                  pathRefs.current.set(c.id, entry);
                }}
                d={c.initialDB}
                fill="none"
                stroke="var(--foreground)"
                strokeWidth={STROKE_W}
                strokeLinecap="round"
                strokeLinejoin="round"
                vectorEffect="non-scaling-stroke"
              />
            </g>
          ))}
        </g>
      </svg>
    </div>
  );
}
Build spec

Build <CrackPolygonOrder seed? className?> as a full-bleed fill for a card-scale panel (give it explicit height via className/a wrapper; the component itself is h-full w-full). STRUCTURE: a plain <div data-crack-polygon-order aria-hidden> holding one inline <svg viewBox="0 0 {measuredWidth} {measuredHeight}" preserveAspectRatio="none"> whose viewBox is set to the container's own measured CSS pixel size via a debounced ResizeObserver (120ms) — unlike an abstract stretched viewBox, 1 viewBox unit here equals 1 on-screen px, because every real-numbers threshold in the mechanic (90px/s growth, a ~2px T-junction test, a ~900px^2 minimum polygon area) is stated in real panel pixels. A <rect> filled var(--background) sits under everything; every crack stroke is var(--foreground) at 1.5px with vector-effect="non-scaling-stroke" — never var(--border), which falls below the light-theme legibility floor at hairline width, and never any per-polygon fill (the tessellation must read from crack lines alone, not shaded cell faces). SIMULATION, not a pre-computed reveal: a Uint8Array occupancy grid at 2px cells (Grid class) records where ink already exists. Each crack is a single nucleation point growing TWO tips outward in roughly opposite headings (heading and heading+PI, +-~17deg jitter) — this is what makes primary cracks read as a widely-spaced network and what makes secondary/tertiary cracks visibly bisect the cell they were born in rather than doodling from one wall. A tip advances by discrete 3-5px segments at a rate that nets 90px/s, turning +-12deg per segment (seeded per-tip via a module-level `sharedRng` stream so the whole pattern is reproducible from one `seed`), and samples 3 points along every new segment against the grid's 3x3-cell neighbourhood (skipping the tip's own last 8 marked cells so it can't immediately T-junction into itself) — the instant a sample lands within that ~2px neighbourhood of ANY other ink, growth stops exactly there: the T-junction, and the reason later cracks can never be seen crossing an earlier one. Every generation is driven by an explicit phase machine (primary -> secondary-wait -> secondary -> tertiary-wait -> tertiary -> hold -> rewet -> cooldown -> primary...): primary spawns 4-7 nucleation points scattered uniformly at t=0; once every primary tip is done (edge or T-junction), a 900ms wait elapses, then a flood fill (findRegions, 4-connected BFS over empty grid cells) finds every still-open polygon, regions under 130px^2 are dropped, the rest sorted by area descending, and cracks nucleate inside the top ceil(60%) of what's left — same 900ms-wait-then-nucleate step repeats once more for the third generation, then the tiling holds for 4000ms untouched. REWET: over the following 2500ms the whole crack <g>'s opacity eases 1 -> 0 on an ease-in-out-cubic computed per frame (not a CSS transition, so it stays exactly in step with the same rAF clock driving growth) — the panel then clears (grid reset, crack map emptied) as a genuine return to blank, not just an invisible-but-still-there state, holds blank for a 600ms cooldown, and the next cycle's primaries begin. Full period is non-uniform by design, roughly 14-17s (6-8s to reach maturity + 4s hold + 2.5s rewet + 0.6s cooldown). PERFORMANCE: geometry writes for already-mounted <path> elements happen via direct refs.current.setAttribute('d', ...) inside the rAF callback, bypassing React entirely for per-frame growth; React state (the `cracks` array, id+gen+initialD only) only updates on the rare event a new generation's crack set changes (three times a cycle) or the panel resizes, gated by a `cracksDirtyRef` flag so a mid-fast-forward burst of spawns collapses into a single re-render. RESUME SAFETY: an IntersectionObserver sets a `visibleRef` the rAF loop checks before ticking (off-screen panels stop simulating but keep scheduling frames so they resume instantly back in view), and every dt fed to the simulation is clamped to 50ms regardless of source, so a backgrounded tab or a slow resize can never hand the sim a multi-second jump that would skip whole generations invisibly. THE t0 REQUIREMENT: a brand-new mount never starts from an empty panel — after the first valid ResizeObserver measurement, the exact same tick() function used by the live loop is called synchronously in a tight while-loop on a fixed 16ms clock for a random 1500-10500ms of simulated time (capped at 4000 iterations) BEFORE the real rAF loop ever starts, landing every visit at an arbitrary, already-partially-tiled mid-cycle frame; a real resize after that initial mount rebuilds the grid at the new size and restarts cleanly at a fresh primary generation (no repeated lead-in — a resize is not a new visit). REDUCED MOTION: on mount, if prefers-reduced-motion matches, skip the rAF loop entirely and instead run that same tick() function synchronously and repeatedly until phaseRef reaches exactly 'hold' (capped at 4000 iterations), then stop for good — the panel renders once, fully tiled, all three generations' T-junctions visible simultaneously, and never rewets. Zero dependencies, no canvas — SVG+DOM+CSS only, and the crack RNG (mulberry32, seeded from the optional `seed` prop or Math.random() per mount) is the only source of variation. DIFFERS FROM craze-rule: that component draws ONE branching fracture line across a fixed 40px divider strip, once, via IntersectionObserver, and never subdivides a cell — no generation hierarchy, no polygon tessellation, no ongoing simulation once armed. DIFFERS FROM compare-crack-seam: that fracture is a single draggable Voronoi-cell seam that IS a before/after image divider, spawning micro-fissures on drag velocity; crack-polygon-order has no pointer interaction, no comparison images, and its cracks free-branch across the whole panel on their own clock rather than tracking a cursor.

Props

PropTypeDefaultDescription
seed?numberseeds the crack RNG; omit for a fresh, unrepeatable tiling every mount
className?string