PageSourceSearch

https://onlinepolicingsolutions.com/assets/js/hero-network.js

js onlinepolicingsolutions.com collected 2026-10-01 23:45:15 UTC 11,698 bytes, 354 lines download raw bytes

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.