1use std::marker::PhantomData;
4
5use crate::{
6 runtime::{anim_dyn::AnimationDyn, typed::TypedAnimation},
7 timing::{Duration, Timing},
8 traits::{Animatable, Animation, AnimationState, IntoMotionAnimation},
9 tween::Tween,
10};
11
12mod anim_dyn;
13mod command;
14mod error;
15mod event;
16mod motion;
17mod typed;
18
19pub use command::AnimationCommand;
20pub use error::MotionError;
21pub use event::{
22 InterruptionReason, MotionEvent, MotionEventKind, MotionEventTarget, MotionId, PlaybackId,
23 RemovalReason,
24};
25pub use motion::Motion;
26
27#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
29pub enum RetainPolicy {
30 #[default]
32 Keep,
33 DropWhenSettled,
35}
36
37struct AnimationSlot {
38 generation: u64,
39 playback_revision: u64,
40 terminal_emitted: bool,
41 transition: Timing,
42 retain_policy: RetainPolicy,
43 animation: Option<Box<dyn AnimationDyn>>,
44 active: bool,
45 queued: bool,
46}
47
48#[derive(Default)]
67pub struct MotionRuntime {
68 slots: Vec<AnimationSlot>,
69 free: Vec<usize>,
70 active: Vec<MotionId>,
71 next_active: Vec<MotionId>,
72 events: Vec<MotionEvent>,
73 active_count: usize,
74 motion_count: usize,
75 last_tick: Option<std::time::Instant>,
76}
77
78impl MotionRuntime {
79 #[must_use]
81 pub fn new() -> Self {
82 Self::default()
83 }
84
85 pub fn motion<T: Animatable>(&mut self, initial: T) -> Motion<T> {
87 self.motion_with(initial, Timing::new(200.0))
88 }
89
90 pub fn motion_with<T: Animatable>(&mut self, initial: T, timing: Timing) -> Motion<T> {
92 self.insert(Tween::with_timing(initial, timing), timing)
93 }
94
95 pub fn insert<T: Animatable>(
97 &mut self,
98 animation: impl Animation<T>,
99 transition: Timing,
100 ) -> Motion<T> {
101 self.insert_with_policy(animation, transition, RetainPolicy::Keep)
102 }
103
104 pub fn play_once<T: Animatable>(&mut self, animation: impl Animation<T>) -> Motion<T> {
106 self.insert_with_policy(animation, Timing::default(), RetainPolicy::DropWhenSettled)
107 }
108
109 pub fn insert_with_policy<T: Animatable>(
111 &mut self,
112 animation: impl Animation<T>,
113 transition: Timing,
114 retain_policy: RetainPolicy,
115 ) -> Motion<T> {
116 let mut animation = TypedAnimation::new(animation);
117 animation.compact();
118 let animation: Box<dyn AnimationDyn> = Box::new(animation);
119 let (id, reused_slot) = if let Some(slot_index) = self.free.pop() {
120 let slot = &mut self.slots[slot_index];
121 slot.generation = slot.generation.wrapping_add(1);
122 slot.playback_revision = 0;
123 slot.terminal_emitted = false;
124 slot.transition = transition;
125 slot.retain_policy = retain_policy;
126 slot.animation = Some(animation);
127 slot.active = false;
128 slot.queued = false;
129 (MotionId::new(slot_index, slot.generation), true)
130 } else {
131 let slot = self.slots.len();
132 self.slots.push(AnimationSlot {
133 generation: 0,
134 playback_revision: 0,
135 terminal_emitted: false,
136 transition,
137 retain_policy,
138 animation: Some(animation),
139 active: false,
140 queued: false,
141 });
142 (MotionId::new(slot, 0), false)
143 };
144
145 self.motion_count += 1;
146 #[cfg(feature = "tracing")]
147 tracing::debug!(
148 target: "aura_anim::runtime",
149 slot = id.slot(),
150 generation = id.generation(),
151 value_type = std::any::type_name::<T>(),
152 ?retain_policy,
153 reused_slot,
154 motion_count = self.motion_count,
155 "inserted motion"
156 );
157 #[cfg(not(feature = "tracing"))]
158 let _ = reused_slot;
159 let motion = Motion::new(id, PhantomData);
160 self.sync_slot_after_change(id);
161 motion
162 }
163
164 pub fn tick(&mut self, delta: impl Into<Duration>) {
166 let delta = delta.into();
167 #[cfg(feature = "tracing")]
168 tracing::trace!(
169 target: "aura_anim::runtime",
170 delta_ms = delta.as_millis(),
171 active_count = self.active_count,
172 queued_count = self.active.len(),
173 "ticking motions"
174 );
175 self.next_active.clear();
176
177 for index in 0..self.active.len() {
178 let id = self.active[index];
179 let settled = {
180 let Some(slot) = self.slots.get_mut(id.slot()) else {
181 continue;
182 };
183 if slot.generation != id.generation() {
184 continue;
185 }
186 slot.queued = false;
187 if !slot.active {
188 continue;
189 }
190 let Some(animation) = slot.animation.as_mut() else {
191 continue;
192 };
193
194 animation.advance(delta);
195 if animation.is_active() {
196 self.next_active.push(id);
197 slot.queued = true;
198 None
199 } else {
200 slot.active = false;
201 Some((animation.state(), slot.retain_policy))
202 }
203 };
204
205 let Some((state, retain_policy)) = settled else {
206 continue;
207 };
208 self.active_count = self.active_count.saturating_sub(1);
209 self.record_terminal(id, state);
210
211 if retain_policy == RetainPolicy::DropWhenSettled
212 && matches!(state, AnimationState::Completed | AnimationState::Canceled)
213 {
214 #[cfg(feature = "tracing")]
215 tracing::debug!(
216 target: "aura_anim::runtime",
217 slot = id.slot(),
218 generation = id.generation(),
219 ?state,
220 "dropping settled transient motion"
221 );
222 self.remove_slot(id, RemovalReason::Settled);
223 continue;
224 }
225
226 #[cfg(feature = "tracing")]
227 tracing::debug!(
228 target: "aura_anim::runtime",
229 slot = id.slot(),
230 generation = id.generation(),
231 ?state,
232 "motion settled"
233 );
234 if let Ok(animation) = self.animation_mut(id) {
235 animation.compact();
236 }
237 }
238
239 std::mem::swap(&mut self.active, &mut self.next_active);
240 if self.active_count == 0 {
241 self.active.clear();
242 self.next_active.clear();
243 self.last_tick = None;
244 }
245 #[cfg(feature = "tracing")]
246 tracing::trace!(
247 target: "aura_anim::runtime",
248 active_count = self.active_count,
249 motion_count = self.motion_count,
250 "finished ticking motions"
251 );
252 }
253
254 pub fn tick_at(&mut self, now: std::time::Instant) {
258 let delta = self.last_tick.map_or(std::time::Duration::ZERO, |last| {
259 now.saturating_duration_since(last)
260 });
261 self.last_tick = Some(now);
262 self.tick(delta);
263 }
264
265 #[must_use]
267 pub fn events(&self) -> &[MotionEvent] {
268 &self.events
269 }
270
271 pub fn take_events(&mut self) -> Vec<MotionEvent> {
275 std::mem::take(&mut self.events)
276 }
277
278 pub fn clear_events(&mut self) {
280 self.events.clear();
281 }
282
283 #[must_use]
285 pub fn pending_event_count(&self) -> usize {
286 self.events.len()
287 }
288
289 #[must_use]
291 pub fn active_count(&self) -> usize {
292 self.active_count
293 }
294
295 #[must_use]
297 pub fn has_active(&self) -> bool {
298 self.active_count > 0
299 }
300
301 #[must_use]
303 pub fn motion_count(&self) -> usize {
304 self.motion_count
305 }
306
307 #[must_use]
309 pub fn slot_capacity(&self) -> usize {
310 self.slots.capacity()
311 }
312
313 pub fn shrink_to_fit(&mut self) {
315 while self
316 .slots
317 .last()
318 .is_some_and(|slot| slot.animation.is_none())
319 {
320 self.slots.pop();
321 }
322 self.free.retain(|slot| *slot < self.slots.len());
323 self.slots.shrink_to_fit();
324 self.free.shrink_to_fit();
325 self.active.shrink_to_fit();
326 self.next_active.shrink_to_fit();
327 self.events.shrink_to_fit();
328 }
329
330 pub fn value<T: Animatable>(&self, motion: Motion<T>) -> Result<&T, MotionError> {
332 let animation = self.animation(motion.id())?;
333 animation.value_any().downcast_ref::<T>().ok_or_else(|| {
334 let error = MotionError::TypeMismatch {
335 expected: std::any::type_name::<T>(),
336 actual: animation.value_type_name(),
337 };
338 #[cfg(feature = "tracing")]
339 trace_motion_error(motion.id(), &error);
340 error
341 })
342 }
343
344 pub fn state<T: Animatable>(&self, motion: Motion<T>) -> Result<AnimationState, MotionError> {
346 self.animation(motion.id()).map(AnimationDyn::state)
347 }
348
349 pub fn playback<T: Animatable>(&self, motion: Motion<T>) -> Result<PlaybackId, MotionError> {
351 self.animation(motion.id())?;
352 Ok(self.current_playback(motion.id()))
353 }
354
355 pub fn is_active<T: Animatable>(&self, motion: Motion<T>) -> Result<bool, MotionError> {
357 self.animation(motion.id()).map(AnimationDyn::is_active)
358 }
359
360 pub fn transition_to<T: Animatable>(
362 &mut self,
363 motion: Motion<T>,
364 target: T,
365 ) -> Result<(), MotionError> {
366 self.transition_to_tracked(motion, target).map(|_| ())
367 }
368
369 pub fn transition_to_tracked<T: Animatable>(
371 &mut self,
372 motion: Motion<T>,
373 target: T,
374 ) -> Result<PlaybackId, MotionError> {
375 let previous_state = self.state(motion)?;
376 if self.animation_mut(motion.id())?.retarget_any(&target) {
377 self.record_interruption(motion.id(), previous_state, InterruptionReason::Retargeted);
378 let playback = self.start_playback(motion.id());
379 #[cfg(feature = "tracing")]
380 tracing::debug!(
381 target: "aura_anim::runtime",
382 slot = motion.id().slot(),
383 generation = motion.id().generation(),
384 value_type = std::any::type_name::<T>(),
385 "retargeted motion in place"
386 );
387 self.sync_slot_after_change(motion.id());
388 return Ok(playback);
389 }
390
391 let current = self.value(motion)?.clone();
392 let transition = self.slots[motion.id().slot()].transition;
393 #[cfg(feature = "tracing")]
394 tracing::debug!(
395 target: "aura_anim::runtime",
396 slot = motion.id().slot(),
397 generation = motion.id().generation(),
398 value_type = std::any::type_name::<T>(),
399 "replacing motion with transition tween"
400 );
401 self.replace(
402 motion.id(),
403 TypedAnimation::new(Tween::between(current, target, transition)),
404 InterruptionReason::Retargeted,
405 )
406 }
407
408 pub fn play<T, P, Kind>(&mut self, motion: Motion<T>, playback: P) -> Result<(), MotionError>
410 where
411 T: Animatable,
412 P: IntoMotionAnimation<T, Kind>,
413 {
414 self.play_tracked(motion, playback).map(|_| ())
415 }
416
417 pub fn play_tracked<T, P, Kind>(
419 &mut self,
420 motion: Motion<T>,
421 playback: P,
422 ) -> Result<PlaybackId, MotionError>
423 where
424 T: Animatable,
425 P: IntoMotionAnimation<T, Kind>,
426 {
427 let animation = playback.into_motion_animation(self.value(motion)?);
428 #[cfg(feature = "tracing")]
429 tracing::debug!(
430 target: "aura_anim::runtime",
431 slot = motion.id().slot(),
432 generation = motion.id().generation(),
433 value_type = std::any::type_name::<T>(),
434 "playing replacement animation"
435 );
436 self.replace(
437 motion.id(),
438 TypedAnimation::new(animation),
439 InterruptionReason::Replaced,
440 )
441 }
442
443 pub fn command<T: Animatable>(
445 &mut self,
446 motion: Motion<T>,
447 command: AnimationCommand,
448 ) -> Result<(), MotionError> {
449 self.animation(motion.id())?;
450 self.apply_command(motion.id(), command);
451 Ok(())
452 }
453
454 pub fn command_all(&mut self, command: AnimationCommand) {
460 #[cfg(feature = "tracing")]
461 tracing::debug!(
462 target: "aura_anim::runtime",
463 ?command,
464 motion_count = self.motion_count,
465 "applying command to all motions"
466 );
467
468 for slot_index in 0..self.slots.len() {
469 let Some(slot) = self.slots.get(slot_index) else {
470 continue;
471 };
472 if slot.animation.is_none() {
473 continue;
474 }
475 let id = MotionId::new(slot_index, slot.generation);
476 self.apply_command(id, command);
477 }
478
479 #[cfg(feature = "tracing")]
480 tracing::debug!(
481 target: "aura_anim::runtime",
482 ?command,
483 active_count = self.active_count,
484 motion_count = self.motion_count,
485 "applied command to all motions"
486 );
487 }
488
489 fn apply_command(&mut self, id: MotionId, command: AnimationCommand) {
490 #[cfg(feature = "tracing")]
491 tracing::debug!(
492 target: "aura_anim::runtime",
493 slot = id.slot(),
494 generation = id.generation(),
495 ?command,
496 "applying motion command"
497 );
498 let (active, state) = {
499 let Some(slot) = self.slots.get_mut(id.slot()) else {
500 return;
501 };
502 if slot.generation != id.generation() {
503 return;
504 }
505 let Some(animation) = slot.animation.as_mut() else {
506 return;
507 };
508 animation.command(command);
509 (animation.is_active(), animation.state())
510 };
511
512 if active {
513 self.activate(id);
514 } else {
515 self.deactivate(id);
516 self.record_terminal(id, state);
517 if self.slots[id.slot()].retain_policy == RetainPolicy::DropWhenSettled
518 && matches!(state, AnimationState::Completed | AnimationState::Canceled)
519 {
520 self.remove_slot(id, RemovalReason::Settled);
521 } else {
522 let Some(animation) = self.slots[id.slot()].animation.as_mut() else {
523 return;
524 };
525 animation.compact();
526 }
527 }
528 #[cfg(feature = "tracing")]
529 tracing::debug!(
530 target: "aura_anim::runtime",
531 slot = id.slot(),
532 generation = id.generation(),
533 ?state,
534 active,
535 "applied motion command"
536 );
537 }
538
539 pub fn remove<T: Animatable>(&mut self, motion: Motion<T>) -> Result<(), MotionError> {
541 let state = self.animation(motion.id())?.state();
542 self.record_interruption(motion.id(), state, InterruptionReason::Removed);
543 self.remove_slot(motion.id(), RemovalReason::Explicit);
544 Ok(())
545 }
546
547 fn current_playback(&self, id: MotionId) -> PlaybackId {
548 PlaybackId::new(id, self.slots[id.slot()].playback_revision)
549 }
550
551 fn start_playback(&mut self, id: MotionId) -> PlaybackId {
552 let slot = &mut self.slots[id.slot()];
553 slot.playback_revision = slot.playback_revision.wrapping_add(1);
554 slot.terminal_emitted = false;
555 PlaybackId::new(id, slot.playback_revision)
556 }
557
558 fn record_terminal(&mut self, id: MotionId, state: AnimationState) {
559 let kind = match state {
560 AnimationState::Completed => MotionEventKind::Completed,
561 AnimationState::Canceled => MotionEventKind::Canceled,
562 AnimationState::Idle | AnimationState::Running | AnimationState::Paused => return,
563 };
564 let playback = {
565 let slot = &mut self.slots[id.slot()];
566 if slot.generation != id.generation() || slot.terminal_emitted {
567 return;
568 }
569 slot.terminal_emitted = true;
570 PlaybackId::new(id, slot.playback_revision)
571 };
572 self.events.push(MotionEvent::new(playback, kind));
573 }
574
575 fn record_interruption(
576 &mut self,
577 id: MotionId,
578 state: AnimationState,
579 reason: InterruptionReason,
580 ) {
581 if !matches!(state, AnimationState::Running | AnimationState::Paused) {
582 return;
583 }
584 let playback = {
585 let slot = &mut self.slots[id.slot()];
586 if slot.generation != id.generation() || slot.terminal_emitted {
587 return;
588 }
589 slot.terminal_emitted = true;
590 PlaybackId::new(id, slot.playback_revision)
591 };
592 self.events.push(MotionEvent::new(
593 playback,
594 MotionEventKind::Interrupted(reason),
595 ));
596 }
597
598 fn sync_slot_after_change(&mut self, id: MotionId) {
599 let Ok(animation) = self.animation(id) else {
600 return;
601 };
602 let active = animation.is_active();
603 let state = animation.state();
604 let retain_policy = self.slots[id.slot()].retain_policy;
605
606 if active {
607 self.activate(id);
608 return;
609 }
610
611 self.deactivate(id);
612 self.record_terminal(id, state);
613 if retain_policy == RetainPolicy::DropWhenSettled
614 && matches!(state, AnimationState::Completed | AnimationState::Canceled)
615 {
616 self.remove_slot(id, RemovalReason::Settled);
617 } else if let Ok(animation) = self.animation_mut(id) {
618 animation.compact();
619 }
620 }
621
622 fn remove_slot(&mut self, id: MotionId, reason: RemovalReason) {
623 let playback = self.current_playback(id);
624 let slot = &mut self.slots[id.slot()];
625 let was_active = slot.active;
626 slot.animation = None;
627 slot.active = false;
628 slot.queued = false;
629 if was_active {
630 self.active_count = self.active_count.saturating_sub(1);
631 }
632 self.motion_count = self.motion_count.saturating_sub(1);
633 self.free.push(id.slot());
634 self.events
635 .push(MotionEvent::new(playback, MotionEventKind::Removed(reason)));
636
637 #[cfg(feature = "tracing")]
638 tracing::debug!(
639 target: "aura_anim::runtime",
640 slot = id.slot(),
641 generation = id.generation(),
642 was_active,
643 motion_count = self.motion_count,
644 ?reason,
645 "removed motion"
646 );
647
648 if self.active_count == 0 {
649 self.active.clear();
650 self.next_active.clear();
651 self.last_tick = None;
652 }
653 }
654
655 fn animation(&self, id: MotionId) -> Result<&dyn AnimationDyn, MotionError> {
656 let Some(slot) = self.slots.get(id.slot()) else {
657 let error = MotionError::SlotOutOfBounds { slot: id.slot() };
658 #[cfg(feature = "tracing")]
659 trace_motion_error(id, &error);
660 return Err(error);
661 };
662 if slot.generation != id.generation() {
663 let error = MotionError::StaleHandle {
664 slot: id.slot(),
665 handle_generation: id.generation(),
666 actual_generation: slot.generation,
667 };
668 #[cfg(feature = "tracing")]
669 trace_motion_error(id, &error);
670 return Err(error);
671 }
672 slot.animation.as_deref().ok_or_else(|| {
673 let error = MotionError::Removed { slot: id.slot() };
674 #[cfg(feature = "tracing")]
675 trace_motion_error(id, &error);
676 error
677 })
678 }
679
680 fn animation_mut(&mut self, id: MotionId) -> Result<&mut (dyn AnimationDyn + '_), MotionError> {
681 let Some(slot) = self.slots.get_mut(id.slot()) else {
682 let error = MotionError::SlotOutOfBounds { slot: id.slot() };
683 #[cfg(feature = "tracing")]
684 trace_motion_error(id, &error);
685 return Err(error);
686 };
687 if slot.generation != id.generation() {
688 let error = MotionError::StaleHandle {
689 slot: id.slot(),
690 handle_generation: id.generation(),
691 actual_generation: slot.generation,
692 };
693 #[cfg(feature = "tracing")]
694 trace_motion_error(id, &error);
695 return Err(error);
696 }
697 if let Some(animation) = slot.animation.as_mut() {
698 Ok(animation.as_mut())
699 } else {
700 let error = MotionError::Removed { slot: id.slot() };
701 #[cfg(feature = "tracing")]
702 trace_motion_error(id, &error);
703 Err(error)
704 }
705 }
706
707 fn replace(
708 &mut self,
709 id: MotionId,
710 animation: TypedAnimation<impl Animatable>,
711 reason: InterruptionReason,
712 ) -> Result<PlaybackId, MotionError> {
713 let previous_state = self.animation(id)?.state();
714 self.record_interruption(id, previous_state, reason);
715 let playback = self.start_playback(id);
716 let slot = &mut self.slots[id.slot()];
717 slot.animation = Some(Box::new(animation));
718 self.sync_slot_after_change(id);
719 Ok(playback)
720 }
721
722 fn activate(&mut self, id: MotionId) {
723 let Some(slot) = self.slots.get_mut(id.slot()) else {
724 return;
725 };
726 if slot.generation != id.generation() || slot.animation.is_none() {
727 return;
728 }
729 if self.active_count == 0 {
730 self.last_tick = None;
731 }
732 if !slot.active {
733 slot.active = true;
734 self.active_count += 1;
735 }
736 if !slot.queued {
737 slot.queued = true;
738 self.active.push(id);
739 }
740 }
741
742 fn deactivate(&mut self, id: MotionId) {
743 let Some(slot) = self.slots.get_mut(id.slot()) else {
744 return;
745 };
746 if slot.generation != id.generation() || !slot.active {
747 return;
748 }
749 slot.active = false;
750 self.active_count = self.active_count.saturating_sub(1);
751 if self.active_count == 0 {
752 for active in &self.active {
753 if let Some(slot) = self.slots.get_mut(active.slot())
754 && slot.generation == active.generation()
755 {
756 slot.queued = false;
757 }
758 }
759 self.active.clear();
760 self.next_active.clear();
761 self.last_tick = None;
762 }
763 }
764}
765
766#[cfg(feature = "tracing")]
767fn trace_motion_error(id: MotionId, error: &MotionError) {
768 tracing::debug!(
769 target: "aura_anim::runtime",
770 slot = id.slot(),
771 generation = id.generation(),
772 error = %error,
773 "motion lookup failed"
774 );
775}
776
777#[cfg(test)]
778mod tests {
779 use super::MotionRuntime;
780 use crate::{
781 Tween,
782 timing::{Duration, Timing},
783 };
784
785 #[test]
786 fn remove_lazily_invalidates_queued_ids() {
787 let mut runtime = MotionRuntime::new();
788 let first = runtime.insert(
789 Tween::between(0.0_f32, 1.0, Timing::new(100.0)),
790 Timing::default(),
791 );
792 let second = runtime.insert(
793 Tween::between(0.0_f32, 1.0, Timing::new(100.0)),
794 Timing::default(),
795 );
796
797 runtime.tick(Duration::ZERO);
798 assert_eq!(runtime.active.len(), 2);
799 assert_eq!(runtime.next_active.len(), 2);
800
801 first.remove(&mut runtime).unwrap();
802 assert_eq!(runtime.active_count(), 1);
803 assert_eq!(runtime.active.len(), 2);
804 assert_eq!(runtime.next_active.len(), 2);
805
806 let replacement = runtime.insert(
807 Tween::between(0.0_f32, 1.0, Timing::new(100.0)),
808 Timing::default(),
809 );
810 assert_eq!(runtime.active_count(), 2);
811 assert_eq!(runtime.active.len(), 3);
812
813 runtime.tick(Duration::ZERO);
814
815 assert_eq!(runtime.active_count(), 2);
816 assert_eq!(runtime.active.len(), 2);
817 assert!(second.is_active(&runtime).unwrap());
818 assert!(replacement.is_active(&runtime).unwrap());
819 }
820}