Scramble

kinetic

Characters scramble through random glyphs before locking into the final text.

I Love Bangladesh

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

  const CHARS = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ'
  const randChar = () => CHARS[Math.floor(Math.random() * CHARS.length)]
  const text = el.textContent ?? ''
  const chars = [...text]
  const points = [0, 1]
  const revealDelay = 0.3
  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 progress = easeAt(points, t)
    const revealT = Math.max(0, (progress - revealDelay) / (1 - revealDelay))
    const revealCount = Math.floor(revealT * chars.length)
    el.textContent = chars.map((c, i) => (i < revealCount || c === ' ' ? c : randChar())).join('')
    if (t < 1) {
      raf = requestAnimationFrame(tick)
    } else {
      el.textContent = text
    }
  }
  raf = requestAnimationFrame(tick)

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

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
}