ferravex_play/
animation.rs

1//! Scalar/vector tracks, shortest-arc quaternion interpolation, and timed sprite clips.
2#[derive(Clone, Copy, Debug, PartialEq, Eq)]
3pub enum AnimationError {
4    Empty,
5    InvalidKey,
6    UnorderedKeys,
7    InvalidTime,
8}
9#[derive(Clone, Copy, Debug, PartialEq, Eq)]
10pub enum LoopMode {
11    Clamp,
12    Repeat,
13}
14#[derive(Clone, Copy, Debug, PartialEq, Eq)]
15pub enum Interpolation {
16    Step,
17    Linear,
18}
19#[derive(Clone, Copy, Debug, PartialEq)]
20pub struct Keyframe<T> {
21    pub time: f32,
22    pub value: T,
23}
24mod sealed {
25    pub trait Sample {}
26}
27pub trait Sample: sealed::Sample + Copy {
28    fn valid(self) -> bool;
29    fn blend(a: Self, b: Self, amount: f32) -> Self;
30}
31impl sealed::Sample for f32 {}
32impl Sample for f32 {
33    fn valid(self) -> bool {
34        self.is_finite()
35    }
36    fn blend(a: Self, b: Self, amount: f32) -> Self {
37        lerp(a, b, amount)
38    }
39}
40impl sealed::Sample for [f32; 3] {}
41impl Sample for [f32; 3] {
42    fn valid(self) -> bool {
43        self.into_iter().all(f32::is_finite)
44    }
45    fn blend(a: Self, b: Self, amount: f32) -> Self {
46        [
47            lerp(a[0], b[0], amount),
48            lerp(a[1], b[1], amount),
49            lerp(a[2], b[2], amount),
50        ]
51    }
52}
53impl sealed::Sample for [f32; 4] {}
54impl Sample for [f32; 4] {
55    fn valid(self) -> bool {
56        self.into_iter().all(f32::is_finite)
57            && self.into_iter().map(|v| f64::from(v).powi(2)).sum::<f64>() > 1e-20
58    }
59    fn blend(a: Self, b: Self, amount: f32) -> Self {
60        slerp(a, b, amount)
61    }
62}
63
64#[derive(Clone, Debug)]
65pub struct Track<T: Sample> {
66    keys: Vec<Keyframe<T>>,
67    interpolation: Interpolation,
68}
69impl<T: Sample> Track<T> {
70    pub fn new(
71        keys: Vec<Keyframe<T>>,
72        interpolation: Interpolation,
73    ) -> Result<Self, AnimationError> {
74        if keys.is_empty() {
75            return Err(AnimationError::Empty);
76        }
77        if keys
78            .iter()
79            .any(|key| !key.time.is_finite() || key.time < 0.0 || !key.value.valid())
80        {
81            return Err(AnimationError::InvalidKey);
82        }
83        if keys.windows(2).any(|pair| pair[0].time >= pair[1].time) {
84            return Err(AnimationError::UnorderedKeys);
85        }
86        Ok(Self {
87            keys,
88            interpolation,
89        })
90    }
91    pub fn duration(&self) -> f32 {
92        self.keys.last().expect("nonempty track").time
93    }
94    pub fn sample(&self, time: f32, mode: LoopMode) -> Result<T, AnimationError> {
95        let time = playback_time(time, self.duration(), mode)?;
96        let upper = self.keys.partition_point(|key| key.time <= time);
97        if upper == 0 {
98            return Ok(T::blend(self.keys[0].value, self.keys[0].value, 0.0));
99        }
100        let a = &self.keys[upper - 1];
101        if upper == self.keys.len() || self.interpolation == Interpolation::Step {
102            return Ok(T::blend(a.value, a.value, 0.0));
103        }
104        let b = &self.keys[upper];
105        Ok(T::blend(
106            a.value,
107            b.value,
108            (time - a.time) / (b.time - a.time),
109        ))
110    }
111}
112pub type ScalarTrack = Track<f32>;
113pub type Vec3Track = Track<[f32; 3]>;
114/// Quaternion order is [x, y, z, w]. Samples are normalized, including exact keys.
115pub type QuatTrack = Track<[f32; 4]>;
116
117#[derive(Clone, Copy, Debug, PartialEq)]
118pub struct SpriteFrame {
119    pub index: u32,
120    pub duration: f32,
121}
122#[derive(Clone, Debug)]
123pub struct SpriteClip {
124    frames: Vec<SpriteFrame>,
125    ends: Vec<f32>,
126    duration: f32,
127}
128impl SpriteClip {
129    pub fn new(frames: Vec<SpriteFrame>) -> Result<Self, AnimationError> {
130        if frames.is_empty() {
131            return Err(AnimationError::Empty);
132        }
133        let mut duration = 0.0;
134        let mut ends = Vec::with_capacity(frames.len());
135        for frame in &frames {
136            if !frame.duration.is_finite() || frame.duration <= 0.0 {
137                return Err(AnimationError::InvalidKey);
138            }
139            duration += frame.duration;
140            if !duration.is_finite() || ends.last().is_some_and(|previous| *previous >= duration) {
141                return Err(AnimationError::InvalidKey);
142            }
143            ends.push(duration);
144        }
145        Ok(Self {
146            frames,
147            ends,
148            duration,
149        })
150    }
151    pub fn duration(&self) -> f32 {
152        self.duration
153    }
154    pub fn frame(&self, time: f32, mode: LoopMode) -> Result<u32, AnimationError> {
155        let time = playback_time(time, self.duration, mode)?;
156        let index = self
157            .ends
158            .partition_point(|end| *end <= time)
159            .min(self.frames.len() - 1);
160        Ok(self.frames[index].index)
161    }
162}
163fn playback_time(time: f32, duration: f32, mode: LoopMode) -> Result<f32, AnimationError> {
164    if !time.is_finite() {
165        return Err(AnimationError::InvalidTime);
166    }
167    Ok(if mode == LoopMode::Repeat && duration > 0.0 {
168        time.rem_euclid(duration)
169    } else {
170        time.clamp(0.0, duration)
171    })
172}
173fn lerp(a: f32, b: f32, amount: f32) -> f32 {
174    (f64::from(a) * (1.0 - f64::from(amount)) + f64::from(b) * f64::from(amount)) as f32
175}
176fn normalize(q: [f32; 4]) -> [f32; 4] {
177    let length = q
178        .into_iter()
179        .map(|v| f64::from(v).powi(2))
180        .sum::<f64>()
181        .sqrt();
182    q.map(|v| (f64::from(v) / length) as f32)
183}
184fn slerp(a: [f32; 4], b: [f32; 4], amount: f32) -> [f32; 4] {
185    let a = normalize(a);
186    let mut b = normalize(b);
187    let mut dot = a.iter().zip(b).map(|(a, b)| a * b).sum::<f32>();
188    if dot < 0.0 {
189        b = b.map(|v| -v);
190        dot = -dot;
191    }
192    dot = dot.clamp(-1.0, 1.0);
193    let (wa, wb) = if dot > 0.9995 {
194        (1.0 - amount, amount)
195    } else {
196        let angle = dot.acos();
197        (
198            ((1.0 - amount) * angle).sin() / angle.sin(),
199            (amount * angle).sin() / angle.sin(),
200        )
201    };
202    normalize(std::array::from_fn(|i| a[i] * wa + b[i] * wb))
203}
204
205#[cfg(test)]
206mod tests {
207    use super::*;
208    #[test]
209    fn vector_keys_clamp_interpolate_and_repeat() {
210        let track = Vec3Track::new(
211            vec![
212                Keyframe {
213                    time: 0.0,
214                    value: [0.0; 3],
215                },
216                Keyframe {
217                    time: 2.0,
218                    value: [2.0, 4.0, 6.0],
219                },
220            ],
221            Interpolation::Linear,
222        )
223        .unwrap();
224        assert_eq!(track.sample(1.0, LoopMode::Clamp).unwrap(), [1.0, 2.0, 3.0]);
225        assert_eq!(track.sample(-1.0, LoopMode::Clamp).unwrap(), [0.0; 3]);
226        assert_eq!(
227            track.sample(10.0, LoopMode::Clamp).unwrap(),
228            [2.0, 4.0, 6.0]
229        );
230        assert_eq!(
231            track.sample(3.0, LoopMode::Repeat).unwrap(),
232            [1.0, 2.0, 3.0]
233        );
234        assert_eq!(
235            track.sample(f32::NAN, LoopMode::Clamp),
236            Err(AnimationError::InvalidTime)
237        );
238    }
239    #[test]
240    fn quaternion_uses_shortest_arc_and_normalizes_keys() {
241        let track = QuatTrack::new(
242            vec![
243                Keyframe {
244                    time: 0.0,
245                    value: [0.0, 0.0, 0.0, 2.0],
246                },
247                Keyframe {
248                    time: 1.0,
249                    value: [0.0, 0.0, 0.0, -3.0],
250                },
251            ],
252            Interpolation::Linear,
253        )
254        .unwrap();
255        for time in [0.0, 0.5, 1.0] {
256            assert!((track.sample(time, LoopMode::Clamp).unwrap()[3].abs() - 1.0).abs() < 1e-6);
257        }
258        let half_turn = QuatTrack::new(
259            vec![
260                Keyframe {
261                    time: 0.0,
262                    value: [0.0, 0.0, 0.0, 1.0],
263                },
264                Keyframe {
265                    time: 1.0,
266                    value: [0.0, 0.0, 1.0, 0.0],
267                },
268            ],
269            Interpolation::Linear,
270        )
271        .unwrap();
272        let q = half_turn.sample(0.5, LoopMode::Clamp).unwrap();
273        assert!((q[2] - std::f32::consts::FRAC_1_SQRT_2).abs() < 1e-5);
274        assert!((q[3] - std::f32::consts::FRAC_1_SQRT_2).abs() < 1e-5);
275    }
276    #[test]
277    fn sprite_frames_use_individual_durations_and_exact_boundaries() {
278        let clip = SpriteClip::new(vec![
279            SpriteFrame {
280                index: 4,
281                duration: 0.25,
282            },
283            SpriteFrame {
284                index: 7,
285                duration: 0.75,
286            },
287        ])
288        .unwrap();
289        assert_eq!(clip.frame(0.24, LoopMode::Clamp).unwrap(), 4);
290        assert_eq!(clip.frame(0.25, LoopMode::Clamp).unwrap(), 7);
291        assert_eq!(clip.frame(1.0, LoopMode::Clamp).unwrap(), 7);
292        assert_eq!(clip.frame(1.0, LoopMode::Repeat).unwrap(), 4);
293    }
294    #[test]
295    fn malformed_keys_and_zero_quaternions_are_rejected() {
296        assert!(matches!(
297            QuatTrack::new(
298                vec![Keyframe {
299                    time: 0.0,
300                    value: [0.0; 4]
301                }],
302                Interpolation::Linear
303            ),
304            Err(AnimationError::InvalidKey)
305        ));
306        assert!(matches!(
307            ScalarTrack::new(
308                vec![
309                    Keyframe {
310                        time: 1.0,
311                        value: 1.0
312                    },
313                    Keyframe {
314                        time: 1.0,
315                        value: 2.0
316                    }
317                ],
318                Interpolation::Linear
319            ),
320            Err(AnimationError::UnorderedKeys)
321        ));
322        assert!(matches!(
323            SpriteClip::new(vec![SpriteFrame {
324                index: 0,
325                duration: 0.0
326            }]),
327            Err(AnimationError::InvalidKey)
328        ));
329    }
330}