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.