179 lines
5.1 KiB
TypeScript
179 lines
5.1 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 createParticles = (x: number, y: number) => {
|
|
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,
|
|
y,
|
|
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,
|
|
y,
|
|
type: "rainbow",
|
|
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,
|
|
})
|
|
}
|
|
|
|
setParticles((prev) => [...prev, ...newParticles])
|
|
|
|
setTimeout(() => {
|
|
setParticles((prev) => prev.filter((p) => !newParticles.find((np) => np.id === p.id)))
|
|
}, 2000)
|
|
}
|
|
|
|
const handleMouseMove = (e: MouseEvent) => {
|
|
createParticles(e.clientX, e.clientY)
|
|
}
|
|
|
|
const handleTouchMove = (e: TouchEvent) => {
|
|
// Prevent scrolling while creating sparkles
|
|
if (e.touches.length > 0) {
|
|
const touch = e.touches[0]
|
|
createParticles(touch.clientX, touch.clientY)
|
|
}
|
|
}
|
|
|
|
const handleTouchStart = (e: TouchEvent) => {
|
|
if (e.touches.length > 0) {
|
|
const touch = e.touches[0]
|
|
createParticles(touch.clientX, touch.clientY)
|
|
}
|
|
}
|
|
|
|
window.addEventListener("mousemove", handleMouseMove)
|
|
window.addEventListener("touchmove", handleTouchMove, { passive: true })
|
|
window.addEventListener("touchstart", handleTouchStart, { passive: true })
|
|
|
|
return () => {
|
|
window.removeEventListener("mousemove", handleMouseMove)
|
|
window.removeEventListener("touchmove", handleTouchMove)
|
|
window.removeEventListener("touchstart", handleTouchStart)
|
|
}
|
|
}, [])
|
|
|
|
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 1.5s 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-4 w-4 -translate-x-1/2 -translate-y-1/2"
|
|
style={
|
|
{
|
|
animation: "cursor-rainbow-star 1.8s ease-out forwards",
|
|
"--vx": particle.vx,
|
|
"--vy": particle.vy,
|
|
} as React.CSSProperties
|
|
}
|
|
>
|
|
<svg viewBox="0 0 24 24" fill="url(#rainbow-gradient)">
|
|
<defs>
|
|
<linearGradient id="rainbow-gradient" x1="0%" y1="0%" x2="100%" y2="100%">
|
|
<stop offset="0%" stopColor="#ff0000" />
|
|
<stop offset="20%" stopColor="#ff9a00" />
|
|
<stop offset="40%" stopColor="#4fdc4a" />
|
|
<stop offset="60%" stopColor="#2fc9e2" />
|
|
<stop offset="80%" stopColor="#5f15f2" />
|
|
<stop offset="100%" stopColor="#fb07d9" />
|
|
</linearGradient>
|
|
</defs>
|
|
<path d="M12 0L14.59 9.41L24 12L14.59 14.59L12 24L9.41 14.59L0 12L9.41 9.41L12 0Z" />
|
|
</svg>
|
|
</div>
|
|
)}
|
|
</div>
|
|
))}
|
|
</div>
|
|
)
|
|
}
|