1use crate::{components::{Component, Transform}, reflect::Field};
3use glam::Vec3;
4use kvas_ecs::World;
5use serde::{Deserialize, Serialize};
6
7#[derive(Clone, Copy, Debug, Serialize, Deserialize)]
9pub struct TransformKey {
10 pub time: f32,
12 pub pose: Transform,
14}
15
16#[derive(Clone, Copy, Debug, Default, Serialize, Deserialize, PartialEq)]
18pub enum Interpolation {
19 Step,
21 #[default]
23 Linear,
24 Smooth,
26}
27
28#[derive(Clone, Debug, Serialize, Deserialize)]
30#[serde(default)]
31pub struct TransformAnimation {
32 pub keys: Vec<TransformKey>,
34 pub time: f32,
36 pub speed: f32,
38 pub looping: bool,
40 pub playing: bool,
42 pub interpolation: Interpolation,
44}
45
46impl Default for TransformAnimation {
47 fn default() -> Self { Self { keys: Vec::new(), time: 0.0, speed: 1.0, looping: true, playing: true, interpolation: Interpolation::Linear } }
48}
49
50impl Component for TransformAnimation {
51 fn fields(&mut self) -> Vec<Field<'_>> {
52 vec![Field::bool("playing", &mut self.playing), Field::bool("looping", &mut self.looping), Field::f32("speed", &mut self.speed)]
53 }
54}
55
56impl TransformAnimation {
57 pub fn duration(&self) -> f32 { self.keys.iter().map(|k| k.time).filter(|t| t.is_finite()).fold(0.0, f32::max) }
59
60 pub fn set_key(&mut self, time: f32, pose: Transform) -> bool {
62 if !time.is_finite() || time < 0.0 || !pose.is_finite() { return false; }
63 self.keys.retain(|k| (k.time - time).abs() > 0.0001);
64 self.keys.push(TransformKey { time, pose });
65 self.keys.sort_by(|a, b| a.time.total_cmp(&b.time));
66 true
67 }
68
69 pub fn sample(&self, time: f32) -> Option<Transform> {
71 if !time.is_finite() { return None; }
72 let valid = |k: &&TransformKey| k.time.is_finite() && k.time >= 0.0 && k.pose.is_finite();
73 let first = self.keys.iter().filter(valid).min_by(|a, b| a.time.total_cmp(&b.time))?;
74 let a = self.keys.iter().filter(valid).filter(|k| k.time <= time).max_by(|a, b| a.time.total_cmp(&b.time)).unwrap_or(first);
75 let Some(b) = self.keys.iter().filter(valid).filter(|k| k.time > time).min_by(|a, b| a.time.total_cmp(&b.time)) else { return Some(a.pose); };
76 let mut t = ((time - a.time) / (b.time - a.time).max(1e-6)).clamp(0.0, 1.0);
77 t = match self.interpolation { Interpolation::Step => 0.0, Interpolation::Linear => t, Interpolation::Smooth => t * t * (3.0 - 2.0 * t) };
78 let mut pose = Transform { position: a.pose.position.lerp(b.pose.position, t), scale: a.pose.scale.lerp(b.pose.scale, t), rotation: Vec3::ZERO };
79 pose.set_quat(a.pose.quat().slerp(b.pose.quat(), t));
80 Some(pose)
81 }
82}
83
84pub fn update(world: &mut World, dt: f32) {
86 if !dt.is_finite() || dt < 0.0 { return; }
87 for e in world.entities_with::<TransformAnimation>() {
88 let pose = {
89 let Some(clip) = world.get_mut::<TransformAnimation>(e) else { continue };
90 if !clip.playing || !clip.speed.is_finite() || !clip.time.is_finite() { continue; }
91 let duration = clip.duration();
92 clip.time += dt * clip.speed;
93 if duration > 0.0 && clip.looping { clip.time = clip.time.rem_euclid(duration); }
94 else if clip.time >= duration || clip.time < 0.0 { clip.time = clip.time.clamp(0.0, duration); clip.playing = false; }
95 clip.sample(clip.time)
96 };
97 if let (Some(pose), Some(target)) = (pose, world.get_mut::<Transform>(e)) { *target = pose; }
98 }
99}
100
101#[cfg(test)]
102mod tests {
103 use super::*;
104 #[test]
105 fn keys_interpolate_shortest_rotation_and_reject_invalid_data() {
106 let mut clip = TransformAnimation::default();
107 let mut a = Transform::at(Vec3::ZERO); a.rotation.y = 350.0;
108 let mut b = Transform::at(Vec3::X * 10.0); b.rotation.y = 10.0;
109 assert!(clip.set_key(2.0, b)); assert!(clip.set_key(0.0, a));
110 let mid = clip.sample(1.0).unwrap();
111 assert!((mid.position.x - 5.0).abs() < 1e-5);
112 assert!(mid.forward().dot(glam::Vec3::NEG_Z) > 0.999);
113 assert!(!clip.set_key(f32::NAN, a));
114 clip.interpolation = Interpolation::Step;
115 assert_eq!(clip.sample(1.0).unwrap().position, Vec3::ZERO);
116 }
117 #[test]
118 fn persisted_clip_runs_and_stops_in_the_shared_runtime() {
119 let mut scene = crate::scene::Scene::default(); let e = scene.spawn("animated");
120 let mut clip = TransformAnimation { looping: false, ..Default::default() };
121 clip.set_key(0.0, Transform::default()); clip.set_key(1.0, Transform::at(Vec3::X));
122 scene.world.insert(e, clip);
123 let reg = crate::registry::Registry::default();
124 let (scene, warnings) = crate::scene::Scene::from_json(®, &scene.to_json(®)).unwrap(); assert!(warnings.is_empty());
125 let mut rt = crate::runtime::Runtime::new(scene, None);
126 for _ in 0..12 { rt.tick(0.1); }
127 let e = crate::hierarchy::find_by_name(&rt.scene.world, "animated").unwrap();
128 assert_eq!(rt.scene.world.get::<Transform>(e).unwrap().position, Vec3::X);
129 assert!(!rt.scene.world.get::<TransformAnimation>(e).unwrap().playing);
130 }
131}