"use client"; import { useEffect, useRef, type ReactNode } from "react"; type PixelRainProps = { children?: ReactNode; className?: string; color?: string; /** Base colour. Also used to fade the trails. */ background?: string; /** Pixel size in pixels. */ size?: number; /** Share of columns raining at once, 0 to 1. */ density?: number; /** 1 is the default pace; 2 is twice as fast. */ speed?: number; }; type Drop = { y: number; v: number; on: boolean }; /* PixelRain: square pixels fall down a grid and leave fading trails. */ export function PixelRain({ children, className = "", color = "#a78bfa", background = "#05040b", size = 10, density = 0.6, speed = 1, }: PixelRainProps) { 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 drops: Drop[] = []; const newDrop = (y: number): Drop => ({ y, v: 8 + Math.random() * 14, on: Math.random() < density }); const cell = (col: number, row: number) => ctx.fillRect(col * size + 1, row * size + 1, size - 2, size - 2); // Still frame: each raining column gets a short fixed trail. const drawStill = () => { ctx.globalAlpha = 1; ctx.fillStyle = background; ctx.fillRect(0, 0, w, h); ctx.fillStyle = color; drops.forEach((d, col) => { if (!d.on) return; for (let k = 0; k < 8; k++) { ctx.globalAlpha = (1 - k / 8) * 0.8; cell(col, Math.floor(d.y) - k); } }); ctx.globalAlpha = 1; }; const frame = (now: number) => { const dt = last ? Math.min((now - last) / 1000, 0.05) * speed : 0; last = now; ctx.globalAlpha = 1 - Math.pow(0.9, dt * 60); ctx.fillStyle = background; ctx.fillRect(0, 0, w, h); ctx.globalAlpha = 1; ctx.fillStyle = color; drops.forEach((d, col) => { d.y += d.v * dt; if (d.y * size > h + size * 10) drops[col] = newDrop(-Math.random() * 20); else if (d.on) cell(col, Math.floor(d.y)); }); 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); drops = Array.from({ length: Math.ceil(w / size) }, () => newDrop(Math.random() * (h / size))); drawStill(); }; // 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, background, size, density, speed]); return (
{children}
); }