feat: Build Manager, Crucible ops deck, branding, and portable Cloudflare tunnel

Add Build Manager with pin-to-dropper, Crucible multi-node terminal with SSH probe/wake, Command Deck chart balance and pretty stats, AetherForge logo and sacred geometry UI, Field Guide refresh, and LAUNCH.bat Cloudflare MSI + token service install flow.
This commit is contained in:
AetherForge
2026-05-30 22:38:48 -07:00
parent e6b8d84edf
commit 9232f4c448
45 changed files with 4222 additions and 406 deletions

View File

@@ -0,0 +1,136 @@
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}
style={{
position: 'fixed',
inset: 0,
pointerEvents: 'none',
zIndex: 9998,
}}
/>
);
}