95 lines
2.3 KiB
TypeScript
95 lines
2.3 KiB
TypeScript
import { useEffect, useRef } from 'react'
|
|
import { useReducedMotion } from '../hooks/useReducedMotion'
|
|
|
|
interface Particle {
|
|
x: number
|
|
y: number
|
|
size: number
|
|
speedX: number
|
|
speedY: number
|
|
opacity: number
|
|
color: string
|
|
}
|
|
|
|
export default function FloatingParticles() {
|
|
const canvasRef = useRef<HTMLCanvasElement>(null)
|
|
const reducedMotion = useReducedMotion()
|
|
|
|
useEffect(() => {
|
|
if (reducedMotion) return
|
|
|
|
const canvas = canvasRef.current
|
|
if (!canvas) return
|
|
|
|
const ctx = canvas.getContext('2d')
|
|
if (!ctx) return
|
|
|
|
let animationId: number
|
|
let particles: Particle[] = []
|
|
|
|
const resize = () => {
|
|
canvas.width = window.innerWidth
|
|
canvas.height = window.innerHeight
|
|
}
|
|
resize()
|
|
window.addEventListener('resize', resize)
|
|
|
|
const colors = ['rgba(255, 45, 122, 0.15)', 'rgba(255, 107, 157, 0.12)', 'rgba(255, 182, 193, 0.1)']
|
|
|
|
const createParticles = () => {
|
|
particles = []
|
|
const count = Math.min(Math.floor(window.innerWidth / 15), 80)
|
|
for (let i = 0; i < count; i++) {
|
|
particles.push({
|
|
x: Math.random() * canvas.width,
|
|
y: Math.random() * canvas.height,
|
|
size: Math.random() * 3 + 1,
|
|
speedX: (Math.random() - 0.5) * 0.3,
|
|
speedY: (Math.random() - 0.5) * 0.3,
|
|
opacity: Math.random() * 0.5 + 0.1,
|
|
color: colors[Math.floor(Math.random() * colors.length)],
|
|
})
|
|
}
|
|
}
|
|
createParticles()
|
|
|
|
const animate = () => {
|
|
ctx.clearRect(0, 0, canvas.width, canvas.height)
|
|
|
|
particles.forEach((p) => {
|
|
p.x += p.speedX
|
|
p.y += p.speedY
|
|
|
|
if (p.x < 0) p.x = canvas.width
|
|
if (p.x > canvas.width) p.x = 0
|
|
if (p.y < 0) p.y = canvas.height
|
|
if (p.y > canvas.height) p.y = 0
|
|
|
|
ctx.beginPath()
|
|
ctx.arc(p.x, p.y, p.size, 0, Math.PI * 2)
|
|
ctx.fillStyle = p.color
|
|
ctx.fill()
|
|
})
|
|
|
|
animationId = requestAnimationFrame(animate)
|
|
}
|
|
|
|
animate()
|
|
|
|
return () => {
|
|
cancelAnimationFrame(animationId)
|
|
window.removeEventListener('resize', resize)
|
|
}
|
|
}, [reducedMotion])
|
|
|
|
if (reducedMotion) return null
|
|
|
|
return (
|
|
<canvas
|
|
ref={canvasRef}
|
|
className="fixed inset-0 z-0 pointer-events-none"
|
|
style={{ opacity: 0.6 }}
|
|
/>
|
|
)
|
|
}
|