1use std::collections::HashMap;
7
8#[allow(dead_code)]
12pub struct EmotionKeyframe {
13 pub time: f32,
15 pub emotions: HashMap<String, f32>,
17 pub easing: TimelineEasing,
19}
20
21#[allow(dead_code)]
23#[derive(Clone, Debug, PartialEq)]
24pub enum TimelineEasing {
25 Linear,
26 EaseIn,
27 EaseOut,
28 EaseInOut,
29 Step,
31}
32
33#[allow(dead_code)]
35#[derive(Clone, Debug, PartialEq)]
36pub enum TimelineLoop {
37 Once,
39 Loop,
41 PingPong,
43}
44
45#[allow(dead_code)]
47pub struct EmotionTimeline {
48 pub keyframes: Vec<EmotionKeyframe>,
50 pub duration: f32,
52 pub loop_mode: TimelineLoop,
54}
55
56impl EmotionTimeline {
59 #[allow(dead_code)]
61 pub fn new(duration: f32, loop_mode: TimelineLoop) -> Self {
62 Self {
63 keyframes: Vec::new(),
64 duration,
65 loop_mode,
66 }
67 }
68
69 #[allow(dead_code)]
71 pub fn add_keyframe(&mut self, kf: EmotionKeyframe) {
72 let pos = self
73 .keyframes
74 .partition_point(|existing| existing.time <= kf.time);
75 self.keyframes.insert(pos, kf);
76 }
77
78 #[allow(dead_code)]
80 pub fn sample(&self, t: f32) -> HashMap<String, f32> {
81 let t = normalize_emotion_time(t, self.duration, &self.loop_mode);
82
83 if self.keyframes.is_empty() {
84 return HashMap::new();
85 }
86
87 let idx = self.keyframes.partition_point(|kf| kf.time <= t);
89
90 if idx == 0 {
91 return self.keyframes[0].emotions.clone();
93 }
94 if idx >= self.keyframes.len() {
95 return self.keyframes[self.keyframes.len() - 1].emotions.clone();
97 }
98
99 let kf_a = &self.keyframes[idx - 1];
100 let kf_b = &self.keyframes[idx];
101
102 let span = kf_b.time - kf_a.time;
103 let raw_t = if span.abs() < f32::EPSILON {
104 1.0_f32
105 } else {
106 (t - kf_a.time) / span
107 };
108
109 let eased_t = apply_easing_fn(raw_t, &kf_a.easing);
110 interpolate_emotions(&kf_a.emotions, &kf_b.emotions, eased_t)
111 }
112
113 #[allow(dead_code)]
116 pub fn bake(&self, fps: f32) -> Vec<HashMap<String, f32>> {
117 let frame_count = (self.duration * fps).ceil() as usize + 1;
118 (0..frame_count)
119 .map(|i| {
120 let t = (i as f32) / fps;
121 self.sample(t)
122 })
123 .collect()
124 }
125}
126
127#[allow(dead_code)]
132pub fn interpolate_emotions(
133 a: &HashMap<String, f32>,
134 b: &HashMap<String, f32>,
135 t: f32,
136) -> HashMap<String, f32> {
137 let mut result = HashMap::new();
138
139 for (k, &va) in a {
140 let vb = b.get(k).copied().unwrap_or(0.0);
141 result.insert(k.clone(), va + (vb - va) * t);
142 }
143 for (k, &vb) in b {
144 if !result.contains_key(k) {
145 result.insert(k.clone(), vb * t);
147 }
148 }
149 result
150}
151
152#[allow(dead_code)]
154pub fn apply_easing_fn(t: f32, easing: &TimelineEasing) -> f32 {
155 let t = t.clamp(0.0, 1.0);
156 match easing {
157 TimelineEasing::Linear => t,
158 TimelineEasing::EaseIn => t * t,
159 TimelineEasing::EaseOut => t * (2.0 - t),
160 TimelineEasing::EaseInOut => {
161 if t < 0.5 {
162 2.0 * t * t
163 } else {
164 -1.0 + (4.0 - 2.0 * t) * t
165 }
166 }
167 TimelineEasing::Step => {
169 if t < 1.0 {
170 0.0
171 } else {
172 1.0
173 }
174 }
175 }
176}
177
178#[allow(dead_code)]
180pub fn normalize_emotion_time(t: f32, duration: f32, loop_mode: &TimelineLoop) -> f32 {
181 if duration <= 0.0 {
182 return 0.0;
183 }
184 match loop_mode {
185 TimelineLoop::Once => t.clamp(0.0, duration),
186 TimelineLoop::Loop => {
187 let wrapped = t % duration;
188 if wrapped < 0.0 {
189 wrapped + duration
190 } else {
191 wrapped
192 }
193 }
194 TimelineLoop::PingPong => {
195 let period = 2.0 * duration;
196 let wrapped = ((t % period) + period) % period;
197 if wrapped <= duration {
198 wrapped
199 } else {
200 period - wrapped
201 }
202 }
203 }
204}
205
206#[cfg(test)]
208mod tests {
209 use super::*;
210
211 fn make_kf(time: f32, emotions: &[(&str, f32)]) -> EmotionKeyframe {
212 EmotionKeyframe {
213 time,
214 emotions: emotions.iter().map(|(k, v)| (k.to_string(), *v)).collect(),
215 easing: TimelineEasing::Linear,
216 }
217 }
218
219 fn make_kf_easing(
220 time: f32,
221 emotions: &[(&str, f32)],
222 easing: TimelineEasing,
223 ) -> EmotionKeyframe {
224 EmotionKeyframe {
225 time,
226 emotions: emotions.iter().map(|(k, v)| (k.to_string(), *v)).collect(),
227 easing,
228 }
229 }
230
231 #[test]
233 fn test_add_keyframe_sorted() {
234 let mut tl = EmotionTimeline::new(2.0, TimelineLoop::Once);
235 tl.add_keyframe(make_kf(1.5, &[]));
236 tl.add_keyframe(make_kf(0.5, &[]));
237 tl.add_keyframe(make_kf(1.0, &[]));
238 let times: Vec<f32> = tl.keyframes.iter().map(|k| k.time).collect();
239 assert_eq!(times, vec![0.5, 1.0, 1.5]);
240 }
241
242 #[test]
244 fn test_sample_exact_keyframe() {
245 let mut tl = EmotionTimeline::new(2.0, TimelineLoop::Once);
246 tl.add_keyframe(make_kf(0.0, &[("happy", 0.0)]));
247 tl.add_keyframe(make_kf(1.0, &[("happy", 1.0)]));
248 let result = tl.sample(0.0);
249 assert!((result["happy"] - 0.0).abs() < 1e-5);
250 let result2 = tl.sample(1.0);
251 assert!((result2["happy"] - 1.0).abs() < 1e-5);
252 }
253
254 #[test]
256 fn test_sample_between_keyframes() {
257 let mut tl = EmotionTimeline::new(2.0, TimelineLoop::Once);
258 tl.add_keyframe(make_kf(0.0, &[("happy", 0.0)]));
259 tl.add_keyframe(make_kf(2.0, &[("happy", 1.0)]));
260 let result = tl.sample(1.0);
261 assert!((result["happy"] - 0.5).abs() < 1e-5);
262 }
263
264 #[test]
266 fn test_sample_past_end_clamps() {
267 let mut tl = EmotionTimeline::new(1.0, TimelineLoop::Once);
268 tl.add_keyframe(make_kf(0.0, &[("sad", 0.0)]));
269 tl.add_keyframe(make_kf(1.0, &[("sad", 0.8)]));
270 let result = tl.sample(99.0);
271 assert!((result["sad"] - 0.8).abs() < 1e-5);
272 }
273
274 #[test]
276 fn test_loop_wraps() {
277 let mut tl = EmotionTimeline::new(1.0, TimelineLoop::Loop);
278 tl.add_keyframe(make_kf(0.0, &[("anger", 1.0)]));
279 tl.add_keyframe(make_kf(1.0, &[("anger", 0.0)]));
280 let result = tl.sample(1.25);
282 assert!((result["anger"] - 0.75).abs() < 1e-4);
283 }
284
285 #[test]
287 fn test_ping_pong() {
288 let mut tl = EmotionTimeline::new(1.0, TimelineLoop::PingPong);
289 tl.add_keyframe(make_kf(0.0, &[("joy", 0.0)]));
290 tl.add_keyframe(make_kf(1.0, &[("joy", 1.0)]));
291 let result = tl.sample(1.5);
293 assert!((result["joy"] - 0.5).abs() < 1e-4);
294 }
295
296 #[test]
298 fn test_bake_frame_count() {
299 let mut tl = EmotionTimeline::new(2.0, TimelineLoop::Once);
300 tl.add_keyframe(make_kf(0.0, &[("e", 0.0)]));
301 tl.add_keyframe(make_kf(2.0, &[("e", 1.0)]));
302 let frames = tl.bake(30.0);
303 let expected = (2.0_f32 * 30.0).ceil() as usize + 1; assert_eq!(frames.len(), expected);
305 }
306
307 #[test]
309 fn test_interpolate_emotions_merges_keys() {
310 let a: HashMap<String, f32> = [("happy".to_string(), 1.0)].into();
311 let b: HashMap<String, f32> = [("sad".to_string(), 1.0)].into();
312 let result = interpolate_emotions(&a, &b, 0.5);
313 assert!((result["happy"] - 0.5).abs() < 1e-5);
314 assert!((result["sad"] - 0.5).abs() < 1e-5);
315 }
316
317 #[test]
319 fn test_easing_linear_identity() {
320 for &v in &[0.0_f32, 0.25, 0.5, 0.75, 1.0] {
321 assert!((apply_easing_fn(v, &TimelineEasing::Linear) - v).abs() < 1e-6);
322 }
323 }
324
325 #[test]
327 fn test_easing_ease_in_out_midpoint() {
328 let v = apply_easing_fn(0.5, &TimelineEasing::EaseInOut);
329 assert!((v - 0.5).abs() < 1e-5);
330 }
331
332 #[test]
334 fn test_easing_step_returns_prior() {
335 assert!((apply_easing_fn(0.0, &TimelineEasing::Step) - 0.0).abs() < 1e-6);
336 assert!((apply_easing_fn(0.5, &TimelineEasing::Step) - 0.0).abs() < 1e-6);
337 assert!((apply_easing_fn(0.999, &TimelineEasing::Step) - 0.0).abs() < 1e-6);
338 assert!((apply_easing_fn(1.0, &TimelineEasing::Step) - 1.0).abs() < 1e-6);
339 }
340
341 #[test]
343 fn test_step_easing_holds_prior_value() {
344 let mut tl = EmotionTimeline::new(2.0, TimelineLoop::Once);
345 tl.add_keyframe(make_kf_easing(0.0, &[("fear", 0.2)], TimelineEasing::Step));
346 tl.add_keyframe(make_kf_easing(1.0, &[("fear", 0.8)], TimelineEasing::Step));
347 tl.add_keyframe(make_kf_easing(2.0, &[("fear", 0.4)], TimelineEasing::Step));
348 let mid = tl.sample(0.5);
350 assert!((mid["fear"] - 0.2).abs() < 1e-5);
351 }
352}