Skip to main content

proof_engine/editor/
cinematic_editor.rs

1
2//! Cinematic / sequencer editor — timeline tracks, keyframes, camera cuts, director sequences.
3
4use glam::{Vec2, Vec3, Vec4, Quat};
5use std::collections::HashMap;
6
7// ---------------------------------------------------------------------------
8// Time / playback
9// ---------------------------------------------------------------------------
10
11#[derive(Debug, Clone, Copy, PartialEq)]
12pub enum LoopMode { Hold, Loop, PingPong }
13
14#[derive(Debug, Clone, Copy)]
15pub struct TimeRange {
16    pub start: f64,
17    pub end: f64,
18}
19
20impl TimeRange {
21    pub fn new(start: f64, end: f64) -> Self { Self { start, end } }
22    pub fn duration(&self) -> f64 { self.end - self.start }
23    pub fn contains(&self, t: f64) -> bool { t >= self.start && t <= self.end }
24    pub fn normalized(&self, t: f64) -> f64 { ((t - self.start) / self.duration().max(1e-10)).clamp(0.0, 1.0) }
25}
26
27// ---------------------------------------------------------------------------
28// Easing
29// ---------------------------------------------------------------------------
30
31#[derive(Debug, Clone, Copy, PartialEq)]
32pub enum EasingType { Linear, SmoothStep, SmootherStep, EaseIn, EaseOut, EaseInOut, Spring, Bounce, Elastic, Back, Custom }
33
34impl EasingType {
35    pub fn apply(self, t: f32) -> f32 {
36        let t = t.clamp(0.0, 1.0);
37        match self {
38            EasingType::Linear => t,
39            EasingType::SmoothStep => t * t * (3.0 - 2.0 * t),
40            EasingType::SmootherStep => t * t * t * (t * (t * 6.0 - 15.0) + 10.0),
41            EasingType::EaseIn => t * t,
42            EasingType::EaseOut => 1.0 - (1.0 - t) * (1.0 - t),
43            EasingType::EaseInOut => if t < 0.5 { 2.0 * t * t } else { 1.0 - (-2.0 * t + 2.0).powi(2) / 2.0 },
44            EasingType::Spring => {
45                let c4 = 2.0 * std::f32::consts::PI / 3.0;
46                if t <= 0.0 { 0.0 } else if t >= 1.0 { 1.0 } else {
47                    -(2.0_f32.powf(10.0 * t - 10.0)) * ((t * 10.0 - 10.75) * c4).sin()
48                }
49            }
50            EasingType::Bounce => {
51                let n1 = 7.5625_f32;
52                let d1 = 2.75_f32;
53                let t = 1.0 - t;
54                let v = if t < 1.0 / d1 { n1 * t * t }
55                    else if t < 2.0 / d1 { let t = t - 1.5 / d1; n1 * t * t + 0.75 }
56                    else if t < 2.5 / d1 { let t = t - 2.25 / d1; n1 * t * t + 0.9375 }
57                    else { let t = t - 2.625 / d1; n1 * t * t + 0.984375 };
58                1.0 - v
59            }
60            EasingType::Elastic => {
61                let c = 2.0 * std::f32::consts::PI / 3.0;
62                if t <= 0.0 { 0.0 } else if t >= 1.0 { 1.0 } else {
63                    2.0_f32.powf(-10.0 * t) * ((t * 10.0 - 0.75) * c).sin() + 1.0
64                }
65            }
66            EasingType::Back => {
67                let c1 = 1.70158_f32;
68                let c3 = c1 + 1.0;
69                1.0 + c3 * (t - 1.0).powi(3) + c1 * (t - 1.0).powi(2)
70            }
71            EasingType::Custom => t,
72        }
73    }
74}
75
76// ---------------------------------------------------------------------------
77// Keyframe types
78// ---------------------------------------------------------------------------
79
80#[derive(Debug, Clone)]
81pub enum KeyframeValue {
82    Float(f32),
83    Vec2(Vec2),
84    Vec3(Vec3),
85    Vec4(Vec4),
86    Quat(Quat),
87    Bool(bool),
88    Int(i32),
89    String(String),
90    Color(Vec4),
91    Event(String, Vec<String>), // event_name, args
92}
93
94#[derive(Debug, Clone)]
95pub struct Keyframe {
96    pub time: f64,
97    pub value: KeyframeValue,
98    pub in_tangent: f32,
99    pub out_tangent: f32,
100    pub easing: EasingType,
101    pub broken_tangents: bool,
102}
103
104impl Keyframe {
105    pub fn new(time: f64, value: KeyframeValue) -> Self {
106        Self { time, value, in_tangent: 0.0, out_tangent: 0.0, easing: EasingType::SmoothStep, broken_tangents: false }
107    }
108    pub fn event(time: f64, name: impl Into<String>) -> Self {
109        Self::new(time, KeyframeValue::Event(name.into(), Vec::new()))
110    }
111}
112
113// ---------------------------------------------------------------------------
114// Tracks
115// ---------------------------------------------------------------------------
116
117#[derive(Debug, Clone, Copy, PartialEq)]
118pub enum TrackKind {
119    Transform,
120    Rotation,
121    Scale,
122    FloatProperty,
123    ColorProperty,
124    BoolProperty,
125    IntProperty,
126    EventTrack,
127    AudioTrack,
128    VideoTrack,
129    CameraTrack,
130    SubsequenceTrack,
131    ActivationTrack,
132    ControlTrack,
133    AnimationTrack,
134    MaterialTrack,
135    ParticleTrack,
136    TextTrack,
137    CustomPropTrack,
138}
139
140impl TrackKind {
141    pub fn label(self) -> &'static str {
142        match self {
143            TrackKind::Transform => "Transform",
144            TrackKind::Rotation => "Rotation",
145            TrackKind::Scale => "Scale",
146            TrackKind::FloatProperty => "Float Property",
147            TrackKind::ColorProperty => "Color",
148            TrackKind::BoolProperty => "Bool",
149            TrackKind::IntProperty => "Int",
150            TrackKind::EventTrack => "Event",
151            TrackKind::AudioTrack => "Audio",
152            TrackKind::VideoTrack => "Video",
153            TrackKind::CameraTrack => "Camera",
154            TrackKind::SubsequenceTrack => "Subsequence",
155            TrackKind::ActivationTrack => "Activation",
156            TrackKind::ControlTrack => "Control",
157            TrackKind::AnimationTrack => "Animation",
158            TrackKind::MaterialTrack => "Material",
159            TrackKind::ParticleTrack => "Particle",
160            TrackKind::TextTrack => "Text",
161            TrackKind::CustomPropTrack => "Custom Property",
162        }
163    }
164}
165
166#[derive(Debug, Clone)]
167pub struct TrackClip {
168    pub id: u32,
169    pub start: f64,
170    pub end: f64,
171    pub clip_in: f64,      // within-asset start
172    pub speed: f64,
173    pub blend_in: f64,
174    pub blend_out: f64,
175    pub asset_id: Option<u64>,
176    pub label: String,
177    pub muted: bool,
178    pub locked: bool,
179    pub color: Vec4,
180    pub loop_mode: LoopMode,
181}
182
183impl TrackClip {
184    pub fn new(id: u32, start: f64, duration: f64, label: impl Into<String>) -> Self {
185        Self {
186            id, start, end: start + duration, clip_in: 0.0, speed: 1.0,
187            blend_in: 0.0, blend_out: 0.0, asset_id: None,
188            label: label.into(), muted: false, locked: false,
189            color: Vec4::new(0.2, 0.5, 0.8, 1.0), loop_mode: LoopMode::Hold,
190        }
191    }
192
193    pub fn duration(&self) -> f64 { self.end - self.start }
194
195    pub fn local_time(&self, global_t: f64) -> f64 {
196        let t = (global_t - self.start) * self.speed + self.clip_in;
197        t.max(0.0)
198    }
199
200    pub fn blend_weight(&self, global_t: f64) -> f32 {
201        if global_t < self.start || global_t > self.end { return 0.0; }
202        let t = global_t - self.start;
203        let dur = self.duration();
204        let from_start = if self.blend_in > 0.0 { (t / self.blend_in).min(1.0) as f32 } else { 1.0 };
205        let from_end = if self.blend_out > 0.0 { ((dur - t) / self.blend_out).min(1.0) as f32 } else { 1.0 };
206        from_start.min(from_end)
207    }
208
209    pub fn overlaps(&self, other: &Self) -> bool {
210        self.start < other.end && self.end > other.start
211    }
212}
213
214#[derive(Debug, Clone)]
215pub struct SequenceTrack {
216    pub id: u32,
217    pub name: String,
218    pub kind: TrackKind,
219    pub entity_id: Option<u64>,
220    pub property_path: String,
221    pub keyframes: Vec<Keyframe>,
222    pub clips: Vec<TrackClip>,
223    pub muted: bool,
224    pub locked: bool,
225    pub solo: bool,
226    pub collapsed: bool,
227    pub height: f32,
228    pub color: Vec4,
229    pub subtracks: Vec<SequenceTrack>,
230    pub infinite_clip_active: bool,
231}
232
233impl SequenceTrack {
234    pub fn new(id: u32, name: impl Into<String>, kind: TrackKind) -> Self {
235        Self {
236            id, name: name.into(), kind, entity_id: None, property_path: String::new(),
237            keyframes: Vec::new(), clips: Vec::new(),
238            muted: false, locked: false, solo: false, collapsed: false,
239            height: 36.0, color: Vec4::new(0.25, 0.55, 0.78, 1.0),
240            subtracks: Vec::new(), infinite_clip_active: false,
241        }
242    }
243
244    pub fn add_keyframe(&mut self, kf: Keyframe) {
245        let i = self.keyframes.partition_point(|k| k.time <= kf.time);
246        self.keyframes.insert(i, kf);
247    }
248
249    pub fn add_clip(&mut self, clip: TrackClip) {
250        self.clips.push(clip);
251        self.clips.sort_by(|a, b| a.start.partial_cmp(&b.start).unwrap_or(std::cmp::Ordering::Equal));
252    }
253
254    pub fn remove_keyframe_at(&mut self, time: f64, tolerance: f64) {
255        self.keyframes.retain(|k| (k.time - time).abs() > tolerance);
256    }
257
258    pub fn evaluate_float(&self, t: f64) -> f32 {
259        if self.keyframes.is_empty() { return 0.0; }
260        let i = self.keyframes.partition_point(|k| k.time <= t);
261        if i == 0 {
262            return match &self.keyframes[0].value { KeyframeValue::Float(v) => *v, _ => 0.0 };
263        }
264        if i >= self.keyframes.len() {
265            let last = self.keyframes.len() - 1;
266            return match &self.keyframes[last].value { KeyframeValue::Float(v) => *v, _ => 0.0 };
267        }
268        let a = &self.keyframes[i-1];
269        let b = &self.keyframes[i];
270        let u = ((t - a.time) / (b.time - a.time).max(1e-10)) as f32;
271        let u = a.easing.apply(u);
272        match (&a.value, &b.value) {
273            (KeyframeValue::Float(va), KeyframeValue::Float(vb)) => va + (vb - va) * u,
274            _ => 0.0,
275        }
276    }
277
278    pub fn evaluate_vec3(&self, t: f64) -> Vec3 {
279        if self.keyframes.is_empty() { return Vec3::ZERO; }
280        let i = self.keyframes.partition_point(|k| k.time <= t);
281        if i == 0 { return match &self.keyframes[0].value { KeyframeValue::Vec3(v) => *v, _ => Vec3::ZERO }; }
282        if i >= self.keyframes.len() {
283            let last = self.keyframes.len() - 1;
284            return match &self.keyframes[last].value { KeyframeValue::Vec3(v) => *v, _ => Vec3::ZERO };
285        }
286        let a = &self.keyframes[i-1];
287        let b = &self.keyframes[i];
288        let u = ((t - a.time) / (b.time - a.time).max(1e-10)) as f32;
289        let u = a.easing.apply(u);
290        match (&a.value, &b.value) {
291            (KeyframeValue::Vec3(va), KeyframeValue::Vec3(vb)) => va.lerp(*vb, u),
292            _ => Vec3::ZERO,
293        }
294    }
295
296    pub fn duration(&self) -> f64 {
297        let kf_end = self.keyframes.last().map(|k| k.time).unwrap_or(0.0);
298        let clip_end = self.clips.iter().map(|c| c.end).fold(0.0_f64, f64::max);
299        kf_end.max(clip_end)
300    }
301}
302
303// ---------------------------------------------------------------------------
304// Camera cuts and shots
305// ---------------------------------------------------------------------------
306
307#[derive(Debug, Clone, Copy, PartialEq)]
308pub enum CameraBlendType { Cut, Linear, EaseIn, EaseOut, EaseInOut, Custom }
309
310#[derive(Debug, Clone)]
311pub struct CameraShot {
312    pub id: u32,
313    pub start: f64,
314    pub duration: f64,
315    pub camera_entity: u64,
316    pub blend_in: CameraBlendType,
317    pub blend_out: CameraBlendType,
318    pub blend_duration: f64,
319    pub look_at_target: Option<u64>,
320    pub fov_override: Option<f32>,
321}
322
323impl CameraShot {
324    pub fn cut(id: u32, start: f64, duration: f64, cam_entity: u64) -> Self {
325        Self {
326            id, start, duration, camera_entity: cam_entity,
327            blend_in: CameraBlendType::Cut, blend_out: CameraBlendType::Cut,
328            blend_duration: 0.0, look_at_target: None, fov_override: None,
329        }
330    }
331}
332
333// ---------------------------------------------------------------------------
334// Sequence (master timeline)
335// ---------------------------------------------------------------------------
336
337#[derive(Debug, Clone)]
338pub struct Sequence {
339    pub id: u64,
340    pub name: String,
341    pub duration: f64,
342    pub frame_rate: f64,
343    pub tracks: Vec<SequenceTrack>,
344    pub camera_shots: Vec<CameraShot>,
345    pub markers: Vec<(f64, String)>,
346    pub loop_mode: LoopMode,
347    pub current_time: f64,
348    pub is_playing: bool,
349    pub playback_speed: f64,
350    pub gravity: bool,
351    pub next_track_id: u32,
352    pub next_clip_id: u32,
353    pub next_shot_id: u32,
354}
355
356impl Sequence {
357    pub fn new(name: impl Into<String>, duration_secs: f64, fps: f64) -> Self {
358        Self {
359            id: 1, name: name.into(), duration: duration_secs, frame_rate: fps,
360            tracks: Vec::new(), camera_shots: Vec::new(), markers: Vec::new(),
361            loop_mode: LoopMode::Hold, current_time: 0.0, is_playing: false,
362            playback_speed: 1.0, gravity: false,
363            next_track_id: 1, next_clip_id: 1, next_shot_id: 1,
364        }
365    }
366
367    pub fn frame_count(&self) -> u64 { (self.duration * self.frame_rate) as u64 }
368    pub fn frame_to_time(&self, frame: u64) -> f64 { frame as f64 / self.frame_rate }
369    pub fn time_to_frame(&self, t: f64) -> u64 { (t * self.frame_rate) as u64 }
370
371    pub fn add_track(&mut self, mut track: SequenceTrack) -> u32 {
372        let id = self.next_track_id;
373        track.id = id;
374        self.next_track_id += 1;
375        self.tracks.push(track);
376        id
377    }
378
379    pub fn add_shot(&mut self, mut shot: CameraShot) -> u32 {
380        let id = self.next_shot_id;
381        shot.id = id;
382        self.next_shot_id += 1;
383        self.camera_shots.push(shot);
384        self.camera_shots.sort_by(|a, b| a.start.partial_cmp(&b.start).unwrap_or(std::cmp::Ordering::Equal));
385        id
386    }
387
388    pub fn add_marker(&mut self, time: f64, label: impl Into<String>) {
389        self.markers.push((time, label.into()));
390        self.markers.sort_by(|a, b| a.0.partial_cmp(&b.0).unwrap_or(std::cmp::Ordering::Equal));
391    }
392
393    pub fn update(&mut self, dt: f64) {
394        if !self.is_playing { return; }
395        self.current_time += dt * self.playback_speed;
396        match self.loop_mode {
397            LoopMode::Hold => self.current_time = self.current_time.min(self.duration),
398            LoopMode::Loop => {
399                if self.current_time >= self.duration { self.current_time = self.current_time % self.duration; }
400            }
401            LoopMode::PingPong => {
402                if self.current_time >= self.duration {
403                    self.playback_speed = -self.playback_speed.abs();
404                } else if self.current_time <= 0.0 {
405                    self.playback_speed = self.playback_speed.abs();
406                }
407            }
408        }
409    }
410
411    pub fn play(&mut self) { self.is_playing = true; }
412    pub fn pause(&mut self) { self.is_playing = false; }
413    pub fn stop(&mut self) { self.is_playing = false; self.current_time = 0.0; }
414
415    pub fn seek_to_frame(&mut self, frame: u64) {
416        self.current_time = self.frame_to_time(frame);
417    }
418
419    pub fn current_camera_shot(&self) -> Option<&CameraShot> {
420        self.camera_shots.iter().rev().find(|s| {
421            self.current_time >= s.start && self.current_time < s.start + s.duration
422        })
423    }
424
425    pub fn current_frame(&self) -> u64 { self.time_to_frame(self.current_time) }
426
427    pub fn active_markers_at(&self, t: f64, window: f64) -> Vec<&str> {
428        self.markers.iter()
429            .filter(|(mt, _)| (mt - t).abs() < window)
430            .map(|(_, label)| label.as_str())
431            .collect()
432    }
433
434    pub fn populate_demo(&mut self) {
435        // Transform track for an entity
436        let mut pos_track = SequenceTrack::new(0, "Camera Position", TrackKind::Transform);
437        pos_track.entity_id = Some(1);
438        pos_track.add_keyframe(Keyframe::new(0.0, KeyframeValue::Vec3(Vec3::new(0.0, 2.0, -10.0))));
439        pos_track.add_keyframe(Keyframe::new(3.0, KeyframeValue::Vec3(Vec3::new(5.0, 3.0, -5.0))));
440        pos_track.add_keyframe(Keyframe::new(6.0, KeyframeValue::Vec3(Vec3::new(0.0, 1.0, 0.0))));
441        self.add_track(pos_track);
442
443        // Float property track (e.g., FOV)
444        let mut fov_track = SequenceTrack::new(0, "Camera FOV", TrackKind::FloatProperty);
445        fov_track.entity_id = Some(1);
446        fov_track.property_path = "fov".into();
447        fov_track.add_keyframe(Keyframe::new(0.0, KeyframeValue::Float(60.0)));
448        fov_track.add_keyframe(Keyframe::new(3.0, KeyframeValue::Float(35.0)));
449        fov_track.add_keyframe(Keyframe::new(6.0, KeyframeValue::Float(60.0)));
450        self.add_track(fov_track);
451
452        // Audio clip track
453        let mut audio_track = SequenceTrack::new(0, "Music", TrackKind::AudioTrack);
454        let music_clip = TrackClip::new(0, 0.0, 10.0, "BackgroundMusic");
455        audio_track.add_clip(music_clip);
456        self.add_track(audio_track);
457
458        // Camera shots
459        self.add_shot(CameraShot::cut(0, 0.0, 3.0, 1));
460        self.add_shot(CameraShot::cut(0, 3.0, 3.0, 2));
461        self.add_shot(CameraShot::cut(0, 6.0, 4.0, 3));
462
463        // Markers
464        self.add_marker(0.0, "Scene Start");
465        self.add_marker(3.0, "Act 1");
466        self.add_marker(6.0, "Act 2");
467    }
468}
469
470// ---------------------------------------------------------------------------
471// Cinematic editor state
472// ---------------------------------------------------------------------------
473
474#[derive(Debug, Clone, Copy, PartialEq)]
475pub enum CinematicEditorTool { Select, Scrub, KeyframeEdit, CutEdit, BladeEdit, Pan }
476
477#[derive(Debug, Clone, Copy, PartialEq)]
478pub enum SnapMode { None, Frame, Bar, Custom }
479
480#[derive(Debug, Clone)]
481pub struct CinematicEditorState {
482    pub sequences: Vec<Sequence>,
483    pub active_sequence: usize,
484    pub selected_tracks: Vec<u32>,
485    pub selected_keyframes: Vec<(u32, usize)>,  // (track_id, kf_index)
486    pub selected_clips: Vec<(u32, u32)>,         // (track_id, clip_id)
487    pub tool: CinematicEditorTool,
488    pub zoom_h: f64,    // horizontal zoom (seconds per pixel)
489    pub zoom_v: f32,    // vertical zoom
490    pub scroll_h: f64,
491    pub scroll_v: f32,
492    pub canvas_width: f32,
493    pub canvas_height: f32,
494    pub header_height: f32,
495    pub track_header_width: f32,
496    pub snap_mode: SnapMode,
497    pub snap_custom_interval: f64,
498    pub show_waveforms: bool,
499    pub show_thumbnails: bool,
500    pub show_markers: bool,
501    pub show_track_groups: bool,
502    pub preview_live: bool,
503    pub record_mode: bool,
504    pub follow_playhead: bool,
505    pub search_query: String,
506    pub copy_buffer: Vec<SequenceTrack>,
507}
508
509impl CinematicEditorState {
510    pub fn new() -> Self {
511        let mut seq = Sequence::new("MainSequence", 10.0, 30.0);
512        seq.populate_demo();
513        Self {
514            sequences: vec![seq],
515            active_sequence: 0,
516            selected_tracks: Vec::new(),
517            selected_keyframes: Vec::new(),
518            selected_clips: Vec::new(),
519            tool: CinematicEditorTool::Select,
520            zoom_h: 0.01,       // 0.01 s/px = 100 px/s
521            zoom_v: 1.0,
522            scroll_h: 0.0,
523            scroll_v: 0.0,
524            canvas_width: 1200.0,
525            canvas_height: 600.0,
526            header_height: 40.0,
527            track_header_width: 200.0,
528            snap_mode: SnapMode::Frame,
529            snap_custom_interval: 0.1,
530            show_waveforms: true,
531            show_thumbnails: true,
532            show_markers: true,
533            show_track_groups: true,
534            preview_live: true,
535            record_mode: false,
536            follow_playhead: true,
537            search_query: String::new(),
538            copy_buffer: Vec::new(),
539        }
540    }
541
542    pub fn active_sequence(&self) -> &Sequence { &self.sequences[self.active_sequence] }
543    pub fn active_sequence_mut(&mut self) -> &mut Sequence { &mut self.sequences[self.active_sequence] }
544
545    pub fn time_to_x(&self, time: f64) -> f32 {
546        self.track_header_width + ((time - self.scroll_h) / self.zoom_h) as f32
547    }
548
549    pub fn x_to_time(&self, x: f32) -> f64 {
550        (x - self.track_header_width) as f64 * self.zoom_h + self.scroll_h
551    }
552
553    pub fn snap_time(&self, t: f64, fps: f64) -> f64 {
554        match self.snap_mode {
555            SnapMode::None => t,
556            SnapMode::Frame => (t * fps).round() / fps,
557            SnapMode::Bar => (t * fps / 4.0).round() * 4.0 / fps,
558            SnapMode::Custom => (t / self.snap_custom_interval).round() * self.snap_custom_interval,
559        }
560    }
561
562    pub fn update(&mut self, dt: f32) {
563        self.active_sequence_mut().update(dt as f64);
564        if self.follow_playhead {
565            let t = self.active_sequence().current_time;
566            let x = self.time_to_x(t);
567            let visible_start = self.track_header_width;
568            let visible_end = self.canvas_width - 20.0;
569            if x < visible_start + 50.0 || x > visible_end - 50.0 {
570                self.scroll_h = (t - (self.canvas_width - self.track_header_width) as f64 * self.zoom_h * 0.4).max(0.0);
571            }
572        }
573    }
574
575    pub fn zoom_to_fit(&mut self) {
576        let dur = self.active_sequence().duration;
577        let visible_w = (self.canvas_width - self.track_header_width) as f64;
578        self.zoom_h = dur / visible_w;
579        self.scroll_h = 0.0;
580    }
581
582    pub fn playhead_x(&self) -> f32 {
583        self.time_to_x(self.active_sequence().current_time)
584    }
585}
586
587// ---------------------------------------------------------------------------
588// Tests
589// ---------------------------------------------------------------------------
590#[cfg(test)]
591mod tests {
592    use super::*;
593
594    #[test]
595    fn test_easing() {
596        let e = EasingType::EaseInOut;
597        assert!((e.apply(0.0)).abs() < 1e-5);
598        assert!((e.apply(1.0) - 1.0).abs() < 1e-5);
599        assert!(e.apply(0.5) >= 0.0 && e.apply(0.5) <= 1.0);
600    }
601
602    #[test]
603    fn test_track_evaluate() {
604        let mut track = SequenceTrack::new(1, "test", TrackKind::FloatProperty);
605        track.add_keyframe(Keyframe::new(0.0, KeyframeValue::Float(0.0)));
606        track.add_keyframe(Keyframe::new(1.0, KeyframeValue::Float(10.0)));
607        let v = track.evaluate_float(0.5);
608        assert!(v > 0.0 && v < 10.0);
609    }
610
611    #[test]
612    fn test_sequence_playback() {
613        let mut seq = Sequence::new("test", 5.0, 30.0);
614        seq.play();
615        seq.update(2.5);
616        assert!((seq.current_time - 2.5).abs() < 0.01);
617        seq.update(5.0);
618        assert!(seq.current_time <= seq.duration);
619    }
620
621    #[test]
622    fn test_sequence_demo() {
623        let mut seq = Sequence::new("test", 10.0, 30.0);
624        seq.populate_demo();
625        assert!(!seq.tracks.is_empty());
626        assert!(!seq.camera_shots.is_empty());
627    }
628
629    #[test]
630    fn test_editor() {
631        let mut ed = CinematicEditorState::new();
632        ed.update(0.016);
633        assert!(ed.active_sequence().tracks.len() > 0);
634    }
635}