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

133 lines
3.6 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

'use client'
import { useEffect, useRef } from 'react'
// Matrix-style characters: katakana, numbers, symbols
const MATRIX_CHARS = 'アイウエオカキクケコサシスセソタチツテトナニヌネハヒフヘホマミムメモヤユヨラリルレロワヲン0123456789@#$%^&*(){}[]|;:<>?/\\~`'
interface MatrixChar {
x: number
y: number
char: string
vx: number
vy: number
life: number
maxLife: number
size: number
rotation: number
rotationSpeed: number
}
export function CursorEffect() {
const canvasRef = useRef<HTMLCanvasElement>(null)
const charsRef = useRef<MatrixChar[]>([])
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 spawnChars = (x: number, y: number, count: number) => {
for (let i = 0; i < count; i++) {
const angle = (Math.PI * 2 * i) / count + (Math.random() - 0.5) * 0.5
const speed = 2 + Math.random() * 4
charsRef.current.push({
x,
y,
char: MATRIX_CHARS[Math.floor(Math.random() * MATRIX_CHARS.length)],
vx: Math.cos(angle) * speed,
vy: Math.sin(angle) * speed,
life: 1,
maxLife: 60 + Math.random() * 60,
size: 12 + Math.random() * 16,
rotation: Math.random() * Math.PI * 2,
rotationSpeed: (Math.random() - 0.5) * 0.2,
})
}
}
const handleClick = (e: MouseEvent) => {
// Burst of matrix characters on click
spawnChars(e.clientX, e.clientY, 8 + Math.floor(Math.random() * 8))
}
window.addEventListener('click', handleClick)
const animate = () => {
// Clear with slight fade for trails
ctx.fillStyle = 'rgba(18, 16, 14, 0.15)'
ctx.fillRect(0, 0, canvas.width, canvas.height)
charsRef.current = charsRef.current.filter(char => {
// Update position
char.x += char.vx
char.y += char.vy
// Gravity and friction
char.vy += 0.08
char.vx *= 0.99
char.vy *= 0.99
// Update rotation
char.rotation += char.rotationSpeed
// Decay life
char.life -= 1 / char.maxLife
// Draw character
if (char.life > 0) {
ctx.save()
ctx.translate(char.x, char.y)
ctx.rotate(char.rotation)
const alpha = char.life * 0.9
// Primary color (warm gold/amber matching the theme)
ctx.fillStyle = `rgba(200, 170, 120, ${alpha})`
ctx.font = `${char.size}px "JetBrains Mono", monospace`
ctx.textAlign = 'center'
ctx.textBaseline = 'middle'
ctx.fillText(char.char, 0, 0)
// Glow effect
ctx.fillStyle = `rgba(200, 170, 120, ${alpha * 0.3})`
ctx.font = `${char.size * 1.1}px "JetBrains Mono", monospace`
ctx.fillText(char.char, 0, 0)
ctx.restore()
}
return char.life > 0
})
frameRef.current = requestAnimationFrame(animate)
}
frameRef.current = requestAnimationFrame(animate)
return () => {
window.removeEventListener('resize', resize)
window.removeEventListener('click', handleClick)
cancelAnimationFrame(frameRef.current)
}
}, [])
return (
<canvas
ref={canvasRef}
className="fixed inset-0 pointer-events-none z-0"
style={{ opacity: 0.9 }}
/>
)
}