Skip to main content

aura_anim_core/
runtime.rs

1//! Runtime storage, typed handles, and animation commands.
2
3use std::marker::PhantomData;
4
5use crate::{
6    runtime::{
7        anim_dyn::AnimationDyn, motion::RawMotionId, settled::Settled, typed::TypedAnimation,
8    },
9    timing::{Duration, Timing},
10    traits::{Animatable, Animation, AnimationState},
11    tween::Tween,
12};
13
14mod anim_dyn;
15mod command;
16mod motion;
17mod settled;
18mod typed;
19
20pub use command::AnimationCommand;
21pub use motion::Motion;
22
23/// Controls whether the runtime retains an animation after it settles.
24#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
25pub enum RetainPolicy {
26    /// Keep the animation and its final value until explicitly removed.
27    #[default]
28    Keep,
29    /// Remove the animation after it completes or is canceled.
30    DropWhenSettled,
31}
32
33struct AnimationSlot {
34    generation: u64,
35    transition: Timing,
36    retain_policy: RetainPolicy,
37    animation: Option<Box<dyn AnimationDyn>>,
38    active: bool,
39    queued: bool,
40}
41
42/// Stores animations and advances their active values.
43///
44/// # Examples
45///
46/// ```
47/// use aura_anim_core::{MotionRuntime, timing::Timing};
48/// use std::time::Duration;
49///
50/// let mut runtime = MotionRuntime::new();
51/// let opacity = runtime.motion_with(0.0_f32, Timing::new(100.0));
52///
53/// assert!(opacity.transition_to(1.0, &mut runtime));
54/// runtime.tick(Duration::from_millis(50));
55///
56/// assert_eq!(opacity.value(&runtime), 0.5);
57/// assert!(opacity.is_active(&runtime));
58/// ```
59#[derive(Default)]
60pub struct MotionRuntime {
61    slots: Vec<AnimationSlot>,
62    free: Vec<usize>,
63    active: Vec<RawMotionId>,
64    next_active: Vec<RawMotionId>,
65    active_count: usize,
66    motion_count: usize,
67    last_tick: Option<std::time::Instant>,
68}
69
70impl MotionRuntime {
71    /// Creates an empty motion runtime.
72    #[must_use]
73    pub fn new() -> Self {
74        Self::default()
75    }
76
77    /// Inserts an idle motion with the default transition timing.
78    pub fn motion<T: Animatable>(&mut self, initial: T) -> Motion<T> {
79        self.motion_with(initial, Timing::new(200.0))
80    }
81
82    /// Inserts an idle motion with the provided transition timing.
83    pub fn motion_with<T: Animatable>(&mut self, initial: T, timing: Timing) -> Motion<T> {
84        self.insert(Tween::with_timing(initial, timing), timing)
85    }
86
87    /// Inserts an animation that remains stored after it settles.
88    pub fn insert<T: Animatable>(
89        &mut self,
90        animation: impl Animation<T>,
91        transition: Timing,
92    ) -> Motion<T> {
93        self.insert_with_policy(animation, transition, RetainPolicy::Keep)
94    }
95
96    /// Inserts an animation that is removed after it settles.
97    pub fn play_once<T: Animatable>(&mut self, animation: impl Animation<T>) -> Motion<T> {
98        self.insert_with_policy(animation, Timing::default(), RetainPolicy::DropWhenSettled)
99    }
100
101    /// Inserts an animation with transition timing and a retention policy.
102    pub fn insert_with_policy<T: Animatable>(
103        &mut self,
104        animation: impl Animation<T>,
105        transition: Timing,
106        retain_policy: RetainPolicy,
107    ) -> Motion<T> {
108        let mut animation = TypedAnimation::new(animation);
109        animation.compact();
110        let animation: Box<dyn AnimationDyn> = Box::new(animation);
111        let id = if let Some(slot_index) = self.free.pop() {
112            let slot = &mut self.slots[slot_index];
113            slot.transition = transition;
114            slot.retain_policy = retain_policy;
115            slot.animation = Some(animation);
116            slot.active = false;
117            slot.queued = false;
118            RawMotionId::new(slot_index, slot.generation)
119        } else {
120            let slot = self.slots.len();
121            self.slots.push(AnimationSlot {
122                generation: 0,
123                transition,
124                retain_policy,
125                animation: Some(animation),
126                active: false,
127                queued: false,
128            });
129            RawMotionId::new(slot, 0)
130        };
131
132        self.motion_count += 1;
133        let motion = Motion::new(id, PhantomData);
134        if self.animation(id).is_some_and(AnimationDyn::is_active) {
135            self.activate(id);
136        } else if retain_policy == RetainPolicy::DropWhenSettled
137            && self.animation(id).is_some_and(|animation| {
138                matches!(
139                    animation.state(),
140                    AnimationState::Completed | AnimationState::Canceled
141                )
142            })
143        {
144            self.remove(motion);
145        }
146        motion
147    }
148
149    /// Advances every active animation by `delta`.
150    pub fn tick(&mut self, delta: impl Into<Duration>) {
151        let delta = delta.into();
152        self.next_active.clear();
153
154        for index in 0..self.active.len() {
155            let id = self.active[index];
156            let Some(slot) = self.slots.get_mut(id.slot()) else {
157                continue;
158            };
159            if slot.generation != id.generation() {
160                continue;
161            }
162            slot.queued = false;
163            if !slot.active {
164                continue;
165            }
166            let Some(animation) = slot.animation.as_mut() else {
167                continue;
168            };
169
170            animation.advance(delta);
171            if animation.is_active() {
172                self.next_active.push(id);
173                slot.queued = true;
174            } else {
175                slot.active = false;
176                self.active_count = self.active_count.saturating_sub(1);
177                if slot.retain_policy == RetainPolicy::DropWhenSettled
178                    && matches!(
179                        animation.state(),
180                        AnimationState::Completed | AnimationState::Canceled
181                    )
182                {
183                    slot.animation = None;
184                    slot.generation = slot.generation.wrapping_add(1);
185                    self.motion_count = self.motion_count.saturating_sub(1);
186                    self.free.push(id.slot());
187                } else {
188                    animation.compact();
189                }
190            }
191        }
192
193        std::mem::swap(&mut self.active, &mut self.next_active);
194        if self.active_count == 0 {
195            self.active.clear();
196            self.next_active.clear();
197            self.last_tick = None;
198        }
199    }
200
201    /// Advances active animations using elapsed wall-clock time.
202    ///
203    /// The first call establishes the clock origin and advances by zero.
204    pub fn tick_at(&mut self, now: std::time::Instant) {
205        let delta = self.last_tick.map_or(std::time::Duration::ZERO, |last| {
206            now.saturating_duration_since(last)
207        });
208        self.last_tick = Some(now);
209        self.tick(delta);
210    }
211
212    /// Returns the number of animations currently marked active.
213    #[must_use]
214    pub fn active_count(&self) -> usize {
215        self.active_count
216    }
217
218    /// Returns whether at least one animation is active.
219    #[must_use]
220    pub fn has_active(&self) -> bool {
221        self.active_count > 0
222    }
223
224    /// Returns the number of stored animations.
225    #[must_use]
226    pub fn motion_count(&self) -> usize {
227        self.motion_count
228    }
229
230    /// Returns the allocated capacity of the runtime slot storage.
231    #[must_use]
232    pub fn slot_capacity(&self) -> usize {
233        self.slots.capacity()
234    }
235
236    /// Releases unused slot and queue capacity.
237    pub fn shrink_to_fit(&mut self) {
238        while self
239            .slots
240            .last()
241            .is_some_and(|slot| slot.animation.is_none())
242        {
243            self.slots.pop();
244        }
245        self.free.retain(|slot| *slot < self.slots.len());
246        self.slots.shrink_to_fit();
247        self.free.shrink_to_fit();
248        self.active.shrink_to_fit();
249        self.next_active.shrink_to_fit();
250    }
251
252    /// Returns the current value for a valid motion handle.
253    #[must_use]
254    pub fn value<T: Animatable>(&self, motion: Motion<T>) -> Option<&T> {
255        self.animation(motion.id())?.value_any().downcast_ref::<T>()
256    }
257
258    /// Returns the state for a valid motion handle.
259    #[must_use]
260    pub fn state<T: Animatable>(&self, motion: Motion<T>) -> Option<AnimationState> {
261        self.animation(motion.id()).map(AnimationDyn::state)
262    }
263
264    /// Returns whether the referenced motion is active.
265    #[must_use]
266    pub fn is_active<T: Animatable>(&self, motion: Motion<T>) -> bool {
267        self.animation(motion.id())
268            .is_some_and(AnimationDyn::is_active)
269    }
270
271    /// Transitions a valid motion toward `target`.
272    ///
273    /// Returns `false` when the handle is no longer valid.
274    pub fn transition_to<T: Animatable>(&mut self, motion: Motion<T>, target: T) -> bool {
275        if self
276            .animation_mut(motion.id())
277            .is_some_and(|animation| animation.retarget_any(&target))
278        {
279            self.activate(motion.id());
280            return true;
281        }
282
283        let Some(current) = self.value(motion).cloned() else {
284            return false;
285        };
286        let transition = self.slots[motion.id().slot()].transition;
287        self.replace(
288            motion.id(),
289            TypedAnimation::new(Tween::between(current, target, transition)),
290        );
291        true
292    }
293
294    /// Replaces the animation associated with a valid motion.
295    ///
296    /// Returns `false` when the handle is no longer valid.
297    pub fn play<T: Animatable>(&mut self, motion: Motion<T>, animation: impl Animation<T>) -> bool {
298        if self.value(motion).is_none() {
299            return false;
300        }
301
302        self.replace(motion.id(), TypedAnimation::new(animation));
303        true
304    }
305
306    /// Applies a command to a valid motion.
307    ///
308    /// Returns `false` when the handle is no longer valid.
309    pub fn command<T: Animatable>(&mut self, motion: Motion<T>, command: AnimationCommand) -> bool {
310        let (active, state) = {
311            let Some(animation) = self.animation_mut(motion.id()) else {
312                return false;
313            };
314            animation.command(command);
315            (animation.is_active(), animation.state())
316        };
317
318        if active {
319            self.activate(motion.id());
320        } else {
321            self.deactivate(motion.id());
322            if self.slots[motion.id().slot()].retain_policy == RetainPolicy::DropWhenSettled
323                && matches!(state, AnimationState::Completed | AnimationState::Canceled)
324            {
325                self.remove(motion);
326            } else if let Some(animation) = self.animation_mut(motion.id()) {
327                animation.compact();
328            }
329        }
330        true
331    }
332
333    /// Removes the animation referenced by `motion`.
334    ///
335    /// Returns `false` when the handle is no longer valid.
336    pub fn remove<T: Animatable>(&mut self, motion: Motion<T>) -> bool {
337        let Some(slot) = self.slots.get_mut(motion.id().slot()) else {
338            return false;
339        };
340        if slot.generation != motion.id().generation() || slot.animation.is_none() {
341            return false;
342        }
343
344        let was_active = slot.active;
345        slot.animation = None;
346        slot.active = false;
347        slot.queued = false;
348        slot.generation = slot.generation.wrapping_add(1);
349        if was_active {
350            self.active_count = self.active_count.saturating_sub(1);
351        }
352        self.motion_count = self.motion_count.saturating_sub(1);
353        self.free.push(motion.id().slot());
354
355        let slot_idx = motion.id().slot();
356        let new_gen = slot.generation;
357        self.active
358            .retain(|id| id.slot() != slot_idx || id.generation() == new_gen);
359        self.next_active
360            .retain(|id| id.slot() != slot_idx || id.generation() == new_gen);
361
362        if self.active_count == 0 {
363            self.active.clear();
364            self.next_active.clear();
365            self.last_tick = None;
366        }
367        true
368    }
369
370    fn animation(&self, id: RawMotionId) -> Option<&dyn AnimationDyn> {
371        let slot = self.slots.get(id.slot())?;
372        if slot.generation != id.generation() {
373            return None;
374        }
375        slot.animation.as_deref()
376    }
377
378    fn animation_mut(&mut self, id: RawMotionId) -> Option<&mut (dyn AnimationDyn + '_)> {
379        let slot = self.slots.get_mut(id.slot())?;
380        if slot.generation != id.generation() {
381            return None;
382        }
383        match slot.animation.as_mut() {
384            Some(animation) => Some(animation.as_mut()),
385            None => None,
386        }
387    }
388
389    fn replace(&mut self, id: RawMotionId, animation: TypedAnimation<impl Animatable>) {
390        let Some(slot) = self.slots.get_mut(id.slot()) else {
391            return;
392        };
393        if slot.generation != id.generation() {
394            return;
395        }
396        let mut animation = animation;
397        animation.compact();
398        slot.animation = Some(Box::new(animation));
399        if slot
400            .animation
401            .as_deref()
402            .is_some_and(AnimationDyn::is_active)
403        {
404            self.activate(id);
405        } else {
406            self.deactivate(id);
407        }
408    }
409
410    fn activate(&mut self, id: RawMotionId) {
411        let Some(slot) = self.slots.get_mut(id.slot()) else {
412            return;
413        };
414        if slot.generation != id.generation() || slot.animation.is_none() {
415            return;
416        }
417        if self.active_count == 0 {
418            self.last_tick = None;
419        }
420        if !slot.active {
421            slot.active = true;
422            self.active_count += 1;
423        }
424        if !slot.queued {
425            slot.queued = true;
426            self.active.push(id);
427        }
428    }
429
430    fn deactivate(&mut self, id: RawMotionId) {
431        let Some(slot) = self.slots.get_mut(id.slot()) else {
432            return;
433        };
434        if slot.generation != id.generation() || !slot.active {
435            return;
436        }
437        slot.active = false;
438        self.active_count = self.active_count.saturating_sub(1);
439        if self.active_count == 0 {
440            for active in &self.active {
441                if let Some(slot) = self.slots.get_mut(active.slot())
442                    && slot.generation == active.generation()
443                {
444                    slot.queued = false;
445                }
446            }
447            self.active.clear();
448            self.next_active.clear();
449            self.last_tick = None;
450        }
451    }
452}