Skip to main content

aura_anim_core/
tween.rs

1//! Duration-based interpolation animations.
2
3use crate::{
4    timing::{Duration, Timing},
5    traits::{Animatable, Animation, AnimationState},
6};
7
8/// The lifecycle state of a [`Tween`].
9pub type TweenState = AnimationState;
10
11/// An animation that interpolates between two values using [`Timing`].
12///
13/// # Examples
14///
15/// ```
16/// use aura_anim_core::{Tween, timing::Timing};
17/// use std::time::Duration;
18///
19/// let mut tween = Tween::between(0.0_f32, 10.0, Timing::new(100.0));
20///
21/// tween.tick(Duration::from_millis(50));
22/// assert_eq!(*tween.value(), 5.0);
23///
24/// tween.tick(Duration::from_millis(50));
25/// assert!(tween.is_completed());
26/// assert_eq!(*tween.value(), 10.0);
27/// ```
28#[derive(Debug, Clone)]
29pub struct Tween<T: Animatable> {
30    from: T,
31    to: T,
32    current: T,
33    elapsed: Duration,
34    timing: Timing,
35    state: AnimationState,
36}
37
38impl<T: Animatable> Tween<T> {
39    /// Creates an idle tween with the default 200 millisecond timing.
40    #[must_use]
41    pub fn new(value: T) -> Self {
42        Self::with_timing(value, Timing::new(200.0))
43    }
44
45    /// Creates an idle tween with the provided timing.
46    #[must_use]
47    pub fn with_timing(value: T, timing: Timing) -> Self {
48        Self {
49            from: value.clone(),
50            to: value.clone(),
51            current: value,
52            elapsed: Duration::ZERO,
53            timing,
54            state: AnimationState::Idle,
55        }
56    }
57
58    /// Creates a running tween from `from` to `to`.
59    #[must_use]
60    pub fn between(from: T, to: T, timing: Timing) -> Self {
61        let mut tween = Self::with_timing(from, timing);
62        tween.transition_to(to);
63        tween
64    }
65
66    /// Returns the current interpolated value.
67    #[must_use]
68    pub fn value(&self) -> &T {
69        &self.current
70    }
71
72    /// Returns the value at the start of the current transition.
73    #[must_use]
74    pub fn from(&self) -> &T {
75        &self.from
76    }
77
78    /// Returns the target value of the current transition.
79    #[must_use]
80    pub fn target(&self) -> &T {
81        &self.to
82    }
83
84    /// Returns the timing configuration.
85    #[must_use]
86    pub const fn timing(&self) -> Timing {
87        self.timing
88    }
89
90    /// Returns the current lifecycle state.
91    #[must_use]
92    pub const fn state(&self) -> AnimationState {
93        self.state
94    }
95
96    /// Returns whether the tween is currently running.
97    #[must_use]
98    pub fn is_active(&self) -> bool {
99        self.state == AnimationState::Running
100    }
101
102    /// Returns whether the tween has completed.
103    #[must_use]
104    pub fn is_completed(&self) -> bool {
105        self.state == AnimationState::Completed
106    }
107
108    /// Starts a transition from the current value to `target`.
109    pub fn transition_to(&mut self, target: T) {
110        self.from = self.current.clone();
111        self.to = target;
112        self.elapsed = Duration::ZERO;
113        self.state = AnimationState::Running;
114        self.sample();
115    }
116
117    /// Advances the tween by `delta`.
118    pub fn tick(&mut self, delta: impl Into<Duration>) {
119        if self.state != AnimationState::Running {
120            return;
121        }
122
123        self.elapsed += delta.into();
124        self.sample();
125    }
126
127    fn remaining(&self) -> Option<Duration> {
128        let total = self.timing.total_duration()?;
129        Some(total.saturating_sub(self.elapsed))
130    }
131
132    /// Pauses the tween when it is running.
133    pub fn pause(&mut self) {
134        if self.state == AnimationState::Running {
135            self.state = AnimationState::Paused;
136        }
137    }
138
139    /// Resumes the tween when it is paused.
140    pub fn resume(&mut self) {
141        if self.state == AnimationState::Paused {
142            self.state = AnimationState::Running;
143        }
144    }
145
146    /// Cancels the tween unless it is already completed or canceled.
147    pub fn cancel(&mut self) {
148        if matches!(
149            self.state,
150            AnimationState::Running | AnimationState::Paused | AnimationState::Idle
151        ) {
152            self.state = AnimationState::Canceled;
153        }
154    }
155
156    /// Seeks to normalized progress within the complete timing interval.
157    pub fn seek(&mut self, progress: f32) {
158        let progress = if progress.is_nan() {
159            0.0
160        } else {
161            progress.clamp(0.0, 1.0)
162        };
163        self.state = AnimationState::Running;
164        let total = self.timing.total_duration().unwrap_or_else(|| {
165            Duration::from_millis(
166                self.timing.delay().as_millis() + self.timing.duration().as_millis(),
167            )
168        });
169        self.elapsed = Duration::from_millis(total.as_millis() * f64::from(progress));
170        self.sample();
171    }
172
173    /// Moves the tween to its final value and completed state.
174    #[allow(clippy::cast_possible_truncation)]
175    pub fn finish(&mut self) {
176        let progress = self
177            .timing
178            .iterations()
179            .finite_count()
180            .map_or(self.timing.direction().sample_progress(1, 1.0), |count| {
181                self.timing.direction().end_progress(count)
182            });
183        self.current = T::interpolate(
184            &self.from,
185            &self.to,
186            self.timing.easing().value(progress as f32),
187        );
188        self.state = AnimationState::Completed;
189    }
190
191    #[allow(clippy::cast_sign_loss)]
192    #[allow(clippy::cast_possible_truncation)]
193    fn sample(&mut self) {
194        let Some(active_elapsed) = self.elapsed.checked_sub_delay(self.timing.delay()) else {
195            self.current = self.from.clone();
196            return;
197        };
198
199        let duration = self.timing.duration();
200        if duration.is_zero() {
201            self.finish();
202            return;
203        }
204
205        if self
206            .timing
207            .active_duration()
208            .is_some_and(|total| active_elapsed >= total)
209        {
210            self.finish();
211            return;
212        }
213
214        let iteration_progress = active_elapsed.as_secs() / duration.as_secs();
215        let iteration = iteration_progress.floor() as u32;
216        let raw_progress = iteration_progress.fract();
217        let progress = self
218            .timing
219            .direction()
220            .sample_progress(iteration, raw_progress);
221        let eased = self.timing.easing().value(progress as f32);
222        self.current = T::interpolate(&self.from, &self.to, eased);
223    }
224}
225
226impl<T: Animatable> Animation<T> for Tween<T> {
227    fn value(&self) -> &T {
228        self.value()
229    }
230
231    fn state(&self) -> AnimationState {
232        self.state()
233    }
234
235    fn duration(&self) -> Option<Duration> {
236        self.timing.total_duration()
237    }
238
239    fn tick(&mut self, delta: Duration) {
240        self.tick(delta);
241    }
242
243    fn advance(&mut self, delta: Duration) -> Duration {
244        if self.state != AnimationState::Running {
245            return delta;
246        }
247
248        let Some(remaining) = self.remaining() else {
249            self.tick(delta);
250            return Duration::ZERO;
251        };
252        let consumed = delta.min(remaining);
253        self.tick(consumed);
254        delta.saturating_sub(consumed)
255    }
256
257    fn pause(&mut self) {
258        self.pause();
259    }
260
261    fn resume(&mut self) {
262        self.resume();
263    }
264
265    fn cancel(&mut self) {
266        self.cancel();
267    }
268
269    fn seek(&mut self, progress: f32) {
270        self.seek(progress);
271    }
272
273    fn finish(&mut self) {
274        self.finish();
275    }
276
277    fn retarget(&mut self, target: &T) -> bool {
278        self.transition_to(target.clone());
279        true
280    }
281
282    fn set_rate(&mut self, rate: f64) {
283        self.timing = self.timing.with_rate(rate);
284    }
285
286    fn into_value(self: Box<Self>) -> T {
287        self.current
288    }
289}
290
291#[cfg(test)]
292mod tests {
293    use super::Tween;
294    use crate::{
295        Animation, AnimationState,
296        timing::{Delay, Direction, Duration, IterationCount, Timing},
297    };
298    use float_cmp::assert_approx_eq;
299
300    #[test]
301    fn new_tween_is_idle_with_matching_endpoints() {
302        let tween = Tween::new(3.0_f32);
303
304        assert_eq!(tween.state(), AnimationState::Idle);
305        assert_approx_eq!(f32, *tween.value(), 3.0);
306        assert_approx_eq!(f32, *tween.from(), 3.0);
307        assert_approx_eq!(f32, *tween.target(), 3.0);
308        assert_approx_eq!(f64, tween.timing().duration().as_millis(), 200.0);
309    }
310
311    #[test]
312    fn transition_uses_current_value_as_new_start() {
313        let mut tween = Tween::between(0.0_f32, 10.0, Timing::new(100.0));
314        tween.tick(Duration::from_millis(40.0));
315        tween.transition_to(20.0);
316
317        assert_approx_eq!(f32, *tween.from(), 4.0);
318        assert_approx_eq!(f32, *tween.value(), 4.0);
319        assert_approx_eq!(f32, *tween.target(), 20.0);
320    }
321
322    #[test]
323    fn rate_changes_tween_playback_duration() {
324        let mut faster = Tween::between(0.0_f32, 10.0, Timing::new(100.0)).rate(2.0);
325        let mut slower = Tween::between(0.0_f32, 10.0, Timing::new(100.0)).rate(0.5);
326
327        faster.tick(Duration::from_millis(50.0));
328        slower.tick(Duration::from_millis(50.0));
329
330        assert_eq!(faster.state(), AnimationState::Completed);
331        assert_approx_eq!(f32, *faster.value(), 10.0);
332        assert_eq!(slower.state(), AnimationState::Running);
333        assert_approx_eq!(f32, *slower.value(), 2.5);
334    }
335
336    #[test]
337    fn paused_and_canceled_tweens_do_not_tick() {
338        let mut tween = Tween::between(0.0_f32, 10.0, Timing::new(100.0));
339        tween.pause();
340        tween.tick(Duration::from_millis(50.0));
341        assert_approx_eq!(f32, *tween.value(), 0.0);
342
343        tween.resume();
344        tween.tick(Duration::from_millis(25.0));
345        assert_approx_eq!(f32, *tween.value(), 2.5);
346
347        tween.cancel();
348        tween.tick(Duration::from_millis(75.0));
349        assert_eq!(tween.state(), AnimationState::Canceled);
350        assert_approx_eq!(f32, *tween.value(), 2.5);
351    }
352
353    #[test]
354    fn advance_returns_unconsumed_duration() {
355        let mut tween = Tween::between(0.0_f32, 1.0, Timing::new(100.0));
356
357        let overflow = Animation::advance(&mut tween, Duration::from_millis(125.0));
358
359        assert_eq!(overflow, Duration::from_millis(25.0));
360        assert_eq!(tween.state(), AnimationState::Completed);
361    }
362
363    #[test]
364    fn infinite_tween_consumes_all_advanced_time() {
365        let timing = Timing::new(100.0).with_iterations(IterationCount::INFINITE);
366        let mut tween = Tween::between(0.0_f32, 1.0, timing);
367
368        let overflow = Animation::advance(&mut tween, Duration::from_millis(250.0));
369
370        assert_eq!(overflow, Duration::ZERO);
371        assert_eq!(tween.state(), AnimationState::Running);
372        assert_approx_eq!(f32, *tween.value(), 0.5);
373    }
374
375    #[test]
376    fn delay_and_submillisecond_progress_preserve_duration_precision() {
377        let timing = Timing::new(0.5).with_delay(Delay::from_millis(0.25));
378        let mut tween = Tween::between(0.0_f32, 10.0, timing);
379
380        tween.tick(std::time::Duration::from_micros(250));
381        assert_approx_eq!(f32, *tween.value(), 0.0);
382
383        tween.tick(std::time::Duration::from_micros(125));
384        assert_approx_eq!(f32, *tween.value(), 2.5);
385    }
386
387    #[test]
388    fn repeated_tween_samples_progress_within_alternate_iteration() {
389        let timing = Timing::new(100.0)
390            .with_iterations(2)
391            .with_direction(Direction::Alternate);
392        let mut tween = Tween::between(0.0_f32, 10.0, timing);
393
394        tween.tick(Duration::from_millis(150.0));
395
396        assert_eq!(tween.state(), AnimationState::Running);
397        assert_approx_eq!(f32, *tween.value(), 5.0);
398    }
399
400    #[test]
401    fn finish_respects_repeated_direction() {
402        let timing = Timing::new(100.0)
403            .with_iterations(2)
404            .with_direction(Direction::Alternate);
405        let mut tween = Tween::between(0.0_f32, 10.0, timing);
406
407        tween.finish();
408
409        assert_eq!(tween.state(), AnimationState::Completed);
410        assert_approx_eq!(f32, *tween.value(), 0.0);
411    }
412
413    #[test]
414    fn zero_duration_tween_finishes_on_tick() {
415        let mut tween = Tween::between(0.0_f32, 10.0, Timing::new(0.0));
416
417        tween.tick(Duration::ZERO);
418
419        assert_eq!(tween.state(), AnimationState::Completed);
420        assert_approx_eq!(f32, *tween.value(), 10.0);
421    }
422}