Pinned scroll story — the viewport becomes a geological core cross-section and scroll drives a drill bit descending through procedurally banded strata, with content panels crossfading per band and a mono HUD ticking depth.
npx shadcn add https://design.helpmarq.com /r/scroll-story-strata.jsonregistry/loud/scroll-story-strata/component.tsx"use client";
import { useEffect, useId, useMemo, useRef, useState } from "react";
// ---------------------------------------------------------------------------
// CoreSampleScroll — pinned scroll story. The viewport becomes a geological
// core cross-section: a 250vh track wraps a sticky 100vh stage, and scroll
// drives a drill bit descending through procedurally banded strata while
// content panels crossfade per band and a mono HUD ticks depth in meters.
// Strata grain (2-octave value noise, 3px cells, quantized to 3 gray levels
// between --border and --muted, per-band thresholds) is rendered ONCE per
// resize/theme into an offscreen band atlas and blitted per frame. Exactly
// one accent vein — a 1.5px --accent polyline at a fixed depth in band 4.
// Direct-DOM rAF, refs only on the hot path; the loop sleeps when the eased
// depth is within epsilon of target and the chip spray is empty.
// ---------------------------------------------------------------------------
export interface CoreSampleBand {
id: string;
label: string;
title: string;
body: string;
spec: string;
}
const DEFAULT_BANDS: CoreSampleBand[] = [
{
id: "interface",
label: "Interface",
title: "Surface layer",
body: "Everything the user touches renders here: design tokens, optimistic state, and a 60fps interaction budget. Nothing below this band is visible, but every band below shapes how it feels.",
spec: "react 19 · edge-rendered · p75 INP 84ms",
},
{
id: "api",
label: "API",
title: "Contract stratum",
body: "A thin, typed boundary where every request is validated, shaped, and signed. Breaking changes surface here as schema drift long before they reach a client.",
spec: "graphql + rest · 214 endpoints · p99 121ms",
},
{
id: "engine",
label: "Engine",
title: "Compute band",
body: "Schedulers, queues, and the rules engine burn through about two million jobs a day. Backpressure is absorbed inside this band, so spikes never reach the surface.",
spec: "12 workers · 2.1M jobs/day · retry ×3",
},
{
id: "storage",
label: "Storage",
title: "Sedimented state",
body: "Every event since day zero is compacted into columnar layers — cheap to scan, impossible to lose. The accent vein marks the write-ahead log, the one seam everything above depends on.",
spec: "wal + columnar · 41TB · rpo 0s",
},
{
id: "bedrock",
label: "Bedrock",
title: "Bedrock / infra",
body: "Machines, networks, and the failure domains everything above stands on. It moves rarely, and when it does, the whole column feels it.",
spec: "3 regions · 9 AZs · chaos-tested weekly",
},
];
// band thickness fractions — each within 12–28% of stage height, sum = 1
const FRACTIONS_5 = [0.17, 0.21, 0.15, 0.27, 0.2];
function bandFractions(n: number): number[] {
if (n === 5) return FRACTIONS_5;
return new Array(n).fill(1 / Math.max(1, n));
}
type Vec3 = [number, number, number];
function parseColor(raw: string): Vec3 | null {
const s = raw.trim();
if (s.startsWith("#")) {
const hex = s.slice(1);
if (hex.length === 3) {
const r = parseInt(hex[0] + hex[0], 16);
const g = parseInt(hex[1] + hex[1], 16);
const b = parseInt(hex[2] + hex[2], 16);
return Number.isNaN(r + g + b) ? null : [r, g, b];
}
if (hex.length >= 6) {
const r = parseInt(hex.slice(0, 2), 16);
const g = parseInt(hex.slice(2, 4), 16);
const b = parseInt(hex.slice(4, 6), 16);
return Number.isNaN(r + g + b) ? null : [r, g, b];
}
return null;
}
const m = s.match(/rgba?\(\s*([\d.]+)[,\s]+([\d.]+)[,\s]+([\d.]+)/);
return m ? [Number(m[1]), Number(m[2]), Number(m[3])] : null;
}
function mix(a: Vec3, b: Vec3, t: number): Vec3 {
return [
Math.round(a[0] + (b[0] - a[0]) * t),
Math.round(a[1] + (b[1] - a[1]) * t),
Math.round(a[2] + (b[2] - a[2]) * t),
];
}
function css(c: Vec3): string {
return `rgb(${c[0]},${c[1]},${c[2]})`;
}
// deterministic lattice hash → [0,1)
function hash2(x: number, y: number, s: number): number {
let h = (x * 374761393 + y * 668265263 + s * 1442695041) | 0;
h = Math.imul(h ^ (h >>> 13), 1274126177);
return ((h ^ (h >>> 16)) >>> 0) / 4294967296;
}
function smooth(t: number): number {
return t * t * (3 - 2 * t);
}
// single-octave value noise on an integer lattice
function vnoise(x: number, y: number, s: number): number {
const ix = Math.floor(x);
const iy = Math.floor(y);
const fx = x - ix;
const fy = y - iy;
const a = hash2(ix, iy, s);
const b = hash2(ix + 1, iy, s);
const c = hash2(ix, iy + 1, s);
const d = hash2(ix + 1, iy + 1, s);
const u = smooth(fx);
const v = smooth(fy);
return a + (b - a) * u + (c - a) * v + (a - b - c + d) * u * v;
}
function mulberry32(a: number) {
return () => {
a |= 0;
a = (a + 0x6d2b79f5) | 0;
let t = Math.imul(a ^ (a >>> 15), 1 | a);
t = (t + Math.imul(t ^ (t >>> 7), 61 | t)) ^ t;
return ((t ^ (t >>> 14)) >>> 0) / 4294967296;
};
}
const CELL = 3; // px — grain cell size
// per-band [t1, t2] quantization thresholds over the 2-octave noise — tuned so
// each stratum has a visibly different dark/mid/light balance
const THRESHOLDS: readonly [number, number][] = [
[0.46, 0.74], // dense dark topsoil
[0.3, 0.52], // light, porous
[0.5, 0.62], // mid-heavy
[0.36, 0.68], // balanced — the vein band
[0.42, 0.5], // light-flooded bedrock
];
const EASE_RATE = 8; // depth += (target-depth)*(1-exp(-dt*8)) ≈ 120ms lag
const SETTLE_MS = 250; // forced-settle deadline after the last scroll event
const EPS = 0.1; // px — sleep epsilon on |depth - target|
const CHIP_LIFE = 400; // ms
const CHIP_BATCH = 12; // particles per emission
const CHIP_CAP = 120;
const EMIT_EVERY = 0.09; // s between 12-particle batches while cutting
const VEL_MIN = 2; // px/s — below this the bit is "parked", no spray
const MIN_TIP = 16; // scene px — resting sliver so the bit tip clears the top edge at rest
type Chip = { x: number; y: number; vx: number; vy: number; born: number };
export function CoreSampleScroll({
bands = DEFAULT_BANDS,
totalDepth = 128,
trackVh = 250,
className = "",
}: {
/** strata content, top → bottom; band 4 (index 3) carries the accent vein */
bands?: CoreSampleBand[];
/** depth in meters the HUD reads at full scroll */
totalDepth?: number;
/** scroll-track height in vh; the sticky stage is always 100vh */
trackVh?: number;
className?: string;
}) {
const uid = useId();
const rootRef = useRef<HTMLElement>(null);
const stageRef = useRef<HTMLDivElement>(null);
const canvasRef = useRef<HTMLCanvasElement>(null);
const labelLayerRef = useRef<HTMLDivElement>(null);
const depthElRef = useRef<HTMLDivElement>(null);
const hoverRef = useRef(-1); // hovered/focused band index, -1 = none
const wakeRef = useRef<() => void>(() => {});
const [active, setActive] = useState(0);
const [reduced, setReduced] = useState(false);
const fractions = useMemo(() => bandFractions(bands.length), [bands.length]);
const starts = useMemo(() => {
const s: number[] = [];
let acc = 0;
for (const f of fractions) {
s.push(acc);
acc += f;
}
return s;
}, [fractions]);
const veinIdx = Math.min(3, bands.length - 1);
// scene is taller than the stage so the 0.15x parallax has strata to reveal
const overscan = reduced ? 1 : 1 + (0.15 * Math.max(0, trackVh - 100)) / 100;
useEffect(() => {
const mq = window.matchMedia("(prefers-reduced-motion: reduce)");
const on = () => setReduced(mq.matches);
on();
mq.addEventListener("change", on);
return () => mq.removeEventListener("change", on);
}, []);
useEffect(() => {
const root = rootRef.current;
const stage = stageRef.current;
const canvas = canvasRef.current;
if (!stage || !canvas) return;
const ctx = canvas.getContext("2d");
if (!ctx) return;
const atlas = document.createElement("canvas");
const actx = atlas.getContext("2d");
const grain = document.createElement("canvas");
const gctx = grain.getContext("2d");
if (!actx || !gctx) return;
const n = bands.length;
const rand = mulberry32(0xc02e);
// -- token inks: read at mount, re-derived live on theme flip ------------
let fg: Vec3 = [237, 237, 237];
let border: Vec3 = [46, 46, 46];
let muted: Vec3 = [143, 143, 143];
let accent: Vec3 = [0, 107, 255];
const derive = () => {
const cs = getComputedStyle(document.documentElement);
fg = parseColor(cs.getPropertyValue("--foreground")) ?? fg;
border = parseColor(cs.getPropertyValue("--border")) ?? border;
muted = parseColor(cs.getPropertyValue("--muted")) ?? muted;
accent = parseColor(cs.getPropertyValue("--accent")) ?? accent;
};
derive();
// -- sizing: explicit style.width/height — sticky inset does NOT size a
// replaced element; backing store is dpr-scaled separately ---------------
let w = 0;
let h = 0;
let dpr = 1;
let sized = false;
let sceneH = 0;
const bandAt = (f: number): number => {
for (let i = n - 1; i >= 0; i--) {
if (f >= (starts[i] ?? 0)) return i;
}
return 0;
};
// grain rendered ONCE per resize/theme into the band atlas, blitted per
// frame — never per-frame noise
const genAtlas = () => {
if (!sized) return;
atlas.width = Math.max(1, Math.round(w * dpr));
atlas.height = Math.max(1, Math.round(sceneH * dpr));
actx.setTransform(dpr, 0, 0, dpr, 0, 0);
const gw = Math.max(1, Math.ceil(w / CELL));
const gh = Math.max(1, Math.ceil(sceneH / CELL));
grain.width = gw;
grain.height = gh;
const img = gctx.createImageData(gw, gh);
const px = img.data;
// 3 gray levels interpolated between --border and --muted
const levels: Vec3[] = [border, mix(border, muted, 0.5), muted];
for (let gy = 0; gy < gh; gy++) {
const f = ((gy + 0.5) * CELL) / sceneH;
const b = bandAt(Math.min(0.9999, f));
// per-band quantization thresholds — each band gets a distinct
// dark/mid/light density so strata read apart in monochrome
const th = THRESHOLDS[b % THRESHOLDS.length] ?? THRESHOLDS[0]!;
const t1 = th[0];
const t2 = th[1];
for (let gx = 0; gx < gw; gx++) {
// 2-octave value noise, 3px cells
const nn =
0.65 * vnoise(gx / 5, gy / 5, b * 31 + 7) +
0.35 * vnoise(gx / 2.2, gy / 2.2, b * 31 + 19);
const c = levels[nn < t1 ? 0 : nn < t2 ? 1 : 2] ?? border;
const o = (gy * gw + gx) * 4;
px[o] = c[0];
px[o + 1] = c[1];
px[o + 2] = c[2];
px[o + 3] = 255;
}
}
gctx.putImageData(img, 0, 0);
actx.imageSmoothingEnabled = false;
actx.drawImage(grain, 0, 0, w, sceneH);
actx.imageSmoothingEnabled = true;
// hairline band boundaries
actx.strokeStyle = css(fg);
actx.globalAlpha = 0.22;
actx.lineWidth = 1;
for (let i = 1; i < n; i++) {
const y = Math.round((starts[i] ?? 0) * sceneH) + 0.5;
actx.beginPath();
actx.moveTo(0, y);
actx.lineTo(w, y);
actx.stroke();
}
actx.globalAlpha = 1;
// exactly one accent vein: 1.5px --accent polyline at a fixed depth
// inside band 4
const vy =
((starts[veinIdx] ?? 0) + (fractions[veinIdx] ?? 0.2) * 0.55) * sceneH;
actx.strokeStyle = css(accent);
actx.lineWidth = 1.5;
actx.lineJoin = "round";
actx.beginPath();
for (let x = 0; x <= w; x += 8) {
const yy = vy + (vnoise(x / 42, 3.7, 991) - 0.5) * 12;
if (x === 0) actx.moveTo(x, yy);
else actx.lineTo(x, yy);
}
actx.stroke();
};
const resize = () => {
const rect = stage.getBoundingClientRect();
if (rect.width < 2 || rect.height < 2) {
sized = false; // zero-size stage guard — loop no-ops until real
return;
}
w = rect.width;
h = rect.height;
sceneH = h * overscan;
dpr = Math.min(2, window.devicePixelRatio || 1);
canvas.style.width = `${w}px`;
canvas.style.height = `${h}px`;
canvas.width = Math.max(1, Math.round(w * dpr));
canvas.height = Math.max(1, Math.round(h * dpr));
ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
sized = true;
genAtlas();
};
resize();
// -- hot-path state: locals only ----------------------------------------
let raf = 0;
let last = 0;
let visible = true;
let tip = 0; // eased drill-tip depth, scene px
let lastScrollT = performance.now();
let emitAccum = 0;
let chips: Chip[] = []; // pruned array — never destination-in fades
let curBand = -1;
// one frame: explicit full clear of the dynamic layer, atlas blit under a
// camera transform, then drill + chip spray in scene coords
const draw = (now: number) => {
ctx.clearRect(0, 0, w, h);
const camProg = sceneH > 0 ? Math.min(1, Math.max(0, tip / sceneH)) : 0;
const camY = -camProg * (sceneH - h); // 0.15x parallax travel
const s = 1 + 0.06 * camProg; // scale 1.00 → 1.06
ctx.save();
ctx.translate(w / 2, h / 2);
ctx.scale(s, s);
ctx.translate(-w / 2, -h / 2);
ctx.translate(0, camY);
ctx.drawImage(atlas, 0, 0, w, sceneH);
// hover/focus affordance: 1px --foreground top-rule on the band
const hov = hoverRef.current;
if (hov >= 0 && hov < n) {
const y = Math.round((starts[hov] ?? 0) * sceneH) + 0.5;
ctx.strokeStyle = css(fg);
ctx.lineWidth = 1;
ctx.beginPath();
ctx.moveTo(0, y);
ctx.lineTo(w, y);
ctx.stroke();
}
// drill string + mono-weight chevron bit, center-left column. Rendered
// at max(tip, MIN_TIP) so the bit tip always clears the top edge at
// rest (mount has tip=0) — physics/HUD still track the real `tip`.
const dx = Math.round(w * 0.34);
const drawTip = Math.max(MIN_TIP, tip);
ctx.strokeStyle = css(fg);
ctx.lineCap = "butt";
ctx.lineJoin = "miter";
ctx.lineWidth = 2;
ctx.beginPath();
ctx.moveTo(dx, -h);
ctx.lineTo(dx, drawTip - 9);
ctx.stroke();
ctx.beginPath();
ctx.moveTo(dx - 7, drawTip - 10);
ctx.lineTo(dx, drawTip);
ctx.lineTo(dx + 7, drawTip - 10);
ctx.stroke();
// depth ticks along the drilled hole
ctx.globalAlpha = 0.3;
ctx.lineWidth = 1;
for (let y = 56; y < drawTip - 12; y += 56) {
ctx.beginPath();
ctx.moveTo(dx - 5, y + 0.5);
ctx.lineTo(dx + 5, y + 0.5);
ctx.stroke();
}
ctx.globalAlpha = 1;
// chip spray
ctx.fillStyle = css(muted);
for (let i = 0; i < chips.length; i++) {
const c = chips[i];
if (!c) continue;
ctx.globalAlpha = Math.max(0, 1 - (now - c.born) / CHIP_LIFE);
ctx.fillRect(c.x - 1, c.y - 1, 2, 2);
}
ctx.globalAlpha = 1;
ctx.restore();
// labels ride the same camera via an offset transform on their layer —
// never absolute canvas/page coords
const layer = labelLayerRef.current;
if (layer) layer.style.transform = `scale(${s}) translateY(${camY}px)`;
};
// -- reduced motion: static labeled cross-section at full depth ----------
if (reduced) {
const renderStatic = () => {
if (!sized) return;
tip = sceneH;
chips = [];
draw(performance.now());
};
renderStatic();
wakeRef.current = renderStatic; // label hover redraws the top-rule
const ro = new ResizeObserver(() => {
resize();
renderStatic();
});
ro.observe(stage);
const mo = new MutationObserver(() => {
derive();
genAtlas(); // regenerate the band atlas for the new theme
renderStatic();
});
mo.observe(document.documentElement, {
attributes: true,
attributeFilter: ["class"],
});
return () => {
ro.disconnect();
mo.disconnect();
wakeRef.current = () => {};
};
}
const spawnChips = (dx: number, now: number) => {
for (let i = 0; i < CHIP_BATCH && chips.length < CHIP_CAP; i++) {
chips.push({
x: dx + (rand() - 0.5) * 12,
y: tip - 2,
vx: (rand() - 0.5) * 170,
vy: -(40 + rand() * 110),
born: now,
});
}
};
const loop = (now: number) => {
raf = 0;
if (!visible || !sized || !root) {
last = 0;
return; // paused offscreen / zero-size — observers wake us
}
const dt = last === 0 ? 1 / 60 : Math.min(0.05, (now - last) / 1000);
last = now;
// scroll progress from track geometry
const rrect = root.getBoundingClientRect();
const range = Math.max(1, rrect.height - window.innerHeight);
const prog = Math.min(1, Math.max(0, -rrect.top / range));
const target = prog * sceneH;
const prev = tip;
if (now - lastScrollT > SETTLE_MS) {
tip = target; // forced-settle deadline — the HUD never ticks forever
} else {
tip += (target - tip) * (1 - Math.exp(-dt * EASE_RATE));
}
const vel = (tip - prev) / dt;
// chip spray while the bit is cutting
const dx = Math.round(w * 0.34);
if (Math.abs(vel) > VEL_MIN) {
emitAccum += dt;
while (emitAccum >= EMIT_EVERY) {
emitAccum -= EMIT_EVERY;
spawnChips(dx, now);
}
} else {
emitAccum = 0;
}
// integrate + prune in place
let keep = 0;
for (let i = 0; i < chips.length; i++) {
const c = chips[i];
if (!c || now - c.born > CHIP_LIFE) continue;
c.vy += 520 * dt;
c.x += c.vx * dt;
c.y += c.vy * dt;
chips[keep++] = c;
}
chips.length = keep;
// band crossings drive the panel crossfade (rare setState, off the
// per-frame path otherwise)
const f = sceneH > 0 ? Math.min(0.9999, Math.max(0, tip / sceneH)) : 0;
const b = bandAt(f);
if (b !== curBand) {
curBand = b;
setActive(b);
}
// HUD depth readout — direct DOM, ticks every frame while moving
const depthEl = depthElRef.current;
if (depthEl) {
const meters =
(sceneH > 0 ? Math.min(1, Math.max(0, tip / sceneH)) : 0) * totalDepth;
depthEl.textContent = `${meters.toFixed(2).padStart(6, "0")} m`;
}
draw(now);
// sleep: settled within epsilon AND the chip list is empty
if (Math.abs(tip - target) < EPS && chips.length === 0) {
last = 0;
return;
}
raf = requestAnimationFrame(loop);
};
const wake = () => {
if (!raf) {
last = 0;
raf = requestAnimationFrame(loop);
}
};
wakeRef.current = wake;
const onScroll = () => {
lastScrollT = performance.now();
wake();
};
window.addEventListener("scroll", onScroll, { passive: true });
const ro = new ResizeObserver(() => {
resize();
wake();
});
ro.observe(stage);
const io = new IntersectionObserver((entries) => {
visible = entries[0]?.isIntersecting ?? true;
if (visible) wake();
});
io.observe(stage);
// live theme re-derive + atlas regeneration on documentElement class flip
const mo = new MutationObserver(() => {
derive();
genAtlas();
wake();
});
mo.observe(document.documentElement, {
attributes: true,
attributeFilter: ["class"],
});
const onVis = () => {
if (!document.hidden) wake();
};
document.addEventListener("visibilitychange", onVis);
wake();
return () => {
cancelAnimationFrame(raf);
raf = 0;
window.removeEventListener("scroll", onScroll);
ro.disconnect();
io.disconnect();
mo.disconnect();
document.removeEventListener("visibilitychange", onVis);
wakeRef.current = () => {};
};
}, [bands, fractions, starts, totalDepth, trackVh, overscan, veinIdx, reduced]);
const scrollToBand = (i: number) => {
if (reduced) {
document
.getElementById(`${uid}-panel-${i}`)
?.scrollIntoView({ block: "start" });
return;
}
const root = rootRef.current;
if (!root) return;
const rect = root.getBoundingClientRect();
const top = window.scrollY + rect.top;
const range = Math.max(1, root.offsetHeight - window.innerHeight);
const f = (starts[i] ?? 0) + (fractions[i] ?? 0) * 0.5;
window.scrollTo({ top: top + f * range, behavior: "smooth" });
};
const labelButtons = (
<div
ref={labelLayerRef}
aria-hidden={false}
className="pointer-events-none absolute inset-0 z-10"
style={{ transformOrigin: "50% 50%" }}
>
{bands.map((b, i) => (
<button
key={b.id}
type="button"
onClick={() => scrollToBand(i)}
onMouseEnter={() => {
hoverRef.current = i;
wakeRef.current();
}}
onMouseLeave={() => {
hoverRef.current = -1;
wakeRef.current();
}}
onFocus={() => {
hoverRef.current = i;
wakeRef.current();
}}
onBlur={() => {
hoverRef.current = -1;
wakeRef.current();
}}
className="pointer-events-auto absolute left-4 -translate-y-1/2 rounded-sm bg-background/60 px-2 py-1 font-mono text-[10px] uppercase tracking-[0.2em] text-muted transition-colors hover:text-foreground focus-visible:text-foreground focus-visible:outline focus-visible:outline-1 focus-visible:outline-offset-2 focus-visible:outline-accent"
style={{
top: `${((starts[i] ?? 0) + (fractions[i] ?? 0) / 2) * overscan * 100}%`,
}}
>
{String(i + 1).padStart(2, "0")} {b.label}
</button>
))}
</div>
);
const panelCard = (b: CoreSampleBand, i: number, extra = "") => (
<article
key={b.id}
id={reduced ? `${uid}-panel-${i}` : undefined}
className={`rounded-md border border-border bg-surface p-6 ${extra}`}
>
<p className="font-mono text-[10px] uppercase tracking-[0.25em] text-muted">
{String(i + 1).padStart(2, "0")} / {b.label}
</p>
<h3 className="mt-3 text-xl font-semibold text-foreground">{b.title}</h3>
<p className="mt-2 text-sm leading-relaxed text-muted">{b.body}</p>
<p className="mt-4 border-t border-border pt-3 font-mono text-xs text-muted">
{b.spec}
</p>
</article>
);
// -- reduced motion: no pin choreography — static cross-section + list ----
if (reduced) {
return (
<section className={`relative ${className}`}>
<div
ref={stageRef}
className="relative h-[80vh] min-h-[480px] overflow-hidden border-y border-border"
>
<canvas
ref={canvasRef}
aria-hidden
className="pointer-events-none absolute left-0 top-0"
/>
{labelButtons}
<div className="pointer-events-none absolute right-6 top-6 z-10 rounded-sm bg-background/60 p-3 text-right font-mono">
<p className="text-[10px] uppercase tracking-[0.25em] text-muted">
depth
</p>
<div className="mt-1 text-sm tabular-nums text-foreground">
{`${totalDepth.toFixed(2).padStart(6, "0")} m`}
</div>
<p className="mt-1 text-[10px] uppercase tracking-[0.2em] text-muted">
full core · {bands.length} bands
</p>
</div>
</div>
<div className="mx-auto mt-8 grid max-w-2xl gap-4 px-6">
{bands.map((b, i) => panelCard(b, i))}
</div>
</section>
);
}
return (
<section
ref={rootRef}
className={`relative ${className}`}
style={{ height: `${trackVh}vh` }}
>
<div
ref={stageRef}
className="sticky top-0 h-screen overflow-hidden border-y border-border"
>
<canvas
ref={canvasRef}
aria-hidden
className="pointer-events-none absolute left-0 top-0"
/>
{labelButtons}
{/* HUD — fixed stage corner, mono, ticks every frame while moving */}
<div className="pointer-events-none absolute right-6 top-6 z-10 rounded-sm bg-background/60 p-3 text-right font-mono">
<p className="text-[10px] uppercase tracking-[0.25em] text-muted">
depth
</p>
<div
ref={depthElRef}
className="mt-1 text-sm tabular-nums text-foreground"
>
000.00 m
</div>
<p
className={`mt-1 text-[10px] uppercase tracking-[0.2em] transition-colors ${
active === veinIdx ? "text-accent" : "text-muted"
}`}
>
{bands[active]?.label ?? ""} · {active + 1}/{bands.length}
</p>
</div>
{/* content panels — right column, crossfade + 12px slide per band */}
<div className="absolute right-[4%] top-1/2 z-10 w-[45%] max-w-[560px] -translate-y-1/2">
<div className="grid">
{bands.map((b, i) => (
<div
key={b.id}
className={`col-start-1 row-start-1 transition-[opacity,transform] duration-[350ms] ease-[cubic-bezier(0.22,1,0.36,1)] ${
i === active
? "translate-y-0 opacity-100"
: i < active
? "pointer-events-none -translate-y-3 opacity-0"
: "pointer-events-none translate-y-3 opacity-0"
}`}
aria-hidden={i !== active}
>
{panelCard(b, i)}
</div>
))}
</div>
</div>
</div>
</section>
);
}
a pinned scroll story where the viewport becomes a geological core sample and a drill bit descends through strata as DOM content panels crossfade per band; use for a long-form narrative with distinct content sections, not an abstract flythrough or a real-image reveal.
Build a pinned scroll story: a 250vh scroll track wrapping a position:sticky 100vh stage. One full-stage Canvas 2D layer draws a geological core cross-section: 5 strata bands (thickness 12-28% of stage height each), each filled with 2-octave value-noise grain (3px cells) quantized to 3 gray levels interpolated between --border and --muted, with slightly different quantization thresholds per band so bands read distinct in monochrome; exactly one accent vein — a 1.5px --accent polyline at a fixed depth inside band 4. Grain is rendered ONCE per resize/theme into an offscreen band atlas and blitted per frame — never per-frame noise. DRILL: center-left column with a mono-weight chevron bit and drill string stroked in --foreground plus faint depth ticks; depth target = scrollProgress * totalDepth and the rendered depth eases with depth += (target-depth)*(1-exp(-dt*8)) (~120ms lag), with a forced-settle deadline that snaps to target 250ms after the last scroll event so the HUD never ticks forever. While |depth velocity| > 2px/s the bit tip emits 12-particle chip-spray batches (life 400ms, gravity-integrated, pruned array with an explicit full clear per frame — no destination-in accumulation). CAMERA: the scene is overscanned ~1.22x and the canvas translates at 0.15x scroll delta plus a scale 1.00→1.06 across full progress; a DOM label layer rides the identical camera via an offset CSS transform (never absolute canvas/page coords). CONTENT: 5 DOM panels stacked in a right column (45% width, padded surface card rounded-md); when the eased depth crosses a band threshold the outgoing panel fades/slides -12px and the incoming fades/slides in from +12px over 350ms cubic-bezier(0.22,1,0.36,1). HUD: stage-corner font-mono depth readout in meters with 2 decimals updated via direct DOM every frame while moving; the band label gains --accent only while inside the vein band. INTERACTION: band labels along the left edge are focusable buttons that scroll the track to that band; hovering/focusing a label draws a 1px --foreground top-rule on its band in the canvas. REDUCED MOTION: no pin choreography — a static labeled cross-section rendered at full depth, panels as a normal stacked list, HUD frozen. ENGINEERING: all strata/grain/vein/HUD inks derived from getComputedStyle tokens at mount with a MutationObserver on documentElement class re-deriving live and regenerating the band atlas on theme flip; canvas sized via explicit style.width/height with a dpr-scaled backing store (sticky inset does not size a replaced element); rAF sleeps when |depth - target| < 0.1px and the chip list is empty; IntersectionObserver pauses the loop offscreen; zero-size stage guard; scroll listener passive and removed with all observers/rAF on unmount; no React state on the hot path.