Contour Drift
Topographic contour hairlines traced from a seeded height field with marching squares, drifting almost imperceptibly.
surveyed terrain
The field is three to four seeded sine octaves over a fixed coarse grid; each frame, several iso levels are traced with marching squares and stroked as 1px hairlines. Line alpha rises with elevation in honest steps, so higher contours read lit from above. Freezes to a single static frame under reduced motion.
Installation
Complete the shared Setup first, then copy the component into
components/ui/contour-drift.tsx.
'use client';import * as React from 'react';import { cn } from '@/lib/cn';export type ContourDriftProps = { accent?: string; accentPeaks?: boolean; levels?: number; speed?: number; className?: string;};function seeded(n: number) { const x = Math.sin(n * 12.9898) * 43758.5453; return x - Math.floor(x);}function toRgb(hex: string): [number, number, number] { let h = hex.replace('#', ''); if (h.length === 3) h = h .split('') .map((c) => c + c) .join(''); const n = parseInt(h, 16); return [(n >> 16) & 255, (n >> 8) & 255, n & 255];}const COLS = 90;const ROWS = 60;const OCTAVES = [0, 1, 2, 3].map((i) => { const angle = seeded(i * 17.3 + 3.1) * Math.PI * 2; const freq = [1.6, 3.1, 5.7, 9.4][i]; return { kx: Math.cos(angle) * freq, ky: Math.sin(angle) * freq, amp: [0.26, 0.13, 0.07, 0.04][i], phase: seeded(i * 91.7 + 11.9) * Math.PI * 2, rate: [0.021, 0.033, 0.047, 0.062][i], };});const SEGS: ReadonlyArray<ReadonlyArray<readonly [number, number]>> = [ [], [[3, 2]], [[2, 1]], [[3, 1]], [[0, 1]], [[0, 1], [3, 2]], [[0, 2]], [[0, 3]], [[0, 3]], [[0, 2]], [[0, 3], [2, 1]], [[0, 1]], [[3, 1]], [[2, 1]], [[3, 2]], [],];export function ContourDrift({ accent = '#f0883e', accentPeaks = false, levels = 9, speed = 1, className,}: ContourDriftProps) { const canvasRef = React.useRef<HTMLCanvasElement>(null); const accentRef = React.useRef(accent); accentRef.current = accent; React.useEffect(() => { const canvas = canvasRef.current; if (!canvas) return; const ctx = canvas.getContext('2d'); if (!ctx) return; const reduce = window.matchMedia('(prefers-reduced-motion: reduce)').matches; const root = document.documentElement; let raf = 0; let w = 0; let h = 0; let dpr = 1; const field = new Float32Array((COLS + 1) * (ROWS + 1)); const resize = () => { const rect = canvas.getBoundingClientRect(); dpr = Math.min(window.devicePixelRatio || 1, 2); w = rect.width; h = rect.height; 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); }; const sample = (u: number, v: number, t: number) => { let n = 0.5; for (const o of OCTAVES) n += o.amp * Math.sin(u * o.kx + v * o.ky + o.phase + t * o.rate); return n; }; const draw = (t: number) => { const dark = root.classList.contains('dark'); const [ar, ag, ab] = toRgb(accentRef.current); ctx.clearRect(0, 0, w, h); ctx.lineWidth = 1; for (let gy = 0; gy <= ROWS; gy++) { const v = (gy / ROWS) * 2.1; for (let gx = 0; gx <= COLS; gx++) { field[gy * (COLS + 1) + gx] = sample((gx / COLS) * 3.4, v, t); } } const cw = w / COLS; const ch = h / ROWS; const nLevels = Math.max(2, levels); const aTop = dark ? 0.42 : 0.34; const aBase = 0.05; for (let li = 0; li < nLevels; li++) { const iso = 0.18 + (li / (nLevels - 1)) * 0.64; const alpha = aBase + (aTop - aBase) * (li / (nLevels - 1)); const peak = accentPeaks && li === nLevels - 1; ctx.strokeStyle = peak ? `rgba(${ar},${ag},${ab},${alpha.toFixed(3)})` : dark ? `rgba(255,255,255,${alpha.toFixed(3)})` : `rgba(0,0,0,${alpha.toFixed(3)})`; ctx.beginPath(); for (let gy = 0; gy < ROWS; gy++) { const r0 = gy * (COLS + 1); const r1 = (gy + 1) * (COLS + 1); for (let gx = 0; gx < COLS; gx++) { const tl = field[r0 + gx]; const tr = field[r0 + gx + 1]; const br = field[r1 + gx + 1]; const bl = field[r1 + gx]; const idx = (tl > iso ? 8 : 0) | (tr > iso ? 4 : 0) | (br > iso ? 2 : 0) | (bl > iso ? 1 : 0); if (idx === 0 || idx === 15) continue; const segs = SEGS[idx]; for (const [e1, e2] of segs) { for (let e = 0; e < 2; e++) { const edge = e === 0 ? e1 : e2; let x = 0; let y = 0; if (edge === 0) { x = (gx + (iso - tl) / (tr - tl)) * cw; y = gy * ch; } else if (edge === 1) { x = (gx + 1) * cw; y = (gy + (iso - tr) / (br - tr)) * ch; } else if (edge === 2) { x = (gx + (iso - bl) / (br - bl)) * cw; y = (gy + 1) * ch; } else { x = gx * cw; y = (gy + (iso - tl) / (bl - tl)) * ch; } if (e === 0) ctx.moveTo(x, y); else ctx.lineTo(x, y); } } } } ctx.stroke(); } }; resize(); if (reduce) { draw(0); } else { let start: number | null = null; const loop = (now: number) => { if (start === null) start = now; const t = ((now - start) / 1000) * speed; draw(t); raf = requestAnimationFrame(loop); }; raf = requestAnimationFrame(loop); } const ro = new ResizeObserver(() => { resize(); if (reduce) draw(0); }); ro.observe(canvas); const mo = new MutationObserver(() => { if (reduce) draw(0); }); mo.observe(root, { attributes: true, attributeFilter: ['class'] }); return () => { cancelAnimationFrame(raf); ro.disconnect(); mo.disconnect(); }; }, [accentPeaks, levels, speed]); return ( <canvas ref={canvasRef} aria-hidden className={cn( 'pointer-events-none h-full w-full select-none', '[mask-image:radial-gradient(ellipse_92%_100%_at_50%_50%,black_38%,transparent_97%)]', className, )} /> );}Usage
import { ContourDrift } from '@/components/ui/contour-drift';
export default function Example() {
return (
<div className="relative h-[340px] w-full overflow-hidden rounded-xl border border-black/[0.08] bg-[#fafafa] dark:border-white/10 dark:bg-[#0a0a0b]">
<ContourDrift />
</div>
);
}Examples
More levels and an accent tint on the single highest contour band.
Props
| Prop | Type | Default | Description |
|---|---|---|---|
accent | string | "#f0883e" | Warm tint for the highest contour band. Only used when accentPeaks is set. |
accentPeaks | boolean | false | Tint the single highest contour band with the accent. |
levels | number | 9 | Number of contour levels. |
speed | number | 1 | Drift speed. 0 freezes it to a static frame. |
className | string | - | Forwarded to the canvas. |