import { useRef, useEffect, useCallback } from 'react'; import { useThemeStore } from '../store/theme'; /** * Aurora background — 5 large diffuse orbs with additive blending. * V2: 接入 themeStore,浅色态用 pastel orb + 白色 vignette/gradient,深色保持原品牌色。 */ interface Orb { cx: number; // base center X ratio (0-1) cy: number; // base center Y ratio (0-1) color: [number, number, number]; alpha: number; phase: number; freqX: number; freqY: number; ampX: number; ampY: number; radius: number; breathFreq: number; breathAmp: number; } // Dark theme orbs — vivid 品牌色(青/紫/蓝) const DARK_ORBS: Orb[] = [ { 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 }, { 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 }, { 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 }, { 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 }, { 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 }, ]; // Light theme orbs — pastel 浅色,alpha 减半左右,给玻璃面提供穿透色源 const LIGHT_ORBS: Orb[] = [ { cx: 0.25, cy: 0.30, color: [180, 167, 255], alpha: 0.32, // pastel lavender phase: 0, freqX: 0.00012, freqY: 0.00010, ampX: 0.12, ampY: 0.10, radius: 0.55, breathFreq: 0.0004, breathAmp: 0.06 }, { cx: 0.72, cy: 0.65, color: [167, 200, 255], alpha: 0.28, // pastel sky phase: 1.8, freqX: 0.00010, freqY: 0.00014, ampX: 0.14, ampY: 0.12, radius: 0.50, breathFreq: 0.0003, breathAmp: 0.08 }, { cx: 0.58, cy: 0.38, color: [255, 180, 130, ], alpha: 0.18, // pastel peach phase: 3.2, freqX: 0.00015, freqY: 0.00008, ampX: 0.10, ampY: 0.15, radius: 0.42, breathFreq: 0.0005, breathAmp: 0.07 }, { cx: 0.20, cy: 0.70, color: [220, 167, 255], alpha: 0.20, // pastel pink-violet phase: 4.5, freqX: 0.00008, freqY: 0.00012, ampX: 0.16, ampY: 0.08, radius: 0.45, breathFreq: 0.00035, breathAmp: 0.10 }, { cx: 0.78, cy: 0.25, color: [180, 220, 255], alpha: 0.22, // pastel blue phase: 5.8, freqX: 0.00014, freqY: 0.00016, ampX: 0.08, ampY: 0.12, radius: 0.40, breathFreq: 0.00045, breathAmp: 0.09 }, ]; 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 theme = useThemeStore((s) => s.theme); const isLight = theme === 'light'; 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(); let smoothMx = -9999; let smoothMy = -9999; const orbs = isLight ? LIGHT_ORBS : DARK_ORBS; function draw(now: number) { const t = now - t0; ctx!.clearRect(0, 0, w, h); // 浅色用 source-over 让 pastel 互融时不会过曝;深色继续用 lighter 加合 ctx!.globalCompositeOperation = isLight ? 'source-over' : 'lighter'; const mouse = mouseRef.current; if (mouse.active) { smoothMx += (mouse.x - smoothMx) * 0.035; smoothMy += (mouse.y - smoothMy) * 0.035; } else { smoothMx += (-9999 - smoothMx) * 0.01; smoothMy += (-9999 - smoothMy) * 0.01; } for (const orb of orbs) { 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); 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; } 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, isLight]); // ── 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); }, []); // 浅色态:vignette / gradient 反相 — 用白色压边,黑色压边在浅色上是错的 const vignetteColor = isLight ? 'rgba(255,255,255,0.6)' : 'rgba(0,0,0,0.8)'; const fadeColor = isLight ? 'rgba(250,250,250,0.7)' : 'rgba(0,0,0,0.5)'; return ( <> {/* Layer 1: Vignette — radial fading, 浅色下用白色 */}
{/* Layer 2: Film grain — 浅色下大幅减弱避免噪点过曝 */} {/* Layer 3: Aurora — blur 让 orb 融成有机晕染 */} {/* Layer 4: 顶/底渐变压角 */}
); }