| 1 |
(function () { |
| 2 |
/* ── Utilities ─────────────────────────────────────────────────── */ |
| 3 |
function extractNumber(str) { |
| 4 |
var m = (str || '').match(/[\d,]+\.?\d*/); |
| 5 |
return m ? parseFloat(m[0].replace(/,/g,'')) : null; |
| 6 |
} |
| 7 |
|
| 8 |
function formatNumber(n, original) { |
| 9 |
/* Preserve suffix/prefix from original string */ |
| 10 |
var hasDecimal = original.indexOf('.') !== -1; |
| 11 |
var prefix = original.replace(/[\d,. ]+.*/, ''); |
| 12 |
var suffix = original.replace(/.*[\d.]+/, ''); |
| 13 |
if (hasDecimal) { |
| 14 |
var decimals = (original.split('.')[1] || '').replace(/\D+/,'').length; |
| 15 |
return prefix + n.toFixed(decimals) + suffix; |
| 16 |
} |
| 17 |
return prefix + Math.round(n).toLocaleString() + suffix; |
| 18 |
} |
| 19 |
|
| 20 |
function animateCount(el, originalText, duration) { |
| 21 |
duration = duration || 1200; |
| 22 |
var targetNum = extractNumber(originalText); |
| 23 |
if (targetNum === null) return; |
| 24 |
var start = null; |
| 25 |
function step(ts) { |
| 26 |
if (!start) start = ts; |
| 27 |
var progress = Math.min((ts - start) / duration, 1); |
| 28 |
var eased = 1 - Math.pow(1 - progress, 3); /* ease-out cubic */ |
| 29 |
el.textContent = formatNumber(eased * targetNum, originalText); |
| 30 |
if (progress < 1) requestAnimationFrame(step); |
| 31 |
else el.textContent = originalText; |
| 32 |
} |
| 33 |
requestAnimationFrame(step); |
| 34 |
} |
| 35 |
|
| 36 |
/* ── Init entrance + count-up ──────────────────────────────────── */ |
| 37 |
function initCaseStudy(wrapper) { |
| 38 |
/* Staggered entrance for main sections */ |
| 39 |
var animEls = wrapper.querySelectorAll('.bkbg-cs-client-bar, .bkbg-cs-card, .bkbg-cs-metric, .bkbg-cs-quote, .bkbg-cs-cta-row'); |
| 40 |
animEls.forEach(function (el, i) { |
| 41 |
el.style.opacity = '0'; |
| 42 |
el.style.transform = 'translateY(20px)'; |
| 43 |
el.style.transition = 'opacity 0.55s ease, transform 0.55s ease'; |
| 44 |
el.style.transitionDelay = (i * 80) + 'ms'; |
| 45 |
}); |
| 46 |
|
| 47 |
var io = new IntersectionObserver(function (entries, obs) { |
| 48 |
entries.forEach(function (entry) { |
| 49 |
if (!entry.isIntersecting) return; |
| 50 |
obs.unobserve(entry.target); |
| 51 |
|
| 52 |
var el = entry.target; |
| 53 |
el.style.opacity = '1'; |
| 54 |
el.style.transform = 'none'; |
| 55 |
|
| 56 |
/* Count-up for metric values */ |
| 57 |
if (el.classList.contains('bkbg-cs-metric')) { |
| 58 |
var valueEl = el.querySelector('.bkbg-cs-metric-value'); |
| 59 |
if (valueEl) { |
| 60 |
var original = valueEl.textContent; |
| 61 |
animateCount(valueEl, original, 1400); |
| 62 |
} |
| 63 |
} |
| 64 |
}); |
| 65 |
}, { threshold: 0.25 }); |
| 66 |
|
| 67 |
animEls.forEach(function (el) { io.observe(el); }); |
| 68 |
} |
| 69 |
|
| 70 |
function init() { |
| 71 |
var wrappers = document.querySelectorAll('.bkbg-cs-wrapper'); |
| 72 |
wrappers.forEach(initCaseStudy); |
| 73 |
} |
| 74 |
|
| 75 |
if (document.readyState !== 'loading') { init(); } |
| 76 |
else { document.addEventListener('DOMContentLoaded', init); } |
| 77 |
})(); |
| 78 |
|