1/* ============================================================================================ 2 AGENT SHOWCASE - scroll choreography 3 -------------------------------------------------------------------------------------------- 4 Rebuilt on the same architecture as the DropPilot hero, because that one is smooth and the 5 first version of this was not. The difference is not tuning, it is structure: 6 7 OLD (wrong): five full cards, each with its own scale + blur + translate, cross-fading. 8 Five composited layers being transformed every frame, and a long crossfade 9 where two cards sat on the same pixels at similar opacity - visible as murk. 10 11 NEW (this): ONE scene that zooms continuously. Everything else is OPACITY ONLY, on 12 independent seg(p, start, end) windows. One transform per frame instead of 13 five, and the copy swap is a quick dissolve rather than a slow overlap. 14 15 seg(p, a, b) is the whole idea: a named window on the SAME global progress. Every element 16 declares when it enters and leaves, so the timeline reads top-to-bottom instead of being 17 derived from a distance function. 18 ============================================================================================ */ 19(function () { 20 'use strict'; 21 var wrap = document.getElementById('agentHandoff'); 22 if (!wrap) return; 23 24 var reduce = false;
25 try { reduce = window.matchMedia('(prefers-reduced-motion: reduce)').matches; } catch (e) {} 26 /* Not a shorter animation - none. The stacked markup is already the accessible answer, so the 27 correct action here is to do nothing at all. */ 28 if (reduce) return; 29 30 var scene = wrap.querySelector('.hoff-scene'); 31 var panels = [].slice.call(wrap.querySelectorAll('.hoff-panel')); 32 var agents = [].slice.call(wrap.querySelectorAll('[data-agent]')); 33 var N = panels.length; 34 if (!N || !scene) return; 35 36 wrap.classList.add('is-on'); 37 38 var CIRC = 87.96; /* 2 * PI * r, r = 14 - the ring circumference */ 39 var clamp = function (v, a, b) { return v < a ? a : (v > b ? b : v); }; 40 var seg = function (p, a, b) { return clamp((p - a) / (b - a), 0, 1); }; 41 42 var queued = false, visible = true, last = -1, lastStep = -1; 43 44 function progress() { 45 var r = wrap.getBoundingClientRect(); /* one layout read per frame, on the wrapper */ 46 var span = Math.max(1, wrap.offsetHeight - window.innerHeight); 47 return clamp(-r.top / span, 0, 1); 48 } 49 50 function paint() { 51 queued = false; 52 if (!visible) return; 53 var p = progress(); 54 /* Sub-pixel scroll deltas cannot change anything on screen but still cost a full style pass. 55 Straight from the DropPilot driver, and it removes most frames on a trackpad. */ 56 if (Math.abs(p - last) < 0.0015) return; 57 last = p; 58 59 /* THE one transform. A slow continuous push-in across the whole section reads as a camera 60 move; five separate card scales read as a slideshow. */ 61 scene.style.transform = 'scale(' + (0.94 + p * 0.12).toFixed(4) + ')'; 62 63 var span = 1 / N; 64 var step = clamp(Math.floor(p / span), 0, N - 1); 65 66 for (var i = 0; i < N; i++) { 67 var s = i * span, e = s + span; 68 /* OPACITY IS STEP-DRIVEN, NOT SCROLL-DRIVEN. This used to be a scroll-linked dissolve: 69 each panel faded in over (s-0.030 .. s+0.018) while the previous faded out over 70 (e-0.018 .. e+0.030). Those two windows sit on the SAME boundary and overlap almost 71 entirely, so roughly 0.048 of the section's scroll had two panels on screen at partial 72 opacity at once. Text sat over text and read as blur - reported, accurately, as the page 73 looking "drunk". 74 75 The narrow windows were an attempt to fix it by making the overlap brief. That reduces 76 how OFTEN you land in it; it cannot remove it, because any scroll-linked crossfade has a 77 band of scroll positions where both layers are visible, and a scroll position is 78 something a person can simply stop at and stay in. 79 80 The earlier build answered this with an auto-scroll "settle" that nudged you to the 81 nearer side. It was removed, correctly: taking the scrollbar out of someone's hands is a 82 worse problem than the one it solves. But removing it left the symptom with nothing in 83 its place. 84 85 So opacity is now binary per panel and the fade is a CSS transition on a class - the same 86 mechanism this file already uses for .hoff-d and .hoff-vz. Because the transition is 87 driven by TIME rather than scroll offset, it always finishes on its own. Stop anywhere, 88 including mid-handover, and about a quarter of a second later exactly one panel is 89 visible. There is no scroll position that can hold a double exposure, and nobody's 90 scrollbar was touched. */ 91 var op = (i === step) ? 1 : 0; 92 93 var pl = panels[i]; 94 pl.classList.toggle('is-shown', op === 1); 95 /* visibility:hidden takes the panel out of the paint path AND out of the tab order, so an 96 invisible link cannot steal a click or a focus stop. */ 97 var live = op === 1; 98 /* visibility is left to CSS: cutting it at the same instant opacity flips would abort the 99 fade. .hoff-panel:not(.is-shown) gets visibility:hidden with a delay matching the 100 transition, so the outgoing panel finishes fading before it leaves the paint path. */ 101 pl.classList.toggle('is-live', live); 102 if (pl.inert !== undefined) pl.inert = !live; 103 pl.setAttribute('aria-hidden', live ? 'false' : 'true'); 104 105 /* Sub-steps fill across this agent's own window. The count varies per agent (Loquelle and Iris 106 have five, tiQ three), so everything divides by bars.length rather than a constant. */ 107 var bars = pl.querySelectorAll('[data-bar]'), nb = bars.length, cur = -1; 108 for (var b = 0; b < nb; b++) { 109 var bs = s + span * (b / nb), be = s + span * ((b + 1) / nb); 110 var f = seg(p, bs, be); 111 bars[b].style.setProperty('--fill', (f * 100).toFixed(0) + '%'); 112 bars[b].classList.toggle('is-done', f >= 1); 113 if (f > 0 && f < 1) cur = b; 114 } 115 /* Open the detail for the running step and close the rest. Class toggle only - the CSS 116 transition does the work, so this costs nothing on the frames in between. */ 117 if (cur < 0) cur = (p >= e) ? nb - 1 : 0; 118 var vz = pl.querySelectorAll('[data-viz]'); 119 for (var c = 0; c < nb; c++) { 120 bars[c].classList.toggle('is-cur', c === cur); 121 /* The glyph zoom is CSS. Toggling two classes is all the driver does; writing transform 122 here would put a second per-frame transform in the loop and undo the rebuild. */ 123 if (vz[c]) { vz[c].classList.toggle('is-cur', c === cur); vz[c].classList.toggle('is-past', c < cur); } 124 } 125 /* Remember which sub-step is running so the chip can name it instead of a static label. */ 126 if (live) pl.setAttribute('data-cur', bars[cur] ? bars[cur].getAttribute('data-label') : ''); 127 } 128 129 /* Agent chips: queued -> working -> handed off, with the ring filling. This is the progress 130 readout, and it is the part that makes the section feel like a system doing work. */
131 agents.forEach(function (node, index) { 132 var s = index * span, e = s + span; 133 var prog = seg(p, s, e); 134 /* active must follow the panel actually on screen, not a strictly-positive progress value. 135 At the very top of the section stage 0 has prog exactly 0, so every chip read "Queued" 136 while Loquelle's card was plainly visible - the readout contradicted the scene. */ 137 var done = prog >= 1, active = !done && index === step; 138 var ring = node.querySelector('[data-ring]'); 139 var glyph = node.querySelector('[data-glyph]'); 140 var status = node.querySelector('[data-status]'); 141 if (ring) { 142 ring.setAttribute('stroke', done ? '#3FA46A' : active ? '#DE6A1E' : 'rgba(138,136,80,.30)'); 143 ring.setAttribute('stroke-dasharray', (CIRC - CIRC * prog).toFixed(2) + ' ' + CIRC); 144 } 145 if (glyph) glyph.textContent = done ? 'â' : String(index + 1); 146 node.classList.toggle('is-done', done); 147 node.classList.toggle('is-active', active); 148 if (status) { 149 /* The chip reports the actual sub-step in flight, not a generic word. That is the whole 150 point of showing progress: it should be telling you something true. */ 151 var curLabel = panels[index] && panels[index].getAttribute('data-cur'); 152 status.textContent = done ? 'Ready' : active ? (curLabel || 'Working') : 'Queued'; 153 } 154 }); 155 156 if (step !== lastStep) { lastStep = step; wrap.setAttribute('data-step', String(step + 1)); } 157 } 158 159 function schedule() { if (!queued) { queued = true; requestAnimationFrame(paint); } } 160 161 /* NO AUTO-SCROLL. There used to be a "settle" here: when scrolling stopped inside a crossfade it 162 smooth-scrolled to the nearer side so you never came to rest on a double exposure. It worked, 163 and it was still wrong. It moved the page 17-23vh with no input, and only fired when you 164 happened to stop inside a ~22vh window out of 320vh - so it was both surprising and 165 intermittent, which is the worst combination. I flagged that risk when writing it and shipped 166 it anyway; the report back was "it goes up or down, never happens every time". 167 The crossfade windows are narrow (~0.03 of total travel), so resting inside one is uncommon 168 and one wheel notch clears it. Taking the scrollbar out of someone's hands to fix that is a 169 bad trade. */ 170 171 /* Park the driver entirely when the section is nowhere near the viewport. */ 172 try { 173 new IntersectionObserver(function (es) { 174 visible = es[0].isIntersecting; 175 if (visible) { last = -1; schedule(); } 176 }, { rootMargin: '100% 0px' }).observe(wrap); 177 } catch (e) {} 178 179 window.addEventListener('scroll', schedule, { passive: true }); 180 window.addEventListener('resize', function () { last = -1; schedule(); }, { passive: true }); 181 182 /* Chips are real buttons: the pinned section has to be navigable without a scroll wheel. */ 183 agents.forEach(function (node, i) { 184 node.addEventListener('click', function () { 185 var sp = wrap.offsetHeight - (window.innerHeight || 0); 186 var mid = (i + 0.5) / N; 187 window.scrollTo({ top: wrap.offsetTop + Math.max(0, sp) * mid, behavior: 'smooth' }); 188 }); 189 }); 190 191 paint(); 192})();
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.