Skip to main content

aura_anim_core/keyframes/
animation.rs

1use std::cmp::Ordering;
2
3use lilt::Easing;
4
5use crate::{
6    keyframes::Keyframe,
7    timing::{Delay, Direction, Duration, IterationCount, Timing},
8    traits::{Animatable, Animation, AnimationState},
9};
10
11/// An animation composed of time-positioned [`Keyframe`] values.
12///
13/// # Examples
14///
15/// ```
16/// use aura_anim_core::{Animation, keyframes::Keyframes, timing::Duration};
17///
18/// let mut animation = Keyframes::new(0.0_f32)
19///     .push(100.0, 1.0)
20///     .push(200.0, 0.0);
21///
22/// animation.tick(Duration::from_millis(100.0));
23/// assert_eq!(*animation.value(), 1.0);
24///
25/// animation.tick(Duration::from_millis(100.0));
26/// assert_eq!(*animation.value(), 0.0);
27/// ```
28#[derive(Debug, Clone)]
29pub struct Keyframes<T: Animatable> {
30    frames: Vec<Keyframe<T>>,
31    current: T,
32    elapsed: Duration,
33    timing: Timing,
34    state: AnimationState,
35}
36
37impl<T: Animatable> Keyframes<T> {
38    /// Creates a running keyframe animation with an initial frame at zero.
39    #[must_use]
40    pub fn new(initial: T) -> Self {
41        Self {
42            frames: vec![Keyframe::new(0.0, initial.clone())],
43            current: initial,
44            elapsed: Duration::ZERO,
45            timing: Timing::default(),
46            state: AnimationState::Running,
47        }
48    }
49
50    /// Adds a keyframe with linear easing after the preceding frame.
51    #[must_use]
52    pub fn push(self, time_ms: f64, value: T) -> Self {
53        self.push_eased(time_ms, value, Easing::Linear)
54    }
55
56    /// Adds a keyframe with the provided easing.
57    #[must_use]
58    pub fn push_eased(self, time_ms: f64, value: T, easing: Easing) -> Self {
59        let frame = Keyframe::new(time_ms.max(0.0), value).with_easing(easing);
60        self.push_frame(frame)
61    }
62
63    /// Adds or replaces a keyframe at the frame's time.
64    #[must_use]
65    pub fn push_frame(mut self, frame: Keyframe<T>) -> Self {
66        match self.frames.binary_search_by(|existing| {
67            existing
68                .time()
69                .partial_cmp(&frame.time())
70                .unwrap_or(Ordering::Equal)
71        }) {
72            Ok(index) => self.frames[index] = frame,
73            Err(index) => self.frames.insert(index, frame),
74        }
75        self.timing = self.timing.with_duration(self.duration());
76        self
77    }
78
79    /// Sets the delay before playback begins.
80    #[must_use]
81    pub fn with_delay(mut self, delay: Delay) -> Self {
82        self.timing = self.timing.with_delay(delay);
83        self
84    }
85
86    /// Sets the number of playback iterations.
87    #[must_use]
88    pub fn with_iterations(mut self, iterations: impl Into<IterationCount>) -> Self {
89        self.timing = self.timing.with_iterations(iterations);
90        self
91    }
92
93    /// Sets the playback direction.
94    #[must_use]
95    pub fn with_direction(mut self, direction: Direction) -> Self {
96        self.timing = self.timing.with_direction(direction);
97        self
98    }
99
100    /// Returns the time of the final keyframe.
101    #[must_use]
102    fn duration(&self) -> Duration {
103        Duration::from_millis(self.frames.last().map_or(0.0, Keyframe::time))
104    }
105
106    fn sample(&mut self) {
107        let delay = self.timing.delay().as_millis();
108        let elapsed = self.elapsed.as_millis();
109        if elapsed < delay {
110            self.current = self.frames[0].value().clone();
111            return;
112        }
113
114        let duration = self.duration().as_millis();
115        if duration <= 0.0 {
116            self.finish();
117            return;
118        }
119
120        let active_elapsed = elapsed - delay;
121        if let Some(count) = self.timing.iterations().finite_count() {
122            let total = duration * f64::from(count);
123            if active_elapsed >= total {
124                self.finish();
125                return;
126            }
127        }
128
129        #[allow(clippy::cast_possible_truncation)]
130        #[allow(clippy::cast_sign_loss)]
131        let iteration = (active_elapsed / duration).floor() as u32;
132
133        let raw_progress = (active_elapsed % duration) / duration;
134        let progress = self
135            .timing
136            .direction()
137            .sample_progress(iteration, raw_progress);
138        self.current = self.value_at(duration * progress);
139    }
140
141    fn value_at(&self, time_ms: f64) -> T {
142        if time_ms <= self.frames[0].time() {
143            return self.frames[0].value().clone();
144        }
145
146        let upper = self.frames.partition_point(|frame| frame.time() <= time_ms);
147        if upper >= self.frames.len() {
148            return self.frames[self.frames.len() - 1].value().clone();
149        }
150
151        let from = &self.frames[upper - 1];
152        let to = &self.frames[upper];
153        let span = (to.time() - from.time()).max(f64::EPSILON);
154        let progress = ((time_ms - from.time()) / span).clamp(0.0, 1.0);
155
156        #[allow(clippy::cast_possible_truncation)]
157        let eased = from.easing().value(progress as f32);
158
159        T::interpolate(from.value(), to.value(), eased)
160    }
161}
162
163impl<T: Animatable> Animation<T> for Keyframes<T> {
164    fn value(&self) -> &T {
165        &self.current
166    }
167
168    fn state(&self) -> AnimationState {
169        self.state
170    }
171
172    fn duration(&self) -> Option<Duration> {
173        self.timing.total_duration()
174    }
175
176    fn tick(&mut self, delta: Duration) {
177        if self.state != AnimationState::Running {
178            return;
179        }
180        self.elapsed += delta;
181        self.sample();
182    }
183
184    fn advance(&mut self, delta: Duration) -> Duration {
185        if self.state != AnimationState::Running {
186            return delta;
187        }
188
189        let Some(total) = self.timing.total_duration() else {
190            self.tick(delta);
191            return Duration::ZERO;
192        };
193        let remaining = total.saturating_sub(self.elapsed);
194        let consumed = delta.min(remaining);
195        self.tick(consumed);
196        delta.saturating_sub(consumed)
197    }
198
199    fn pause(&mut self) {
200        if self.state == AnimationState::Running {
201            self.state = AnimationState::Paused;
202        }
203    }
204
205    fn resume(&mut self) {
206        if self.state == AnimationState::Paused {
207            self.state = AnimationState::Running;
208        }
209    }
210
211    fn cancel(&mut self) {
212        if matches!(self.state, AnimationState::Running | AnimationState::Paused) {
213            self.state = AnimationState::Canceled;
214        }
215    }
216
217    fn seek(&mut self, progress: f32) {
218        let progress = if progress.is_nan() {
219            0.0
220        } else {
221            progress.clamp(0.0, 1.0)
222        };
223        self.state = AnimationState::Running;
224        let total = self
225            .timing
226            .total_duration()
227            .unwrap_or(self.duration())
228            .as_millis();
229        self.elapsed = Duration::from_millis(total * f64::from(progress));
230        self.sample();
231    }
232
233    fn finish(&mut self) {
234        let progress = self
235            .timing
236            .iterations()
237            .finite_count()
238            .map_or(self.timing.direction().sample_progress(1, 1.0), |count| {
239                self.timing.direction().end_progress(count)
240            });
241        self.current = self.value_at(self.duration().as_millis() * progress);
242        self.state = AnimationState::Completed;
243    }
244}