PageSourceSearch

https://www.hireworkforce.ai/wf-motion.js

js hireworkforce.ai collected 2026-09-24 11:18:26 UTC 7,695 bytes, 161 lines download raw bytes

1/* Workforce AI · shared site motion engine.
2   Exposes window.initWfMotion(root) so every page DC can run the
3   same scroll-reveal / count-up / typewriter / chat / spotlight / parallax
4   behaviour the homepage uses. Per-element guards make it safe to call twice. */
5(function () {
6  if (window.initWfMotion) return;
7  /* Analytics hook (CRO). Provider-agnostic stub: buffers events and mirrors to
8     dataLayer if present. At build, point wfTrack at GA4 / Segment / PostHog.
9     Fires automatically for any clicked element carrying [data-wf-event]. */
10  if (!window.wfTrack) {
11    window.wfTrackQueue = window.wfTrackQueue || [];
12    window.wfTrack = function (event, props) {
13      var payload = { event: event, ts: Date.now() };
14      if (props) for (var k in props) payload[k] = props[k];
15      window.wfTrackQueue.push(payload);
16      try { (window.dataLayer = window.dataLayer || []).push(payload); } catch (e) {}
17    };
18    document.addEventListener('click', function (e) {
19      var el = e.target && e.target.closest ? e.target.closest('[data-wf-event]') : null;
20      if (el) window.wfTrack(el.getAttribute('data-wf-event'), { label: (el.textContent || '').trim().slice(0, 60) });
21    }, true);
22  }
23  window.initWfMotion = function initWfMotion() {
24    if (typeof window === 'undefined') return;
25    var reduce = window.matchMedia && window.matchMedia('(prefers-reduced-motion: reduce)').matches;
26    var hasIO = 'IntersectionObserver' in window;
27    var q = function (s) { return [].slice.call(document.querySelectorAll(s)); };
28
29    // 1) SCROLL REVEAL
30    var reveals = q('[data-reveal]');
31    var showReveal = function (el) {
32      if (el.dataset.shown) return;
33      el.dataset.shown = '1';
34      var dly = parseInt(el.getAttribute('data-reveal-delay') || '0', 10);
35      el.style.transitionDelay = (dly / 1000) + 's';
36      el.style.transform = 'none';
37    };
38    if (!reduce) {
39      reveals.forEach(function (el) {
40        if (el.dataset.shown) return;
41        el.style.transform = 'translateY(14px)';
42        el.style.transition = 'transform .7s cubic-bezier(.2,.7,.2,1)';
43        el.style.willChange = 'transform';
44      });
45      if (hasIO) {
46        var io = new IntersectionObserver(function (ents) {
47          ents.forEach(function (e) { if (e.isIntersecting) { showReveal(e.target); io.unobserve(e.target); } });
48        }, { threshold: 0.01, rootMargin: '0px 0px 20% 0px' });
49        reveals.forEach(function (el) { if (!el.dataset.shown) io.observe(el); });
50      }
51      setTimeout(function () { reveals.forEach(showReveal); }, 800);
52    }
53
54    // 1b) ANIMATION GATE: hold CSS @keyframes animations inside a reveal (or an
55    //     explicit [data-anim-gate]) until it scrolls into view, so the ticks,
56    //     typing, and other screenshot animations play when the visitor sees them
57    //     instead of firing off-screen on load. Paired global CSS pauses them
58    //     until the container gets .wf-anim-run (see Base.astro is:global).
59    var animGates = q('[data-reveal], [data-anim-gate]');
60    if (reduce || !hasIO) {
61      animGates.forEach(function (el) { el.classList.add('wf-anim-run'); });
62    } else {
63      var ioAnim = new IntersectionObserver(function (ents) {
64        ents.forEach(function (e) { if (e.isIntersecting) { e.target.classList.add('wf-anim-run'); ioAnim.unobserve(e.target); } });
65      }, { threshold: 0.2 });
66      animGates.forEach(function (el) { ioAnim.observe(el); });
67    }
68
69    // 2) COUNT-UP
70    var fmt = function (v) { return v.toLocaleString(); };
71    var runCount = function (el) {
72      if (el.dataset.ran) return; el.dataset.ran = '1';
73      var to = parseFloat(el.getAttribute('data-countup')) || 0;
74      var pre = el.getAttribute('data-prefix') || '';
75      var suf = el.getAttribute('data-suffix') || '';
76      if (reduce) { el.textContent = pre + fmt(to) + suf; return; }
77      var dur = 1100, t0 = performance.now();
78      var step = function (now) {
79        var p = Math.min(1, (now - t0) / dur);
80        var v = Math.round(to * (1 - Math.pow(1 - p, 3)));
81        el.textContent = pre + fmt(v) + suf;
82        if (p < 1) requestAnimationFrame(step);
83      };
84      requestAnimationFrame(step);
85    };
86    var counters = q('[data-countup]');
87    if (hasIO && !reduce) {
88      var io2 = new IntersectionObserver(function (ents) {
89        ents.forEach(function (e) { if (e.isIntersecting) { runCount(e.target); io2.unobserve(e.target); } });
90      }, { threshold: 0.5 });
91      counters.forEach(function (el) { if (!el.dataset.ran) io2.observe(el); });
92      setTimeout(function () { counters.forEach(runCount); }, 3000);
93    } else { counters.forEach(runCount); }
94
95    // 3) TYPEWRITER
96    var runType = function (el) {
97      if (el.dataset.ran) return; el.dataset.ran = '1';
98      var full = el.getAttribute('data-type') || '';
99      if (reduce) { el.textContent = full; return; }
100      el.textContent = ''; var i = 0;
101      var tick = function () { el.textContent = full.slice(0, i++); if (i <= full.length) setTimeout(tick, 16 + Math.random() * 32); };
102      setTimeout(tick, 400);
103    };
104    var typers = q('[data-type]');
105    if (hasIO && !reduce) {
106      var io3 = new IntersectionObserver(function (ents) {
107        ents.forEach(function (e) { if (e.isIntersecting) { runType(e.target); io3.unobserve(e.target); } });
108      }, { threshold: 0.6 });
109      typers.forEach(function (el) { if (!el.dataset.ran) io3.observe(el); });
110      setTimeout(function () { typers.forEach(runType); }, 3500);
111    } else { typers.forEach(runType); }
112
113    // 4) PHONE / CHAT message pop-in
114    q('[data-chat]').forEach(function (c) {
115      if (c.dataset.wfChat) return; c.dataset.wfChat = '1';
116      var msgs = [].slice.call(c.querySelectorAll('[data-chat-msg]'));
117      if (reduce || !msgs.length) return;
118      var play = function () {
119        if (c.dataset.ran) return; c.dataset.ran = '1';
120        msgs.forEach(function (m, i) { setTimeout(function () { m.style.opacity = '1'; m.style.transform = 'none'; }, 300 + i * 680); });
121      };
122      msgs.forEach(function (m) { m.style.opacity = '0'; m.style.transform = 'translateY(10px) scale(.98)'; m.style.transition = 'opacity .4s ease, transform .4s ease'; });
123      if (hasIO) {
124        var io4 = new IntersectionObserver(function (ents) {
125          ents.forEach(function (e) { if (e.isIntersecting) { play(); io4.unobserve(e.target); } });
126        }, { threshold: 0.35 });
127        io4.observe(c); setTimeout(play, 3500);
128      } else { play(); }
129    });
130
131    // 5) CURSOR SPOTLIGHT
132    q('[data-spotlight]').forEach(function (el) {
133      if (el.dataset.wfSpot) return; el.dataset.wfSpot = '1';
134      el.addEventListener('pointermove', function (ev) {
135        var r = el.getBoundingClientRect();
136        el.style.setProperty('--mx', ((ev.clientX - r.left) / r.width * 100) + '%');
137        el.style.setProperty('--my', ((ev.clientY - r.top) / r.height * 100) + '%');
138      });
139    });
140
141    // 6) PARALLAX
142    var px = q('[data-parallax]');
143    if (!reduce && px.length && hasIO && !window.__wfParallax) {
144      window.__wfParallax = true;
145      var ticking = false;
146      var onScroll = function () {
147        if (ticking) return; ticking = true;
148        requestAnimationFrame(function () {
149          q('[data-parallax]').forEach(function (el) {
150            var r = el.getBoundingClientRect();
151            var off = (r.top + r.height / 2 - window.innerHeight / 2) * -0.05;
152            if (off > 10) off = 10; else if (off < -10) off = -10;
153            el.style.transform = 'translateY(' + off.toFixed(1) + 'px)';
154          });
155          ticking = false;
156        });
157      };
158      window.addEventListener('scroll', onScroll, { passive: true }); onScroll();
159    }
160  };
161})();

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.