Parts fly in from a scattered start, each turning about its own centre as it slots into place.
Every animation on the page redraws with the shape you pick.
<!-- 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'))