animate-counters.js 4.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141
  1. // General counter animation system
  2. function animateCounter(element, options = {}) {
  3. const defaultOptions = {
  4. duration: 2000,
  5. easing: 'easeOutQuart',
  6. delay: 0,
  7. separator: true
  8. };
  9. const config = { ...defaultOptions, ...options };
  10. // Extract number and suffix from element content
  11. const originalText = element.textContent || element.getAttribute('data-target');
  12. const target = parseInt(originalText.replace(/[^\d]/g, ''));
  13. const prefix = originalText.split(/\d/)[0] || '';
  14. const suffix = originalText.replace(/[\d,]/g, '').replace(prefix, '') || '';
  15. if (isNaN(target)) return;
  16. // Easing functions
  17. const easingFunctions = {
  18. linear: t => t,
  19. easeOutQuart: t => 1 - Math.pow(1 - t, 4),
  20. easeOutCubic: t => 1 - Math.pow(1 - t, 3),
  21. easeInOutQuart: t => t < 0.5 ? 8 * t * t * t * t : 1 - 8 * (--t) * t * t * t
  22. };
  23. const easingFunction = easingFunctions[config.easing] || easingFunctions.easeOutQuart;
  24. // Start animation after delay
  25. setTimeout(() => {
  26. const startTime = performance.now();
  27. function updateCounter(currentTime) {
  28. const elapsed = currentTime - startTime;
  29. const progress = Math.min(elapsed / config.duration, 1);
  30. const easedProgress = easingFunction(progress);
  31. const current = Math.floor(target * easedProgress);
  32. // Format number with separator if enabled
  33. const formattedNumber = config.separator ? current.toLocaleString() : current.toString();
  34. element.textContent = prefix + formattedNumber + suffix;
  35. if (progress < 1) {
  36. requestAnimationFrame(updateCounter);
  37. } else {
  38. const finalNumber = config.separator ? target.toLocaleString() : target.toString();
  39. element.textContent = prefix + finalNumber + suffix;
  40. // Dispatch custom event when animation completes
  41. element.dispatchEvent(new CustomEvent('counterComplete', {
  42. detail: { target, element }
  43. }));
  44. }
  45. }
  46. // Set initial value
  47. element.textContent = prefix + '0' + suffix;
  48. requestAnimationFrame(updateCounter);
  49. }, config.delay);
  50. }
  51. // Auto-detect and animate counters when they become visible
  52. function createCounterObserver() {
  53. // Select all elements with counter classes or data attributes
  54. const counters = document.querySelectorAll(`
  55. [data-counter],
  56. [data-count],
  57. .counter,
  58. .count,
  59. .number[data-target],
  60. .animate-number,
  61. .count-up
  62. `);
  63. if (counters.length === 0) return;
  64. const observer = new IntersectionObserver((entries) => {
  65. entries.forEach(entry => {
  66. if (entry.isIntersecting) {
  67. const element = entry.target;
  68. // Get options from data attributes
  69. const options = {
  70. duration: parseInt(element.dataset.duration) || 2000,
  71. easing: element.dataset.easing || 'easeOutQuart',
  72. delay: parseInt(element.dataset.delay) || 0,
  73. separator: element.dataset.separator !== 'false'
  74. };
  75. // Animate the counter
  76. animateCounter(element, options);
  77. // Stop observing this element
  78. observer.unobserve(element);
  79. }
  80. });
  81. }, {
  82. threshold: parseFloat(document.documentElement.dataset.counterThreshold) || 0.3,
  83. rootMargin: document.documentElement.dataset.counterMargin || '0px 0px -50px 0px'
  84. });
  85. // Observe all counter elements
  86. counters.forEach(counter => observer.observe(counter));
  87. }
  88. // Manual function to animate specific selectors
  89. function animateCounters(selector = '[data-counter], .counter, .count', options = {}) {
  90. const elements = document.querySelectorAll(selector);
  91. elements.forEach((element, index) => {
  92. const elementOptions = {
  93. ...options,
  94. delay: (options.delay || 0) + (options.stagger || 0) * index
  95. };
  96. animateCounter(element, elementOptions);
  97. });
  98. }
  99. // Initialize when DOM is ready
  100. function initCounterSystem() {
  101. // Auto-detect mode (default)
  102. if (document.documentElement.dataset.counterMode !== 'manual') {
  103. createCounterObserver();
  104. }
  105. // Expose global functions
  106. window.animateCounter = animateCounter;
  107. window.animateCounters = animateCounters;
  108. }
  109. // Multiple initialization methods
  110. if (document.readyState === 'loading') {
  111. document.addEventListener('DOMContentLoaded', initCounterSystem);
  112. } else {
  113. initCounterSystem();
  114. }
  115. // Also expose for manual calling
  116. window.initCounterSystem = initCounterSystem;