libsurfer/
viewport.rs

1use std::ops::RangeInclusive;
2
3use derive_more::{Add, AddAssign, Div, Mul, Neg, Sub, SubAssign};
4use num::{BigInt, BigRational, FromPrimitive, ToPrimitive};
5use serde::{Deserialize, Serialize};
6
7#[derive(
8    Debug,
9    Clone,
10    Copy,
11    Serialize,
12    Deserialize,
13    Add,
14    Sub,
15    Mul,
16    Neg,
17    AddAssign,
18    SubAssign,
19    PartialOrd,
20    PartialEq,
21)]
22pub struct Relative(pub f64);
23
24impl Relative {
25    pub fn absolute(&self, num_timestamps: &BigInt) -> Absolute {
26        Absolute(
27            self.0
28                * num_timestamps
29                    .to_f64()
30                    .expect("Failed to convert timestamp to f64"),
31        )
32    }
33
34    pub fn min(&self, other: &Relative) -> Self {
35        Self(self.0.min(other.0))
36    }
37
38    pub fn max(&self, other: &Relative) -> Self {
39        Self(self.0.max(other.0))
40    }
41}
42
43impl std::ops::Div for Relative {
44    type Output = Relative;
45
46    fn div(self, rhs: Self) -> Self::Output {
47        Self(self.0 / rhs.0)
48    }
49}
50
51#[derive(
52    Debug, Clone, Copy, Serialize, Deserialize, Add, Sub, Mul, Neg, Div, PartialOrd, PartialEq,
53)]
54pub struct Absolute(pub f64);
55
56impl Absolute {
57    pub fn relative(&self, num_timestamps: &BigInt) -> Relative {
58        Relative(
59            self.0
60                / num_timestamps
61                    .to_f64()
62                    .expect("Failed to convert timestamp to f64"),
63        )
64    }
65}
66
67impl std::ops::Div for Absolute {
68    type Output = Absolute;
69
70    fn div(self, rhs: Self) -> Self::Output {
71        Self(self.0 / rhs.0)
72    }
73}
74
75impl From<&BigInt> for Absolute {
76    fn from(value: &BigInt) -> Self {
77        Self(value.to_f64().expect("Failed to convert timestamp to f64"))
78    }
79}
80
81fn default_edge_space() -> f64 {
82    0.2
83}
84
85fn default_min_width() -> Absolute {
86    Absolute(0.5)
87}
88
89#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
90pub struct Viewport {
91    pub curr_left: Relative,
92    pub curr_right: Relative,
93
94    target_left: Relative,
95    target_right: Relative,
96
97    move_start_left: Relative,
98    move_start_right: Relative,
99
100    // Number of seconds since the the last time a movement happened
101    move_duration: Option<f32>,
102    pub move_strategy: ViewportStrategy,
103    #[serde(skip, default = "default_edge_space")]
104    edge_space: f64,
105
106    #[serde(skip, default = "default_min_width")]
107    min_width: Absolute,
108}
109
110impl Default for Viewport {
111    fn default() -> Self {
112        Self {
113            curr_left: Relative(0.0),
114            curr_right: Relative(1.0),
115            target_left: Relative(0.0),
116            target_right: Relative(1.0),
117            move_start_left: Relative(0.0),
118            move_start_right: Relative(1.0),
119            move_duration: None,
120            move_strategy: ViewportStrategy::Instant,
121            edge_space: default_edge_space(),
122            min_width: default_min_width(),
123        }
124    }
125}
126
127impl Viewport {
128    pub fn new() -> Self {
129        Self::default()
130    }
131    pub fn left_edge_time(self, num_timestamps: &BigInt) -> BigInt {
132        BigInt::from(self.curr_left.absolute(num_timestamps).0 as i64)
133    }
134    pub fn right_edge_time(self, num_timestamps: &BigInt) -> BigInt {
135        BigInt::from(self.curr_right.absolute(num_timestamps).0 as i64)
136    }
137
138    pub fn as_absolute_time(&self, x: f64, view_width: f32, num_timestamps: &BigInt) -> Absolute {
139        let time_spacing = self.width_absolute(num_timestamps) / view_width as f64;
140
141        self.curr_left.absolute(num_timestamps) + time_spacing * x
142    }
143
144    pub fn as_time_bigint(&self, x: f32, view_width: f32, num_timestamps: &BigInt) -> BigInt {
145        let Viewport {
146            curr_left: left,
147            curr_right: right,
148            ..
149        } = &self;
150
151        let big_right = BigRational::from_f64(right.absolute(num_timestamps).0)
152            .unwrap_or_else(|| BigRational::from_u8(1).unwrap());
153        let big_left = BigRational::from_f64(left.absolute(num_timestamps).0)
154            .unwrap_or_else(|| BigRational::from_u8(1).unwrap());
155        let big_width =
156            BigRational::from_f32(view_width).unwrap_or_else(|| BigRational::from_u8(1).unwrap());
157        let big_x = BigRational::from_f32(x).unwrap_or_else(|| BigRational::from_u8(1).unwrap());
158
159        let time = big_left.clone() + (big_right - big_left) / big_width * big_x;
160        time.round().to_integer()
161    }
162
163    pub fn to_time_f64(&self, x: f64, view_width: f32, num_timestamps: &BigInt) -> Absolute {
164        let time_spacing = self.width_absolute(num_timestamps) / view_width as f64;
165
166        self.curr_left.absolute(num_timestamps) + time_spacing * x
167    }
168
169    pub fn to_time_bigint(&self, x: f32, view_width: f32, num_timestamps: &BigInt) -> BigInt {
170        let Viewport {
171            curr_left: left,
172            curr_right: right,
173            ..
174        } = &self;
175
176        let big_right = BigRational::from_f64(right.absolute(num_timestamps).0)
177            .unwrap_or_else(|| BigRational::from_u8(1).unwrap());
178        let big_left = BigRational::from_f64(left.absolute(num_timestamps).0)
179            .unwrap_or_else(|| BigRational::from_u8(1).unwrap());
180        let big_width =
181            BigRational::from_f32(view_width).unwrap_or_else(|| BigRational::from_u8(1).unwrap());
182        let big_x = BigRational::from_f32(x).unwrap_or_else(|| BigRational::from_u8(1).unwrap());
183
184        let time = big_left.clone() + (big_right - big_left) / big_width * big_x;
185        time.round().to_integer()
186    }
187
188    /// Computes which x-pixel corresponds to the specified time adduming the viewport is rendered
189    /// into a viewport of `view_width`
190    pub fn pixel_from_time(&self, time: &BigInt, view_width: f32, num_timestamps: &BigInt) -> f32 {
191        let distance_from_left =
192            Absolute(time.to_f64().unwrap()) - self.curr_left.absolute(num_timestamps);
193
194        (((distance_from_left / self.width_absolute(num_timestamps)).0) * (view_width as f64))
195            as f32
196    }
197
198    pub fn pixel_from_time_f64(
199        &self,
200        time: Absolute,
201        view_width: f32,
202        num_timestamps: &BigInt,
203    ) -> f32 {
204        let distance_from_left = time - self.curr_left.absolute(num_timestamps);
205
206        (((distance_from_left / self.width_absolute(num_timestamps)).0) * (view_width as f64))
207            as f32
208    }
209
210    pub fn pixel_from_absolute_time(
211        &self,
212        time: Absolute,
213        view_width: f32,
214        num_timestamps: &BigInt,
215    ) -> f32 {
216        let distance_from_left = time - self.curr_left.absolute(num_timestamps);
217
218        (((distance_from_left / self.width_absolute(num_timestamps)).0) * (view_width as f64))
219            as f32
220    }
221
222    /// Return new viewport for a different file length
223    ///
224    /// Tries to keep the current zoom level and position. If zoom is not possible it
225    /// will zoom in as much as needed to keep border margins. If the new waveform is
226    /// too short, the viewport will be moved to the left as much as needed for the zoom level.
227    pub fn clip_to(&self, old_num_timestamps: &BigInt, new_num_timestamps: &BigInt) -> Viewport {
228        let left_timestamp = self.curr_left.absolute(old_num_timestamps);
229        let right_timestamp = self.curr_right.absolute(old_num_timestamps);
230        let absolute_width = right_timestamp - left_timestamp;
231
232        let new_absolute_width = new_num_timestamps
233            .to_f64()
234            .expect("Failed to convert timestamp to f64")
235            * (2.0 * self.edge_space);
236        let (left, right) = if absolute_width.0 > new_absolute_width {
237            // is the new waveform so short that we can't keep the current zoom level?
238            (Relative(-self.edge_space), Relative(1.0 + self.edge_space))
239        } else {
240            // our zoom level is achievable but we don't know the waveform is long enough
241
242            let unmoved_right = Relative(
243                (left_timestamp + absolute_width).0.to_f64().unwrap()
244                    / new_num_timestamps.to_f64().unwrap(),
245            );
246            if unmoved_right <= Relative(1.0 + self.edge_space) {
247                // waveform is long enough, keep current view as-is
248                (self.curr_left, unmoved_right)
249            } else {
250                // waveform is too short, clip end to the right edge (including empty space)
251                // since we checked above for zoom level, we know that there must be enough
252                // waveform to the left to keep the current zoom level
253                (
254                    Relative(1.0 + self.edge_space - absolute_width.0),
255                    Relative(1.0 + self.edge_space),
256                )
257            }
258        };
259
260        Viewport {
261            curr_left: left,
262            curr_right: right,
263            target_left: left,
264            target_right: right,
265            move_start_left: left,
266            move_start_right: right,
267            move_duration: None,
268            move_strategy: self.move_strategy,
269            edge_space: self.edge_space,
270            min_width: self.min_width,
271        }
272    }
273
274    #[inline]
275    fn width(&self) -> Relative {
276        self.curr_right - self.curr_left
277    }
278
279    #[inline]
280    fn width_absolute(&self, num_timestamps: &BigInt) -> Absolute {
281        self.width().absolute(num_timestamps)
282    }
283
284    pub fn go_to_time(&mut self, center: &BigInt, num_timestamps: &BigInt) {
285        let center_point: Absolute = center.into();
286        let half_width = self.half_width_absolute(num_timestamps);
287
288        let target_left = (center_point - half_width).relative(num_timestamps);
289        let target_right = (center_point + half_width).relative(num_timestamps);
290        self.set_viewport_to_clipped(target_left, target_right, num_timestamps);
291    }
292
293    pub fn zoom_to_fit(&mut self) {
294        self.set_target_left(Relative(0.0));
295        self.set_target_right(Relative(1.0));
296    }
297
298    pub fn go_to_start(&mut self) {
299        let old_width = self.width();
300        self.set_target_left(Relative(0.0));
301        self.set_target_right(old_width);
302    }
303
304    pub fn go_to_end(&mut self) {
305        self.set_target_left(Relative(1.0) - self.width());
306        self.set_target_right(Relative(1.0));
307    }
308
309    pub fn handle_canvas_zoom(
310        &mut self,
311        mouse_ptr_timestamp: Option<BigInt>,
312        delta: f64,
313        num_timestamps: &BigInt,
314    ) {
315        // Zoom or scroll
316        let Viewport {
317            curr_left: left,
318            curr_right: right,
319            ..
320        } = &self;
321
322        let (target_left, target_right) =
323            match mouse_ptr_timestamp.map(|t| Absolute::from(&t).relative(num_timestamps)) {
324                Some(mouse_location) => (
325                    (*left - mouse_location) / Relative(delta) + mouse_location,
326                    (*right - mouse_location) / Relative(delta) + mouse_location,
327                ),
328                None => {
329                    let mid_point = self.midpoint();
330                    let offset = self.half_width() * delta;
331
332                    (mid_point - offset, mid_point + offset)
333                }
334            };
335
336        self.set_viewport_to_clipped(target_left, target_right, num_timestamps);
337    }
338
339    pub fn handle_canvas_scroll(&mut self, deltay: f64) {
340        // Scroll 5% of the viewport per scroll event.
341        // One scroll event yields 50
342        let scroll_step = -self.width() / Relative(50. * 20.);
343        let scaled_deltay = scroll_step * deltay;
344        self.set_viewport_to_clipped_no_width_check(
345            self.curr_left + scaled_deltay,
346            self.curr_right + scaled_deltay,
347        );
348    }
349
350    fn set_viewport_to_clipped(
351        &mut self,
352        target_left: Relative,
353        target_right: Relative,
354        num_timestamps: &BigInt,
355    ) {
356        let rel_min_width = self.min_width.relative(num_timestamps);
357
358        if (target_right - target_left) <= rel_min_width + Relative(f64::EPSILON) {
359            let center = (target_left + target_right) * 0.5;
360            self.set_viewport_to_clipped_no_width_check(
361                center - rel_min_width,
362                center + rel_min_width,
363            );
364        } else {
365            self.set_viewport_to_clipped_no_width_check(target_left, target_right);
366        }
367    }
368
369    fn set_viewport_to_clipped_no_width_check(
370        &mut self,
371        target_left: Relative,
372        target_right: Relative,
373    ) {
374        let width = target_right - target_left;
375
376        let abs_min = Relative(-self.edge_space);
377        let abs_max = Relative(1.0 + self.edge_space);
378
379        let max_right = Relative(1.0) + width * self.edge_space;
380        let min_left = -width * self.edge_space;
381        if width > (abs_max - abs_min) {
382            self.set_target_left(abs_min);
383            self.set_target_right(abs_max);
384        } else if target_left < min_left {
385            self.set_target_left(min_left);
386            self.set_target_right(min_left + width);
387        } else if target_right > max_right {
388            self.set_target_left(max_right - width);
389            self.set_target_right(max_right);
390        } else {
391            self.set_target_left(target_left);
392            self.set_target_right(target_right);
393        }
394    }
395
396    #[inline]
397    fn midpoint(&self) -> Relative {
398        (self.curr_right + self.curr_left) * 0.5
399    }
400
401    #[inline]
402    fn half_width(&self) -> Relative {
403        self.width() * 0.5
404    }
405
406    #[inline]
407    fn half_width_absolute(&self, num_timestamps: &BigInt) -> Absolute {
408        (self.width() * 0.5).absolute(num_timestamps)
409    }
410
411    pub fn zoom_to_range(&mut self, left: &BigInt, right: &BigInt, num_timestamps: &BigInt) {
412        self.set_viewport_to_clipped(
413            Absolute::from(left).relative(num_timestamps),
414            Absolute::from(right).relative(num_timestamps),
415            num_timestamps,
416        );
417    }
418
419    pub fn go_to_cursor_if_not_in_view(
420        &mut self,
421        cursor: &BigInt,
422        num_timestamps: &BigInt,
423    ) -> bool {
424        let fcursor = cursor.into();
425        if fcursor <= self.curr_left.absolute(num_timestamps)
426            || fcursor >= self.curr_right.absolute(num_timestamps)
427        {
428            self.go_to_time_f64(fcursor, num_timestamps);
429            true
430        } else {
431            false
432        }
433    }
434
435    pub fn go_to_time_f64(&mut self, center: Absolute, num_timestamps: &BigInt) {
436        let half_width = (self.curr_right.absolute(num_timestamps)
437            - self.curr_left.absolute(num_timestamps))
438            / 2.;
439
440        self.set_viewport_to_clipped(
441            (center - half_width).relative(num_timestamps),
442            (center + half_width).relative(num_timestamps),
443            num_timestamps,
444        );
445    }
446
447    fn set_target_left(&mut self, target_left: Relative) {
448        if let ViewportStrategy::Instant = self.move_strategy {
449            self.curr_left = target_left
450        } else {
451            self.target_left = target_left;
452            self.move_start_left = self.curr_left;
453            self.move_duration = Some(0.);
454        }
455    }
456    fn set_target_right(&mut self, target_right: Relative) {
457        if let ViewportStrategy::Instant = self.move_strategy {
458            self.curr_right = target_right
459        } else {
460            self.target_right = target_right;
461            self.move_start_right = self.curr_right;
462            self.move_duration = Some(0.);
463        }
464    }
465
466    pub fn move_viewport(&mut self, frame_time: f32) {
467        match &self.move_strategy {
468            ViewportStrategy::Instant => {
469                self.curr_left = self.target_left;
470                self.curr_right = self.target_right;
471                self.move_duration = None;
472            }
473            ViewportStrategy::EaseInOut { duration } => {
474                if let Some(move_duration) = &mut self.move_duration {
475                    if *move_duration + frame_time >= *duration {
476                        self.move_duration = None;
477                        self.curr_left = self.target_left;
478                        self.curr_right = self.target_right;
479                    } else {
480                        *move_duration += frame_time;
481
482                        self.curr_left = Relative(ease_in_out_size(
483                            self.move_start_left.0..=self.target_left.0,
484                            (*move_duration as f64) / (*duration as f64),
485                        ));
486                        self.curr_right = Relative(ease_in_out_size(
487                            self.move_start_right.0..=self.target_right.0,
488                            (*move_duration as f64) / (*duration as f64),
489                        ));
490                    }
491                }
492            }
493        }
494    }
495
496    pub fn is_moving(&self) -> bool {
497        self.move_duration.is_some()
498    }
499}
500
501pub fn ease_in_out_size(r: RangeInclusive<f64>, t: f64) -> f64 {
502    r.start() + ((r.end() - r.start()) * -((std::f64::consts::PI * t).cos() - 1.) / 2.)
503}
504
505#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
506pub enum ViewportStrategy {
507    Instant,
508    EaseInOut { duration: f32 },
509}