Skip to main content

blockworx/
progress.rs

1//! Unit-interval progress through an animation or gesture, clamped to
2//! `[0, 1]`. The saturating arithmetic and total ordering come from
3//! [`Bounded`]; construction from a ratio of durations is the common case.
4
5use std::time::Duration;
6
7use blockworx_geom::{Bounded, Bounds};
8pub struct ProgressBounds;
9
10impl Bounds for ProgressBounds {
11    const MIN: f32 = 0.0;
12    const MAX: f32 = 1.0;
13}
14
15/// A newtype rather than a `Bounded<ProgressBounds>` alias so the domain
16/// methods below can be inherent: the clamping and the total order are
17/// [`Bounded`]'s, everything a *progress* means is here.
18#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)]
19pub struct Progress(Bounded<ProgressBounds>);
20
21impl Progress {
22    pub const fn new(value: f32) -> Self {
23        Self(Bounded::new(value))
24    }
25
26    pub const fn get(self) -> f32 {
27        self.0.get()
28    }
29
30    pub const fn zero() -> Self {
31        Self(Bounded::min_value())
32    }
33
34    pub const fn one() -> Self {
35        Self(Bounded::max_value())
36    }
37
38    pub fn is_complete(self) -> bool {
39        self == Self::one()
40    }
41
42    /// How far `elapsed` has run through `total` (complete for a zero total).
43    pub fn through(elapsed: Duration, total: Duration) -> Self {
44        if total.is_zero() {
45            return Self::one();
46        }
47        Self::new(elapsed.div_duration_f32(total))
48    }
49
50    /// The remaining fraction: `1 - self`. Fades read as
51    /// `opacity(t.complement())` instead of `1.0 - t` at every site.
52    pub fn complement(self) -> Self {
53        Self::new(1.0 - self.get())
54    }
55
56    /// Smoothstep: the same progress with the ends eased, so a glide starts
57    /// and stops the way a hand moves a pointer.
58    pub fn eased(self) -> Self {
59        let t = self.get();
60        Self::new(t * t * (3.0 - 2.0 * t))
61    }
62}
63
64impl From<Progress> for f32 {
65    fn from(progress: Progress) -> f32 {
66        progress.get()
67    }
68}
69
70#[cfg(test)]
71mod tests {
72    use super::*;
73
74    #[test]
75    fn through_is_the_clamped_ratio() {
76        let total = Duration::from_secs(2);
77        assert_eq!(Progress::through(Duration::from_secs(1), total).get(), 0.5);
78        assert_eq!(
79            Progress::through(Duration::from_secs(5), total),
80            Progress::one()
81        );
82        assert_eq!(Progress::through(Duration::ZERO, total), Progress::zero());
83        assert_eq!(
84            Progress::through(Duration::from_secs(1), Duration::ZERO),
85            Progress::one()
86        );
87    }
88
89    #[test]
90    fn complement_mirrors_around_the_midpoint() {
91        assert_eq!(Progress::new(0.25).complement().get(), 0.75);
92        assert_eq!(Progress::one().complement(), Progress::zero());
93    }
94}