Line Draw

draw

The stroke draws itself on, as if being written. Measures each path and animates its dash offset to zero.

Demo shape

Every animation on the page redraws with the shape you pick.

Align
Ease
Duration
s
0.13
Stagger
s
00.2
Delay
s
02
<!-- 1. Markup -->
<svg viewBox="0 0 120 120" class="k-line-draw" aria-hidden="true">
  <circle cx="60" cy="60" r="34" fill="none" stroke="currentColor" stroke-width="8" vector-effect="non-scaling-stroke" />
  <line x1="60" y1="12" x2="60" y2="38" stroke="currentColor" stroke-width="8" stroke-linecap="round" vector-effect="non-scaling-stroke" />
  <line x1="60" y1="82" x2="60" y2="108" stroke="currentColor" stroke-width="8" stroke-linecap="round" vector-effect="non-scaling-stroke" />
  <circle cx="60" cy="60" r="10" fill="currentColor" />
</svg>

// 2. JS
function lineDraw(svg) {
  const _reducedMotion = window.matchMedia('(prefers-reduced-motion: reduce)').matches
  const _duration = _reducedMotion ? 0.01 : 1
  const _stagger = _reducedMotion ? 0 : 0.15
  const _delay = _reducedMotion ? 0 : 0

  const shapes = paths(svg)
  const easing = 'linear(0.0000,0.0001,0.0005,0.0018,0.0043,0.0084,0.0146,0.0231,0.0345,0.0492,0.0674,0.0898,0.1165,0.1481,0.1850,0.2276,0.2762,0.3313,0.3933,0.4625,0.5375,0.6067,0.6687,0.7238,0.7724,0.8150,0.8519,0.8835,0.9102,0.9326,0.9508,0.9655,0.9769,0.9854,0.9916,0.9957,0.9982,0.9995,0.9999,1.0000)'

  const animations = shapes.map((shape, i) => {
    const length = strokeLength(shape)
    const pad = capPad(shape)
    const hidden = length + pad
    shape.style.strokeDasharray = `${length} ${hidden + pad}`
    const animation = shape.animate([{ strokeDashoffset: hidden }, { strokeDashoffset: 0 }], {
      duration: _duration * 1000,
      delay: _delay * 1000 + i * _stagger * 1000,
      easing,
      fill: 'both',
    })
    // Once drawn, drop the dash: a resize after this point would otherwise
    // leave a screen-pixel dash that no longer covers the path.
    animation.finished.then(() => (shape.style.strokeDasharray = 'none')).catch(() => {})
    return animation
  })

  return () => {
    animations.forEach((a) => a.cancel())
    shapes.forEach((shape) => {
      shape.style.strokeDasharray = ''
    })
  }
}

function paths(svg) {
  return [...svg.querySelectorAll('path, line, polyline, polygon, circle, ellipse, rect')].filter(
    (el) => !el.closest('defs, clipPath, mask'),
  )
}

function strokeLength(el) {
  const m = el.getScreenCTM()
  const nonScaling = getComputedStyle(el).vectorEffect === 'non-scaling-stroke'
  return el.getTotalLength() * (nonScaling && m ? Math.hypot(m.a, m.b) : 1)
}

function capPad(el) {
  const s = getComputedStyle(el)
  return s.strokeLinecap === 'butt' ? 0 : (parseFloat(s.strokeWidth) || 0) / 2 + 1
}


lineDraw(document.querySelector('.k-line-draw'))