'use client'; import { useEffect, useRef, useState, useCallback } from 'react'; import { motion, useMotionValue } from 'framer-motion'; interface Particle { x: number; y: number; vx: number; vy: number; size: number; opacity: number; } interface ParticleGalaxyProps { particleCount?: number; connectionDistance?: number; mouseRadius?: number; particleColor?: string; lineColor?: string; className?: string; } export function ParticleGalaxy({ particleCount = 100, connectionDistance = 150, mouseRadius = 150, particleColor = '196, 30, 58', lineColor = '196, 30, 58', className = '' }: ParticleGalaxyProps) { const canvasRef = useRef(null); const particlesRef = useRef([]); const animationRef = useRef(undefined); const [isVisible, setIsVisible] = useState(false); const mouseX = useMotionValue(0); const mouseY = useMotionValue(0); const mouseInCanvas = useMotionValue(false); const createParticle = useCallback((width: number, height: number): Particle => { return { x: Math.random() * width, y: Math.random() * height, vx: (Math.random() - 0.5) * 0.8, vy: (Math.random() - 0.5) * 0.8, size: Math.random() * 2 + 1, opacity: Math.random() * 0.5 + 0.2 }; }, []); const initParticles = useCallback((width: number, height: number) => { particlesRef.current = Array.from({ length: particleCount }, () => createParticle(width, height) ); }, [particleCount, createParticle]); const drawParticles = useCallback((ctx: CanvasRenderingContext2D, width: number, height: number) => { const mx = mouseX.get(); const my = mouseY.get(); const inCanvas = mouseInCanvas.get(); ctx.clearRect(0, 0, width, height); const particles = particlesRef.current; particles.forEach((particle, i) => { let { x, y, vx, vy, size, opacity } = particle; if (inCanvas) { const dx = x - mx; const dy = y - my; const distance = Math.sqrt(dx * dx + dy * dy); if (distance < mouseRadius) { const force = (mouseRadius - distance) / mouseRadius; const angle = Math.atan2(dy, dx); vx += Math.cos(angle) * force * 0.5; vy += Math.sin(angle) * force * 0.5; } } x += vx; y += vy; if (x < 0 || x > width) {vx *= -1;} if (y < 0 || y > height) {vy *= -1;} x = Math.max(0, Math.min(width, x)); y = Math.max(0, Math.min(height, y)); vx *= 0.99; vy *= 0.99; const speed = Math.sqrt(vx * vx + vy * vy); if (speed < 0.1) { vx += (Math.random() - 0.5) * 0.1; vy += (Math.random() - 0.5) * 0.1; } particlesRef.current[i] = { x, y, vx, vy, size, opacity }; ctx.beginPath(); ctx.arc(x, y, size, 0, Math.PI * 2); ctx.fillStyle = `rgba(${particleColor}, ${opacity})`; ctx.fill(); }); for (let i = 0; i < particles.length; i++) { for (let j = i + 1; j < particles.length; j++) { const p1 = particles[i]; const p2 = particles[j]; if (!p1 || !p2) {continue;} const dx = p1.x - p2.x; const dy = p1.y - p2.y; const distance = Math.sqrt(dx * dx + dy * dy); if (distance < connectionDistance) { const opacity = (1 - distance / connectionDistance) * 0.3; ctx.beginPath(); ctx.moveTo(p1.x, p1.y); ctx.lineTo(p2.x, p2.y); ctx.strokeStyle = `rgba(${lineColor}, ${opacity})`; ctx.lineWidth = 0.5; ctx.stroke(); } } } }, [mouseX, mouseY, mouseInCanvas, mouseRadius, particleColor, lineColor, connectionDistance]); const animate = useCallback(() => { const canvas = canvasRef.current; if (!canvas) {return;} const ctx = canvas.getContext('2d'); if (!ctx) {return;} drawParticles(ctx, canvas.width, canvas.height); animationRef.current = requestAnimationFrame(animate); }, [drawParticles]); const handleResize = useCallback(() => { const canvas = canvasRef.current; if (!canvas) {return;} const container = canvas.parentElement; if (!container) {return;} canvas.width = container.clientWidth; canvas.height = container.clientHeight; initParticles(canvas.width, canvas.height); }, [initParticles]); useEffect(() => { const canvas = canvasRef.current; if (!canvas) {return;} const container = canvas.parentElement; if (!container) {return;} canvas.width = container.clientWidth; canvas.height = container.clientHeight; initParticles(canvas.width, canvas.height); const observer = new IntersectionObserver( (entries) => { entries.forEach((entry) => { setIsVisible(entry.isIntersecting); }); }, { threshold: 0.1 } ); observer.observe(canvas); if (isVisible) { animate(); } const handleResizeWithDebounce = () => { setTimeout(handleResize, 250); }; window.addEventListener('resize', handleResizeWithDebounce); return () => { observer.disconnect(); window.removeEventListener('resize', handleResizeWithDebounce); if (animationRef.current) { cancelAnimationFrame(animationRef.current); } }; }, [isVisible, animate, initParticles, handleResize]); useEffect(() => { if (isVisible) { animate(); } else if (animationRef.current) { cancelAnimationFrame(animationRef.current); } }, [isVisible, animate]); return ( { mouseX.set(e.clientX); mouseY.set(e.clientY); mouseInCanvas.set(true); }} onMouseLeave={() => { mouseInCanvas.set(false); }} /> ); } export default ParticleGalaxy;