Decompose 2D Motion Into X and Y Springs
MUST: Decompose 2D motion into two independent springs — one on `x`, one on `y`, each seeded with its OWN release velocity. A single spring on the 2D distance (`Math.hypot(x, y)` → 0) can hold only one velocity, so both axes arrive together, the card slides home along a rigid radial line, and the tangential half of the throw is discarded.
A gesture that moves in two dimensions needs two springs, not one spring on the distance
animate(el, { x: 0 }, { type: "spring", velocity: vx });
animate(el, { y: 0 }, { type: "spring", velocity: vy }); // not one spring on hypot(x, y)Bad
Good
Why it matters
This is the answer to a question the corpus has not asked before: not how to configure a spring, but HOW MANY springs a gesture needs. animations-spring-physics argues for springs over tweens because they carry velocity through an interruption; animations-emil-subtle-bounce bounds the bounce. Neither says anything about dimensionality — and a 2D drag is where the naive implementation quietly breaks.
The tempting shortcut is to spring the scalar distance home: `Math.hypot(x, y)` → 0, one spring, one velocity. But one spring can only hold one velocity, so both axes are forced to share it and to arrive at the same instant. Throw a card diagonally with more speed on X than Y and the faster axis drags the slower one along with it; whatever direction you actually flicked, the card slides home along a rigid radial line, and the tangential half of your gesture is simply discarded.
Two springs — one on x, one on y, each seeded with its OWN release velocity — let each axis settle on its own clock. The emergent 2D path curves: it continues the way you threw the thing and hooks back, which is what "it followed my finger" actually means. This is Apple's point from Designing Fluid Interfaces, and it also explains why Motion's `x`/`y` values are separate animatable channels rather than a single vector.