PageSourceSearch

https://www.aidash.com/wp-content/themes/aiwp/js/home-pf.js?v=15

js aidash.com collected 2026-10-01 11:24:24 UTC 20,250 bytes, 419 lines download raw bytes

1/* ============================================================
2   AiDASH PreventionFirst homepage — vanilla-JS port of the
3   framer-motion / Lenis behaviours from the Next.js build.
4   ============================================================ */
5   (function () {
6    "use strict";
7    var root = document.querySelector(".pf-home");
8    if (!root) return;
9  
10    var mqMobile = window.matchMedia("(max-width:767px)");
11    var isMobile = function () { return mqMobile.matches; };
12  
13    /* ── helpers ─────────────────────────────────────────── */
14    function clamp(v, a, b) { return Math.min(b, Math.max(a, v)); }
15    // framer useTransform: piecewise-linear, clamped at the ends
16    function mapRange(v, inp, out) {
17      if (v <= inp[0]) return out[0];
18      if (v >= inp[inp.length - 1]) return out[out.length - 1];
19      for (var i = 1; i < inp.length; i++) {
20        if (v <= inp[i]) {
21          var t = (v - inp[i - 1]) / (inp[i] - inp[i - 1]);
22          return out[i - 1] + t * (out[i] - out[i - 1]);
23        }
24      }
25      return out[out.length - 1];
26    }
27    // scroll progress mirroring framer useScroll()
28    // mode 'cover'  = offset ['start start','end end']
29    // mode 'pass'   = offset ['start end','end start']
30    function progress(el, mode) {
31      if (!el || el.offsetParent === null && el.style.position !== "sticky") {
32        var r0 = el && el.getBoundingClientRect();
33        if (!r0 || (r0.width === 0 && r0.height === 0)) return -1; // hidden
34      }
35      var r = el.getBoundingClientRect();
36      var vh = window.innerHeight;
37      if (mode === "pass") {
38        return clamp((vh - r.top) / (r.height + vh), 0, 1);
39      }
40      return clamp(-r.top / (r.height - vh), 0, 1);
41    }
42  
43    /* ── Scrolling ─────────────────────────────────────────────
44       We intentionally use NATIVE scrolling. The CDN Lenis build
45       damped wheel input to ~10-20% of normal speed (measured), which
46       is why the page felt like it "barely moved". Native scroll is
47       1:1, smooth at the OS level, and drives every animation below
48       via window.scrollY just the same. */
49    var lenis = null;
50  
51    /* ── Reveal on scroll (framer whileInView) ─────────────── */
52    var io = new IntersectionObserver(function (entries) {
53      entries.forEach(function (e) {
54        if (e.isIntersecting) {
55          var d = e.target.getAttribute("data-delay");
56          if (d) e.target.style.transitionDelay = d + "s";
57          e.target.classList.add("pf-in");
58          io.unobserve(e.target);
59        }
60      });
61    }, { threshold: 0.1, rootMargin: "-40px 0px" });
62    root.querySelectorAll(".pf-reveal").forEach(function (el) { io.observe(el); });
63  
64    // Mockup entrance (platform pillars)
65    var mock = root.querySelector(".pf-mock__inner");
66    if (mock) {
67      var mio = new IntersectionObserver(function (es) {
68        es.forEach(function (e) { if (e.isIntersecting) { mock.classList.add("pf-in"); mio.disconnect(); } });
69      }, { threshold: 0.2 });
70      mio.observe(mock);
71    }
72  
73    /* ── HERO: load fade-ups ───────────────────────────────── */
74    requestAnimationFrame(function () {
75      root.querySelectorAll(".pf-hero .pf-fade").forEach(function (el) {
76        var d = parseFloat(el.getAttribute("data-delay") || "0");
77        setTimeout(function () { el.classList.add("pf-in"); }, d * 1000);
78      });
79    });
80  
81    /* ── HERO: flipboard word rotation (3D split-flap flip) ─── */
82    (function () {
83      var flip = root.querySelector(".pf-hero__flip-word");
84      if (!flip) return;
85      var words;
86      try { words = JSON.parse(flip.getAttribute("data-words") || "[]"); } catch (e) { words = []; }
87      if (!words.length) return;
88      var i = 0, HOLD = 2200, HALF = 300;
89      // fast fold-away, then a slightly springy settle as the new flap lands
90      var OUT = "cubic-bezier(.45,0,.85,.35)", IN = "cubic-bezier(.2,.9,.35,1.25)";
91      var reduce = window.matchMedia && window.matchMedia("(prefers-reduced-motion: reduce)").matches;
92  
93      flip.textContent = words[0];
94      flip.style.willChange = "transform, opacity";
95  
96      if (reduce) {
97        // honour reduced-motion: plain crossfade, no movement
98        setInterval(function () {
99          flip.style.transition = "opacity .25s ease";
100          flip.style.opacity = "0";
101          setTimeout(function () {
102            i = (i + 1) % words.length;
103            flip.textContent = words[i];
104            flip.style.opacity = "1";
105          }, 260);
106        }, HOLD + 520);
107        return;
108      }
109  
110      // Pure vertical slide + fade — no rotation — matching the Next.js flipboard:
111      // outgoing rises up and clips out the top (y: 0 -> -32, fade out);
112      // incoming rises from below (y: 32 -> 0, fade in). IN eases with a small
113      // overshoot to emulate the framer spring (stiffness 300, damping 28).
114      var RISE = 32; // px — matches Next.js desktop (y: 32 / -32)
115      // Animate the container height (no fixed height) so the content below is
116      // pushed down for two-line words and springs back up for one-line ones —
117      // this mirrors framer-motion's `layout` animation in the Next.js hero.
118      // On mobile the box is a fixed two-line slot in CSS instead — the words flip
119      // inside it so the paragraph and CTAs below never move (any inline height is
120      // cleared, otherwise it would fight the reserved slot).
121      var flipBox = flip.parentNode; // .pf-hero__flip
122      flipBox.style.transition = "height " + HALF + "ms ease";
123      setInterval(function () {
124        var fluid = !isMobile();
125        // pin current height so the upcoming resize can transition from it
126        if (fluid) flipBox.style.height = flip.offsetHeight + "px";
127        else flipBox.style.height = "";
128        flip.style.transition = "transform " + HALF + "ms " + OUT + ", opacity " + HALF + "ms ease-in";
129        flip.style.transform = "translateY(-" + RISE + "px)";
130        flip.style.opacity = "0";
131        setTimeout(function () {
132          i = (i + 1) % words.length;
133          flip.textContent = words[i];
134          flip.style.transition = "none";
135          flip.style.transform = "translateY(" + RISE + "px)";
136          flip.style.opacity = "0";
137          void flip.offsetHeight; // force reflow before animating in
138          flip.style.transition = "transform " + HALF + "ms " + IN + ", opacity " + HALF + "ms ease-out";
139          flip.style.transform = "translateY(0)";
140          flip.style.opacity = "1";
141          if (fluid) flipBox.style.height = flip.offsetHeight + "px"; // grow/shrink to new word
142        }, HALF);
143      }, HOLD + HALF * 2);
144    })();
145  
146    /* ── GlowCards: cursor-following spotlight ─────────────────
147       Drives --x/--y on every .pf-glow so the radial spotlight and
148       border glow track the pointer (Outcomes, SatelliteFirst
149       Advantages, and Cost-of-Reaction cards). Without this the
150       gradients stay frozen at the top-left corner (0,0). */
151    (function () {
152      var cards = root.querySelectorAll(".pf-glow");
153      if (!cards.length) return;
154      for (var c = 0; c < cards.length; c++) {
155        (function (card) {
156          card.addEventListener("pointermove", function (e) {
157            var r = card.getBoundingClientRect();
158            if (!r.width || !r.height) return;
159            var x = e.clientX - r.left, y = e.clientY - r.top;
160            card.style.setProperty("--x", x.toFixed(2));
161            card.style.setProperty("--y", y.toFixed(2));
162            card.style.setProperty("--xp", (x / r.width).toFixed(2));
163            card.style.setProperty("--yp", (y / r.height).toFixed(2));
164          });
165        })(cards[c]);
166      }
167    })();
168  
169    /* ── Cost-of-Reaction: mobile swipe carousel dots ─────────
170       Syncs the pagination dots with the horizontal scroll position and
171       lets a dot tap scroll its card into view. Dots are hidden on desktop. */
172    (function () {
173      var grid = root.querySelector(".pf-stats__grid");
174      var dotWrap = root.querySelector(".pf-stats__dots");
175      if (!grid || !dotWrap) return;
176      var dots = dotWrap.querySelectorAll(".pf-stats__dot");
177      var cards = grid.querySelectorAll(".pf-stat");
178      if (!dots.length || !cards.length) return;
179  
180      function activeIndex() {
181        var g = grid.getBoundingClientRect();
182        var mid = g.left + g.width / 2;
183        var best = 0, bestD = Infinity;
184        for (var k = 0; k < cards.length; k++) {
185          var r = cards[k].getBoundingClientRect();
186          var d = Math.abs(r.left + r.width / 2 - mid);
187          if (d < bestD) { bestD = d; best = k; }
188        }
189        return best;
190      }
191      function sync() {
192        var a = activeIndex();
193        for (var k = 0; k < dots.length; k++) dots[k].classList.toggle("is-active", k === a);
194      }
195      var raf;
196      grid.addEventListener("scroll", function () {
197        if (raf) cancelAnimationFrame(raf);
198        raf = requestAnimationFrame(sync);
199      }, { passive: true });
200      for (var k = 0; k < dots.length; k++) {
201        (function (idx) {
202          dots[idx].addEventListener("click", function () {
203            var g = grid.getBoundingClientRect(), r = cards[idx].getBoundingClientRect();
204            grid.scrollTo({ left: grid.scrollLeft + (r.left - g.left) - (g.width - r.width) / 2, behavior: "smooth" });
205          });
206        })(k);
207      }
208      sync();
209    })();
210  
211    /* ── HERO: cursor spotlight overlay ────────────────────── */
212    var heroSpot = (function () {
213      var hero = root.querySelector(".pf-hero");
214      var ov = root.querySelector(".pf-hero__overlay");
215      if (!hero || !ov) return function () {};
216      var tx = -9999, ty = -9999, cx = -9999, cy = -9999;
217      hero.addEventListener("mouseenter", function (e) {
218        var r = hero.getBoundingClientRect(); tx = cx = e.clientX - r.left; ty = cy = e.clientY - r.top;
219      });
220      hero.addEventListener("mousemove", function (e) {
221        var r = hero.getBoundingClientRect(); tx = e.clientX - r.left; ty = e.clientY - r.top;
222      });
223      hero.addEventListener("mouseleave", function () { tx = -9999; ty = -9999; });
224      return function () {
225        cx += (tx - cx) * 0.18; cy += (ty - cy) * 0.18;
226        var mask = "radial-gradient(circle 130px at " + cx + "px " + cy + "px, transparent 0%, transparent 70px, #00000040 100px, #000000 130px)";
227        ov.style.webkitMaskImage = mask; ov.style.maskImage = mask;
228      };
229    })();
230  
231    /* ── CLOUD TRANSITION parallax ─────────────────────────── */
232    var cloud = (function () {
233      var el = root.querySelector(".pf-cloud");
234      if (!el) return function () {};
235      var c = el.querySelector(".pf-cloud__c"), l = el.querySelector(".pf-cloud__l"), r = el.querySelector(".pf-cloud__r");
236      return function () {
237        var p = progress(el, "pass"); if (p < 0) return;
238        if (l) { l.style.transform = "translateX(" + mapRange(p, [0, 1], [0, -30]) + "%)"; l.style.opacity = mapRange(p, [0, 1], [1, 0]); }
239        if (r) { r.style.transform = "translateX(" + mapRange(p, [0, 1], [0, 30]) + "%)"; r.style.opacity = mapRange(p, [0, 1], [1, 0]); }
240        if (c) { c.style.transform = "translateY(" + mapRange(p, [0, 1], [0, -25]) + "%)"; c.style.opacity = mapRange(p, [0, 1], [1, 0]); }
241      };
242    })();
243  
244    /* ── ZOOM PARALLAX (desktop) ───────────────────────────── */
245    var zoom = (function () {
246      var el = root.querySelector(".pf-zoom");
247      if (!el) return function () {};
248      var scalers = el.querySelectorAll(".pf-zoom__scaler");
249      var box0 = el.querySelector(".pf-zi-0");
250      var hots = el.querySelector(".pf-zoom__hotspots");
251      var targets = [5, 7, 8, 7, 9, 10, 11];
252      return function () {
253        if (el.offsetParent === null) return;
254        var p = progress(el, "cover"); if (p < 0) return;
255        scalers.forEach(function (s, i) {
256          s.style.transform = "scale(" + mapRange(p, [0, 1], [1, targets[i] || 5]) + ")";
257        });
258        if (box0) box0.style.borderRadius = mapRange(p, [0, 0.3], [6, 0]) + "px";
259        if (hots) hots.style.opacity = mapRange(p, [0, 0.70, 0.88, 1], [0, 0, 1, 1]);
260      };
261    })();
262  
263    /* ── SATELLITE FIRST (desktop tabs) ────────────────────── */
264    (function () {
265      var el = root.querySelector(".pf-sat__desktop");
266      if (!el) return;
267      var tabs   = el.querySelectorAll("[data-sat-tab]");
268      var panels = el.querySelectorAll("[data-sat-panel]");
269      var wraps  = el.querySelectorAll(".pf-sat__vid");
270      var nexts  = el.querySelectorAll("[data-sat-advance]");
271      if (!tabs.length) return;
272      var AUTO = 8000;                       /* auto-advance cadence (matches the Next.js build) */
273      var active = 0, timer = null, primed = false;
274  
275      /* the two layouts both ship in the markup — only fetch the clips for the one
276         that is actually displayed, so phones never pull the desktop stack */
277      function visible() { return el.offsetParent !== null; }
278      function prime() {
279        if (primed || !visible()) return;
280        primed = true;
281        wraps.forEach(function (w) { var v = w.querySelector("video"); if (v) v.preload = "auto"; });
282      }
283      function render() {
284        prime();
285        tabs.forEach(function (t, i) {
286          t.classList.toggle("pf-active", i === active);
287          t.classList.toggle("pf-filled", i <= active);
288          t.setAttribute("aria-selected", i === active ? "true" : "false");
289        });
290        panels.forEach(function (p, i) {
291          var on = i === active;
292          p.classList.toggle("pf-active", on);
293          p.setAttribute("aria-hidden", on ? "false" : "true");
294          var btn = p.querySelector("[data-sat-advance]");
295          if (btn) btn.disabled = !on;
296        });
297        wraps.forEach(function (w, i) {
298          w.classList.toggle("pf-active", i === active);
299          var v = w.querySelector("video"); if (!v) return;
300          if (i === active && visible()) { try { v.currentTime = 0; } catch (e) {} v.play().catch(function () {}); }
301          else { v.pause(); try { v.currentTime = 0; } catch (e) {} }
302        });
303      }
304      function go(i) { active = (i + tabs.length) % tabs.length; render(); }
305      function stopAuto() { if (timer) { clearInterval(timer); timer = null; } }
306  
307      tabs.forEach(function (t, i) { t.addEventListener("click", function () { stopAuto(); go(i); }); });
308      nexts.forEach(function (b) { b.addEventListener("click", function () { stopAuto(); go(active + 1); }); });
309      window.addEventListener("resize", function () { if (!primed) render(); }, { passive: true });
310  
311      render();
312      timer = setInterval(function () { if (visible()) go(active + 1); }, AUTO);
313    })();
314  
315    /* ── SATELLITE FIRST (mobile tap stepper) ──────────────── */
316    (function () {
317      var el = root.querySelector(".pf-satm");
318      if (!el) return;
319      var vids = el.querySelectorAll(".pf-satm__videos video");
320      var stepsEls = el.querySelectorAll(".pf-satm__step");
321      var prevBtns = el.querySelectorAll("[data-sat-prev]");
322      var nextBtns = el.querySelectorAll("[data-sat-next]");
323      var active = 0, max = stepsEls.length - 1, primed = false;
324      /* same deal as the desktop tabs: only fetch this stack when it is on screen */
325      function visible() { return el.offsetParent !== null; }
326      function prime() {
327        if (primed || !visible()) return;
328        primed = true;
329        vids.forEach(function (v) { v.preload = "auto"; });
330      }
331      function render() {
332        prime();
333        vids.forEach(function (v, i) {
334          if (i === active) { v.classList.add("pf-active"); if (visible()) { v.currentTime = 0; v.play().catch(function () {}); } }
335          else { v.classList.remove("pf-active"); v.pause(); }
336        });
337        stepsEls.forEach(function (s, i) { s.classList.toggle("pf-active", i === active); });
338        prevBtns.forEach(function (b) { b.disabled = active === 0; });
339        nextBtns.forEach(function (b) { b.disabled = active === max; });
340      }
341      prevBtns.forEach(function (b) { b.addEventListener("click", function () { active = Math.max(0, active - 1); render(); }); });
342      nextBtns.forEach(function (b) { b.addEventListener("click", function () { active = Math.min(max, active + 1); render(); }); });
343      window.addEventListener("resize", function () { if (!primed) render(); }, { passive: true });
344      render();
345    })();
346  
347    /* ── SHIFT mobile hotspot toggles ──────────────────────── */
348    root.querySelectorAll(".pf-mhot__pt").forEach(function (pt) {
349      pt.addEventListener("click", function () {
350        var was = pt.classList.contains("pf-open");
351        root.querySelectorAll(".pf-mhot__pt").forEach(function (p) { p.classList.remove("pf-open"); });
352        if (!was) pt.classList.add("pf-open");
353      });
354    });
355  
356    /* ── CITYSCAPE (scroll beats + auto-scroll) ────────────── */
357    var city = (function () {
358      var el = root.querySelector(".pf-city");
359      if (!el) return { frame: function () {} };
360      var lit = el.querySelector(".pf-city__lit");
361      var shade = el.querySelector(".pf-city__shade");
362      var b1 = el.querySelector('[data-beat="1"]');
363      var b2 = el.querySelector('[data-beat="2"]');
364      var end = el.querySelector(".pf-city__end");
365      var prompt = el.querySelector(".pf-city__prompt");
366      var cur = 0, showPrompt = false, auto = false, autoRAF = null, idleT = null;
367  
368      function stopAuto() { auto = false; if (autoRAF) { cancelAnimationFrame(autoRAF); autoRAF = null; } }
369      function startAuto() {
370        if (auto) return; auto = true;
371        var step = function () {
372          if (!auto) return;
373          if (lenis) lenis.scrollTo(window.scrollY + 1.5, { immediate: true });
374          else window.scrollBy(0, 1.5);
375          autoRAF = requestAnimationFrame(step);
376        };
377        autoRAF = requestAnimationFrame(step);
378      }
379      function resetIdle() {
380        stopAuto(); showPrompt = false; if (prompt) prompt.style.opacity = "0";
381        if (idleT) clearTimeout(idleT);
382        // Show the "keep scrolling" chevron hint after idle. (No auto-scroll:
383        // hijacking the scroll position fought the user and felt broken.)
384        idleT = setTimeout(function () { showPrompt = true; }, 5000);
385      }
386      ["scroll", "wheel", "touchmove", "keydown"].forEach(function (ev) {
387        window.addEventListener(ev, function () { stopAuto(); resetIdle(); }, { passive: true });
388      });
389      resetIdle();
390  
391      return {
392        frame: function () {
393          if (el.offsetParent === null) return;
394          var p = progress(el, "cover"); if (p < 0) return;
395          cur = p;
396          var litOp = clamp(mapRange(p, [0.30, 0.46], [0, 1]), 0, 1);
397          if (lit) lit.style.opacity = litOp;
398          if (shade) shade.style.opacity = litOp;
399          if (b1) b1.style.opacity = mapRange(p, [0, 0.06, 0.32, 0.40], [0, 1, 1, 0]);
400          if (b2) b2.style.opacity = mapRange(p, [0.38, 0.46, 0.68, 0.76], [0, 1, 1, 0]);
401          var e3 = clamp(mapRange(p, [0.76, 0.84], [0, 1]), 0, 1);
402          if (end) end.style.opacity = e3;
403          if (p >= 0.98) { stopAuto(); showPrompt = false; if (idleT) clearTimeout(idleT); }
404          if (prompt) prompt.style.opacity = (showPrompt && p < 0.92) ? "1" : "0";
405        }
406      };
407    })();
408  
409    /* ── master rAF loop ───────────────────────────────────── */
410    function loop() {
411      heroSpot();
412      cloud();
413      zoom();
414      city.frame();
415      requestAnimationFrame(loop);
416    }
417    requestAnimationFrame(loop);
418  })();
419  

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.