PageSourceSearch

https://onlinepolicingsolutions.com/assets/js/home-trace.js

js onlinepolicingsolutions.com collected 2026-10-01 23:45:14 UTC 9,053 bytes, 265 lines download raw bytes

1/* Decorative scroll-traced dotted path connecting home page section titles.
2   The path runs title-to-title; when the trace reaches a title, its dot
3   becomes a pulsing beacon. */
4(function () {
5  if (!document.querySelector('.product-rail')) return; // homepage only
6
7  var NS = 'http://www.w3.org/2000/svg';
8  var reduced = window.matchMedia('(prefers-reduced-motion: reduce)').matches;
9  var svg = null;
10  var maskPath = null;
11  var nodeGroups = [];
12  var nodeLens = [];
13  var nodeYs = [];
14  var totalLen = 0;
15  var raf = 0;
16
17  // The first beacon stays dark until the hero network animation has
18  // converged onto it (with a timer fallback in case that never fires).
19  var heroHandoff = false;
20  function openHeroHandoff() {
21    if (heroHandoff) return;
22    heroHandoff = true;
23    update();
24  }
25  window.addEventListener('opsHeroConverged', openHeroHandoff);
26  setTimeout(openHeroHandoff, 9000);
27
28  // The trace runs from the hero title through the jobs section and, when
29  // the full suite is expanded, down the product titles — ending at the
30  // last product (Public Online Reporting Portal™). Hidden titles are
31  // filtered out in computePoints, and the suite toggle fires a resize
32  // event so the path rebuilds.
33  function getTargets() {
34    return Array.prototype.slice.call(
35      document.querySelectorAll('.hero-content h1, .home-jobs .section-head h2, .product-copy h2')
36    );
37  }
38
39  // Anchor each point immediately to the left of its title.
40  function computePoints() {
41    var docW = document.documentElement.clientWidth;
42    var pts = [];
43    getTargets().forEach(function (el) {
44      var r = el.getBoundingClientRect();
45      if (r.width === 0 && r.height === 0) return;
46      var y = r.top + window.scrollY + r.height / 2;
47      // Unrevealed sections sit 24px lower until their entrance animation
48      // runs; measure against their settled position.
49      var rev = el.closest ? el.closest('.reveal') : null;
50      if (rev && !rev.classList.contains('is-visible')) y -= 24;
51      var x = r.left + window.scrollX - 26;
52      x = Math.max(40, Math.min(docW - 40, x));
53      pts.push({ x: x, y: y });
54    });
55    return pts;
56  }
57
58  // Circuit-trace style segment: drop straight down from a, turn 90°,
59  // run horizontally, then turn 90° down into b. Corners get a small
60  // rounded radius.
61  function segD(a, b) {
62    var dx = b.x - a.x;
63    var dy = b.y - a.y;
64    if (Math.abs(dx) < 24) {
65      return ' L ' + b.x + ' ' + b.y;
66    }
67    var midY = a.y + dy / 2;
68    var dir = dx > 0 ? 1 : -1;
69    var r = Math.min(20, Math.abs(dx) / 2 - 2, Math.abs(dy) / 4);
70    if (r < 2) {
71      return ' L ' + a.x + ' ' + midY + ' L ' + b.x + ' ' + midY + ' L ' + b.x + ' ' + b.y;
72    }
73    return (
74      ' L ' + a.x + ' ' + (midY - r) +
75      ' Q ' + a.x + ' ' + midY + ' ' + (a.x + dir * r) + ' ' + midY +
76      ' L ' + (b.x - dir * r) + ' ' + midY +
77      ' Q ' + b.x + ' ' + midY + ' ' + b.x + ' ' + (midY + r) +
78      ' L ' + b.x + ' ' + b.y
79    );
80  }
81
82  function buildPathD(pts, count) {
83    var n = typeof count === 'number' ? count : pts.length;
84    var d = 'M ' + pts[0].x + ' ' + pts[0].y;
85    for (var i = 1; i < n; i++) d += segD(pts[i - 1], pts[i]);
86    return d;
87  }
88
89  // Path length from the start to each anchor point.
90  function computeNodeLens(pts) {
91    var probe = document.createElementNS(NS, 'path');
92    var lens = [0];
93    for (var i = 2; i <= pts.length; i++) {
94      probe.setAttribute('d', buildPathD(pts, i));
95      lens.push(probe.getTotalLength());
96    }
97    return lens;
98  }
99
100  function destroy() {
101    if (svg && svg.parentNode) svg.parentNode.removeChild(svg);
102    svg = null;
103    nodeGroups = [];
104    nodeLens = [];
105  }
106
107  function build() {
108    destroy();
109    if (document.documentElement.clientWidth < 1000) return;
110
111    var pts = computePoints();
112    if (pts.length < 2) return;
113
114    var docW = document.documentElement.clientWidth;
115    var docH = document.documentElement.scrollHeight;
116    var d = buildPathD(pts);
117
118    svg = document.createElementNS(NS, 'svg');
119    svg.setAttribute('class', 'home-trace');
120    svg.setAttribute('width', docW);
121    svg.setAttribute('height', docH);
122    svg.setAttribute('viewBox', '0 0 ' + docW + ' ' + docH);
123    svg.setAttribute('aria-hidden', 'true');
124
125    var defs = document.createElementNS(NS, 'defs');
126    var mask = document.createElementNS(NS, 'mask');
127    mask.setAttribute('id', 'home-trace-mask');
128    mask.setAttribute('maskUnits', 'userSpaceOnUse');
129    mask.setAttribute('x', '0');
130    mask.setAttribute('y', '0');
131    mask.setAttribute('width', docW);
132    mask.setAttribute('height', docH);
133
134    maskPath = document.createElementNS(NS, 'path');
135    maskPath.setAttribute('d', d);
136    maskPath.setAttribute('fill', 'none');
137    maskPath.setAttribute('stroke', '#fff');
138    maskPath.setAttribute('stroke-width', '16');
139    maskPath.setAttribute('stroke-linecap', 'round');
140    mask.appendChild(maskPath);
141    defs.appendChild(mask);
142    svg.appendChild(defs);
143
144    var maskedGroup = document.createElementNS(NS, 'g');
145    maskedGroup.setAttribute('mask', 'url(#home-trace-mask)');
146
147    var tracePath = document.createElementNS(NS, 'path');
148    tracePath.setAttribute('d', d);
149    tracePath.setAttribute('class', 'home-trace-line');
150    tracePath.setAttribute('fill', 'none');
151    tracePath.setAttribute('stroke-width', '2.5');
152    tracePath.setAttribute('stroke-linecap', 'round');
153    tracePath.setAttribute('stroke-dasharray', '0.5 9');
154    maskedGroup.appendChild(tracePath);
155    svg.appendChild(maskedGroup);
156
157    // Beacons live outside the mask so their pulse is not clipped.
158    pts.forEach(function (p) {
159      var g = document.createElementNS(NS, 'g');
160      g.setAttribute('class', 'home-trace-node-g');
161
162      var ring = document.createElementNS(NS, 'circle');
163      ring.setAttribute('cx', p.x);
164      ring.setAttribute('cy', p.y);
165      ring.setAttribute('r', '7');
166      ring.setAttribute('class', 'home-trace-beacon');
167
168      var core = document.createElementNS(NS, 'circle');
169      core.setAttribute('cx', p.x);
170      core.setAttribute('cy', p.y);
171      core.setAttribute('r', '4');
172      core.setAttribute('class', 'home-trace-node');
173
174      g.appendChild(ring);
175      g.appendChild(core);
176      svg.appendChild(g);
177      nodeGroups.push(g);
178    });
179
180    document.body.appendChild(svg);
181
182    totalLen = maskPath.getTotalLength();
183    maskPath.setAttribute('stroke-dasharray', String(totalLen));
184    nodeLens = computeNodeLens(pts);
185    nodeYs = pts.map(function (p) { return p.y; });
186
187    if (reduced) {
188      maskPath.setAttribute('stroke-dashoffset', '0');
189      nodeGroups.forEach(function (g) { g.classList.add('is-lit'); });
190    } else {
191      update();
192    }
193  }
194
195  // Piecewise mapping: the drawn length reaches each node exactly when
196  // its title sits at the middle of the viewport.
197  function update() {
198    if (!maskPath || reduced || !nodeYs.length) return;
199    var vh = window.innerHeight;
200    var maxScroll = Math.max(0, document.documentElement.scrollHeight - vh);
201    var stops = [];
202    for (var i = 0; i < nodeYs.length; i++) {
203      var s = Math.min(nodeYs[i] - vh * 0.5, maxScroll);
204      if (stops.length && s <= stops[stops.length - 1]) {
205        s = stops[stops.length - 1] + 1;
206      }
207      stops.push(s);
208    }
209
210    var sy = window.scrollY;
211    var drawn;
212    if (sy <= stops[0]) {
213      drawn = 0;
214    } else if (sy >= stops[stops.length - 1]) {
215      drawn = totalLen;
216    } else {
217      for (var j = 0; j < stops.length - 1; j++) {
218        if (sy <= stops[j + 1]) {
219          var f = (sy - stops[j]) / (stops[j + 1] - stops[j]);
220          drawn = nodeLens[j] + (nodeLens[j + 1] - nodeLens[j]) * f;
221          break;
222        }
223      }
224    }
225
226    maskPath.setAttribute('stroke-dashoffset', String(totalLen - drawn));
227    for (var k = 0; k < nodeGroups.length; k++) {
228      var lit = drawn >= nodeLens[k] - 1;
229      if (k === 0 && !heroHandoff) lit = false;
230      nodeGroups[k].classList.toggle('is-lit', lit);
231    }
232  }
233
234  function onScroll() {
235    if (raf) return;
236    raf = requestAnimationFrame(function () {
237      raf = 0;
238      update();
239    });
240  }
241
242  var resizeTimer = 0;
243  function onResize() {
244    clearTimeout(resizeTimer);
245    resizeTimer = setTimeout(build, 180);
246  }
247
248  window.addEventListener('scroll', onScroll, { passive: true });
249  window.addEventListener('resize', onResize);
250  window.addEventListener('load', function () {
251    setTimeout(build, 100);
252  });
253
254  // Rebuild when the async news grid changes the page height
255  var news = document.getElementById('home-news-grid');
256  if (news && 'MutationObserver' in window) {
257    new MutationObserver(function () {
258      onResize();
259    }).observe(news, { childList: true, subtree: true });
260  }
261
262  if (document.readyState === 'complete') {
263    build();
264  }
265})();

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.