Krolyxon
Commit f2bd31a

Branches

Tags

Refs →

remagesrcstateview.rs

435 lines · 13.8 KB · Rust

1//! Zoom/pan/fit/rotate/flip maths and their animation targets.
2//!
3//! Coordinates: the image is centred on the origin of *display space* (image pixels after
4//! rotation and flip). `center` is the display-space point shown at the middle of the viewport.
5//! A screen point is `viewport_centre + scale * (q - center)` for display-space point `q`.
6
7use std::f32::consts::FRAC_PI_2;
8use std::time::{Duration, Instant};
9
10use crate::anim::Tween;
11
12pub const MIN_SCALE: f32 = 0.01;
13pub const MAX_SCALE: f32 = 64.0;
14/// Magnification above which `filter = auto` switches to nearest.
15pub const NEAREST_ABOVE: f32 = 3.0;
16
17#[derive(Clone, Copy, Debug, PartialEq, Eq, clap::ValueEnum)]
18pub enum FitMode {
19 /// Fit, but never enlarge.
20 Down,
21 Fit,
22 Fill,
23 Width,
24 Height,
25 Original,
26}
27
28#[derive(Clone, Copy, Debug, PartialEq, Eq)]
29pub enum Dir {
30 Left,
31 Right,
32 Up,
33 Down,
34}
35
36impl Dir {
37 pub fn vector(self) -> [f32; 2] {
38 match self {
39 Dir::Left => [-1.0, 0.0],
40 Dir::Right => [1.0, 0.0],
41 Dir::Up => [0.0, -1.0],
42 Dir::Down => [0.0, 1.0],
43 }
44 }
45}
46
47#[derive(Clone, Copy, Debug, PartialEq, Eq)]
48pub enum Axis {
49 Horizontal,
50 Vertical,
51}
52
53/// Maps the unit quad onto the screen: `screen = m * uv + t`, in physical pixels.
54#[derive(Clone, Copy, Debug, PartialEq)]
55pub struct Affine {
56 pub m: [f32; 4],
57 pub t: [f32; 2],
58}
59
60#[derive(Debug, Clone)]
61pub struct View {
62 img: [f32; 2],
63 viewport: [f32; 2],
64 fit: Option<FitMode>,
65 // Targets.
66 scale: f32,
67 center: [f32; 2],
68 quarter: i32,
69 flip: [f32; 2],
70 // Displayed values: scale tweens in log space around a fixed screen anchor `d`
71 // (offset from the viewport centre), so `center = anchor - d / scale`.
72 log_s: Tween<f32>,
73 anchor: Tween<[f32; 2]>,
74 d: [f32; 2],
75 rot: Tween<f32>,
76 flip_t: Tween<[f32; 2]>,
77}
78
79fn add(a: [f32; 2], b: [f32; 2]) -> [f32; 2] {
80 [a[0] + b[0], a[1] + b[1]]
81}
82
83fn sub(a: [f32; 2], b: [f32; 2]) -> [f32; 2] {
84 [a[0] - b[0], a[1] - b[1]]
85}
86
87fn mul(a: [f32; 2], s: f32) -> [f32; 2] {
88 [a[0] * s, a[1] * s]
89}
90
91impl View {
92 pub fn new(now: Instant) -> Self {
93 Self {
94 img: [1.0, 1.0],
95 viewport: [1.0, 1.0],
96 fit: Some(FitMode::Down),
97 scale: 1.0,
98 center: [0.0, 0.0],
99 quarter: 0,
100 flip: [1.0, 1.0],
101 log_s: Tween::new(0.0, now),
102 anchor: Tween::new([0.0, 0.0], now),
103 d: [0.0, 0.0],
104 rot: Tween::new(0.0, now),
105 flip_t: Tween::new([1.0, 1.0], now),
106 }
107 }
108
109 /// Show a new image: rotation and flips reset, then `fit` or a fixed `zoom` applies.
110 pub fn reset(&mut self, img: [u32; 2], fit: FitMode, zoom: Option<f32>, now: Instant) {
111 self.img = [img[0].max(1) as f32, img[1].max(1) as f32];
112 self.quarter = 0;
113 self.flip = [1.0, 1.0];
114 self.rot.set(0.0);
115 self.flip_t.set([1.0, 1.0]);
116 match zoom {
117 Some(z) => {
118 self.fit = None;
119 self.scale = z.clamp(MIN_SCALE, MAX_SCALE);
120 }
121 None => {
122 self.fit = Some(fit);
123 self.scale = self.fit_scale(fit);
124 }
125 }
126 self.center = self.clamp_center([0.0, 0.0], self.scale);
127 self.jump();
128 let _ = now;
129 }
130
131 /// The image area changed; refit if a fit mode is active, without animating.
132 pub fn set_viewport(&mut self, w: f32, h: f32) {
133 self.viewport = [w.max(1.0), h.max(1.0)];
134 if let Some(fit) = self.fit {
135 self.scale = self.fit_scale(fit);
136 }
137 self.center = self.clamp_center(self.center, self.scale);
138 self.jump();
139 }
140
141 pub fn viewport(&self) -> [f32; 2] {
142 self.viewport
143 }
144
145 #[cfg(test)]
146 pub fn fit_mode(&self) -> Option<FitMode> {
147 self.fit
148 }
149
150 /// Target scale (1.0 = one image pixel per physical pixel).
151 pub fn scale(&self) -> f32 {
152 self.scale
153 }
154
155 pub fn display_scale(&self, now: Instant) -> f32 {
156 self.log_s.value(now).exp()
157 }
158
159 pub fn is_animating(&self, now: Instant) -> bool {
160 self.log_s.is_active(now)
161 || self.anchor.is_active(now)
162 || self.rot.is_active(now)
163 || self.flip_t.is_active(now)
164 }
165
166 pub fn set_fit(&mut self, fit: FitMode, now: Instant, dur: Duration) {
167 self.fit = Some(fit);
168 let s = self.fit_scale(fit);
169 let c = self.clamp_center(self.center, s);
170 self.animate_to(s, c, [0.0, 0.0], now, dur);
171 }
172
173 /// Zoom by `factor` around `anchor` (viewport pixels), or the centre when `None`.
174 pub fn zoom_by(&mut self, factor: f32, anchor: Option<[f32; 2]>, now: Instant, dur: Duration) {
175 self.zoom_to(self.scale * factor, anchor, now, dur);
176 }
177
178 pub fn zoom_to(&mut self, scale: f32, anchor: Option<[f32; 2]>, now: Instant, dur: Duration) {
179 let d = match anchor {
180 Some(a) => sub(a, mul(self.viewport, 0.5)),
181 None => [0.0, 0.0],
182 };
183 let s1 = scale.clamp(MIN_SCALE, MAX_SCALE);
184 let p = add(self.center, mul(d, 1.0 / self.scale));
185 let c1 = self.clamp_center(sub(p, mul(d, 1.0 / s1)), s1);
186 self.fit = None;
187 self.animate_to(s1, c1, d, now, dur);
188 }
189
190 /// Move the image by a screen-space delta immediately (drag, held keys).
191 pub fn pan_pixels(&mut self, delta: [f32; 2]) {
192 let c1 = self.clamp_center(sub(self.center, mul(delta, 1.0 / self.scale)), self.scale);
193 let moved = sub(c1, self.center);
194 self.center = c1;
195 self.anchor.shift(moved);
196 }
197
198 /// Pan by a fraction of the viewport, animated.
199 pub fn pan_step(&mut self, dir: Dir, frac: f32, now: Instant, dur: Duration) {
200 let v = dir.vector();
201 let step = [
202 v[0] * self.viewport[0] * frac,
203 v[1] * self.viewport[1] * frac,
204 ];
205 let c1 = self.clamp_center(add(self.center, mul(step, 1.0 / self.scale)), self.scale);
206 self.animate_to(self.scale, c1, [0.0, 0.0], now, dur);
207 }
208
209 pub fn pan_edge(&mut self, dir: Dir, now: Instant, dur: Duration) {
210 let far = mul(dir.vector(), 1e9);
211 let c1 = self.clamp_center(far, self.scale);
212 let c1 = match dir {
213 Dir::Left | Dir::Right => [c1[0], self.center[1]],
214 Dir::Up | Dir::Down => [self.center[0], c1[1]],
215 };
216 self.animate_to(self.scale, c1, [0.0, 0.0], now, dur);
217 }
218
219 pub fn recenter(&mut self, now: Instant, dur: Duration) {
220 let c1 = self.clamp_center([0.0, 0.0], self.scale);
221 self.animate_to(self.scale, c1, [0.0, 0.0], now, dur);
222 }
223
224 /// Rotate clockwise on screen by `quarters` quarter turns (negative = counter-clockwise).
225 pub fn rotate(&mut self, quarters: i32, now: Instant, dur: Duration) {
226 // A single mirror reverses the visual sense of the rotation angle.
227 let sign = if self.flip[0] * self.flip[1] < 0.0 {
228 -1
229 } else {
230 1
231 };
232 self.quarter += sign * quarters;
233 let mut c = self.center;
234 for _ in 0..quarters.rem_euclid(4) {
235 c = [-c[1], c[0]];
236 }
237 let s = match self.fit {
238 Some(fit) => self.fit_scale(fit),
239 None => self.scale,
240 };
241 let c = self.clamp_center(c, s);
242 self.rot.retarget(self.quarter as f32 * FRAC_PI_2, now, dur);
243 self.animate_to(s, c, [0.0, 0.0], now, dur);
244 }
245
246 pub fn flip(&mut self, axis: Axis, now: Instant, dur: Duration) {
247 let i = match axis {
248 Axis::Horizontal => 0,
249 Axis::Vertical => 1,
250 };
251 self.flip[i] = -self.flip[i];
252 let mut c = self.center;
253 c[i] = -c[i];
254 self.flip_t.retarget(self.flip, now, dur);
255 self.animate_to(self.scale, c, [0.0, 0.0], now, dur);
256 }
257
258 /// The transform for the frame at `now`.
259 pub fn transform(&self, now: Instant) -> Affine {
260 let s = self.display_scale(now);
261 let c = sub(self.anchor.value(now), mul(self.d, 1.0 / s));
262 let (sin, cos) = self.rot.value(now).sin_cos();
263 let f = self.flip_t.value(now);
264 // M = F · R(θ) · diag(img) · s
265 let m = [
266 f[0] * cos * self.img[0] * s,
267 -f[0] * sin * self.img[1] * s,
268 f[1] * sin * self.img[0] * s,
269 f[1] * cos * self.img[1] * s,
270 ];
271 let vc = mul(self.viewport, 0.5);
272 let t = [
273 vc[0] - s * c[0] - 0.5 * (m[0] + m[1]),
274 vc[1] - s * c[1] - 0.5 * (m[2] + m[3]),
275 ];
276 Affine { m, t }
277 }
278
279 fn display_dims(&self) -> [f32; 2] {
280 if self.quarter.rem_euclid(2) == 1 {
281 [self.img[1], self.img[0]]
282 } else {
283 self.img
284 }
285 }
286
287 fn fit_scale(&self, fit: FitMode) -> f32 {
288 let [dw, dh] = self.display_dims();
289 let [vw, vh] = self.viewport;
290 let fit_s = (vw / dw).min(vh / dh);
291 let s = match fit {
292 FitMode::Down => fit_s.min(1.0),
293 FitMode::Fit => fit_s,
294 FitMode::Fill => (vw / dw).max(vh / dh),
295 FitMode::Width => vw / dw,
296 FitMode::Height => vh / dh,
297 FitMode::Original => 1.0,
298 };
299 s.clamp(MIN_SCALE, MAX_SCALE)
300 }
301
302 /// Keep the image covering the viewport where it is larger, and centred where smaller.
303 fn clamp_center(&self, c: [f32; 2], s: f32) -> [f32; 2] {
304 let dims = self.display_dims();
305 let mut out = c;
306 for i in 0..2 {
307 let extent = (dims[i] - self.viewport[i] / s) * 0.5;
308 out[i] = if extent <= 0.0 {
309 0.0
310 } else {
311 c[i].clamp(-extent, extent)
312 };
313 }
314 out
315 }
316
317 fn animate_to(&mut self, s1: f32, c1: [f32; 2], d: [f32; 2], now: Instant, dur: Duration) {
318 let s0 = self.display_scale(now);
319 let c0 = sub(self.anchor.value(now), mul(self.d, 1.0 / s0));
320 let p0 = add(c0, mul(d, 1.0 / s0));
321 let p1 = add(c1, mul(d, 1.0 / s1));
322 self.log_s.animate(s0.ln(), s1.ln(), now, dur);
323 self.anchor.animate(p0, p1, now, dur);
324 self.d = d;
325 self.scale = s1;
326 self.center = c1;
327 }
328
329 fn jump(&mut self) {
330 self.log_s.set(self.scale.ln());
331 self.anchor.set(self.center);
332 self.d = [0.0, 0.0];
333 self.rot.set(self.quarter as f32 * FRAC_PI_2);
334 self.flip_t.set(self.flip);
335 }
336}
337
338#[cfg(test)]
339mod tests {
340 use super::*;
341
342 const D: Duration = Duration::from_millis(100);
343
344 fn view(img: [u32; 2], vp: [f32; 2], fit: FitMode) -> (View, Instant) {
345 let now = Instant::now();
346 let mut v = View::new(now);
347 v.set_viewport(vp[0], vp[1]);
348 v.reset(img, fit, None, now);
349 (v, now)
350 }
351
352 fn apply(a: Affine, u: [f32; 2]) -> [f32; 2] {
353 [
354 a.m[0] * u[0] + a.m[1] * u[1] + a.t[0],
355 a.m[2] * u[0] + a.m[3] * u[1] + a.t[1],
356 ]
357 }
358
359 fn close(a: [f32; 2], b: [f32; 2]) -> bool {
360 (a[0] - b[0]).abs() < 0.01 && (a[1] - b[1]).abs() < 0.01
361 }
362
363 #[test]
364 fn fit_modes() {
365 let vp = [1000.0, 500.0];
366 assert_eq!(view([2000, 2000], vp, FitMode::Fit).0.scale(), 0.25);
367 assert_eq!(view([2000, 2000], vp, FitMode::Fill).0.scale(), 0.5);
368 assert_eq!(view([2000, 2000], vp, FitMode::Width).0.scale(), 0.5);
369 assert_eq!(view([2000, 2000], vp, FitMode::Height).0.scale(), 0.25);
370 assert_eq!(view([100, 100], vp, FitMode::Down).0.scale(), 1.0);
371 assert_eq!(view([100, 100], vp, FitMode::Fit).0.scale(), 5.0);
372 assert_eq!(view([100, 100], vp, FitMode::Original).0.scale(), 1.0);
373 }
374
375 #[test]
376 fn fitted_image_is_centred() {
377 let (v, now) = view([200, 100], [400.0, 400.0], FitMode::Fit);
378 let a = v.transform(now);
379 assert!(close(apply(a, [0.0, 0.0]), [0.0, 100.0]));
380 assert!(close(apply(a, [1.0, 1.0]), [400.0, 300.0]));
381 }
382
383 #[test]
384 fn zoom_keeps_anchor_fixed_throughout() {
385 let (mut v, now) = view([4000, 4000], [1000.0, 1000.0], FitMode::Fit);
386 let cursor = [700.0, 300.0];
387 let a0 = v.transform(now);
388 // Image uv under the cursor before zooming.
389 let uv = [
390 (cursor[0] - a0.t[0]) / a0.m[0],
391 (cursor[1] - a0.t[1]) / a0.m[3],
392 ];
393 v.zoom_by(2.0, Some(cursor), now, D);
394 for ms in [0, 20, 50, 80, 100, 150] {
395 let a = v.transform(now + Duration::from_millis(ms));
396 assert!(close(apply(a, uv), cursor), "drifted at {ms} ms");
397 }
398 assert!(v.fit_mode().is_none());
399 }
400
401 #[test]
402 fn rotation_swaps_fit_dimensions() {
403 let (mut v, now) = view([200, 100], [400.0, 400.0], FitMode::Fit);
404 assert_eq!(v.scale(), 2.0);
405 v.rotate(1, now, Duration::ZERO);
406 assert_eq!(v.scale(), 2.0);
407 let (mut v, now) = view([400, 100], [400.0, 200.0], FitMode::Fit);
408 assert_eq!(v.scale(), 1.0);
409 v.rotate(1, now, Duration::ZERO);
410 assert_eq!(v.scale(), 0.5);
411 // A clockwise turn puts the image's top-left corner at the top-right.
412 let a = v.transform(now);
413 assert!(close(apply(a, [0.0, 0.0]), [225.0, 0.0]));
414 }
415
416 #[test]
417 fn pan_is_clamped_to_edges() {
418 let (mut v, now) = view([1000, 1000], [500.0, 500.0], FitMode::Original);
419 v.pan_pixels([10_000.0, 0.0]);
420 let a = v.transform(now);
421 // Dragging right as far as possible shows the left edge.
422 assert!(close(apply(a, [0.0, 0.0]), [0.0, -250.0]));
423 v.pan_edge(Dir::Right, now, Duration::ZERO);
424 let a = v.transform(now);
425 assert!(close(apply(a, [1.0, 1.0]), [500.0, 750.0]));
426 }
427
428 #[test]
429 fn flip_mirrors_on_screen() {
430 let (mut v, now) = view([200, 100], [200.0, 100.0], FitMode::Fit);
431 v.flip(Axis::Horizontal, now, Duration::ZERO);
432 let a = v.transform(now);
433 assert!(close(apply(a, [0.0, 0.0]), [200.0, 0.0]));
434 }
435}