Krolyxon
Commit a44bdf7

Branches

Tags

Refs →

remagesrcstateview.rs

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