Hold the slides for the whole turn, not until it first pauses

The slides are held at the size a turn is heading for so the picture steps
once, straight into the shape it keeps, instead of being re-letterboxed
through every shape the viewport passes on its way round.

The hold came off once the height had gone six frames unchanged, which
stood in for "the viewport has stopped moving". It does not: a turn
arrives in steps, and the gap between two of them is routinely longer
than six frames -- on a phone and in a loaded tab alike. So the pin came
off mid-turn, the slides took an intermediate shape, and the picture
jumped twice on the way round. That is the stutter the pin exists to
remove, and TURN_HOLD_FRAMES was meant to be "long enough that a viewport
animating round in steps is not mistaken for one that has arrived" -- six
frames is not.

The device does say when a turn is underway, so believe it: while the
announcement stands the pin holds, and the frame count is left to end the
pins nothing announced, where there is no signal to go on and no turn to
ruin. The realign window has to stop concluding mid-turn too, since
closing it releases the pin by the other door; SETTLE_MAX_MS outlasts the
announcement and still guarantees both of them end.

The cost is that an announced turn keeps the old size until the
announcement lapses rather than letting go the moment the viewport looks
still. That buys the one step.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MPZFnNdHbPGDTqQUNiE4ZN
This commit is contained in:
Simon
2026-10-27 22:26:20 +00:00
parent 872786a657
commit 7033288bc4

View File

@@ -1492,15 +1492,29 @@ App.feed = App.feed || {};
if (!turnPin && !turnResolved && isTurn(settleFromShape, shape)) { if (!turnPin && !turnResolved && isTurn(settleFromShape, shape)) {
pinTurn(settleFromShape); pinTurn(settleFromShape);
} }
// A turn the device announced is not over because the viewport paused.
// It arrives in steps, and the gap between two of them is routinely
// longer than the handful of frames that stands in for "stopped" --
// which is how the pin came off mid-turn and let the picture take an
// intermediate shape, the one thing it is here to prevent. So while
// the announcement stands the pin holds, and the frame count is left
// to end the pins nothing announced: a window dragged across the
// square, where there is no signal to go on and no turn to ruin.
const announced = Date.now() - turnSignalAt < TURN_SIGNAL_MS;
// The viewport has stopped moving, so what it is now is better than // The viewport has stopped moving, so what it is now is better than
// what the pin guessed it would be. Letting go here, with the realign // what the pin guessed it would be. Letting go here, with the realign
// below in the same frame, is the one step the picture takes. // below in the same frame, is the one step the picture takes.
if (turnPin && settleStableFrames >= TURN_HOLD_FRAMES) { if (turnPin && !announced && settleStableFrames >= TURN_HOLD_FRAMES) {
releaseTurn(); releaseTurn();
turnResolved = true; turnResolved = true;
} }
realignToActive(); realignToActive();
if (settleStableFrames < SETTLE_STABLE_FRAMES && // The window may not conclude mid-turn either: closing it releases the
// pin on the way out, which would put the picture through the same
// intermediate shape by the other door. An announced turn therefore
// holds the window open too, and SETTLE_MAX_MS -- longer than the
// announcement -- is what still guarantees it ends.
if ((announced || settleStableFrames < SETTLE_STABLE_FRAMES) &&
(Date.now() - settleStartedAt) < SETTLE_MAX_MS) { (Date.now() - settleStartedAt) < SETTLE_MAX_MS) {
resizeSettleRaf = requestAnimationFrame(settleStep); resizeSettleRaf = requestAnimationFrame(settleStep);
return; return;