1use super::{Lerp, Easing};
10use glam::{Vec2, Vec3, Vec4};
11
12#[derive(Clone, Debug)]
16pub struct Keyframe<T: Lerp + Clone + std::fmt::Debug> {
17 pub time: f32,
18 pub value: T,
19 pub easing_out: Easing,
21 pub tangent: f32,
23}
24
25impl<T: Lerp + Clone + std::fmt::Debug> Keyframe<T> {
26 pub fn new(time: f32, value: T) -> Self {
27 Self { time, value, easing_out: Easing::EaseInOutCubic, tangent: 1.0 }
28 }
29
30 pub fn with_easing(mut self, easing: Easing) -> Self {
31 self.easing_out = easing;
32 self
33 }
34
35 pub fn linear(time: f32, value: T) -> Self {
36 Self { time, value, easing_out: Easing::Linear, tangent: 1.0 }
37 }
38
39 pub fn step(time: f32, value: T) -> Self {
40 Self { time, value, easing_out: Easing::Step, tangent: 0.0 }
41 }
42}
43
44pub struct KeyframeTrack<T: Lerp + Clone + std::fmt::Debug> {
51 pub frames: Vec<Keyframe<T>>,
52 pub extrapolate: ExtrapolateMode,
53}
54
55#[derive(Clone, Copy, Debug, PartialEq)]
57pub enum ExtrapolateMode {
58 Clamp,
60 Loop,
62 PingPong,
64 Linear,
66}
67
68impl<T: Lerp + Clone + std::fmt::Debug> KeyframeTrack<T> {
69 pub fn new(extrapolate: ExtrapolateMode) -> Self {
70 Self { frames: Vec::new(), extrapolate }
71 }
72
73 pub fn from_keyframes(mut frames: Vec<Keyframe<T>>, extrapolate: ExtrapolateMode) -> Self {
75 frames.sort_by(|a, b| a.time.partial_cmp(&b.time).unwrap_or(std::cmp::Ordering::Equal));
76 Self { frames, extrapolate }
77 }
78
79 pub fn insert(&mut self, frame: Keyframe<T>) {
81 let pos = self.frames.partition_point(|f| f.time < frame.time);
82 self.frames.insert(pos, frame);
83 }
84
85 pub fn is_empty(&self) -> bool { self.frames.is_empty() }
86
87 pub fn duration(&self) -> f32 {
89 if self.frames.len() < 2 { return 0.0; }
90 self.frames.last().unwrap().time - self.frames.first().unwrap().time
91 }
92
93 pub fn start_time(&self) -> f32 {
95 self.frames.first().map(|f| f.time).unwrap_or(0.0)
96 }
97
98 pub fn end_time(&self) -> f32 {
100 self.frames.last().map(|f| f.time).unwrap_or(0.0)
101 }
102
103 pub fn evaluate(&self, time: f32) -> T {
105 if self.frames.is_empty() { return T::zero(); }
106 if self.frames.len() == 1 { return self.frames[0].value.clone(); }
107
108 let (start, end) = (self.start_time(), self.end_time());
109 let span = (end - start).max(f32::EPSILON);
110
111 let local_t = match self.extrapolate {
112 ExtrapolateMode::Clamp => time.clamp(start, end),
113 ExtrapolateMode::Loop => {
114 let offset = time - start;
115 start + ((offset % span) + span) % span
116 }
117 ExtrapolateMode::PingPong => {
118 let offset = (time - start) / span;
119 let cycle = offset.floor() as u32;
120 let frac = offset - offset.floor();
121 start + if cycle % 2 == 0 { frac * span } else { (1.0 - frac) * span }
122 }
123 ExtrapolateMode::Linear => time,
124 };
125
126 let right_idx = self.frames.partition_point(|f| f.time <= local_t);
128
129 if right_idx == 0 {
130 if self.extrapolate == ExtrapolateMode::Linear && self.frames.len() >= 2 {
132 let a = &self.frames[0];
133 let b = &self.frames[1];
134 let seg = (b.time - a.time).max(f32::EPSILON);
135 let t = (local_t - a.time) / seg;
136 return T::lerp(&a.value, &b.value, t);
137 }
138 return self.frames[0].value.clone();
139 }
140
141 if right_idx >= self.frames.len() {
142 if self.extrapolate == ExtrapolateMode::Linear && self.frames.len() >= 2 {
144 let n = self.frames.len();
145 let a = &self.frames[n - 2];
146 let b = &self.frames[n - 1];
147 let seg = (b.time - a.time).max(f32::EPSILON);
148 let t = (local_t - a.time) / seg;
149 return T::lerp(&a.value, &b.value, t);
150 }
151 return self.frames.last().unwrap().value.clone();
152 }
153
154 let left = &self.frames[right_idx - 1];
156 let right = &self.frames[right_idx];
157 let seg_duration = (right.time - left.time).max(f32::EPSILON);
158 let t = ((local_t - left.time) / seg_duration).clamp(0.0, 1.0);
159 let curved_t = left.easing_out.apply(t);
160 T::lerp(&left.value, &right.value, curved_t)
161 }
162
163 pub fn crossing_times(&self, threshold: f32, steps_per_segment: u32) -> Vec<f32>
166 where T: Into<f32> + Copy,
167 {
168 let mut crossings = Vec::new();
169 if self.frames.len() < 2 { return crossings; }
170
171 for w in self.frames.windows(2) {
172 let a = &w[0];
173 let b = &w[1];
174 let seg_duration = (b.time - a.time).max(f32::EPSILON);
175 let dt = seg_duration / steps_per_segment as f32;
176
177 let mut prev_val: f32 = a.value.clone().into();
178 let mut prev_t = a.time;
179
180 for s in 1..=steps_per_segment {
181 let t = a.time + s as f32 * dt;
182 let v: f32 = self.evaluate(t).into();
183 if (prev_val < threshold) != (v < threshold) {
184 let cross_frac = (threshold - prev_val) / (v - prev_val).max(f32::EPSILON);
186 crossings.push(prev_t + cross_frac * dt);
187 }
188 prev_val = v;
189 prev_t = t;
190 }
191 }
192 crossings
193 }
194
195 pub fn bake(&self, step: f32) -> Vec<(f32, T)> {
197 if self.frames.is_empty() { return Vec::new(); }
198 let start = self.start_time();
199 let end = self.end_time();
200 let mut result = Vec::new();
201 let mut t = start;
202 while t <= end + f32::EPSILON {
203 result.push((t, self.evaluate(t)));
204 t += step;
205 }
206 result
207 }
208}
209
210pub struct MultiTrack {
216 pub tracks: std::collections::HashMap<String, KeyframeTrack<f32>>,
217 pub elapsed: f32,
218 pub looping: bool,
219 duration: f32,
220}
221
222impl MultiTrack {
223 pub fn new(looping: bool) -> Self {
224 Self { tracks: std::collections::HashMap::new(), elapsed: 0.0, looping, duration: 0.0 }
225 }
226
227 pub fn add(&mut self, name: impl Into<String>, track: KeyframeTrack<f32>) {
228 self.duration = self.duration.max(track.end_time());
229 self.tracks.insert(name.into(), track);
230 }
231
232 pub fn tick(&mut self, dt: f32) {
233 self.elapsed += dt;
234 if self.looping && self.elapsed > self.duration {
235 self.elapsed -= self.duration;
236 }
237 }
238
239 pub fn get(&self, name: &str) -> f32 {
240 self.tracks.get(name).map(|t| t.evaluate(self.elapsed)).unwrap_or(0.0)
241 }
242
243 pub fn is_done(&self) -> bool {
244 !self.looping && self.elapsed >= self.duration
245 }
246
247 pub fn reset(&mut self) { self.elapsed = 0.0; }
248
249 pub fn duration(&self) -> f32 { self.duration }
250}
251
252pub struct CameraPath {
258 pub positions: KeyframeTrack<Vec3>,
259 pub targets: KeyframeTrack<Vec3>,
260 pub fov: KeyframeTrack<f32>,
261 pub elapsed: f32,
262 pub speed: f32,
263 pub looping: bool,
264}
265
266impl CameraPath {
267 pub fn new(speed: f32, looping: bool) -> Self {
268 Self {
269 positions: KeyframeTrack::new(
270 if looping { ExtrapolateMode::Loop } else { ExtrapolateMode::Clamp }
271 ),
272 targets: KeyframeTrack::new(
273 if looping { ExtrapolateMode::Loop } else { ExtrapolateMode::Clamp }
274 ),
275 fov: KeyframeTrack::new(ExtrapolateMode::Clamp),
276 elapsed: 0.0,
277 speed,
278 looping,
279 }
280 }
281
282 pub fn add_waypoint(&mut self, time: f32, position: Vec3, target: Vec3, fov: f32) {
284 self.positions.insert(Keyframe::new(time, position)
285 .with_easing(Easing::EaseInOutCubic));
286 self.targets.insert(Keyframe::new(time, target)
287 .with_easing(Easing::EaseInOutCubic));
288 self.fov.insert(Keyframe::new(time, fov)
289 .with_easing(Easing::EaseInOutSine));
290 }
291
292 pub fn tick(&mut self, dt: f32) {
293 self.elapsed += dt * self.speed;
294 let duration = self.positions.end_time();
295 if self.looping && self.elapsed > duration {
296 self.elapsed -= duration;
297 }
298 }
299
300 pub fn position(&self) -> Vec3 { self.positions.evaluate(self.elapsed) }
301 pub fn target(&self) -> Vec3 { self.targets.evaluate(self.elapsed) }
302 pub fn fov(&self) -> f32 { self.fov.evaluate(self.elapsed) }
303
304 pub fn is_done(&self) -> bool {
305 !self.looping && self.elapsed >= self.positions.end_time()
306 }
307
308 pub fn orbit(center: Vec3, radius: f32, height: f32, duration: f32, fov: f32) -> Self {
310 let mut path = Self::new(1.0, true);
311 let steps = 16;
312 for i in 0..=steps {
313 let angle = (i as f32 / steps as f32) * std::f32::consts::TAU;
314 let pos = center + Vec3::new(angle.cos() * radius, height, angle.sin() * radius);
315 let t = (i as f32 / steps as f32) * duration;
316 path.add_waypoint(t, pos, center, fov);
317 }
318 path
319 }
320
321 pub fn flythrough(waypoints: &[(Vec3, Vec3)], duration_each: f32, fov: f32) -> Self {
323 let mut path = Self::new(1.0, false);
324 for (i, (pos, target)) in waypoints.iter().enumerate() {
325 path.add_waypoint(i as f32 * duration_each, *pos, *target, fov);
326 }
327 path
328 }
329}
330
331#[cfg(test)]
332mod tests {
333 use super::*;
334 use glam::Vec3;
335
336 #[test]
337 fn track_clamp_extrapolation() {
338 let mut track: KeyframeTrack<f32> = KeyframeTrack::new(ExtrapolateMode::Clamp);
339 track.insert(Keyframe::linear(0.0, 0.0));
340 track.insert(Keyframe::linear(1.0, 1.0));
341 assert!((track.evaluate(-1.0) - 0.0).abs() < 1e-5, "before start should clamp to first");
342 assert!((track.evaluate(2.0) - 1.0).abs() < 1e-5, "after end should clamp to last");
343 }
344
345 #[test]
346 fn track_midpoint_linear() {
347 let mut track: KeyframeTrack<f32> = KeyframeTrack::new(ExtrapolateMode::Clamp);
348 track.insert(Keyframe::linear(0.0, 0.0));
349 track.insert(Keyframe::linear(2.0, 4.0));
350 let mid = track.evaluate(1.0);
351 assert!((mid - 2.0).abs() < 1e-4, "midpoint of linear should be 2.0, got {mid}");
352 }
353
354 #[test]
355 fn track_loop_wraps() {
356 let mut track: KeyframeTrack<f32> = KeyframeTrack::new(ExtrapolateMode::Loop);
357 track.insert(Keyframe::linear(0.0, 0.0));
358 track.insert(Keyframe::linear(1.0, 1.0));
359 let v = track.evaluate(1.5);
360 assert!(v >= 0.0 && v <= 1.0, "looped value should wrap: {v}");
361 }
362
363 #[test]
364 fn track_sorted_on_insert() {
365 let mut track: KeyframeTrack<f32> = KeyframeTrack::new(ExtrapolateMode::Clamp);
366 track.insert(Keyframe::linear(2.0, 2.0));
367 track.insert(Keyframe::linear(0.0, 0.0));
368 track.insert(Keyframe::linear(1.0, 1.0));
369 assert_eq!(track.frames[0].time, 0.0);
370 assert_eq!(track.frames[1].time, 1.0);
371 assert_eq!(track.frames[2].time, 2.0);
372 }
373
374 #[test]
375 fn vec3_track_interpolates() {
376 let mut track: KeyframeTrack<Vec3> = KeyframeTrack::new(ExtrapolateMode::Clamp);
377 track.insert(Keyframe::linear(0.0, Vec3::ZERO));
378 track.insert(Keyframe::linear(1.0, Vec3::ONE));
379 let mid = track.evaluate(0.5);
380 assert!((mid.x - 0.5).abs() < 1e-4);
381 }
382
383 #[test]
384 fn bake_returns_correct_count() {
385 let mut track: KeyframeTrack<f32> = KeyframeTrack::new(ExtrapolateMode::Clamp);
386 track.insert(Keyframe::linear(0.0, 0.0));
387 track.insert(Keyframe::linear(1.0, 1.0));
388 let baked = track.bake(0.1);
389 assert!(baked.len() >= 10 && baked.len() <= 12, "expected ~11 samples, got {}", baked.len());
390 }
391
392 #[test]
393 fn camera_path_orbit() {
394 let path = CameraPath::orbit(Vec3::ZERO, 5.0, 2.0, 10.0, 60.0);
395 let pos = path.position();
396 let dist = glam::Vec2::new(pos.x, pos.z).length();
397 assert!((dist - 5.0).abs() < 0.5, "orbit radius should be ~5, got {dist}");
398 }
399}