import { useEffect, useRef } from 'react'; interface Particle { x: number; y: number; vx: number; vy: number; life: number; // 1 → 0 size: number; decay: number; } export default function CursorFire() { const canvasRef = useRef(null); const particles = useRef([]); const mouse = useRef({ x: -9999, y: -9999, moved: false }); const rafRef = useRef(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 onMove = (e: MouseEvent) => { mouse.current = { x: e.clientX, y: e.clientY, moved: true }; }; window.addEventListener('mousemove', onMove); const emit = () => { const { x, y } = mouse.current; // Emit 6 particles per frame at cursor for (let i = 0; i < 6; i++) { const spread = 8; particles.current.push({ x: x + (Math.random() - 0.5) * spread, y: y + (Math.random() - 0.5) * (spread * 0.5), vx: (Math.random() - 0.5) * 1.2, vy: -(Math.random() * 2.8 + 1.8), life: 1, size: Math.random() * 14 + 7, decay: Math.random() * 0.022 + 0.016, }); } // Cap particle count for perf if (particles.current.length > 400) { particles.current = particles.current.slice(-400); } }; const draw = () => { ctx.clearRect(0, 0, canvas.width, canvas.height); // Additive blending makes overlapping particles look white-hot ctx.globalCompositeOperation = 'screen'; emit(); const alive: Particle[] = []; for (const p of particles.current) { // Turbulent horizontal drift p.vx += (Math.random() - 0.5) * 0.35; // Slight drag on vx p.vx *= 0.97; // Upward acceleration (heat rises) p.vy -= 0.04; p.x += p.vx; p.y += p.vy; p.life -= p.decay; // Particles shrink as they cool p.size *= 0.982; if (p.life <= 0 || p.size < 1) continue; alive.push(p); const l = p.life; // Color temperature: white-yellow core → orange → red → dark red let r: number, g: number, b: number; if (l > 0.75) { // White-hot r = 255; g = 255; b = Math.round((l - 0.75) / 0.25 * 220); } else if (l > 0.5) { // Yellow-orange r = 255; g = Math.round(100 + (l - 0.5) / 0.25 * 155); b = 0; } else if (l > 0.25) { // Orange-red r = 255; g = Math.round((l - 0.25) / 0.25 * 100); b = 0; } else { // Deep red, fading r = Math.round(160 + l / 0.25 * 95); g = 0; b = 0; } const grad = ctx.createRadialGradient(p.x, p.y, 0, p.x, p.y, p.size); grad.addColorStop(0, `rgba(${r},${g},${b},${l})`); grad.addColorStop(0.4, `rgba(${r},${Math.round(g * 0.6)},0,${l * 0.6})`); grad.addColorStop(1, `rgba(0,0,0,0)`); ctx.beginPath(); ctx.arc(p.x, p.y, p.size, 0, Math.PI * 2); ctx.fillStyle = grad; ctx.fill(); } particles.current = alive; rafRef.current = requestAnimationFrame(draw); }; rafRef.current = requestAnimationFrame(draw); return () => { cancelAnimationFrame(rafRef.current); window.removeEventListener('resize', resize); window.removeEventListener('mousemove', onMove); }; }, []); return ( ); }