127 lines
3.3 KiB
TypeScript
127 lines
3.3 KiB
TypeScript
import { useEffect, useRef } from 'react'
|
|
|
|
interface Particle {
|
|
x: number
|
|
y: number
|
|
size: number
|
|
speedX: number
|
|
speedY: number
|
|
opacity: number
|
|
}
|
|
|
|
export default function ParticleBackground() {
|
|
const canvasRef = useRef<HTMLCanvasElement>(null)
|
|
|
|
useEffect(() => {
|
|
const canvas = canvasRef.current
|
|
if (!canvas) return
|
|
|
|
const ctx = canvas.getContext('2d')
|
|
if (!ctx) return
|
|
|
|
let animationId: number
|
|
let particles: Particle[] = []
|
|
let mouseX = 0
|
|
let mouseY = 0
|
|
|
|
const resize = () => {
|
|
canvas.width = window.innerWidth
|
|
canvas.height = window.innerHeight
|
|
}
|
|
|
|
const createParticles = () => {
|
|
particles = []
|
|
const count = Math.floor((canvas.width * canvas.height) / 15000)
|
|
for (let i = 0; i < count; i++) {
|
|
particles.push({
|
|
x: Math.random() * canvas.width,
|
|
y: Math.random() * canvas.height,
|
|
size: Math.random() * 2 + 0.5,
|
|
speedX: (Math.random() - 0.5) * 0.5,
|
|
speedY: (Math.random() - 0.5) * 0.5,
|
|
opacity: Math.random() * 0.5 + 0.1
|
|
})
|
|
}
|
|
}
|
|
|
|
const drawParticles = () => {
|
|
ctx.clearRect(0, 0, canvas.width, canvas.height)
|
|
|
|
particles.forEach((particle, i) => {
|
|
particle.x += particle.speedX
|
|
particle.y += particle.speedY
|
|
|
|
if (particle.x < 0) particle.x = canvas.width
|
|
if (particle.x > canvas.width) particle.x = 0
|
|
if (particle.y < 0) particle.y = canvas.height
|
|
if (particle.y > canvas.height) particle.y = 0
|
|
|
|
ctx.beginPath()
|
|
ctx.arc(particle.x, particle.y, particle.size, 0, Math.PI * 2)
|
|
ctx.fillStyle = `rgba(102, 126, 234, ${particle.opacity})`
|
|
ctx.fill()
|
|
|
|
particles.forEach((particle2, j) => {
|
|
if (i === j) return
|
|
const dx = particle.x - particle2.x
|
|
const dy = particle.y - particle2.y
|
|
const distance = Math.sqrt(dx * dx + dy * dy)
|
|
|
|
if (distance < 150) {
|
|
ctx.beginPath()
|
|
ctx.strokeStyle = `rgba(102, 126, 234, ${0.15 * (1 - distance / 150)})`
|
|
ctx.lineWidth = 0.5
|
|
ctx.moveTo(particle.x, particle.y)
|
|
ctx.lineTo(particle2.x, particle2.y)
|
|
ctx.stroke()
|
|
}
|
|
})
|
|
|
|
const dx = particle.x - mouseX
|
|
const dy = particle.y - mouseY
|
|
const distance = Math.sqrt(dx * dx + dy * dy)
|
|
|
|
if (distance < 200) {
|
|
const force = (200 - distance) / 200
|
|
particle.speedX += (dx / distance) * force * 0.02
|
|
particle.speedY += (dy / distance) * force * 0.02
|
|
}
|
|
|
|
particle.speedX *= 0.99
|
|
particle.speedY *= 0.99
|
|
})
|
|
|
|
animationId = requestAnimationFrame(drawParticles)
|
|
}
|
|
|
|
const handleMouseMove = (e: MouseEvent) => {
|
|
mouseX = e.clientX
|
|
mouseY = e.clientY
|
|
}
|
|
|
|
resize()
|
|
createParticles()
|
|
drawParticles()
|
|
|
|
window.addEventListener('resize', () => {
|
|
resize()
|
|
createParticles()
|
|
})
|
|
window.addEventListener('mousemove', handleMouseMove)
|
|
|
|
return () => {
|
|
cancelAnimationFrame(animationId)
|
|
window.removeEventListener('resize', resize)
|
|
window.removeEventListener('mousemove', handleMouseMove)
|
|
}
|
|
}, [])
|
|
|
|
return (
|
|
<canvas
|
|
ref={canvasRef}
|
|
className="absolute inset-0 pointer-events-none"
|
|
style={{ zIndex: 1 }}
|
|
/>
|
|
)
|
|
}
|