// 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 }) ); }