1
2use glam::{Vec2, Vec3, Vec4, Quat};
5use std::collections::HashMap;
6
7#[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#[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#[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>), }
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#[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, 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#[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#[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 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 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 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 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 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#[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)>, pub selected_clips: Vec<(u32, u32)>, pub tool: CinematicEditorTool,
488 pub zoom_h: f64, pub zoom_v: f32, 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, 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#[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}