1#![allow(dead_code)]
5
6use std::collections::HashMap;
7
8#[derive(Debug, Clone, PartialEq)]
14pub enum InterpMode {
15 Step,
17 Linear,
19 Smooth,
21 Bezier,
23 Sine,
25}
26
27pub struct Keyframe {
33 pub time: f32,
35 pub value: f32,
37 pub interp: InterpMode,
39 pub tan_in: f32,
41 pub tan_out: f32,
43}
44
45impl Keyframe {
46 pub fn new(time: f32, value: f32) -> Self {
48 Self {
49 time,
50 value,
51 interp: InterpMode::Linear,
52 tan_in: 0.0,
53 tan_out: 0.0,
54 }
55 }
56
57 pub fn step(time: f32, value: f32) -> Self {
59 Self {
60 time,
61 value,
62 interp: InterpMode::Step,
63 tan_in: 0.0,
64 tan_out: 0.0,
65 }
66 }
67
68 pub fn smooth(time: f32, value: f32) -> Self {
70 Self {
71 time,
72 value,
73 interp: InterpMode::Smooth,
74 tan_in: 0.0,
75 tan_out: 0.0,
76 }
77 }
78}
79
80#[derive(Debug, Clone, PartialEq)]
86pub enum LoopMode {
87 Clamp,
89 Loop,
91 PingPong,
93}
94
95pub struct ParamTrack {
101 pub param_name: String,
103 pub keyframes: Vec<Keyframe>,
105 pub loop_mode: LoopMode,
107 pub pre_infinity: f32,
109 pub post_infinity: f32,
111}
112
113impl ParamTrack {
114 pub fn new(param_name: &str) -> Self {
116 Self {
117 param_name: param_name.to_owned(),
118 keyframes: Vec::new(),
119 loop_mode: LoopMode::Clamp,
120 pre_infinity: 0.0,
121 post_infinity: 0.0,
122 }
123 }
124
125 pub fn add_keyframe(&mut self, kf: Keyframe) {
128 self.keyframes.push(kf);
129 self.sort_keyframes();
131 }
132
133 pub fn sort_keyframes(&mut self) {
135 self.keyframes.sort_by(|a, b| {
136 a.time
137 .partial_cmp(&b.time)
138 .unwrap_or(std::cmp::Ordering::Equal)
139 });
140 }
141
142 pub fn duration(&self) -> f32 {
144 self.keyframes.last().map(|k| k.time).unwrap_or(0.0)
145 }
146
147 pub fn frame_count(&self) -> usize {
149 self.keyframes.len()
150 }
151
152 fn apply_loop(&self, t: f32) -> Option<f32> {
155 if self.keyframes.is_empty() {
156 return None;
157 }
158 let first = self.keyframes.first().map_or(0.0, |k| k.time);
159 let last = self.keyframes.last().map_or(0.0, |k| k.time);
160 let span = last - first;
161
162 if span <= 0.0 {
163 return Some(first);
164 }
165
166 let local = match self.loop_mode {
167 LoopMode::Clamp => t.clamp(first, last),
168 LoopMode::Loop => {
169 let offset = t - first;
170 let wrapped = offset.rem_euclid(span);
171 first + wrapped
172 }
173 LoopMode::PingPong => {
174 let offset = t - first;
175 let cycle = span * 2.0;
176 let wrapped = offset.rem_euclid(cycle);
177 let ping = if wrapped <= span {
178 wrapped
179 } else {
180 cycle - wrapped
181 };
182 first + ping
183 }
184 };
185 Some(local)
186 }
187
188 pub fn evaluate(&self, t: f32) -> f32 {
190 if self.keyframes.is_empty() {
191 return 0.0;
192 }
193 if self.keyframes.len() == 1 {
194 return self.keyframes[0].value;
195 }
196
197 let first = &self.keyframes[0];
198 let last = &self.keyframes[self.keyframes.len() - 1];
199
200 if self.loop_mode == LoopMode::Clamp {
202 if t < first.time {
203 return self.pre_infinity;
204 }
205 if t > last.time {
206 return self.post_infinity;
207 }
208 }
209
210 let local_t = match self.apply_loop(t) {
211 Some(v) => v,
212 None => return 0.0,
213 };
214
215 let idx_b = self.keyframes.partition_point(|k| k.time <= local_t);
217
218 if idx_b == 0 {
220 return self.keyframes[0].value;
221 }
222 if idx_b >= self.keyframes.len() {
223 return self.keyframes[self.keyframes.len() - 1].value;
224 }
225
226 let idx_a = idx_b - 1;
227 let ka = &self.keyframes[idx_a];
228 let kb = &self.keyframes[idx_b];
229
230 let span = kb.time - ka.time;
231 let frac = if span > 0.0 {
232 (local_t - ka.time) / span
233 } else {
234 0.0
235 };
236
237 interpolate(ka.value, kb.value, frac, &ka.interp, ka.tan_out, kb.tan_in)
238 }
239
240 pub fn bake(&self, sample_count: usize) -> Vec<(f32, f32)> {
243 if sample_count == 0 || self.keyframes.is_empty() {
244 return Vec::new();
245 }
246 let end = self.duration();
247 (0..sample_count)
248 .map(|i| {
249 let t = if sample_count == 1 {
250 0.0
251 } else {
252 end * (i as f32 / (sample_count - 1) as f32)
253 };
254 (t, self.evaluate(t))
255 })
256 .collect()
257 }
258}
259
260pub struct ParamClip {
266 pub name: String,
268 pub tracks: Vec<ParamTrack>,
270 pub fps: f32,
272}
273
274impl ParamClip {
275 pub fn new(name: &str) -> Self {
277 Self {
278 name: name.to_owned(),
279 tracks: Vec::new(),
280 fps: 30.0,
281 }
282 }
283
284 pub fn add_track(&mut self, track: ParamTrack) {
286 self.tracks.push(track);
287 }
288
289 pub fn find_track(&self, param: &str) -> Option<&ParamTrack> {
291 self.tracks.iter().find(|t| t.param_name == param)
292 }
293
294 pub fn track_count(&self) -> usize {
296 self.tracks.len()
297 }
298
299 pub fn duration(&self) -> f32 {
301 self.tracks
302 .iter()
303 .map(|t| t.duration())
304 .fold(0.0_f32, f32::max)
305 }
306
307 pub fn evaluate_all(&self, t: f32) -> HashMap<String, f32> {
309 self.tracks
310 .iter()
311 .map(|tr| (tr.param_name.clone(), tr.evaluate(t)))
312 .collect()
313 }
314
315 pub fn bake_all(&self, fps: f32) -> Vec<HashMap<String, f32>> {
318 let dur = self.duration();
319 if dur <= 0.0 || fps <= 0.0 {
320 return Vec::new();
321 }
322 let dt = 1.0 / fps;
323 let frame_count = (dur * fps).ceil() as usize + 1;
324 (0..frame_count)
325 .map(|i| {
326 let t = (i as f32 * dt).min(dur);
327 self.evaluate_all(t)
328 })
329 .collect()
330 }
331
332 pub fn scale_time(&mut self, factor: f32) {
334 for track in &mut self.tracks {
335 for kf in &mut track.keyframes {
336 kf.time *= factor;
337 }
338 }
339 }
340
341 pub fn shift_time(&mut self, offset: f32) {
343 for track in &mut self.tracks {
344 for kf in &mut track.keyframes {
345 kf.time += offset;
346 }
347 }
348 }
349}
350
351#[inline]
357pub fn smoothstep_interp(t: f32) -> f32 {
358 let t = t.clamp(0.0, 1.0);
359 t * t * (3.0 - 2.0 * t)
360}
361
362pub fn cubic_hermite(p0: f32, p1: f32, m0: f32, m1: f32, t: f32) -> f32 {
364 let t2 = t * t;
365 let t3 = t2 * t;
366 (2.0 * t3 - 3.0 * t2 + 1.0) * p0
367 + (t3 - 2.0 * t2 + t) * m0
368 + (-2.0 * t3 + 3.0 * t2) * p1
369 + (t3 - t2) * m1
370}
371
372#[allow(clippy::too_many_arguments)]
376pub fn interpolate(a: f32, b: f32, t: f32, mode: &InterpMode, tan_out: f32, tan_in: f32) -> f32 {
377 let t = t.clamp(0.0, 1.0);
378 match mode {
379 InterpMode::Step => a,
380 InterpMode::Linear => a + (b - a) * t,
381 InterpMode::Smooth => {
382 let s = smoothstep_interp(t);
383 a + (b - a) * s
384 }
385 InterpMode::Bezier => cubic_hermite(a, b, tan_out, tan_in, t),
386 InterpMode::Sine => {
387 let s = 0.5 - 0.5 * (std::f32::consts::PI * t).cos();
389 a + (b - a) * s
390 }
391 }
392}
393
394pub fn breathing_clip(breath_rate_hz: f32) -> ParamClip {
405 let mut clip = ParamClip::new("breathing");
406 let period = if breath_rate_hz > 0.0 {
407 1.0 / breath_rate_hz
408 } else {
409 4.0
410 };
411 let half = period * 0.5;
412
413 let mut chest = ParamTrack::new("chest_expand");
415 chest.loop_mode = LoopMode::Loop;
416 chest.add_keyframe(Keyframe::smooth(0.0, 0.0));
417 chest.add_keyframe(Keyframe::smooth(half, 1.0));
418 chest.add_keyframe(Keyframe::smooth(period, 0.0));
419 clip.add_track(chest);
420
421 let mut belly = ParamTrack::new("belly_push");
423 belly.loop_mode = LoopMode::Loop;
424 belly.add_keyframe(Keyframe::smooth(0.0, 0.0));
425 belly.add_keyframe(Keyframe::smooth(half * 0.4, 0.6));
426 belly.add_keyframe(Keyframe::smooth(half, 0.8));
427 belly.add_keyframe(Keyframe::smooth(period, 0.0));
428 clip.add_track(belly);
429
430 let mut shift = ParamTrack::new("weight_shift");
432 shift.loop_mode = LoopMode::Loop;
433 shift.add_keyframe(Keyframe {
434 time: 0.0,
435 value: 0.5,
436 interp: InterpMode::Sine,
437 tan_in: 0.0,
438 tan_out: 0.0,
439 });
440 shift.add_keyframe(Keyframe {
441 time: half,
442 value: 0.55,
443 interp: InterpMode::Sine,
444 tan_in: 0.0,
445 tan_out: 0.0,
446 });
447 shift.add_keyframe(Keyframe {
448 time: period,
449 value: 0.5,
450 interp: InterpMode::Sine,
451 tan_in: 0.0,
452 tan_out: 0.0,
453 });
454 clip.add_track(shift);
455
456 clip
457}
458
459pub fn blend_clip(from: &str, to: &str, duration: f32) -> ParamClip {
462 let name = format!("blend_{}_to_{}", from, to);
463 let mut clip = ParamClip::new(&name);
464
465 let mut from_track = ParamTrack::new(from);
467 from_track.add_keyframe(Keyframe::smooth(0.0, 1.0));
468 from_track.add_keyframe(Keyframe::smooth(duration, 0.0));
469 clip.add_track(from_track);
470
471 let mut to_track = ParamTrack::new(to);
473 to_track.add_keyframe(Keyframe::smooth(0.0, 0.0));
474 to_track.add_keyframe(Keyframe::smooth(duration, 1.0));
475 clip.add_track(to_track);
476
477 clip
478}
479
480#[cfg(test)]
485mod tests {
486 use super::*;
487 use std::fs;
488
489 #[test]
492 fn smoothstep_boundaries() {
493 assert!((smoothstep_interp(0.0) - 0.0).abs() < 1e-6);
494 assert!((smoothstep_interp(1.0) - 1.0).abs() < 1e-6);
495 }
496
497 #[test]
498 fn smoothstep_midpoint() {
499 assert!((smoothstep_interp(0.5) - 0.5).abs() < 1e-6);
501 }
502
503 #[test]
506 fn cubic_hermite_endpoints() {
507 let p0 = 0.2_f32;
509 let p1 = 0.8_f32;
510 assert!((cubic_hermite(p0, p1, 0.0, 0.0, 0.0) - p0).abs() < 1e-6);
511 assert!((cubic_hermite(p0, p1, 0.0, 0.0, 1.0) - p1).abs() < 1e-6);
512 }
513
514 #[test]
517 fn interpolate_step_holds_a() {
518 let v = interpolate(0.3, 0.9, 0.8, &InterpMode::Step, 0.0, 0.0);
519 assert!((v - 0.3).abs() < 1e-6);
520 }
521
522 #[test]
523 fn interpolate_linear_midpoint() {
524 let v = interpolate(0.0, 1.0, 0.5, &InterpMode::Linear, 0.0, 0.0);
525 assert!((v - 0.5).abs() < 1e-6);
526 }
527
528 #[test]
529 fn interpolate_smooth_midpoint() {
530 let v = interpolate(0.0, 1.0, 0.5, &InterpMode::Smooth, 0.0, 0.0);
532 assert!((v - 0.5).abs() < 1e-6);
533 }
534
535 #[test]
536 fn interpolate_sine_boundaries() {
537 let a = interpolate(0.0, 1.0, 0.0, &InterpMode::Sine, 0.0, 0.0);
538 let b = interpolate(0.0, 1.0, 1.0, &InterpMode::Sine, 0.0, 0.0);
539 assert!(a.abs() < 1e-6);
540 assert!((b - 1.0).abs() < 1e-6);
541 }
542
543 #[test]
546 fn keyframe_new_is_linear() {
547 let kf = Keyframe::new(1.0, 0.5);
548 assert_eq!(kf.interp, InterpMode::Linear);
549 assert!((kf.time - 1.0).abs() < 1e-6);
550 assert!((kf.value - 0.5).abs() < 1e-6);
551 }
552
553 #[test]
554 fn keyframe_step_constructor() {
555 let kf = Keyframe::step(2.0, 0.7);
556 assert_eq!(kf.interp, InterpMode::Step);
557 }
558
559 #[test]
560 fn keyframe_smooth_constructor() {
561 let kf = Keyframe::smooth(3.0, 0.3);
562 assert_eq!(kf.interp, InterpMode::Smooth);
563 }
564
565 #[test]
568 fn param_track_evaluate_linear() {
569 let mut track = ParamTrack::new("test");
570 track.add_keyframe(Keyframe::new(0.0, 0.0));
571 track.add_keyframe(Keyframe::new(1.0, 1.0));
572 let v = track.evaluate(0.5);
573 assert!((v - 0.5).abs() < 1e-5);
574 }
575
576 #[test]
577 fn param_track_evaluate_step() {
578 let mut track = ParamTrack::new("step_track");
579 track.add_keyframe(Keyframe::step(0.0, 0.0));
580 track.add_keyframe(Keyframe::step(1.0, 1.0));
581 let v = track.evaluate(0.5);
583 assert!((v - 0.0).abs() < 1e-5);
584 }
585
586 #[test]
587 fn param_track_clamp_pre_post() {
588 let mut track = ParamTrack::new("clamp");
589 track.loop_mode = LoopMode::Clamp;
590 track.pre_infinity = 0.1;
591 track.post_infinity = 0.9;
592 track.add_keyframe(Keyframe::new(1.0, 0.2));
593 track.add_keyframe(Keyframe::new(2.0, 0.8));
594 let pre = track.evaluate(0.0);
596 assert!((pre - 0.1).abs() < 1e-5, "pre={pre}");
597 let post = track.evaluate(5.0);
599 assert!((post - 0.9).abs() < 1e-5, "post={post}");
600 }
601
602 #[test]
603 fn param_track_loop_mode() {
604 let mut track = ParamTrack::new("loop_track");
605 track.loop_mode = LoopMode::Loop;
606 track.add_keyframe(Keyframe::new(0.0, 0.0));
607 track.add_keyframe(Keyframe::new(1.0, 1.0));
608 let v_original = track.evaluate(0.5);
610 let v_looped = track.evaluate(1.5);
611 assert!((v_original - v_looped).abs() < 1e-4);
612 }
613
614 #[test]
615 fn param_track_pingpong_mode() {
616 let mut track = ParamTrack::new("pp");
617 track.loop_mode = LoopMode::PingPong;
618 track.add_keyframe(Keyframe::new(0.0, 0.0));
619 track.add_keyframe(Keyframe::new(1.0, 1.0));
620 let v_fwd = track.evaluate(0.5); let v_rev = track.evaluate(1.5); assert!((v_fwd - v_rev).abs() < 1e-4);
624 }
625
626 #[test]
627 fn param_track_duration_and_frame_count() {
628 let mut track = ParamTrack::new("dur");
629 track.add_keyframe(Keyframe::new(0.0, 0.0));
630 track.add_keyframe(Keyframe::new(3.0, 1.0));
631 assert!((track.duration() - 3.0).abs() < 1e-5);
632 assert_eq!(track.frame_count(), 2);
633 }
634
635 #[test]
636 fn param_track_bake_count() {
637 let mut track = ParamTrack::new("bake");
638 track.add_keyframe(Keyframe::new(0.0, 0.0));
639 track.add_keyframe(Keyframe::new(1.0, 1.0));
640 let samples = track.bake(11);
641 assert_eq!(samples.len(), 11);
642 assert!((samples[0].1 - 0.0).abs() < 1e-5);
644 assert!((samples[10].1 - 1.0).abs() < 1e-5);
646 }
647
648 #[test]
651 fn param_clip_find_track() {
652 let mut clip = ParamClip::new("test_clip");
653 let mut track = ParamTrack::new("jaw_open");
654 track.add_keyframe(Keyframe::new(0.0, 0.0));
655 clip.add_track(track);
656 assert!(clip.find_track("jaw_open").is_some());
657 assert!(clip.find_track("missing").is_none());
658 }
659
660 #[test]
661 fn param_clip_duration_max() {
662 let mut clip = ParamClip::new("multi");
663 let mut t1 = ParamTrack::new("a");
664 t1.add_keyframe(Keyframe::new(0.0, 0.0));
665 t1.add_keyframe(Keyframe::new(2.0, 1.0));
666 let mut t2 = ParamTrack::new("b");
667 t2.add_keyframe(Keyframe::new(0.0, 0.0));
668 t2.add_keyframe(Keyframe::new(5.0, 1.0));
669 clip.add_track(t1);
670 clip.add_track(t2);
671 assert!((clip.duration() - 5.0).abs() < 1e-5);
672 }
673
674 #[test]
675 fn param_clip_evaluate_all() {
676 let mut clip = ParamClip::new("eval");
677 let mut tr = ParamTrack::new("smile");
678 tr.add_keyframe(Keyframe::new(0.0, 0.0));
679 tr.add_keyframe(Keyframe::new(1.0, 1.0));
680 clip.add_track(tr);
681 let map = clip.evaluate_all(0.5);
682 let v = map["smile"];
683 assert!((v - 0.5).abs() < 1e-5);
684 }
685
686 #[test]
687 fn param_clip_scale_shift_time() {
688 let mut clip = ParamClip::new("scale_shift");
689 let mut tr = ParamTrack::new("p");
690 tr.add_keyframe(Keyframe::new(0.0, 0.0));
691 tr.add_keyframe(Keyframe::new(2.0, 1.0));
692 clip.add_track(tr);
693 clip.scale_time(2.0);
694 assert!((clip.duration() - 4.0).abs() < 1e-5);
695 clip.shift_time(1.0);
696 assert!((clip.duration() - 5.0).abs() < 1e-5);
697 }
698
699 #[test]
700 fn param_clip_bake_all_frames() {
701 let mut clip = ParamClip::new("bake_all");
702 let mut tr = ParamTrack::new("eye");
703 tr.add_keyframe(Keyframe::new(0.0, 0.0));
704 tr.add_keyframe(Keyframe::new(1.0, 1.0));
705 clip.add_track(tr);
706 let frames = clip.bake_all(10.0);
707 assert!(!frames.is_empty());
709 assert!(frames[0].contains_key("eye"));
710 }
711
712 #[test]
715 fn breathing_clip_has_three_tracks() {
716 let clip = breathing_clip(0.25); assert_eq!(clip.track_count(), 3);
718 assert!(clip.find_track("chest_expand").is_some());
719 assert!(clip.find_track("belly_push").is_some());
720 assert!(clip.find_track("weight_shift").is_some());
721 }
722
723 #[test]
724 fn breathing_clip_loops() {
725 let clip = breathing_clip(1.0); let track = clip.find_track("chest_expand").expect("should succeed");
727 assert_eq!(track.loop_mode, LoopMode::Loop);
728 let v0 = track.evaluate(0.0);
730 let v1 = track.evaluate(1.0);
731 assert!((v0 - v1).abs() < 1e-4, "v0={v0}, v1={v1}");
732 }
733
734 #[test]
735 fn blend_clip_two_tracks() {
736 let clip = blend_clip("neutral", "happy", 2.0);
737 assert_eq!(clip.track_count(), 2);
738 assert!(clip.find_track("neutral").is_some());
739 assert!(clip.find_track("happy").is_some());
740 let map0 = clip.evaluate_all(0.0);
742 assert!((map0["neutral"] - 1.0).abs() < 1e-5);
743 assert!((map0["happy"] - 0.0).abs() < 1e-5);
744 let map2 = clip.evaluate_all(2.0);
746 assert!((map2["neutral"] - 0.0).abs() < 1e-5);
747 assert!((map2["happy"] - 1.0).abs() < 1e-5);
748 }
749
750 #[test]
751 fn blend_clip_name_contains_expressions() {
752 let clip = blend_clip("sad", "surprised", 1.0);
753 assert!(clip.name.contains("sad"));
754 assert!(clip.name.contains("surprised"));
755 }
756
757 #[test]
760 fn bake_writes_to_tmp() {
761 let mut clip = ParamClip::new("write_test");
762 let mut tr = ParamTrack::new("brow");
763 tr.add_keyframe(Keyframe::new(0.0, 0.0));
764 tr.add_keyframe(Keyframe::new(1.0, 1.0));
765 clip.add_track(tr);
766 let frames = clip.bake_all(5.0);
767 let mut lines = Vec::new();
768 for (i, frame) in frames.iter().enumerate() {
769 let v = frame.get("brow").copied().unwrap_or(0.0);
770 lines.push(format!("frame {i}: brow={v:.4}"));
771 }
772 let output = lines.join("\n");
773 fs::write("/tmp/param_animation_bake_test.txt", &output).expect("should succeed");
774 let read_back =
775 fs::read_to_string("/tmp/param_animation_bake_test.txt").expect("should succeed");
776 assert!(read_back.contains("brow="));
777 }
778
779 #[test]
780 fn bezier_interpolate_boundaries() {
781 let a = interpolate(0.0, 1.0, 0.0, &InterpMode::Bezier, 0.0, 0.0);
782 let b = interpolate(0.0, 1.0, 1.0, &InterpMode::Bezier, 0.0, 0.0);
783 assert!(a.abs() < 1e-6);
784 assert!((b - 1.0).abs() < 1e-6);
785 }
786
787 #[test]
788 fn param_clip_track_count() {
789 let mut clip = ParamClip::new("count_test");
790 assert_eq!(clip.track_count(), 0);
791 clip.add_track(ParamTrack::new("a"));
792 clip.add_track(ParamTrack::new("b"));
793 assert_eq!(clip.track_count(), 2);
794 }
795}