"use client"; import { useEffect, useRef, type ReactNode } from "react"; type StarDriftProps = { children?: ReactNode; className?: string; /** Number of stars. */ count?: number; color?: string; background?: string; /** 1 is the default pace; 2 is twice as fast. */ speed?: number; }; type Star = { x: number; y: number; z: number; phase: number }; /* StarDrift: a field of stars drifting at different depths. Near stars are bigger, brighter and faster. */ export function StarDrift({ children, className = "", count = 160, color = "#ffffff", background = "#04050d", speed = 1, }: StarDriftProps) { 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; let stars: Star[] = []; const draw = () => { ctx.clearRect(0, 0, w, h); ctx.fillStyle = color; for (const s of stars) { ctx.globalAlpha = (0.3 + 0.7 * s.z) * (0.75 + 0.25 * Math.sin(time * 3 * s.z + s.phase)); ctx.beginPath(); ctx.arc(s.x, s.y, 0.4 + s.z * 1.3, 0, Math.PI * 2); ctx.fill(); } ctx.globalAlpha = 1; }; const frame = (now: number) => { const dt = last ? Math.min((now - last) / 1000, 0.05) * speed : 0; last = now; time += dt; for (const s of stars) { s.x += 14 * s.z * dt; s.y -= 4 * s.z * dt; if (s.x > w + 2) s.x = -2; if (s.y < -2) s.y = h + 2; } 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); stars = Array.from({ length: count }, () => ({ x: Math.random() * w, y: Math.random() * h, z: 0.15 + Math.random() * 0.85, phase: Math.random() * Math.PI * 2, })); 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(); }; }, [count, color, speed]); return (
{children}
); }