Skip to main content

clasp_core/
timeline.rs

1//! Timeline execution engine
2//!
3//! Provides interpolation and playback of timeline automation data.
4//!
5//! # Example
6//!
7//! ```ignore
8//! use clasp_core::{TimelineData, TimelineKeyframe, EasingType, Value, timeline::TimelinePlayer};
9//!
10//! let timeline = TimelineData::new(vec![
11//!     TimelineKeyframe { time: 0, value: Value::Float(0.0), easing: EasingType::Linear, bezier: None },
12//!     TimelineKeyframe { time: 1_000_000, value: Value::Float(1.0), easing: EasingType::EaseOut, bezier: None },
13//! ]);
14//!
15//! let mut player = TimelinePlayer::new(timeline);
16//! player.start(current_time_us);
17//!
18//! // Later...
19//! if let Some(value) = player.sample(current_time_us) {
20//!     // Use interpolated value
21//! }
22//! ```
23
24use crate::{EasingType, TimelineData, TimelineKeyframe, Value};
25
26/// State of timeline playback
27#[derive(Debug, Clone, Copy, PartialEq, Eq)]
28pub enum PlaybackState {
29    /// Not started
30    Stopped,
31    /// Playing forward
32    Playing,
33    /// Paused at current position
34    Paused,
35    /// Completed (not looping)
36    Finished,
37}
38
39/// Timeline player for real-time interpolation
40#[derive(Debug, Clone)]
41pub struct TimelinePlayer {
42    /// The timeline data
43    timeline: TimelineData,
44    /// Current playback state
45    state: PlaybackState,
46    /// Start time in microseconds (server time)
47    start_time: u64,
48    /// Pause time (for resume)
49    pause_time: Option<u64>,
50    /// Current loop iteration
51    loop_count: u32,
52}
53
54impl TimelinePlayer {
55    /// Create a new timeline player
56    pub fn new(timeline: TimelineData) -> Self {
57        Self {
58            timeline,
59            state: PlaybackState::Stopped,
60            start_time: 0,
61            pause_time: None,
62            loop_count: 0,
63        }
64    }
65
66    /// Start playback from the beginning
67    pub fn start(&mut self, current_time_us: u64) {
68        self.start_time = current_time_us;
69        self.state = PlaybackState::Playing;
70        self.pause_time = None;
71        self.loop_count = 0;
72    }
73
74    /// Start playback at a specific time
75    pub fn start_at(&mut self, start_time_us: u64) {
76        self.start_time = start_time_us;
77        self.state = PlaybackState::Playing;
78        self.pause_time = None;
79        self.loop_count = 0;
80    }
81
82    /// Pause playback
83    pub fn pause(&mut self, current_time_us: u64) {
84        if self.state == PlaybackState::Playing {
85            self.pause_time = Some(current_time_us);
86            self.state = PlaybackState::Paused;
87        }
88    }
89
90    /// Resume playback
91    pub fn resume(&mut self, current_time_us: u64) {
92        if self.state == PlaybackState::Paused {
93            if let Some(pause_time) = self.pause_time {
94                // Adjust start time to account for pause duration
95                let pause_duration = current_time_us.saturating_sub(pause_time);
96                self.start_time = self.start_time.saturating_add(pause_duration);
97            }
98            self.state = PlaybackState::Playing;
99            self.pause_time = None;
100        }
101    }
102
103    /// Stop playback
104    pub fn stop(&mut self) {
105        self.state = PlaybackState::Stopped;
106        self.pause_time = None;
107    }
108
109    /// Get current playback state
110    pub fn state(&self) -> PlaybackState {
111        self.state
112    }
113
114    /// Get current loop count
115    pub fn loop_count(&self) -> u32 {
116        self.loop_count
117    }
118
119    /// Get timeline duration
120    pub fn duration(&self) -> u64 {
121        self.timeline.duration()
122    }
123
124    /// Sample the timeline at the current time
125    ///
126    /// Returns None if stopped or no keyframes exist.
127    /// Returns the interpolated value based on current time.
128    pub fn sample(&mut self, current_time_us: u64) -> Option<Value> {
129        if self.state == PlaybackState::Stopped {
130            return None;
131        }
132
133        if self.timeline.keyframes.is_empty() {
134            return None;
135        }
136
137        // Calculate elapsed time
138        let elapsed = if self.state == PlaybackState::Paused {
139            self.pause_time.unwrap_or(current_time_us) - self.start_time
140        } else {
141            current_time_us.saturating_sub(self.start_time)
142        };
143
144        let duration = self.timeline.duration();
145        if duration == 0 {
146            return Some(self.timeline.keyframes[0].value.clone());
147        }
148
149        // Handle looping
150        let position = if self.timeline.loop_ {
151            let new_loop_count = (elapsed / duration) as u32;
152            if new_loop_count > self.loop_count {
153                self.loop_count = new_loop_count;
154            }
155            elapsed % duration
156        } else if elapsed >= duration {
157            self.state = PlaybackState::Finished;
158            return Some(self.timeline.keyframes.last()?.value.clone());
159        } else {
160            elapsed
161        };
162
163        // Find surrounding keyframes
164        let (prev_kf, next_kf) = self.find_keyframes(position)?;
165
166        // Calculate interpolation factor
167        let segment_duration = next_kf.time.saturating_sub(prev_kf.time);
168        if segment_duration == 0 {
169            return Some(prev_kf.value.clone());
170        }
171
172        let local_t = (position - prev_kf.time) as f64 / segment_duration as f64;
173        let eased_t = apply_easing(local_t, prev_kf.easing, prev_kf.bezier);
174
175        // Interpolate value
176        Some(interpolate_value(&prev_kf.value, &next_kf.value, eased_t))
177    }
178
179    /// Find the keyframes surrounding the given position
180    fn find_keyframes(&self, position: u64) -> Option<(&TimelineKeyframe, &TimelineKeyframe)> {
181        let keyframes = &self.timeline.keyframes;
182
183        if keyframes.is_empty() {
184            return None;
185        }
186
187        if keyframes.len() == 1 {
188            return Some((&keyframes[0], &keyframes[0]));
189        }
190
191        // Find the first keyframe after position
192        let next_idx = keyframes
193            .iter()
194            .position(|kf| kf.time > position)
195            .unwrap_or(keyframes.len());
196
197        if next_idx == 0 {
198            // Before first keyframe
199            Some((&keyframes[0], &keyframes[0]))
200        } else if next_idx >= keyframes.len() {
201            // After last keyframe
202            let last = &keyframes[keyframes.len() - 1];
203            Some((last, last))
204        } else {
205            Some((&keyframes[next_idx - 1], &keyframes[next_idx]))
206        }
207    }
208}
209
210/// Apply easing function to normalized time (0.0 - 1.0)
211fn apply_easing(t: f64, easing: EasingType, bezier: Option<[f64; 4]>) -> f64 {
212    let t = t.clamp(0.0, 1.0);
213
214    match easing {
215        EasingType::Linear => t,
216        EasingType::EaseIn => t * t,
217        EasingType::EaseOut => 1.0 - (1.0 - t) * (1.0 - t),
218        EasingType::EaseInOut => {
219            if t < 0.5 {
220                2.0 * t * t
221            } else {
222                1.0 - (-2.0 * t + 2.0).powi(2) / 2.0
223            }
224        }
225        EasingType::Step => {
226            if t < 1.0 {
227                0.0
228            } else {
229                1.0
230            }
231        }
232        EasingType::CubicBezier => {
233            if let Some([x1, y1, x2, y2]) = bezier {
234                cubic_bezier(t, x1, y1, x2, y2)
235            } else {
236                t // Fall back to linear if no control points
237            }
238        }
239    }
240}
241
242/// Cubic bezier interpolation
243/// Control points define the curve: P0=(0,0), P1=(x1,y1), P2=(x2,y2), P3=(1,1)
244fn cubic_bezier(t: f64, x1: f64, y1: f64, x2: f64, y2: f64) -> f64 {
245    // For simplicity, we approximate by using Newton-Raphson to find t for x, then compute y
246    // This is a simplified implementation - full CSS cubic-bezier would use more iterations
247
248    // Solve for parameter u where bezier_x(u) = t
249    let mut u = t;
250    for _ in 0..8 {
251        let x = bezier_sample(u, x1, x2);
252        let dx = bezier_derivative(u, x1, x2);
253        if dx.abs() < 1e-10 {
254            break;
255        }
256        u -= (x - t) / dx;
257        u = u.clamp(0.0, 1.0);
258    }
259
260    // Compute y at parameter u
261    bezier_sample(u, y1, y2)
262}
263
264fn bezier_sample(t: f64, p1: f64, p2: f64) -> f64 {
265    // B(t) = 3(1-t)²t*P1 + 3(1-t)t²*P2 + t³
266    let mt = 1.0 - t;
267    3.0 * mt * mt * t * p1 + 3.0 * mt * t * t * p2 + t * t * t
268}
269
270fn bezier_derivative(t: f64, p1: f64, p2: f64) -> f64 {
271    // B'(t) = 3(1-t)²*P1 + 6(1-t)t*(P2-P1) + 3t²*(1-P2)
272    let mt = 1.0 - t;
273    3.0 * mt * mt * p1 + 6.0 * mt * t * (p2 - p1) + 3.0 * t * t * (1.0 - p2)
274}
275
276/// Interpolate between two values
277fn interpolate_value(a: &Value, b: &Value, t: f64) -> Value {
278    match (a, b) {
279        (Value::Float(a), Value::Float(b)) => Value::Float(a + (b - a) * t),
280        (Value::Int(a), Value::Int(b)) => Value::Int(*a + ((*b - *a) as f64 * t) as i64),
281        (Value::Array(arr_a), Value::Array(arr_b)) if arr_a.len() == arr_b.len() => Value::Array(
282            arr_a
283                .iter()
284                .zip(arr_b.iter())
285                .map(|(a, b)| interpolate_value(a, b, t))
286                .collect(),
287        ),
288        // For non-interpolatable values, use step at 0.5
289        _ => {
290            if t < 0.5 {
291                a.clone()
292            } else {
293                b.clone()
294            }
295        }
296    }
297}
298
299#[cfg(test)]
300mod tests {
301    use super::*;
302
303    fn make_simple_timeline() -> TimelineData {
304        TimelineData::new(vec![
305            TimelineKeyframe {
306                time: 0,
307                value: Value::Float(0.0),
308                easing: EasingType::Linear,
309                bezier: None,
310            },
311            TimelineKeyframe {
312                time: 1_000_000, // 1 second
313                value: Value::Float(100.0),
314                easing: EasingType::Linear,
315                bezier: None,
316            },
317        ])
318    }
319
320    #[test]
321    fn test_timeline_player_creation() {
322        let timeline = make_simple_timeline();
323        let player = TimelinePlayer::new(timeline);
324        assert_eq!(player.state(), PlaybackState::Stopped);
325    }
326
327    #[test]
328    fn test_timeline_player_start() {
329        let timeline = make_simple_timeline();
330        let mut player = TimelinePlayer::new(timeline);
331
332        player.start(0);
333        assert_eq!(player.state(), PlaybackState::Playing);
334    }
335
336    #[test]
337    fn test_timeline_linear_interpolation() {
338        let timeline = make_simple_timeline();
339        let mut player = TimelinePlayer::new(timeline);
340
341        player.start(0);
342
343        // At t=0
344        let val = player.sample(0).unwrap();
345        assert!(matches!(val, Value::Float(v) if (v - 0.0).abs() < 0.01));
346
347        // At t=0.5s (500ms)
348        let val = player.sample(500_000).unwrap();
349        assert!(matches!(val, Value::Float(v) if (v - 50.0).abs() < 0.01));
350
351        // At t=1s
352        let val = player.sample(1_000_000).unwrap();
353        assert!(matches!(val, Value::Float(v) if (v - 100.0).abs() < 0.01));
354    }
355
356    #[test]
357    fn test_timeline_finished_state() {
358        let timeline = make_simple_timeline();
359        let mut player = TimelinePlayer::new(timeline);
360
361        player.start(0);
362
363        // After timeline ends
364        let _ = player.sample(2_000_000);
365        assert_eq!(player.state(), PlaybackState::Finished);
366    }
367
368    #[test]
369    fn test_timeline_looping() {
370        let timeline = make_simple_timeline().with_loop(true);
371        let mut player = TimelinePlayer::new(timeline);
372
373        player.start(0);
374
375        // First loop
376        let val = player.sample(500_000).unwrap();
377        assert!(matches!(val, Value::Float(v) if (v - 50.0).abs() < 0.01));
378
379        // Second loop (at 1.5s = 500ms into second loop)
380        let val = player.sample(1_500_000).unwrap();
381        assert!(matches!(val, Value::Float(v) if (v - 50.0).abs() < 0.01));
382
383        assert_eq!(player.loop_count(), 1);
384    }
385
386    #[test]
387    fn test_timeline_pause_resume() {
388        let timeline = make_simple_timeline();
389        let mut player = TimelinePlayer::new(timeline);
390
391        player.start(0);
392
393        // Play to 250ms
394        let _ = player.sample(250_000);
395
396        // Pause at 250ms
397        player.pause(250_000);
398        assert_eq!(player.state(), PlaybackState::Paused);
399
400        // Time passes (500ms later)
401        let val = player.sample(750_000).unwrap();
402        // Should still be at 250ms position (25.0)
403        assert!(matches!(val, Value::Float(v) if (v - 25.0).abs() < 0.01));
404
405        // Resume at 750ms
406        player.resume(750_000);
407        assert_eq!(player.state(), PlaybackState::Playing);
408
409        // 250ms later (1000ms total, but only 500ms of playback)
410        let val = player.sample(1_000_000).unwrap();
411        // Should be at 500ms position (50.0)
412        assert!(matches!(val, Value::Float(v) if (v - 50.0).abs() < 0.01));
413    }
414
415    #[test]
416    fn test_easing_ease_in() {
417        let timeline = TimelineData::new(vec![
418            TimelineKeyframe {
419                time: 0,
420                value: Value::Float(0.0),
421                easing: EasingType::EaseIn,
422                bezier: None,
423            },
424            TimelineKeyframe {
425                time: 1_000_000,
426                value: Value::Float(100.0),
427                easing: EasingType::Linear,
428                bezier: None,
429            },
430        ]);
431        let mut player = TimelinePlayer::new(timeline);
432        player.start(0);
433
434        // At t=0.5, ease-in should be less than linear (25 instead of 50)
435        let val = player.sample(500_000).unwrap();
436        assert!(matches!(val, Value::Float(v) if v < 50.0));
437    }
438
439    #[test]
440    fn test_easing_ease_out() {
441        let timeline = TimelineData::new(vec![
442            TimelineKeyframe {
443                time: 0,
444                value: Value::Float(0.0),
445                easing: EasingType::EaseOut,
446                bezier: None,
447            },
448            TimelineKeyframe {
449                time: 1_000_000,
450                value: Value::Float(100.0),
451                easing: EasingType::Linear,
452                bezier: None,
453            },
454        ]);
455        let mut player = TimelinePlayer::new(timeline);
456        player.start(0);
457
458        // At t=0.5, ease-out should be more than linear (75 instead of 50)
459        let val = player.sample(500_000).unwrap();
460        assert!(matches!(val, Value::Float(v) if v > 50.0));
461    }
462
463    #[test]
464    fn test_array_interpolation() {
465        let timeline = TimelineData::new(vec![
466            TimelineKeyframe {
467                time: 0,
468                value: Value::Array(vec![Value::Float(0.0), Value::Float(0.0)]),
469                easing: EasingType::Linear,
470                bezier: None,
471            },
472            TimelineKeyframe {
473                time: 1_000_000,
474                value: Value::Array(vec![Value::Float(100.0), Value::Float(200.0)]),
475                easing: EasingType::Linear,
476                bezier: None,
477            },
478        ]);
479        let mut player = TimelinePlayer::new(timeline);
480        player.start(0);
481
482        let val = player.sample(500_000).unwrap();
483        if let Value::Array(arr) = val {
484            assert!(matches!(arr[0], Value::Float(v) if (v - 50.0).abs() < 0.01));
485            assert!(matches!(arr[1], Value::Float(v) if (v - 100.0).abs() < 0.01));
486        } else {
487            panic!("Expected array value");
488        }
489    }
490}