- Calibrate: per-event Telegram/SMTP toggles, test notification, chat ID help - Notify on agent connect/reconnect, offline/hashrate/rejection, forge complete - Sigil scramble post-forge uniquification and Dispense Reveal ceremony - Full system check, desktop push, BITS/host-binary persistence, Path Tracer - Dashboard/Crucible visual polish, haptics, sacred geometry, mobile nav - README documents alerts, sigil scramble, and pack-usb workflow - USB bundle repacked via pack-usb.bat (AetherForge.exe + synced agent source)
138 lines
3.8 KiB
TypeScript
138 lines
3.8 KiB
TypeScript
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<HTMLCanvasElement>(null);
|
|
const particles = useRef<Particle[]>([]);
|
|
const mouse = useRef({ x: -9999, y: -9999, moved: false });
|
|
const rafRef = 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 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 (
|
|
<canvas
|
|
ref={canvasRef}
|
|
className="cursor-fire-fx"
|
|
style={{
|
|
position: 'fixed',
|
|
inset: 0,
|
|
pointerEvents: 'none',
|
|
zIndex: 9998,
|
|
}}
|
|
/>
|
|
);
|
|
}
|