fix(editor): improve scroll animation interpolation (#11562)

This commit is contained in:
David Luzar
2026-06-26 09:56:12 +02:00
committed by GitHub
parent e4c70cb6c6
commit c070c8ffa6
+44 -11
View File
@@ -103,11 +103,53 @@ const getTargetViewport = (
return { scrollX, scrollY, zoom: state.zoom }; return { scrollX, scrollY, zoom: state.zoom };
}; };
/**
* Interpolates the viewport from `from` to `target` at the (already-eased)
* blend amount `factor` (0 = `from`, 1 = `target`).
*
* Zoom is interpolated geometrically (so it feels uniform), and rather than
* tweening scrollX/scrollY directly we tween the *focal point* — the scene
* point under the viewport center — and derive scroll from it. Mixing a linear
* scroll with a geometric zoom makes the focal point swoop sideways
* mid-animation (most visible when zooming out); gliding the focal point keeps
* it steady. `width/2/zoom - scroll` is the inverse of `centerScrollOn` without
* offsets, so factor 0/1 land exactly on `from`/`target`.
*/
export const interpolateViewport = ({
from,
target,
factor,
}: {
from: Pick<AppState, "scrollX" | "scrollY" | "zoom" | "width" | "height">;
target: Viewport;
factor: number;
}): Viewport => {
const zoom = (from.zoom.value *
Math.pow(
target.zoom.value / from.zoom.value,
factor,
)) as NormalizedZoomValue;
const fromCenterX = from.width / 2 / from.zoom.value - from.scrollX;
const fromCenterY = from.height / 2 / from.zoom.value - from.scrollY;
const toCenterX = from.width / 2 / target.zoom.value - target.scrollX;
const toCenterY = from.height / 2 / target.zoom.value - target.scrollY;
const centerX = fromCenterX + (toCenterX - fromCenterX) * factor;
const centerY = fromCenterY + (toCenterY - fromCenterY) * factor;
return {
scrollX: from.width / 2 / zoom - centerX,
scrollY: from.height / 2 / zoom - centerY,
zoom: { value: zoom },
};
};
/** Eases the viewport from its current position to `target` over `duration`, /** Eases the viewport from its current position to `target` over `duration`,
* driving the transition through the shared AnimationController so it doesn't * driving the transition through the shared AnimationController so it doesn't
* slow down other processes. */ * slow down other processes. */
const animateToViewport = ( const animateToViewport = (
from: Pick<AppState, "scrollX" | "scrollY" | "zoom">, from: Pick<AppState, "scrollX" | "scrollY" | "zoom" | "width" | "height">,
target: Viewport, target: Viewport,
duration: number, duration: number,
onFrame: ( onFrame: (
@@ -125,17 +167,8 @@ const animateToViewport = (
const factor = easeOut(clamp(progress, 0, 1)); const factor = easeOut(clamp(progress, 0, 1));
onFrame({ onFrame({
...interpolateViewport({ from, target, factor }),
shouldCacheIgnoreZoom: progress < 1, // ignore zoom caching while animating shouldCacheIgnoreZoom: progress < 1, // ignore zoom caching while animating
scrollX: from.scrollX + (target.scrollX - from.scrollX) * factor,
scrollY: from.scrollY + (target.scrollY - from.scrollY) * factor,
// zoom interpolates geometrically so the transition feels natural
zoom: {
value: (from.zoom.value *
Math.pow(
target.zoom.value / from.zoom.value,
factor,
)) as NormalizedZoomValue,
},
}); });
// returning a falsy value signals the AnimationController to remove the // returning a falsy value signals the AnimationController to remove the