Skip to main content

motion_core/animation/
interval.rs

1//! A half-open time interval that turns a time point into clamped progress.
2
3use crate::{Duration, Easing, Time};
4
5/// A half-open interval `[start, start + duration)` on a timeline.
6///
7/// This is the single definition of "time range to progress" shared by
8/// `SceneContext::progress_in`, `SceneContext::ease_in` and [`crate::Tween`].
9/// Comparisons use integer ticks; a zero duration jumps from zero to one at `start`.
10///
11/// ```
12/// use motion_core::prelude::*;
13/// use motion_core::Interval;
14/// let interval = Interval::new(Time::ZERO + 1.seconds(), 2.seconds());
15/// assert_eq!(interval.progress(Time::ZERO), 0.0);
16/// assert_eq!(interval.progress(Time::ZERO + 2.seconds()), 0.5);
17/// assert_eq!(interval.progress(Time::ZERO + 5.seconds()), 1.0);
18/// ```
19#[derive(Clone, Copy, Debug, PartialEq, Eq)]
20pub struct Interval {
21    start: Time,
22    duration: Duration,
23}
24
25impl Interval {
26    /// Creates an interval starting at `start` and lasting `duration`.
27    pub const fn new(start: Time, duration: Duration) -> Self {
28        Self { start, duration }
29    }
30
31    /// Returns the first time inside the interval.
32    pub const fn start(&self) -> Time {
33        self.start
34    }
35
36    /// Returns the nonnegative length of the interval.
37    pub const fn duration(&self) -> Duration {
38        self.duration
39    }
40
41    /// Reports whether `time` lies in `[start, start + duration)`.
42    ///
43    /// Wide integer differences allow queries across opposite signed tick extremes.
44    pub fn contains(&self, time: Time) -> bool {
45        let elapsed = self.elapsed(time);
46        elapsed >= 0 && elapsed < i128::from(self.duration.ticks())
47    }
48
49    /// Returns progress clamped to `0.0..=1.0`: zero before the start, one at
50    /// or after the end.
51    pub fn progress(&self, time: Time) -> f64 {
52        let elapsed = self.elapsed(time);
53        if elapsed < 0 {
54            0.0
55        } else if elapsed >= i128::from(self.duration.ticks()) {
56            1.0
57        } else {
58            elapsed as f64 / self.duration.ticks() as f64
59        }
60    }
61
62    /// Returns [`Self::progress`] shaped by an easing curve.
63    pub fn ease(&self, time: Time, easing: Easing) -> f64 {
64        easing.apply(self.progress(time))
65    }
66
67    fn elapsed(&self, time: Time) -> i128 {
68        i128::from(time.ticks()) - i128::from(self.start.ticks())
69    }
70}
71
72#[cfg(test)]
73mod tests {
74    use super::*;
75    use crate::TimeLiteral;
76
77    #[test]
78    fn progress_clamps_and_zero_duration_jumps() {
79        let interval = Interval::new(Time::ZERO + 1.seconds(), 2.seconds());
80        assert!(!interval.contains(Time::ZERO));
81        assert!(interval.contains(Time::ZERO + 1.seconds()));
82        assert!(!interval.contains(Time::ZERO + 3.seconds()));
83        assert_eq!(interval.progress(Time::ZERO + 1.seconds()), 0.0);
84        assert_eq!(interval.progress(Time::ZERO + 3.seconds()), 1.0);
85
86        let jump = Interval::new(Time::ZERO, Duration::ZERO);
87        assert_eq!(jump.progress(Time::from_ticks(-1)), 0.0);
88        assert_eq!(jump.progress(Time::ZERO), 1.0);
89        assert!(!jump.contains(Time::ZERO));
90    }
91
92    #[test]
93    fn extreme_ticks_do_not_overflow() {
94        let interval = Interval::new(Time::from_ticks(i64::MIN), Duration::ZERO);
95        assert_eq!(interval.progress(Time::from_ticks(i64::MAX)), 1.0);
96    }
97}