Files
enricobuehler 50ef253e43 feat(console): the screen transition becomes an interruptible spring (vectors v2)
Pressing B while a screen was still arriving did nothing at all. The transition
was a 0.26 s ease-out-cubic and every event that landed inside it was thrown
away, so holding B to back out of a deep stack stuttered at every level: press,
wait, press again. That wall is the one place the console felt less alive than
the Apple shell it is modelled on.

`Motion` becomes one sprung scalar. The paint recipes read `spring.pos` exactly
where they read `ease_out_cubic(raw)` - same 36 dp slide, same 0.985/0.96
scales, same 0.4 reveal alpha, so the choreography is untouched and only the
time-course changed. Those four numbers stop being literals buried in the paint
sites and become named constants, because the vectors file claims to pin them
for all three clients and a literal is a thing no test can reach.

What the spring buys is the retarget. Back mid-push flips THAT SAME spring from
1.0 to 0.0: velocity carries, so the entering screen decelerates, turns, and
goes back down as one continuous motion, and `finish_nav` takes it off the stack
when the spring lands. A tween cannot do that - its progress is a function of
elapsed time, so reversing means either a snap or a second animation played
behind the first. The test asserts the continuity directly (no sample jumps by
more than a frame of the travel already underway), because "cancel and play a
pop" would pass a weaker test and still look wrong.

Back is now always heard; everything else stays queued-dropped until the spring
passes 0.85, so the double-tap protection survives. The threshold is on
POSITION, not elapsed time - with a spring those are different questions, and
the one that matters for input is whether the screen under the cursor is the one
being aimed at. Mid-pop, A is still refused (activating a half-dismissed screen
is a mis-tap, not intent) while Back starts the next pop immediately, which is
the stutter this removes. At the root the transition declines the press, because
there is no parent to fall back to and B there means quit.

Parity: `console-vectors.json` gains a `motion_spring` block and goes to version
2. Springs are integrator-dependent, so v2 pins PARAMETERS where v1 pinned
sampled positions - two runtimes that both honour response/damping agree to the
eye and disagree in the third decimal, and sampling would pin the disagreement
instead of the feel. The v1 `motion` block stays in place, marked `$deprecated`
and still correct, because the Android client's ConsoleVectorsTest reads it;
this crate's test switches to the new block and asserts the note is still there,
so deleting v1 here would fail loudly rather than redden Android silently.
(Note for the follow-ups: the Apple client mirrors the v1 constants in
GamepadShell but does NOT test them - its ConsoleVectorsTests covers the palette
table only. The plan assumed otherwise.)

`Progress` is deleted. The transition was its last caller, and this crate lints
dead code as an error - keeping a timer nothing times behind an allow would be
the wrong kind of tidy.

Verified in the pf-gtkflow container: fmt, clippy --all-targets -D warnings,
plain build, 97 tests green.
2026-08-16 15:19:25 +02:00
..