Keep a transition's pace through new steps and pans (#309)

A wheel step in the middle of a transition restarted the ease-out
curve at full speed, so a run of quick steps kicked at each one.
A pan called finish() first and made the zoom jump to its target.
Both made the zoom and the gliding lists feel jerky.

- A new step starts from the speed the view already has and then
  eases out. Quick steps in one direction move in one stroke. A step
  the other way still starts from rest.
- A pan takes the transition along. The zoom goes on while dragging
  and lands where it was heading, moved by the pan. This covers all
  eight pan places: scene and container, graph, plugin board,
  texture and pixel art, timeline, pattern, song and sound.
- finish() is gone, it has no callers left. Dragging the scrollbar,
  a jump to a row, another document or a new view size still end
  the transition as before.

Found: on a second step the old curve jumped to 2.47 times the
speed the view had just before.

Tests:
- npm run check on stein2: 299 files, 4598 tests (5 new)
- e2e 46 on stein2: ALL PASSED, 24 checks (1 new). A pan by 40/30
  px during a 1000 ms zoom step leaves it moving (2.33 on the way
  from 2 to 3), it lands on 3 with the world point moved by the pan.
- Counter-checks: with the pace reset to 3 the new unit test fails
  with 2.47 instead of at most 1.1. Without carry the pan test fails.
- melektaus-atx went offline mid-run, so both ran on stein2.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
adminandClaude Opus 5.5 committed 2026-10-11 23:08:43 +02:00
1 parent be41b1f0d7
commit 597367c804
12 files changed
+307 -72

No files matched your search

+11 -5
View File
@@ -593,8 +593,9 @@ export class GraphCanvas extends Widget {
// The middle button pans from anywhere, a node under the pointer included – it is the one
// gesture that never means "grab this". Nodes let it through for exactly that reason.
const startPan = () => {
this.tween.finish();
this.panning = { startX: e.global.x, startY: e.global.y, panX: this.pan.x, panY: this.pan.y };
this.tween.target(() => {
this.panning = { startX: e.global.x, startY: e.global.y, panX: this.pan.x, panY: this.pan.y };
});
};
if (e.button === 1) {
startPan();
@@ -782,9 +783,14 @@ export class GraphCanvas extends Widget {
this.trackIncoming(dragged, e);
return;
}
if (this.panning) {
this.pan.x = this.panning.panX + (e.global.x - this.panning.startX);
this.pan.y = this.panning.panY + (e.global.y - this.panning.startY);
const { panning } = this;
if (panning) {
this.tween.carry(() => {
this.pan = {
x: panning.panX + (e.global.x - panning.startX),
y: panning.panY + (e.global.y - panning.startY),
};
});
this.applyView();
return;
}
+6 -3
View File
@@ -76,13 +76,16 @@ export function panAndZoom(
): void {
let panning: { start: Point; pan: Point } | null = null;
surface.on('pointerdown', (e: FederatedPointerEvent) => {
tween.finish();
panning = { start: { x: e.global.x, y: e.global.y }, pan: { ...get().pan } };
tween.target(() => {
panning = { start: { x: e.global.x, y: e.global.y }, pan: { ...get().pan } };
});
});
surface.on('globalpointermove', (e: FederatedPointerEvent) => {
if (!panning) return;
const { start, pan } = panning;
set({ ...get(), pan: { x: pan.x + e.global.x - start.x, y: pan.y + e.global.y - start.y } }, 'pan');
tween.carry(() =>
set({ ...get(), pan: { x: pan.x + e.global.x - start.x, y: pan.y + e.global.y - start.y } }, 'pan'),
);
});
surface.on('pointerup', () => (panning = null));
surface.on('pointerupoutside', () => (panning = null));