Spaces:
Sleeping
Sleeping
| import React, { useEffect, useRef } from 'react'; | |
| interface Particle { | |
| x: number; | |
| y: number; | |
| vx: number; | |
| vy: number; | |
| size: number; | |
| opacity: number; | |
| color: string; | |
| } | |
| export function ParticleBackground() { | |
| const canvasRef = useRef<HTMLCanvasElement>(null); | |
| const particlesRef = useRef<Particle[]>([]); | |
| const animationRef = useRef<number>(); | |
| useEffect(() => { | |
| const canvas = canvasRef.current; | |
| if (!canvas) return; | |
| const ctx = canvas.getContext('2d'); | |
| if (!ctx) return; | |
| const resizeCanvas = () => { | |
| canvas.width = window.innerWidth; | |
| canvas.height = window.innerHeight; | |
| }; | |
| const createParticles = () => { | |
| particlesRef.current = []; | |
| const particleCount = Math.min(50, Math.floor((canvas.width * canvas.height) / 15000)); | |
| for (let i = 0; i < particleCount; i++) { | |
| particlesRef.current.push({ | |
| x: Math.random() * canvas.width, | |
| y: Math.random() * canvas.height, | |
| vx: (Math.random() - 0.5) * 0.8, | |
| vy: (Math.random() - 0.5) * 0.8, | |
| size: Math.random() * 3 + 1, | |
| opacity: Math.random() * 0.5 + 0.1, | |
| color: Math.random() > 0.5 ? '#3B82F6' : '#14B8A6' | |
| }); | |
| } | |
| }; | |
| const animate = () => { | |
| ctx.clearRect(0, 0, canvas.width, canvas.height); | |
| particlesRef.current.forEach((particle, index) => { | |
| // Update position | |
| particle.x += particle.vx; | |
| particle.y += particle.vy; | |
| // Bounce off edges | |
| if (particle.x < 0 || particle.x > canvas.width) particle.vx *= -1; | |
| if (particle.y < 0 || particle.y > canvas.height) particle.vy *= -1; | |
| // Keep particles in bounds | |
| particle.x = Math.max(0, Math.min(canvas.width, particle.x)); | |
| particle.y = Math.max(0, Math.min(canvas.height, particle.y)); | |
| // Draw particle | |
| ctx.beginPath(); | |
| ctx.arc(particle.x, particle.y, particle.size, 0, Math.PI * 2); | |
| ctx.fillStyle = `${particle.color}${Math.floor(particle.opacity * 255).toString(16).padStart(2, '0')}`; | |
| ctx.fill(); | |
| // Draw connections | |
| particlesRef.current.slice(index + 1).forEach(otherParticle => { | |
| const dx = particle.x - otherParticle.x; | |
| const dy = particle.y - otherParticle.y; | |
| const distance = Math.sqrt(dx * dx + dy * dy); | |
| if (distance < 100) { | |
| const opacity = (1 - distance / 100) * 0.2; | |
| ctx.beginPath(); | |
| ctx.moveTo(particle.x, particle.y); | |
| ctx.lineTo(otherParticle.x, otherParticle.y); | |
| ctx.strokeStyle = `#60A5FA${Math.floor(opacity * 255).toString(16).padStart(2, '0')}`; | |
| ctx.lineWidth = 1; | |
| ctx.stroke(); | |
| } | |
| }); | |
| }); | |
| animationRef.current = requestAnimationFrame(animate); | |
| }; | |
| resizeCanvas(); | |
| createParticles(); | |
| animate(); | |
| const handleResize = () => { | |
| resizeCanvas(); | |
| createParticles(); | |
| }; | |
| window.addEventListener('resize', handleResize); | |
| return () => { | |
| window.removeEventListener('resize', handleResize); | |
| if (animationRef.current) { | |
| cancelAnimationFrame(animationRef.current); | |
| } | |
| }; | |
| }, []); | |
| return ( | |
| <canvas | |
| ref={canvasRef} | |
| className="absolute inset-0 w-full h-full pointer-events-none opacity-60 dark:opacity-40" | |
| /> | |
| ); | |
| } |