import { useEffect, useRef, type CSSProperties } from 'react'; import { getLocalizedPath, getTranslation, type Locale } from '../i18n/utils'; import { PLACEHOLDER_IMG } from '../placeholder'; import { cssVars } from './css'; import { Icon, statsIcons } from '../icons'; /** * 首屏全屏粒子 + 邻近连线 Canvas 背景: * - 4 色粒子(白/蓝/紫/极光淡粉)约 70~95 颗(按屏幕面积自适应) * - 邻近粒子间画淡蓝连线(dist < 120)形成柔和"网" * - 鼠标靠近时粒子轻微吸附,连线高亮 * - prefers-reduced-motion 下固定为静态散点,不执行动画 */ function HeroParticleCanvas() { const ref = useRef(null); useEffect(() => { const canvas = ref.current; if (!canvas) return; const ctx = canvas.getContext('2d'); if (!ctx) return; const reduced = window.matchMedia('(prefers-reduced-motion: reduce)').matches; let raf = 0; let dpr = Math.min(window.devicePixelRatio || 1, 2); type P = { x: number; y: number; vx: number; vy: number; r: number; color: string; alpha: number; alphaSpeed: number; }; const PALETTE = [ '255, 255, 255', // 白星 '148, 184, 255', // 品牌淡蓝 '196, 178, 255', // 薰衣草紫 '255, 180, 215', // 极光淡粉 ]; let W = 0, H = 0; let particles: P[] = []; const mouse = { x: -9999, y: -9999, active: false }; const resize = () => { const { clientWidth, clientHeight } = canvas.parentElement || canvas; W = clientWidth; H = clientHeight; dpr = Math.min(window.devicePixelRatio || 1, 2); canvas.width = Math.floor(W * dpr); canvas.height = Math.floor(H * dpr); canvas.style.width = W + 'px'; canvas.style.height = H + 'px'; ctx.setTransform(dpr, 0, 0, dpr, 0, 0); const area = W * H; const count = Math.max(55, Math.min(100, Math.floor(area / 14000))); particles = Array.from({ length: count }, () => ({ x: Math.random() * W, y: Math.random() * H, vx: (Math.random() - 0.5) * 0.10, // 速度砍 71%,停止"游来游去"的感觉 vy: (Math.random() - 0.5) * 0.10, r: Math.random() * 1.4 + 0.6, color: PALETTE[Math.floor(Math.random() * PALETTE.length)], alpha: Math.random() * 0.5 + 0.4, alphaSpeed: (Math.random() - 0.5) * 0.008, })); }; const step = () => { ctx.clearRect(0, 0, W, H); // 1) 粒子 for (let i = 0; i < particles.length; i++) { const p = particles[i]; if (!reduced) { // 鼠标吸附(力度很小,避免头晕) if (mouse.active) { const dx = mouse.x - p.x, dy = mouse.y - p.y; const d2 = dx * dx + dy * dy; if (d2 < 140 * 140) { const f = (1 - Math.sqrt(d2) / 140) * 0.04; p.vx += dx * f * 0.02; p.vy += dy * f * 0.02; } } p.x += p.vx; p.y += p.vy; // 速度阻尼 p.vx *= 0.992; p.vy *= 0.992; // 边界反弹 if (p.x < p.r) { p.x = p.r; p.vx = Math.abs(p.vx); } if (p.x > W - p.r) { p.x = W - p.r; p.vx = -Math.abs(p.vx); } if (p.y < p.r) { p.y = p.r; p.vy = Math.abs(p.vy); } if (p.y > H - p.r) { p.y = H - p.r; p.vy = -Math.abs(p.vy); } // 闪烁 alpha p.alpha += p.alphaSpeed; if (p.alpha > 0.95 || p.alpha < 0.35) p.alphaSpeed = -p.alphaSpeed; } ctx.beginPath(); const g = ctx.createRadialGradient(p.x, p.y, 0, p.x, p.y, p.r * 3); g.addColorStop(0, `rgba(${p.color}, ${p.alpha})`); g.addColorStop(0.5, `rgba(${p.color}, ${p.alpha * 0.35})`); g.addColorStop(1, `rgba(${p.color}, 0)`); ctx.fillStyle = g; ctx.arc(p.x, p.y, p.r * 3, 0, Math.PI * 2); ctx.fill(); } // 2) 邻近连线(仅动画模式执行,静态省开销) if (!reduced) { const MAX = 120; for (let i = 0; i < particles.length; i++) { const a = particles[i]; for (let j = i + 1; j < particles.length; j++) { const b = particles[j]; const dx = a.x - b.x, dy = a.y - b.y; const d2 = dx * dx + dy * dy; if (d2 < MAX * MAX) { const d = Math.sqrt(d2); const t = 1 - d / MAX; // 鼠标增强:若 a 或 b 靠近鼠标,连线加亮 let boost = 0; if (mouse.active) { const adx = a.x - mouse.x, ady = a.y - mouse.y; const bdx = b.x - mouse.x, bdy = b.y - mouse.y; const near = Math.min(adx * adx + ady * ady, bdx * bdx + bdy * bdy); if (near < 120 * 120) boost = (1 - Math.sqrt(near) / 120) * 0.45; } const alpha = Math.min(0.55, t * 0.28 + boost); ctx.strokeStyle = `rgba(148, 184, 255, ${alpha})`; ctx.lineWidth = 0.7; ctx.beginPath(); ctx.moveTo(a.x, a.y); ctx.lineTo(b.x, b.y); ctx.stroke(); } } } } if (!reduced) raf = requestAnimationFrame(step); }; const onMove = (e: PointerEvent) => { const rect = canvas.getBoundingClientRect(); mouse.x = e.clientX - rect.left; mouse.y = e.clientY - rect.top; mouse.active = true; }; const onLeave = () => { mouse.active = false; mouse.x = -9999; mouse.y = -9999; }; const onResize = () => { resize(); }; resize(); raf = requestAnimationFrame(step); window.addEventListener('resize', onResize); window.addEventListener('pointermove', onMove, { passive: true }); window.addEventListener('pointerleave', onLeave); return () => { cancelAnimationFrame(raf); window.removeEventListener('resize', onResize); window.removeEventListener('pointermove', onMove); window.removeEventListener('pointerleave', onLeave); }; }, []); return (