Skip to main content

blockworx_paint/
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(
19    Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, serde::Serialize, serde::Deserialize,
20)]
21#[serde(transparent)]
22pub struct Progress(Bounded<ProgressBounds>);
23
24impl Progress {
25    pub const fn new(value: f32) -> Self {
26        Self(Bounded::new(value))
27    }
28
29    pub const fn get(self) -> f32 {
30        self.0.get()
31    }
32
33    pub const fn zero() -> Self {
34        Self(Bounded::min_value())
35    }
36
37    pub const fn one() -> Self {
38        Self(Bounded::max_value())
39    }
40
41    pub fn is_complete(self) -> bool {
42        self == Self::one()
43    }
44
45    /// How far `elapsed` has run through `total` (complete for a zero total).
46    pub fn through(elapsed: Duration, total: Duration) -> Self {
47        if total.is_zero() {
48            return Self::one();
49        }
50        Self::new(elapsed.div_duration_f32(total))
51    }
52
53    /// The remaining fraction: `1 - self`. Fades read as
54    /// `opacity(t.complement())` instead of `1.0 - t` at every site.
55    #[must_use]
56    pub fn complement(self) -> Self {
57        Self::new(1.0 - self.get())
58    }
59
60    /// Smoothstep: the same progress with the ends eased, so a glide starts
61    /// and stops the way a hand moves a pointer.
62    #[must_use]
63    pub fn eased(self) -> Self {
64        let t = self.get();
65        Self::new(t * t * (3.0 - 2.0 * t))
66    }
67}
68
69impl From<Progress> for f32 {
70    fn from(progress: Progress) -> f32 {
71        progress.get()
72    }
73}
74
75#[cfg(test)]
76mod tests {
77    use super::*;
78
79    #[test]
80    fn through_is_the_clamped_ratio() {
81        let total = Duration::from_secs(2);
82        assert_eq!(Progress::through(Duration::from_secs(1), total).get(), 0.5);
83        assert_eq!(
84            Progress::through(Duration::from_secs(5), total),
85            Progress::one()
86        );
87        assert_eq!(Progress::through(Duration::ZERO, total), Progress::zero());
88        assert_eq!(
89            Progress::through(Duration::from_secs(1), Duration::ZERO),
90            Progress::one()
91        );
92    }
93
94    #[test]
95    fn complement_mirrors_around_the_midpoint() {
96        assert_eq!(Progress::new(0.25).complement().get(), 0.75);
97        assert_eq!(Progress::one().complement(), Progress::zero());
98    }
99}