Download src/components/ParticleNetwork.tsx from teganmosi/Portfolio: direct link, hf CLI and curl.
- Browser
- Download file 3.97 kB
-
https://huggingface.co/spaces/teganmosi/Portfolio/resolve/main/src/components/ParticleNetwork.tsx
- Command line
-
hf download hf://spaces/teganmosi/Portfolio/src/components/ParticleNetwork.tsx
-
curl -L -o ParticleNetwork.tsx https://huggingface.co/spaces/teganmosi/Portfolio/resolve/main/src/components/ParticleNetwork.tsx
3.97 kB
| 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; | |
| // Interaction with mouse | |
| 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 }} | |
| /> | |
| ); | |
| } | |