131 lines
3.6 KiB
TypeScript
131 lines
3.6 KiB
TypeScript
"use client"
|
|
|
|
import type React from "react"
|
|
|
|
import { useEffect, useState } from "react"
|
|
|
|
interface Particle {
|
|
id: number
|
|
x: number
|
|
y: number
|
|
type: "sparkle" | "rainbow"
|
|
color?: string
|
|
vx?: number
|
|
vy?: number
|
|
angle?: number
|
|
}
|
|
|
|
export function CursorEffects() {
|
|
const [particles, setParticles] = useState<Particle[]>([])
|
|
|
|
useEffect(() => {
|
|
let particleId = 0
|
|
let lastTime = Date.now()
|
|
|
|
const handleMouseMove = (e: MouseEvent) => {
|
|
const now = Date.now()
|
|
// Throttle particle creation to every 50ms
|
|
if (now - lastTime < 50) return
|
|
lastTime = now
|
|
|
|
const newParticles: Particle[] = []
|
|
|
|
for (let i = 0; i < 3; i++) {
|
|
const angle = Math.random() * Math.PI * 2
|
|
const speed = 2 + Math.random() * 3
|
|
newParticles.push({
|
|
id: particleId++,
|
|
x: e.clientX,
|
|
y: e.clientY,
|
|
type: "sparkle",
|
|
color: [
|
|
"#ff0000",
|
|
"#ff9a00",
|
|
"#d0de21",
|
|
"#4fdc4a",
|
|
"#3fdad8",
|
|
"#2fc9e2",
|
|
"#1c7fee",
|
|
"#5f15f2",
|
|
"#ba0cf8",
|
|
"#fb07d9",
|
|
][Math.floor(Math.random() * 10)],
|
|
vx: Math.cos(angle) * speed,
|
|
vy: Math.sin(angle) * speed,
|
|
angle: angle,
|
|
})
|
|
}
|
|
|
|
for (let i = 0; i < 2; i++) {
|
|
const angle = Math.random() * Math.PI * 2
|
|
const speed = 3 + Math.random() * 2
|
|
newParticles.push({
|
|
id: particleId++,
|
|
x: e.clientX,
|
|
y: e.clientY,
|
|
type: "rainbow",
|
|
vx: Math.cos(angle) * speed,
|
|
vy: Math.sin(angle) * speed,
|
|
angle: angle,
|
|
})
|
|
}
|
|
|
|
setParticles((prev) => [...prev, ...newParticles])
|
|
|
|
// Remove old particles after animation
|
|
setTimeout(() => {
|
|
setParticles((prev) => prev.filter((p) => !newParticles.find((np) => np.id === p.id)))
|
|
}, 1000)
|
|
}
|
|
|
|
window.addEventListener("mousemove", handleMouseMove)
|
|
return () => window.removeEventListener("mousemove", handleMouseMove)
|
|
}, [])
|
|
|
|
return (
|
|
<div className="pointer-events-none fixed inset-0 z-50">
|
|
{particles.map((particle) => (
|
|
<div
|
|
key={particle.id}
|
|
className="absolute"
|
|
style={{
|
|
left: particle.x,
|
|
top: particle.y,
|
|
}}
|
|
>
|
|
{particle.type === "sparkle" ? (
|
|
<div
|
|
className="h-3 w-3 -translate-x-1/2 -translate-y-1/2"
|
|
style={
|
|
{
|
|
animation: "cursor-spark 0.8s ease-out forwards",
|
|
"--vx": particle.vx,
|
|
"--vy": particle.vy,
|
|
} as React.CSSProperties
|
|
}
|
|
>
|
|
<svg viewBox="0 0 24 24" fill={particle.color}>
|
|
<path d="M12 0L14.59 9.41L24 12L14.59 14.59L12 24L9.41 14.59L0 12L9.41 9.41L12 0Z" />
|
|
</svg>
|
|
</div>
|
|
) : (
|
|
<div
|
|
className="h-12 w-2 -translate-x-1/2 -translate-y-1/2 rounded-full"
|
|
style={
|
|
{
|
|
background:
|
|
"linear-gradient(to bottom, #ff0000, #ff9a00, #d0de21, #4fdc4a, #3fdad8, #2fc9e2, #1c7fee, #5f15f2, #ba0cf8, #fb07d9)",
|
|
animation: "cursor-arc 0.7s ease-out forwards",
|
|
transform: `rotate(${(particle.angle || 0) * (180 / Math.PI)}deg)`,
|
|
"--vx": particle.vx,
|
|
"--vy": particle.vy,
|
|
} as React.CSSProperties
|
|
}
|
|
/>
|
|
)}
|
|
</div>
|
|
))}
|
|
</div>
|
|
)
|
|
}
|