155 lines
4.4 KiB
TypeScript
155 lines
4.4 KiB
TypeScript
"use client"
|
|
|
|
import { useEffect, useRef } from "react"
|
|
|
|
interface Trail {
|
|
points: { x: number; y: number }[]
|
|
color: string
|
|
opacity: number
|
|
createdAt: number
|
|
}
|
|
|
|
export function CursorTrails() {
|
|
const canvasRef = useRef<HTMLCanvasElement>(null)
|
|
const trailsRef = useRef<Trail[]>([])
|
|
const mouseRef = useRef({ x: 0, y: 0, lastX: 0, lastY: 0 })
|
|
const animationRef = useRef<number>()
|
|
|
|
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 colors = [
|
|
"rgba(0, 255, 255, 0.6)", // Cyan
|
|
"rgba(255, 0, 255, 0.6)", // Magenta
|
|
"rgba(138, 43, 226, 0.6)", // Purple
|
|
"rgba(255, 140, 0, 0.6)", // Orange
|
|
"rgba(20, 20, 30, 0.8)", // Near-black
|
|
"rgba(0, 200, 200, 0.6)", // Teal
|
|
"rgba(255, 100, 50, 0.6)", // Orange-red
|
|
]
|
|
|
|
let colorIndex = 0
|
|
let frameCount = 0
|
|
|
|
const handleMouseMove = (e: MouseEvent) => {
|
|
mouseRef.current.lastX = mouseRef.current.x
|
|
mouseRef.current.lastY = mouseRef.current.y
|
|
mouseRef.current.x = e.clientX
|
|
mouseRef.current.y = e.clientY
|
|
|
|
const dx = mouseRef.current.x - mouseRef.current.lastX
|
|
const dy = mouseRef.current.y - mouseRef.current.lastY
|
|
const distance = Math.sqrt(dx * dx + dy * dy)
|
|
|
|
if (distance > 5 && frameCount % 2 === 0) {
|
|
const points: { x: number; y: number }[] = []
|
|
const steps = 20
|
|
|
|
// Create a perpendicular vector for sine wave oscillation
|
|
const perpX = -dy / distance
|
|
const perpY = dx / distance
|
|
|
|
// Random frequency and amplitude for varied squiggles
|
|
const frequency = 0.3 + Math.random() * 0.5
|
|
const amplitude = 15 + Math.random() * 25
|
|
|
|
for (let i = 0; i <= steps; i++) {
|
|
const t = i / steps
|
|
// Base position along the line
|
|
const baseX = mouseRef.current.lastX + dx * t
|
|
const baseY = mouseRef.current.lastY + dy * t
|
|
|
|
// Add sine wave oscillation perpendicular to movement direction
|
|
const wave = Math.sin(t * Math.PI * frequency * 4) * amplitude * (1 - t * 0.3)
|
|
|
|
points.push({
|
|
x: baseX + perpX * wave,
|
|
y: baseY + perpY * wave,
|
|
})
|
|
}
|
|
|
|
trailsRef.current.push({
|
|
points,
|
|
color: colors[colorIndex],
|
|
opacity: 1,
|
|
createdAt: Date.now(),
|
|
})
|
|
|
|
colorIndex = (colorIndex + 1) % colors.length
|
|
}
|
|
frameCount++
|
|
}
|
|
|
|
const animate = () => {
|
|
ctx.clearRect(0, 0, canvas.width, canvas.height)
|
|
|
|
const now = Date.now()
|
|
trailsRef.current = trailsRef.current.filter((trail) => {
|
|
const age = now - trail.createdAt
|
|
const maxAge = 2000
|
|
|
|
if (age > maxAge) return false
|
|
|
|
trail.opacity = 1 - age / maxAge
|
|
|
|
ctx.beginPath()
|
|
ctx.strokeStyle = trail.color.replace(/[\d.]+\)$/, `${trail.opacity * 0.6})`)
|
|
ctx.lineWidth = 2 + trail.opacity * 2
|
|
ctx.lineCap = "round"
|
|
ctx.lineJoin = "round"
|
|
|
|
if (trail.points.length > 0) {
|
|
ctx.moveTo(trail.points[0].x, trail.points[0].y)
|
|
|
|
for (let i = 1; i < trail.points.length - 1; i++) {
|
|
const xc = (trail.points[i].x + trail.points[i + 1].x) / 2
|
|
const yc = (trail.points[i].y + trail.points[i + 1].y) / 2
|
|
ctx.quadraticCurveTo(trail.points[i].x, trail.points[i].y, xc, yc)
|
|
}
|
|
|
|
if (trail.points.length > 1) {
|
|
const last = trail.points[trail.points.length - 1]
|
|
ctx.lineTo(last.x, last.y)
|
|
}
|
|
}
|
|
|
|
// Add glow effect
|
|
ctx.shadowBlur = 15 * trail.opacity
|
|
ctx.shadowColor = trail.color
|
|
ctx.stroke()
|
|
ctx.shadowBlur = 0
|
|
|
|
return true
|
|
})
|
|
|
|
animationRef.current = requestAnimationFrame(animate)
|
|
}
|
|
|
|
window.addEventListener("mousemove", handleMouseMove)
|
|
animate()
|
|
|
|
return () => {
|
|
window.removeEventListener("mousemove", handleMouseMove)
|
|
window.removeEventListener("resize", resize)
|
|
if (animationRef.current) {
|
|
cancelAnimationFrame(animationRef.current)
|
|
}
|
|
}
|
|
}, [])
|
|
|
|
return (
|
|
<canvas ref={canvasRef} className="pointer-events-none fixed inset-0 z-50" style={{ mixBlendMode: "screen" }} />
|
|
)
|
|
}
|