import { useRef, useEffect, useCallback } from 'react'; /** * Aurora background — 5 large diffuse orbs with additive blending. * Orbs are deliberately offset from center to avoid uniform "blue blob" look. * Mouse gently pushes nearby orbs (10-20px, very subtle). */ interface Orb { cx: number; // base center X ratio (0-1) cy: number; // base center Y ratio (0-1) color: [number, number, number]; alpha: number; // peak alpha — varies per orb for brightness contrast phase: number; freqX: number; freqY: number; ampX: number; ampY: number; radius: number; // ratio of max(w,h) // breathing breathFreq: number; breathAmp: number; } // Orbs deliberately NOT centered — some in corners, some offset, // creating uneven light distribution with dark pockets. const ORBS: Orb[] = [ // Cyan — upper-left area, large and bright { cx: 0.25, cy: 0.30, color: [126, 220, 200], alpha: 0.28, phase: 0, freqX: 0.00012, freqY: 0.00010, ampX: 0.12, ampY: 0.10, radius: 0.50, breathFreq: 0.0004, breathAmp: 0.06 }, // Purple — lower-right, medium { cx: 0.72, cy: 0.65, color: [108, 99, 255], alpha: 0.22, phase: 1.8, freqX: 0.00010, freqY: 0.00014, ampX: 0.14, ampY: 0.12, radius: 0.45, breathFreq: 0.0003, breathAmp: 0.08 }, // Blue — center-right, smaller and dimmer (fills gap) { cx: 0.58, cy: 0.38, color: [59, 130, 246], alpha: 0.18, phase: 3.2, freqX: 0.00015, freqY: 0.00008, ampX: 0.10, ampY: 0.15, radius: 0.38, breathFreq: 0.0005, breathAmp: 0.07 }, // Light purple — bottom-left corner, dim accent { cx: 0.20, cy: 0.70, color: [167, 139, 250], alpha: 0.15, phase: 4.5, freqX: 0.00008, freqY: 0.00012, ampX: 0.16, ampY: 0.08, radius: 0.40, breathFreq: 0.00035, breathAmp: 0.10 }, // Bright cyan — upper-right, small bright accent { cx: 0.78, cy: 0.25, color: [34, 211, 238], alpha: 0.20, phase: 5.8, freqX: 0.00014, freqY: 0.00016, ampX: 0.08, ampY: 0.12, radius: 0.35, breathFreq: 0.00045, breathAmp: 0.09 }, ]; // Mouse influence radius (px) and max push distance (px) const MOUSE_RADIUS = 400; const MOUSE_PUSH = 28; export function AuroraCanvas() { const canvasRef = useRef(null); const grainRef = useRef(null); const mouseRef = useRef({ x: -9999, y: -9999, active: false }); const handleMouseMove = useCallback((e: MouseEvent) => { mouseRef.current.x = e.clientX; mouseRef.current.y = e.clientY; mouseRef.current.active = true; }, []); const handleMouseLeave = useCallback(() => { mouseRef.current.active = false; }, []); // ── Aurora animation ── useEffect(() => { const canvas = canvasRef.current; if (!canvas) return; const ctx = canvas.getContext('2d'); if (!ctx) return; let w = window.innerWidth; let h = window.innerHeight; function resize() { w = window.innerWidth; h = window.innerHeight; const dpr = w < 768 ? 0.5 : Math.min(window.devicePixelRatio, 1.5); canvas!.width = w * dpr; canvas!.height = h * dpr; canvas!.style.width = w + 'px'; canvas!.style.height = h + 'px'; ctx!.setTransform(dpr, 0, 0, dpr, 0, 0); } resize(); let animId: number; const t0 = performance.now(); // Smoothed mouse position for gentle push let smoothMx = -9999; let smoothMy = -9999; function draw(now: number) { const t = now - t0; ctx!.clearRect(0, 0, w, h); ctx!.globalCompositeOperation = 'lighter'; // Smooth mouse tracking (lerp toward actual position) const mouse = mouseRef.current; if (mouse.active) { smoothMx += (mouse.x - smoothMx) * 0.035; smoothMy += (mouse.y - smoothMy) * 0.035; } else { // Slowly drift smoothed mouse away (return to no-influence) smoothMx += (-9999 - smoothMx) * 0.01; smoothMy += (-9999 - smoothMy) * 0.01; } for (const orb of ORBS) { // Base position from slow sinusoidal movement let x = w * (orb.cx + Math.sin(t * orb.freqX + orb.phase) * orb.ampX); let y = h * (orb.cy + Math.cos(t * orb.freqY + orb.phase * 0.7) * orb.ampY); // Mouse push — gently offset orb away from cursor const dx = x - smoothMx; const dy = y - smoothMy; const dist = Math.sqrt(dx * dx + dy * dy); if (dist < MOUSE_RADIUS && dist > 1) { const strength = (1 - dist / MOUSE_RADIUS) * MOUSE_PUSH; x += (dx / dist) * strength; y += (dy / dist) * strength; } // Breathing: radius and alpha pulse slowly const breathT = Math.sin(t * orb.breathFreq + orb.phase * 1.3); const r = Math.max(w, h) * orb.radius * (1 + breathT * orb.breathAmp); const a = orb.alpha * (1 + breathT * 0.15); const [cr, cg, cb] = orb.color; const grad = ctx!.createRadialGradient(x, y, 0, x, y, r); grad.addColorStop(0, `rgba(${cr},${cg},${cb},${(a).toFixed(3)})`); grad.addColorStop(0.25, `rgba(${cr},${cg},${cb},${(a * 0.5).toFixed(3)})`); grad.addColorStop(0.5, `rgba(${cr},${cg},${cb},${(a * 0.15).toFixed(3)})`); grad.addColorStop(0.8, `rgba(${cr},${cg},${cb},${(a * 0.03).toFixed(3)})`); grad.addColorStop(1, `rgba(${cr},${cg},${cb},0)`); ctx!.fillStyle = grad; ctx!.beginPath(); ctx!.arc(x, y, r, 0, Math.PI * 2); ctx!.fill(); } ctx!.globalCompositeOperation = 'source-over'; animId = requestAnimationFrame(draw); } animId = requestAnimationFrame(draw); window.addEventListener('resize', resize); window.addEventListener('mousemove', handleMouseMove, { passive: true }); document.addEventListener('mouseleave', handleMouseLeave); return () => { cancelAnimationFrame(animId); window.removeEventListener('resize', resize); window.removeEventListener('mousemove', handleMouseMove); document.removeEventListener('mouseleave', handleMouseLeave); }; }, [handleMouseMove, handleMouseLeave]); // ── Film grain — 4 FPS low-noise ── useEffect(() => { const canvas = grainRef.current; if (!canvas) return; const ctx = canvas.getContext('2d'); if (!ctx) return; canvas.width = 256; canvas.height = 256; let animId: number; let lastFrame = 0; function drawGrain(now: number) { if (now - lastFrame > 250) { lastFrame = now; const img = ctx!.createImageData(256, 256); const d = img.data; for (let i = 0; i < d.length; i += 4) { const v = Math.random() * 255; d[i] = d[i + 1] = d[i + 2] = v; d[i + 3] = 255; } ctx!.putImageData(img, 0, 0); } animId = requestAnimationFrame(drawGrain); } animId = requestAnimationFrame(drawGrain); return () => cancelAnimationFrame(animId); }, []); return ( <> {/* Layer 1: Vignette — radial darkening, heavy at edges */}
{/* Layer 2: Film grain */} {/* Layer 3: Aurora — blur merges orbs into organic glow */} {/* Layer 4: Top/bottom gradient mask */}
); }