Skip to main content

aura_anim_core/
timing.rs

1//! Timing configuration and elapsed-time normalization.
2
3mod duration;
4mod iteration;
5mod mode;
6mod utils;
7
8pub use duration::{Delay, Duration};
9pub use iteration::IterationCount;
10pub use mode::Direction;
11
12pub use lilt::Easing;
13
14/// Timing configuration shared by duration-based animations.
15///
16/// # Examples
17///
18/// ```
19/// use aura_anim_core::timing::{Delay, Direction, Timing};
20///
21/// let timing = Timing::ease_out(250.0)
22///     .with_delay(Delay::from_millis(50.0))
23///     .with_direction(Direction::Alternate)
24///     .with_iterations(2);
25///
26/// assert_eq!(timing.duration().as_millis(), 250.0);
27/// assert_eq!(timing.total_duration().unwrap().as_millis(), 550.0);
28/// ```
29#[derive(Debug, Clone, Copy, PartialEq)]
30pub struct Timing {
31    /// Active duration for one iteration.
32    duration: Duration,
33    /// Start delay before the active interval.
34    delay: Delay,
35    /// Playback direction configuration.
36    direction: Direction,
37    /// Easing curve applied to normalized iteration progress.
38    easing: Easing,
39    /// Number of active iterations.
40    iterations: IterationCount,
41}
42
43impl Timing {
44    /// Creates a timing value with a duration in milliseconds.
45    #[must_use]
46    pub fn new(duration_ms: f64) -> Self {
47        Self {
48            duration: Duration::from_millis(duration_ms),
49            ..Self::default()
50        }
51    }
52
53    /// Creates a linear timing with a duration in milliseconds.
54    #[must_use]
55    pub fn linear(duration_ms: f64) -> Self {
56        Self::new(duration_ms)
57    }
58
59    /// Creates an ease-in timing with a duration in milliseconds.
60    #[must_use]
61    pub fn ease_in(duration_ms: f64) -> Self {
62        Self::new(duration_ms).with_easing(Easing::EaseIn)
63    }
64
65    /// Creates an ease-out timing with a duration in milliseconds.
66    #[must_use]
67    pub fn ease_out(duration_ms: f64) -> Self {
68        Self::new(duration_ms).with_easing(Easing::EaseOut)
69    }
70
71    /// Creates an ease-in-out timing with a duration in milliseconds.
72    #[must_use]
73    pub fn ease_in_out(duration_ms: f64) -> Self {
74        Self::new(duration_ms).with_easing(Easing::EaseInOut)
75    }
76
77    /// Returns the duration of the timing.
78    #[must_use]
79    pub const fn duration(&self) -> Duration {
80        self.duration
81    }
82
83    /// Returns the delay of the timing.
84    #[must_use]
85    pub const fn delay(&self) -> Delay {
86        self.delay
87    }
88
89    /// Returns the direction of the timing.
90    #[must_use]
91    pub const fn direction(&self) -> Direction {
92        self.direction
93    }
94
95    /// Returns the easing curve of the timing.
96    #[must_use]
97    pub const fn easing(&self) -> Easing {
98        self.easing
99    }
100
101    /// Returns the number of iterations of the timing.
102    #[must_use]
103    pub const fn iterations(&self) -> IterationCount {
104        self.iterations
105    }
106
107    /// Sets the start delay.
108    #[must_use]
109    pub const fn with_delay(mut self, delay: Delay) -> Self {
110        self.delay = delay;
111        self
112    }
113
114    /// Sets the playback direction.
115    #[must_use]
116    pub const fn with_direction(mut self, direction: Direction) -> Self {
117        self.direction = direction;
118        self
119    }
120
121    /// Sets the easing curve.
122    #[must_use]
123    pub const fn with_easing(mut self, easing: Easing) -> Self {
124        self.easing = easing;
125        self
126    }
127
128    /// Sets the iteration count.
129    #[must_use]
130    pub fn with_iterations(mut self, iterations: impl Into<IterationCount>) -> Self {
131        self.iterations = iterations.into();
132        self
133    }
134
135    /// Returns the total active duration when the timing has a finite length.
136    #[must_use]
137    pub fn active_duration(self) -> Option<Duration> {
138        let count = self.iterations.finite_count()?;
139
140        self.duration.checked_mul(count)
141    }
142
143    /// Returns the total duration including delay when finite.
144    #[must_use]
145    pub fn total_duration(self) -> Option<Duration> {
146        let active = self.active_duration()?;
147
148        active.checked_add_delay(self.delay)
149    }
150
151    pub(crate) fn with_duration(mut self, duration: Duration) -> Self {
152        self.duration = duration;
153        self
154    }
155
156    pub(crate) fn with_rate(mut self, rate: f64) -> Self {
157        self.duration = self.duration.divided_by(rate);
158        self
159    }
160}
161
162impl Default for Timing {
163    fn default() -> Self {
164        Self {
165            duration: Duration::ZERO,
166            delay: Delay::ZERO,
167            direction: Direction::default(),
168            easing: Easing::Linear,
169            iterations: IterationCount::default(),
170        }
171    }
172}
173
174#[cfg(test)]
175mod tests {
176    use super::{Delay, Timing};
177    use float_cmp::assert_approx_eq;
178
179    #[test]
180    fn rate_scales_active_duration_without_changing_delay() {
181        let faster = Timing::new(200.0)
182            .with_delay(Delay::from_millis(40.0))
183            .with_iterations(3)
184            .with_rate(2.0);
185        let slower = Timing::new(200.0).with_rate(0.5);
186
187        assert_approx_eq!(f64, faster.duration().as_millis(), 100.0);
188        assert_approx_eq!(f64, faster.delay().as_millis(), 40.0);
189        assert_approx_eq!(f64, faster.total_duration().unwrap().as_millis(), 340.0);
190        assert_approx_eq!(f64, slower.duration().as_millis(), 400.0);
191    }
192
193    #[test]
194    fn invalid_rate_leaves_duration_unchanged() {
195        let timing = Timing::new(200.0);
196
197        assert_eq!(timing.with_rate(0.0).duration(), timing.duration());
198        assert_eq!(timing.with_rate(-1.0).duration(), timing.duration());
199        assert_eq!(timing.with_rate(f64::NAN).duration(), timing.duration());
200        assert_eq!(
201            timing.with_rate(f64::INFINITY).duration(),
202            timing.duration()
203        );
204    }
205}