Skip to main content

embedded_3dgfx/
transform_anim.rs

1//! Keyframed rigid-body transform animation (position, euler rotation, scale).
2//!
3//! Designed for boot logos and menu transitions. Store keyframes in flash as `const` data
4//! and drive meshes with [`AnimationPlayer`].
5
6use crate::mesh::K3dMesh;
7use crate::tween::lerp;
8use nalgebra::Point3;
9
10/// One keyframe of object transform data.
11#[derive(Debug, Clone, Copy)]
12pub struct TransformKeyframe {
13    pub time: f32,
14    pub position: [f32; 3],
15    /// Euler angles in radians: roll (X), pitch (Y), yaw (Z) — passed to [`K3dMesh::set_attitude`].
16    pub roll: f32,
17    pub pitch: f32,
18    pub yaw: f32,
19    pub scale: f32,
20}
21
22impl TransformKeyframe {
23    pub const fn new(
24        time: f32,
25        position: [f32; 3],
26        roll: f32,
27        pitch: f32,
28        yaw: f32,
29        scale: f32,
30    ) -> Self {
31        Self {
32            time,
33            position,
34            roll,
35            pitch,
36            yaw,
37            scale,
38        }
39    }
40}
41
42/// Sampled transform at a point in time.
43#[derive(Debug, Clone, Copy, PartialEq)]
44pub struct SampledTransform {
45    pub position: [f32; 3],
46    pub roll: f32,
47    pub pitch: f32,
48    pub yaw: f32,
49    pub scale: f32,
50}
51
52impl SampledTransform {
53    /// Apply this transform to a mesh.
54    pub fn apply_to(&self, mesh: &mut K3dMesh<'_>) {
55        mesh.set_position(self.position[0], self.position[1], self.position[2]);
56        mesh.set_attitude(self.roll, self.pitch, self.yaw);
57        mesh.set_scale(self.scale);
58    }
59
60    /// Apply position only to a camera.
61    pub fn apply_position_to_camera(&self, camera: &mut crate::camera::Camera) {
62        camera.set_position(Point3::new(
63            self.position[0],
64            self.position[1],
65            self.position[2],
66        ));
67    }
68}
69
70/// Keyframed transform track (heapless — keyframes live in `const` or static storage).
71#[derive(Debug)]
72pub struct TransformTrack<'a> {
73    keyframes: &'a [TransformKeyframe],
74    looping: bool,
75}
76
77impl<'a> TransformTrack<'a> {
78    pub fn new(keyframes: &'a [TransformKeyframe], looping: bool) -> Self {
79        assert!(
80            !keyframes.is_empty(),
81            "TransformTrack requires at least one keyframe"
82        );
83        Self { keyframes, looping }
84    }
85
86    pub fn duration(&self) -> f32 {
87        self.keyframes.last().map(|k| k.time).unwrap_or(0.0)
88    }
89
90    pub fn keyframe_count(&self) -> usize {
91        self.keyframes.len()
92    }
93
94    pub fn is_looping(&self) -> bool {
95        self.looping
96    }
97
98    /// Sample the track at `time` (seconds).
99    pub fn sample(&self, time: f32) -> SampledTransform {
100        if self.keyframes.len() == 1 {
101            return keyframe_to_sampled(self.keyframes[0]);
102        }
103
104        let duration = self.duration();
105        let t = if self.looping {
106            if duration > 0.0 { time % duration } else { 0.0 }
107        } else {
108            time.clamp(0.0, duration)
109        };
110
111        let mut kf1_idx = 0usize;
112        let mut kf2_idx = 0usize;
113
114        for (i, kf) in self.keyframes.iter().enumerate() {
115            if kf.time <= t {
116                kf1_idx = i;
117            }
118            if kf.time >= t {
119                kf2_idx = i;
120                break;
121            }
122        }
123
124        if kf1_idx == self.keyframes.len() - 1 {
125            return keyframe_to_sampled(self.keyframes[kf1_idx]);
126        }
127
128        let kf1 = self.keyframes[kf1_idx];
129        let kf2 = self.keyframes[kf2_idx];
130
131        let alpha = if kf2.time > kf1.time {
132            (t - kf1.time) / (kf2.time - kf1.time)
133        } else {
134            0.0
135        };
136
137        SampledTransform {
138            position: [
139                lerp(kf1.position[0], kf2.position[0], alpha),
140                lerp(kf1.position[1], kf2.position[1], alpha),
141                lerp(kf1.position[2], kf2.position[2], alpha),
142            ],
143            roll: lerp(kf1.roll, kf2.roll, alpha),
144            pitch: lerp(kf1.pitch, kf2.pitch, alpha),
145            yaw: lerp(kf1.yaw, kf2.yaw, alpha),
146            scale: lerp(kf1.scale, kf2.scale, alpha),
147        }
148    }
149}
150
151fn keyframe_to_sampled(kf: TransformKeyframe) -> SampledTransform {
152    SampledTransform {
153        position: kf.position,
154        roll: kf.roll,
155        pitch: kf.pitch,
156        yaw: kf.yaw,
157        scale: kf.scale,
158    }
159}
160
161/// Playback state for a [`TransformTrack`].
162#[derive(Debug)]
163pub struct AnimationPlayer<'a> {
164    track: TransformTrack<'a>,
165    time: f32,
166    playing: bool,
167    speed: f32,
168}
169
170impl<'a> AnimationPlayer<'a> {
171    pub fn new(track: TransformTrack<'a>) -> Self {
172        Self {
173            track,
174            time: 0.0,
175            playing: true,
176            speed: 1.0,
177        }
178    }
179
180    pub fn with_speed(mut self, speed: f32) -> Self {
181        self.speed = speed;
182        self
183    }
184
185    pub fn set_playing(&mut self, playing: bool) {
186        self.playing = playing;
187    }
188
189    pub fn is_playing(&self) -> bool {
190        self.playing
191    }
192
193    pub fn reset(&mut self) {
194        self.time = 0.0;
195    }
196
197    pub fn set_time(&mut self, time: f32) {
198        self.time = time;
199    }
200
201    pub fn time(&self) -> f32 {
202        self.time
203    }
204
205    pub fn track(&self) -> &TransformTrack<'a> {
206        &self.track
207    }
208
209    pub fn advance(&mut self, dt: f32) {
210        if self.playing && dt > 0.0 {
211            self.time += dt * self.speed;
212        }
213    }
214
215    pub fn sample(&self) -> SampledTransform {
216        self.track.sample(self.time)
217    }
218
219    pub fn apply_to(&self, mesh: &mut K3dMesh<'_>) {
220        self.sample().apply_to(mesh);
221    }
222
223    /// `true` when non-looping track has passed the last keyframe time.
224    pub fn is_done(&self) -> bool {
225        !self.track.is_looping() && self.time >= self.track.duration()
226    }
227}
228
229#[cfg(test)]
230mod tests {
231    extern crate std;
232
233    use super::*;
234
235    const TRACK: &[TransformKeyframe] = &[
236        TransformKeyframe::new(0.0, [0.0, -2.0, 0.0], 0.0, 0.0, 0.0, 1.0),
237        TransformKeyframe::new(1.0, [0.0, 0.0, 0.0], 0.0, 0.0, 3.14, 1.0),
238    ];
239
240    #[test]
241    fn test_track_interpolation() {
242        let track = TransformTrack::new(TRACK, false);
243        let mid = track.sample(0.5);
244        assert!((mid.position[1] - (-1.0)).abs() < 1e-5);
245        assert!((mid.yaw - 1.57).abs() < 0.1);
246    }
247
248    #[test]
249    fn test_player_done() {
250        let track = TransformTrack::new(TRACK, false);
251        let mut player = AnimationPlayer::new(track);
252        player.advance(1.5);
253        assert!(player.is_done());
254    }
255
256    #[test]
257    fn test_looping_track() {
258        const LOOP_TRACK: &[TransformKeyframe] = &[
259            TransformKeyframe::new(0.0, [0.0, 0.0, 0.0], 0.0, 0.0, 0.0, 1.0),
260            TransformKeyframe::new(1.0, [1.0, 0.0, 0.0], 0.0, 0.0, 0.0, 1.0),
261        ];
262        let track = TransformTrack::new(LOOP_TRACK, true);
263        let s = track.sample(1.5);
264        assert!((s.position[0] - 0.5).abs() < 1e-5);
265    }
266}