"use client"; import { useEffect, useRef, type ReactNode } from "react"; type CosmicDustProps = { children?: ReactNode; className?: string; /** Colour of the fine dust. */ color?: string; /** Colour of the large soft specks. */ accent?: string; background?: string; /** Number of specks. */ count?: number; /** 1 is the default pace; 2 is twice as fast. */ speed?: number; }; type Speck = { x: number; y: number; r: number; vy: number; phase: number; alpha: number; big: boolean }; /* CosmicDust: fine dust and a few soft out-of-focus specks float upward, swaying and flickering. */ export function CosmicDust({ children, className = "", color = "#e9e3ff", accent = "#a78bfa", background = "#07060f", count = 220, speed = 1, }: CosmicDustProps) { 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 specks: Speck[] = []; const draw = () => { ctx.clearRect(0, 0, w, h); for (const s of specks) { ctx.fillStyle = s.big ? accent : color; ctx.globalAlpha = s.alpha * (0.6 + 0.4 * Math.sin(time * 2 + s.phase)); ctx.beginPath(); ctx.arc(s.x + Math.sin(time * 0.5 + s.phase) * 10, s.y, s.r, 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 specks) { s.y -= s.vy * dt; if (s.y < -s.r * 2) { s.y = h + s.r * 2; s.x = Math.random() * w; } } 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); specks = Array.from({ length: count }, () => { const big = Math.random() < 0.08; return { x: Math.random() * w, y: Math.random() * h, r: big ? 3 + Math.random() * 4 : 0.5 + Math.random() * 1.1, vy: big ? 4 + Math.random() * 6 : 8 + Math.random() * 16, phase: Math.random() * Math.PI * 2, alpha: big ? 0.15 : 0.4 + Math.random() * 0.5, big, }; }); 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, accent, count, speed]); return (
{children}
); }