Skip to main content

oxihuman_morph/
timeline.rs

1// Copyright (C) 2026 COOLJAPAN OU (Team KitaSan)
2// SPDX-License-Identifier: Apache-2.0
3
4#![allow(dead_code)]
5
6use std::collections::HashMap;
7
8/// Interpolation mode between keyframes.
9#[derive(Clone, Debug, PartialEq)]
10pub enum TrackInterp {
11    /// Hold value until next keyframe.
12    Step,
13    /// Linear interpolation between keyframes.
14    Linear,
15    /// Smoothstep interpolation between keyframes.
16    SmoothStep,
17    /// Cubic Catmull-Rom interpolation between keyframes.
18    Cubic,
19}
20
21/// A single keyframe: time (seconds) and scalar value.
22#[derive(Clone, Debug)]
23pub struct Keyframe {
24    /// Time in seconds.
25    pub time: f32,
26    /// Scalar value at this keyframe.
27    pub value: f32,
28}
29
30/// A named animation track holding keyframes for one parameter.
31pub struct AnimTrack {
32    /// Track name (identifies the morph parameter).
33    pub name: String,
34    /// Interpolation mode used when evaluating between keyframes.
35    pub interp: TrackInterp,
36    /// Keyframes sorted by ascending time.
37    keyframes: Vec<Keyframe>,
38}
39
40impl AnimTrack {
41    /// Create a new empty track with the given name and interpolation mode.
42    pub fn new(name: impl Into<String>, interp: TrackInterp) -> Self {
43        Self {
44            name: name.into(),
45            interp,
46            keyframes: Vec::new(),
47        }
48    }
49
50    /// Add a keyframe at `time` with `value`.
51    /// Keyframes are kept sorted by time; if a keyframe at the exact time already
52    /// exists its value is replaced.
53    pub fn add_keyframe(&mut self, time: f32, value: f32) {
54        // Check for exact match and replace.
55        if let Some(kf) = self
56            .keyframes
57            .iter_mut()
58            .find(|k| (k.time - time).abs() < f32::EPSILON)
59        {
60            kf.value = value;
61            return;
62        }
63        let pos = self.keyframes.partition_point(|k| k.time < time);
64        self.keyframes.insert(pos, Keyframe { time, value });
65    }
66
67    /// Remove the keyframe whose time exactly matches `time`.
68    /// Returns `true` if a keyframe was removed.
69    pub fn remove_keyframe(&mut self, time: f32) -> bool {
70        if let Some(pos) = self
71            .keyframes
72            .iter()
73            .position(|k| (k.time - time).abs() < f32::EPSILON)
74        {
75            self.keyframes.remove(pos);
76            true
77        } else {
78            false
79        }
80    }
81
82    /// Number of keyframes in this track.
83    pub fn keyframe_count(&self) -> usize {
84        self.keyframes.len()
85    }
86
87    /// Slice of all keyframes in time order.
88    pub fn keyframes(&self) -> &[Keyframe] {
89        &self.keyframes
90    }
91
92    /// Time of the last keyframe, or `0.0` if the track is empty.
93    pub fn duration(&self) -> f32 {
94        self.keyframes.last().map(|k| k.time).unwrap_or(0.0)
95    }
96
97    /// Evaluate the track at `time` using the configured interpolation mode.
98    pub fn evaluate(&self, time: f32) -> f32 {
99        if self.keyframes.is_empty() {
100            return 0.0;
101        }
102        let first = &self.keyframes[0];
103        let last = &self.keyframes[self.keyframes.len() - 1];
104
105        if time <= first.time {
106            return first.value;
107        }
108        if time >= last.time {
109            return last.value;
110        }
111
112        // Find bracket [i, i+1] such that keyframes[i].time <= time < keyframes[i+1].time.
113        let idx_b = self.keyframes.partition_point(|k| k.time <= time);
114        let idx_a = idx_b - 1;
115
116        let ka = &self.keyframes[idx_a];
117        let kb = &self.keyframes[idx_b];
118
119        let span = kb.time - ka.time;
120        let t = if span > 0.0 {
121            (time - ka.time) / span
122        } else {
123            0.0
124        };
125
126        match self.interp {
127            TrackInterp::Step => ka.value,
128            TrackInterp::Linear => lerp(ka.value, kb.value, t),
129            TrackInterp::SmoothStep => {
130                let s = t * t * (3.0 - 2.0 * t);
131                lerp(ka.value, kb.value, s)
132            }
133            TrackInterp::Cubic => {
134                // Catmull-Rom: use neighbours as tangent guides, clamped at boundaries.
135                let n = self.keyframes.len();
136                let p0 = if idx_a == 0 {
137                    ka.value
138                } else {
139                    self.keyframes[idx_a - 1].value
140                };
141                let p3 = if idx_b + 1 >= n {
142                    kb.value
143                } else {
144                    self.keyframes[idx_b + 1].value
145                };
146                catmull_rom_scalar(p0, ka.value, kb.value, p3, t)
147            }
148        }
149    }
150
151    /// Change the interpolation mode for this track.
152    pub fn set_interp(&mut self, interp: TrackInterp) {
153        self.interp = interp;
154    }
155}
156
157// ---------------------------------------------------------------------------
158// Helper math
159// ---------------------------------------------------------------------------
160
161#[inline]
162fn lerp(a: f32, b: f32, t: f32) -> f32 {
163    a + (b - a) * t
164}
165
166/// Catmull-Rom spline interpolation between p1 and p2 with neighbours p0, p3.
167#[inline]
168fn catmull_rom_scalar(p0: f32, p1: f32, p2: f32, p3: f32, t: f32) -> f32 {
169    let t2 = t * t;
170    let t3 = t2 * t;
171    0.5 * ((2.0 * p1)
172        + (-p0 + p2) * t
173        + (2.0 * p0 - 5.0 * p1 + 4.0 * p2 - p3) * t2
174        + (-p0 + 3.0 * p1 - 3.0 * p2 + p3) * t3)
175}
176
177// ---------------------------------------------------------------------------
178// Timeline
179// ---------------------------------------------------------------------------
180
181/// Multi-track animation timeline.
182pub struct Timeline {
183    /// Human-readable name for this timeline.
184    pub name: String,
185    /// Frames per second (used by `frame_count`).
186    pub fps: f32,
187    tracks: HashMap<String, AnimTrack>,
188}
189
190impl Timeline {
191    /// Create a new timeline with default fps of 24.
192    pub fn new(name: impl Into<String>) -> Self {
193        Self {
194            name: name.into(),
195            fps: 24.0,
196            tracks: HashMap::new(),
197        }
198    }
199
200    /// Create a new timeline with an explicit fps value.
201    pub fn with_fps(name: impl Into<String>, fps: f32) -> Self {
202        Self {
203            name: name.into(),
204            fps,
205            tracks: HashMap::new(),
206        }
207    }
208
209    /// Add a track to the timeline.
210    pub fn add_track(&mut self, track: AnimTrack) {
211        self.tracks.insert(track.name.clone(), track);
212    }
213
214    /// Remove the track named `name`. Returns `true` if a track was removed.
215    pub fn remove_track(&mut self, name: &str) -> bool {
216        self.tracks.remove(name).is_some()
217    }
218
219    /// Get an immutable reference to the named track.
220    pub fn get_track(&self, name: &str) -> Option<&AnimTrack> {
221        self.tracks.get(name)
222    }
223
224    /// Get a mutable reference to the named track.
225    pub fn get_track_mut(&mut self, name: &str) -> Option<&mut AnimTrack> {
226        self.tracks.get_mut(name)
227    }
228
229    /// Sorted list of all track names.
230    pub fn track_names(&self) -> Vec<&str> {
231        let mut names: Vec<&str> = self.tracks.keys().map(String::as_str).collect();
232        names.sort_unstable();
233        names
234    }
235
236    /// Number of tracks in this timeline.
237    pub fn track_count(&self) -> usize {
238        self.tracks.len()
239    }
240
241    /// Maximum duration across all tracks.
242    pub fn duration(&self) -> f32 {
243        self.tracks
244            .values()
245            .map(|t| t.duration())
246            .fold(0.0_f32, f32::max)
247    }
248
249    /// Number of frames: `ceil(duration * fps)`.
250    pub fn frame_count(&self) -> usize {
251        (self.duration() * self.fps).ceil() as usize
252    }
253
254    /// Evaluate all tracks at `time`, returning a map of track name to value.
255    pub fn evaluate(&self, time: f32) -> HashMap<String, f32> {
256        self.tracks
257            .iter()
258            .map(|(name, track)| (name.clone(), track.evaluate(time)))
259            .collect()
260    }
261
262    /// Sample the timeline at uniform intervals of `1.0 / sample_fps` from `t = 0`
263    /// to the timeline duration, returning a `Vec` of name→value snapshots.
264    pub fn bake(&self, sample_fps: f32) -> Vec<HashMap<String, f32>> {
265        let duration = self.duration();
266        if duration <= 0.0 || sample_fps <= 0.0 {
267            return Vec::new();
268        }
269        let dt = 1.0 / sample_fps;
270        let mut frames = Vec::new();
271        let mut t = 0.0_f32;
272        while t <= duration + f32::EPSILON {
273            frames.push(self.evaluate(t));
274            t += dt;
275        }
276        frames
277    }
278
279    /// Add a keyframe to the named track, creating it (with `Linear` interp) if absent.
280    pub fn set_key(&mut self, track_name: &str, time: f32, value: f32) {
281        let track = self
282            .tracks
283            .entry(track_name.to_owned())
284            .or_insert_with(|| AnimTrack::new(track_name, TrackInterp::Linear));
285        track.add_keyframe(time, value);
286    }
287
288    /// Shift all keyframes in every track by `dt` seconds.
289    pub fn time_offset(&mut self, dt: f32) {
290        for track in self.tracks.values_mut() {
291            for kf in track.keyframes.iter_mut() {
292                kf.time += dt;
293            }
294        }
295    }
296
297    /// Scale all keyframe times by `factor` (speed up or slow down the timeline).
298    pub fn time_scale(&mut self, factor: f32) {
299        for track in self.tracks.values_mut() {
300            for kf in track.keyframes.iter_mut() {
301                kf.time *= factor;
302            }
303        }
304    }
305}
306
307impl Default for Timeline {
308    fn default() -> Self {
309        Self::new("untitled")
310    }
311}
312
313// ---------------------------------------------------------------------------
314// Tests
315// ---------------------------------------------------------------------------
316
317#[cfg(test)]
318mod tests {
319    use super::*;
320
321    // ---- AnimTrack tests ----
322
323    #[test]
324    fn test_track_new() {
325        let track = AnimTrack::new("jaw_open", TrackInterp::Linear);
326        assert_eq!(track.name, "jaw_open");
327        assert_eq!(track.interp, TrackInterp::Linear);
328        assert_eq!(track.keyframe_count(), 0);
329    }
330
331    #[test]
332    fn test_track_add_keyframe_sorted() {
333        let mut track = AnimTrack::new("t", TrackInterp::Linear);
334        track.add_keyframe(2.0, 0.8);
335        track.add_keyframe(0.0, 0.0);
336        track.add_keyframe(1.0, 0.5);
337        let kfs = track.keyframes();
338        assert_eq!(kfs.len(), 3);
339        assert!((kfs[0].time - 0.0).abs() < f32::EPSILON);
340        assert!((kfs[1].time - 1.0).abs() < f32::EPSILON);
341        assert!((kfs[2].time - 2.0).abs() < f32::EPSILON);
342        // Replace existing key at t=1.0
343        track.add_keyframe(1.0, 0.9);
344        assert_eq!(track.keyframe_count(), 3);
345        assert!((track.keyframes()[1].value - 0.9).abs() < f32::EPSILON);
346    }
347
348    #[test]
349    fn test_track_evaluate_empty() {
350        let track = AnimTrack::new("empty", TrackInterp::Linear);
351        assert!((track.evaluate(0.5) - 0.0).abs() < f32::EPSILON);
352    }
353
354    #[test]
355    fn test_track_evaluate_single() {
356        let mut track = AnimTrack::new("single", TrackInterp::Linear);
357        track.add_keyframe(1.0, 0.42);
358        assert!((track.evaluate(0.0) - 0.42).abs() < 1e-6);
359        assert!((track.evaluate(1.0) - 0.42).abs() < 1e-6);
360        assert!((track.evaluate(5.0) - 0.42).abs() < 1e-6);
361    }
362
363    #[test]
364    fn test_track_evaluate_linear() {
365        let mut track = AnimTrack::new("lin", TrackInterp::Linear);
366        track.add_keyframe(0.0, 0.0);
367        track.add_keyframe(1.0, 1.0);
368        assert!((track.evaluate(0.5) - 0.5).abs() < 1e-6);
369        assert!((track.evaluate(0.25) - 0.25).abs() < 1e-6);
370        assert!((track.evaluate(0.75) - 0.75).abs() < 1e-6);
371    }
372
373    #[test]
374    fn test_track_evaluate_step() {
375        let mut track = AnimTrack::new("step", TrackInterp::Step);
376        track.add_keyframe(0.0, 0.0);
377        track.add_keyframe(1.0, 1.0);
378        // Before the second key, should return first key's value.
379        assert!((track.evaluate(0.5) - 0.0).abs() < 1e-6);
380        // At or after last key, returns last value.
381        assert!((track.evaluate(1.0) - 1.0).abs() < 1e-6);
382    }
383
384    #[test]
385    fn test_track_evaluate_smoothstep() {
386        let mut track = AnimTrack::new("smooth", TrackInterp::SmoothStep);
387        track.add_keyframe(0.0, 0.0);
388        track.add_keyframe(1.0, 1.0);
389        // smoothstep(0.5) = 0.5^2 * (3 - 2*0.5) = 0.25 * 2 = 0.5
390        let v = track.evaluate(0.5);
391        assert!((v - 0.5).abs() < 1e-5);
392        // smoothstep is slower at edges; value at 0.25 < 0.25 (linear)
393        let v_low = track.evaluate(0.25);
394        assert!(v_low < 0.25 + 1e-5);
395    }
396
397    #[test]
398    fn test_track_duration() {
399        let track = AnimTrack::new("empty", TrackInterp::Linear);
400        assert!((track.duration() - 0.0).abs() < f32::EPSILON);
401
402        let mut track2 = AnimTrack::new("t2", TrackInterp::Linear);
403        track2.add_keyframe(0.0, 0.0);
404        track2.add_keyframe(3.5, 1.0);
405        assert!((track2.duration() - 3.5).abs() < 1e-6);
406    }
407
408    #[test]
409    fn test_track_remove_keyframe() {
410        let mut track = AnimTrack::new("rem", TrackInterp::Linear);
411        track.add_keyframe(0.0, 0.0);
412        track.add_keyframe(1.0, 1.0);
413        assert!(track.remove_keyframe(0.0));
414        assert_eq!(track.keyframe_count(), 1);
415        // Removing non-existent key returns false.
416        assert!(!track.remove_keyframe(99.0));
417    }
418
419    // ---- Timeline tests ----
420
421    #[test]
422    fn test_timeline_new() {
423        let tl = Timeline::new("main");
424        assert_eq!(tl.name, "main");
425        assert!((tl.fps - 24.0).abs() < f32::EPSILON);
426        assert_eq!(tl.track_count(), 0);
427    }
428
429    #[test]
430    fn test_timeline_add_track() {
431        let mut tl = Timeline::new("main");
432        let track = AnimTrack::new("smile", TrackInterp::Linear);
433        tl.add_track(track);
434        assert_eq!(tl.track_count(), 1);
435        assert!(tl.get_track("smile").is_some());
436        assert!(tl.remove_track("smile"));
437        assert_eq!(tl.track_count(), 0);
438        assert!(!tl.remove_track("smile"));
439    }
440
441    #[test]
442    fn test_timeline_evaluate() {
443        let mut tl = Timeline::new("main");
444        let mut track = AnimTrack::new("brow_raise", TrackInterp::Linear);
445        track.add_keyframe(0.0, 0.0);
446        track.add_keyframe(1.0, 1.0);
447        tl.add_track(track);
448        let snapshot = tl.evaluate(0.5);
449        let v = snapshot["brow_raise"];
450        assert!((v - 0.5).abs() < 1e-5);
451    }
452
453    #[test]
454    fn test_timeline_bake() {
455        let mut tl = Timeline::new("bake_test");
456        let mut track = AnimTrack::new("p", TrackInterp::Linear);
457        track.add_keyframe(0.0, 0.0);
458        track.add_keyframe(1.0, 1.0);
459        tl.add_track(track);
460        // At 10 fps → frames at t=0, 0.1, 0.2, …, 1.0 = 11 frames.
461        let frames = tl.bake(10.0);
462        assert_eq!(frames.len(), 11);
463        // First frame value should be 0.0.
464        assert!((frames[0]["p"] - 0.0).abs() < 1e-5);
465        // Last frame value should be 1.0.
466        assert!((frames[10]["p"] - 1.0).abs() < 1e-4);
467    }
468
469    #[test]
470    fn test_timeline_set_key() {
471        let mut tl = Timeline::new("sk");
472        tl.set_key("eye_blink", 0.0, 0.0);
473        tl.set_key("eye_blink", 0.5, 1.0);
474        let track = tl.get_track("eye_blink").expect("should succeed");
475        assert_eq!(track.keyframe_count(), 2);
476        assert_eq!(track.interp, TrackInterp::Linear);
477    }
478
479    #[test]
480    fn test_timeline_time_scale() {
481        let mut tl = Timeline::new("scale");
482        tl.set_key("p", 0.0, 0.0);
483        tl.set_key("p", 2.0, 1.0);
484        assert!((tl.duration() - 2.0).abs() < 1e-5);
485        tl.time_scale(0.5);
486        assert!((tl.duration() - 1.0).abs() < 1e-5);
487    }
488}