Unknown-Pleasures ridgeline chart whose live history recedes into scrolling ambient noise terrain, dented gravitationally by the cursor.
npx shadcn add https://design.helpmarq.com /r/chart-ridgeline-terrain.jsonregistry/core/chart-ridgeline-terrain/component.tsx"use client";
import { useEffect, useRef } from "react";
// ---------------------------------------------------------------------------
// deterministic 2-octave value noise — no deps, stable across frames
// ---------------------------------------------------------------------------
function hash2(x: number, y: number) {
const n = Math.sin(x * 127.1 + y * 311.7) * 43758.5453123;
return n - Math.floor(n);
}
function vnoise(x: number, y: number) {
const xi = Math.floor(x);
const yi = Math.floor(y);
const xf = x - xi;
const yf = y - yi;
const u = xf * xf * (3 - 2 * xf);
const v = yf * yf * (3 - 2 * yf);
const a = hash2(xi, yi);
const b = hash2(xi + 1, yi);
const c = hash2(xi, yi + 1);
const d = hash2(xi + 1, yi + 1);
return a + (b - a) * u + (c - a) * v + (a - b - c + d) * u * v;
}
function noise2(x: number, y: number) {
return 0.65 * vnoise(x, y) + 0.35 * vnoise(x * 2.1 + 19.7, y * 2.1 + 7.3);
}
// cubic-bezier(0.22, 1, 0.36, 1) solved via Newton–Raphson
function makeBezier(p1x: number, p1y: number, p2x: number, p2y: number) {
const cx = 3 * p1x;
const bx = 3 * (p2x - p1x) - cx;
const ax = 1 - cx - bx;
const cy = 3 * p1y;
const by = 3 * (p2y - p1y) - cy;
const ay = 1 - cy - by;
const sampleX = (t: number) => ((ax * t + bx) * t + cx) * t;
const sampleY = (t: number) => ((ay * t + by) * t + cy) * t;
const slopeX = (t: number) => (3 * ax * t + 2 * bx) * t + cx;
return (x: number) => {
if (x <= 0) return 0;
if (x >= 1) return 1;
let t = x;
for (let i = 0; i < 6; i++) {
const s = slopeX(t);
if (Math.abs(s) < 1e-6) break;
t -= (sampleX(t) - x) / s;
}
return sampleY(Math.min(1, Math.max(0, t)));
};
}
const glideEase = makeBezier(0.22, 1, 0.36, 1);
const EMPTY: number[] = [];
function parseColor(raw: string): { r: number; g: number; b: number } | null {
const v = raw.trim();
if (v.startsWith("#")) {
const hex = v.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);
if (Number.isNaN(r) || Number.isNaN(g) || Number.isNaN(b)) return null;
return { 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);
if (Number.isNaN(r) || Number.isNaN(g) || Number.isNaN(b)) return null;
return { r, g, b };
}
return null;
}
const m = v.match(/rgba?\(\s*([\d.]+)[,\s]+([\d.]+)[,\s]+([\d.]+)/i);
if (!m) return null;
return { r: Number(m[1]), g: Number(m[2]), b: Number(m[3]) };
}
// ---------------------------------------------------------------------------
// SignalTerrain — Unknown-Pleasures ridgeline landscape where incoming data
// samples scroll forward as ridgelines (history recedes into ambient Perlin
// terrain) and the cursor dents the mesh gravitationally. Canvas 2D,
// painter's-algorithm occlusion, refs only — the rAF loop is the sole writer.
// ---------------------------------------------------------------------------
export function SignalTerrain({
series = EMPTY,
cols = 96,
rows = 40,
ambientAmplitude = 18,
dataAmplitude = 68,
glideMs = 600,
dentSigma = 90,
dentDepth = 26,
className = "h-96",
"aria-label": ariaLabel = "Live signal terrain",
}: {
/** data samples, oldest → newest; newest enters at the front row */
series?: number[];
/** vertices per ridgeline */
cols?: number;
/** ridgeline count (back to front) */
rows?: number;
/** ambient noise height in px at the front row */
ambientAmplitude?: number;
/** data peak height in px at the front row */
dataAmplitude?: number;
/** ms for the history to glide one row back after a push */
glideMs?: number;
/** gaussian radius of the cursor dent in screen px */
dentSigma?: number;
/** max cursor dent depth in px */
dentDepth?: number;
className?: string;
"aria-label"?: string;
}) {
const rootRef = useRef<HTMLDivElement>(null);
const canvasRef = useRef<HTMLCanvasElement>(null);
const seriesRef = useRef<number[]>(series);
const transRef = useRef(-1); // performance.now() of last push, -1 = settled
const reducedRef = useRef(false);
const drawRef = useRef<(() => void) | null>(null);
// series is data, not render state: push detection + redraw happen in refs
useEffect(() => {
const prev = seriesRef.current;
if (series === prev) return;
const pushed =
series.length !== prev.length ||
(series.length > 0 &&
series[series.length - 1] !== prev[prev.length - 1]);
seriesRef.current = series;
if (pushed) {
if (reducedRef.current) drawRef.current?.();
else transRef.current = performance.now();
}
}, [series]);
useEffect(() => {
const root = rootRef.current;
const canvas = canvasRef.current;
if (!root || !canvas) return;
const ctx = canvas.getContext("2d");
if (!ctx) return;
const reduced = window.matchMedia(
"(prefers-reduced-motion: reduce)"
).matches;
reducedRef.current = reduced;
let w = 0;
let h = 0;
let dpr = 1;
const resize = () => {
const rect = root.getBoundingClientRect();
w = rect.width;
h = rect.height;
dpr = Math.min(2, window.devicePixelRatio || 1);
canvas.width = Math.max(1, Math.round(w * dpr));
canvas.height = Math.max(1, Math.round(h * dpr));
};
resize();
// hot-path state — refs/locals only, never React state
let raf = 0;
let last = 0;
let visible = true;
let hovered = false;
let px = 0;
let py = 0;
let dentAmt = 0;
let dentVel = 0;
let smoothMax = 0;
const xs = new Float32Array(cols);
const ys = new Float32Array(cols);
const invCols = 1 / Math.max(1, cols - 1);
const invRows = 1 / Math.max(1, rows - 1);
const twoSigma2 = 2 * dentSigma * dentSigma;
// theme tokens — resolved from CSS custom properties, never hardcoded,
// so occlusion fill and ridgeline stroke read correctly in light or dark
let fillRGB = { r: 10, g: 10, b: 10 };
let strokeLowRGB = { r: 143, g: 143, b: 143 };
let strokeHighRGB = { r: 237, g: 237, b: 237 };
const deriveTokens = () => {
const cs = getComputedStyle(document.documentElement);
fillRGB =
parseColor(cs.getPropertyValue("--surface")) ??
parseColor(cs.getPropertyValue("--background")) ??
fillRGB;
strokeHighRGB = parseColor(cs.getPropertyValue("--foreground")) ?? strokeHighRGB;
strokeLowRGB = parseColor(cs.getPropertyValue("--muted")) ?? strokeLowRGB;
};
deriveTokens();
// theme can flip after mount (class toggle on <html>, no remount) — without
// this, fillRGB/strokeRGB stay frozen at whatever theme was active at mount
const themeObserver = new MutationObserver(() => {
deriveTokens();
if (reduced) drawRef.current?.();
});
themeObserver.observe(document.documentElement, {
attributes: true,
attributeFilter: ["class"],
});
const draw = (now: number) => {
ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
ctx.clearRect(0, 0, w, h);
const s = seriesRef.current;
const len = s.length;
// glide progress: 1 = settled, eases 0 → 1 over glideMs after a push
let et = 1;
if (!reduced && transRef.current >= 0) {
const p = (now - transRef.current) / glideMs;
if (p >= 1) {
transRef.current = -1;
} else {
et = glideEase(p);
}
}
// smoothed normalization peak over the visible window
let mx = 1e-6;
for (let i = Math.max(0, len - rows - 1); i < len; i++) {
const sv = s[i] ?? 0;
if (sv > mx) mx = sv;
}
smoothMax = smoothMax === 0 ? mx : smoothMax + (mx - smoothMax) * 0.05;
const scroll = reduced ? 0 : (now / 1000) * 0.06; // 0.06 noise u/s
const horizonY = h * 0.16;
const baseY = h * 0.94;
const pad = w * 0.03;
const usable = Math.max(1, w - pad * 2);
const hasDent = dentAmt > 0.001 || dentAmt < -0.001;
ctx.lineJoin = "round";
// back → front: stroke then fill below with the background so nearer
// ridges occlude farther ones (painter's-algorithm ridgeline trick)
for (let r = 0; r < rows; r++) {
const t = r * invRows; // 0 = horizon, 1 = front
const rowY = horizonY + (baseY - horizonY) * t * t; // quadratic ease
const rowW = usable * (0.65 + 0.35 * t); // narrows 35% at the back
const x0 = pad + (usable - rowW) / 2;
// fractional sample index: history glides one row back per push
const idx = len - rows + r - 1 + et;
const i0 = Math.floor(idx);
const f = idx - i0;
const v0 = i0 >= 0 && i0 < len ? (s[i0] ?? 0) : 0;
const v1 = i0 + 1 >= 0 && i0 + 1 < len ? (s[i0 + 1] ?? 0) : 0;
const val = Math.min(1.4, Math.max(0, (v0 + (v1 - v0) * f) / smoothMax));
const texIdx = Math.round(idx); // texture travels with the sample
const frontScale = 0.25 + 0.75 * t;
const ambScale = ambientAmplitude * (0.3 + 0.7 * t);
const depthCoord = (rows - 1 - r) * 0.35 + scroll;
for (let c = 0; c < cols; c++) {
const cn = c * invCols;
const x = x0 + rowW * cn;
const amb = ambScale * noise2(cn * 4 + 7.3, depthCoord);
const env = Math.exp(-((cn - 0.5) * (cn - 0.5)) / (2 * 0.18 * 0.18));
const tex = 0.55 + 0.45 * noise2(cn * 9 + 3.1, texIdx * 0.7);
let y = rowY - amb - dataAmplitude * frontScale * val * env * tex;
if (hasDent) {
const dx = x - px;
const dy = y - py;
y += dentDepth * dentAmt * Math.exp(-(dx * dx + dy * dy) / twoSigma2);
}
xs[c] = x;
ys[c] = y;
}
// occlusion fill down to the bottom edge
ctx.beginPath();
ctx.moveTo(xs[0] ?? 0, ys[0] ?? 0);
for (let c = 1; c < cols; c++) ctx.lineTo(xs[c] ?? 0, ys[c] ?? 0);
ctx.lineTo(x0 + rowW, h + 2);
ctx.lineTo(x0, h + 2);
ctx.closePath();
ctx.fillStyle = `rgb(${fillRGB.r},${fillRGB.g},${fillRGB.b})`;
ctx.fill();
// ridgeline stroke: muted-token color 1px at the horizon fading to
// foreground-token color 1.5px at the front — theme-derived, not hardcoded
const mix = Math.pow(t, 1.3);
const rr = Math.round(strokeLowRGB.r + (strokeHighRGB.r - strokeLowRGB.r) * mix);
const rg = Math.round(strokeLowRGB.g + (strokeHighRGB.g - strokeLowRGB.g) * mix);
const rb = Math.round(strokeLowRGB.b + (strokeHighRGB.b - strokeLowRGB.b) * mix);
const a = 0.25 + 0.75 * mix;
ctx.beginPath();
ctx.moveTo(xs[0] ?? 0, ys[0] ?? 0);
for (let c = 1; c < cols; c++) ctx.lineTo(xs[c] ?? 0, ys[c] ?? 0);
ctx.strokeStyle = `rgba(${rr},${rg},${rb},${a})`;
ctx.lineWidth = 1 + 0.5 * mix;
ctx.stroke();
}
};
if (reduced) {
// static fallback: current series at rest, no scroll, no dent;
// redrawn instantly on data change (via drawRef) and on resize
drawRef.current = () => draw(0);
draw(0);
const ro = new ResizeObserver(() => {
resize();
draw(0);
});
ro.observe(root);
return () => {
ro.disconnect();
themeObserver.disconnect();
drawRef.current = null;
};
}
const loop = (now: number) => {
const dt = last === 0 ? 1 / 60 : Math.min(0.05, (now - last) / 1000);
last = now;
if (hovered) {
// lerp toward full dent at 0.12/frame (framerate-normalized)
const prev = dentAmt;
dentAmt += (1 - dentAmt) * (1 - Math.pow(0.88, dt * 60));
dentVel = (dentAmt - prev) / dt; // carry velocity into the release
} else if (dentAmt !== 0 || dentVel !== 0) {
// underdamped spring release: k = 70 s^-2, zeta = 0.6 → one rebound
const k = 70;
const c = 2 * 0.6 * Math.sqrt(k);
dentVel += (-k * dentAmt - c * dentVel) * dt;
dentAmt += dentVel * dt;
if (Math.abs(dentAmt) < 0.0005 && Math.abs(dentVel) < 0.005) {
dentAmt = 0;
dentVel = 0;
}
}
draw(now);
raf = visible ? requestAnimationFrame(loop) : 0;
};
raf = requestAnimationFrame(loop);
// ambient scroll never settles, so "sleep" = pause offscreen
const io = new IntersectionObserver((entries) => {
visible = entries[0]?.isIntersecting ?? true;
if (visible && raf === 0) {
last = 0;
raf = requestAnimationFrame(loop);
}
});
io.observe(root);
const onMove = (e: PointerEvent) => {
const rect = canvas.getBoundingClientRect();
px = e.clientX - rect.left;
py = e.clientY - rect.top;
hovered = true;
};
const onLeave = () => {
hovered = false;
};
const ro = new ResizeObserver(resize);
ro.observe(root);
root.addEventListener("pointermove", onMove);
root.addEventListener("pointerdown", onMove);
root.addEventListener("pointerleave", onLeave);
return () => {
cancelAnimationFrame(raf);
io.disconnect();
ro.disconnect();
themeObserver.disconnect();
root.removeEventListener("pointermove", onMove);
root.removeEventListener("pointerdown", onMove);
root.removeEventListener("pointerleave", onLeave);
};
}, [cols, rows, ambientAmplitude, dataAmplitude, glideMs, dentSigma, dentDepth]);
return (
<div
ref={rootRef}
role="img"
aria-label={ariaLabel}
className={`relative w-full overflow-hidden ${className}`}
>
<canvas
ref={canvasRef}
aria-hidden
className="absolute inset-0 h-full w-full"
/>
</div>
);
}
an Unknown-Pleasures ridgeline chart where live history recedes into scrolling noise terrain, dented by the cursor; use for a live/ambient data feed with a generative-landscape feel, not when trend direction is the point.
An Unknown-Pleasures wireframe landscape on a DPR-aware Canvas 2D: ~96 columns by 40 rows of ridgeline polylines drawn back to front, each row stroked then filled below with the theme's surface/background token so nearer ridges occlude farther ones (painter's-algorithm ridgeline trick); row y-spacing eases quadratically so rows compress at the horizon and row width narrows ~35% toward the back. Height per vertex = ambient + data: ambient is 2-octave value noise scrolling toward the viewer at 0.06 u/s with 18px amplitude scaled up toward the front; data is a series prop (number[]) where sample age maps to row depth, so each new sample enters at the front row and the whole history glides one row back over 600ms with cubic-bezier(0.22,1,0.36,1) interpolation between fractional row offsets, the chart's history literally receding into ambient terrain. The cursor dents the mesh with a screen-space gaussian (sigma 90px, max depth 26px), plus a soft foreground-token glow bloomed at the dent center so the interaction reads clearly: dent amount lerps toward full at 0.12/frame while hovered and releases through an underdamped spring (k=70 s^-2, zeta=0.6) for one visible rebound. Stroke fades from 1.5px foreground-token color at the front row to 1px muted-token color at ~25% opacity at the horizon. Fill and stroke colors are resolved from CSS custom properties (--surface/--background, --foreground, --muted) at mount and re-derived via a MutationObserver on the document root's class attribute, so the terrain repaints correctly on theme toggle without a remount. Data and pointer live in refs; a single rAF loop is the only writer and pauses when the element leaves the viewport. Under prefers-reduced-motion: no noise scroll, no dent, a static render of the current series redrawn instantly on data change or theme change. With an empty series it idles as pure atmosphere; with no noise it reads as a strict chart.