Rotate Assemble

transform

Parts fly in from a scattered start, each turning about its own centre as it slots into place.

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-rotate-assemble" 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 rotateAssemble(svg) {
  const _reducedMotion = window.matchMedia('(prefers-reduced-motion: reduce)').matches
  const _duration = _reducedMotion ? 0.01 : 0.9
  const _stagger = _reducedMotion ? 0 : 0.06
  const _delay = _reducedMotion ? 0 : 0

  const parts = paths(svg)
  // A fixed scatter rather than Math.random, so every replay flies in the same way.
  const SCATTER = [
    [-38, -30, -140],
    [34, -26, 120],
    [-30, 34, 160],
    [36, 30, -110],
    [0, -44, 90],
  ]
  // Without fill-box an SVG origin resolves against the whole canvas, and each
  // part would turn about the SVG's top-left corner instead of itself.
  parts.forEach((part) => {
    part.style.transformBox = 'fill-box'
    part.style.transformOrigin = 'center'
  })
  const easing = 'linear(0.0000,0.0987,0.1899,0.2740,0.3513,0.4224,0.4874,0.5467,0.6008,0.6499,0.6943,0.7343,0.7703,0.8025,0.8311,0.8566,0.8790,0.8987,0.9159,0.9308,0.9437,0.9546,0.9639,0.9717,0.9781,0.9834,0.9877,0.9910,0.9937,0.9957,0.9972,0.9982,0.9990,0.9994,0.9997,0.9999,1.0000,1.0000,1.0000,1.0000)'
  const animations = parts.map((part, i) => {
    const [x, y, rotate] = SCATTER[i % SCATTER.length]
    return part.animate(
      [
        { transform: `translate(${x}px, ${y}px) rotate(${rotate}deg)`, opacity: 0 },
        { transform: 'translate(0px, 0px) rotate(0deg)', opacity: 1 },
      ],
      { duration: _duration * 1000, delay: _delay * 1000 + i * _stagger * 1000, easing, fill: 'both' },
    )
  })

  return () => {
    animations.forEach((a) => a.cancel())
    parts.forEach((part) => {
      part.style.transformBox = ''
      part.style.transformOrigin = ''
    })
  }
}

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


rotateAssemble(document.querySelector('.k-rotate-assemble'))