/* Animated actors for the orchestration figure (assets/figures/agentic_orchestration.svg). Resident robots stand in the three agent boxes, researchers in the four research-team boxes, and courier bots carry configs, jobs, checkpoints and metrics along the arrows. Coordinates use the drawing's 532 x 576 pt space, the same as the hotspot layer. Variant: ?actors=all | couriers | residents | none (default all). couriers – courier bots replace the lime dot; residents idle only residents – lime dot kept; residents idle and react to the flow all – both */ (function () { const box = document.getElementById('s1-fig'); if (!box) return; const params = new URLSearchParams(location.search); if (params.get('embed')) document.body.classList.add('actors-embed'); const variant = params.get('actors') || document.body.dataset.actors || 'all'; if (variant === 'none') return; const couriers = variant === 'all' || variant === 'couriers'; const reactive = variant === 'all' || variant === 'residents'; const reduced = matchMedia('(prefers-reduced-motion: reduce)').matches; const NS = 'http://www.w3.org/2000/svg'; const INK = '#141414', GREY = '#575951', LIME = '#b6d640', OLIVE = '#819827', PALE = '#f2f6e6', WHITE = '#ffffff'; const el = (name, attrs, parent) => { const n = document.createElementNS(NS, name); for (const k in attrs) n.setAttribute(k, attrs[k]); if (parent) parent.appendChild(n); return n; }; const place = (g, x, y, s, dir = 1) => g.setAttribute('transform', `translate(${x.toFixed(2)} ${y.toFixed(2)}) scale(${(s * dir).toFixed(3)} ${s.toFixed(3)})`); const svg = el('svg', { class: 'actors' + (reduced ? '' : ' live'), viewBox: '0 0 532 576', preserveAspectRatio: 'none', 'aria-hidden': 'true' }); const tracks = el('g', { style: 'visibility:hidden' }, svg); const track = d => el('path', { d, fill: 'none' }, tracks); const layer = el('g', {}, svg); // ---------------------------------------------------------------- glyphs function robot(o) { const g = el('g', { class: 'actor robot', 'data-id': o.id }, layer); place(g, o.x, o.y, o.s); el('rect', { x: -6, y: -2.2, width: 4, height: 2.2, rx: .6, fill: GREY }, g); el('rect', { x: 2, y: -2.2, width: 4, height: 2.2, rx: .6, fill: GREY }, g); const t = el('g', { class: 'torso' }, g); el('line', { x1: 0, y1: -27, x2: 0, y2: -31, stroke: INK, 'stroke-width': .9 }, t); el('circle', { class: 'bulb', cx: 0, cy: -32.4, r: 1.8, fill: LIME, stroke: OLIVE, 'stroke-width': .7 }, t); el('rect', { x: -7.5, y: -27, width: 15, height: 11.5, rx: 3, fill: WHITE, stroke: INK, 'stroke-width': .9 }, t); const eyes = el('g', { class: 'eyes' }, t); el('circle', { cx: -3.2, cy: -21.4, r: 1.5, fill: INK }, eyes); el('circle', { cx: 3.2, cy: -21.4, r: 1.5, fill: INK }, eyes); el('rect', { x: -1.6, y: -15.6, width: 3.2, height: 2.4, fill: GREY }, t); el('rect', { x: -8.5, y: -13.3, width: 17, height: 11.3, rx: 2.2, fill: PALE, stroke: OLIVE, 'stroke-width': .9 }, t); el('rect', { x: -4, y: -10.6, width: 8, height: 4.6, rx: .8, fill: LIME, stroke: OLIVE, 'stroke-width': .6 }, t); el('path', { d: 'M -2.4 -8.9 H 0.6 M -2.4 -7.4 H 2.4', stroke: OLIVE, 'stroke-width': .6 }, t); const al = el('g', { class: 'arm left' }, t); el('line', { x1: -8.5, y1: -11, x2: -12.5, y2: -4, stroke: INK, 'stroke-width': 1.1, 'stroke-linecap': 'round' }, al); el('circle', { cx: -12.5, cy: -4, r: 1.3, fill: INK }, al); const ar = el('g', { class: 'arm right' }, t); el('line', { x1: 8.5, y1: -11, x2: 12.5, y2: -4, stroke: INK, 'stroke-width': 1.1, 'stroke-linecap': 'round' }, ar); el('circle', { cx: 12.5, cy: -4, r: 1.3, fill: INK }, ar); return g; } const HAIR = { a: 'M -4 -19.4 Q -3 -24.8 0 -23.4 Q 3.2 -25.2 4 -19.4 Q 0 -21.8 -4 -19.4 Z', b: 'M -4 -19.2 Q 0 -25.4 4 -19.2 Q 0 -21 -4 -19.2 Z', c: 'M -4 -19.6 Q 0 -24.6 4 -19.6 Q 0 -21.2 -4 -19.6 Z', d: 'M -4 -19 Q -2.8 -25.2 2.8 -23.6 Q 4.6 -22.2 4 -19 Q 0 -21.6 -4 -19 Z' }; function researcher(o) { const g = el('g', { class: 'actor researcher', 'data-id': o.id }, layer); place(g, o.x, o.y, o.s); const f = el('g', { class: 'figure' }, g); el('path', { d: 'M -6.3 -13.6 L 6.3 -13.6 L 7.8 0 L -7.8 0 Z', fill: WHITE, stroke: INK, 'stroke-width': .9, 'stroke-linejoin': 'round' }, f); el('path', { d: 'M -2.4 -13.6 L 0 -9.6 L 2.4 -13.6 M 0 -9.6 V 0', fill: 'none', stroke: GREY, 'stroke-width': .6 }, f); el('circle', { cx: 0, cy: -18.6, r: 4, fill: WHITE, stroke: INK, 'stroke-width': .9 }, f); el('path', { d: HAIR[o.hair], fill: INK }, f); if (o.hair === 'b') el('circle', { cx: 3.6, cy: -22.8, r: 1.6, fill: INK }, f); if (o.hair === 'c') el('path', { d: 'M -3.1 -18.2 a 1.3 1.3 0 1 0 2.6 0 a 1.3 1.3 0 1 0 -2.6 0 M 0.5 -18.2 a 1.3 1.3 0 1 0 2.6 0 a 1.3 1.3 0 1 0 -2.6 0 M -0.5 -18.4 H 0.5', fill: 'none', stroke: INK, 'stroke-width': .55 }, f); const al = el('g', { class: 'arm left' }, f); el('line', { x1: -6, y1: -12.6, x2: -9.2, y2: -6, stroke: INK, 'stroke-width': 1, 'stroke-linecap': 'round' }, al); el('circle', { cx: -9.2, cy: -6, r: 1.1, fill: INK }, al); const ar = el('g', { class: 'arm right' }, f); el('line', { x1: 6, y1: -12.6, x2: 9.2, y2: -6, stroke: INK, 'stroke-width': 1, 'stroke-linecap': 'round' }, ar); el('circle', { cx: 9.2, cy: -6, r: 1.1, fill: INK }, ar); if (o.tablet) { const tb = el('g', { class: 'tablet', transform: 'translate(9.2 -6)' }, ar); el('rect', { x: -.6, y: -5.8, width: 5.4, height: 7.2, rx: .6, fill: WHITE, stroke: INK, 'stroke-width': .6 }, tb); [.55, .85, .7].forEach((h, i) => el('rect', { class: 'bar', x: .3 + i * 1.5, y: -4.2, width: 1.1, height: 4.6, fill: OLIVE, style: `transform: scaleY(${h})` }, tb)); el('line', { x1: 0, y1: .5, x2: 4.2, y2: .5, stroke: GREY, 'stroke-width': .4 }, tb); } return g; } function payload(kind, parent) { const p = el('g', { transform: 'translate(9.6 0)' }, parent); if (kind === 'configs') { el('rect', { x: -2.5, y: -3.2, width: 5, height: 6.4, rx: .5, fill: WHITE, stroke: INK, 'stroke-width': .6 }, p); el('path', { d: 'M -1.4 -1.4 H 1.4 M -1.4 0 H 1.4 M -1.4 1.4 H 0.4', stroke: GREY, 'stroke-width': .5 }, p); } else if (kind === 'job') { el('rect', { x: -2.2, y: -3.4, width: 4.4, height: 6.8, rx: .5, fill: LIME, stroke: OLIVE, 'stroke-width': .6 }, p); } else if (kind === 'checkpoint') { el('path', { d: 'M -2.6 -2.4 V 2.4 A 2.6 .95 0 0 0 2.6 2.4 V -2.4', fill: WHITE, stroke: INK, 'stroke-width': .6 }, p); el('ellipse', { cx: 0, cy: -2.4, rx: 2.6, ry: .95, fill: WHITE, stroke: INK, 'stroke-width': .6 }, p); el('path', { d: 'M -2.6 0 A 2.6 .95 0 0 0 2.6 0', fill: 'none', stroke: GREY, 'stroke-width': .45 }, p); } else { el('line', { x1: -3, y1: 3.2, x2: 3, y2: 3.2, stroke: GREY, 'stroke-width': .5 }, p); [[-2.4, 2.6], [-.6, 4.6], [1.2, 3.4]].forEach(([x, h]) => el('rect', { x, y: 3.2 - h, width: 1.4, height: h, fill: OLIVE }, p)); } return p; } function courier(kind) { const g = el('g', { class: 'courier', 'data-flow': kind, style: 'opacity:0' }, layer); el('line', { class: 'rotor', x1: -4.6, y1: -6.2, x2: 4.6, y2: -6.2, stroke: INK, 'stroke-width': .8, 'stroke-linecap': 'round' }, g); el('rect', { x: -.5, y: -6.2, width: 1, height: 1.8, fill: GREY }, g); el('rect', { x: -4.5, y: -4.4, width: 9, height: 8.8, rx: 2.4, fill: PALE, stroke: OLIVE, 'stroke-width': .8 }, g); el('circle', { cx: -1.2, cy: -1.5, r: 1, fill: INK }, g); el('circle', { cx: 2.2, cy: -1.5, r: 1, fill: INK }, g); el('line', { x1: 4.5, y1: 1, x2: 7.2, y2: 1, stroke: INK, 'stroke-width': .8, 'stroke-linecap': 'round' }, g); payload(kind, g); return g; } function paperGlyph() { const g = el('g', { class: 'paper', style: 'opacity:0' }, layer); el('rect', { x: -2.6, y: -3.3, width: 5.2, height: 6.6, rx: .5, fill: WHITE, stroke: INK, 'stroke-width': .6 }, g); el('path', { d: 'M -1.5 -1.5 H 1.5 M -1.5 0 H 1.5 M -1.5 1.5 H 0.3', stroke: GREY, 'stroke-width': .5 }, g); return g; } function bubbleGlyph() { const g = el('g', { class: 'bubble', style: 'opacity:0' }, layer); el('path', { d: 'M -4.5 -4 h 9 a 1.4 1.4 0 0 1 1.4 1.4 v 6 a 1.4 1.4 0 0 1 -1.4 1.4 h -3.6 l -1.8 2.4 l -1 -2.4 h -2.6 a 1.4 1.4 0 0 1 -1.4 -1.4 v -6 a 1.4 1.4 0 0 1 1.4 -1.4 z', fill: WHITE, stroke: OLIVE, 'stroke-width': .8, 'stroke-linejoin': 'round' }, g); el('path', { d: 'M 0 -2.2 V 1.6', stroke: OLIVE, 'stroke-width': 1.3, 'stroke-linecap': 'round' }, g); el('circle', { cx: 0, cy: 3.4, r: .8, fill: OLIVE }, g); return g; } function igniteOverlay(rects) { return rects.map(([x, y, w, h]) => el('rect', { class: 'ignite', x, y, width: w, height: h, rx: 2 }, layer)); } // ---------------------------------------------------------------- cast const R = { plan: researcher({ id: 'r-plan', x: 25.5, y: 44.6, s: .85, hair: 'a' }), gateway: researcher({ id: 'r-gateway', x: 386.7, y: 304.2, s: .85, hair: 'b' }), results: researcher({ id: 'r-results', x: 405, y: 521.3, s: .85, hair: 'c', tablet: true }), eval: researcher({ id: 'r-eval', x: 32, y: 452.2, s: .75, hair: 'd' }) }; const B = { plan: robot({ id: 'b-plan', x: 470, y: 87.5, s: 1 }), gateway: robot({ id: 'b-gateway', x: 487, y: 212.5, s: 1 }), eval: robot({ id: 'b-eval', x: 112, y: 527.3, s: .88 }) }; box.appendChild(svg); if (reduced) return; // ---------------------------------------------------------------- timeline // Dispatch -> checkpoint evaluations -> results -> response to the orchestrator. // One initial configuration starts continuous checkpoint traffic. Each checkpoint // produces one result after evaluation, then one response to the orchestrator. const INTRO = 2200, P = 5400, CADENCE = 320; const checkpointStart = 1200, evalArrival = checkpointStart + 1600; const metricsStart = evalArrival + CADENCE * 2, resultsArrival = metricsStart + 1600; const LEGS = [ { kind: 'job', d: 'M 352 226 L 206 226', start: 0, duration: 1200, interval: 1600, dir: -1, s: 1 }, { kind: 'checkpoint', d: 'M 121 318 L 121 379', start: checkpointStart, duration: 1600, interval: CADENCE, s: .8 }, { kind: 'metrics', d: 'M 237 458 L 379 458', start: metricsStart, duration: 1600, interval: CADENCE, s: .9 }, // Metrics return beside the results-query connection, clear of its labels. { kind: 'feedback', d: 'M 518 410 L 518 226 Q 518 218 510 218 L 502 218', start: resultsArrival + CADENCE, duration: 1600, interval: CADENCE, s: .8 }, { kind: 'configs', d: 'M 495 91 L 495 166', start: 1000, duration: 1200, once: true } ]; const smooth = u => u <= 0 ? 0 : u >= 1 ? 1 : u * u * (3 - 2 * u); const pointAt = (path, u) => path.getPointAtLength(Math.max(0, Math.min(1, u)) * path.getTotalLength()); let bots = null; if (couriers) { box.classList.add('has-couriers'); bots = LEGS.flatMap(leg => { const path = track(leg.d), length = path.getTotalLength(); // Reuse a fixed pool of couriers without interrupting the emission schedule. const count = leg.once ? 1 : Math.ceil(leg.duration / leg.interval); return Array.from({ length: count }, (_, i) => ({ path, length, g: courier(leg.kind), dir: leg.dir || 1, s: leg.s || 1.3, start: leg.start + i * (leg.interval || 0), duration: leg.duration, once: !!leg.once, repeat: count * leg.interval, index: i, count })); }); } const trainIgnite = igniteOverlay([[54.715, 243.839, 25.914, 41.762], [89.812, 243.839, 25.914, 41.762], [124.914, 243.839, 25.91, 41.762], [160.011, 243.839, 25.914, 41.762]]); const evalIgnite = igniteOverlay([[139.183, 476.382, 16.734, 32.926], [161.851, 476.382, 16.734, 32.926], [184.519, 476.382, 16.734, 32.926], [207.187, 476.382, 16.738, 32.926]]); const ignite = rects => rects.forEach((r, i) => setTimeout(() => { r.classList.remove('on'); void r.getBBox(); r.classList.add('on'); }, i * 130)); const paper = paperGlyph(), paperPath = track('M 34 28 Q 250 42 457 81'); const bubbleG = bubbleGlyph(), bubblePath = track('M 487 176 Q 400 163 386 276'); const bubbleE = bubbleGlyph(); const tabletBars = Array.from(R.results.querySelectorAll('.bar')); const arrivals = [ { start: 1200, interval: 1600, last: -1, update: n => ignite([trainIgnite[n % trainIgnite.length]]) }, { start: evalArrival, interval: CADENCE, last: -1, update: n => ignite([evalIgnite[n % evalIgnite.length]]) }, { start: resultsArrival, interval: CADENCE * 4, last: -1, update: () => tabletBars.forEach(b => { b.style.transform = `scaleY(${(0.3 + Math.random() * 0.7).toFixed(2)})`; }) } ]; // One-time events use the intro clock; all others use the repeating loop clock. const cls = (node, name) => ({ on: () => node.classList.add(name), off: () => node.classList.remove(name) }); const events = [ { start: 0, dur: 700, once: true, ...cls(R.plan, 'raise') }, { start: 100, dur: 850, once: true, move: { g: paper, path: paperPath, hold: 0 } }, { start: 800, dur: 500, once: true, ...cls(B.plan, 'typing') }, { start: 1000, dur: 500, once: true, ...cls(B.plan, 'wave') }, { start: 2000, dur: 200, once: true, on: () => { B.gateway.classList.remove('catch'); void B.gateway.getBBox(); B.gateway.classList.add('catch'); }, off: () => {} }, { start: 5200, dur: 200, on: () => { B.gateway.classList.remove('catch'); void B.gateway.getBBox(); B.gateway.classList.add('catch'); }, off: () => {} }, { start: 0, dur: 600, ...cls(B.gateway, 'wave') }, { start: 2100, dur: 900, ...cls(B.eval, 'typing') }, // warnings, every third loop { start: 1200, dur: 900, every: 3, phase: 1, ...cls(B.gateway, 'alert') }, { start: 1300, dur: 700, every: 3, phase: 1, move: { g: bubbleG, path: bubblePath, hold: 500 } }, { start: 1950, dur: 800, every: 3, phase: 1, ...cls(R.gateway, 'raise') }, { start: 3100, dur: 700, every: 3, phase: 1, ...cls(B.eval, 'alert') }, { start: 3100, dur: 1300, every: 3, phase: 1, pop: { g: bubbleE, x: 96, y: 511 } }, { start: 3800, dur: 600, every: 3, phase: 1, pop: { g: bubbleE, x: 21, y: 434 } }, { start: 3850, dur: 700, every: 3, phase: 1, ...cls(R.eval, 'raise') } ]; events.forEach(e => { e.active = false; }); const seek = Number(params.get('t')) || 0; // debug: start the loop at this offset in ms let t0 = null; function frame(ts) { if (t0 === null) t0 = ts - seek; const T = ts - t0, runTime = T - INTRO; const t = Math.max(0, runTime) % P, cycle = Math.floor(Math.max(0, runTime) / P); if (bots) { bots.forEach(b => { const elapsed = (b.once ? T : runTime) - b.start; const dt = b.once ? elapsed : elapsed % b.repeat; if (elapsed < 0 || dt >= b.duration) { b.g.style.opacity = 0; return; } if (!b.once) b.g.dataset.packet = Math.floor(elapsed / b.repeat) * b.count + b.index; const u = dt / b.duration; const q = b.path.getPointAtLength(u * b.length); const bob = Math.sin(T / 140) * .6; place(b.g, q.x, q.y + bob, b.s, b.dir); b.g.style.opacity = smooth(Math.min(u / .1, (1 - u) / .1, 1)).toFixed(2); }); } if (reactive) arrivals.forEach(a => { const n = Math.floor((runTime - a.start) / a.interval); if (n >= 0 && n > a.last) { a.last = n; a.update(n); } }); if (reactive) events.forEach(e => { const total = e.dur + (e.move ? e.move.hold : 0); const eligible = e.once || (runTime >= 0 && (!e.every || cycle % e.every === (e.phase || 0))); const dt = (e.once ? T : t) - e.start, active = eligible && dt >= 0 && dt < total; if (e.move) { if (active) { const q = pointAt(e.move.path, smooth(Math.min(dt / e.dur, 1))); place(e.move.g, q.x, q.y, 1, 1); e.move.g.style.opacity = dt < 120 ? (dt / 120).toFixed(2) : dt > total - 160 ? ((total - dt) / 160).toFixed(2) : 1; } else if (e.active) e.move.g.style.opacity = 0; } else if (e.pop) { if (active) { const k = dt < 150 ? dt / 150 : dt > total - 150 ? (total - dt) / 150 : 1; place(e.pop.g, e.pop.x, e.pop.y - (1 - smooth(k)) * 2, .7 + .3 * smooth(k)); e.pop.g.style.opacity = smooth(k).toFixed(2); } else if (e.active) e.pop.g.style.opacity = 0; } else if (active && !e.active) e.on(); else if (!active && e.active) e.off(); e.active = active; }); requestAnimationFrame(frame); } const start = () => requestAnimationFrame(frame); if ('IntersectionObserver' in window) { const io = new IntersectionObserver(es => { if (es.some(e => e.isIntersecting)) { io.disconnect(); start(); } }, { threshold: .2 }); io.observe(box); } else start(); })();