Line Mask Up

entrance

Each line slides up from behind a clipped mask.

I Love Bangladesh

17/120
Align
Ease
Duration
s
0.13
Stagger
s
00.2
Delay
s
02
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;
}