Symptom

Panning the swarm canvas (ClientScreen.kt) glided smoothly, but zooming — via mouse scroll or pinch — rendered as a series of visible steps. Ben, watching a demo render, described it as jerky, and noted it was “smooth when things reset” (i.e. during a fitSwarm call).

Root cause

DisplayNodes held two pieces of canvas transform state, scale and targetOffset, but only targetOffset was spring-animated (animateFloatAsStateanimatedOffsetX/animatedOffsetY, fed into graphicsLayer). scale was written directly from the scroll and pinch gesture handlers and read straight into graphicsLayer(scaleX = scale, ...) with no interpolation, so every gesture delta was an instant jump. A fitSwarm request set scale once and animated only the offset, which is why a fit read as one smooth move while a user-driven zoom read as a stair-step — the “smooth when things reset” clue pointed straight at the asymmetry.

Fix

Prevention

When a composable holds two pieces of state that drive the same visual transform (here, pan and zoom feeding one graphicsLayer), give them matching animation treatment from the start — an interpolated value next to a raw one is an easy asymmetry to introduce and only shows up as a “feel” bug, not a compile or logic error. For Compose spring/animation behavior that’s awkward to unit test in place because the composable is deeply wired to production DI, extract the animation itself into an independent composable and test it directly with runDesktopComposeUiTest’s virtual mainClock (not wall-clock delay) — see AnimatedScaleTest.kt.