Skip to main content

animato_devtools/
inspector.rs

1//! Timeline inspection state.
2
3use animato_core::{AnimationKind, Easing, PlaybackState};
4use animato_driver::{AnimationDriver, AnimationId};
5
6/// Snapshot of one running animation for DevTools rendering.
7#[derive(Clone, Debug, PartialEq)]
8pub struct AnimationSnapshot {
9    /// Stable animation id returned by the driver.
10    pub id: AnimationId,
11    /// Optional user-facing label.
12    pub label: Option<String>,
13    /// High-level animation category.
14    pub kind: AnimationKind,
15    /// Normalized progress in `[0.0, 1.0]`.
16    pub progress: f32,
17    /// Elapsed seconds.
18    pub elapsed: f32,
19    /// Finite duration in seconds, if known.
20    pub duration: Option<f32>,
21    /// Coarse playback state.
22    pub state: PlaybackState,
23    /// Active easing curve, if applicable.
24    pub easing: Option<Easing>,
25}
26
27impl AnimationSnapshot {
28    /// Return a stable color name for the animation kind.
29    pub fn color_name(&self) -> &'static str {
30        match self.kind {
31            AnimationKind::Tween => "blue",
32            AnimationKind::Spring => "green",
33            AnimationKind::Keyframe => "violet",
34            AnimationKind::Timeline => "amber",
35            AnimationKind::Group => "cyan",
36            AnimationKind::Custom => "gray",
37        }
38    }
39
40    /// Render an ASCII progress bar with a stable width.
41    pub fn progress_bar(&self, width: usize) -> String {
42        let width = width.max(1);
43        let filled = ((self.progress.clamp(0.0, 1.0) * width as f32).round() as usize).min(width);
44        let mut out = String::with_capacity(width);
45        out.extend(core::iter::repeat_n('#', filled));
46        out.extend(core::iter::repeat_n('-', width - filled));
47        out
48    }
49}
50
51/// Live animation timeline inspector.
52#[derive(Clone, Debug, Default, PartialEq)]
53pub struct TimelineInspector {
54    snapshots: Vec<AnimationSnapshot>,
55    completed_count: usize,
56}
57
58impl TimelineInspector {
59    /// Create an empty inspector.
60    pub fn new() -> Self {
61        Self::default()
62    }
63
64    /// Capture all inspectable animations from a driver.
65    pub fn capture(&mut self, driver: &AnimationDriver) {
66        self.snapshots.clear();
67        self.snapshots
68            .extend(driver.snapshots().into_iter().map(|snapshot| {
69                let introspection = snapshot.introspection;
70                AnimationSnapshot {
71                    id: snapshot.id,
72                    label: snapshot.label,
73                    kind: introspection.kind,
74                    progress: introspection.progress,
75                    elapsed: introspection.elapsed,
76                    duration: introspection.duration,
77                    state: introspection.state,
78                    easing: introspection.easing,
79                }
80            }));
81        self.completed_count = driver.completed_count();
82    }
83
84    /// Write snapshots into a reusable output buffer.
85    pub fn capture_into(&self, out: &mut Vec<AnimationSnapshot>) {
86        out.clear();
87        out.extend_from_slice(&self.snapshots);
88    }
89
90    /// Captured snapshots.
91    pub fn snapshots(&self) -> &[AnimationSnapshot] {
92        &self.snapshots
93    }
94
95    /// Number of currently active inspectable animations.
96    pub fn active_count(&self) -> usize {
97        self.snapshots
98            .iter()
99            .filter(|snapshot| snapshot.state != PlaybackState::Complete)
100            .count()
101    }
102
103    /// Number of animations completed by the driver.
104    pub fn completed_count(&self) -> usize {
105        self.completed_count
106    }
107}
108
109#[cfg(test)]
110mod tests {
111    use super::*;
112    use animato_core::{AnimationKind, PlaybackState};
113    use animato_driver::AnimationDriver;
114    use animato_tween::Tween;
115
116    #[test]
117    fn captures_driver_snapshots() {
118        let mut driver = AnimationDriver::new();
119        driver.add_inspectable("fade", Tween::new(0.0_f32, 1.0).duration(1.0).build());
120        driver.tick(0.25);
121
122        let mut inspector = TimelineInspector::new();
123        inspector.capture(&driver);
124
125        assert_eq!(inspector.snapshots().len(), 1);
126        let snapshot = &inspector.snapshots()[0];
127        assert_eq!(snapshot.label.as_deref(), Some("fade"));
128        assert_eq!(snapshot.kind, AnimationKind::Tween);
129        assert_eq!(snapshot.state, PlaybackState::Playing);
130        assert!((snapshot.progress - 0.25).abs() < 0.001);
131        assert_eq!(snapshot.progress_bar(4), "#---");
132    }
133
134    #[test]
135    fn reports_completed_count() {
136        let mut driver = AnimationDriver::new();
137        driver.add_inspectable("fade", Tween::new(0.0_f32, 1.0).duration(0.1).build());
138        driver.tick(1.0);
139
140        let mut inspector = TimelineInspector::new();
141        inspector.capture(&driver);
142        assert_eq!(inspector.completed_count(), 1);
143        assert_eq!(inspector.active_count(), 0);
144    }
145}