jeffemmett-website-redesign/components/cursor-effect.tsx

162 lines
4.4 KiB
TypeScript

'use client'
import { useEffect, useRef } from 'react'
interface Spore {
x: number
y: number
vx: number
vy: number
life: number
maxLife: number
size: number
}
export function CursorEffect() {
const canvasRef = useRef<HTMLCanvasElement>(null)
const sporesRef = useRef<Spore[]>([])
const mouseRef = useRef({ x: 0, y: 0 })
const frameRef = useRef<number>(0)
useEffect(() => {
const canvas = canvasRef.current
if (!canvas) return
const ctx = canvas.getContext('2d')
if (!ctx) return
const resize = () => {
canvas.width = window.innerWidth
canvas.height = window.innerHeight
}
resize()
window.addEventListener('resize', resize)
const handleMouseMove = (e: MouseEvent) => {
const dx = e.clientX - mouseRef.current.x
const dy = e.clientY - mouseRef.current.y
const speed = Math.sqrt(dx * dx + dy * dy)
mouseRef.current = { x: e.clientX, y: e.clientY }
// Spawn spores based on movement speed
if (speed > 3) {
const count = Math.min(Math.floor(speed / 8), 3)
for (let i = 0; i < count; i++) {
const angle = Math.atan2(dy, dx) + Math.PI + (Math.random() - 0.5) * 1.5
const sporeSpeed = 0.5 + Math.random() * 1.5
sporesRef.current.push({
x: e.clientX + (Math.random() - 0.5) * 10,
y: e.clientY + (Math.random() - 0.5) * 10,
vx: Math.cos(angle) * sporeSpeed,
vy: Math.sin(angle) * sporeSpeed,
life: 1,
maxLife: 80 + Math.random() * 60,
size: 1.5 + Math.random() * 2.5,
})
}
}
}
window.addEventListener('mousemove', handleMouseMove)
const animate = () => {
// Clear canvas completely each frame - no lingering
ctx.clearRect(0, 0, canvas.width, canvas.height)
const spores = sporesRef.current
// Update and draw spores
sporesRef.current = spores.filter(spore => {
// Drift toward other nearby spores (flocking)
let avgX = 0, avgY = 0, count = 0
for (const other of spores) {
if (other === spore) continue
const dx = other.x - spore.x
const dy = other.y - spore.y
const dist = Math.sqrt(dx * dx + dy * dy)
if (dist < 80 && dist > 5) {
avgX += dx / dist
avgY += dy / dist
count++
// Draw faint connection lines
if (dist < 50 && spore.life > 0.3 && other.life > 0.3) {
const alpha = Math.min(spore.life, other.life) * 0.15 * (1 - dist / 50)
ctx.beginPath()
ctx.moveTo(spore.x, spore.y)
ctx.lineTo(other.x, other.y)
ctx.strokeStyle = `rgba(180, 160, 140, ${alpha})`
ctx.lineWidth = 0.5
ctx.stroke()
}
}
}
// Apply gentle flocking
if (count > 0) {
spore.vx += (avgX / count) * 0.02
spore.vy += (avgY / count) * 0.02
}
// Add slight upward drift
spore.vy -= 0.01
// Damping
spore.vx *= 0.98
spore.vy *= 0.98
// Update position
spore.x += spore.vx
spore.y += spore.vy
// Decay
spore.life -= 1 / spore.maxLife
// Draw spore
if (spore.life > 0) {
const alpha = spore.life * 0.7
const size = spore.size * (0.5 + spore.life * 0.5)
ctx.beginPath()
ctx.arc(spore.x, spore.y, size, 0, Math.PI * 2)
ctx.fillStyle = `rgba(200, 175, 140, ${alpha})`
ctx.fill()
// Soft glow
ctx.beginPath()
ctx.arc(spore.x, spore.y, size * 2, 0, Math.PI * 2)
ctx.fillStyle = `rgba(200, 175, 140, ${alpha * 0.2})`
ctx.fill()
}
return spore.life > 0
})
// Limit spore count
if (sporesRef.current.length > 150) {
sporesRef.current = sporesRef.current.slice(-150)
}
frameRef.current = requestAnimationFrame(animate)
}
frameRef.current = requestAnimationFrame(animate)
return () => {
window.removeEventListener('resize', resize)
window.removeEventListener('mousemove', handleMouseMove)
cancelAnimationFrame(frameRef.current)
}
}, [])
return (
<canvas
ref={canvasRef}
className="fixed inset-0 pointer-events-none z-50"
style={{ opacity: 0.9 }}
/>
)
}