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 delay = self.timing.delay().as_millis();
195        let elapsed = self.elapsed.as_millis();
196
197        if elapsed < delay {
198            self.current = self.from.clone();
199            return;
200        }
201
202        let duration = self.timing.duration().as_millis();
203        if duration <= 0.0 {
204            self.finish();
205            return;
206        }
207
208        let active_elapsed = elapsed - delay;
209        let iterations = self.timing.iterations().finite_count();
210
211        if let Some(count) = iterations {
212            let total = duration * f64::from(count);
213            if active_elapsed >= total {
214                self.finish();
215                return;
216            }
217        }
218
219        let iteration = (active_elapsed / duration).floor() as u32;
220        let raw_progress = (active_elapsed % duration) / duration;
221        let progress = self
222            .timing
223            .direction()
224            .sample_progress(iteration, raw_progress);
225        let eased = self.timing.easing().value(progress as f32);
226        self.current = T::interpolate(&self.from, &self.to, eased);
227    }
228}
229
230impl<T: Animatable> Animation<T> for Tween<T> {
231    fn value(&self) -> &T {
232        self.value()
233    }
234
235    fn state(&self) -> AnimationState {
236        self.state()
237    }
238
239    fn duration(&self) -> Option<Duration> {
240        self.timing.total_duration()
241    }
242
243    fn tick(&mut self, delta: Duration) {
244        self.tick(delta);
245    }
246
247    fn advance(&mut self, delta: Duration) -> Duration {
248        if self.state != AnimationState::Running {
249            return delta;
250        }
251
252        let Some(remaining) = self.remaining() else {
253            self.tick(delta);
254            return Duration::ZERO;
255        };
256        let consumed = delta.min(remaining);
257        self.tick(consumed);
258        delta.saturating_sub(consumed)
259    }
260
261    fn pause(&mut self) {
262        self.pause();
263    }
264
265    fn resume(&mut self) {
266        self.resume();
267    }
268
269    fn cancel(&mut self) {
270        self.cancel();
271    }
272
273    fn seek(&mut self, progress: f32) {
274        self.seek(progress);
275    }
276
277    fn finish(&mut self) {
278        self.finish();
279    }
280
281    fn retarget(&mut self, target: &T) -> bool {
282        self.transition_to(target.clone());
283        true
284    }
285}
286
287#[cfg(test)]
288mod tests {
289    use super::Tween;
290    use crate::{
291        Animation, AnimationState,
292        timing::{Direction, Duration, IterationCount, Timing},
293    };
294    use float_cmp::assert_approx_eq;
295
296    #[test]
297    fn new_tween_is_idle_with_matching_endpoints() {
298        let tween = Tween::new(3.0_f32);
299
300        assert_eq!(tween.state(), AnimationState::Idle);
301        assert_approx_eq!(f32, *tween.value(), 3.0);
302        assert_approx_eq!(f32, *tween.from(), 3.0);
303        assert_approx_eq!(f32, *tween.target(), 3.0);
304        assert_approx_eq!(f64, tween.timing().duration().as_millis(), 200.0);
305    }
306
307    #[test]
308    fn transition_uses_current_value_as_new_start() {
309        let mut tween = Tween::between(0.0_f32, 10.0, Timing::new(100.0));
310        tween.tick(Duration::from_millis(40.0));
311        tween.transition_to(20.0);
312
313        assert_approx_eq!(f32, *tween.from(), 4.0);
314        assert_approx_eq!(f32, *tween.value(), 4.0);
315        assert_approx_eq!(f32, *tween.target(), 20.0);
316    }
317
318    #[test]
319    fn paused_and_canceled_tweens_do_not_tick() {
320        let mut tween = Tween::between(0.0_f32, 10.0, Timing::new(100.0));
321        tween.pause();
322        tween.tick(Duration::from_millis(50.0));
323        assert_approx_eq!(f32, *tween.value(), 0.0);
324
325        tween.resume();
326        tween.tick(Duration::from_millis(25.0));
327        assert_approx_eq!(f32, *tween.value(), 2.5);
328
329        tween.cancel();
330        tween.tick(Duration::from_millis(75.0));
331        assert_eq!(tween.state(), AnimationState::Canceled);
332        assert_approx_eq!(f32, *tween.value(), 2.5);
333    }
334
335    #[test]
336    fn advance_returns_unconsumed_duration() {
337        let mut tween = Tween::between(0.0_f32, 1.0, Timing::new(100.0));
338
339        let overflow = Animation::advance(&mut tween, Duration::from_millis(125.0));
340
341        assert_eq!(overflow, Duration::from_millis(25.0));
342        assert_eq!(tween.state(), AnimationState::Completed);
343    }
344
345    #[test]
346    fn infinite_tween_consumes_all_advanced_time() {
347        let timing = Timing::new(100.0).with_iterations(IterationCount::INFINITE);
348        let mut tween = Tween::between(0.0_f32, 1.0, timing);
349
350        let overflow = Animation::advance(&mut tween, Duration::from_millis(250.0));
351
352        assert_eq!(overflow, Duration::ZERO);
353        assert_eq!(tween.state(), AnimationState::Running);
354        assert_approx_eq!(f32, *tween.value(), 0.5);
355    }
356
357    #[test]
358    fn finish_respects_repeated_direction() {
359        let timing = Timing::new(100.0)
360            .with_iterations(2)
361            .with_direction(Direction::Alternate);
362        let mut tween = Tween::between(0.0_f32, 10.0, timing);
363
364        tween.finish();
365
366        assert_eq!(tween.state(), AnimationState::Completed);
367        assert_approx_eq!(f32, *tween.value(), 0.0);
368    }
369
370    #[test]
371    fn zero_duration_tween_finishes_on_tick() {
372        let mut tween = Tween::between(0.0_f32, 10.0, Timing::new(0.0));
373
374        tween.tick(Duration::ZERO);
375
376        assert_eq!(tween.state(), AnimationState::Completed);
377        assert_approx_eq!(f32, *tween.value(), 10.0);
378    }
379}