'use client'; import { useEffect, useRef, forwardRef, useImperativeHandle } from 'react'; const ParticlesBg = forwardRef((props, ref) => { const canvasRef = useRef(null); const targetColorRef = useRef({ r: 56, g: 189, b: 248 }); const currentColorRef = useRef({ r: 56, g: 189, b: 248 }); const hexToRgb = (hex) => { const result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex); return result ? { r: parseInt(result[1], 16), g: parseInt(result[2], 16), b: parseInt(result[3], 16) } : { r: 56, g: 189, b: 248 }; }; useImperativeHandle(ref, () => ({ setAccentColor: (hex) => { if (hex) { targetColorRef.current = hexToRgb(hex); } else { targetColorRef.current = { r: 56, g: 189, b: 248 }; } } })); useEffect(() => { const canvas = canvasRef.current; if (!canvas) return; const ctx = canvas.getContext('2d'); let animationFrameId; let particles = []; let connectionDist = 120; const BASELINE_AREA = 1920 * 1080; const BASELINE_PARTICLES = 70; const BASELINE_DIAGONAL = Math.sqrt(1920 * 1920 + 1080 * 1080); const BASELINE_CONN_DIST = 120; const MIN_PARTICLES = 12; const resize = () => { canvas.width = window.innerWidth; canvas.height = window.innerHeight; const area = canvas.width * canvas.height; const diagonal = Math.sqrt(canvas.width ** 2 + canvas.height ** 2); const areaRatio = area / BASELINE_AREA; const targetCount = Math.max(MIN_PARTICLES, Math.round(BASELINE_PARTICLES * areaRatio)); connectionDist = Math.round(BASELINE_CONN_DIST * (diagonal / BASELINE_DIAGONAL)); if (particles.length > targetCount) { particles.length = targetCount; } else { while (particles.length < targetCount) { particles.push(new Particle()); } } }; window.addEventListener('resize', resize); class Particle { constructor() { this.x = Math.random() * canvas.width; this.y = Math.random() * canvas.height; this.vx = (Math.random() - 0.5) * 0.8; this.vy = (Math.random() - 0.5) * 0.8; this.radius = Math.random() * 2 + 1; } update() { this.x += this.vx; this.y += this.vy; if (this.x < 0 || this.x > canvas.width) this.vx *= -1; if (this.y < 0 || this.y > canvas.height) this.vy *= -1; } draw(ctx, color) { ctx.beginPath(); ctx.arc(this.x, this.y, this.radius, 0, Math.PI * 2); ctx.fillStyle = `rgba(${Math.floor(color.r)}, ${Math.floor(color.g)}, ${Math.floor(color.b)}, 0.5)`; ctx.fill(); } } resize(); const animate = () => { currentColorRef.current.r += (targetColorRef.current.r - currentColorRef.current.r) * 0.05; currentColorRef.current.g += (targetColorRef.current.g - currentColorRef.current.g) * 0.05; currentColorRef.current.b += (targetColorRef.current.b - currentColorRef.current.b) * 0.05; ctx.clearRect(0, 0, canvas.width, canvas.height); particles.forEach(p => { p.update(); p.draw(ctx, currentColorRef.current); }); for (let i = 0; i < particles.length; i++) { for (let j = i + 1; j < particles.length; j++) { const dx = particles[i].x - particles[j].x; const dy = particles[i].y - particles[j].y; const dist = Math.sqrt(dx * dx + dy * dy); if (dist < connectionDist) { ctx.beginPath(); ctx.moveTo(particles[i].x, particles[i].y); ctx.lineTo(particles[j].x, particles[j].y); ctx.strokeStyle = `rgba(${Math.floor(currentColorRef.current.r)}, ${Math.floor(currentColorRef.current.g)}, ${Math.floor(currentColorRef.current.b)}, ${(1 - dist / connectionDist) * 0.4})`; ctx.lineWidth = 1; ctx.stroke(); } } } animationFrameId = requestAnimationFrame(animate); }; animate(); return () => { window.removeEventListener('resize', resize); cancelAnimationFrame(animationFrameId); }; }, []); return ( ); }); ParticlesBg.displayName = 'ParticlesBg'; export default ParticlesBg;