Random Order Fade

entrance

Characters fade in one by one in a random order.

I Love Bangladesh

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

  const split = splitChars(el)
  const easing = 'linear(0.0000,0.0506,0.0999,0.1479,0.1946,0.2400,0.2840,0.3268,0.3682,0.4083,0.4471,0.4845,0.5207,0.5556,0.5891,0.6213,0.6522,0.6818,0.7101,0.7370,0.7627,0.7870,0.8100,0.8317,0.8521,0.8711,0.8889,0.9053,0.9204,0.9343,0.9467,0.9579,0.9678,0.9763,0.9836,0.9895,0.9941,0.9974,0.9993,1.0000)'
  const order = split.chars.map((_, i) => i)
  for (let i = order.length - 1; i > 0; i--) {
    const j = Math.floor(Math.random() * (i + 1))
    ;[order[i], order[j]] = [order[j], order[i]]
  }
  const anims = split.chars.map((c, i) =>
    c.animate([{ opacity: 0 }, { opacity: 1 }], {
      duration: _duration * 1000,
      delay: (_delay + order[i] * _stagger) * 1000,
      easing,
      fill: 'backwards',
    }),
  )
  return () => {
    anims.forEach((a) => a.cancel())
    split.revert()
  }
}

function splitChars(el) {
  const text = el.textContent ?? ''

  if (el.children.length > 0) {
    return { chars: [el], words: [el], revert: () => {} }
  }

  const escape = (s) => s.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;')

  const segmenter =
    typeof Intl !== 'undefined' && 'Segmenter' in Intl
      ? new Intl.Segmenter(undefined, { granularity: 'grapheme' })
      : null
  const toChars = (s) => (segmenter ? Array.from(segmenter.segment(s), (seg) => seg.segment) : Array.from(s))

  const tokens = text.match(/\S+|\s+/g) ?? []
  el.setAttribute('aria-label', text)
  el.innerHTML = tokens
    .map((token) => {
      if (/^\s+$/.test(token)) return token
      const chars = toChars(token)
        .map((c) => `<span class="k-char">${escape(c)}</span>`)
        .join('')
      return `<span class="k-word" aria-hidden="true">${chars}</span>`
    })
    .join('')

  return {
    chars: [...el.querySelectorAll('.k-char')],
    words: [...el.querySelectorAll('.k-word')],
    revert: () => {
      el.textContent = text
      el.removeAttribute('aria-label')
    },
  }
}

/* CSS */
.k-word {
  display: inline-block;
  white-space: nowrap;
}

.k-char {
  display: inline-block;
}