1/* Hero story animation: "How an alert travels the coalition." 2 A tip comes in â it reaches the OPS Network hub â partner agencies 3 across the network light up. The motion IS the pitch; it loops with a 4 fresh tip each cycle. Captions label each stage so the story is 5 unmistakable in the first four seconds. */ 6(function () { 7 var hero = document.querySelector('.hero'); 8 if (!hero) return; 9 10 var reduced = window.matchMedia('(prefers-reduced-motion: reduce)').matches; 11 12 var canvas = document.createElement('canvas'); 13 canvas.className = 'hero-network'; 14 canvas.setAttribute('aria-hidden', 'true'); 15 var media = hero.querySelector('.hero-media'); 16 if (media) { 17 media.insertAdjacentElement('afterend', canvas); 18 } else { 19 hero.insertBefore(canvas, hero.firstChild); 20 } 21 var ctx = canvas.getContext('2d'); 22 23 var W = 0, H = 0, DPR = 1; 24 var hub = null; // OPS network hub 25 var agencies = []; // partner agency nodes 26 var tip = null; // current cycle's tip origin 27 var tipSeed = 0; 28 var startTime = 0; 29 var running = false; 30 var rafId = 0; 31 var handedOff = false; 32 33 // Cycle timeline (seconds) 34 var T_TIP_IN = 0.6; // tip appears + pings 35 var T_ARRIVE = 1.3; // tip packet reaches the hub 36 var T_HUB_FLASH = 1.6; // hub lights up 37 var T_CAST_END = 2.6; // all agencies alerted 38 var T_FADE = 4.4; // network holds, working 39 var CYCLE = 5.0; // fade out, next tip 40 41 var BLUE = '82, 190, 255'; 42 var SOFT = '150, 210, 255'; 43 44 // Each cycle showcases a different kind of transmission moving 45 // through the coalition, with its own broadcast outcome message. 46 var TIP_LABELS = [ 47 ['TIP RECEIVED', 'SHARED INSTANTLY WITH PARTNER AGENCIES'], 48 ['OFFICER ALERT', 'SELECTED MEMBERS NOTIFIED IN REAL TIME'], 49 ['LENS NOTIFICATION', '600+ AGENCIES NOTIFIED INSTANTLY'], 50 ['SHIFT BRIEFING', 'ALL OFFICERS AND SHIFTS NOTIFIED'], 51 ['CITIZEN NOTIFICATION', '"MY BIKE WAS STOLEN"'], 52 ['CAMERA ALERT', 'INSTANT STATEWIDE CAMERA LOCATIONS'], 53 ['GENERATE PHOTO ARRAY', 'SUSPECT IMAGE DISTRIBUTED'], 54 ['SCENE SECURED', 'IMS|HUB⢠ACTIVATED'] 55 ]; 56 var tipLabel = TIP_LABELS[0][0]; 57 var castLabel = TIP_LABELS[0][1]; 58 59 function rand(min, max) { return min + Math.random() * (max - min); } 60 function clamp01(v) { return Math.max(0, Math.min(1, v)); } 61 function easeOut(x) { return 1 - Math.pow(1 - x, 3); } 62 function easeInOut(x) { 63 return x < 0.5 ? 4 * x * x * x : 1 - Math.pow(-2 * x + 2, 3) / 2; 64 } 65 66 function announceConverged() { 67 if (handedOff) return; 68 handedOff = true; 69 try { 70 window.dispatchEvent(new CustomEvent('opsHeroConverged')); 71 } catch (e) { /* ignore */ } 72 } 73 74 function resize() { 75 DPR = Math.min(2, window.devicePixelRatio || 1); 76 W = hero.clientWidth; 77 H = hero.clientHeight; 78 canvas.width = W * DPR; 79 canvas.height = H * DPR; 80 canvas.style.width = W + 'px'; 81 canvas.style.height = H + 'px'; 82 ctx.setTransform(DPR, 0, 0, DPR, 0, 0); 83 } 84 85 // Layout keeps clear of the hero copy (center-left, lower half) and the 86 // officer in the photo (right-center). Story lives in the upper band and 87 // the left edge. 88 function build() { 89 var small = W < 900; 90 hub = { x: W * 0.30, y: Math.max(150, H * 0.24) }; 91 92 var slots = small 93 ? [ 94 [0.08, 0.14], [0.24, 0.09], [0.42, 0.13], 95 [0.06, 0.38], [0.07, 0.60], [0.50, 0.26] 96 ] 97 : [ 98 [0.07, 0.13], [0.17, 0.09], [0.31, 0.07], [0.44, 0.11], 99 [0.55, 0.18], [0.14, 0.27], [0.47, 0.28], 100 [0.05, 0.36], [0.045, 0.52], [0.065, 0.68], 101 [0.91, 0.09], [0.955, 0.19] 102 ]; 103 104 agencies = slots.map(function (s, i) { 105 return { 106 x: W * s[0] + rand(-8, 8), 107 y: Math.max(128, H * s[1] + rand(-6, 6)), 108 arrive: T_HUB_FLASH + ((i * 7) % slots.length) / slots.length * (T_CAST_END - T_HUB_FLASH - 0.15) + rand(0, 0.12), 109 phase: rand(0, Math.PI * 2) 110 }; 111 }); 112 113 newTip(); 114 } 115 116 // A fresh tip origin each cycle, from varied corners of the scene. 117 function newTip() { 118 var pool = W < 900 119 ? [[0.06, 0.78], [0.12, 0.86], [0.55, 0.08], [0.04, 0.26]] 120 : [[0.05, 0.78], [0.11, 0.86], [0.56, 0.07], [0.035, 0.24], [0.60, 0.33], [0.88, 0.28]]; 121 var p = pool[tipSeed % pool.length]; 122 var pair = TIP_LABELS[tipSeed % TIP_LABELS.length]; 123 tipLabel = pair[0];
124 castLabel = pair[1]; 125 tipSeed++; 126 tip = { x: W * p[0], y: Math.max(128, H * p[1]) }; 127 } 128 129 // Quadratic path tip â hub with a gentle bow. 130 function tipPathPoint(f) { 131 var mx = (tip.x + hub.x) / 2; 132 var my = (tip.y + hub.y) / 2; 133 var dx = hub.x - tip.x; 134 var dy = hub.y - tip.y; 135 var len = Math.sqrt(dx * dx + dy * dy) || 1; 136 var cx = mx - (dy / len) * len * 0.18; 137 var cy = my + (dx / len) * len * 0.18; 138 var u = 1 - f; 139 return { 140 x: u * u * tip.x + 2 * u * f * cx + f * f * hub.x, 141 y: u * u * tip.y + 2 * u * f * cy + f * f * hub.y, 142 cx: cx, 143 cy: cy 144 }; 145 } 146 147 function caption(text, x, y, alpha, align) { 148 if (W < 700 || alpha <= 0) return; 149 ctx.font = '700 11px "Source Sans 3", Arial, sans-serif'; 150 ctx.textAlign = align || 'left'; 151 ctx.textBaseline = 'middle'; 152 ctx.fillStyle = 'rgba(205, 232, 255, ' + Math.min(0.9, alpha) + ')'; 153 ctx.fillText(text, x, y); 154 } 155 156 function ping(x, y, f, maxR, strength) { 157 if (f <= 0 || f >= 1) return; 158 ctx.beginPath(); 159 ctx.arc(x, y, 4 + f * maxR, 0, Math.PI * 2); 160 ctx.strokeStyle = 'rgba(' + BLUE + ', ' + (strength * (1 - f)) + ')'; 161 ctx.lineWidth = 1.6; 162 ctx.stroke(); 163 } 164 165 function dot(x, y, r, alpha, glow) { 166 ctx.beginPath(); 167 ctx.arc(x, y, r, 0, Math.PI * 2); 168 ctx.fillStyle = 'rgba(' + BLUE + ', ' + alpha + ')'; 169 if (glow) { 170 ctx.shadowColor = 'rgba(' + BLUE + ', 0.9)'; 171 ctx.shadowBlur = glow; 172 } 173 ctx.fill(); 174 ctx.shadowBlur = 0; 175 } 176 177 function draw(t) { 178 ctx.clearRect(0, 0, W, H); 179 var ct = t % CYCLE; 180 181 // Master fade at the end of each cycle 182 var fade = 1; 183 if (ct > T_FADE) fade = 1 - easeInOut(clamp01((ct - T_FADE) / (CYCLE - T_FADE))); 184 185 /* â Stage 1: the tip â */ 186 var tipIn = clamp01(ct / 0.5); 187 if (tipIn > 0) { 188 dot(tip.x, tip.y, 3.5 * easeOut(tipIn), 0.9 * fade, 8); 189 ping(tip.x, tip.y, clamp01(ct / T_TIP_IN), 26, 0.6 * fade); 190 ping(tip.x, tip.y, clamp01((ct - 0.35) / T_TIP_IN), 26, 0.45 * fade); 191 var tipLblA = clamp01((ct - 0.15) / 0.4) * clamp01((T_ARRIVE + 0.3 - ct) / 0.4); 192 caption(tipLabel, tip.x + 14, tip.y - 14, tipLblA * fade); 193 } 194 195 /* â Stage 2: tip travels to OPS â */ 196 var travelF = clamp01((ct - T_TIP_IN) / (T_ARRIVE - T_TIP_IN)); 197 if (travelF > 0) { 198 var eF = easeInOut(travelF); 199 // trail 200 ctx.beginPath(); 201 ctx.moveTo(tip.x, tip.y); 202 var steps = 24; 203 for (var s = 1; s <= steps * eF; s++) { 204 var pp = tipPathPoint(s / steps); 205 ctx.lineTo(pp.x, pp.y); 206 } 207 ctx.strokeStyle = 'rgba(' + SOFT + ', ' + (0.4 * fade) + ')'; 208 ctx.lineWidth = 1.4; 209 ctx.setLineDash([2, 6]); 210 ctx.stroke(); 211 ctx.setLineDash([]); 212 // packet head 213 if (travelF < 1) { 214 var head = tipPathPoint(eF); 215 dot(head.x, head.y, 3, 0.95 * fade, 12); 216 } 217 } 218 219 /* â Stage 3: OPS hub lights â */ 220 var hubLit = ct >= T_ARRIVE; 221 if (hubLit) { 222 var flashF = clamp01((ct - T_ARRIVE) / (T_HUB_FLASH - T_ARRIVE)); 223 var breathe = 0.8 + 0.2 * Math.sin(t * 2.2); 224 ping(hub.x, hub.y, flashF, 44, 0.85 * fade); 225 dot(hub.x, hub.y, 5.5, (0.95 * breathe) * fade, 16); 226 // hub ring 227 ctx.beginPath(); 228 ctx.arc(hub.x, hub.y, 11, 0, Math.PI * 2); 229 ctx.strokeStyle = 'rgba(' + BLUE + ', ' + (0.55 * fade) + ')'; 230 ctx.lineWidth = 1.4; 231 ctx.stroke(); 232 caption('OPS NETWORKâ¢', hub.x + 20, hub.y - 2, fade * clamp01((ct - T_ARRIVE) / 0.35)); 233 } else { 234 // dormant hub marker so the destination reads before arrival 235 dot(hub.x, hub.y, 3, 0.35 * fade, 0); 236 ctx.beginPath(); 237 ctx.arc(hub.x, hub.y, 11, 0, Math.PI * 2); 238 ctx.strokeStyle = 'rgba(' + SOFT + ', ' + (0.22 * fade) + ')'; 239 ctx.lineWidth = 1; 240 ctx.stroke(); 241 } 242 243 /* â Stage 4: broadcast to partner agencies â */ 244 var anyLit = false; 245 for (var i = 0; i < agencies.length; i++) { 246 var a = agencies[i]; 247 var castStart = T_HUB_FLASH - 0.1; 248 var f = clamp01((ct - castStart) / (a.arrive - castStart)); 249 if (f <= 0) continue; 250 var ef = easeOut(f); 251 var lx = hub.x + (a.x - hub.x) * ef; 252 var ly = hub.y + (a.y - hub.y) * ef; 253 254 // link line 255 ctx.beginPath(); 256 ctx.moveTo(hub.x, hub.y); 257 ctx.lineTo(lx, ly); 258 ctx.strokeStyle = 'rgba(' + SOFT + ', ' + ((f >= 1 ? 0.20 : 0.32) * fade) + ')'; 259 ctx.lineWidth = 1; 260 ctx.stroke(); 261 262 if (f < 1) { 263 dot(lx, ly, 2.4, 0.9 * fade, 9); // pulse in flight 264 } else { 265 anyLit = true; 266 var since = ct - a.arrive; 267 ping(a.x, a.y, clamp01(since / 0.9), 20, 0.55 * fade); 268 var br = 0.7 + 0.3 * Math.sin(t * 1.8 + a.phase); 269 dot(a.x, a.y, 3, 0.85 * br * fade, 7); 270 } 271 } 272 273 if (anyLit) { 274 var castLblA = clamp01((ct - T_HUB_FLASH - 0.3) / 0.5); 275 caption(castLabel, W * 0.055, Math.max(104, H * 0.115), castLblA * fade); 276 } 277 278 /* â Stage 5: the network keeps working â */ 279 if (ct >
279 T_CAST_END && ct < T_FADE) { 280 var k = Math.floor(ct / 0.55) % agencies.length; 281 var wf = (ct % 0.55) / 0.55; 282 var wa = agencies[k]; 283 if (ct - 0.55 > wa.arrive) { 284 var wx = hub.x + (wa.x - hub.x) * wf; 285 var wy = hub.y + (wa.y - hub.y) * wf; 286 dot(wx, wy, 1.8, 0.7 * fade, 6); 287 } 288 } 289 } 290 291 function frame(now) { 292 if (!startTime) startTime = now; 293 var t = (now - startTime) / 1000; 294 if (t > T_CAST_END) announceConverged(); 295 // fresh tip origin at each cycle boundary 296 var cycleIdx = Math.floor(t / CYCLE); 297 if (cycleIdx !== frame.lastCycle) { 298 frame.lastCycle = cycleIdx; 299 if (cycleIdx > 0) newTip(); 300 } 301 draw(t); 302 if (running) rafId = requestAnimationFrame(frame); 303 } 304 frame.lastCycle = 0; 305 306 function start() { 307 if (running || reduced) return; 308 running = true; 309 rafId = requestAnimationFrame(frame); 310 } 311 312 function stop() { 313 running = false; 314 if (rafId) cancelAnimationFrame(rafId); 315 rafId = 0; 316 } 317 318 function init() { 319 resize(); 320 build(); 321 startTime = 0; 322 frame.lastCycle = 0; 323 if (reduced) { 324 // Static end-state: hub + agencies lit, story labels visible. 325 announceConverged(); 326 draw(T_CAST_END + 0.4); 327 return; 328 } 329 start(); 330 } 331 332 if ('IntersectionObserver' in window && !reduced) { 333 new IntersectionObserver(function (entries) { 334 entries.forEach(function (e) { 335 if (e.isIntersecting) start(); else stop(); 336 }); 337 }, { threshold: 0.05 }).observe(hero); 338 } 339 document.addEventListener('visibilitychange', function () { 340 if (reduced) return; 341 if (document.hidden) stop(); else start(); 342 }); 343 344 var resizeTimer = 0; 345 window.addEventListener('resize', function () { 346 clearTimeout(resizeTimer); 347 resizeTimer = setTimeout(function () { 348 resize(); 349 build(); 350 }, 180); 351 }); 352 353 init(); 354})();
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.