Skip to main content

viewport_lib/runtime/plugins/animation/
plugin.rs

1//! AnimationPlugin: keyframed and procedural transform animation.
2
3use crate::interaction::selection::NodeId;
4use crate::runtime::context::RuntimeStepContext;
5use crate::runtime::plugin::{phase, RuntimePlugin};
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 { (time - a.time) / span } else { 0.0 };
64
65        let (sa, ra, ta) = a.transform.to_scale_rotation_translation();
66        let (sb, rb, tb) = b.transform.to_scale_rotation_translation();
67
68        let s = sa.lerp(sb, t);
69        let r = ra.slerp(rb, t);
70        let p = ta.lerp(tb, t);
71
72        Some(glam::Affine3A::from_scale_rotation_translation(s, r, p))
73    }
74}
75
76/// A plugin that drives node transforms from keyframed animation tracks.
77///
78/// Runs in the [`RuntimePhase::Animate`] phase. Each frame it advances the
79/// playhead by `dt * speed` and writes the interpolated transform for each
80/// track via [`crate::TransformWriteback`].
81///
82/// # Example
83///
84/// ```rust,ignore
85/// use viewport_lib::{AnimationPlugin, AnimationTrack, Keyframe, ViewportRuntime};
86///
87/// let mut anim = AnimationPlugin::new();
88/// anim.add_track(AnimationTrack {
89///     node_id: my_node,
90///     keyframes: vec![
91///         Keyframe { time: 0.0, transform: start_transform },
92///         Keyframe { time: 2.0, transform: end_transform },
93///     ],
94///     looping: true,
95/// });
96///
97/// let runtime = ViewportRuntime::new().with_plugin(anim);
98/// ```
99pub struct AnimationPlugin {
100    tracks: Vec<AnimationTrack>,
101    time: f32,
102    playing: bool,
103    speed: f32,
104}
105
106impl Default for AnimationPlugin {
107    fn default() -> Self {
108        Self::new()
109    }
110}
111
112impl AnimationPlugin {
113    /// Create a new AnimationPlugin with no tracks. Starts playing at speed 1.
114    pub fn new() -> Self {
115        Self {
116            tracks: Vec::new(),
117            time: 0.0,
118            playing: true,
119            speed: 1.0,
120        }
121    }
122
123    /// Add an animation track.
124    pub fn add_track(&mut self, track: AnimationTrack) {
125        self.tracks.push(track);
126    }
127
128    /// Start or resume playback.
129    pub fn play(&mut self) {
130        self.playing = true;
131    }
132
133    /// Pause playback. Transforms remain at the current time.
134    pub fn pause(&mut self) {
135        self.playing = false;
136    }
137
138    /// Reset the playhead to time zero.
139    pub fn reset(&mut self) {
140        self.time = 0.0;
141    }
142
143    /// Set playback speed. Negative values play backward.
144    pub fn set_speed(&mut self, speed: f32) {
145        self.speed = speed;
146    }
147
148    /// Current playhead position in seconds.
149    pub fn time(&self) -> f32 {
150        self.time
151    }
152
153    /// True if the plugin is currently advancing the playhead each step.
154    pub fn is_playing(&self) -> bool {
155        self.playing
156    }
157
158    /// Duration of the longest track in seconds.
159    pub fn duration(&self) -> f32 {
160        self.tracks
161            .iter()
162            .map(|t| t.duration())
163            .fold(0.0_f32, f32::max)
164    }
165}
166
167impl RuntimePlugin for AnimationPlugin {
168    fn priority(&self) -> i32 {
169        phase::ANIMATE
170    }
171
172    fn step(&mut self, ctx: &mut RuntimeStepContext<'_>) {
173        if self.playing {
174            self.time += ctx.dt * self.speed;
175        }
176        for track in &self.tracks {
177            if let Some(transform) = track.sample(self.time) {
178                ctx.writeback.set(track.node_id, transform);
179            }
180        }
181    }
182}