/* Hero dashboard — a live, product-grade panel that shows the outcomes Martz Forge delivers (recovered hours, revenue, fewer errors). Doubles as proof of the studio's craft. Canvas draws an ambient system + a live climbing area chart; the DOM overlay holds the chip, live badge and KPIs. */ const useCountUp = (target, { duration = 1600, decimals = 0, delay = 0 } = {}) => { const [val, setVal] = React.useState(0); React.useEffect(() => { let raf, t0 = null; const tick = (ts) => { if (t0 === null) t0 = ts; const p = Math.min(1, (ts - t0 - delay) / duration); const eased = p <= 0 ? 0 : 1 - Math.pow(1 - p, 3); setVal(target * eased); if (p < 1) raf = requestAnimationFrame(tick); }; raf = requestAnimationFrame(tick); return () => cancelAnimationFrame(raf); }, [target]); return decimals ? val.toFixed(decimals) : Math.round(val).toLocaleString('en-US'); }; const HeroCanvas = () => { const canvasRef = React.useRef(null); React.useEffect(() => { const canvas = canvasRef.current; if (!canvas) return; const ctx = canvas.getContext('2d'); let raf, w, h, dpr; const resize = () => { const rect = canvas.getBoundingClientRect(); dpr = window.devicePixelRatio || 1; w = rect.width; h = rect.height; canvas.width = w * dpr; canvas.height = h * dpr; ctx.setTransform(1, 0, 0, 1, 0, 0); ctx.scale(dpr, dpr); }; resize(); const ro = new ResizeObserver(resize); ro.observe(canvas); // ambient "system" nodes — subtle texture behind the chart const NUM = 16, amb = []; for (let i = 0; i < NUM; i++) { amb.push({ x: Math.random(), y: Math.random(), vx: (Math.random() - 0.5) * 0.00012, vy: (Math.random() - 0.5) * 0.00012, r: 1 + Math.random() * 1.8, phase: Math.random() * Math.PI * 2, sp: 0.5 + Math.random() * 0.7, }); } const clampN = (v, a, b) => Math.max(a, Math.min(b, v)); let t = 0; const draw = () => { t += 1 / 60; ctx.clearRect(0, 0, w, h); // ---- ambient network (dim) ---- amb.forEach(n => { n.x += n.vx; n.y += n.vy; if (n.x < 0.04 || n.x > 0.96) n.vx *= -1; if (n.y < 0.04 || n.y > 0.96) n.vy *= -1; }); for (let i = 0; i < amb.length; i++) { for (let j = i + 1; j < amb.length; j++) { const dx = amb[i].x - amb[j].x, dy = amb[i].y - amb[j].y; const d = Math.hypot(dx, dy); if (d < 0.24) { ctx.strokeStyle = `rgba(120,180,160,${(1 - d / 0.24) * 0.08})`; ctx.lineWidth = 1; ctx.beginPath(); ctx.moveTo(amb[i].x * w, amb[i].y * h); ctx.lineTo(amb[j].x * w, amb[j].y * h); ctx.stroke(); } } } amb.forEach(n => { const px = n.x * w, py = n.y * h; const pulse = 0.5 + Math.sin(t * n.sp + n.phase) * 0.5; ctx.fillStyle = `rgba(140,200,180,${0.12 + pulse * 0.12})`; ctx.beginPath(); ctx.arc(px, py, n.r, 0, Math.PI * 2); ctx.fill(); }); // ---- live climbing area chart ---- const padX = 26; const cx0 = padX, cx1 = w - padX; const cTop = h * 0.315, cBot = h * 0.60; const GREEN = '34,197,94', CYAN = '6,182,212'; // horizontal gridlines for (let g = 0; g <= 3; g++) { const gy = cTop + (cBot - cTop) * (g / 3); ctx.strokeStyle = 'rgba(255,255,255,0.05)'; ctx.lineWidth = 1; ctx.beginPath(); ctx.moveTo(cx0, gy); ctx.lineTo(cx1, gy); ctx.stroke(); } const N = 90, pts = []; for (let i = 0; i <= N; i++) { const fx = i / N; const growth = Math.pow(fx, 0.85); const wig = 0.05 * Math.sin(fx * 6.5 - t * 1.1) + 0.028 * Math.sin(fx * 15 - t * 1.9) + 0.02 * Math.sin(fx * 3 - t * 0.6); const v = clampN(0.12 + growth * 0.72 + wig, 0.05, 0.97); pts.push({ x: cx0 + fx * (cx1 - cx0), y: cBot - v * (cBot - cTop) }); } // area fill ctx.beginPath(); ctx.moveTo(pts[0].x, cBot); pts.forEach(p => ctx.lineTo(p.x, p.y)); ctx.lineTo(pts[pts.length - 1].x, cBot); ctx.closePath(); const ga = ctx.createLinearGradient(0, cTop, 0, cBot); ga.addColorStop(0, `rgba(${GREEN},0.30)`); ga.addColorStop(1, `rgba(${CYAN},0.02)`); ctx.fillStyle = ga; ctx.fill(); // line ctx.beginPath(); pts.forEach((p, i) => i === 0 ? ctx.moveTo(p.x, p.y) : ctx.lineTo(p.x, p.y)); const gl = ctx.createLinearGradient(cx0, 0, cx1, 0); gl.addColorStop(0, `rgb(${GREEN})`); gl.addColorStop(1, `rgb(${CYAN})`); ctx.strokeStyle = gl; ctx.lineWidth = 2.6; ctx.lineJoin = 'round'; ctx.shadowColor = `rgba(${GREEN},0.5)`; ctx.shadowBlur = 10; ctx.stroke(); ctx.shadowBlur = 0; // travelling data pulse along the line const tp = (t * 0.11) % 1; const pi = Math.floor(tp * N); const pp = pts[pi]; if (pp) { const glow = ctx.createRadialGradient(pp.x, pp.y, 0, pp.x, pp.y, 12); glow.addColorStop(0, 'rgba(255,255,255,0.9)'); glow.addColorStop(1, 'rgba(34,197,94,0)'); ctx.fillStyle = glow; ctx.beginPath(); ctx.arc(pp.x, pp.y, 12, 0, Math.PI * 2); ctx.fill(); } // leading edge marker (the "now" point) const last = pts[pts.length - 1]; ctx.strokeStyle = 'rgba(255,255,255,0.10)'; ctx.lineWidth = 1; ctx.setLineDash([3, 3]); ctx.beginPath(); ctx.moveTo(last.x, last.y); ctx.lineTo(last.x, cBot); ctx.stroke(); ctx.setLineDash([]); const lp = 0.5 + Math.sin(t * 2.4) * 0.5; const lhalo = ctx.createRadialGradient(last.x, last.y, 0, last.x, last.y, 16); lhalo.addColorStop(0, `rgba(${CYAN},${0.35 + lp * 0.3})`); lhalo.addColorStop(1, `rgba(${CYAN},0)`); ctx.fillStyle = lhalo; ctx.beginPath(); ctx.arc(last.x, last.y, 16, 0, Math.PI * 2); ctx.fill(); ctx.fillStyle = `rgb(${CYAN})`; ctx.beginPath(); ctx.arc(last.x, last.y, 4, 0, Math.PI * 2); ctx.fill(); ctx.fillStyle = '#fff'; ctx.beginPath(); ctx.arc(last.x, last.y, 1.8, 0, Math.PI * 2); ctx.fill(); raf = requestAnimationFrame(draw); }; draw(); return () => { cancelAnimationFrame(raf); ro.disconnect(); }; }, []); return ; }; const HeroKpi = ({ k, prefix, target, decimals, suffix, dir, grad, delay }) => { const val = useCountUp(target, { decimals, delay }); return (
{k}
{prefix}{val} {suffix && {suffix}} {dir === 'down' ? : }
); }; const HeroDashboard = () => { const delta = useCountUp(32, { duration: 1400 }); return (
sistema.activo · uptime 99.98%
EN VIVO
Ingresos recuperados +{delta}%
); }; window.HeroDashboard = HeroDashboard; /* keep old name working in case other views reference it */ window.NetworkCanvas = HeroCanvas;