"use client"; import { useEffect, useMemo, useRef, type CSSProperties } from "react"; import { useReducedMotion } from "motion/react"; type DominoRunProps = { /** Number of tiles in the run. */ tiles?: number; /** Tile colour. */ tile?: string; /** Colour of the pips and the centre line. */ pip?: string; /** 1 is the default pace; 2 is twice as fast. */ speed?: number; /** Read out by screen readers. */ label?: string; className?: string; }; // All sizes are in em and 1em is 1% of the width, so the run scales with its box. const W = 8; const H = 16; const D = 2.1; const GAP = 5.5; // Floor distance between tiles. const REST = 0.2; const clamp = (v: number) => Math.min(1, Math.max(0, v)); const span = (p: number, a: number, b: number) => clamp((p - a) / (b - a)); // Pip spots on a 3 by 3 grid, for faces 0 to 6. const SPOTS = [[], [4], [0, 8], [0, 4, 8], [0, 2, 6, 8], [0, 2, 4, 6, 8], [0, 2, 3, 5, 6, 8]]; function face(a: number, b: number, pip: string) { const dot = (spot: number, half: number) => { const x = 26 + (spot % 3) * 24; const y = 12 + half * 50 + Math.floor(spot / 3) * 13; return `radial-gradient(circle at ${x}% ${y}%,${pip} 0.62em,transparent 0.68em)`; }; return [ ...SPOTS[a].map((s) => dot(s, 0)), ...SPOTS[b].map((s) => dot(s, 1)), `linear-gradient(0deg,transparent calc(50% - 0.12em),${pip} calc(50% - 0.12em),${pip} calc(50% + 0.12em),transparent calc(50% + 0.12em))`, ].join(","); } // The floor line: walks an S curve at even steps, so spacing holds round the bends. function layout(count: number) { // Starts far away and comes toward the viewer, so the falling tiles show their pips. const curve = (u: number) => [46 - 92 * u, 15 * Math.sin(u * Math.PI * 2 + 0.4) + 6 * u - 9]; const out: { x: number; y: number; heading: number; bend: number }[] = []; const total = (count - 1) * GAP; let [x, y] = curve(0); let walked = 0; let next = 0; for (let u = 0; u <= 1.6 && out.length < count; u += 0.001) { const [nx, ny] = curve(u); walked += Math.hypot(nx - x, ny - y); x = nx; y = ny; if (walked >= next) { const [ax, ay] = curve(u + 0.002); out.push({ x, y, heading: Math.atan2(ay - y, ax - x), bend: walked / Math.max(total, 1) }); next += GAP; } } return out; } // Tile angle at clock p: a wave down the run, a hold, then the wave runs back and stands them up. function angle(i: number, count: number, p: number) { const lean = (Math.acos(D / GAP) * 180) / Math.PI; const end = i === count - 1 ? 84 : lean; const step = 0.42 / count; const down = span(p, 0.02 + i * step, 0.02 + i * step + 0.09); const up = span(p, 0.62 + (count - 1 - i) * 0.018, 0.62 + (count - 1 - i) * 0.018 + 0.1); return end * (down * down) * (1 - up * up * (3 - 2 * up)); } function hinge(a: number) { return `rotateX(${-a}deg)`; } /* DominoRun: ivory tiles with real pips stand on an S curve in true 3D. A wave knocks them down one by one, then runs back and stands them up again. */ export function DominoRun({ tiles = 18, tile = "#efe6d2", pip = "#1d1b18", speed = 1, label = "Loading", className = "" }: DominoRunProps) { const reduce = useReducedMotion(); const box = useRef(null); const line = useMemo(() => layout(tiles), [tiles]); useEffect(() => { if (reduce) return; const hinges = [...box.current!.querySelectorAll("[data-hinge]")]; const shades = [...box.current!.querySelectorAll("[data-shade]")]; const seconds = 4.4 / speed; const start = performance.now() - REST * seconds * 1000; let raf = 0; const tick = (now: number) => { const p = ((now - start) / (seconds * 1000)) % 1; hinges.forEach((part, i) => { const a = angle(i, hinges.length, p); part.style.transform = hinge(a); shades[i].style.transform = `scaleY(${0.2 + a / 70})`; }); raf = requestAnimationFrame(tick); }; raf = requestAnimationFrame(tick); return () => cancelAnimationFrame(raf); }, [reduce, speed, line]); const sides: CSSProperties = { backgroundColor: tile, backgroundImage: "linear-gradient(90deg,rgba(0,0,0,0.3),rgba(0,0,0,0.12))" }; return (
{line.map((spot, i) => { const a = angle(i, tiles, REST); const value = [(i * 5 + 3) % 7, (i * 3 + 1) % 7]; return (
); })}
); } // One side of a tile: its bottom edge sits on the origin, the transform stands it up. function Face({ w, h, at, style }: { w: number; h: number; at: string; style: CSSProperties }) { return ( ); }