(function(){ function animateAmount(el){ var target = parseFloat(el.getAttribute('data-amt')); if (isNaN(target)) return; var txt = el.textContent.replace(/[^\d\.\-]/g,''); var start = parseFloat(txt); if (isNaN(start)) start = target; if (Math.abs(target - start) < 0.01) return; var dur = 900, t0 = performance.now(); function step(t){ var k = Math.min(1, (t - t0)/dur); // ease-out var v = start + (target - start) * (1 - Math.pow(1-k,3)); el.textContent = (el.textContent.trim().charAt(0) === '£' ? '£' : '') + v.toLocaleString(undefined,{minimumFractionDigits:2, maximumFractionDigits:2}); if (k < 1) requestAnimationFrame(step); } requestAnimationFrame(step); } function run(){ document.querySelectorAll('.js-amt').forEach(animateAmount); } // initial + after ajax/partial loads document.addEventListener('DOMContentLoaded', run); // re-run periodically in case the page is long-lived and shortcodes refresh via cache ttl setInterval(run, 5000); })(); (function(){ // Easing (easeOutCubic) function ease(t){ return 1 - Math.pow(1 - t, 3); } // Format number with 2 dp + locale thousands function fmt(n){ return n.toLocaleString(undefined, {minimumFractionDigits:2, maximumFractionDigits:2}); } function parseCurrent(el){ // current text may include currency; strip to get the numeric part var numTxt = (el.textContent || '').replace(/[^\d\.\-]/g,''); var n = parseFloat(numTxt); return isNaN(n) ? null : n; } function getTarget(el){ var t = parseFloat(el.getAttribute('data-amt')); return isNaN(t) ? null : t; } function getCurrencyPrefix(el){ // if first non-space char is a currency symbol, keep it var txt = el.textContent.trim(); var first = txt.charAt(0); return /[£$€¥]/.test(first) ? first : ''; } function animateAmount(el, target, opts){ var prefix = getCurrencyPrefix(el); var start = parseCurrent(el); if (start === null) start = target; if (Math.abs(target - start) < 0.005) { // tiny diff → snap el.textContent = (prefix?prefix:'') + fmt(target); el._amt = target; return; } var dur = (opts && opts.duration) || 900; var t0 = performance.now(); function step(now){ var k = Math.min(1, (now - t0)/dur); var v = start + (target - start) * ease(k); el.textContent = (prefix?prefix:'') + fmt(v); if (k < 1) { requestAnimationFrame(step); } else { el.textContent = (prefix?prefix:'') + fmt(target); el._amt = target; } } requestAnimationFrame(step); } function initOne(el){ var target = getTarget(el); if (target === null) return; // first paint: ensure display matches target (no jump next time) if (typeof el._amt !== 'number') { var prefix = getCurrencyPrefix(el); el.textContent = (prefix?prefix:'') + fmt(target); el._amt = target; } } function tick(){ document.querySelectorAll('.js-amt').forEach(function(el){ var target = getTarget(el); if (target === null) return; if (typeof el._amt !== 'number') { initOne(el); return; } if (Math.abs(target - el._amt) >= 0.01) { animateAmount(el, target, {duration: 900}); } }); } // Run document.addEventListener('DOMContentLoaded', function(){ document.querySelectorAll('.js-amt').forEach(initOne); tick(); // Periodically re-check (in case markup is refreshed or TTL pulls new numbers) setInterval(tick, 4000); }); // If you ever update data-amt dynamically, this will catch it instantly. var mo = new MutationObserver(function(muts){ var changed = false; muts.forEach(function(m){ if (m.type === 'attributes' && m.attributeName === 'data-amt') changed = true; }); if (changed) tick(); }); document.addEventListener('DOMContentLoaded', function(){ document.querySelectorAll('.js-amt').forEach(function(el){ mo.observe(el, {attributes:true, attributeFilter:['data-amt']}); }); }); })();