Each line slides up from behind a clipped mask.
I Love Bangladesh
export function lineMaskUp(el) {
const _reducedMotion = window.matchMedia('(prefers-reduced-motion: reduce)').matches
const _duration = _reducedMotion ? 0.01 : 0.7
const _stagger = _reducedMotion ? 0 : 0.12
const _delay = _reducedMotion ? 0 : 0
const text = el.textContent ?? ''
el.setAttribute('aria-label', text)
// Range.getClientRects() returns one rect per rendered line — the browser
// has already done line-breaking for us, no text-splitting library needed.
const range = document.createRange()
range.selectNodeContents(el)
const lineRects = Array.from(range.getClientRects())
const elTop = el.getBoundingClientRect().top
el.textContent = ''
const lines = lineRects.map((r) => {
const mask = document.createElement('span')
mask.className = 'k-line-mask'
mask.style.height = `${r.height}px`
const line = document.createElement('span')
line.className = 'k-line'
line.textContent = text
// Each clone holds the FULL text at the SAME width, so it wraps
// identically; shifting it up by this line's own offset and cropping to
// one line's height reveals just that line's slice.
line.style.marginTop = `${elTop - r.top}px`
mask.append(line)
el.append(mask)
return line
})
const easing = 'linear(0.0000,0.1218,0.2314,0.3298,0.4179,0.4964,0.5662,0.6281,0.6827,0.7307,0.7727,0.8092,0.8410,0.8683,0.8918,0.9117,0.9287,0.9429,0.9547,0.9645,0.9726,0.9791,0.9843,0.9884,0.9916,0.9940,0.9959,0.9972,0.9982,0.9989,0.9993,0.9996,0.9998,0.9999,1.0000,1.0000,1.0000,1.0000,1.0000,1.0000)'
const anims = lines.map((line, i) =>
line.animate([{ transform: 'translateY(100%)' }, { transform: 'none' }], {
duration: _duration * 1000,
delay: (_delay + i * _stagger) * 1000,
easing,
fill: 'backwards',
}),
)
return () => {
anims.forEach((a) => a.cancel())
el.textContent = text
el.removeAttribute('aria-label')
}
}
/* CSS */
.k-line-mask {
display: block;
overflow: hidden;
}
.k-line {
display: block;
}