PageSourceSearch

https://securepaidvpn.com/site-anim.js?v=mup52x9q

js securepaidvpn.com collected 2026-10-02 01:42:45 UTC 5,126 bytes, 146 lines download raw bytes

1// Lightweight, no-dependency animation layer.
2// Honors `prefers-reduced-motion` — bails out early if reduced motion is preferred.
3
4(() => {
5  const reduced = window.matchMedia('(prefers-reduced-motion: reduce)').matches;
6  if (reduced) {
7    document.documentElement.classList.add('motion-reduced');
8    return;
9  }
10  document.documentElement.classList.add('motion-ok');
11
12  const onReady = (fn) => {
13    if (document.readyState !== 'loading') fn();
14    else document.addEventListener('DOMContentLoaded', fn, { once: true });
15  };
16
17  // ─── Reveal on scroll ──────────────────────────────────────────────────
18  const observeReveal = () => {
19    const targets = [];
20
21    document.querySelectorAll('[data-reveal]').forEach((el) => targets.push(el));
22
23    document.querySelectorAll('[data-reveal-children]').forEach((parent) => {
24      Array.from(parent.children).forEach((child, i) => {
25        child.classList.add('reveal-child');
26        child.style.setProperty('--stagger', i);
27        targets.push(child);
28      });
29    });
30
31    if (!targets.length) return;
32
33    const io = new IntersectionObserver(
34      (entries) => {
35        entries.forEach((e) => {
36          if (!e.isIntersecting) return;
37          e.target.classList.add('is-revealed');
38          io.unobserve(e.target);
39        });
40      },
41      { threshold: 0.12, rootMargin: '0px 0px -8% 0px' }
42    );
43
44    targets.forEach((el) => io.observe(el));
45  };
46
47  // ─── Count-up numbers in stat band ────────────────────────────────────
48  const countUp = (el) => {
49    const target = parseFloat(el.dataset.countTo);
50    if (Number.isNaN(target)) return;
51    const suffix = el.dataset.suffix || '';
52    const isFloat = !Number.isInteger(target);
53    const duration = 1300;
54    const start = performance.now();
55    const tick = (now) => {
56      const p = Math.min((now - start) / duration, 1);
57      const eased = 1 - Math.pow(1 - p, 3);
58      const v = target * eased;
59      el.textContent = (isFloat ? v.toFixed(1) : Math.round(v)) + suffix;
60      if (p < 1) requestAnimationFrame(tick);
61    };
62    requestAnimationFrame(tick);
63  };
64
65  const observeCounters = () => {
66    const counters = document.querySelectorAll('[data-count-to]');
67    if (!counters.length) return;
68    const io = new IntersectionObserver(
69      (entries) => {
70        entries.forEach((e) => {
71          if (!e.isIntersecting) return;
72          countUp(e.target);
73          io.unobserve(e.target);
74        });
75      },
76      { threshold: 0.5 }
77    );
78    counters.forEach((el) => {
79      // Reset to 0 so the animation has somewhere to count from.
80      const initial = el.textContent;
81      el.dataset.initial = initial;
82      el.textContent = (el.dataset.suffix || '').includes('★') ? '0.0' + el.dataset.suffix : '0' + (el.dataset.suffix || '');
83      io.observe(el);
84    });
85  };
86
87  // ─── Magnetic mouse-position spotlight on cards ────────────────────────
88  const enableSpotlight = () => {
89    const cards = document.querySelectorAll('.feature, .plan-card, .loc-chip, .dl-card');
90    cards.forEach((card) => {
91      card.classList.add('has-spotlight');
92      card.addEventListener('mousemove', (e) => {
93        const r = card.getBoundingClientRect();
94        const x = ((e.clientX - r.left) / r.width) * 100;
95        const y = ((e.clientY - r.top) / r.height) * 100;
96        card.style.setProperty('--mx', x + '%');
97        card.style.setProperty('--my', y + '%');
98      });
99    });
100  };
101
102  // ─── Subtle parallax on hero phone ─────────────────────────────────────
103  const enableHeroParallax = () => {
104    const art = document.querySelector('.hero-art');
105    if (!art) return;
106    let raf = 0;
107    let tx = 0, ty = 0;
108    document.addEventListener(
109      'mousemove',
110      (e) => {
111        const cx = window.innerWidth / 2;
112        const cy = window.innerHeight / 2;
113        tx = ((e.clientX - cx) / cx) * 6;
114        ty = ((e.clientY - cy) / cy) * 4;
115        if (!raf) {
116          raf = requestAnimationFrame(() => {
117            art.style.setProperty('--px', tx + 'px');
118            art.style.setProperty('--py', ty + 'px');
119            raf = 0;
120          });
121        }
122      },
123      { passive: true }
124    );
125  };
126
127  // ─── Sticky-nav shadow on scroll ───────────────────────────────────────
128  const enableNavShadow = () => {
129    const nav = document.querySelector('.site-nav');
130    if (!nav) return;
131    const onScroll = () => {
132      if (window.scrollY > 6) nav.classList.add('is-scrolled');
133      else nav.classList.remove('is-scrolled');
134    };
135    window.addEventListener('scroll', onScroll, { passive: true });
136    onScroll();
137  };
138
139  onReady(() => {
140    observeReveal();
141    observeCounters();
142    enableSpotlight();
143    enableHeroParallax();
144    enableNavShadow();
145  });
146})();

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.