Characters drop from above and bounce into place.
export function dropBounce(el) {
const _reducedMotion = window.matchMedia('(prefers-reduced-motion: reduce)').matches
const _duration = _reducedMotion ? 0.01 : 0.8
const _stagger = _reducedMotion ? 0 : 0.04
const _delay = _reducedMotion ? 0 : 0
const split = splitChars(el)
const easing = 'linear(0.0000,0.0022,0.0087,0.0196,0.0348,0.0543,0.0782,0.1065,0.1390,0.1760,0.2173,0.2629,0.3128,0.3672,0.4258,0.4888,0.5562,0.6279,0.7039,0.7843,0.8690,0.9581,0.9752,0.9332,0.8954,0.8621,0.8330,0.8083,0.7880,0.7720,0.7603,0.7530,0.7501,0.7515,0.7572,0.7673,0.7817,0.8004,0.8235,0.8510,0.8828,0.9189,0.9594,0.9979,0.9772,0.9608,0.9487,0.9410,0.9377,0.9386,0.9440,0.9537,0.9677,0.9860,0.9960,0.9881,0.9846,0.9854,0.9905,1.0000)'
const anims = split.chars.map((c, i) =>
c.animate([{ transform: 'translateY(-80px)' }, { transform: 'none' }], {
duration: _duration * 1000,
delay: (_delay + 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, '&').replace(/</g, '<').replace(/>/g, '>')
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;
}