From 727db602df3f454d12410b518eecaa8271f11d98 Mon Sep 17 00:00:00 2001 From: elwrcl Date: Fri, 2 Oct 2026 17:25:45 +0300 Subject: [PATCH] fix: keep the point under the pointer while zoom animates --- src/zoom/view.rs | 26 ++++++++++++++++++++++---- 1 file changed, 22 insertions(+), 4 deletions(-) diff --git a/src/zoom/view.rs b/src/zoom/view.rs index b4211e5..9bff957 100644 --- a/src/zoom/view.rs +++ b/src/zoom/view.rs @@ -11,7 +11,8 @@ pub const MIN_RADIUS: f64 = 20.0; pub const MAX_RADIUS: f64 = 2000.0; /// Logical px of touchpad scrolling per zoom step. const TOUCHPAD_PX_PER_STEP: f64 = 15.0; -/// Per second: 1 - e^(-RATE * 0.12) ≈ 0.9, 90 % of the way in 120 ms. +/// Per second: 1 - e^(-RATE * 0.12) ≈ 0.9, 90 % of the way in 120 ms +/// (measured in the visible span, 1 / scale). const RATE: f64 = 19.2; #[derive(Debug, Clone, Copy, PartialEq)] @@ -115,7 +116,9 @@ impl Animated { let k = 1.0 - (-RATE * dt).exp(); let (s, t) = (&mut self.shown, self.target); let lerp = |a: f64, b: f64| a + (b - a) * k; - s.scale = lerp(s.scale, t.scale); + // Easing the visible span (1 / scale) and the origin together keeps + // the frame point under the pointer still the whole way. + s.scale = 1.0 / lerp(1.0 / s.scale, 1.0 / t.scale); s.origin = (lerp(s.origin.0, t.origin.0), lerp(s.origin.1, t.origin.1)); let settled = (s.scale - t.scale).abs() < 1e-3 && (s.origin.0 - t.origin.0).abs() < 0.05 @@ -228,14 +231,29 @@ mod tests { assert!(frames < 120, "never settles"); } assert_eq!(a.shown, a.target); - // About 90 % of the way after 120 ms. + // About 90 % of the way after 120 ms, in the visible span (1 / scale). let mut b = Animated::new(); b.target = View { scale: 11.0, origin: (0.0, 0.0), }; b.step(0.12); - assert!((b.shown.scale - 10.0).abs() < 0.2, "{:?}", b.shown); + let span = 1.0 / b.shown.scale; + let expected = 1.0 + 0.9 * (1.0 / 11.0 - 1.0); + assert!((span - expected).abs() < 0.02, "{:?}", b.shown); + } + + #[test] + fn animation_keeps_the_point_under_the_pointer() { + for p in [(960.0, 540.0), (100.0, 900.0)] { + let mut a = Animated::new(); + a.target = View::IDENTITY.zoom_at(p, 2.0, SIZE); + let anchor = a.target.to_frame(p); + for _ in 0..5 { + a.step(0.02); + assert!(close(a.shown.to_frame(p), anchor), "{p:?}: {:?}", a.shown); + } + } } #[test]