Skip to main content

viewport_lib/plugins/animation/
plugin.rs

1//! AnimationPlugin: keyframed and procedural transform animation.
2
3use crate::interaction::select::selection::NodeId;
4use crate::runtime::context::RuntimeStepContext;
5use crate::runtime::plugin::{RuntimePlugin, phase};
6
7/// A single keyframe: a time value and the transform at that time.
8#[derive(Debug, Clone)]
9#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
10pub struct Keyframe {
11    /// Time in seconds from track start.
12    pub time: f32,
13    /// Transform at this keyframe.
14    pub transform: glam::Affine3A,
15}
16
17/// An animation track: a sequence of keyframes for one scene node.
18///
19/// Keyframes must be sorted by ascending time. Call
20/// [`AnimationPlugin::add_track`] to register the track.
21#[derive(Debug, Clone)]
22pub struct AnimationTrack {
23    /// Node this track drives.
24    pub node_id: NodeId,
25    /// Keyframes sorted by time.
26    pub keyframes: Vec<Keyframe>,
27    /// If true the track wraps when the playhead passes the last keyframe time.
28    pub looping: bool,
29}
30
31impl AnimationTrack {
32    /// Duration of the track (time of the last keyframe).
33    pub fn duration(&self) -> f32 {
34        self.keyframes.last().map_or(0.0, |k| k.time)
35    }
36
37    /// Sample the track at `time`, returning the interpolated transform.
38    ///
39    /// Returns `None` if the track has no keyframes.
40    pub fn sample(&self, mut time: f32) -> Option<glam::Affine3A> {
41        if self.keyframes.is_empty() {
42            return None;
43        }
44        if self.keyframes.len() == 1 {
45            return Some(self.keyframes[0].transform);
46        }
47
48        let dur = self.duration();
49        if self.looping && dur > 1e-6 {
50            time = time.rem_euclid(dur);
51        } else {
52            time = time.clamp(0.0, dur);
53        }
54
55        // Find the pair of keyframes that bracket `time`.
56        let upper = self.keyframes.partition_point(|k| k.time <= time);
57        let idx = upper.saturating_sub(1).min(self.keyframes.len() - 2);
58
59        let a = &self.keyframes[idx];
60        let b = &self.keyframes[idx + 1];
61
62        let span = b.time - a.time;
63        let t = if span > 1e-6 {
64            (time - a.time) / span
65        } else {
66            0.0
67        };
68
69        let (sa, ra, ta) = a.transform.to_scale_rotation_translation();
70        let (sb, rb, tb) = b.transform.to_scale_rotation_translation();
71
72        let s = sa.lerp(sb, t);
73        let r = ra.slerp(rb, t);
74        let p = ta.lerp(tb, t);
75
76        Some(glam::Affine3A::from_scale_rotation_translation(s, r, p))
77    }
78}
79
80/// A plugin that drives node transforms from keyframed animation tracks.
81///
82/// Runs in the [`RuntimePhase::Animate`] phase. Each frame it advances the
83/// playhead by `dt * speed` and writes the interpolated transform for each
84/// track via [`crate::TransformWriteback`].
85///
86/// # Example
87///
88/// ```rust,ignore
89/// use viewport_lib::{AnimationPlugin, AnimationTrack, Keyframe, ViewportRuntime};
90///
91/// let mut anim = AnimationPlugin::new();
92/// anim.add_track(AnimationTrack {
93///     node_id: my_node,
94///     keyframes: vec![
95///         Keyframe { time: 0.0, transform: start_transform },
96///         Keyframe { time: 2.0, transform: end_transform },
97///     ],
98///     looping: true,
99/// });
100///
101/// let runtime = ViewportRuntime::new().with_plugin(anim);
102/// ```
103pub struct AnimationPlugin {
104    tracks: Vec<AnimationTrack>,
105    time: f32,
106    playing: bool,
107    speed: f32,
108}
109
110impl Default for AnimationPlugin {
111    fn default() -> Self {
112        Self::new()
113    }
114}
115
116impl AnimationPlugin {
117    /// Create a new AnimationPlugin with no tracks. Starts playing at speed 1.
118    pub fn new() -> Self {
119        Self {
120            tracks: Vec::new(),
121            time: 0.0,
122            playing: true,
123            speed: 1.0,
124        }
125    }
126
127    /// Add an animation track.
128    pub fn add_track(&mut self, track: AnimationTrack) {
129        self.tracks.push(track);
130    }
131
132    /// Start or resume playback.
133    pub fn play(&mut self) {
134        self.playing = true;
135    }
136
137    /// Pause playback. Transforms remain at the current time.
138    pub fn pause(&mut self) {
139        self.playing = false;
140    }
141
142    /// Reset the playhead to time zero.
143    pub fn reset(&mut self) {
144        self.time = 0.0;
145    }
146
147    /// Set playback speed. Negative values play backward.
148    pub fn set_speed(&mut self, speed: f32) {
149        self.speed = speed;
150    }
151
152    /// Current playhead position in seconds.
153    pub fn time(&self) -> f32 {
154        self.time
155    }
156
157    /// True if the plugin is currently advancing the playhead each step.
158    pub fn is_playing(&self) -> bool {
159        self.playing
160    }
161
162    /// Duration of the longest track in seconds.
163    pub fn duration(&self) -> f32 {
164        self.tracks
165            .iter()
166            .map(|t| t.duration())
167            .fold(0.0_f32, f32::max)
168    }
169}
170
171impl RuntimePlugin for AnimationPlugin {
172    fn priority(&self) -> i32 {
173        phase::ANIMATE
174    }
175
176    fn step(&mut self, ctx: &mut RuntimeStepContext<'_>) {
177        if self.playing {
178            self.time += ctx.dt * self.speed;
179        }
180        for track in &self.tracks {
181            if let Some(transform) = track.sample(self.time) {
182                ctx.writeback.set(track.node_id, transform);
183            }
184        }
185    }
186}