Portfolio / src /components /ParticleNetwork.tsx
teganmosi
Initial commit of portfolio
4dd77d6
Raw
History Blame Contribute Delete
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 }}
/>
);
}