"use client"; import { useEffect, useRef, type ReactNode } from "react"; type WaveFieldProps = { children?: ReactNode; className?: string; color?: string; background?: string; /** Number of wave lines. */ lines?: number; /** Wave height in pixels. */ amplitude?: number; /** 1 is the default pace; 2 is twice as fast. */ speed?: number; }; /* WaveField: stacked lines that ripple like a contour map. Middle lines are brightest. */ export function WaveField({ children, className = "", color = "#a78bfa", background = "#06050d", lines = 16, amplitude = 24, speed = 1, }: WaveFieldProps) { const canvasRef = useRef(null); useEffect(() => { const canvas = canvasRef.current; const ctx = canvas?.getContext("2d"); if (!canvas || !ctx) return; const still = window.matchMedia("(prefers-reduced-motion: reduce)").matches; let w = 0; let h = 0; let raf = 0; let last = 0; let time = 0; const draw = () => { ctx.clearRect(0, 0, w, h); ctx.strokeStyle = color; ctx.lineWidth = 1; const n = Math.max(lines, 2); for (let i = 0; i < n; i++) { const k = i / (n - 1); const base = h * (0.15 + 0.7 * k); ctx.globalAlpha = 0.12 + 0.6 * (1 - Math.abs(k - 0.5) * 2); ctx.beginPath(); for (let x = 0; x <= w + 8; x += 8) { const swell = 0.4 + 0.6 * Math.sin((Math.PI * x) / w); const wave = Math.sin(x * 0.008 + time * 0.6 + i * 0.35) * 0.6 + Math.sin(x * 0.017 - time * 0.4 + i * 0.2) * 0.4; const y = base + amplitude * swell * wave; if (x === 0) ctx.moveTo(x, y); else ctx.lineTo(x, y); } ctx.stroke(); } ctx.globalAlpha = 1; }; const frame = (now: number) => { time += last ? Math.min((now - last) / 1000, 0.05) * speed : 0; last = now; draw(); raf = requestAnimationFrame(frame); }; const start = () => { if (still || raf) return; last = 0; raf = requestAnimationFrame(frame); }; const stop = () => { cancelAnimationFrame(raf); raf = 0; }; const resize = () => { const dpr = Math.min(window.devicePixelRatio || 1, 2); w = canvas.clientWidth; h = canvas.clientHeight; canvas.width = Math.round(w * dpr); canvas.height = Math.round(h * dpr); ctx.setTransform(dpr, 0, 0, dpr, 0, 0); draw(); }; // Redraw on size change; only animate while on screen. const ro = new ResizeObserver(resize); const io = new IntersectionObserver(([e]) => (e.isIntersecting ? start() : stop())); ro.observe(canvas); io.observe(canvas); return () => { stop(); ro.disconnect(); io.disconnect(); }; }, [color, lines, amplitude, speed]); return (
{children}
); }