dragonflight/services/web-ui/public/screens-home.jsx
ZGaetano 463cc3694d feat(web-ui): nested bins tree, DragonFlame logo, recorder modal 2x2 grid, cleanup .bak
- Library: nested bins with expand/collapse tree in sidebar
  - buildBinTree() + collectDescendantIds() helpers
  - BinTreeNodes recursive component with hover sub-bin create (+) button
  - Selecting a parent bin shows assets from all descendant bins too
- Home: canvas DragonFlame particle animation behind logo (90 flame + 30 spark), logo 140px
- Recorder modal: source-type-grid 3-col → 2x2 so Deltacast card no longer overflows
- CSS: launcher background radial gradient taller; launcher-logo-wrap 160x200px
- Cleanup: remove capture.js.bak: screens-home.jsx
2026-06-02 23:33:58 -04:00

139 lines
No EOL
4.8 KiB
JavaScript

// screens-home.jsx
//
// Two routes share this file:
//
// • Home - the launcher. Big-button entry into each section of the MAM.
//
// • Dashboard - the operations view. Rebuilt as a control-room status board.
// ─── DragonFlame ─────────────────────────────────────────────
// Canvas-based particle flame rendered behind the logo. Each particle rises
// from the base, fades as it climbs, and shifts hue from deep orange → yellow.
// Spectral shimmer is added via a secondary "spark" layer with lighter colors.
function DragonFlame() {
const canvasRef = React.useRef(null);
const rafRef = React.useRef(null);
React.useEffect(function() {
const canvas = canvasRef.current;
if (!canvas) return;
const ctx = canvas.getContext('2d');
// Particle pool
const W = 160, H = 200;
canvas.width = W;
canvas.height = H;
// Particle factory
function mkParticle(isSpark) {
const x = W * 0.5 + (Math.random() - 0.5) * (isSpark ? 30 : 50);
return {
x,
y: H - 10 - Math.random() * 20,
vx: (Math.random() - 0.5) * (isSpark ? 0.6 : 0.9),
vy: -(0.8 + Math.random() * (isSpark ? 2.2 : 1.6)),
life: 0,
maxLife: 50 + Math.random() * (isSpark ? 30 : 50),
size: isSpark ? 1 + Math.random() * 2 : 3 + Math.random() * 5,
spark: isSpark,
wobble: (Math.random() - 0.5) * 0.04,
};
}
const COUNT = 90, SPARK_COUNT = 30;
const particles = Array.from({ length: COUNT }, function() { return mkParticle(false); });
const sparks = Array.from({ length: SPARK_COUNT }, function() { return mkParticle(true); });
function reset(p) {
var n = mkParticle(p.spark);
Object.assign(p, n);
}
function draw() {
ctx.clearRect(0, 0, W, H);
// Draw glow base
const grad = ctx.createRadialGradient(W * 0.5, H - 5, 0, W * 0.5, H - 5, 55);
grad.addColorStop(0, 'rgba(255,120,0,0.18)');
grad.addColorStop(0.5, 'rgba(255,70,0,0.07)');
grad.addColorStop(1, 'transparent');
ctx.fillStyle = grad;
ctx.beginPath();
ctx.ellipse(W * 0.5, H - 5, 55, 30, 0, 0, Math.PI * 2);
ctx.fill();
// Update + draw each particle
var all = particles.concat(sparks);
all.forEach(function(p) {
p.life += 1;
if (p.life >= p.maxLife) { reset(p); return; }
var t = p.life / p.maxLife; // 0 → 1
// Gentle horizontal drift (wobble)
p.vx += p.wobble;
p.vx *= 0.98;
p.x += p.vx;
p.y += p.vy;
// Decelerate rise near end
p.vy *= 0.99;
var alpha = p.spark
? Math.sin(t * Math.PI) * 0.85
: (t < 0.2 ? t / 0.2 : 1 - (t - 0.2) / 0.8) * 0.7;
// Colour: deep orange (0°) → orange (20°) → yellow (50°) as t rises
var hue = p.spark
? 40 + t * 20 // sparks: golden yellow
: 10 + t * 45; // flame: orange→yellow
var sat = 100;
var lgt = p.spark ? 70 + t * 20 : 50 + t * 20;
ctx.save();
ctx.globalAlpha = alpha;
if (p.spark) {
// Sparks: small bright dots
ctx.fillStyle = 'hsl(' + hue + ',' + sat + '%,' + lgt + '%)';
ctx.beginPath();
ctx.arc(p.x, p.y, p.size * (1 - t * 0.5), 0, Math.PI * 2);
ctx.fill();
} else {
// Flame particles: soft blurred ellipses
var size = p.size * (1 - t * 0.6);
var g2 = ctx.createRadialGradient(p.x, p.y, 0, p.x, p.y, size * 2.2);
g2.addColorStop(0, 'hsla(' + hue + ',' + sat + '%,' + lgt + '%,1)');
g2.addColorStop(0.4, 'hsla(' + (hue - 8) + ',' + sat + '%,' + (lgt - 10) + '%,0.5)');
g2.addColorStop(1, 'transparent');
ctx.fillStyle = g2;
ctx.beginPath();
ctx.ellipse(p.x, p.y, size * 2.2, size * 3.5, 0, 0, Math.PI * 2);
ctx.fill();
}
ctx.restore();
});
rafRef.current = requestAnimationFrame(draw);
}
// Check reduced-motion preference
var mq = window.matchMedia && window.matchMedia('(prefers-reduced-motion: reduce)');
if (mq && mq.matches) {
// Static glow only
var sg = ctx.createRadialGradient(W * 0.5, H * 0.65, 0, W * 0.5, H * 0.65, 80);
sg.addColorStop(0, 'rgba(255,130,0,0.22)');
sg.addColorStop(0.6, 'rgba(255,70,0,0.08)');
sg.addColorStop(1, 'transparent');
ctx.fillStyle = sg;
ctx.fillRect(0, 0, W, H);
} else {
draw();
}
return function() {
if (rafRef.current) cancelAnimationFrame(rafRef.current);
};
}, []);
return React.createElement('span', { className: 'launcher-logo-pulse' },
React.createElement('canvas', { ref: canvasRef })
);
}