PageSourceSearch

https://www.responsible.ai/wp-content/themes/rai-theme/assets/src/js/stats-counter.js

js responsible.ai collected 2026-10-01 10:34:54 UTC 2,559 bytes, 91 lines download raw bytes

1/**
2 * Stats Bar Count-Up Animation
3 * Triggers when section scrolls into viewport
4 * Respects prefers-reduced-motion accessibility setting
5 */
6(function() {
7  'use strict';
8
9  const CONFIG = {
10    duration: 2000,        // 2 seconds
11    threshold: 0.5,        // 50% visible before triggering
12    rootMargin: '0px'
13  };
14
15  function initStatsCountUp() {
16    // Skip animation if user prefers reduced motion
17    if (window.matchMedia('(prefers-reduced-motion: reduce)').matches) {
18      return;
19    }
20
21    const statElements = document.querySelectorAll('[data-countup="true"]');
22    if (!statElements.length) return;
23
24    const observer = new IntersectionObserver(handleIntersection, {
25      threshold: CONFIG.threshold,
26      rootMargin: CONFIG.rootMargin
27    });
28
29    statElements.forEach(el => observer.observe(el));
30  }
31
32  function handleIntersection(entries, observer) {
33    entries.forEach(entry => {
34      if (entry.isIntersecting && !entry.target.dataset.animated) {
35        animateCounter(entry.target);
36        entry.target.dataset.animated = 'true';
37        // Unobserve after animation to free memory
38        observer.unobserve(entry.target);
39      }
40    });
41  }
42
43  function animateCounter(element) {
44    const targetStr = element.dataset.target || '0';
45    const suffix = element.dataset.suffix || '';
46
47    // Parse number (remove commas)
48    const cleanNum = targetStr.replace(/,/g, '');
49    const target = parseFloat(cleanNum);
50
51    if (isNaN(target) || target === 0) {
52      element.textContent = targetStr + suffix;
53      return;
54    }
55
56    const startTime = performance.now();
57    const hasDecimal = targetStr.includes('.');
58
59    function update(currentTime) {
60      const elapsed = currentTime - startTime;
61      const progress = Math.min(elapsed / CONFIG.duration, 1);
62
63      // Cubic ease-out: smooth deceleration
64      const easeOut = 1 - Math.pow(1 - progress, 3);
65      const current = target * easeOut;
66
67      // Format based on original number type
68      const formatted = hasDecimal
69        ? current.toFixed(1)
70        : Math.floor(current).toLocaleString();
71
72      element.textContent = formatted + suffix;
73
74      if (progress < 1) {
75        requestAnimationFrame(update);
76      } else {
77        // Set final value with original formatting
78        element.textContent = targetStr + suffix;
79      }
80    }
81
82    requestAnimationFrame(update);
83  }
84
85  // Initialize on DOM ready
86  if (document.readyState === 'loading') {
87    document.addEventListener('DOMContentLoaded', initStatsCountUp);
88  } else {
89    initStatsCountUp();
90  }
91})();

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.