Skip to main content

oxihuman_core/
animation_curve.rs

1// Copyright (C) 2026 COOLJAPAN OU (Team KitaSan)
2// SPDX-License-Identifier: Apache-2.0
3
4//! Keyframe animation curve with lerp/cubic interpolation.
5
6#![allow(dead_code)]
7
8/// A single keyframe: (time, value).
9#[allow(dead_code)]
10#[derive(Debug, Clone, Copy, PartialEq)]
11pub struct Keyframe {
12    pub time: f32,
13    pub value: f32,
14    /// Optional in-tangent for Hermite interpolation.
15    pub tan_in: f32,
16    /// Optional out-tangent for Hermite interpolation.
17    pub tan_out: f32,
18}
19
20impl Keyframe {
21    pub fn new(time: f32, value: f32) -> Self {
22        Self {
23            time,
24            value,
25            tan_in: 0.0,
26            tan_out: 0.0,
27        }
28    }
29    pub fn with_tangents(time: f32, value: f32, tan_in: f32, tan_out: f32) -> Self {
30        Self {
31            time,
32            value,
33            tan_in,
34            tan_out,
35        }
36    }
37}
38
39/// Interpolation mode between keyframes.
40#[allow(dead_code)]
41#[derive(Debug, Clone, Copy, PartialEq, Eq)]
42pub enum InterpMode {
43    Linear,
44    Cubic, // Hermite
45    Step,
46}
47
48/// Animation curve: sorted list of keyframes.
49#[allow(dead_code)]
50#[derive(Debug, Clone)]
51pub struct AnimCurve {
52    pub keyframes: Vec<Keyframe>,
53    pub interp: InterpMode,
54}
55
56#[allow(dead_code)]
57impl AnimCurve {
58    pub fn new(interp: InterpMode) -> Self {
59        Self {
60            keyframes: Vec::new(),
61            interp,
62        }
63    }
64
65    /// Insert a keyframe (maintained sorted by time).
66    pub fn insert(&mut self, kf: Keyframe) {
67        let pos = self.keyframes.partition_point(|k| k.time <= kf.time);
68        self.keyframes.insert(pos, kf);
69    }
70
71    /// Remove all keyframes.
72    pub fn clear(&mut self) {
73        self.keyframes.clear();
74    }
75
76    /// Evaluate the curve at time t.
77    pub fn evaluate(&self, t: f32) -> f32 {
78        let kfs = &self.keyframes;
79        if kfs.is_empty() {
80            return 0.0;
81        }
82        if t <= kfs[0].time {
83            return kfs[0].value;
84        }
85        if t >= kfs[kfs.len() - 1].time {
86            return kfs[kfs.len() - 1].value;
87        }
88        // Find the segment
89        let i = kfs.partition_point(|k| k.time <= t) - 1;
90        let k0 = &kfs[i];
91        let k1 = &kfs[i + 1];
92        let dt = k1.time - k0.time;
93        if dt < 1e-10 {
94            return k0.value;
95        }
96        let s = (t - k0.time) / dt;
97        match self.interp {
98            InterpMode::Step => k0.value,
99            InterpMode::Linear => k0.value + s * (k1.value - k0.value),
100            InterpMode::Cubic => {
101                // Hermite spline with stored tangents
102                let h00 = 2.0 * s * s * s - 3.0 * s * s + 1.0;
103                let h10 = s * s * s - 2.0 * s * s + s;
104                let h01 = -2.0 * s * s * s + 3.0 * s * s;
105                let h11 = s * s * s - s * s;
106                h00 * k0.value + h10 * dt * k0.tan_out + h01 * k1.value + h11 * dt * k1.tan_in
107            }
108        }
109    }
110
111    /// Duration of the curve (time of last minus first keyframe).
112    pub fn duration(&self) -> f32 {
113        if self.keyframes.len() < 2 {
114            return 0.0;
115        }
116        self.keyframes[self.keyframes.len() - 1].time - self.keyframes[0].time
117    }
118
119    pub fn keyframe_count(&self) -> usize {
120        self.keyframes.len()
121    }
122}
123
124#[cfg(test)]
125mod tests {
126    use super::*;
127
128    fn simple_linear() -> AnimCurve {
129        let mut c = AnimCurve::new(InterpMode::Linear);
130        c.insert(Keyframe::new(0.0, 0.0));
131        c.insert(Keyframe::new(1.0, 10.0));
132        c
133    }
134
135    #[test]
136    fn evaluate_at_start() {
137        let c = simple_linear();
138        assert!((c.evaluate(0.0) - 0.0).abs() < 1e-5);
139    }
140
141    #[test]
142    fn evaluate_at_end() {
143        let c = simple_linear();
144        assert!((c.evaluate(1.0) - 10.0).abs() < 1e-5);
145    }
146
147    #[test]
148    fn evaluate_linear_midpoint() {
149        let c = simple_linear();
150        assert!((c.evaluate(0.5) - 5.0).abs() < 1e-4);
151    }
152
153    #[test]
154    fn evaluate_before_start_clamps() {
155        let c = simple_linear();
156        assert!((c.evaluate(-1.0) - 0.0).abs() < 1e-5);
157    }
158
159    #[test]
160    fn evaluate_after_end_clamps() {
161        let c = simple_linear();
162        assert!((c.evaluate(2.0) - 10.0).abs() < 1e-5);
163    }
164
165    #[test]
166    fn duration() {
167        let c = simple_linear();
168        assert!((c.duration() - 1.0).abs() < 1e-5);
169    }
170
171    #[test]
172    fn step_mode_holds_value() {
173        let mut c = AnimCurve::new(InterpMode::Step);
174        c.insert(Keyframe::new(0.0, 5.0));
175        c.insert(Keyframe::new(1.0, 10.0));
176        // At 0.5 should hold first value
177        assert!((c.evaluate(0.5) - 5.0).abs() < 1e-5);
178    }
179
180    #[test]
181    fn cubic_mode_endpoints() {
182        let mut c = AnimCurve::new(InterpMode::Cubic);
183        c.insert(Keyframe::with_tangents(0.0, 0.0, 0.0, 0.0));
184        c.insert(Keyframe::with_tangents(1.0, 1.0, 0.0, 0.0));
185        assert!((c.evaluate(0.0) - 0.0).abs() < 1e-4);
186        assert!((c.evaluate(1.0) - 1.0).abs() < 1e-4);
187    }
188
189    #[test]
190    fn insert_maintains_order() {
191        let mut c = AnimCurve::new(InterpMode::Linear);
192        c.insert(Keyframe::new(2.0, 2.0));
193        c.insert(Keyframe::new(0.0, 0.0));
194        c.insert(Keyframe::new(1.0, 1.0));
195        assert!(c.keyframes[0].time <= c.keyframes[1].time);
196        assert!(c.keyframes[1].time <= c.keyframes[2].time);
197    }
198
199    #[test]
200    fn empty_curve_returns_zero() {
201        let c = AnimCurve::new(InterpMode::Linear);
202        assert!((c.evaluate(0.5) - 0.0).abs() < 1e-5);
203    }
204}