PageSourceSearch

https://www.nextiqumai.com/assets/js/nx-handoff.js?v=14

js nextiqumai.com collected 2026-09-24 14:48:51 UTC 10,513 bytes, 192 lines download raw bytes

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.