Counter Roll

kinetic

Digits count up from zero to the target number. Type a number as the sample text to try it.

I Love Bangladesh
17/120
Align
Ease
Duration
s
0.13
Stagger
s
00.2
Delay
s
02
export function counterRoll(el) {
  const _reducedMotion = window.matchMedia('(prefers-reduced-motion: reduce)').matches
  const _duration = _reducedMotion ? 0.01 : 1.2
  const _delay = _reducedMotion ? 0 : 0

  const raw = el.textContent ?? ''
  const match = raw.match(/[\d,]*\d/)
  if (!match) {
    return () => {}
  }
  const idx = match.index ?? 0
  const prefix = raw.slice(0, idx)
  const suffix = raw.slice(idx + match[0].length)
  const target = Number(match[0].replace(/,/g, ''))
  const points = [0, 0.075, 0.1461, 0.2135, 0.2772, 0.3374, 0.3942, 0.4476, 0.4978, 0.5448, 0.5889, 0.6299, 0.6682, 0.7037, 0.7366, 0.767, 0.7949, 0.8205, 0.8439, 0.8651, 0.8844, 0.9017, 0.9172, 0.9309, 0.9431, 0.9537, 0.963, 0.9709, 0.9776, 0.9831, 0.9877, 0.9914, 0.9942, 0.9964, 0.9979, 0.9989, 0.9995, 0.9999, 1, 1]
  const durationMs = _duration * 1000
  const delayMs = _delay * 1000
  let raf = 0
  let start = null
  let cancelled = false

  const tick = (now) => {
    if (cancelled) return
    if (start === null) start = now
    const elapsed = now - start - delayMs
    if (elapsed < 0) {
      raf = requestAnimationFrame(tick)
      return
    }
    const t = Math.min(1, elapsed / durationMs)
    const value = target * easeAt(points, t)
    el.textContent = prefix + Math.round(value).toLocaleString() + suffix
    if (t < 1) {
      raf = requestAnimationFrame(tick)
    } else {
    }
  }
  raf = requestAnimationFrame(tick)

  return () => {
    cancelled = true
    cancelAnimationFrame(raf)
    el.textContent = raw
  }
}

function easeAt(points, t) {
  const clamped = t < 0 ? 0 : t > 1 ? 1 : t
  const scaled = clamped * (points.length - 1)
  const i = Math.floor(scaled)
  const frac = scaled - i
  const a = points[i]
  const b = points[Math.min(i + 1, points.length - 1)]
  return a + (b - a) * frac
}