Each part drifts back and forth on its own slow period, forever, so the drawing breathes without any two parts moving together.
Every animation on the page redraws with the shape you pick.
<!-- 1. Markup -->
<svg viewBox="0 0 120 120" class="k-float-parts" 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 floatParts(svg) {
if (window.matchMedia('(prefers-reduced-motion: reduce)').matches) return () => {}
const parts = paths(svg)
// Periods that share no common beat, so the parts never visibly fall into step.
const RATES = [1, 1.37, 0.83, 1.19, 0.91]
const easing = 'linear(0.0000,0.0016,0.0065,0.0145,0.0257,0.0400,0.0573,0.0774,0.1003,0.1257,0.1536,0.1838,0.2160,0.2500,0.2857,0.3227,0.3609,0.4000,0.4397,0.4799,0.5201,0.5603,0.6000,0.6391,0.6773,0.7143,0.7500,0.7840,0.8162,0.8464,0.8743,0.8997,0.9226,0.9427,0.9600,0.9743,0.9855,0.9935,0.9984,1.0000)'
const animations = parts.map((part, i) =>
part.animate(
[{ transform: 'translate(0px, 0px)' }, { transform: `translate(${i % 2 ? 3 : -3}px, ${i % 3 ? -4 : 4}px)` }],
{ duration: 2.4 * 1000 * RATES[i % RATES.length], iterations: Infinity, direction: 'alternate', easing },
),
)
return () => animations.forEach((a) => a.cancel())
}
function paths(svg) {
return [...svg.querySelectorAll('path, line, polyline, polygon, circle, ellipse, rect')].filter(
(el) => !el.closest('defs, clipPath, mask'),
)
}
floatParts(document.querySelector('.k-float-parts'))