"use client"; /** * 自实现 Canvas 粒子背景 - 封装待用 * 无第三方依赖,轻量级替代方案 */ import { useEffect, useRef } from "react"; const PARTICLE_COUNT = 50; const CONNECT_DISTANCE = 120; type Particle = { x: number; y: number; vx: number; vy: number; r: number; opacity: number; }; export function CustomParticleBackground() { const canvasRef = useRef(null); useEffect(() => { const canvas = canvasRef.current; if (!canvas) return; const ctx = canvas.getContext("2d"); if (!ctx) return; let animationId: number; let particles: Particle[] = []; const resize = () => { const dpr = Math.min(window.devicePixelRatio || 1, 2); const w = canvas.offsetWidth; const h = canvas.offsetHeight; canvas.width = w * dpr; canvas.height = h * dpr; ctx.scale(dpr, dpr); canvas.style.width = `${w}px`; canvas.style.height = `${h}px`; particles = Array.from({ length: PARTICLE_COUNT }, () => ({ x: Math.random() * w, y: Math.random() * h, vx: (Math.random() - 0.5) * 0.3, vy: (Math.random() - 0.5) * 0.3, r: Math.random() * 2 + 0.5, opacity: Math.random() * 0.4 + 0.1, })); }; const getAccentRgb = () => { const style = getComputedStyle(document.documentElement); const accent = style.getPropertyValue("--accent").trim() || "#f59e0b"; const m = accent.match(/^#([0-9a-f]{2})([0-9a-f]{2})([0-9a-f]{2})$/i); if (m) return `${parseInt(m[1], 16)}, ${parseInt(m[2], 16)}, ${parseInt(m[3], 16)}`; return "245, 158, 11"; }; const draw = () => { const w = canvas.offsetWidth; const h = canvas.offsetHeight; const rgb = getAccentRgb(); ctx.clearRect(0, 0, w, h); particles.forEach((p, i) => { p.x += p.vx; p.y += p.vy; if (p.x < 0 || p.x > w) p.vx *= -1; if (p.y < 0 || p.y > h) p.vy *= -1; ctx.beginPath(); ctx.arc(p.x, p.y, p.r, 0, Math.PI * 2); ctx.fillStyle = `rgba(${rgb}, ${p.opacity})`; ctx.fill(); for (let j = i + 1; j < particles.length; j++) { const q = particles[j]; const dx = p.x - q.x; const dy = p.y - q.y; const dist = Math.hypot(dx, dy); if (dist < CONNECT_DISTANCE) { ctx.beginPath(); ctx.moveTo(p.x, p.y); ctx.lineTo(q.x, q.y); ctx.strokeStyle = `rgba(${rgb}, ${0.08 * (1 - dist / CONNECT_DISTANCE)})`; ctx.lineWidth = 0.5; ctx.stroke(); } } }); animationId = requestAnimationFrame(draw); }; resize(); window.addEventListener("resize", resize); draw(); return () => { window.removeEventListener("resize", resize); cancelAnimationFrame(animationId); }; }, []); return ( ); }