'use client'; import { useEffect, useRef } from 'react'; interface Particle { x: number; y: number; vx: number; vy: number; radius: number; alpha: number; type: 'lost' | 'converted' | 'regular'; pulseSpeed: number; pulseTime: number; } export default function ProblemParticles() { const canvasRef = useRef(null); useEffect(() => { const canvas = canvasRef.current; if (!canvas) return; const ctx = canvas.getContext('2d'); if (!ctx) return; let animationFrameId = 0; let cssWidth = canvas.offsetWidth; let cssHeight = canvas.offsetHeight; const particles: Particle[] = []; const storeNode = { x: cssWidth / 2, y: cssHeight / 2, radius: 18 }; const setCanvasSize = () => { const dpr = Math.max(1, window.devicePixelRatio || 1); cssWidth = canvas.offsetWidth; cssHeight = canvas.offsetHeight; canvas.width = Math.round(cssWidth * dpr); canvas.height = Math.round(cssHeight * dpr); // Map drawing coordinates to CSS pixels ctx.setTransform(dpr, 0, 0, dpr, 0, 0); storeNode.x = cssWidth / 2; storeNode.y = cssHeight / 2; }; const createParticle = (isInitial = false): Particle => { const typeRand = Math.random(); const type = typeRand < 0.2 ? 'lost' : typeRand < 0.45 ? 'converted' : 'regular'; let x: number, y: number; if (isInitial) { x = Math.random() * cssWidth; y = Math.random() * cssHeight; } else { // Spawn from edges if (Math.random() < 0.5) { x = Math.random() < 0.5 ? 0 : cssWidth; y = Math.random() * cssHeight; } else { x = Math.random() * cssWidth; y = Math.random() < 0.5 ? 0 : cssHeight; } } // Vector pointing towards the store node (center) const angle = Math.atan2(storeNode.y - y, storeNode.x - x); const speed = 0.4 + Math.random() * 0.8; return { x, y, vx: Math.cos(angle) * speed + (Math.random() - 0.5) * 0.2, vy: Math.sin(angle) * speed + (Math.random() - 0.5) * 0.2, radius: 2 + Math.random() * 4, alpha: 0.1 + Math.random() * 0.7, type, pulseSpeed: 0.05 + Math.random() * 0.05, pulseTime: Math.random() * Math.PI, }; }; // Initialize setCanvasSize(); for (let i = 0; i < 45; i++) particles.push(createParticle(true)); const handleResize = () => { if (!canvas) return; setCanvasSize(); }; window.addEventListener('resize', handleResize); const draw = () => { ctx.clearRect(0, 0, cssWidth, cssHeight); // Background glow const bgGlow = ctx.createRadialGradient( storeNode.x, storeNode.y, 10, storeNode.x, storeNode.y, cssWidth * 0.4 ); bgGlow.addColorStop(0, 'rgba(0, 102, 255, 0.08)'); bgGlow.addColorStop(1, 'rgba(0, 0, 0, 0)'); ctx.fillStyle = bgGlow; ctx.fillRect(0, 0, cssWidth, cssHeight); // Store hub ctx.shadowBlur = 30; ctx.shadowColor = '#0066ff'; ctx.fillStyle = 'rgba(0, 102, 255, 0.9)'; ctx.beginPath(); ctx.arc(storeNode.x, storeNode.y, storeNode.radius, 0, Math.PI * 2); ctx.fill(); ctx.strokeStyle = 'rgba(255, 255, 255, 0.6)'; ctx.lineWidth = 2; ctx.beginPath(); ctx.arc(storeNode.x, storeNode.y, storeNode.radius - 6, 0, Math.PI * 2); ctx.stroke(); ctx.shadowBlur = 0; // Particles particles.forEach((p, idx) => { p.x += p.vx; p.y += p.vy; p.pulseTime += p.pulseSpeed; let alpha = p.alpha; let radius = p.radius; if (p.type === 'converted') { alpha = Math.max(0.4, p.alpha + Math.sin(p.pulseTime) * 0.3); radius = p.radius + Math.sin(p.pulseTime) * 1.5; } else if (p.type === 'lost') { const dist = Math.hypot(storeNode.x - p.x, storeNode.y - p.y); if (dist < 150) p.alpha -= 0.008; } // Connections between particles for (let j = idx + 1; j < particles.length; j++) { const p2 = particles[j]; const dist = Math.hypot(p2.x - p.x, p2.y - p.y); if (dist < 80 && p.alpha > 0 && p2.alpha > 0) { const lineAlpha = (1 - dist / 80) * 0.18 * Math.min(alpha, p2.alpha); ctx.strokeStyle = `rgba(0, 102, 255, ${lineAlpha})`; ctx.lineWidth = 1; ctx.beginPath(); ctx.moveTo(p.x, p.y); ctx.lineTo(p2.x, p2.y); ctx.stroke(); } } // Connection to store hub const distToStore = Math.hypot(storeNode.x - p.x, storeNode.y - p.y); if (distToStore < 120 && p.alpha > 0) { const lineAlpha = (1 - distToStore / 120) * 0.25 * p.alpha; ctx.strokeStyle = p.type === 'converted' ? `rgba(0, 220, 255, ${lineAlpha})` : `rgba(0, 102, 255, ${lineAlpha})`; ctx.beginPath(); ctx.moveTo(p.x, p.y); ctx.lineTo(storeNode.x, storeNode.y); ctx.stroke(); } // Respawn check if (distToStore < storeNode.radius || p.x < 0 || p.x > cssWidth || p.y < 0 || p.y > cssHeight || p.alpha <= 0) { particles[idx] = createParticle(); } else { ctx.fillStyle = p.type === 'converted' ? `rgba(0, 220, 255, ${alpha})` : p.type === 'lost' ? `rgba(255, 68, 68, ${alpha})` : `rgba(255, 255, 255, ${alpha})`; if (p.type === 'converted') { ctx.shadowBlur = 10; ctx.shadowColor = '#00dcff'; } ctx.beginPath(); ctx.arc(p.x, p.y, radius, 0, Math.PI * 2); ctx.fill(); ctx.shadowBlur = 0; } }); animationFrameId = requestAnimationFrame(draw); }; draw(); return () => { window.removeEventListener('resize', handleResize); cancelAnimationFrame(animationFrameId); }; }, []); return ; }