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.