| import { useEffect, useRef, useState } from 'react'; |
|
|
| export default function ParticleNetwork() { |
| const canvasRef = useRef<HTMLCanvasElement>(null); |
| const [isMobile, setIsMobile] = useState(false); |
|
|
| useEffect(() => { |
| const isMobileDevice = ('ontouchstart' in window) || (navigator.maxTouchPoints > 0) || window.innerWidth < 768; |
| setIsMobile(isMobileDevice); |
| if (isMobileDevice) return; |
|
|
| const canvas = canvasRef.current; |
| if (!canvas) return; |
|
|
| const ctx = canvas.getContext('2d'); |
| if (!ctx) return; |
|
|
| let animationFrameId: number; |
| let particles: Particle[] = []; |
| |
| const particleCount = 40; |
| const connectionDistance = 180; |
| const mouseRadius = 120; |
|
|
| const mouse = { |
| x: -1000, |
| y: -1000, |
| }; |
|
|
| class Particle { |
| x: number; |
| y: number; |
| vx: number; |
| vy: number; |
| size: number; |
|
|
| constructor(width: number, height: number) { |
| this.x = Math.random() * width; |
| this.y = Math.random() * height; |
| this.vx = (Math.random() - 0.5) * 0.3; |
| this.vy = (Math.random() - 0.5) * 0.3; |
| this.size = Math.random() * 2 + 1; |
| } |
|
|
| update(width: number, height: number) { |
| this.x += this.vx; |
| this.y += this.vy; |
|
|
| if (this.x < 0 || this.x > width) this.vx *= -1; |
| if (this.y < 0 || this.y > height) this.vy *= -1; |
|
|
| |
| const dx = mouse.x - this.x; |
| const dy = mouse.y - this.y; |
| const distance = Math.sqrt(dx * dx + dy * dy); |
|
|
| if (distance < mouseRadius) { |
| const force = (mouseRadius - distance) / mouseRadius; |
| this.x -= dx * force * 0.02; |
| this.y -= dy * force * 0.02; |
| } |
| } |
|
|
| draw() { |
| if (!ctx) return; |
| ctx.beginPath(); |
| ctx.arc(this.x, this.y, this.size, 0, Math.PI * 2); |
| ctx.fillStyle = 'rgba(255, 255, 255, 0.15)'; |
| ctx.fill(); |
| } |
| } |
|
|
| const init = () => { |
| canvas.width = window.innerWidth; |
| canvas.height = window.innerHeight; |
| particles = []; |
| for (let i = 0; i < particleCount; i++) { |
| particles.push(new Particle(canvas.width, canvas.height)); |
| } |
| }; |
|
|
| const animate = () => { |
| ctx.clearRect(0, 0, canvas.width, canvas.height); |
|
|
| for (let i = 0; i < particles.length; i++) { |
| particles[i].update(canvas.width, canvas.height); |
| particles[i].draw(); |
|
|
| 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 distance = Math.sqrt(dx * dx + dy * dy); |
|
|
| if (distance < connectionDistance) { |
| ctx.beginPath(); |
| ctx.moveTo(particles[i].x, particles[i].y); |
| ctx.lineTo(particles[j].x, particles[j].y); |
| ctx.strokeStyle = `rgba(255, 255, 255, ${0.1 * (1 - distance / connectionDistance)})`; |
| ctx.lineWidth = 0.5; |
| ctx.stroke(); |
| } |
| } |
| } |
|
|
| animationFrameId = requestAnimationFrame(animate); |
| }; |
|
|
| const handleMouseMove = (e: MouseEvent) => { |
| mouse.x = e.clientX; |
| mouse.y = e.clientY; |
| }; |
|
|
| let resizeTimeout: NodeJS.Timeout; |
| const handleResize = () => { |
| clearTimeout(resizeTimeout); |
| resizeTimeout = setTimeout(() => { |
| init(); |
| }, 150); |
| }; |
|
|
| window.addEventListener('mousemove', handleMouseMove); |
| window.addEventListener('resize', handleResize); |
|
|
| init(); |
| animate(); |
|
|
| return () => { |
| window.removeEventListener('mousemove', handleMouseMove); |
| window.removeEventListener('resize', handleResize); |
| clearTimeout(resizeTimeout); |
| cancelAnimationFrame(animationFrameId); |
| }; |
| }, []); |
|
|
| if (isMobile) return null; |
|
|
| return ( |
| <canvas |
| ref={canvasRef} |
| className="fixed inset-0 w-full h-full pointer-events-none z-0" |
| style={{ opacity: 0.8 }} |
| /> |
| ); |
| } |
|
|