1use serde::{Deserialize, Serialize};
6use std::hash::{Hash, Hasher};
7use std::path::Path;
8
9pub type Id = u64;
10pub const MIN_CLIP: f64 = 0.001;
12const EPS: f64 = 1e-6;
13const KEY_EPS: f64 = 1e-4;
14pub const ABUT_EPS: f64 = 1e-4;
16
17#[derive(Clone, Copy, PartialEq, Eq, Debug, Serialize, Deserialize, Hash)]
18pub enum TrackKind {
19 Video,
20 Audio,
21}
22
23#[derive(Clone, Copy, PartialEq, Eq, Debug, Serialize, Deserialize, Hash)]
24pub enum ClipKind {
25 Video,
26 Image,
27 Text,
28 Audio,
29 Sequence,
32 Shape,
34 Adjustment,
37}
38
39#[derive(Clone, Copy, PartialEq, Eq, Debug, Serialize, Deserialize, Default, Hash)]
41pub enum Scaler {
42 Nearest,
43 #[default]
44 Bilinear,
45 Bicubic,
46}
47
48impl Scaler {
49 pub const ALL: [Scaler; 3] = [Scaler::Nearest, Scaler::Bilinear, Scaler::Bicubic];
50 pub fn name(self) -> &'static str {
51 match self {
52 Scaler::Nearest => "Nearest neighbour",
53 Scaler::Bilinear => "Bilinear",
54 Scaler::Bicubic => "Bicubic",
55 }
56 }
57}
58
59#[derive(Clone, Copy, PartialEq, Eq, Debug, Serialize, Deserialize, Default, Hash)]
60pub enum BlendMode {
61 #[default]
62 Normal,
63 Multiply,
64 Screen,
65 Overlay,
66 Darken,
67 Lighten,
68 Add,
69 Subtract,
70 Difference,
71 SoftLight,
72 HardLight,
73 ColorDodge,
74 ColorBurn,
75}
76
77impl BlendMode {
78 pub const ALL: [BlendMode; 13] = [
79 BlendMode::Normal,
80 BlendMode::Multiply,
81 BlendMode::Screen,
82 BlendMode::Overlay,
83 BlendMode::Darken,
84 BlendMode::Lighten,
85 BlendMode::Add,
86 BlendMode::Subtract,
87 BlendMode::Difference,
88 BlendMode::SoftLight,
89 BlendMode::HardLight,
90 BlendMode::ColorDodge,
91 BlendMode::ColorBurn,
92 ];
93 pub fn name(self) -> &'static str {
94 match self {
95 BlendMode::Normal => "Normal",
96 BlendMode::Multiply => "Multiply",
97 BlendMode::Screen => "Screen",
98 BlendMode::Overlay => "Overlay",
99 BlendMode::Darken => "Darken",
100 BlendMode::Lighten => "Lighten",
101 BlendMode::Add => "Add",
102 BlendMode::Subtract => "Subtract",
103 BlendMode::Difference => "Difference",
104 BlendMode::SoftLight => "Soft Light",
105 BlendMode::HardLight => "Hard Light",
106 BlendMode::ColorDodge => "Color Dodge",
107 BlendMode::ColorBurn => "Color Burn",
108 }
109 }
110}
111
112#[derive(Clone, Copy, PartialEq, Debug, Serialize, Deserialize, Default)]
114pub enum Ease {
115 #[default]
116 Linear,
117 EaseIn,
118 EaseOut,
119 EaseInOut,
120 Hold,
122 Bezier {
125 x1: f32,
126 y1: f32,
127 x2: f32,
128 y2: f32,
129 },
130}
131
132impl Ease {
133 pub const ALL: [Ease; 5] = [Ease::Linear, Ease::EaseIn, Ease::EaseOut, Ease::EaseInOut, Ease::Hold];
135 pub const PRESETS: [(&'static str, Ease); 6] = [
137 ("Smooth", Ease::Bezier { x1: 0.42, y1: 0.0, x2: 0.58, y2: 1.0 }),
138 ("Snap", Ease::Bezier { x1: 0.9, y1: 0.0, x2: 0.1, y2: 1.0 }),
139 ("Slow Start", Ease::Bezier { x1: 0.55, y1: 0.0, x2: 1.0, y2: 0.45 }),
140 ("Slow End", Ease::Bezier { x1: 0.0, y1: 0.55, x2: 0.45, y2: 1.0 }),
141 ("Overshoot", Ease::Bezier { x1: 0.34, y1: 1.56, x2: 0.64, y2: 1.0 }),
142 ("Anticipate", Ease::Bezier { x1: 0.36, y1: 0.0, x2: 0.66, y2: -0.56 }),
143 ];
144 pub fn name(self) -> &'static str {
145 match self {
146 Ease::Linear => "Linear",
147 Ease::EaseIn => "Ease In",
148 Ease::EaseOut => "Ease Out",
149 Ease::EaseInOut => "Ease In/Out",
150 Ease::Hold => "Hold",
151 Ease::Bezier { .. } => "Bezier",
152 }
153 }
154 pub fn handles(self) -> (f32, f32, f32, f32) {
156 match self {
157 Ease::Linear | Ease::Hold => (0.33, 0.33, 0.67, 0.67),
158 Ease::EaseIn => (0.42, 0.0, 1.0, 1.0),
159 Ease::EaseOut => (0.0, 0.0, 0.58, 1.0),
160 Ease::EaseInOut => (0.42, 0.0, 0.58, 1.0),
161 Ease::Bezier { x1, y1, x2, y2 } => (x1, y1, x2, y2),
162 }
163 }
164 pub fn apply(self, f: f64) -> f64 {
166 let f = f.clamp(0.0, 1.0);
167 match self {
168 Ease::Linear => f,
169 Ease::EaseIn => f * f,
170 Ease::EaseOut => 1.0 - (1.0 - f) * (1.0 - f),
171 Ease::EaseInOut => f * f * (3.0 - 2.0 * f),
172 Ease::Hold => 0.0,
173 Ease::Bezier { x1, y1, x2, y2 } => cubic_bezier(f, x1 as f64, y1 as f64, x2 as f64, y2 as f64),
174 }
175 }
176}
177
178fn cubic_bezier(f: f64, x1: f64, y1: f64, x2: f64, y2: f64) -> f64 {
180 let (x1, x2) = (x1.clamp(0.0, 1.0), x2.clamp(0.0, 1.0));
181 let bx = |t: f64| 3.0 * (1.0 - t) * (1.0 - t) * t * x1 + 3.0 * (1.0 - t) * t * t * x2 + t * t * t;
182 let by = |t: f64| 3.0 * (1.0 - t) * (1.0 - t) * t * y1 + 3.0 * (1.0 - t) * t * t * y2 + t * t * t;
183 let dbx = |t: f64| 3.0 * (1.0 - t) * (1.0 - t) * x1 + 6.0 * (1.0 - t) * t * (x2 - x1) + 3.0 * t * t * (1.0 - x2);
184 let mut t = f;
185 for _ in 0..8 {
186 let d = dbx(t);
187 if d.abs() < 1e-6 {
188 break;
189 }
190 t -= (bx(t) - f) / d;
191 t = t.clamp(0.0, 1.0);
192 }
193 by(t)
194}
195
196fn is_linear(e: &Ease) -> bool {
197 *e == Ease::Linear
198}
199
200#[derive(Clone, Copy, Debug, Serialize, Deserialize, PartialEq)]
201pub struct Keyframe {
202 pub t: f64,
203 pub v: f64,
204 #[serde(default, skip_serializing_if = "is_linear")]
206 pub ease: Ease,
207}
208
209#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
211pub struct Animated {
212 pub value: f64,
213 #[serde(default, skip_serializing_if = "Vec::is_empty")]
214 pub keys: Vec<Keyframe>,
215}
216
217impl Animated {
218 pub fn new(value: f64) -> Self {
219 Self { value, keys: Vec::new() }
220 }
221 pub fn is_animated(&self) -> bool {
222 !self.keys.is_empty()
223 }
224 pub fn is_default(&self, def: f64) -> bool {
225 !self.is_animated() && (self.value - def).abs() < EPS
226 }
227 pub fn at(&self, t: f64) -> f64 {
229 let k = &self.keys;
230 if k.is_empty() {
231 return self.value;
232 }
233 if t <= k[0].t {
234 return k[0].v;
235 }
236 let last = k[k.len() - 1];
237 if t >= last.t {
238 return last.v;
239 }
240 for w in k.windows(2) {
241 if t < w[1].t {
242 let span = w[1].t - w[0].t;
243 let f = if span > 0.0 { (t - w[0].t) / span } else { 1.0 };
244 return w[0].v + (w[1].v - w[0].v) * w[0].ease.apply(f);
245 }
246 }
247 last.v
248 }
249 pub fn key_index_at(&self, t: f64) -> Option<usize> {
250 self.keys.iter().position(|k| (k.t - t).abs() < KEY_EPS)
251 }
252 pub fn has_key_at(&self, t: f64) -> bool {
253 self.key_index_at(t).is_some()
254 }
255 fn insert(&mut self, t: f64, v: f64) {
256 let i = self.keys.partition_point(|k| k.t < t);
257 self.keys.insert(i, Keyframe { t, v, ease: Ease::Linear });
258 }
259 pub fn set_at(&mut self, t: f64, v: f64) {
261 if !self.is_animated() {
262 self.value = v;
263 return;
264 }
265 match self.key_index_at(t) {
266 Some(i) => self.keys[i].v = v,
267 None => self.insert(t, v),
268 }
269 }
270 pub fn toggle_key(&mut self, t: f64) {
272 if let Some(i) = self.key_index_at(t) {
273 let v = self.keys.remove(i).v;
274 if self.keys.is_empty() {
275 self.value = v;
276 }
277 } else {
278 let v = self.at(t);
279 self.insert(t, v);
280 }
281 }
282 pub fn clear_keys(&mut self, t: f64) {
284 self.value = self.at(t);
285 self.keys.clear();
286 }
287 pub fn shift(&mut self, dt: f64) {
289 for k in &mut self.keys {
290 k.t += dt;
291 }
292 }
293 pub fn move_key(&mut self, i: usize, new_t: f64) -> usize {
297 if i >= self.keys.len() {
298 return i;
299 }
300 let mut k = self.keys.remove(i);
301 k.t = new_t;
302 if let Some(j) = self.key_index_at(new_t) {
303 self.keys[j] = k;
304 return j;
305 }
306 let j = self.keys.partition_point(|o| o.t < new_t);
307 self.keys.insert(j, k);
308 j
309 }
310 pub fn set_ease_at(&mut self, t: f64, ease: Ease) {
311 if let Some(i) = self.key_index_at(t) {
312 self.keys[i].ease = ease;
313 }
314 }
315}
316
317#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
319#[serde(default)]
320pub struct TextStyle {
321 pub text: String,
322 pub font: String,
323 pub size: f32,
324 pub bold: bool,
325 pub italic: bool,
326 pub color: [u8; 4],
327 pub outline_width: f32,
328 pub outline_color: [u8; 4],
329 pub shadow: bool,
330 pub shadow_color: [u8; 4],
331 pub shadow_x: f32,
332 pub shadow_y: f32,
333 pub shadow_blur: f32,
334 pub align: u8,
336 pub line_spacing: f32,
337 pub letter_spacing: f32,
338 pub box_color: [u8; 4],
340 pub box_padding: f32,
341}
342
343impl Default for TextStyle {
344 fn default() -> Self {
345 Self {
346 text: "Text".into(),
347 font: "Segoe UI".into(),
348 size: 72.0,
349 bold: false,
350 italic: false,
351 color: [255, 255, 255, 255],
352 outline_width: 0.0,
353 outline_color: [0, 0, 0, 255],
354 shadow: false,
355 shadow_color: [0, 0, 0, 160],
356 shadow_x: 4.0,
357 shadow_y: 4.0,
358 shadow_blur: 2.0,
359 align: 1,
360 line_spacing: 1.0,
361 letter_spacing: 0.0,
362 box_color: [0, 0, 0, 0],
363 box_padding: 8.0,
364 }
365 }
366}
367
368impl TextStyle {
369 pub fn subtitle_default() -> Self {
371 Self { text: String::new(), size: 48.0, outline_width: 3.0, ..Self::default() }
372 }
373 pub fn cache_key(&self) -> u64 {
375 let mut h = std::collections::hash_map::DefaultHasher::new();
376 self.text.hash(&mut h);
377 self.font.hash(&mut h);
378 for f in [
379 self.size,
380 self.outline_width,
381 self.shadow_x,
382 self.shadow_y,
383 self.shadow_blur,
384 self.line_spacing,
385 self.letter_spacing,
386 self.box_padding,
387 ] {
388 f.to_bits().hash(&mut h);
389 }
390 (self.bold, self.italic, self.shadow, self.align).hash(&mut h);
391 (self.color, self.outline_color, self.shadow_color, self.box_color).hash(&mut h);
392 h.finish()
393 }
394}
395
396#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Default)]
397#[serde(default)]
398pub struct AudioStreamInfo {
399 pub index: usize,
401 pub channels: u32,
402 pub sample_rate: u32,
403 pub language: String,
404 pub title: String,
405 pub codec: String,
406}
407
408impl AudioStreamInfo {
409 pub fn label(&self) -> String {
410 if !self.title.is_empty() {
411 self.title.clone()
412 } else if !self.language.is_empty() && self.language != "und" {
413 self.language.clone()
414 } else {
415 format!("Audio {}", self.index + 1)
416 }
417 }
418}
419
420pub const LABEL_COLORS: [(&str, [u8; 3]); 8] = [
422 ("Red", [220, 70, 70]),
423 ("Orange", [230, 140, 50]),
424 ("Yellow", [220, 200, 60]),
425 ("Green", [80, 180, 90]),
426 ("Teal", [60, 180, 180]),
427 ("Blue", [70, 120, 220]),
428 ("Purple", [150, 90, 200]),
429 ("Gray", [150, 150, 150]),
430];
431
432#[derive(Clone, Debug, Serialize, Deserialize)]
433pub struct Asset {
434 pub id: Id,
435 pub path: String,
436 pub kind: ClipKind,
438 pub duration: f64,
439 pub width: u32,
440 pub height: u32,
441 pub fps: f64,
442 #[serde(default)]
443 pub audio_streams: Vec<AudioStreamInfo>,
444 #[serde(default)]
445 pub codec: String,
446 #[serde(default)]
448 pub folder: String,
449 #[serde(default)]
450 pub tags: Vec<String>,
451 #[serde(default)]
453 pub label: u8,
454 #[serde(default)]
456 pub description: String,
457}
458
459impl Asset {
460 pub fn name(&self) -> String {
461 Path::new(&self.path).file_name().map(|s| s.to_string_lossy().into_owned()).unwrap_or_else(|| self.path.clone())
462 }
463 pub fn has_video(&self) -> bool {
464 matches!(self.kind, ClipKind::Video | ClipKind::Image)
465 }
466}
467
468#[derive(Clone, Copy, PartialEq, Eq, Debug, Serialize, Deserialize, Hash)]
471pub enum EffectKind {
472 Blur,
473 Pixelate,
474 Tint,
475 Color,
476 Vignette,
477 Sharpen,
478 Invert,
479 Grayscale,
480 Flip,
481 Crop,
482 Wobble,
483 ChromaKey,
485 Curves,
486 Levels,
487 HueShift,
488 JpegCompress,
489 MotionBlur,
490 Plane3d,
491 EdgeGlow,
492 Threshold,
493 BlobTrack,
494 Vhs,
495 RecDot,
496 ColorReplace,
498 Shader,
500}
501
502#[derive(Clone, Copy, Debug)]
504pub struct ParamSpec {
505 pub name: &'static str,
506 pub default: f64,
507 pub min: f64,
508 pub max: f64,
509}
510
511const fn ps(name: &'static str, default: f64, min: f64, max: f64) -> ParamSpec {
512 ParamSpec { name, default, min, max }
513}
514
515impl EffectKind {
516 pub const ALL: [EffectKind; 25] = [
517 EffectKind::Blur,
518 EffectKind::MotionBlur,
519 EffectKind::Pixelate,
520 EffectKind::JpegCompress,
521 EffectKind::Vhs,
522 EffectKind::ChromaKey,
523 EffectKind::ColorReplace,
524 EffectKind::Threshold,
525 EffectKind::EdgeGlow,
526 EffectKind::Tint,
527 EffectKind::Color,
528 EffectKind::Curves,
529 EffectKind::Levels,
530 EffectKind::HueShift,
531 EffectKind::Grayscale,
532 EffectKind::Invert,
533 EffectKind::Vignette,
534 EffectKind::Sharpen,
535 EffectKind::Flip,
536 EffectKind::Crop,
537 EffectKind::Plane3d,
538 EffectKind::Wobble,
539 EffectKind::BlobTrack,
540 EffectKind::RecDot,
541 EffectKind::Shader,
542 ];
543 pub fn category(self) -> &'static str {
545 use EffectKind::*;
546 match self {
547 Color | Curves | Levels | HueShift | Tint | Grayscale | Invert | Threshold => "Adjustments",
548 Blur | MotionBlur | Sharpen | Pixelate | JpegCompress | Vhs | EdgeGlow | Vignette | RecDot => "Stylize",
549 ChromaKey | ColorReplace | Crop | BlobTrack => "Keying & Matte",
550 Flip | Plane3d | Wobble => "Transform",
551 Shader => "Custom",
552 }
553 }
554 pub fn applies_to_audio(self) -> bool {
560 use EffectKind::*;
561 match self {
562 Blur | Pixelate | Tint | Color | Vignette | Sharpen | Invert | Grayscale | Flip | Crop | Wobble
563 | ChromaKey | Curves | Levels | HueShift | JpegCompress | MotionBlur | Plane3d | EdgeGlow | Threshold
564 | BlobTrack | Vhs | RecDot | ColorReplace | Shader => false,
565 }
566 }
567 pub fn name(self) -> &'static str {
568 match self {
569 EffectKind::Blur => "Blur",
570 EffectKind::Pixelate => "Pixelate",
571 EffectKind::Tint => "Color Tint",
572 EffectKind::Color => "Color Correction",
573 EffectKind::Vignette => "Vignette",
574 EffectKind::Sharpen => "Sharpen",
575 EffectKind::Invert => "Invert",
576 EffectKind::Grayscale => "Black & White",
577 EffectKind::Flip => "Flip",
578 EffectKind::Crop => "Crop",
579 EffectKind::Wobble => "Camera Shake",
580 EffectKind::ChromaKey => "Chroma Key",
581 EffectKind::ColorReplace => "Color Replace",
582 EffectKind::Curves => "Color Curves",
583 EffectKind::Levels => "Levels",
584 EffectKind::HueShift => "Hue / Saturation",
585 EffectKind::JpegCompress => "JPEG Compression",
586 EffectKind::MotionBlur => "Motion Blur",
587 EffectKind::Plane3d => "3D Plane",
588 EffectKind::EdgeGlow => "Edge Glow",
589 EffectKind::Threshold => "Threshold",
590 EffectKind::BlobTrack => "Blob Tracking",
591 EffectKind::Vhs => "VHS",
592 EffectKind::RecDot => "Security Camera REC",
593 EffectKind::Shader => "Custom Shader",
594 }
595 }
596 pub fn params(self) -> &'static [ParamSpec] {
598 match self {
599 EffectKind::Blur => P_BLUR,
600 EffectKind::Pixelate => P_PIXELATE,
601 EffectKind::Tint => P_TINT,
602 EffectKind::Color => P_COLOR,
603 EffectKind::Vignette => P_VIGNETTE,
604 EffectKind::Sharpen => P_SHARPEN,
605 EffectKind::Invert => P_INVERT,
606 EffectKind::Grayscale => P_GRAYSCALE,
607 EffectKind::Flip => P_FLIP,
608 EffectKind::Crop => P_CROP,
609 EffectKind::Wobble => P_WOBBLE,
610 EffectKind::ChromaKey => P_CHROMA,
611 EffectKind::ColorReplace => P_COLOR_REPLACE,
612 EffectKind::Curves => P_CURVES,
613 EffectKind::Levels => P_LEVELS,
614 EffectKind::HueShift => P_HUE,
615 EffectKind::JpegCompress => P_JPEG,
616 EffectKind::MotionBlur => P_MOTIONBLUR,
617 EffectKind::Plane3d => P_PLANE3D,
618 EffectKind::EdgeGlow => P_EDGEGLOW,
619 EffectKind::Threshold => P_THRESHOLD,
620 EffectKind::BlobTrack => P_BLOBTRACK,
621 EffectKind::Vhs => P_VHS,
622 EffectKind::RecDot => P_RECDOT,
623 EffectKind::Shader => P_SHADER,
624 }
625 }
626 pub fn is_bool_param(self, i: usize) -> bool {
628 matches!(
629 (self, i),
630 (EffectKind::Flip, 0)
631 | (EffectKind::Flip, 1)
632 | (EffectKind::ChromaKey, 5)
633 | (EffectKind::Vhs, 6)
634 | (EffectKind::RecDot, 3)
635 | (EffectKind::BlobTrack, 4)
636 | (EffectKind::Threshold, 2)
637 | (EffectKind::JpegCompress, 2)
638 | (EffectKind::MotionBlur, 2)
639 | (EffectKind::EdgeGlow, 6)
640 )
641 }
642 pub fn needs_motion(self) -> bool {
644 self == EffectKind::MotionBlur
645 }
646 pub fn is_geometric(self) -> bool {
648 matches!(self, EffectKind::Wobble | EffectKind::Plane3d)
649 }
650}
651
652const P_BLUR: &[ParamSpec] = &[ps("Radius", 8.0, 0.0, 100.0)];
653const P_PIXELATE: &[ParamSpec] = &[ps("Block size", 16.0, 1.0, 200.0)];
654const P_TINT: &[ParamSpec] = &[
655 ps("Red", 255.0, 0.0, 255.0),
656 ps("Green", 128.0, 0.0, 255.0),
657 ps("Blue", 0.0, 0.0, 255.0),
658 ps("Amount", 0.5, 0.0, 1.0),
659];
660const P_COLOR: &[ParamSpec] = &[
661 ps("Brightness", 0.0, -1.0, 1.0),
662 ps("Contrast", 1.0, 0.0, 3.0),
663 ps("Saturation", 1.0, 0.0, 3.0),
664 ps("Hue", 0.0, -180.0, 180.0),
665 ps("Gamma", 1.0, 0.1, 5.0),
666];
667const P_VIGNETTE: &[ParamSpec] =
668 &[ps("Radius", 0.8, 0.0, 1.5), ps("Softness", 0.5, 0.0, 1.0), ps("Strength", 0.6, 0.0, 1.0)];
669const P_SHARPEN: &[ParamSpec] = &[ps("Amount", 0.5, 0.0, 3.0), ps("Radius", 2.0, 0.5, 20.0)];
670const P_INVERT: &[ParamSpec] = &[ps("Amount", 1.0, 0.0, 1.0)];
671const P_GRAYSCALE: &[ParamSpec] = &[ps("Amount", 1.0, 0.0, 1.0)];
672const P_FLIP: &[ParamSpec] = &[ps("Horizontal", 1.0, 0.0, 1.0), ps("Vertical", 0.0, 0.0, 1.0)];
673const P_CROP: &[ParamSpec] = &[
674 ps("Left", 0.0, 0.0, 0.5),
675 ps("Right", 0.0, 0.0, 0.5),
676 ps("Top", 0.0, 0.0, 0.5),
677 ps("Bottom", 0.0, 0.0, 0.5),
678 ps("Feather", 0.0, 0.0, 0.5),
679];
680const P_COLOR_REPLACE: &[ParamSpec] = &[
683 ps("From R", 255.0, 0.0, 255.0),
684 ps("From G", 0.0, 0.0, 255.0),
685 ps("From B", 0.0, 0.0, 255.0),
686 ps("To R", 0.0, 0.0, 255.0),
687 ps("To G", 128.0, 0.0, 255.0),
688 ps("To B", 255.0, 0.0, 255.0),
689 ps("Tolerance", 0.2, 0.0, 1.0),
690 ps("Softness", 0.1, 0.0, 1.0),
691];
692const P_CHROMA: &[ParamSpec] = &[
693 ps("Key R", 0.0, 0.0, 255.0),
694 ps("Key G", 255.0, 0.0, 255.0),
695 ps("Key B", 0.0, 0.0, 255.0),
696 ps("Similarity", 0.4, 0.0, 1.0),
697 ps("Smoothness", 0.1, 0.0, 1.0),
698 ps("Show mask", 0.0, 0.0, 1.0),
699 ps("Spill removal", 0.5, 0.0, 1.0),
700 ps("Edge shrink", 0.0, -5.0, 5.0),
701];
702const P_CURVES: &[ParamSpec] = &[
705 ps("Master 1/4", 0.25, 0.0, 1.0),
706 ps("Master 2/4", 0.5, 0.0, 1.0),
707 ps("Master 3/4", 0.75, 0.0, 1.0),
708 ps("Red 1/4", 0.25, 0.0, 1.0),
709 ps("Red 2/4", 0.5, 0.0, 1.0),
710 ps("Red 3/4", 0.75, 0.0, 1.0),
711 ps("Green 1/4", 0.25, 0.0, 1.0),
712 ps("Green 2/4", 0.5, 0.0, 1.0),
713 ps("Green 3/4", 0.75, 0.0, 1.0),
714 ps("Blue 1/4", 0.25, 0.0, 1.0),
715 ps("Blue 2/4", 0.5, 0.0, 1.0),
716 ps("Blue 3/4", 0.75, 0.0, 1.0),
717];
718const P_LEVELS: &[ParamSpec] = &[
719 ps("In black", 0.0, 0.0, 1.0),
720 ps("In white", 1.0, 0.0, 1.0),
721 ps("Gamma", 1.0, 0.1, 5.0),
722 ps("Out black", 0.0, 0.0, 1.0),
723 ps("Out white", 1.0, 0.0, 1.0),
724];
725const P_HUE: &[ParamSpec] =
726 &[ps("Hue", 0.0, -180.0, 180.0), ps("Saturation", 1.0, 0.0, 3.0), ps("Lightness", 0.0, -1.0, 1.0)];
727const P_JPEG: &[ParamSpec] =
728 &[ps("Quality", 20.0, 1.0, 100.0), ps("Block size", 8.0, 2.0, 32.0), ps("Chroma subsample", 1.0, 0.0, 1.0)];
729const P_MOTIONBLUR: &[ParamSpec] =
730 &[ps("Shutter angle", 180.0, 0.0, 360.0), ps("Samples", 8.0, 2.0, 32.0), ps("Adaptive", 1.0, 0.0, 1.0)];
731const P_PLANE3D: &[ParamSpec] = &[
733 ps("Yaw", 0.0, -89.0, 89.0),
734 ps("Pitch", 0.0, -89.0, 89.0),
735 ps("Roll", 0.0, -180.0, 180.0),
736 ps("Distance", 2.0, 0.5, 20.0),
737 ps("Field of view", 45.0, 5.0, 120.0),
738 ps("Offset Z", 0.0, -5.0, 5.0),
739];
740const P_EDGEGLOW: &[ParamSpec] = &[
741 ps("Threshold", 0.2, 0.0, 1.0),
742 ps("Width", 2.0, 0.5, 20.0),
743 ps("Glow", 1.0, 0.0, 4.0),
744 ps("R", 120.0, 0.0, 255.0),
745 ps("G", 200.0, 0.0, 255.0),
746 ps("B", 255.0, 0.0, 255.0),
747 ps("Keep source", 1.0, 0.0, 1.0),
748];
749const P_THRESHOLD: &[ParamSpec] =
750 &[ps("Level", 0.5, 0.0, 1.0), ps("Softness", 0.05, 0.0, 0.5), ps("Per channel", 0.0, 0.0, 1.0)];
751const P_BLOBTRACK: &[ParamSpec] = &[
753 ps("Target R", 255.0, 0.0, 255.0),
754 ps("Target G", 0.0, 0.0, 255.0),
755 ps("Target B", 0.0, 0.0, 255.0),
756 ps("Tolerance", 0.25, 0.0, 1.0),
757 ps("Show overlay", 1.0, 0.0, 1.0),
758 ps("Smoothing", 0.5, 0.0, 1.0),
759];
760const P_VHS: &[ParamSpec] = &[
761 ps("Noise", 0.3, 0.0, 1.0),
762 ps("Chroma bleed", 0.5, 0.0, 1.0),
763 ps("Scanlines", 0.4, 0.0, 1.0),
764 ps("Tracking jitter", 0.2, 0.0, 1.0),
765 ps("Head switching", 0.3, 0.0, 1.0),
766 ps("Sharpen ringing", 0.4, 0.0, 1.0),
767 ps("Colour bleed only", 0.0, 0.0, 1.0),
768 ps("Tape wear", 0.2, 0.0, 1.0),
769];
770const P_RECDOT: &[ParamSpec] = &[
771 ps("Size", 18.0, 2.0, 200.0),
772 ps("Blink Hz", 0.5, 0.0, 5.0),
773 ps("Corner", 0.0, 0.0, 3.0),
774 ps("Timecode", 1.0, 0.0, 1.0),
775 ps("Margin", 40.0, 0.0, 500.0),
776];
777const P_SHADER: &[ParamSpec] = &[
778 ps("u1", 0.0, -10.0, 10.0),
779 ps("u2", 0.0, -10.0, 10.0),
780 ps("u3", 0.0, -10.0, 10.0),
781 ps("u4", 0.0, -10.0, 10.0),
782 ps("u5", 0.0, -10.0, 10.0),
783 ps("u6", 0.0, -10.0, 10.0),
784 ps("u7", 0.0, -10.0, 10.0),
785 ps("u8", 0.0, -10.0, 10.0),
786];
787const P_WOBBLE: &[ParamSpec] = &[
788 ps("Amplitude X", 20.0, 0.0, 500.0),
789 ps("Amplitude Y", 20.0, 0.0, 500.0),
790 ps("Roll", 2.0, 0.0, 45.0),
791 ps("Yaw", 3.0, 0.0, 45.0),
792 ps("Pitch", 3.0, 0.0, 45.0),
793 ps("Frequency", 2.0, 0.05, 30.0),
794 ps("Seed", 1.0, 0.0, 1000.0),
795 ps("Motion", 1.0, 0.0, 4.0),
798 ps("Smoothness", 0.0, 0.0, 1.0),
800];
801
802pub const WOBBLE_MOTIONS: [&str; 5] = ["Sine", "Layered", "Cubic", "Triangle", "Random"];
804
805pub const DEFAULT_SHADER: &str = r#"// custom effect — edit freely
809vec4 effect(vec4 src, vec2 uv) {
810 // u1 = amount, u2 = speed
811 float wave = sin(uv.y * 40.0 + u_time * max(u2, 0.0) * 6.28318) * u1 * 0.02;
812 return texture(tex, vec2(uv.x + wave, uv.y));
813}
814"#;
815
816#[derive(Clone, Copy, PartialEq, Eq, Debug, Serialize, Deserialize, Hash, Default)]
819pub enum MaskShape {
820 #[default]
821 Rect,
822 Ellipse,
823 Polygon,
825 Path,
827}
828
829impl MaskShape {
830 pub const ALL: [MaskShape; 4] = [MaskShape::Rect, MaskShape::Ellipse, MaskShape::Polygon, MaskShape::Path];
831 pub fn name(self) -> &'static str {
832 match self {
833 MaskShape::Rect => "Rectangle",
834 MaskShape::Ellipse => "Ellipse",
835 MaskShape::Polygon => "Polygon",
836 MaskShape::Path => "Path",
837 }
838 }
839}
840
841#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
842#[serde(default)]
843pub struct Mask {
844 pub shape: MaskShape,
845 pub cx: Animated,
847 pub cy: Animated,
848 pub rx: Animated,
849 pub ry: Animated,
850 pub points: Vec<(f32, f32)>,
852 pub rotation: Animated,
853 pub feather: Animated,
855 pub expand: Animated,
857 pub opacity: Animated,
859 pub invert: bool,
860 pub enabled: bool,
861}
862
863impl Default for Mask {
864 fn default() -> Self {
865 Self {
866 shape: MaskShape::Rect,
867 cx: Animated::new(0.0),
868 cy: Animated::new(0.0),
869 rx: Animated::new(200.0),
870 ry: Animated::new(200.0),
871 points: Vec::new(),
872 rotation: Animated::new(0.0),
873 feather: Animated::new(0.0),
874 expand: Animated::new(0.0),
875 opacity: Animated::new(1.0),
876 invert: false,
877 enabled: true,
878 }
879 }
880}
881
882impl Mask {
883 pub fn new(shape: MaskShape) -> Self {
884 Self { shape, ..Default::default() }
885 }
886 pub fn animated_mut(&mut self) -> Vec<&mut Animated> {
887 vec![
888 &mut self.cx,
889 &mut self.cy,
890 &mut self.rx,
891 &mut self.ry,
892 &mut self.rotation,
893 &mut self.feather,
894 &mut self.expand,
895 &mut self.opacity,
896 ]
897 }
898 pub fn animated(&self) -> Vec<&Animated> {
899 vec![&self.cx, &self.cy, &self.rx, &self.ry, &self.rotation, &self.feather, &self.expand, &self.opacity]
900 }
901}
902
903#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
905pub struct Effect {
906 pub kind: EffectKind,
907 #[serde(default = "tru")]
908 pub enabled: bool,
909 #[serde(default)]
910 pub params: Vec<Animated>,
911 #[serde(default)]
913 pub mask: Option<Mask>,
914 #[serde(default, skip_serializing_if = "String::is_empty")]
916 pub shader: String,
917 #[serde(default)]
919 pub start: f64,
920 #[serde(default)]
923 pub len: f64,
924}
925
926impl Effect {
927 pub fn new(kind: EffectKind) -> Self {
928 Self {
929 kind,
930 enabled: true,
931 params: kind.params().iter().map(|p| Animated::new(p.default)).collect(),
932 mask: None,
933 shader: if kind == EffectKind::Shader { DEFAULT_SHADER.to_string() } else { String::new() },
934 start: 0.0,
935 len: 0.0,
936 }
937 }
938 pub fn on_at(&self, t: f64) -> bool {
941 self.enabled && t >= self.start - EPS && (self.len <= 0.0 || t <= self.start + self.len + EPS)
942 }
943 pub fn at(&self, i: usize, t: f64) -> f64 {
945 self.params
946 .get(i)
947 .map(|a| a.at(t))
948 .unwrap_or_else(|| self.kind.params().get(i).map(|p| p.default).unwrap_or(0.0))
949 }
950 pub fn specs(&self) -> &'static [ParamSpec] {
951 self.kind.params()
952 }
953}
954
955#[derive(Clone, Copy, PartialEq, Eq, Debug, Serialize, Deserialize, Default, Hash)]
959pub enum MathOp {
960 #[default]
961 Add,
962 Sub,
963 Mul,
964 Div,
965 Min,
966 Max,
967 Pow,
968 Mod,
969}
970
971impl MathOp {
972 pub const ALL: [MathOp; 8] =
973 [MathOp::Add, MathOp::Sub, MathOp::Mul, MathOp::Div, MathOp::Min, MathOp::Max, MathOp::Pow, MathOp::Mod];
974 pub fn name(self) -> &'static str {
975 match self {
976 MathOp::Add => "Add",
977 MathOp::Sub => "Subtract",
978 MathOp::Mul => "Multiply",
979 MathOp::Div => "Divide",
980 MathOp::Min => "Min",
981 MathOp::Max => "Max",
982 MathOp::Pow => "Power",
983 MathOp::Mod => "Modulo",
984 }
985 }
986}
987
988#[derive(Clone, Copy, PartialEq, Eq, Debug, Serialize, Deserialize, Default, Hash)]
990pub enum CmpOp {
991 #[default]
992 Lt,
993 Le,
994 Gt,
995 Ge,
996 Eq,
997 Ne,
998}
999
1000impl CmpOp {
1001 pub const ALL: [CmpOp; 6] = [CmpOp::Lt, CmpOp::Le, CmpOp::Gt, CmpOp::Ge, CmpOp::Eq, CmpOp::Ne];
1002 pub fn name(self) -> &'static str {
1003 match self {
1004 CmpOp::Lt => "Less",
1005 CmpOp::Le => "Less or equal",
1006 CmpOp::Gt => "Greater",
1007 CmpOp::Ge => "Greater or equal",
1008 CmpOp::Eq => "Equal",
1009 CmpOp::Ne => "Not equal",
1010 }
1011 }
1012}
1013
1014#[derive(Clone, Copy, PartialEq, Eq, Debug, Serialize, Deserialize, Default, Hash)]
1016pub enum LogicOp {
1017 #[default]
1018 And,
1019 Or,
1020 Not,
1021 Xor,
1022}
1023
1024impl LogicOp {
1025 pub const ALL: [LogicOp; 4] = [LogicOp::And, LogicOp::Or, LogicOp::Not, LogicOp::Xor];
1026 pub fn name(self) -> &'static str {
1027 match self {
1028 LogicOp::And => "And",
1029 LogicOp::Or => "Or",
1030 LogicOp::Not => "Not",
1031 LogicOp::Xor => "Xor",
1032 }
1033 }
1034}
1035
1036#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
1042pub enum NodeKind {
1043 Input,
1045 Color([u8; 4]),
1047 Clip(Id),
1049 Asset(Id),
1051 Effect(Effect),
1052 Blend {
1054 mode: BlendMode,
1055 opacity: Animated,
1056 },
1057 Combine {
1059 mode: BlendMode,
1060 factor: Animated,
1061 },
1062 Merge,
1064 Matte {
1066 invert: bool,
1067 use_alpha: bool,
1068 },
1069 Mask(Mask),
1071 #[serde(alias = "Text")]
1075 String(TextStyle),
1076 Number(Animated),
1078 Bool(bool),
1080 Random {
1083 seed: u32,
1084 min: f64,
1085 max: f64,
1086 },
1087 Math(MathOp),
1089 Compare(CmpOp),
1091 Logic(LogicOp),
1093 Select,
1095 Output,
1096}
1097
1098impl NodeKind {
1099 pub fn inputs(&self) -> usize {
1102 match self {
1103 NodeKind::Input
1104 | NodeKind::Color(_)
1105 | NodeKind::Clip(_)
1106 | NodeKind::Asset(_)
1107 | NodeKind::Mask(_)
1108 | NodeKind::String(_)
1109 | NodeKind::Number(_)
1110 | NodeKind::Bool(_)
1111 | NodeKind::Random { .. } => 0,
1112 NodeKind::Output => 1,
1113 NodeKind::Effect(e) => 1 + e.specs().len(),
1114 NodeKind::Merge | NodeKind::Matte { .. } | NodeKind::Math(_) | NodeKind::Compare(_) => 2,
1115 NodeKind::Logic(op) => {
1116 if *op == LogicOp::Not {
1117 1
1118 } else {
1119 2
1120 }
1121 }
1122 NodeKind::Blend { .. } | NodeKind::Combine { .. } | NodeKind::Select => 3,
1123 }
1124 }
1125 pub fn port_label(&self, i: usize) -> &str {
1127 match self {
1128 NodeKind::Effect(e) => {
1129 if i == 0 {
1130 "in"
1131 } else {
1132 e.specs().get(i - 1).map(|s| s.name).unwrap_or("")
1133 }
1134 }
1135 NodeKind::Blend { .. } => ["a", "b", "opacity"][i.min(2)],
1136 NodeKind::Combine { .. } => ["a", "b", "factor"][i.min(2)],
1137 NodeKind::Merge => ["a", "b"][i.min(1)],
1138 NodeKind::Matte { .. } => ["a", "matte"][i.min(1)],
1139 NodeKind::Math(_) | NodeKind::Compare(_) | NodeKind::Logic(_) => ["a", "b"][i.min(1)],
1140 NodeKind::Select => ["cond", "a", "b"][i.min(2)],
1141 _ => "",
1142 }
1143 }
1144 pub fn is_value(&self) -> bool {
1147 matches!(
1148 self,
1149 NodeKind::Number(_)
1150 | NodeKind::Bool(_)
1151 | NodeKind::Random { .. }
1152 | NodeKind::Math(_)
1153 | NodeKind::Compare(_)
1154 | NodeKind::Logic(_)
1155 )
1156 }
1157 pub fn title(&self) -> String {
1158 match self {
1159 NodeKind::Input => "Input".into(),
1160 NodeKind::Color(_) => "Color".into(),
1161 NodeKind::Clip(_) => "Clip".into(),
1162 NodeKind::Asset(_) => "Asset".into(),
1163 NodeKind::Effect(e) => e.kind.name().into(),
1164 NodeKind::Blend { .. } => "Blend".into(),
1165 NodeKind::Combine { .. } => "Combine".into(),
1166 NodeKind::Merge => "Merge".into(),
1167 NodeKind::Matte { .. } => "Matte".into(),
1168 NodeKind::Mask(m) => format!("Mask ({})", m.shape.name()),
1169 NodeKind::String(_) => "String".into(),
1170 NodeKind::Number(_) => "Number".into(),
1171 NodeKind::Bool(_) => "Boolean".into(),
1172 NodeKind::Random { .. } => "Random".into(),
1173 NodeKind::Math(op) => op.name().into(),
1174 NodeKind::Compare(op) => op.name().into(),
1175 NodeKind::Logic(op) => op.name().into(),
1176 NodeKind::Select => "Select".into(),
1177 NodeKind::Output => "Output".into(),
1178 }
1179 }
1180}
1181
1182fn hash01(x: u64) -> f64 {
1184 let mut z = x.wrapping_add(0x9E37_79B9_7F4A_7C15);
1185 z = (z ^ (z >> 30)).wrapping_mul(0xBF58_476D_1CE4_E5B9);
1186 z = (z ^ (z >> 27)).wrapping_mul(0x94D0_49BB_1331_11EB);
1187 z ^= z >> 31;
1188 (z >> 11) as f64 / (1u64 << 53) as f64
1189}
1190
1191pub fn expand_text(fmt: &str, t: f64, lt: f64, fps: f64) -> String {
1195 let fps = if fps.is_finite() && fps > 0.0 { fps } else { 30.0 };
1196 let count = |s: f64| (s.max(0.0) * fps).floor() as i64;
1197 let mut out = fmt.to_string();
1198 if out.contains("{frame}") {
1199 out = out.replace("{frame}", &count(t).to_string());
1200 }
1201 if out.contains("{n}") {
1202 out = out.replace("{n}", &count(lt).to_string());
1203 }
1204 if out.contains("{time}") {
1205 let s = t.max(0.0);
1206 let (h, m, sec, cs) = (s as i64 / 3600, (s as i64 / 60) % 60, s as i64 % 60, (s.fract() * 100.0) as i64);
1207 let clock = if h > 0 { format!("{h}:{m:02}:{sec:02}.{cs:02}") } else { format!("{m:02}:{sec:02}.{cs:02}") };
1208 out = out.replace("{time}", &clock);
1209 }
1210 out
1211}
1212
1213#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
1214pub struct Node {
1215 pub id: Id,
1216 pub kind: NodeKind,
1217 pub x: f32,
1219 pub y: f32,
1220 #[serde(default = "tru")]
1221 pub enabled: bool,
1222}
1223
1224#[derive(Clone, Copy, Debug, Serialize, Deserialize, PartialEq)]
1226pub struct Edge {
1227 pub from: Id,
1228 pub to: Id,
1229 pub port: usize,
1230}
1231
1232#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Default)]
1235#[serde(default)]
1236pub struct NodeGraph {
1237 pub nodes: Vec<Node>,
1238 pub edges: Vec<Edge>,
1239}
1240
1241impl NodeGraph {
1242 pub fn new(next_id: &mut impl FnMut() -> Id) -> Self {
1244 let (i, o) = (next_id(), next_id());
1245 Self {
1246 nodes: vec![
1247 Node { id: i, kind: NodeKind::Input, x: 0.0, y: 0.0, enabled: true },
1248 Node { id: o, kind: NodeKind::Output, x: 320.0, y: 0.0, enabled: true },
1249 ],
1250 edges: vec![Edge { from: i, to: o, port: 0 }],
1251 }
1252 }
1253 pub fn from_effects(effects: &[Effect], next_id: &mut impl FnMut() -> Id) -> Self {
1255 let mut g = Self::new(next_id);
1256 let out = g.output().unwrap();
1257 let mut prev = g.nodes[0].id;
1258 g.edges.clear();
1259 for (i, e) in effects.iter().enumerate() {
1260 let id = next_id();
1261 g.nodes.push(Node {
1262 id,
1263 kind: NodeKind::Effect(e.clone()),
1264 x: 160.0 * (i + 1) as f32,
1265 y: 0.0,
1266 enabled: e.enabled,
1267 });
1268 g.edges.push(Edge { from: prev, to: id, port: 0 });
1269 prev = id;
1270 }
1271 if let Some(o) = g.nodes.iter_mut().find(|n| n.id == out) {
1272 o.x = 160.0 * (effects.len() + 1) as f32;
1273 }
1274 g.edges.push(Edge { from: prev, to: out, port: 0 });
1275 g
1276 }
1277 pub fn to_effects(&self) -> Result<Vec<Effect>, String> {
1281 let out = self.output().ok_or("it has no Output node")?;
1282 let mut chain = Vec::new();
1283 let mut spine = vec![out];
1284 let mut cur = self.input_of(out, 0);
1285 while let Some(id) = cur {
1286 let n = self.node(id).ok_or("a wire points at a missing node")?;
1287 spine.push(id);
1288 match &n.kind {
1289 NodeKind::Input => break,
1290 NodeKind::Effect(e) => {
1291 let mut e = e.clone();
1292 e.enabled &= n.enabled;
1293 chain.push(e);
1294 cur = self.input_of(id, 0);
1295 }
1296 k => return Err(format!("a {} node has no effect-stack equivalent", k.title())),
1297 }
1298 }
1299 if cur.is_none() {
1300 return Err("the chain does not start at the Input node".into());
1301 }
1302 if let Some(extra) = self.eval_order().into_iter().find(|id| !spine.contains(id)) {
1303 let name = self.node(extra).map(|n| n.kind.title()).unwrap_or_default();
1304 return Err(format!("the {name} node feeds a branch a flat effect list can't hold"));
1305 }
1306 chain.reverse();
1307 Ok(chain)
1308 }
1309 pub fn node(&self, id: Id) -> Option<&Node> {
1310 self.nodes.iter().find(|n| n.id == id)
1311 }
1312 pub fn node_mut(&mut self, id: Id) -> Option<&mut Node> {
1313 self.nodes.iter_mut().find(|n| n.id == id)
1314 }
1315 pub fn output(&self) -> Option<Id> {
1316 self.nodes.iter().find(|n| n.kind == NodeKind::Output).map(|n| n.id)
1317 }
1318 pub fn input_of(&self, to: Id, port: usize) -> Option<Id> {
1320 self.edges.iter().find(|e| e.to == to && e.port == port).map(|e| e.from)
1321 }
1322 pub fn connect(&mut self, from: Id, to: Id, port: usize) -> bool {
1324 if from == to || self.node(from).is_none() || self.node(to).is_none() {
1325 return false;
1326 }
1327 let prev: Vec<Edge> = self.edges.iter().filter(|e| e.to == to && e.port == port).copied().collect();
1328 self.edges.retain(|e| !(e.to == to && e.port == port));
1329 self.edges.push(Edge { from, to, port });
1330 if self.has_cycle() {
1331 self.edges.pop();
1332 self.edges.extend(prev);
1333 return false;
1334 }
1335 true
1336 }
1337 pub fn disconnect(&mut self, to: Id, port: usize) {
1338 self.edges.retain(|e| !(e.to == to && e.port == port));
1339 }
1340 pub fn remove_node(&mut self, id: Id) {
1342 if self.node(id).map(|n| n.kind == NodeKind::Output).unwrap_or(true) {
1343 return;
1344 }
1345 let up = self.input_of(id, 0);
1346 let down: Vec<(Id, usize)> = self.edges.iter().filter(|e| e.from == id).map(|e| (e.to, e.port)).collect();
1347 self.nodes.retain(|n| n.id != id);
1348 self.edges.retain(|e| e.from != id && e.to != id);
1349 if let Some(up) = up {
1350 for (to, port) in down {
1351 self.connect(up, to, port);
1352 }
1353 }
1354 }
1355 pub fn has_cycle(&self) -> bool {
1357 fn visit(g: &NodeGraph, id: Id, state: &mut std::collections::HashMap<Id, u8>) -> bool {
1358 match state.get(&id) {
1359 Some(1) => return true,
1360 Some(2) => return false,
1361 _ => {}
1362 }
1363 state.insert(id, 1);
1364 let ins: Vec<Id> = g.edges.iter().filter(|e| e.to == id).map(|e| e.from).collect();
1365 for i in ins {
1366 if visit(g, i, state) {
1367 return true;
1368 }
1369 }
1370 state.insert(id, 2);
1371 false
1372 }
1373 let mut state = std::collections::HashMap::new();
1374 self.nodes.iter().any(|n| visit(self, n.id, &mut state))
1375 }
1376 pub fn eval_order(&self) -> Vec<Id> {
1378 let Some(out) = self.output() else { return Vec::new() };
1379 let mut order = Vec::new();
1380 let mut seen = std::collections::HashSet::new();
1381 fn walk(g: &NodeGraph, id: Id, seen: &mut std::collections::HashSet<Id>, order: &mut Vec<Id>) {
1382 if !seen.insert(id) {
1383 return;
1384 }
1385 let mut ins: Vec<(usize, Id)> = g.edges.iter().filter(|e| e.to == id).map(|e| (e.port, e.from)).collect();
1386 ins.sort();
1387 for (_, from) in ins {
1388 walk(g, from, seen, order);
1389 }
1390 order.push(id);
1391 }
1392 walk(self, out, &mut seen, &mut order);
1393 order
1394 }
1395 pub fn eval_values(&self, lt: f64, fps: f64) -> std::collections::HashMap<Id, f64> {
1400 let fps = if fps.is_finite() && fps > 0.0 { fps } else { 30.0 };
1401 let frame = (lt.max(0.0) * fps).floor() as u64;
1402 let mut vals: std::collections::HashMap<Id, f64> = std::collections::HashMap::new();
1403 for id in self.eval_order() {
1404 let Some(n) = self.node(id) else { continue };
1405 let port = |p: usize| self.input_of(id, p).and_then(|f| vals.get(&f).copied()).unwrap_or(0.0);
1406 let (a, b) = (port(0), port(1));
1407 let v = if !n.enabled {
1409 a
1410 } else {
1411 match &n.kind {
1412 NodeKind::Number(x) => x.at(lt),
1413 NodeKind::Bool(x) => *x as u8 as f64,
1414 NodeKind::Random { seed, min, max } => min + (max - min) * hash01((*seed as u64) << 32 ^ frame),
1415 NodeKind::Math(op) => match op {
1416 MathOp::Add => a + b,
1417 MathOp::Sub => a - b,
1418 MathOp::Mul => a * b,
1419 MathOp::Div => {
1420 if b == 0.0 {
1421 0.0
1422 } else {
1423 a / b
1424 }
1425 }
1426 MathOp::Min => a.min(b),
1427 MathOp::Max => a.max(b),
1428 MathOp::Pow => a.powf(b),
1429 MathOp::Mod => {
1430 if b == 0.0 {
1431 0.0
1432 } else {
1433 a.rem_euclid(b)
1434 }
1435 }
1436 },
1437 NodeKind::Compare(op) => {
1438 let t = match op {
1439 CmpOp::Lt => a < b,
1440 CmpOp::Le => a <= b,
1441 CmpOp::Gt => a > b,
1442 CmpOp::Ge => a >= b,
1443 CmpOp::Eq => (a - b).abs() < 1e-9,
1444 CmpOp::Ne => (a - b).abs() >= 1e-9,
1445 };
1446 t as u8 as f64
1447 }
1448 NodeKind::Logic(op) => {
1449 let (x, y) = (a >= 0.5, b >= 0.5);
1450 let t = match op {
1451 LogicOp::And => x && y,
1452 LogicOp::Or => x || y,
1453 LogicOp::Not => !x,
1454 LogicOp::Xor => x != y,
1455 };
1456 t as u8 as f64
1457 }
1458 NodeKind::Select => {
1459 if a >= 0.5 {
1460 b
1461 } else {
1462 port(2)
1463 }
1464 }
1465 NodeKind::Blend { opacity, .. } => opacity.at(lt),
1466 NodeKind::Combine { factor, .. } => factor.at(lt),
1467 _ => 0.0,
1468 }
1469 };
1470 vals.insert(id, if v.is_finite() { v } else { 0.0 });
1471 }
1472 vals
1473 }
1474 pub fn animated_mut(&mut self) -> Vec<&mut Animated> {
1476 let mut v = Vec::new();
1477 for n in &mut self.nodes {
1478 match &mut n.kind {
1479 NodeKind::Effect(e) => {
1480 v.extend(e.params.iter_mut());
1481 if let Some(m) = &mut e.mask {
1482 v.extend(m.animated_mut());
1483 }
1484 }
1485 NodeKind::Blend { opacity, .. } => v.push(opacity),
1486 NodeKind::Combine { factor, .. } => v.push(factor),
1487 NodeKind::Number(a) => v.push(a),
1488 NodeKind::Mask(m) => v.extend(m.animated_mut()),
1489 _ => {}
1490 }
1491 }
1492 v
1493 }
1494 pub fn animated(&self) -> Vec<&Animated> {
1495 let mut v = Vec::new();
1496 for n in &self.nodes {
1497 match &n.kind {
1498 NodeKind::Effect(e) => {
1499 v.extend(e.params.iter());
1500 if let Some(m) = &e.mask {
1501 v.extend(m.animated());
1502 }
1503 }
1504 NodeKind::Blend { opacity, .. } => v.push(opacity),
1505 NodeKind::Combine { factor, .. } => v.push(factor),
1506 NodeKind::Number(a) => v.push(a),
1507 NodeKind::Mask(m) => v.extend(m.animated()),
1508 _ => {}
1509 }
1510 }
1511 v
1512 }
1513}
1514
1515#[derive(Clone, Copy, PartialEq, Eq, Debug, Serialize, Deserialize, Hash, Default)]
1518pub enum ShapeKind {
1519 #[default]
1520 Rect,
1521 Ellipse,
1522 Triangle,
1523 Polygon,
1524 Star,
1525 Line,
1526 Arrow,
1527 Draw,
1529}
1530
1531impl ShapeKind {
1532 pub const ALL: [ShapeKind; 8] = [
1533 ShapeKind::Rect,
1534 ShapeKind::Ellipse,
1535 ShapeKind::Triangle,
1536 ShapeKind::Polygon,
1537 ShapeKind::Star,
1538 ShapeKind::Line,
1539 ShapeKind::Arrow,
1540 ShapeKind::Draw,
1541 ];
1542 pub fn name(self) -> &'static str {
1543 match self {
1544 ShapeKind::Rect => "Rectangle",
1545 ShapeKind::Ellipse => "Ellipse",
1546 ShapeKind::Triangle => "Triangle",
1547 ShapeKind::Polygon => "Polygon",
1548 ShapeKind::Star => "Star",
1549 ShapeKind::Line => "Line",
1550 ShapeKind::Arrow => "Arrow",
1551 ShapeKind::Draw => "Drawing",
1552 }
1553 }
1554}
1555
1556#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Default)]
1559pub struct Stroke {
1560 pub color: [u8; 4],
1561 pub width: f32,
1562 pub points: Vec<(f32, f32, f32)>,
1564}
1565
1566#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
1567#[serde(default)]
1568pub struct ShapeStyle {
1569 pub kind: ShapeKind,
1570 pub fill: [u8; 4],
1571 pub stroke: [u8; 4],
1572 pub stroke_width: f32,
1573 pub corner: f32,
1575 pub sides: u32,
1577 pub points: Vec<(f32, f32)>,
1580 pub w: Animated,
1582 pub h: Animated,
1583 pub strokes: Vec<Stroke>,
1585 pub draw_rate: f32,
1587 pub page: [u8; 4],
1589}
1590
1591impl Default for ShapeStyle {
1592 fn default() -> Self {
1593 Self {
1594 kind: ShapeKind::Rect,
1595 fill: [255, 255, 255, 255],
1596 stroke: [0, 0, 0, 0],
1597 stroke_width: 4.0,
1598 corner: 0.0,
1599 sides: 5,
1600 points: Vec::new(),
1601 w: Animated::new(300.0),
1602 h: Animated::new(200.0),
1603 strokes: Vec::new(),
1604 draw_rate: 1.0,
1605 page: [0, 0, 0, 0],
1606 }
1607 }
1608}
1609
1610impl ShapeStyle {
1611 pub fn new(kind: ShapeKind) -> Self {
1612 let mut s = Self { kind, ..Default::default() };
1613 if matches!(kind, ShapeKind::Line | ShapeKind::Arrow | ShapeKind::Draw) {
1614 s.stroke = [255, 255, 255, 255];
1615 s.fill = [0, 0, 0, 0];
1616 }
1617 s
1618 }
1619 pub fn poly_points(&self) -> Option<&[(f32, f32)]> {
1622 (self.kind == ShapeKind::Polygon && self.points.len() >= 3).then_some(&self.points[..])
1623 }
1624 pub fn path_points(&self) -> Vec<(f32, f32, f32)> {
1627 if self.kind != ShapeKind::Draw {
1628 return self.points.iter().enumerate().map(|(i, &(x, y))| (x, y, i as f32)).collect();
1629 }
1630 let mut out: Vec<(f32, f32, f32)> = Vec::new();
1631 for st in &self.strokes {
1632 let off = (out.last().map_or(0.0, |&(_, _, t)| t) - st.points.first().map_or(0.0, |p| p.2)).max(0.0);
1635 out.extend(st.points.iter().map(|&(x, y, t)| (x, y, t + off)));
1636 }
1637 out
1638 }
1639 pub fn draw_duration(&self) -> f64 {
1641 self.strokes.iter().flat_map(|s| s.points.iter().map(|p| p.2 as f64)).fold(0.0, f64::max)
1642 }
1643 pub fn cache_key(&self) -> u64 {
1644 let mut h = std::collections::hash_map::DefaultHasher::new();
1645 self.kind.hash(&mut h);
1646 (self.fill, self.stroke, self.page).hash(&mut h);
1647 for f in [self.stroke_width, self.corner, self.draw_rate] {
1648 f.to_bits().hash(&mut h);
1649 }
1650 self.sides.hash(&mut h);
1651 for a in [&self.w, &self.h] {
1656 a.value.to_bits().hash(&mut h);
1657 a.keys.len().hash(&mut h);
1658 for k in &a.keys {
1659 (k.t.to_bits(), k.v.to_bits()).hash(&mut h);
1660 match k.ease {
1662 Ease::Linear => 0u8.hash(&mut h),
1663 Ease::EaseIn => 1u8.hash(&mut h),
1664 Ease::EaseOut => 2u8.hash(&mut h),
1665 Ease::EaseInOut => 3u8.hash(&mut h),
1666 Ease::Hold => 4u8.hash(&mut h),
1667 Ease::Bezier { x1, y1, x2, y2 } => {
1668 5u8.hash(&mut h);
1669 for f in [x1, y1, x2, y2] {
1670 f.to_bits().hash(&mut h);
1671 }
1672 }
1673 }
1674 }
1675 }
1676 for p in &self.points {
1678 (p.0.to_bits(), p.1.to_bits()).hash(&mut h);
1679 }
1680 self.strokes.len().hash(&mut h);
1681 for st in &self.strokes {
1682 st.points.len().hash(&mut h);
1683 st.color.hash(&mut h);
1684 st.width.to_bits().hash(&mut h);
1685 }
1686 h.finish()
1687 }
1688}
1689
1690#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Default)]
1693#[serde(default)]
1694pub struct PathAsset {
1695 pub id: Id,
1696 pub name: String,
1697 pub points: Vec<(f32, f32, f32)>,
1699}
1700
1701pub fn path_to_keys(points: &[(f32, f32, f32)], duration: f64) -> (Animated, Animated) {
1706 let (mut x, mut y) = (Animated::new(0.0), Animated::new(0.0));
1707 let (Some(first), Some(last)) = (points.first(), points.last()) else { return (x, y) };
1708 let span = (last.2 - first.2) as f64;
1709 let dur = duration.max(MIN_CLIP);
1710 for (i, p) in points.iter().enumerate() {
1711 let f = match (span > 0.0, points.len() > 1) {
1712 (true, _) => (p.2 - first.2) as f64 / span,
1713 (false, true) => i as f64 / (points.len() - 1) as f64,
1714 (false, false) => 0.0,
1715 };
1716 let t = (f * dur).clamp(0.0, dur);
1717 let t = match x.keys.last() {
1718 Some(k) if t <= k.t + KEY_EPS => k.t + KEY_EPS,
1719 _ => t,
1720 };
1721 x.keys.push(Keyframe { t, v: p.0 as f64, ease: Ease::Linear });
1722 y.keys.push(Keyframe { t, v: p.1 as f64, ease: Ease::Linear });
1723 }
1724 if let Some(k) = x.keys.last().filter(|k| k.t > dur) {
1727 let s = dur / k.t;
1728 for (a, b) in x.keys.iter_mut().zip(y.keys.iter_mut()) {
1729 a.t *= s;
1730 b.t *= s;
1731 }
1732 }
1733 (x, y)
1734}
1735
1736#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
1740#[serde(default)]
1741pub struct Marker {
1742 pub id: Id,
1743 pub t: f64,
1745 pub duration: f64,
1747 pub name: String,
1748 pub note: String,
1749 pub label: u8,
1751}
1752
1753impl Default for Marker {
1754 fn default() -> Self {
1755 Self { id: 0, t: 0.0, duration: 0.0, name: String::new(), note: String::new(), label: 0 }
1756 }
1757}
1758
1759#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
1763pub struct Label {
1764 pub name: String,
1765 pub color: [u8; 3],
1766}
1767
1768pub fn default_labels() -> Vec<Label> {
1769 LABEL_COLORS.iter().map(|(n, c)| Label { name: (*n).to_string(), color: *c }).collect()
1770}
1771
1772#[derive(Clone, Copy, PartialEq, Eq, Debug, Serialize, Deserialize, Hash)]
1775pub enum FilterKind {
1776 Eq,
1778 HighPass,
1779 LowPass,
1780 Reverb,
1781 Echo,
1782 Distortion,
1783 Compressor,
1784 NoiseGate,
1785 Noise,
1787 Gain,
1788}
1789
1790impl FilterKind {
1791 pub const ALL: [FilterKind; 10] = [
1792 FilterKind::Eq,
1793 FilterKind::HighPass,
1794 FilterKind::LowPass,
1795 FilterKind::Reverb,
1796 FilterKind::Echo,
1797 FilterKind::Distortion,
1798 FilterKind::Compressor,
1799 FilterKind::NoiseGate,
1800 FilterKind::Noise,
1801 FilterKind::Gain,
1802 ];
1803 pub fn name(self) -> &'static str {
1804 match self {
1805 FilterKind::Eq => "EQ (5-band)",
1806 FilterKind::HighPass => "High-pass",
1807 FilterKind::LowPass => "Low-pass",
1808 FilterKind::Reverb => "Reverb",
1809 FilterKind::Echo => "Echo / Delay",
1810 FilterKind::Distortion => "Distortion",
1811 FilterKind::Compressor => "Compressor",
1812 FilterKind::NoiseGate => "Noise gate",
1813 FilterKind::Noise => "Noise",
1814 FilterKind::Gain => "Gain",
1815 }
1816 }
1817 pub fn params(self) -> &'static [ParamSpec] {
1818 match self {
1819 FilterKind::Eq => F_EQ,
1820 FilterKind::HighPass | FilterKind::LowPass => F_PASS,
1821 FilterKind::Reverb => F_REVERB,
1822 FilterKind::Echo => F_ECHO,
1823 FilterKind::Distortion => F_DIST,
1824 FilterKind::Compressor => F_COMP,
1825 FilterKind::NoiseGate => F_GATE,
1826 FilterKind::Noise => F_NOISE,
1827 FilterKind::Gain => F_GAIN,
1828 }
1829 }
1830}
1831
1832const F_EQ: &[ParamSpec] = &[
1835 ps("Low gain dB", 0.0, -24.0, 24.0),
1836 ps("Low freq", 120.0, 20.0, 1000.0),
1837 ps("Mid gain dB", 0.0, -24.0, 24.0),
1838 ps("Mid freq", 1000.0, 100.0, 8000.0),
1839 ps("Mid Q", 1.0, 0.1, 10.0),
1840 ps("High gain dB", 0.0, -24.0, 24.0),
1841 ps("High freq", 6000.0, 1000.0, 20000.0),
1842 ps("Low Q", 0.707, 0.1, 10.0),
1843 ps("High Q", 0.707, 0.1, 10.0),
1844 ps("Low-mid gain dB", 0.0, -24.0, 24.0),
1845 ps("Low-mid freq", 400.0, 40.0, 4000.0),
1846 ps("Low-mid Q", 1.0, 0.1, 10.0),
1847 ps("High-mid gain dB", 0.0, -24.0, 24.0),
1848 ps("High-mid freq", 3000.0, 400.0, 16000.0),
1849 ps("High-mid Q", 1.0, 0.1, 10.0),
1850];
1851const F_PASS: &[ParamSpec] = &[ps("Frequency", 200.0, 20.0, 20000.0), ps("Resonance", 0.7, 0.1, 10.0)];
1852const F_REVERB: &[ParamSpec] = &[
1853 ps("Room size", 0.5, 0.0, 1.0),
1854 ps("Damping", 0.5, 0.0, 1.0),
1855 ps("Width", 1.0, 0.0, 1.0),
1856 ps("Mix", 0.25, 0.0, 1.0),
1857 ps("Pre-delay ms", 20.0, 0.0, 200.0),
1858];
1859const F_ECHO: &[ParamSpec] = &[
1860 ps("Delay ms", 350.0, 1.0, 2000.0),
1861 ps("Feedback", 0.35, 0.0, 0.95),
1862 ps("Mix", 0.3, 0.0, 1.0),
1863 ps("Ping-pong", 0.0, 0.0, 1.0),
1864];
1865const F_DIST: &[ParamSpec] = &[ps("Drive", 4.0, 1.0, 50.0), ps("Tone", 0.5, 0.0, 1.0), ps("Mix", 1.0, 0.0, 1.0)];
1866const F_COMP: &[ParamSpec] = &[
1867 ps("Threshold dB", -18.0, -60.0, 0.0),
1868 ps("Ratio", 4.0, 1.0, 20.0),
1869 ps("Attack ms", 10.0, 0.1, 200.0),
1870 ps("Release ms", 120.0, 5.0, 2000.0),
1871 ps("Makeup dB", 0.0, -12.0, 24.0),
1872];
1873const F_GATE: &[ParamSpec] =
1874 &[ps("Threshold dB", -45.0, -80.0, 0.0), ps("Attack ms", 2.0, 0.1, 100.0), ps("Release ms", 120.0, 5.0, 2000.0)];
1875const F_NOISE: &[ParamSpec] =
1876 &[ps("Level dB", -40.0, -80.0, 0.0), ps("Type", 0.0, 0.0, 2.0), ps("Tone Hz", 1000.0, 20.0, 18000.0)];
1877const F_GAIN: &[ParamSpec] = &[ps("Gain dB", 0.0, -60.0, 24.0)];
1878
1879#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
1880pub struct AudioFilter {
1881 pub kind: FilterKind,
1882 #[serde(default = "tru")]
1883 pub enabled: bool,
1884 #[serde(default, deserialize_with = "de_params")]
1885 pub params: Vec<Animated>,
1886}
1887
1888fn de_params<'de, D: serde::Deserializer<'de>>(d: D) -> Result<Vec<Animated>, D::Error> {
1890 #[derive(Deserialize)]
1891 #[serde(untagged)]
1892 enum NumOrAnim {
1893 Num(f64),
1894 Anim(Animated),
1895 }
1896 Ok(Vec::<NumOrAnim>::deserialize(d)?
1897 .into_iter()
1898 .map(|p| match p {
1899 NumOrAnim::Num(v) => Animated::new(v),
1900 NumOrAnim::Anim(a) => a,
1901 })
1902 .collect())
1903}
1904
1905impl AudioFilter {
1906 pub fn new(kind: FilterKind) -> Self {
1907 Self { kind, enabled: true, params: kind.params().iter().map(|p| Animated::new(p.default)).collect() }
1908 }
1909 pub fn fill_params(&mut self) {
1911 let specs = self.kind.params();
1912 for s in &specs[self.params.len().min(specs.len())..] {
1913 self.params.push(Animated::new(s.default));
1914 }
1915 }
1916 pub fn at(&self, i: usize, t: f64) -> f64 {
1918 self.params
1919 .get(i)
1920 .map(|a| a.at(t))
1921 .unwrap_or_else(|| self.kind.params().get(i).map(|p| p.default).unwrap_or(0.0))
1922 }
1923}
1924
1925#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
1927#[serde(default)]
1928pub struct Bus {
1929 pub id: Id,
1930 pub name: String,
1931 pub gain: Animated,
1932 pub pan: Animated,
1933 pub muted: bool,
1934 pub solo: bool,
1935 pub mono: bool,
1937 pub filters: Vec<AudioFilter>,
1938 pub output: Id,
1940}
1941
1942impl Default for Bus {
1943 fn default() -> Self {
1944 Self {
1945 id: 0,
1946 name: "Bus".into(),
1947 gain: Animated::new(1.0),
1948 pan: Animated::new(0.0),
1949 muted: false,
1950 solo: false,
1951 mono: false,
1952 filters: Vec::new(),
1953 output: 0,
1954 }
1955 }
1956}
1957
1958#[derive(Clone, Copy, PartialEq, Eq, Debug, Serialize, Deserialize, Hash)]
1961pub enum TransitionKind {
1962 CrossFade,
1964 FadeToColor,
1966 Push,
1968 Wipe,
1970}
1971
1972impl TransitionKind {
1973 pub const ALL: [TransitionKind; 4] =
1974 [TransitionKind::CrossFade, TransitionKind::FadeToColor, TransitionKind::Push, TransitionKind::Wipe];
1975 pub fn name(self) -> &'static str {
1976 match self {
1977 TransitionKind::CrossFade => "Cross Fade",
1978 TransitionKind::FadeToColor => "Fade to Color",
1979 TransitionKind::Push => "Push",
1980 TransitionKind::Wipe => "Wipe",
1981 }
1982 }
1983 pub fn has_direction(self) -> bool {
1984 matches!(self, TransitionKind::Push | TransitionKind::Wipe)
1985 }
1986}
1987
1988#[derive(Clone, Copy, PartialEq, Eq, Debug, Serialize, Deserialize, Default, Hash)]
1992pub enum TransitionEdge {
1993 #[default]
1994 Cut,
1995 In,
1996 Out,
1997}
1998
1999#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
2005pub struct Transition {
2006 pub id: Id,
2007 pub right: Id,
2009 pub kind: TransitionKind,
2010 pub duration: f64,
2011 #[serde(default = "black")]
2012 pub color: [u8; 4],
2013 #[serde(default)]
2015 pub direction: u8,
2016 #[serde(default)]
2017 pub ease: Ease,
2018 #[serde(default)]
2019 pub edge: TransitionEdge,
2020}
2021
2022fn black() -> [u8; 4] {
2023 [0, 0, 0, 255]
2024}
2025
2026impl Transition {
2027 pub fn half(&self, left: &Clip, right: &Clip) -> f64 {
2030 (self.duration / 2.0).min(left.duration).min(right.duration)
2031 }
2032 pub fn progress_at(&self, cut: f64, half: f64, t: f64) -> f64 {
2034 if half <= 0.0 {
2035 return 1.0;
2036 }
2037 self.ease.apply(((t - (cut - half)) / (2.0 * half)).clamp(0.0, 1.0))
2038 }
2039 pub fn cut_half(&self, left: Option<&Clip>, right: Option<&Clip>) -> Option<(f64, f64)> {
2042 match self.edge {
2043 TransitionEdge::Cut => {
2044 let (l, r) = (left?, right?);
2045 Some((r.start, self.half(l, r)))
2046 }
2047 TransitionEdge::In => {
2048 let c = right?;
2049 let h = (self.duration / 2.0).min(c.duration / 2.0);
2050 Some((c.start + h, h))
2051 }
2052 TransitionEdge::Out => {
2053 let c = left?;
2054 let h = (self.duration / 2.0).min(c.duration / 2.0);
2055 Some((c.end() - h, h))
2056 }
2057 }
2058 }
2059}
2060
2061#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
2064pub struct Cue {
2065 pub id: Id,
2066 pub start: f64,
2067 pub end: f64,
2068 pub text: String,
2069}
2070
2071fn tru() -> bool {
2072 true
2073}
2074fn one() -> f64 {
2075 1.0
2076}
2077fn a0() -> Animated {
2078 Animated::new(0.0)
2079}
2080fn a1() -> Animated {
2081 Animated::new(1.0)
2082}
2083
2084#[derive(Clone, Debug, Serialize, Deserialize)]
2085pub struct Clip {
2086 pub id: Id,
2087 #[serde(default)]
2089 pub link: Id,
2090 pub kind: ClipKind,
2091 #[serde(default)]
2093 pub container: bool,
2094 #[serde(default)]
2096 pub container_label: String,
2097 #[serde(default)]
2099 pub asset: Id,
2100 pub name: String,
2101 pub start: f64,
2103 pub duration: f64,
2104 #[serde(default)]
2106 pub src_in: f64,
2107 #[serde(default)]
2109 pub audio_stream: usize,
2110 #[serde(default)]
2112 pub sequence: Id,
2113 #[serde(default = "tru")]
2114 pub enabled: bool,
2115 #[serde(default)]
2117 pub label: u8,
2118 #[serde(default = "one")]
2121 pub speed: f64,
2122 #[serde(default = "a1")]
2125 pub speed_curve: Animated,
2126 #[serde(default)]
2128 pub reverse: bool,
2129 #[serde(default)]
2131 pub freeze: Option<f64>,
2132 #[serde(default = "a0")]
2134 pub x: Animated,
2135 #[serde(default = "a0")]
2136 pub y: Animated,
2137 #[serde(default = "a1")]
2138 pub scale: Animated,
2139 #[serde(default = "a0")]
2140 pub rotation: Animated,
2141 #[serde(default = "a1")]
2142 pub opacity: Animated,
2143 #[serde(default)]
2144 pub blend: BlendMode,
2145 #[serde(default)]
2147 pub effects: Vec<Effect>,
2148 #[serde(default = "a1")]
2151 pub volume: Animated,
2152 #[serde(default = "a0")]
2154 pub pan: Animated,
2155 #[serde(default)]
2157 pub fade_in: f64,
2158 #[serde(default)]
2159 pub fade_out: f64,
2160 #[serde(default)]
2162 pub bus: Id,
2163 #[serde(default)]
2165 pub text: Option<TextStyle>,
2166 #[serde(default)]
2168 pub shape: Option<ShapeStyle>,
2169 #[serde(default)]
2171 pub mask: Option<Mask>,
2172 #[serde(default)]
2174 pub graph: Option<NodeGraph>,
2175 #[serde(default)]
2177 pub markers: Vec<Marker>,
2178}
2179
2180impl Clip {
2181 pub fn new(id: Id, kind: ClipKind, name: impl Into<String>, start: f64, duration: f64) -> Self {
2182 Self {
2183 id,
2184 link: 0,
2185 kind,
2186 container: false,
2187 container_label: String::new(),
2188 asset: 0,
2189 name: name.into(),
2190 start,
2191 duration,
2192 src_in: 0.0,
2193 audio_stream: 0,
2194 sequence: 0,
2195 enabled: true,
2196 label: 0,
2197 speed: 1.0,
2198 speed_curve: a1(),
2199 reverse: false,
2200 freeze: None,
2201 x: a0(),
2202 y: a0(),
2203 scale: a1(),
2204 rotation: a0(),
2205 opacity: a1(),
2206 blend: BlendMode::Normal,
2207 effects: Vec::new(),
2208 volume: a1(),
2209 pan: a0(),
2210 fade_in: 0.0,
2211 fade_out: 0.0,
2212 bus: 0,
2213 text: if kind == ClipKind::Text { Some(TextStyle::default()) } else { None },
2214 shape: if kind == ClipKind::Shape { Some(ShapeStyle::default()) } else { None },
2215 mask: None,
2216 graph: None,
2217 markers: Vec::new(),
2218 }
2219 }
2220 pub fn is_empty_container(&self) -> bool {
2222 self.container && self.asset == 0
2223 }
2224 pub fn end(&self) -> f64 {
2225 self.start + self.duration
2226 }
2227 pub fn contains(&self, t: f64) -> bool {
2229 t >= self.start - EPS && t < self.end() - EPS
2230 }
2231 pub fn local(&self, t: f64) -> f64 {
2233 t - self.start
2234 }
2235 pub fn fade_mult(&self, lt: f64) -> f64 {
2238 let lt = lt.clamp(0.0, self.duration);
2239 let mut g = 1.0;
2240 if self.fade_in > 0.0 && lt < self.fade_in {
2241 g *= (lt / self.fade_in).clamp(0.0, 1.0);
2242 }
2243 if self.fade_out > 0.0 && lt > self.duration - self.fade_out {
2244 g *= ((self.duration - lt) / self.fade_out).clamp(0.0, 1.0);
2245 }
2246 g
2247 }
2248 pub fn src_len(&self) -> f64 {
2250 self.duration * self.speed
2251 }
2252 pub fn rate(&self, l: f64) -> f64 {
2254 if self.speed_curve.is_animated() {
2255 self.speed_curve.at(l).clamp(0.01, 100.0)
2256 } else {
2257 self.speed
2258 }
2259 }
2260 pub fn src_time(&self, t: f64) -> f64 {
2262 if let Some(f) = self.freeze {
2263 return f;
2264 }
2265 let l = self.local(t) * self.rate(self.local(t));
2269 if self.reverse {
2270 self.src_in + self.src_len() - l
2271 } else {
2272 self.src_in + l
2273 }
2274 }
2275 pub fn src_end(&self) -> f64 {
2277 self.src_in + self.src_len()
2278 }
2279 pub fn is_visual(&self) -> bool {
2280 self.kind != ClipKind::Audio
2281 }
2282 pub fn draws(&self) -> bool {
2284 self.is_visual() && self.kind != ClipKind::Adjustment
2285 }
2286 pub fn uses_asset(&self) -> bool {
2287 matches!(self.kind, ClipKind::Video | ClipKind::Image | ClipKind::Audio)
2288 }
2289 pub fn uses_graph(&self) -> bool {
2293 self.graph.as_ref().is_some_and(|g| g.nodes.len() > 2)
2294 }
2295 pub fn is_retimed(&self) -> bool {
2297 (self.speed - 1.0).abs() > EPS || self.speed_curve.is_animated() || self.reverse || self.freeze.is_some()
2298 }
2299 pub fn has_effects(&self) -> bool {
2301 !self.x.is_default(0.0)
2302 || !self.y.is_default(0.0)
2303 || !self.scale.is_default(1.0)
2304 || !self.rotation.is_default(0.0)
2305 || !self.opacity.is_default(1.0)
2306 || self.blend != BlendMode::Normal
2307 || self.is_retimed()
2308 || !self.effects.is_empty()
2309 || self.mask.is_some()
2310 || self.uses_graph()
2311 || !self.pan.is_default(0.0)
2312 || self.fade_in > 0.0
2313 || self.fade_out > 0.0
2314 }
2315 pub fn set_speed(&mut self, speed: f64) {
2317 let speed = if speed.is_finite() { speed.clamp(0.01, 100.0) } else { 1.0 };
2318 if self.freeze.is_none() {
2319 let len = self.src_len();
2320 self.duration = (len / speed).max(MIN_CLIP);
2321 }
2322 self.speed = speed;
2323 if !self.speed_curve.is_animated() {
2325 self.speed_curve.value = speed;
2326 }
2327 }
2328 pub fn animated(&self) -> [&Animated; 8] {
2329 [&self.x, &self.y, &self.scale, &self.rotation, &self.opacity, &self.volume, &self.pan, &self.speed_curve]
2330 }
2331 pub fn all_animated_mut(&mut self) -> Vec<&mut Animated> {
2333 let mut v: Vec<&mut Animated> = vec![
2334 &mut self.x,
2335 &mut self.y,
2336 &mut self.scale,
2337 &mut self.rotation,
2338 &mut self.opacity,
2339 &mut self.volume,
2340 &mut self.pan,
2341 &mut self.speed_curve,
2342 ];
2343 for e in &mut self.effects {
2344 v.extend(e.params.iter_mut());
2345 if let Some(m) = &mut e.mask {
2346 v.extend(m.animated_mut());
2347 }
2348 }
2349 if let Some(m) = &mut self.mask {
2350 v.extend(m.animated_mut());
2351 }
2352 if let Some(g) = &mut self.graph {
2353 v.extend(g.animated_mut());
2354 }
2355 if let Some(sh) = &mut self.shape {
2356 v.push(&mut sh.w);
2357 v.push(&mut sh.h);
2358 }
2359 v
2360 }
2361 pub fn all_animated(&self) -> Vec<&Animated> {
2362 let mut v: Vec<&Animated> = self.animated().to_vec();
2363 for e in &self.effects {
2364 v.extend(e.params.iter());
2365 if let Some(m) = &e.mask {
2366 v.extend(m.animated());
2367 }
2368 }
2369 if let Some(m) = &self.mask {
2370 v.extend(m.animated());
2371 }
2372 if let Some(g) = &self.graph {
2373 v.extend(g.animated());
2374 }
2375 if let Some(sh) = &self.shape {
2376 v.push(&sh.w);
2377 v.push(&sh.h);
2378 }
2379 v
2380 }
2381 pub fn props_mut(&mut self) -> Vec<(&'static str, &mut Animated)> {
2383 if self.is_visual() {
2384 vec![
2385 ("Position X", &mut self.x),
2386 ("Position Y", &mut self.y),
2387 ("Scale", &mut self.scale),
2388 ("Rotation", &mut self.rotation),
2389 ("Opacity", &mut self.opacity),
2390 ]
2391 } else {
2392 vec![("Volume", &mut self.volume), ("Pan", &mut self.pan)]
2393 }
2394 }
2395 pub fn key_times(&self) -> Vec<f64> {
2397 let mut v: Vec<f64> = self.all_animated().iter().flat_map(|a| a.keys.iter().map(|k| k.t)).collect();
2398 v.sort_by(f64::total_cmp);
2399 v.dedup_by(|a, b| (*a - *b).abs() < KEY_EPS);
2400 v
2401 }
2402 pub fn shift_keys(&mut self, dt: f64) {
2404 for a in self.all_animated_mut() {
2405 a.shift(dt);
2406 }
2407 }
2408 pub fn move_keys(&mut self, t_old: f64, t_new: f64) {
2410 let t_new = t_new.clamp(0.0, self.duration);
2411 for a in self.all_animated_mut() {
2412 if let Some(i) = a.key_index_at(t_old) {
2413 a.move_key(i, t_new);
2414 }
2415 }
2416 }
2417 pub fn split(&mut self, t: f64, new_id: Id) -> Option<Clip> {
2420 if t <= self.start + MIN_CLIP || t >= self.end() - MIN_CLIP {
2421 return None;
2422 }
2423 let off = t - self.start;
2424 let mut right = self.clone();
2425 right.id = new_id;
2426 right.start = t;
2427 right.duration = self.end() - t;
2428 if self.freeze.is_none() {
2429 if self.reverse {
2430 let src_in = self.src_in;
2432 self.src_in = src_in + (self.duration - off) * self.speed;
2433 right.src_in = src_in;
2434 } else {
2435 right.src_in = self.src_in + off * self.speed;
2436 }
2437 }
2438 right.shift_keys(-off);
2439 self.duration = off;
2440 Some(right)
2441 }
2442 pub fn trim_start(&mut self, new_start: f64, headroom: f64) {
2445 let min_start =
2446 if headroom.is_finite() { self.start - headroom.max(0.0) / self.speed } else { f64::NEG_INFINITY };
2447 let ns = new_start.max(min_start).max(0.0).min(self.end() - MIN_CLIP);
2448 let d = ns - self.start;
2449 self.start = ns;
2450 self.duration -= d;
2451 if self.freeze.is_none() && !self.reverse {
2452 self.src_in += d * self.speed;
2453 }
2454 self.shift_keys(-d);
2455 }
2456 pub fn trim_end(&mut self, new_end: f64, max_duration: f64) {
2458 let ne = new_end.min(self.start + max_duration).max(self.start + MIN_CLIP);
2459 let d = ne - self.end();
2460 self.duration = ne - self.start;
2461 if self.freeze.is_none() && self.reverse {
2462 self.src_in = (self.src_in - d * self.speed).max(0.0);
2464 }
2465 }
2466}
2467
2468fn dh() -> f32 {
2469 60.0
2470}
2471
2472#[derive(Clone, Debug, Serialize, Deserialize)]
2473pub struct Track {
2474 pub id: Id,
2475 pub name: String,
2476 pub kind: TrackKind,
2477 #[serde(default)]
2479 pub muted: bool,
2480 #[serde(default)]
2481 pub solo: bool,
2482 #[serde(default = "dh")]
2484 pub height: f32,
2485 #[serde(default)]
2486 pub clips: Vec<Clip>,
2487 #[serde(default)]
2488 pub transitions: Vec<Transition>,
2489 #[serde(default)]
2491 pub bus: Id,
2492}
2493
2494impl Track {
2495 pub fn new(id: Id, kind: TrackKind, name: impl Into<String>) -> Self {
2496 Self {
2497 id,
2498 name: name.into(),
2499 kind,
2500 muted: false,
2501 solo: false,
2502 height: if kind == TrackKind::Video { 64.0 } else { 56.0 },
2503 clips: Vec::new(),
2504 transitions: Vec::new(),
2505 bus: 0,
2506 }
2507 }
2508 pub fn sort(&mut self) {
2509 self.clips.sort_by(|a, b| a.start.total_cmp(&b.start));
2510 }
2511 pub fn end(&self) -> f64 {
2512 self.clips.iter().map(|c| c.end()).fold(0.0, f64::max)
2513 }
2514 pub fn fits(&self, start: f64, dur: f64, ignore: &[Id]) -> bool {
2516 start >= -EPS
2517 && !self
2518 .clips
2519 .iter()
2520 .any(|c| !ignore.contains(&c.id) && c.start < start + dur - EPS && start < c.end() - EPS)
2521 }
2522 pub fn left_of(&self, right: &Clip) -> Option<&Clip> {
2524 self.clips.iter().find(|c| c.id != right.id && (c.end() - right.start).abs() < ABUT_EPS)
2525 }
2526 pub fn transition_clips(&self, tr: &Transition) -> Option<(Option<&Clip>, Option<&Clip>)> {
2529 let c = self.clips.iter().find(|c| c.id == tr.right)?;
2530 match tr.edge {
2531 TransitionEdge::Cut => Some((Some(self.left_of(c)?), Some(c))),
2532 TransitionEdge::In => Some((None, Some(c))),
2533 TransitionEdge::Out => Some((Some(c), None)),
2534 }
2535 }
2536 pub fn transition_at(&self, t: f64) -> Option<(&Transition, Option<&Clip>, Option<&Clip>)> {
2538 self.transitions.iter().find_map(|tr| {
2539 let (l, r) = self.transition_clips(tr)?;
2540 let (cut, h) = tr.cut_half(l, r)?;
2541 (t >= cut - h && t < cut + h).then_some((tr, l, r))
2542 })
2543 }
2544 pub fn prune_transitions(&mut self) {
2546 let keep: Vec<Id> =
2547 self.transitions.iter().filter(|t| self.transition_clips(t).is_some()).map(|t| t.id).collect();
2548 self.transitions.retain(|t| keep.contains(&t.id));
2549 }
2550}
2551
2552#[derive(Clone, Debug, Serialize, Deserialize)]
2556pub struct Sequence {
2557 pub id: Id,
2558 pub name: String,
2559 pub width: u32,
2560 pub height: u32,
2561 pub fps: f64,
2562 pub tracks: Vec<Track>,
2563}
2564
2565impl Sequence {
2566 pub fn duration(&self) -> f64 {
2567 self.tracks.iter().map(|t| t.end()).fold(0.0, f64::max)
2568 }
2569}
2570
2571#[derive(Clone, Debug, Serialize, Deserialize)]
2573pub struct Stash {
2574 pub tracks: Vec<Track>,
2575 pub width: u32,
2576 pub height: u32,
2577 pub fps: f64,
2578 pub in_point: Option<f64>,
2579 pub out_point: Option<f64>,
2580}
2581
2582#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
2584#[serde(default)]
2585pub struct PlanItem {
2586 pub id: Id,
2587 pub title: String,
2588 pub done: bool,
2589 pub notes: String,
2590 pub color: u8,
2592 pub assets: Vec<Id>,
2594 pub asset_notes: Vec<String>,
2595 pub children: Vec<PlanItem>,
2596}
2597
2598impl Default for PlanItem {
2599 fn default() -> Self {
2600 Self {
2601 id: 0,
2602 title: String::new(),
2603 done: false,
2604 notes: String::new(),
2605 color: 0,
2606 assets: Vec::new(),
2607 asset_notes: Vec::new(),
2608 children: Vec::new(),
2609 }
2610 }
2611}
2612
2613#[derive(Clone, Debug, Serialize, Deserialize)]
2614#[serde(default)]
2615pub struct Project {
2616 pub version: u32,
2617 pub name: String,
2618 pub width: u32,
2619 pub height: u32,
2620 pub fps: f64,
2621 pub assets: Vec<Asset>,
2622 pub folders: Vec<String>,
2624 pub linked_folders: Vec<String>,
2626 pub tracks: Vec<Track>,
2629 pub in_point: Option<f64>,
2630 pub out_point: Option<f64>,
2631 pub source_video: Option<String>,
2633 pub subtitles: Vec<Cue>,
2635 pub subtitle_style: TextStyle,
2636 pub subtitle_margin: f32,
2638 pub show_subtitles: bool,
2639 pub subtitle_cont_prefix: String,
2642 pub subtitle_cont_suffix: String,
2643 pub scaler: Scaler,
2645 pub labels: Vec<Label>,
2647 pub markers: Vec<Marker>,
2649 pub buses: Vec<Bus>,
2651 pub sequences: Vec<Sequence>,
2653 pub editing: Option<Id>,
2655 pub main_stash: Option<Stash>,
2656 pub plan: Vec<PlanItem>,
2658 pub notes: String,
2659 pub paths: Vec<PathAsset>,
2661 next_id: Id,
2662}
2663
2664impl Default for Project {
2665 fn default() -> Self {
2666 Self::new()
2667 }
2668}
2669
2670impl Project {
2671 pub fn new() -> Self {
2672 let mut p = Self {
2673 version: 1,
2674 name: "Untitled".into(),
2675 width: 1920,
2676 height: 1080,
2677 fps: 30.0,
2678 assets: Vec::new(),
2679 folders: Vec::new(),
2680 linked_folders: Vec::new(),
2681 tracks: Vec::new(),
2682 in_point: None,
2683 out_point: None,
2684 source_video: None,
2685 subtitles: Vec::new(),
2686 subtitle_style: TextStyle::subtitle_default(),
2687 subtitle_margin: 60.0,
2688 show_subtitles: true,
2689 subtitle_cont_prefix: String::new(),
2690 subtitle_cont_suffix: String::new(),
2691 scaler: Scaler::Bilinear,
2692 labels: default_labels(),
2693 markers: Vec::new(),
2694 buses: Vec::new(),
2695 sequences: Vec::new(),
2696 editing: None,
2697 main_stash: None,
2698 plan: Vec::new(),
2699 notes: String::new(),
2700 paths: Vec::new(),
2701 next_id: 0,
2702 };
2703 p.add_track(TrackKind::Video);
2704 p.add_track(TrackKind::Audio);
2705 p
2706 }
2707
2708 pub fn from_media(asset: Asset) -> Self {
2710 let mut p = Self::new();
2711 if asset.has_video() && asset.width > 0 && asset.height > 0 {
2712 p.width = asset.width;
2713 p.height = asset.height;
2714 if asset.kind == ClipKind::Video && asset.fps > 1.0 {
2715 p.fps = asset.fps;
2716 }
2717 }
2718 p.name = Path::new(&asset.path)
2719 .file_stem()
2720 .map(|s| s.to_string_lossy().into_owned())
2721 .unwrap_or_else(|| "Untitled".into());
2722 if asset.kind == ClipKind::Video {
2723 p.source_video = Some(asset.path.clone());
2724 }
2725 let aid = p.add_asset(asset);
2726 p.insert_asset_clips(aid, 0.0, None);
2727 p
2728 }
2729
2730 pub fn new_id(&mut self) -> Id {
2731 self.next_id += 1;
2732 self.next_id
2733 }
2734
2735 pub fn asset(&self, id: Id) -> Option<&Asset> {
2737 self.assets.iter().find(|a| a.id == id)
2738 }
2739 pub fn asset_mut(&mut self, id: Id) -> Option<&mut Asset> {
2740 self.assets.iter_mut().find(|a| a.id == id)
2741 }
2742 pub fn asset_by_path(&self, path: &str) -> Option<&Asset> {
2743 self.assets.iter().find(|a| a.path.eq_ignore_ascii_case(path))
2744 }
2745 pub fn add_asset(&mut self, mut a: Asset) -> Id {
2747 if let Some(e) = self.asset_by_path(&a.path) {
2748 return e.id;
2749 }
2750 a.id = self.new_id();
2751 let id = a.id;
2752 self.assets.push(a);
2753 id
2754 }
2755 pub fn remove_asset(&mut self, id: Id) {
2759 self.assets.retain(|a| a.id != id);
2760 let drop_clips = |tracks: &mut Vec<Track>| {
2761 for t in tracks.iter_mut() {
2762 t.clips.retain(|c| !(c.uses_asset() && c.asset == id));
2763 t.prune_transitions();
2764 }
2765 };
2766 drop_clips(&mut self.tracks);
2767 if let Some(st) = &mut self.main_stash {
2768 drop_clips(&mut st.tracks);
2769 }
2770 for seq in &mut self.sequences {
2771 drop_clips(&mut seq.tracks);
2772 }
2773 self.tidy();
2774 }
2775 pub fn folder_names(&self) -> Vec<String> {
2777 let mut v: Vec<String> = self.folders.clone();
2778 v.extend(self.assets.iter().filter(|a| !a.folder.is_empty()).map(|a| a.folder.clone()));
2779 v.sort();
2780 v.dedup();
2781 v
2782 }
2783 pub fn add_folder(&mut self, name: &str) -> bool {
2785 let name = name.trim().trim_matches('/').to_string();
2786 if name.is_empty() {
2787 return false;
2788 }
2789 if !self.folders.contains(&name) {
2790 self.folders.push(name);
2791 self.folders.sort();
2792 }
2793 true
2794 }
2795 pub fn remove_folder(&mut self, name: &str) {
2797 let prefix = format!("{name}/");
2798 self.folders.retain(|f| f != name && !f.starts_with(&prefix));
2799 for a in &mut self.assets {
2800 if a.folder == name || a.folder.starts_with(&prefix) {
2801 a.folder.clear();
2802 }
2803 }
2804 }
2805
2806 pub fn duration(&self) -> f64 {
2808 self.tracks.iter().map(|t| t.end()).fold(0.0, f64::max)
2809 }
2810 pub fn is_empty(&self) -> bool {
2811 self.tracks.iter().all(|t| t.clips.is_empty())
2812 }
2813 pub fn frame_dur(&self) -> f64 {
2814 1.0 / self.fps.max(1.0)
2815 }
2816 pub fn snap_frame(&self, t: f64) -> f64 {
2817 (t * self.fps).round() / self.fps
2818 }
2819 pub fn find(&self, id: Id) -> Option<(usize, usize)> {
2821 for (ti, t) in self.tracks.iter().enumerate() {
2822 if let Some(ci) = t.clips.iter().position(|c| c.id == id) {
2823 return Some((ti, ci));
2824 }
2825 }
2826 None
2827 }
2828 pub fn clip(&self, id: Id) -> Option<&Clip> {
2829 self.find(id).map(|(t, c)| &self.tracks[t].clips[c])
2830 }
2831 pub fn clip_mut(&mut self, id: Id) -> Option<&mut Clip> {
2832 let (t, c) = self.find(id)?;
2833 Some(&mut self.tracks[t].clips[c])
2834 }
2835 pub fn track_of(&self, clip: Id) -> Option<usize> {
2836 self.find(clip).map(|(t, _)| t)
2837 }
2838 pub fn all_clips(&self) -> impl Iterator<Item = (usize, &Clip)> {
2839 self.tracks.iter().enumerate().flat_map(|(i, t)| t.clips.iter().map(move |c| (i, c)))
2840 }
2841 pub fn linked(&self, id: Id) -> Vec<Id> {
2843 match self.clip(id) {
2844 Some(c) if c.link != 0 => {
2845 let l = c.link;
2846 self.all_clips().filter(|(_, c)| c.link == l).map(|(_, c)| c.id).collect()
2847 }
2848 Some(_) => vec![id],
2849 None => Vec::new(),
2850 }
2851 }
2852 pub fn expand_links(&self, ids: &[Id]) -> Vec<Id> {
2854 let mut out: Vec<Id> = Vec::new();
2855 for &id in ids {
2856 for l in self.linked(id) {
2857 if !out.contains(&l) {
2858 out.push(l);
2859 }
2860 }
2861 }
2862 out
2863 }
2864 pub fn video_tracks(&self) -> Vec<usize> {
2865 (0..self.tracks.len()).filter(|&i| self.tracks[i].kind == TrackKind::Video).collect()
2866 }
2867 pub fn audio_tracks(&self) -> Vec<usize> {
2868 (0..self.tracks.len()).filter(|&i| self.tracks[i].kind == TrackKind::Audio).collect()
2869 }
2870 pub fn cut_points(&self) -> Vec<f64> {
2872 let mut v = vec![0.0];
2873 for (_, c) in self.all_clips() {
2874 v.push(c.start);
2875 v.push(c.end());
2876 }
2877 v.sort_by(f64::total_cmp);
2878 v.dedup_by(|a, b| (*a - *b).abs() < 1e-6);
2879 v
2880 }
2881 pub fn active(&self, tidx: usize) -> bool {
2883 let t = &self.tracks[tidx];
2884 let any_solo = self.tracks.iter().any(|o| o.kind == t.kind && o.solo);
2885 if any_solo {
2886 t.solo
2887 } else {
2888 !t.muted
2889 }
2890 }
2891 pub fn head_room(&self, clip: &Clip) -> f64 {
2893 if !matches!(clip.kind, ClipKind::Video | ClipKind::Audio) || clip.freeze.is_some() {
2894 return f64::INFINITY;
2895 }
2896 let Some(a) = self.asset(clip.asset) else { return f64::INFINITY };
2897 if clip.reverse {
2898 (a.duration - clip.src_end()).max(0.0)
2899 } else {
2900 clip.src_in.max(0.0)
2901 }
2902 }
2903 pub fn max_clip_duration(&self, clip: &Clip) -> f64 {
2905 if !matches!(clip.kind, ClipKind::Video | ClipKind::Audio) || clip.freeze.is_some() {
2906 return f64::INFINITY;
2907 }
2908 let Some(a) = self.asset(clip.asset) else { return f64::INFINITY };
2909 let extra = if clip.reverse { clip.src_in } else { a.duration - clip.src_end() };
2910 (clip.duration + extra.max(0.0) / clip.speed).max(MIN_CLIP)
2911 }
2912
2913 pub fn add_track(&mut self, kind: TrackKind) -> usize {
2916 let n = self.tracks.iter().filter(|t| t.kind == kind).count() + 1;
2917 let id = self.new_id();
2918 let name = format!("{}{}", if kind == TrackKind::Video { "V" } else { "A" }, n);
2919 let track = Track::new(id, kind, name);
2920 let idx = match kind {
2921 TrackKind::Video => self.video_tracks().last().map(|i| i + 1).unwrap_or(0),
2922 TrackKind::Audio => self.tracks.len(),
2923 };
2924 self.tracks.insert(idx, track);
2925 idx
2926 }
2927 pub fn remove_track(&mut self, idx: usize) {
2928 if idx < self.tracks.len() {
2929 self.tracks.remove(idx);
2930 self.rename_tracks();
2931 }
2932 }
2933 fn rename_tracks(&mut self) {
2934 let (mut v, mut a) = (0, 0);
2935 for t in &mut self.tracks {
2936 match t.kind {
2937 TrackKind::Video => {
2938 v += 1;
2939 t.name = format!("V{v}");
2940 }
2941 TrackKind::Audio => {
2942 a += 1;
2943 t.name = format!("A{a}");
2944 }
2945 }
2946 }
2947 }
2948 pub fn find_free_track(&mut self, kind: TrackKind, start: f64, dur: f64, prefer: Option<usize>) -> usize {
2950 if let Some(p) = prefer {
2951 if p < self.tracks.len() && self.tracks[p].kind == kind && self.tracks[p].fits(start, dur, &[]) {
2952 return p;
2953 }
2954 }
2955 let list = if kind == TrackKind::Video { self.video_tracks() } else { self.audio_tracks() };
2956 for i in list {
2957 if self.tracks[i].fits(start, dur, &[]) {
2958 return i;
2959 }
2960 }
2961 self.add_track(kind)
2962 }
2963
2964 pub fn tidy(&mut self) {
2967 for t in &mut self.tracks {
2968 t.sort();
2969 t.prune_transitions();
2970 }
2971 }
2972
2973 pub fn insert_asset_clips(&mut self, asset_id: Id, at: f64, video_track: Option<usize>) -> Vec<Id> {
2976 let Some(asset) = self.asset(asset_id).cloned() else { return Vec::new() };
2977 let dur = if asset.kind == ClipKind::Image { 5.0 } else { asset.duration.max(MIN_CLIP) };
2978 let n_parts = asset.has_video() as usize + asset.audio_streams.len();
2979 let link = if n_parts > 1 { self.new_id() } else { 0 };
2980 let mut ids = Vec::new();
2981 if asset.has_video() {
2982 let ti = self.find_free_track(TrackKind::Video, at, dur, video_track);
2983 let mut c = Clip::new(self.new_id(), asset.kind, asset.name(), at, dur);
2984 c.asset = asset.id;
2985 c.link = link;
2986 ids.push(c.id);
2987 self.tracks[ti].clips.push(c);
2988 self.tracks[ti].sort();
2989 }
2990 for (i, s) in asset.audio_streams.iter().enumerate() {
2991 let prefer = self.audio_tracks().get(i).copied();
2992 let ti = self.find_free_track(TrackKind::Audio, at, dur, prefer);
2993 let name =
2994 if asset.audio_streams.len() > 1 { format!("{} [{}]", asset.name(), s.label()) } else { asset.name() };
2995 let mut c = Clip::new(self.new_id(), ClipKind::Audio, name, at, dur);
2996 c.asset = asset.id;
2997 c.audio_stream = s.index;
2998 c.link = link;
2999 ids.push(c.id);
3000 self.tracks[ti].clips.push(c);
3001 self.tracks[ti].sort();
3002 }
3003 ids
3004 }
3005
3006 pub fn add_text_clip(&mut self, at: f64, dur: f64) -> Id {
3008 let prefer = self.video_tracks().last().copied();
3009 let ti = self.find_free_track(TrackKind::Video, at, dur, prefer);
3010 let c = Clip::new(self.new_id(), ClipKind::Text, "Text", at, dur);
3011 let id = c.id;
3012 self.tracks[ti].clips.push(c);
3013 self.tracks[ti].sort();
3014 id
3015 }
3016
3017 pub fn split_at(&mut self, t: f64, only: Option<&[Id]>) -> Vec<Id> {
3020 let mut new_ids = Vec::new();
3021 let mut link_map: std::collections::HashMap<Id, Id> = std::collections::HashMap::new();
3022 for ti in 0..self.tracks.len() {
3023 let mut added = Vec::new();
3024 for ci in 0..self.tracks[ti].clips.len() {
3025 let c = &self.tracks[ti].clips[ci];
3026 if !c.contains(t) || only.map(|o| !o.contains(&c.id)).unwrap_or(false) {
3027 continue;
3028 }
3029 let old_link = c.link;
3030 let nid = self.new_id();
3031 let new_link = if old_link != 0 {
3032 match link_map.get(&old_link) {
3033 Some(&l) => l,
3034 None => {
3035 let l = self.new_id();
3036 link_map.insert(old_link, l);
3037 l
3038 }
3039 }
3040 } else {
3041 0
3042 };
3043 if let Some(mut right) = self.tracks[ti].clips[ci].split(t, nid) {
3044 right.link = new_link;
3045 new_ids.push(right.id);
3046 added.push(right);
3047 }
3048 }
3049 if !added.is_empty() {
3050 self.tracks[ti].clips.extend(added);
3051 self.tracks[ti].sort();
3052 }
3053 }
3054 self.tidy();
3055 new_ids
3056 }
3057
3058 pub fn freeze_at(&mut self, t: f64, ids: &[Id]) -> Vec<Id> {
3061 let ids = self.expand_links(ids);
3062 let mut frozen = Vec::new();
3063 for id in ids {
3064 let Some(c) = self.clip(id) else { continue };
3065 if !c.contains(t) {
3066 continue; }
3068 let src = c.src_time(t);
3069 let target =
3070 if t > c.start + MIN_CLIP { self.split_at(t, Some(&[id])).first().copied().unwrap_or(id) } else { id };
3071 if let Some(c) = self.clip_mut(target) {
3072 c.freeze = Some(src);
3073 frozen.push(target);
3074 }
3075 }
3076 frozen
3077 }
3078
3079 pub fn delete_clips(&mut self, ids: &[Id], ripple: bool) {
3081 let mut ranges: Vec<(f64, f64)> = Vec::new();
3082 for &id in ids {
3083 if let Some(c) = self.clip(id) {
3084 ranges.push((c.start, c.end()));
3085 }
3086 }
3087 for t in &mut self.tracks {
3088 t.clips.retain(|c| !ids.contains(&c.id));
3089 }
3090 if ripple {
3091 ranges.sort_by(|a, b| b.0.total_cmp(&a.0)); ranges.dedup_by(|a, b| (a.0 - b.0).abs() < EPS && (a.1 - b.1).abs() < EPS);
3093 for (a, b) in ranges {
3094 self.close_gap(a, b);
3095 }
3096 }
3097 self.tidy();
3098 }
3099
3100 fn close_gap(&mut self, a: f64, b: f64) {
3102 let len = b - a;
3103 if len <= 0.0 {
3104 return;
3105 }
3106 for t in &mut self.tracks {
3107 if !t.fits(a, len, &[]) {
3108 continue;
3109 }
3110 for c in &mut t.clips {
3111 if c.start >= b - EPS {
3112 c.start -= len;
3113 }
3114 }
3115 }
3116 }
3117
3118 pub fn ripple_delete_range(&mut self, a: f64, b: f64) {
3120 if b <= a + EPS {
3121 return;
3122 }
3123 self.split_at(a, None);
3124 self.split_at(b, None);
3125 let ids: Vec<Id> =
3126 self.all_clips().filter(|(_, c)| c.start >= a - EPS && c.end() <= b + EPS).map(|(_, c)| c.id).collect();
3127 self.delete_clips(&ids, false);
3128 self.close_gap(a, b);
3129 self.tidy();
3130 }
3131
3132 pub fn ripple_open(&mut self, at: f64, span: f64) {
3135 if span <= EPS {
3136 return;
3137 }
3138 self.split_at(at, None);
3139 let mut ids: Vec<(Id, f64)> =
3141 self.all_clips().filter(|(_, c)| c.start >= at - EPS).map(|(_, c)| (c.id, c.start)).collect();
3142 ids.sort_by(|x, y| y.1.total_cmp(&x.1));
3143 for (id, _) in ids {
3144 self.move_clips(&[id], span, 0, None);
3145 }
3146 self.tidy();
3147 }
3148
3149 pub fn trim_to_range(&mut self, a: f64, b: f64) {
3151 let end = self.duration().max(b) + 1.0;
3152 self.ripple_delete_range(b, end);
3153 self.ripple_delete_range(0.0, a);
3154 self.in_point = None;
3155 self.out_point = None;
3156 }
3157
3158 pub fn move_clips(&mut self, ids: &[Id], dt: f64, dtrack: i32, track_kind: Option<TrackKind>) -> bool {
3161 let mut plan: Vec<(Id, usize, usize, f64, f64)> = Vec::new();
3163 for &id in ids {
3164 let Some((ti, ci)) = self.find(id) else { return false };
3165 let c = &self.tracks[ti].clips[ci];
3166 let kind = self.tracks[ti].kind;
3167 let mut to = ti;
3168 if dtrack != 0 && track_kind == Some(kind) {
3169 let list = if kind == TrackKind::Video { self.video_tracks() } else { self.audio_tracks() };
3170 let pos = list.iter().position(|&x| x == ti).unwrap() as i32 + dtrack;
3171 if pos < 0 || pos >= list.len() as i32 {
3172 return false;
3173 }
3174 to = list[pos as usize];
3175 }
3176 let ns = c.start + dt;
3177 if ns < -EPS {
3178 return false;
3179 }
3180 plan.push((id, ti, to, ns.max(0.0), c.duration));
3181 }
3182 plan.sort_by(|a, b| a.2.cmp(&b.2).then(a.3.total_cmp(&b.3)));
3185 for w in plan.windows(2) {
3186 if w[0].2 == w[1].2 && w[1].3 < w[0].3 + w[0].4 - EPS {
3187 return false;
3188 }
3189 }
3190 let moved: std::collections::HashSet<Id> = ids.iter().copied().collect();
3191 for (_, _, to, ns, dur) in &plan {
3192 let blocked = self.tracks[*to]
3193 .clips
3194 .iter()
3195 .any(|c| !moved.contains(&c.id) && c.start < ns + dur - EPS && *ns < c.end() - EPS);
3196 if blocked {
3197 return false;
3198 }
3199 }
3200 for (id, from, to, ns, _) in plan {
3201 let ci = self.tracks[from].clips.iter().position(|c| c.id == id).unwrap();
3202 let mut c = self.tracks[from].clips.remove(ci);
3203 c.start = ns;
3204 self.tracks[to].clips.push(c);
3206 }
3207 self.tidy();
3208 true
3209 }
3210
3211 pub fn set_enabled(&mut self, ids: &[Id], enabled: bool) {
3213 for id in self.expand_links(ids) {
3214 if let Some(c) = self.clip_mut(id) {
3215 c.enabled = enabled;
3216 }
3217 }
3218 }
3219 pub fn toggle_link(&mut self, ids: &[Id]) {
3221 let linked = ids.iter().any(|&id| self.clip(id).map(|c| c.link != 0).unwrap_or(false));
3222 let link = if linked { 0 } else { self.new_id() };
3223 for &id in ids {
3224 if let Some(c) = self.clip_mut(id) {
3225 c.link = link;
3226 }
3227 }
3228 }
3229 pub fn set_speed(&mut self, ids: &[Id], speed: f64, reverse: bool) -> bool {
3232 let ids = self.expand_links(ids);
3233 let mut tmp: Vec<(usize, usize, Clip)> = Vec::new();
3234 for &id in &ids {
3235 let Some((ti, ci)) = self.find(id) else { continue };
3236 let mut c = self.tracks[ti].clips[ci].clone();
3237 c.set_speed(speed);
3238 c.reverse = reverse;
3239 if !self.tracks[ti].fits(c.start, c.duration, &ids) {
3240 return false;
3241 }
3242 tmp.push((ti, ci, c));
3243 }
3244 for (ti, ci, c) in tmp {
3245 self.tracks[ti].clips[ci] = c;
3246 }
3247 self.tidy();
3248 true
3249 }
3250
3251 pub fn add_transition(&mut self, right: Id, kind: TransitionKind, duration: f64) -> Option<Id> {
3256 self.add_transition_at(right, kind, duration, TransitionEdge::Cut)
3257 }
3258 pub fn add_edge_transition(&mut self, clip: Id, kind: TransitionKind, duration: f64, out: bool) -> Option<Id> {
3261 self.add_transition_at(clip, kind, duration, if out { TransitionEdge::Out } else { TransitionEdge::In })
3262 }
3263 fn add_transition_at(
3264 &mut self,
3265 right: Id,
3266 kind: TransitionKind,
3267 duration: f64,
3268 edge: TransitionEdge,
3269 ) -> Option<Id> {
3270 let (ti, _) = self.find(right)?;
3271 let r = self.clip(right)?.clone();
3272 if edge == TransitionEdge::Cut {
3273 self.tracks[ti].left_of(&r)?;
3274 }
3275 let duration = duration.max(MIN_CLIP);
3276 let id = self.new_id();
3277 Self::clear_transition_slot(&mut self.tracks[ti], &r, edge);
3278 self.tracks[ti].transitions.push(Transition {
3279 id,
3280 right,
3281 kind,
3282 duration,
3283 color: [0, 0, 0, 255],
3284 direction: 0,
3285 ease: Ease::Linear,
3286 edge,
3287 });
3288 if r.link != 0 {
3290 let partners: Vec<Id> = self.linked(right).into_iter().filter(|&p| p != right).collect();
3291 for p in partners {
3292 let Some((pti, _)) = self.find(p) else { continue };
3293 if pti == ti {
3294 continue;
3295 }
3296 let pc = self.clip(p).unwrap().clone();
3297 if edge == TransitionEdge::Cut && self.tracks[pti].left_of(&pc).is_none() {
3298 continue;
3299 }
3300 let pid = self.new_id();
3301 let pkind = if self.tracks[pti].kind == TrackKind::Audio { TransitionKind::CrossFade } else { kind };
3302 Self::clear_transition_slot(&mut self.tracks[pti], &pc, edge);
3303 self.tracks[pti].transitions.push(Transition {
3304 id: pid,
3305 right: p,
3306 kind: pkind,
3307 duration,
3308 color: [0, 0, 0, 255],
3309 direction: 0,
3310 ease: Ease::Linear,
3311 edge,
3312 });
3313 }
3314 }
3315 Some(id)
3316 }
3317 fn clear_transition_slot(track: &mut Track, c: &Clip, edge: TransitionEdge) {
3321 let prev = track.left_of(c).map(|l| l.id);
3322 let next = track.clips.iter().find(|o| o.id != c.id && (o.start - c.end()).abs() < ABUT_EPS).map(|o| o.id);
3323 let (start_clip, end_clip) = match edge {
3324 TransitionEdge::Cut | TransitionEdge::In => (Some(c.id), prev),
3325 TransitionEdge::Out => (next, Some(c.id)),
3326 };
3327 track.transitions.retain(|t| {
3328 !(start_clip.is_some_and(|s| t.right == s && t.edge != TransitionEdge::Out)
3329 || end_clip.is_some_and(|e| t.right == e && t.edge == TransitionEdge::Out))
3330 });
3331 }
3332 pub fn remove_transition(&mut self, id: Id) {
3333 for t in &mut self.tracks {
3334 t.transitions.retain(|x| x.id != id);
3335 }
3336 }
3337 pub fn transition_mut(&mut self, id: Id) -> Option<&mut Transition> {
3338 self.tracks.iter_mut().flat_map(|t| t.transitions.iter_mut()).find(|x| x.id == id)
3339 }
3340 pub fn transitions_of(&self, clip: Id) -> Vec<(usize, &Transition)> {
3342 let mut out = Vec::new();
3343 for (ti, t) in self.tracks.iter().enumerate() {
3344 for tr in &t.transitions {
3345 if let Some((l, r)) = t.transition_clips(tr) {
3346 if l.is_some_and(|c| c.id == clip) || r.is_some_and(|c| c.id == clip) {
3347 out.push((ti, tr));
3348 }
3349 }
3350 }
3351 }
3352 out
3353 }
3354
3355 pub fn cue_at(&self, t: f64) -> Option<&Cue> {
3357 self.subtitles.iter().find(|c| t >= c.start && t < c.end)
3358 }
3359 pub fn add_cue(&mut self, start: f64, end: f64, text: impl Into<String>) -> Id {
3360 let id = self.new_id();
3361 self.subtitles.push(Cue { id, start, end: end.max(start + MIN_CLIP), text: text.into() });
3362 self.sort_cues();
3363 id
3364 }
3365 pub fn remove_cue(&mut self, id: Id) {
3366 self.subtitles.retain(|c| c.id != id);
3367 }
3368 pub fn split_cue(&mut self, id: Id, t: f64) -> Option<Id> {
3371 let c = self.subtitles.iter_mut().find(|c| c.id == id)?;
3372 if t < c.start + 0.05 || t > c.end - 0.05 {
3373 return None;
3374 }
3375 let (end, text) = (c.end, c.text.clone());
3376 c.end = t;
3377 Some(self.add_cue(t, end, text))
3378 }
3379 pub fn cues_to_text_clips(&mut self, only: Option<&[Id]>) -> usize {
3382 let take: Vec<Cue> =
3383 self.subtitles.iter().filter(|c| only.is_none_or(|o| o.contains(&c.id))).cloned().collect();
3384 if take.is_empty() {
3385 return 0;
3386 }
3387 let ti = match self.tracks.iter().position(|t| t.kind == TrackKind::Video && t.name == "Subtitles") {
3388 Some(i) => i,
3389 None => {
3390 let id = self.new_id();
3391 self.tracks.push(Track::new(id, TrackKind::Video, "Subtitles"));
3392 self.tracks.len() - 1
3393 }
3394 };
3395 let y = self.height as f64 / 2.0 - self.subtitle_margin as f64 - self.subtitle_style.size as f64 * 0.75;
3397 for cue in &take {
3398 let mut c =
3399 Clip::new(self.new_id(), ClipKind::Text, "Subtitle", cue.start, (cue.end - cue.start).max(MIN_CLIP));
3400 let mut style = self.subtitle_style.clone();
3401 style.text.clone_from(&cue.text);
3402 c.text = Some(style);
3403 c.y = Animated::new(y);
3404 self.tracks[ti].clips.push(c);
3405 }
3406 self.tracks[ti].sort();
3407 let ids: Vec<Id> = take.iter().map(|c| c.id).collect();
3408 self.subtitles.retain(|c| !ids.contains(&c.id));
3409 take.len()
3410 }
3411 pub fn sort_cues(&mut self) {
3412 self.subtitles.sort_by(|a, b| a.start.total_cmp(&b.start));
3413 }
3414
3415 pub fn sequence(&self, id: Id) -> Option<&Sequence> {
3417 self.sequences.iter().find(|s| s.id == id)
3418 }
3419 pub fn sequence_mut(&mut self, id: Id) -> Option<&mut Sequence> {
3420 self.sequences.iter_mut().find(|s| s.id == id)
3421 }
3422 pub fn new_sequence(&mut self, name: impl Into<String>, width: u32, height: u32, fps: f64) -> Id {
3424 let id = self.new_id();
3425 let mut seq = Sequence { id, name: name.into(), width, height, fps, tracks: Vec::new() };
3426 let v = Track::new(self.new_id(), TrackKind::Video, "V1");
3427 let a = Track::new(self.new_id(), TrackKind::Audio, "A1");
3428 seq.tracks.push(v);
3429 seq.tracks.push(a);
3430 self.sequences.push(seq);
3431 id
3432 }
3433 pub fn sequence_duration(&self, id: Id) -> f64 {
3435 if self.editing == Some(id) {
3436 return self.duration();
3437 }
3438 self.sequence(id).map(|s| s.duration()).unwrap_or(0.0)
3439 }
3440 pub fn sequence_tracks(&self, id: Id) -> Option<&[Track]> {
3442 if self.editing == Some(id) {
3443 return Some(&self.tracks);
3444 }
3445 self.sequence(id).map(|s| s.tracks.as_slice())
3446 }
3447 pub fn sequence_contains(&self, outer: Id, inner: Id) -> bool {
3449 fn walk(p: &Project, tracks: &[Track], inner: Id, depth: u32) -> bool {
3450 if depth > 32 {
3451 return true; }
3453 tracks.iter().flat_map(|t| t.clips.iter()).any(|c| {
3454 c.kind == ClipKind::Sequence
3455 && (c.sequence == inner
3456 || p.sequence_tracks(c.sequence).map(|tr| walk(p, tr, inner, depth + 1)).unwrap_or(false))
3457 })
3458 }
3459 outer == inner || self.sequence_tracks(outer).map(|t| walk(self, t, inner, 0)).unwrap_or(false)
3460 }
3461 pub fn open_sequence(&mut self, id: Id) -> bool {
3463 if self.editing == Some(id) {
3464 return true;
3465 }
3466 if self.sequence(id).is_none() {
3467 return false;
3468 }
3469 self.close_sequence();
3470 let stash = Stash {
3471 tracks: std::mem::take(&mut self.tracks),
3472 width: self.width,
3473 height: self.height,
3474 fps: self.fps,
3475 in_point: self.in_point.take(),
3476 out_point: self.out_point.take(),
3477 };
3478 let seq = self.sequence_mut(id).unwrap();
3479 let tracks = std::mem::take(&mut seq.tracks);
3480 let (w, h, fps) = (seq.width, seq.height, seq.fps);
3481 self.main_stash = Some(stash);
3482 self.tracks = tracks;
3483 self.width = w;
3484 self.height = h;
3485 self.fps = fps;
3486 self.editing = Some(id);
3487 true
3488 }
3489 pub fn close_sequence(&mut self) {
3491 let Some(id) = self.editing.take() else { return };
3492 let tracks = std::mem::take(&mut self.tracks);
3493 let (w, h, fps) = (self.width, self.height, self.fps);
3494 if let Some(seq) = self.sequence_mut(id) {
3495 seq.tracks = tracks;
3496 seq.width = w;
3497 seq.height = h;
3498 seq.fps = fps;
3499 }
3500 if let Some(st) = self.main_stash.take() {
3501 self.tracks = st.tracks;
3502 self.width = st.width;
3503 self.height = st.height;
3504 self.fps = st.fps;
3505 self.in_point = st.in_point;
3506 self.out_point = st.out_point;
3507 }
3508 }
3509 pub fn insert_sequence_clip(&mut self, seq: Id, at: f64, video_track: Option<usize>) -> Option<Id> {
3511 let name = self.sequence(seq)?.name.clone();
3512 if let Some(cur) = self.editing {
3514 if self.sequence_contains(seq, cur) {
3515 return None;
3516 }
3517 }
3518 let dur = self.sequence_duration(seq).max(MIN_CLIP);
3519 let ti = self.find_free_track(TrackKind::Video, at, dur, video_track);
3520 let mut c = Clip::new(self.new_id(), ClipKind::Sequence, name, at, dur);
3521 c.sequence = seq;
3522 let id = c.id;
3523 self.tracks[ti].clips.push(c);
3524 self.tracks[ti].sort();
3525 Some(id)
3526 }
3527 pub fn nest_selection(&mut self, ids: &[Id], name: impl Into<String>) -> Option<Id> {
3530 let ids = self.expand_links(ids);
3531 if ids.is_empty() {
3532 return None;
3533 }
3534 let start = ids.iter().filter_map(|&id| self.clip(id)).map(|c| c.start).fold(f64::INFINITY, f64::min);
3535 let end = ids.iter().filter_map(|&id| self.clip(id)).map(|c| c.end()).fold(0.0, f64::max);
3536 if !start.is_finite() || end <= start {
3537 return None;
3538 }
3539 let (w, h, fps) = (self.width, self.height, self.fps);
3540 let seq_id = self.new_sequence(name, w, h, fps);
3541 let mut moved: Vec<(TrackKind, usize, Clip)> = Vec::new(); let mut top_video: Option<usize> = None;
3544 for &id in &ids {
3545 let Some((ti, ci)) = self.find(id) else { continue };
3546 let kind = self.tracks[ti].kind;
3547 let list = if kind == TrackKind::Video { self.video_tracks() } else { self.audio_tracks() };
3548 let pos = list.iter().position(|&x| x == ti).unwrap_or(0);
3549 if kind == TrackKind::Video {
3550 top_video = Some(top_video.map_or(ti, |t: usize| t.max(ti)));
3551 }
3552 let mut c = self.tracks[ti].clips.remove(ci);
3553 c.start -= start;
3554 moved.push((kind, pos, c));
3555 }
3556 self.tidy();
3557 let next_ids: Vec<Id> = (0..64).map(|_| self.new_id()).collect();
3558 let mut nid = next_ids.into_iter();
3559 if let Some(seq) = self.sequence_mut(seq_id) {
3560 for (kind, pos, c) in moved {
3561 loop {
3563 let have = seq.tracks.iter().filter(|t| t.kind == kind).count();
3564 if have > pos {
3565 break;
3566 }
3567 let id = nid.next().unwrap_or(0);
3568 let n = have + 1;
3569 let name = format!("{}{}", if kind == TrackKind::Video { "V" } else { "A" }, n);
3570 let t = Track::new(id, kind, name);
3571 let idx = if kind == TrackKind::Video {
3572 seq.tracks.iter().rposition(|t| t.kind == TrackKind::Video).map(|i| i + 1).unwrap_or(0)
3573 } else {
3574 seq.tracks.len()
3575 };
3576 seq.tracks.insert(idx, t);
3577 }
3578 let ti =
3579 seq.tracks.iter().enumerate().filter(|(_, t)| t.kind == kind).nth(pos).map(|(i, _)| i).unwrap_or(0);
3580 seq.tracks[ti].clips.push(c);
3581 seq.tracks[ti].sort();
3582 }
3583 }
3584 let vt = top_video.or_else(|| self.video_tracks().last().copied());
3585 self.insert_sequence_clip(seq_id, start, vt);
3586 Some(seq_id)
3587 }
3588
3589 pub fn clip_label(&self, clip: &Clip) -> u8 {
3592 if clip.label != 0 {
3593 return clip.label;
3594 }
3595 if clip.uses_asset() {
3596 return self.asset(clip.asset).map(|a| a.label).unwrap_or(0);
3597 }
3598 0
3599 }
3600
3601 fn plan_find_in(items: &mut [PlanItem], id: Id) -> Option<&mut PlanItem> {
3603 for it in items {
3604 if it.id == id {
3605 return Some(it);
3606 }
3607 if let Some(f) = Self::plan_find_in(&mut it.children, id) {
3608 return Some(f);
3609 }
3610 }
3611 None
3612 }
3613 pub fn plan_item_mut(&mut self, id: Id) -> Option<&mut PlanItem> {
3614 Self::plan_find_in(&mut self.plan, id)
3615 }
3616 pub fn plan_add(&mut self, parent: Option<Id>, title: impl Into<String>) -> Id {
3618 let id = self.new_id();
3619 let item = PlanItem { id, title: title.into(), ..Default::default() };
3620 match parent.and_then(|p| self.plan_item_mut(p)) {
3621 Some(p) => p.children.push(item),
3622 None => self.plan.push(item),
3623 }
3624 id
3625 }
3626 pub fn plan_remove(&mut self, id: Id) {
3627 fn rm(items: &mut Vec<PlanItem>, id: Id) {
3628 items.retain(|i| i.id != id);
3629 for i in items {
3630 rm(&mut i.children, id);
3631 }
3632 }
3633 rm(&mut self.plan, id);
3634 }
3635 pub fn plan_assets(&self) -> std::collections::HashSet<Id> {
3637 fn walk(items: &[PlanItem], out: &mut std::collections::HashSet<Id>) {
3638 for i in items {
3639 out.extend(i.assets.iter().copied());
3640 walk(&i.children, out);
3641 }
3642 }
3643 let mut s = std::collections::HashSet::new();
3644 walk(&self.plan, &mut s);
3645 s
3646 }
3647
3648 pub fn used_assets(&self) -> std::collections::HashSet<Id> {
3651 let mut s = std::collections::HashSet::new();
3652 let mut add = |tracks: &[Track]| {
3653 for c in tracks.iter().flat_map(|t| t.clips.iter()) {
3654 if c.uses_asset() {
3655 s.insert(c.asset);
3656 }
3657 }
3658 };
3659 add(&self.tracks);
3660 if let Some(st) = &self.main_stash {
3661 add(&st.tracks);
3662 }
3663 for seq in &self.sequences {
3664 add(&seq.tracks);
3665 }
3666 s
3667 }
3668 pub fn remove_unused_assets(&mut self) -> usize {
3670 let used = self.used_assets();
3671 let planned = self.plan_assets();
3672 let before = self.assets.len();
3673 self.assets.retain(|a| used.contains(&a.id) || planned.contains(&a.id));
3674 before - self.assets.len()
3675 }
3676
3677 pub fn auto_cut(&mut self, ids: &[Id], cuts: &[f64], remove: &[(f64, f64)], ripple: bool) -> usize {
3682 let ids = self.expand_links(ids);
3683 let mut group = ids.clone();
3684 for &t in cuts {
3685 let new = self.split_at(t, Some(&group));
3686 group.extend(new);
3687 }
3688 let victims: Vec<Id> = group
3689 .iter()
3690 .filter_map(|&id| self.clip(id))
3691 .filter(|c| {
3692 let mid = c.start + c.duration / 2.0;
3693 remove.iter().any(|&(a, b)| mid >= a && mid < b)
3694 })
3695 .map(|c| c.id)
3696 .collect();
3697 let n = victims.len();
3698 if n > 0 {
3699 self.delete_clips(&victims, ripple);
3700 }
3701 n
3702 }
3703
3704 pub fn place_clips(&mut self, clips: Vec<Clip>, assets: Vec<Asset>, at: f64) -> Vec<Id> {
3708 let mut asset_map: std::collections::HashMap<Id, Id> = std::collections::HashMap::new();
3709 for a in assets {
3710 let old = a.id;
3711 let new = self.add_asset(a);
3712 asset_map.insert(old, new);
3713 }
3714 let mut link_map: std::collections::HashMap<Id, Id> = std::collections::HashMap::new();
3715 let mut out = Vec::new();
3716 for mut c in clips {
3717 c.id = self.new_id();
3718 for i in 0..c.markers.len() {
3720 c.markers[i].id = self.new_id();
3721 }
3722 if c.link != 0 {
3723 let l = *link_map.entry(c.link).or_insert_with(|| 0);
3724 c.link = if l == 0 {
3725 let nl = self.new_id();
3726 link_map.insert(c.link, nl);
3727 nl
3728 } else {
3729 l
3730 };
3731 }
3732 if c.uses_asset() && c.asset != 0 {
3733 match asset_map.get(&c.asset) {
3734 Some(&a) => c.asset = a,
3735 None => continue,
3736 }
3737 }
3738 if c.kind == ClipKind::Sequence && self.sequence(c.sequence).is_none() {
3739 continue;
3740 }
3741 c.start += at;
3742 let kind = if c.kind == ClipKind::Audio { TrackKind::Audio } else { TrackKind::Video };
3743 let ti = self.find_free_track(kind, c.start, c.duration, None);
3744 out.push(c.id);
3745 self.tracks[ti].clips.push(c);
3746 self.tracks[ti].sort();
3747 }
3748 out
3749 }
3750
3751 pub fn flow_clips(&mut self, a: Id, b: Id) -> bool {
3756 let (Some(ca), Some(cb)) = (self.clip(a).cloned(), self.clip(b).cloned()) else { return false };
3757 if (ca.end() - cb.start).abs() > ABUT_EPS {
3758 return false;
3759 }
3760 let props = ["x", "y", "scale", "rotation", "opacity"];
3761 fn pick(c: &mut Clip, i: usize) -> &mut Animated {
3762 match i {
3763 0 => &mut c.x,
3764 1 => &mut c.y,
3765 2 => &mut c.scale,
3766 3 => &mut c.rotation,
3767 _ => &mut c.opacity,
3768 }
3769 }
3770 let (da, db) = (ca.duration, cb.duration);
3771 let mut na = ca.clone();
3772 let mut nb = cb.clone();
3773 for i in 0..props.len() {
3774 let va_end = pick(&mut na, i).at(da);
3775 let vb_start = pick(&mut nb, i).at(0.0);
3776 let animated = pick(&mut na, i).is_animated() || pick(&mut nb, i).is_animated();
3777 if !animated {
3778 continue;
3779 }
3780 let meet = (va_end + vb_start) / 2.0;
3781 let pa = pick(&mut na, i);
3782 if !pa.is_animated() {
3783 pa.toggle_key(0.0);
3784 }
3785 pa.set_at(da, meet);
3786 if let Some(k) = pa.keys.iter_mut().rev().nth(1) {
3787 k.ease = Ease::EaseIn;
3788 }
3789 let pb = pick(&mut nb, i);
3790 if !pb.is_animated() {
3791 pb.toggle_key(db);
3792 }
3793 pb.set_at(0.0, meet);
3794 if let Some(k) = pb.keys.first_mut() {
3795 k.ease = Ease::EaseOut;
3796 }
3797 }
3798 *self.clip_mut(a).unwrap() = na;
3799 *self.clip_mut(b).unwrap() = nb;
3800 true
3801 }
3802
3803 pub fn label_color(&self, idx: u8) -> Option<[u8; 3]> {
3806 (idx > 0).then(|| self.labels.get(idx as usize - 1).map(|l| l.color)).flatten()
3807 }
3808 pub fn label_name(&self, idx: u8) -> &str {
3809 if idx == 0 {
3810 return "None";
3811 }
3812 self.labels.get(idx as usize - 1).map(|l| l.name.as_str()).unwrap_or("None")
3813 }
3814 pub fn add_label(&mut self, name: impl Into<String>, color: [u8; 3]) -> u8 {
3816 self.labels.push(Label { name: name.into(), color });
3817 self.labels.len() as u8
3818 }
3819 pub fn remove_label(&mut self, idx: u8) {
3821 if idx == 0 || idx as usize > self.labels.len() {
3822 return;
3823 }
3824 self.labels.remove(idx as usize - 1);
3825 let fix = |l: &mut u8| {
3826 if *l == idx {
3827 *l = 0;
3828 } else if *l > idx {
3829 *l -= 1;
3830 }
3831 };
3832 for a in &mut self.assets {
3833 fix(&mut a.label);
3834 }
3835 for m in &mut self.markers {
3836 fix(&mut m.label);
3837 }
3838 let mut all: Vec<&mut Track> = self.tracks.iter_mut().collect();
3839 if let Some(st) = &mut self.main_stash {
3840 all.extend(st.tracks.iter_mut());
3841 }
3842 for sq in &mut self.sequences {
3843 all.extend(sq.tracks.iter_mut());
3844 }
3845 for t in all {
3846 for c in &mut t.clips {
3847 fix(&mut c.label);
3848 for m in &mut c.markers {
3849 fix(&mut m.label);
3850 }
3851 }
3852 }
3853 }
3854
3855 pub fn add_marker(&mut self, t: f64, name: impl Into<String>) -> Id {
3857 let id = self.new_id();
3858 self.markers.push(Marker { id, t: t.max(0.0), name: name.into(), ..Default::default() });
3859 self.sort_markers();
3860 id
3861 }
3862 pub fn remove_marker(&mut self, id: Id) {
3863 self.markers.retain(|m| m.id != id);
3864 for t in &mut self.tracks {
3865 for c in &mut t.clips {
3866 c.markers.retain(|m| m.id != id);
3867 }
3868 }
3869 }
3870 pub fn marker_mut(&mut self, id: Id) -> Option<&mut Marker> {
3871 if let Some(i) = self.markers.iter().position(|m| m.id == id) {
3872 return self.markers.get_mut(i);
3873 }
3874 self.tracks.iter_mut().flat_map(|t| t.clips.iter_mut()).flat_map(|c| c.markers.iter_mut()).find(|m| m.id == id)
3875 }
3876 pub fn sort_markers(&mut self) {
3877 self.markers.sort_by(|a, b| a.t.total_cmp(&b.t));
3878 }
3879 pub fn add_clip_marker(&mut self, clip: Id, local_t: f64, name: impl Into<String>) -> Option<Id> {
3881 let id = self.new_id();
3882 let name = name.into();
3883 let c = self.clip_mut(clip)?;
3884 c.markers.push(Marker { id, t: local_t.clamp(0.0, c.duration), name, ..Default::default() });
3885 c.markers.sort_by(|a, b| a.t.total_cmp(&b.t));
3886 Some(id)
3887 }
3888 pub fn markers_in_timeline(&self) -> Vec<(Id, f64, f64, String, u8)> {
3890 let mut v: Vec<(Id, f64, f64, String, u8)> =
3891 self.markers.iter().map(|m| (m.id, m.t, m.duration, m.name.clone(), m.label)).collect();
3892 for (_, c) in self.all_clips() {
3893 for m in &c.markers {
3894 v.push((m.id, c.start + m.t, m.duration, m.name.clone(), m.label));
3895 }
3896 }
3897 v.sort_by(|a, b| a.1.total_cmp(&b.1));
3898 v
3899 }
3900
3901 pub fn main_bus(&mut self) -> Id {
3904 if self.buses.is_empty() {
3905 let id = self.new_id();
3906 self.buses.push(Bus { id, name: "Main".into(), output: 0, ..Default::default() });
3907 }
3908 self.buses[0].id
3909 }
3910 pub fn bus(&self, id: Id) -> Option<&Bus> {
3911 self.buses.iter().find(|b| b.id == id)
3912 }
3913 pub fn bus_mut(&mut self, id: Id) -> Option<&mut Bus> {
3914 self.buses.iter_mut().find(|b| b.id == id)
3915 }
3916 pub fn add_bus(&mut self, name: impl Into<String>) -> Id {
3917 let main = self.main_bus();
3918 let id = self.new_id();
3919 self.buses.push(Bus { id, name: name.into(), output: main, ..Default::default() });
3920 id
3921 }
3922 pub fn remove_bus(&mut self, id: Id) {
3924 let main = self.main_bus();
3925 if id == main {
3926 return;
3927 }
3928 self.buses.retain(|b| b.id != id);
3929 for b in &mut self.buses {
3930 if b.output == id {
3931 b.output = main;
3932 }
3933 }
3934 for t in &mut self.tracks {
3935 if t.bus == id {
3936 t.bus = 0;
3937 }
3938 for c in &mut t.clips {
3939 if c.bus == id {
3940 c.bus = 0;
3941 }
3942 }
3943 }
3944 }
3945 pub fn bus_of(&self, track: usize, clip: &Clip) -> Id {
3947 let main = self.buses.first().map(|b| b.id).unwrap_or(0);
3948 if clip.bus != 0 && self.bus(clip.bus).is_some() {
3949 return clip.bus;
3950 }
3951 let t = self.tracks.get(track).map(|t| t.bus).unwrap_or(0);
3952 if t != 0 && self.bus(t).is_some() {
3953 t
3954 } else {
3955 main
3956 }
3957 }
3958
3959 pub fn add_shape_clip(&mut self, kind: ShapeKind, at: f64, dur: f64) -> Id {
3962 let prefer = self.video_tracks().last().copied();
3963 let ti = self.find_free_track(TrackKind::Video, at, dur, prefer);
3964 let mut c = Clip::new(self.new_id(), ClipKind::Shape, kind.name(), at, dur);
3965 c.shape = Some(ShapeStyle::new(kind));
3966 let id = c.id;
3967 self.tracks[ti].clips.push(c);
3968 self.tracks[ti].sort();
3969 id
3970 }
3971 pub fn add_adjustment_clip(&mut self, at: f64, dur: f64) -> Id {
3973 let prefer = self.video_tracks().last().copied();
3974 let ti = self.find_free_track(TrackKind::Video, at, dur, prefer);
3975 let c = Clip::new(self.new_id(), ClipKind::Adjustment, "Adjustment", at, dur);
3976 let id = c.id;
3977 self.tracks[ti].clips.push(c);
3978 self.tracks[ti].sort();
3979 id
3980 }
3981 pub fn add_container_clip(&mut self, at: f64, dur: f64) -> (Id, Id) {
3983 let dur = dur.max(MIN_CLIP);
3984 let link = self.new_id();
3985 let prefer_v = self.video_tracks().last().copied();
3986 let vti = self.find_free_track(TrackKind::Video, at, dur, prefer_v);
3987 let mut vc = Clip::new(self.new_id(), ClipKind::Video, "Container", at, dur);
3988 vc.link = link;
3989 vc.container = true;
3990 let vid = vc.id;
3991 self.tracks[vti].clips.push(vc);
3992 self.tracks[vti].sort();
3993
3994 let prefer_a = self.audio_tracks().first().copied();
3995 let ati = self.find_free_track(TrackKind::Audio, at, dur, prefer_a);
3996 let mut ac = Clip::new(self.new_id(), ClipKind::Audio, "Container Audio", at, dur);
3997 ac.link = link;
3998 ac.container = true;
3999 let aid = ac.id;
4000 self.tracks[ati].clips.push(ac);
4001 self.tracks[ati].sort();
4002
4003 (vid, aid)
4004 }
4005 pub fn add_container_from_asset(&mut self, asset_id: Id, at: f64, video_track: Option<usize>) -> Vec<Id> {
4007 let ids = self.insert_asset_clips(asset_id, at, video_track);
4008 for &id in &ids {
4009 if let Some(c) = self.clip_mut(id) {
4010 c.container = true;
4011 }
4012 }
4013 ids
4014 }
4015 pub fn replace_container_media(&mut self, clip_id: Id, new_asset_id: Id) -> bool {
4017 let Some(asset) = self.asset(new_asset_id).cloned() else { return false };
4018 let Some(clip) = self.clip_mut(clip_id) else { return false };
4019 if !clip.container {
4020 return false;
4021 }
4022 clip.asset = asset.id;
4023 clip.src_in = 0.0;
4024 if clip.kind == ClipKind::Audio {
4025 clip.audio_stream = 0;
4026 } else if asset.kind == ClipKind::Image {
4027 clip.kind = ClipKind::Image;
4028 } else if asset.kind == ClipKind::Video {
4029 clip.kind = ClipKind::Video;
4030 }
4031 let base_name = asset.name();
4032 clip.name = if !clip.container_label.is_empty() {
4033 format!("{} [{}]", base_name, clip.container_label)
4034 } else {
4035 base_name
4036 };
4037 true
4038 }
4039 pub fn replace_container_pair(&mut self, video_id: Id, new_asset_id: Id) -> bool {
4041 let Some(asset) = self.asset(new_asset_id).cloned() else { return false };
4042 let Some(vclip) = self.clip(video_id).cloned() else { return false };
4043 if !vclip.container {
4044 return false;
4045 }
4046 let link = vclip.link;
4047 if !self.replace_container_media(video_id, new_asset_id) {
4048 return false;
4049 }
4050 if link != 0 {
4051 let mut stream_idx = 0;
4052 let linked_ids: Vec<Id> =
4053 self.all_clips().filter(|(_, c)| c.link == link && c.id != video_id).map(|(_, c)| c.id).collect();
4054 for aid in linked_ids {
4055 if let Some(c) = self.clip_mut(aid) {
4056 if c.kind == ClipKind::Audio && c.container {
4057 c.asset = asset.id;
4058 c.src_in = 0.0;
4059 if stream_idx < asset.audio_streams.len() {
4060 c.audio_stream = asset.audio_streams[stream_idx].index;
4061 stream_idx += 1;
4062 } else {
4063 c.audio_stream = 0;
4064 }
4065 let base_name = if asset.audio_streams.len() > 1 {
4066 format!(
4067 "{} [{}]",
4068 asset.name(),
4069 asset.audio_streams.get(c.audio_stream).map(|s| s.label()).unwrap_or_default()
4070 )
4071 } else {
4072 asset.name()
4073 };
4074 c.name = if !c.container_label.is_empty() {
4075 format!("{} [{}]", base_name, c.container_label)
4076 } else {
4077 base_name
4078 };
4079 }
4080 }
4081 }
4082 }
4083 true
4084 }
4085 pub fn make_container(&mut self, clip_ids: &[Id]) {
4087 let all = self.expand_links(clip_ids);
4088 for id in all {
4089 if let Some(c) = self.clip_mut(id) {
4090 c.container = true;
4091 }
4092 }
4093 }
4094 pub fn unmake_container(&mut self, clip_ids: &[Id]) {
4096 let all = self.expand_links(clip_ids);
4097 for id in all {
4098 if let Some(c) = self.clip_mut(id) {
4099 c.container = false;
4100 c.container_label.clear();
4101 }
4102 }
4103 }
4104
4105 pub fn path(&self, id: Id) -> Option<&PathAsset> {
4107 self.paths.iter().find(|p| p.id == id)
4108 }
4109 pub fn add_path(&mut self, name: String, points: Vec<(f32, f32, f32)>) -> Id {
4111 let id = self.new_id();
4112 let name = if name.trim().is_empty() { format!("Path {}", self.paths.len() + 1) } else { name };
4113 self.paths.push(PathAsset { id, name, points });
4114 id
4115 }
4116 pub fn path_from_clip(&self, clip: Id) -> Vec<(f32, f32, f32)> {
4119 let Some(c) = self.clip(clip) else { return Vec::new() };
4120 let (ox, oy) = (c.x.value as f32, c.y.value as f32);
4121 let Some(s) = &c.shape else { return Vec::new() };
4122 s.path_points().into_iter().map(|(x, y, t)| (x + ox, y + oy, t)).collect()
4123 }
4124 pub fn apply_path(&mut self, clip: Id, points: &[(f32, f32, f32)]) -> bool {
4127 let Some(dur) = self.clip(clip).map(|c| c.duration) else { return false };
4128 let (x, y) = path_to_keys(points, dur);
4129 if x.keys.len() < 2 {
4130 return false;
4131 }
4132 let Some(c) = self.clip_mut(clip) else { return false };
4133 c.x = x;
4134 c.y = y;
4135 true
4136 }
4137
4138 pub fn ensure_graph(&mut self, clip: Id) -> bool {
4141 let Some(c) = self.clip(clip) else { return false };
4142 if c.graph.is_some() {
4143 return true;
4144 }
4145 let effects = c.effects.clone();
4146 let mut next = || {
4147 self.next_id += 1;
4148 self.next_id
4149 };
4150 let g = NodeGraph::from_effects(&effects, &mut next);
4151 if let Some(c) = self.clip_mut(clip) {
4152 c.graph = Some(g);
4153 }
4154 true
4155 }
4156 pub fn unlink_graph(&mut self, clip: Id) -> Result<usize, String> {
4163 let g = self.clip(clip).and_then(|c| c.graph.as_ref()).ok_or("that clip has no node graph")?;
4164 let converted = g.to_effects().ok();
4165 let c = self.clip_mut(clip).ok_or("that clip is gone")?;
4166 let n = match converted {
4167 Some(effects) => {
4168 let n = effects.len();
4169 c.effects = effects;
4170 n
4171 }
4172 None => 0,
4174 };
4175 c.graph = None;
4176 Ok(n)
4177 }
4178 pub fn add_node(&mut self, clip: Id, kind: NodeKind, x: f32, y: f32) -> Option<Id> {
4180 self.ensure_graph(clip);
4181 let id = self.new_id();
4182 let c = self.clip_mut(clip)?;
4183 let g = c.graph.as_mut()?;
4184 g.nodes.push(Node { id, kind, x, y, enabled: true });
4185 Some(id)
4186 }
4187
4188 pub fn copy_attributes(&self, clip: Id) -> Option<Clip> {
4191 self.clip(clip).cloned()
4192 }
4193 pub fn paste_attributes(&mut self, src: &Clip, ids: &[Id], set: AttrSet) -> usize {
4196 let mut n = 0;
4197 for &id in ids {
4198 let Some(c) = self.clip_mut(id) else { continue };
4199 if c.id == src.id {
4200 continue;
4201 }
4202 if set.transform {
4203 c.x = src.x.clone();
4204 c.y = src.y.clone();
4205 c.scale = src.scale.clone();
4206 c.rotation = src.rotation.clone();
4207 }
4208 if set.opacity {
4209 c.opacity = src.opacity.clone();
4210 }
4211 if set.blend {
4212 c.blend = src.blend;
4213 }
4214 if set.effects {
4215 c.effects = src.effects.clone();
4216 }
4217 if set.graph {
4218 c.graph = src.graph.clone();
4219 }
4220 if set.mask {
4221 c.mask = src.mask.clone();
4222 }
4223 if set.speed {
4224 c.set_speed(src.speed);
4225 c.reverse = src.reverse;
4226 c.freeze = src.freeze;
4227 }
4228 if set.audio {
4229 c.volume = src.volume.clone();
4230 c.pan = src.pan.clone();
4231 c.fade_in = src.fade_in;
4232 c.fade_out = src.fade_out;
4233 c.bus = src.bus;
4234 }
4235 if set.text {
4236 if let Some(t) = &src.text {
4237 c.text = Some(t.clone());
4238 }
4239 }
4240 if set.shape {
4241 if let Some(sh) = &src.shape {
4242 c.shape = Some(sh.clone());
4243 }
4244 }
4245 if set.label {
4246 c.label = src.label;
4247 }
4248 if set.markers {
4249 c.markers = src.markers.clone();
4250 }
4251 n += 1;
4252 }
4253 if n > 0 {
4254 self.tidy();
4255 }
4256 n
4257 }
4258
4259 pub fn to_json(&self) -> String {
4261 serde_json::to_string_pretty(self).unwrap_or_default()
4262 }
4263 pub fn from_json(s: &str) -> Result<Self, String> {
4264 let mut p: Project = serde_json::from_str(s).map_err(|e| e.to_string())?;
4265 let max_id = p
4266 .assets
4267 .iter()
4268 .map(|a| a.id)
4269 .chain(p.tracks.iter().map(|t| t.id))
4270 .chain(p.tracks.iter().flat_map(|t| t.transitions.iter().map(|x| x.id)))
4271 .chain(p.subtitles.iter().map(|c| c.id))
4272 .chain(p.markers.iter().map(|m| m.id))
4273 .chain(p.buses.iter().map(|b| b.id))
4274 .chain(p.all_clips().flat_map(|(_, c)| c.markers.iter().map(|m| m.id)))
4275 .chain(p.all_clips().filter_map(|(_, c)| c.graph.as_ref()).flat_map(|g| g.nodes.iter().map(|n| n.id)))
4276 .chain(p.sequences.iter().map(|s| s.id))
4277 .chain(p.sequences.iter().flat_map(|s| s.tracks.iter().map(|t| t.id)))
4278 .chain(
4279 p.sequences.iter().flat_map(|s| s.tracks.iter().flat_map(|t| t.clips.iter().map(|c| c.id.max(c.link)))),
4280 )
4281 .chain(
4282 p.main_stash
4283 .iter()
4284 .flat_map(|s| s.tracks.iter().flat_map(|t| t.clips.iter().map(|c| c.id.max(c.link)))),
4285 )
4286 .chain(p.all_clips().map(|(_, c)| c.id.max(c.link)))
4287 .max()
4288 .unwrap_or(0);
4289 fn plan_max(items: &[PlanItem]) -> Id {
4290 items.iter().map(|i| i.id.max(plan_max(&i.children))).max().unwrap_or(0)
4291 }
4292 p.next_id = p.next_id.max(max_id).max(plan_max(&p.plan));
4293 if p.editing.is_some_and(|id| p.sequence(id).is_none()) {
4294 p.editing = None;
4295 }
4296 if !(p.fps >= 1.0 && p.fps <= 1000.0) {
4298 p.fps = 30.0;
4299 }
4300 p.width = p.width.max(16);
4301 p.height = p.height.max(16);
4302 for t in &mut p.tracks {
4303 t.clips.retain(|c| c.start >= 0.0 && c.duration.is_finite() && c.duration > 0.0);
4304 for c in &mut t.clips {
4305 if !(c.speed.is_finite() && c.speed > 0.0) {
4306 c.speed = 1.0;
4307 }
4308 c.speed_curve.keys.retain(|k| k.t.is_finite() && k.v.is_finite() && k.v > 0.0);
4309 if !c.speed_curve.is_animated() {
4310 c.speed_curve.value = c.speed; }
4312 for e in &mut c.effects {
4313 while e.params.len() < e.kind.params().len() {
4315 let d = e.kind.params()[e.params.len()].default;
4316 e.params.push(Animated::new(d));
4317 }
4318 }
4319 }
4320 }
4321 p.subtitles.retain(|c| c.start.is_finite() && c.end.is_finite() && c.end > c.start);
4322 if p.labels.is_empty() {
4323 p.labels = default_labels();
4324 }
4325 p.markers.retain(|m| m.t.is_finite() && m.t >= 0.0);
4326 p.sort_markers();
4327 p.tidy();
4328 p.sort_cues();
4329 Ok(p)
4330 }
4331 pub fn save(&self, path: &Path) -> std::io::Result<()> {
4333 let mut tmp = path.as_os_str().to_owned();
4334 tmp.push(".tmp");
4335 std::fs::write(&tmp, self.to_json())?;
4336 std::fs::rename(&tmp, path)
4337 }
4338 pub fn load(path: &Path) -> Result<Self, String> {
4339 let s = std::fs::read_to_string(path).map_err(|e| e.to_string())?;
4340 Self::from_json(&s)
4341 }
4342}
4343
4344#[cfg(test)]
4345mod tests {
4346 use super::*;
4347
4348 fn asset(id: Id, dur: f64, streams: usize) -> Asset {
4349 Asset {
4350 id,
4351 path: format!("C:/v{id}.mp4"),
4352 kind: ClipKind::Video,
4353 duration: dur,
4354 width: 1280,
4355 height: 720,
4356 fps: 30.0,
4357 audio_streams: (0..streams)
4358 .map(|i| AudioStreamInfo { index: i, channels: 2, sample_rate: 48000, ..Default::default() })
4359 .collect(),
4360 codec: "h264".into(),
4361 folder: String::new(),
4362 tags: Vec::new(),
4363 label: 0,
4364 description: String::new(),
4365 }
4366 }
4367
4368 #[test]
4369 fn animated_interp() {
4370 let mut a = Animated::new(5.0);
4371 assert_eq!(a.at(3.0), 5.0);
4372 a.toggle_key(0.0);
4373 a.set_at(2.0, 15.0);
4374 assert_eq!(a.keys.len(), 2);
4375 assert!((a.at(1.0) - 10.0).abs() < 1e-9);
4376 assert_eq!(a.at(-1.0), 5.0);
4377 assert_eq!(a.at(9.0), 15.0);
4378 a.toggle_key(2.0);
4379 a.toggle_key(0.0);
4380 assert!(!a.is_animated());
4381 assert_eq!(a.value, 5.0);
4382 }
4383
4384 #[test]
4385 fn easing_and_key_moves() {
4386 let mut a = Animated::new(0.0);
4387 a.toggle_key(0.0);
4388 a.set_at(2.0, 10.0);
4389 a.set_ease_at(0.0, Ease::Hold);
4390 assert_eq!(a.at(1.0), 0.0);
4391 a.set_ease_at(0.0, Ease::EaseInOut);
4392 assert!((a.at(1.0) - 5.0).abs() < 1e-9);
4393 assert!(a.at(0.5) < 2.5);
4394 a.set_ease_at(0.0, Ease::EaseIn);
4395 assert!((a.at(1.0) - 2.5).abs() < 1e-9);
4396 let j = a.move_key(1, -1.0); assert_eq!(j, 0);
4398 assert_eq!(a.keys[0].v, 10.0);
4399 }
4400
4401 #[test]
4402 fn from_media_layout() {
4403 let p = Project::from_media(asset(0, 10.0, 2));
4404 assert_eq!(p.width, 1280);
4405 assert_eq!(p.tracks.len(), 3); assert_eq!(p.tracks[0].clips.len(), 1);
4407 assert_eq!(p.tracks[2].clips[0].audio_stream, 1);
4408 let v = &p.tracks[0].clips[0];
4409 assert_eq!(p.linked(v.id).len(), 3);
4410 assert_eq!(p.duration(), 10.0);
4411 assert!(p.source_video.is_some());
4412 }
4413
4414 #[test]
4415 fn split_delete_ripple() {
4416 let mut p = Project::from_media(asset(0, 10.0, 1));
4417 let new = p.split_at(4.0, None);
4418 assert_eq!(new.len(), 2);
4419 assert_eq!(p.tracks[0].clips.len(), 2);
4420 assert!((p.tracks[0].clips[1].src_in - 4.0).abs() < 1e-9);
4421 assert_eq!(p.linked(new[0]).len(), 2);
4423 assert!(p.linked(new[0]).contains(&new[1]));
4424 let left = p.tracks[0].clips[0].id;
4425 assert!(!p.linked(left).contains(&new[0]));
4426 let ids = p.linked(left);
4428 p.delete_clips(&ids, true);
4429 assert_eq!(p.tracks[0].clips.len(), 1);
4430 assert!((p.tracks[0].clips[0].start).abs() < 1e-9);
4431 assert!((p.tracks[1].clips[0].start).abs() < 1e-9);
4432 assert!((p.duration() - 6.0).abs() < 1e-9);
4433 }
4434
4435 #[test]
4436 fn trim_to_range() {
4437 let mut p = Project::from_media(asset(0, 10.0, 1));
4438 p.trim_to_range(2.0, 5.0);
4439 assert!((p.duration() - 3.0).abs() < 1e-9);
4440 let c = &p.tracks[0].clips[0];
4441 assert!((c.src_in - 2.0).abs() < 1e-9);
4442 assert!(c.start.abs() < 1e-9);
4443 }
4444
4445 #[test]
4446 fn move_and_fit() {
4447 let mut p = Project::from_media(asset(0, 10.0, 1));
4448 let a2 = asset(1, 3.0, 0);
4449 let aid = p.add_asset(a2);
4450 let ids = p.insert_asset_clips(aid, 10.0, Some(0));
4451 assert_eq!(ids.len(), 1);
4452 assert_eq!(p.track_of(ids[0]), Some(0));
4453 assert!(!p.move_clips(&ids, -5.0, 0, None));
4455 assert!(p.move_clips(&ids, 5.0, 0, None));
4457 assert!((p.clip(ids[0]).unwrap().start - 15.0).abs() < 1e-9);
4458 assert!(!p.move_clips(&ids, 0.0, 1, Some(TrackKind::Video)));
4460 p.add_track(TrackKind::Video);
4461 assert!(p.move_clips(&ids, -15.0, 1, Some(TrackKind::Video)));
4462 assert_eq!(p.track_of(ids[0]), Some(1));
4463 }
4464
4465 #[test]
4466 fn json_roundtrip() {
4467 let mut p = Project::from_media(asset(0, 10.0, 2));
4468 p.add_text_clip(1.0, 2.0);
4469 p.add_cue(0.5, 1.5, "hi");
4470 let s = p.to_json();
4471 let q = Project::from_json(&s).unwrap();
4472 assert_eq!(q.tracks.len(), p.tracks.len());
4473 assert_eq!(q.to_json(), s);
4474 let mut q = q;
4475 let id = q.new_id();
4476 assert!(id > p.next_id);
4477 }
4478
4479 #[test]
4480 fn polygon_points_round_trip() {
4481 let mut p = Project::new();
4482 let id = p.add_shape_clip(ShapeKind::Polygon, 0.0, 2.0);
4483 let s = p.clip_mut(id).unwrap().shape.as_mut().unwrap();
4484 assert!(s.poly_points().is_none(), "no points = the regular n-gon");
4485 s.points = vec![(0.0, -50.0), (60.0, 40.0), (-60.0, 40.0)];
4486 let key = s.cache_key();
4487 let t = Project::from_json(&p.to_json()).unwrap().clip(id).unwrap().shape.clone().unwrap();
4488 assert_eq!(t.points, vec![(0.0, -50.0), (60.0, 40.0), (-60.0, 40.0)]);
4489 assert_eq!(t.poly_points().map(|v| v.len()), Some(3));
4490 assert_eq!(t.cache_key(), key, "a round trip is the same rasterised layer");
4491 let mut moved = t.clone();
4493 moved.points[1].0 += 5.0;
4494 assert_ne!(moved.cache_key(), key);
4495 let old = p.to_json().replace("\"points\"", "\"was_not_a_field\"");
4497 let o = Project::from_json(&old).unwrap();
4498 assert!(o.clip(id).unwrap().shape.as_ref().unwrap().poly_points().is_none());
4499 }
4500
4501 #[test]
4502 fn a_recorded_stroke_becomes_a_motion_path() {
4503 let mut p = Project::new();
4504 let id = p.add_shape_clip(ShapeKind::Draw, 0.0, 4.0);
4505 let s = p.clip_mut(id).unwrap().shape.as_mut().unwrap();
4506 s.strokes = vec![
4508 Stroke {
4509 color: [255; 4],
4510 width: 6.0,
4511 points: vec![(-100.0, 0.0, 0.0), (0.0, 50.0, 0.5), (0.0, 50.0, 0.5)],
4512 },
4513 Stroke { color: [255; 4], width: 6.0, points: vec![(60.0, 20.0, 2.0), (120.0, -30.0, 2.5)] },
4514 ];
4515 p.clip_mut(id).unwrap().x.value = 10.0; let pts = p.path_from_clip(id);
4517 assert_eq!(pts.len(), 5);
4518 let mover = p.add_shape_clip(ShapeKind::Rect, 0.0, 8.0);
4519 assert!(p.apply_path(mover, &pts));
4520 let c = p.clip(mover).unwrap();
4521 assert_eq!(c.x.keys.len(), 5);
4522 for w in c.x.keys.windows(2) {
4523 assert!(w[1].t > w[0].t, "key times must increase: {:?} -> {:?}", w[0].t, w[1].t);
4524 }
4525 assert!(c.x.keys.iter().all(|k| k.t <= 8.0 + 1e-9), "the path fits the clip: {:?}", c.x.keys);
4526 assert!((c.x.at(0.0) - -90.0).abs() < 1e-6 && (c.y.at(0.0) - 0.0).abs() < 1e-6);
4528 assert!((c.x.at(8.0) - 130.0).abs() < 1e-6 && (c.y.at(8.0) - -30.0).abs() < 1e-6);
4529 let poly = path_to_keys(&[(0.0, -50.0, 0.0), (60.0, 40.0, 0.0), (-60.0, 40.0, 0.0)], 2.0);
4531 assert_eq!(poly.0.keys.iter().map(|k| k.t).collect::<Vec<_>>(), vec![0.0, 1.0, 2.0]);
4532 assert!(path_to_keys(&[], 3.0).0.keys.is_empty(), "an empty path animates nothing");
4533 }
4534
4535 #[test]
4536 fn trims() {
4537 let mut c = Clip::new(1, ClipKind::Video, "c", 2.0, 5.0);
4538 c.src_in = 1.0;
4539 c.opacity.toggle_key(1.0);
4540 c.trim_start(0.0, 1.0); assert!((c.start - 1.0).abs() < 1e-9);
4542 assert!((c.src_in).abs() < 1e-9);
4543 assert!((c.duration - 6.0).abs() < 1e-9);
4544 assert!((c.opacity.keys[0].t - 2.0).abs() < 1e-9);
4545 c.trim_end(100.0, 10.0 - c.src_in);
4546 assert!((c.duration - 10.0).abs() < 1e-9);
4547 let mut img = Clip::new(2, ClipKind::Image, "i", 5.0, 5.0);
4549 img.trim_start(3.0, f64::INFINITY);
4550 assert!((img.start - 3.0).abs() < 1e-9);
4551 assert!((img.duration - 7.0).abs() < 1e-9);
4552 }
4553
4554 #[test]
4555 fn speed_reverse_freeze() {
4556 let mut p = Project::from_media(asset(0, 10.0, 1));
4557 let v = p.tracks[0].clips[0].id;
4558 assert!(p.set_speed(&[v], 2.0, false));
4560 let c = p.clip(v).unwrap().clone();
4561 assert!((c.duration - 5.0).abs() < 1e-9);
4562 assert!((c.src_time(1.0) - 2.0).abs() < 1e-9);
4563 assert!((p.tracks[1].clips[0].duration - 5.0).abs() < 1e-9);
4564 assert!((p.max_clip_duration(&c) - 5.0).abs() < 1e-9);
4565 p.split_at(2.0, None);
4567 assert!((p.tracks[0].clips[1].src_in - 4.0).abs() < 1e-9);
4568 assert!((p.tracks[0].clips[1].src_time(3.0) - 6.0).abs() < 1e-9);
4569 let r = p.tracks[0].clips[1].id;
4571 assert!(p.set_speed(&[r], 2.0, true));
4572 let c = p.clip(r).unwrap().clone();
4573 assert!((c.src_time(2.0) - 10.0).abs() < 1e-9);
4574 assert!((c.src_time(5.0) - 4.0).abs() < 1e-9);
4575 assert!((p.head_room(&c)).abs() < 1e-9);
4577 assert!((p.max_clip_duration(&c) - 5.0).abs() < 1e-9);
4578 let mut c2 = c.clone();
4580 c2.trim_end(6.0, p.max_clip_duration(&c));
4581 assert!((c2.src_in - 2.0).abs() < 1e-9);
4582 assert!((c2.src_time(6.0) - 2.0).abs() < 1e-9);
4583 let frozen = p.freeze_at(3.0, &[r]);
4585 assert_eq!(frozen.len(), 2); let f = p.clip(frozen[0]).unwrap();
4587 assert_eq!(f.freeze, Some(8.0));
4588 assert!((f.src_time(4.0) - 8.0).abs() < 1e-9);
4589 assert!(p.max_clip_duration(f).is_infinite());
4590 }
4591
4592 #[test]
4593 fn keyframed_speed_ramps_src_time() {
4594 let mut c = Clip::new(1, ClipKind::Video, "v", 2.0, 4.0);
4595 c.speed_curve.keys =
4596 vec![Keyframe { t: 0.0, v: 1.0, ease: Ease::Linear }, Keyframe { t: 4.0, v: 3.0, ease: Ease::Linear }];
4597 assert!(c.is_retimed());
4598 assert!((c.src_time(4.0) - 4.0).abs() < 1e-9, "{}", c.src_time(4.0));
4600 assert!((c.src_time(5.0) - 7.5).abs() < 1e-9, "{}", c.src_time(5.0));
4601 c.speed_curve.keys.clear();
4603 c.set_speed(2.0);
4604 assert!((c.src_time(4.0) - 4.0).abs() < 1e-9);
4605 assert!((c.speed_curve.value - 2.0).abs() < 1e-9);
4606 }
4607
4608 #[test]
4609 fn transitions_add_and_prune() {
4610 let mut p = Project::from_media(asset(0, 10.0, 1));
4611 let right = p.split_at(4.0, None)[0];
4612 let id = p.add_transition(right, TransitionKind::CrossFade, 1.0).unwrap();
4613 assert_eq!(p.tracks[0].transitions.len(), 1);
4614 assert_eq!(p.tracks[1].transitions.len(), 1); let (tr, l, r) = p.tracks[0].transition_at(3.9).unwrap();
4616 let (l, r) = (l.unwrap(), r.unwrap());
4617 assert_eq!(tr.id, id);
4618 assert!(l.end() == r.start);
4619 let (cut, half) = tr.cut_half(Some(l), Some(r)).unwrap();
4620 assert!((tr.progress_at(cut, half, 3.5) - 0.0).abs() < 1e-9);
4621 assert!((tr.progress_at(cut, half, 4.5) - 1.0).abs() < 1e-9);
4622 assert!(p.tracks[0].transition_at(4.6).is_none());
4623 let first = p.tracks[0].clips[0].id;
4625 assert!(p.add_transition(first, TransitionKind::Push, 1.0).is_none());
4626 assert!(p.move_clips(&p.expand_links(&[right]), 2.0, 0, None));
4628 assert!(p.tracks[0].transitions.is_empty());
4629 assert!(p.tracks[1].transitions.is_empty());
4630 }
4631
4632 #[test]
4633 fn edge_transitions_need_no_neighbour() {
4634 let mut p = Project::from_media(asset(0, 10.0, 1));
4635 let lone = p.tracks[0].clips[0].id;
4636 let id = p.add_edge_transition(lone, TransitionKind::CrossFade, 2.0, false).unwrap();
4637 assert_eq!(p.tracks[1].transitions.len(), 1, "mirrored fade on the linked audio clip");
4638 assert_eq!(p.tracks[1].transitions[0].kind, TransitionKind::CrossFade);
4639 let (tr, l, r) = p.tracks[0].transition_at(0.5).unwrap();
4640 assert_eq!((tr.id, tr.edge), (id, TransitionEdge::In));
4641 assert!(l.is_none() && r.is_some_and(|c| c.id == lone));
4642 let (cut, h) = tr.cut_half(l, r).unwrap();
4643 assert!((cut - 1.0).abs() < 1e-9 && (h - 1.0).abs() < 1e-9, "window = first 2 s of the clip");
4644 assert!((tr.progress_at(cut, h, 0.0) - 0.0).abs() < 1e-9);
4645 assert!((tr.progress_at(cut, h, 2.0) - 1.0).abs() < 1e-9);
4646 assert!(p.tracks[0].transition_at(2.5).is_none());
4647 let out = p.add_edge_transition(lone, TransitionKind::FadeToColor, 30.0, true).unwrap();
4649 let (tr, l, r) = p.tracks[0].transition_at(9.9).unwrap();
4650 assert_eq!(tr.id, out);
4651 assert!(l.is_some_and(|c| c.id == lone) && r.is_none());
4652 let (cut, h) = tr.cut_half(l, r).unwrap();
4653 assert!((cut - 5.0).abs() < 1e-9 && (h - 5.0).abs() < 1e-9, "clamped to the 10 s clip");
4654 assert_eq!(p.tracks[0].transitions.len(), 2);
4656 p.tracks[0].prune_transitions();
4657 assert_eq!(p.tracks[0].transitions.len(), 2);
4658 }
4659
4660 #[test]
4661 fn a_cut_and_an_in_share_the_start_slot() {
4662 let mut p = Project::from_media(asset(0, 10.0, 1));
4663 let right = p.split_at(4.0, None)[0];
4664 let left = p.tracks[0].clips[0].id;
4665 p.add_edge_transition(right, TransitionKind::CrossFade, 1.0, false).unwrap();
4666 p.add_transition(right, TransitionKind::Wipe, 1.0).unwrap();
4668 assert_eq!(p.tracks[0].transitions.len(), 1);
4669 assert_eq!(p.tracks[0].transitions[0].edge, TransitionEdge::Cut);
4670 p.add_edge_transition(left, TransitionKind::CrossFade, 1.0, true).unwrap();
4672 assert_eq!(p.tracks[0].transitions.len(), 1);
4673 assert_eq!(p.tracks[0].transitions[0].edge, TransitionEdge::Out);
4674 }
4675
4676 #[test]
4677 fn fade_mult_ramps_and_clamps() {
4678 let mut c = Clip::new(1, ClipKind::Video, "v", 0.0, 10.0);
4679 c.fade_in = 2.0;
4680 c.fade_out = 4.0;
4681 assert!((c.fade_mult(-1.0) - 0.0).abs() < 1e-9, "virtual extension holds the edge value");
4682 assert!((c.fade_mult(1.0) - 0.5).abs() < 1e-9);
4683 assert!((c.fade_mult(5.0) - 1.0).abs() < 1e-9);
4684 assert!((c.fade_mult(8.0) - 0.5).abs() < 1e-9);
4685 assert!((c.fade_mult(11.0) - 0.0).abs() < 1e-9);
4686 }
4687
4688 #[test]
4689 fn effects_params_and_keys() {
4690 let mut c = Clip::new(1, ClipKind::Video, "c", 0.0, 4.0);
4691 c.effects.push(Effect::new(EffectKind::Blur));
4692 assert_eq!(c.effects[0].at(0, 1.0), 8.0);
4693 c.effects[0].params[0].toggle_key(1.0);
4694 c.effects[0].params[0].set_at(3.0, 20.0);
4695 assert_eq!(c.key_times(), vec![1.0, 3.0]);
4696 c.move_keys(3.0, 2.0);
4697 assert_eq!(c.key_times(), vec![1.0, 2.0]);
4698 assert!(c.has_effects());
4699 let json = serde_json::to_string(&c).unwrap();
4700 let d: Clip = serde_json::from_str(&json).unwrap();
4701 assert_eq!(d.effects[0].kind, EffectKind::Blur);
4702 }
4703
4704 #[test]
4708 fn no_effect_kind_applies_to_audio_yet() {
4709 assert!(EffectKind::ALL.iter().all(|k| !k.applies_to_audio()));
4710 }
4711
4712 #[test]
4713 fn subtitles_and_folders() {
4714 let mut p = Project::new();
4715 p.add_cue(2.0, 3.0, "b");
4716 p.add_cue(0.0, 1.0, "a");
4717 assert_eq!(p.subtitles[0].text, "a");
4718 assert_eq!(p.cue_at(2.5).unwrap().text, "b");
4719 assert!(p.cue_at(1.5).is_none());
4720 assert!(p.add_folder("Footage/Day 1"));
4721 assert!(!p.add_folder(" "));
4722 let aid = p.add_asset(asset(0, 1.0, 0));
4723 p.asset_mut(aid).unwrap().folder = "Footage/Day 1".into();
4724 assert_eq!(p.folder_names(), vec!["Footage/Day 1".to_string()]);
4725 p.remove_folder("Footage");
4726 assert!(p.folders.is_empty());
4727 assert_eq!(p.asset(aid).unwrap().folder, "");
4728 }
4729
4730 #[test]
4731 fn move_many() {
4732 let mut p = Project::new();
4733 let ids: Vec<Id> = (0..4).map(|i| p.add_text_clip(i as f64 * 2.0, 2.0)).collect(); let blocker = p.add_text_clip(10.0, 1.0);
4735 assert!(p.move_clips(&ids, 1.0, 0, None)); assert!(!p.move_clips(&ids, 2.0, 0, None)); assert!(!p.move_clips(&[ids[1]], 1.0, 0, None)); assert!((p.clip(ids[0]).unwrap().start - 1.0).abs() < 1e-9);
4739 assert!((p.clip(blocker).unwrap().start - 10.0).abs() < 1e-9);
4740 }
4741
4742 #[test]
4743 fn from_json_sanitizes() {
4744 let mut p = Project::from_media(asset(0, 10.0, 1));
4745 p.add_text_clip(12.0, 1.0);
4746 let s = p.to_json().replace("\"fps\": 30.0", "\"fps\": 0.0").replace("\"width\": 1280", "\"width\": 0");
4747 let q = Project::from_json(&s).unwrap();
4748 assert_eq!(q.fps, 30.0);
4749 assert!(q.width >= 16);
4750 assert!(q.snap_frame(1.0).is_finite());
4751 let mut bad = Project::new();
4752 bad.tracks[0].clips.push(Clip::new(99, ClipKind::Text, "t", 0.0, -1.0));
4753 assert!(Project::from_json(&bad.to_json()).unwrap().is_empty());
4754 }
4755
4756 #[test]
4757 fn sequences_open_close_nest() {
4758 let mut p = Project::from_media(asset(0, 10.0, 1));
4759 let v = p.tracks[0].clips[0].id;
4760 let seq = p.nest_selection(&[v], "Intro").unwrap();
4762 assert_eq!(p.sequences.len(), 1);
4763 assert_eq!(p.sequence(seq).unwrap().tracks.len(), 2);
4764 assert_eq!(p.tracks[0].clips.len(), 1);
4765 assert_eq!(p.tracks[0].clips[0].kind, ClipKind::Sequence);
4766 assert!((p.sequence_duration(seq) - 10.0).abs() < 1e-9);
4767 assert!((p.duration() - 10.0).abs() < 1e-9);
4768 assert!(p.used_assets().len() == 1); assert!(p.open_sequence(seq));
4771 assert_eq!(p.editing, Some(seq));
4772 assert_eq!(p.tracks[0].clips[0].kind, ClipKind::Video);
4773 assert!(p.main_stash.is_some());
4774 assert!(p.insert_sequence_clip(seq, 0.0, None).is_none());
4776 p.close_sequence();
4777 assert_eq!(p.editing, None);
4778 assert_eq!(p.tracks[0].clips[0].kind, ClipKind::Sequence);
4779 let q = Project::from_json(&p.to_json()).unwrap();
4781 assert_eq!(q.sequences.len(), 1);
4782 assert!((q.sequence_duration(seq) - 10.0).abs() < 1e-9);
4783 }
4784
4785 #[test]
4786 fn planner_and_unused() {
4787 let mut p = Project::from_media(asset(0, 10.0, 1));
4788 let extra = p.add_asset(asset(1, 3.0, 0));
4789 let third = p.add_asset(asset(2, 3.0, 0));
4790 let a = p.plan_add(None, "Intro");
4791 let b = p.plan_add(Some(a), "Hook shot");
4792 p.plan_item_mut(b).unwrap().assets.push(extra);
4793 p.plan_item_mut(b).unwrap().done = true;
4794 assert_eq!(p.plan[0].children[0].title, "Hook shot");
4795 assert!(p.plan_assets().contains(&extra));
4796 assert_eq!(p.remove_unused_assets(), 1); assert!(p.asset(extra).is_some() && p.asset(third).is_none());
4798 p.plan_remove(a);
4799 assert!(p.plan.is_empty());
4800 }
4801
4802 #[test]
4803 fn auto_cut_removes_quiet_parts() {
4804 let mut p = Project::from_media(asset(0, 10.0, 1));
4805 let a = p.tracks[1].clips[0].id;
4806 let n = p.auto_cut(&[a], &[2.0, 4.0, 6.0, 8.0], &[(0.0, 2.0), (4.0, 6.0), (8.0, 10.0)], true);
4808 assert_eq!(n, 6); assert_eq!(p.tracks[1].clips.len(), 2);
4810 assert_eq!(p.tracks[0].clips.len(), 2);
4811 assert!((p.duration() - 4.0).abs() < 1e-9);
4812 assert!((p.tracks[0].clips[1].src_in - 6.0).abs() < 1e-9);
4813 }
4814
4815 #[test]
4816 fn flow_and_place() {
4817 let mut p = Project::new();
4818 let a = p.add_text_clip(0.0, 2.0);
4819 let b = p.add_text_clip(2.0, 2.0);
4820 p.clip_mut(a).unwrap().x.toggle_key(0.0);
4821 p.clip_mut(a).unwrap().x.set_at(2.0, 100.0);
4822 p.clip_mut(b).unwrap().x.toggle_key(0.0);
4823 p.clip_mut(b).unwrap().x.set_at(2.0, -100.0); assert!(p.flow_clips(a, b));
4825 assert!((p.clip(a).unwrap().x.at(2.0) - 50.0).abs() < 1e-9);
4826 assert!((p.clip(b).unwrap().x.at(0.0) - 50.0).abs() < 1e-9);
4827 assert_eq!(p.clip(b).unwrap().x.keys[0].ease, Ease::EaseOut);
4828 let clips: Vec<Clip> = [a, b].iter().map(|&id| p.clip(id).unwrap().clone()).collect();
4830 let ids = p.place_clips(clips, Vec::new(), 10.0);
4831 assert_eq!(ids.len(), 2);
4832 assert!((p.clip(ids[0]).unwrap().start - 10.0).abs() < 1e-9);
4833 assert_eq!(p.tracks[0].clips.len(), 4);
4834 }
4835
4836 #[test]
4837 fn bezier_ease() {
4838 let e = Ease::Bezier { x1: 0.42, y1: 0.0, x2: 0.58, y2: 1.0 };
4839 assert!((e.apply(0.0)).abs() < 1e-6 && (e.apply(1.0) - 1.0).abs() < 1e-6);
4840 assert!((e.apply(0.5) - 0.5).abs() < 1e-3);
4841 assert!(e.apply(0.25) < 0.25); let s = serde_json::to_string(&e).unwrap();
4843 assert_eq!(serde_json::from_str::<Ease>(&s).unwrap(), e);
4844 }
4845
4846 #[test]
4847 fn save_is_atomic() {
4848 let dir = std::env::temp_dir().join("simple-editor-model-test");
4849 std::fs::create_dir_all(&dir).unwrap();
4850 let path = dir.join("p.sedit");
4851 let p = Project::from_media(asset(0, 10.0, 1));
4852 p.save(&path).unwrap();
4853 p.save(&path).unwrap(); assert!(!dir.join("p.sedit.tmp").exists());
4855 assert_eq!(Project::load(&path).unwrap().to_json(), p.to_json());
4856 let _ = std::fs::remove_dir_all(&dir);
4857 }
4858
4859 #[test]
4860 fn unlink_graph_round_trips_the_effect_stack() {
4861 let mut p = Project::from_media(asset(0, 10.0, 1));
4862 let id = p.tracks[0].clips[0].id;
4863 let mut blur = Effect::new(EffectKind::Blur);
4864 blur.params[0].value = 7.0;
4865 let mut off = Effect::new(EffectKind::Sharpen);
4866 off.enabled = false;
4867 p.clip_mut(id).unwrap().effects = vec![blur, off];
4868 p.ensure_graph(id);
4869 assert_eq!(p.unlink_graph(id), Ok(2));
4870 let c = p.clip(id).unwrap();
4871 assert!(c.graph.is_none(), "the graph is gone");
4872 assert_eq!(c.effects.iter().map(|e| e.kind).collect::<Vec<_>>(), vec![EffectKind::Blur, EffectKind::Sharpen]);
4874 assert_eq!(c.effects[0].at(0, 0.0), 7.0);
4875 assert!(c.effects[0].enabled && !c.effects[1].enabled);
4876
4877 p.ensure_graph(id);
4880 let g = p.clip(id).unwrap().graph.clone().unwrap();
4881 let (input, out) = (g.nodes[0].id, g.output().unwrap());
4882 let blend = p.add_node(id, NodeKind::Blend { mode: BlendMode::Normal, opacity: Animated::new(1.0) }, 0.0, 0.0);
4883 let color = p.add_node(id, NodeKind::Color([255, 0, 0, 255]), 0.0, 0.0).unwrap();
4884 let blend = blend.unwrap();
4885 let g = p.clip_mut(id).unwrap().graph.as_mut().unwrap();
4886 assert!(g.connect(input, blend, 0) && g.connect(color, blend, 1) && g.connect(blend, out, 0));
4887 let kept: Vec<EffectKind> = p.clip(id).unwrap().effects.iter().map(|e| e.kind).collect();
4888 assert_eq!(p.unlink_graph(id), Ok(0), "nothing of a branch converts");
4889 assert!(p.clip(id).unwrap().graph.is_none(), "but the graph is gone either way");
4890 assert_eq!(
4891 p.clip(id).unwrap().effects.iter().map(|e| e.kind).collect::<Vec<_>>(),
4892 kept,
4893 "and the clip keeps the effect stack it already had"
4894 );
4895 assert!(p.unlink_graph(id).is_err(), "no graph, nothing to unlink");
4897 }
4898
4899 #[test]
4902 fn every_shape_kind_round_trips_through_json() {
4903 let mut p = Project::new();
4904 for kind in [
4905 ShapeKind::Rect,
4906 ShapeKind::Ellipse,
4907 ShapeKind::Triangle,
4908 ShapeKind::Polygon,
4909 ShapeKind::Star,
4910 ShapeKind::Line,
4911 ShapeKind::Arrow,
4912 ShapeKind::Draw,
4913 ] {
4914 let id = p.add_shape_clip(kind, 0.0, 4.0);
4915 if let Some(sh) = p.clip_mut(id).and_then(|c| c.shape.as_mut()) {
4917 sh.w.value = 60.0;
4918 sh.h.value = -40.0;
4919 }
4920 }
4921 let json = p.to_json();
4922 let back = Project::from_json(&json).expect("a project full of shapes must parse back");
4923 let kinds: Vec<ShapeKind> = back.all_clips().filter_map(|(_, c)| c.shape.as_ref().map(|s| s.kind)).collect();
4924 assert_eq!(kinds.len(), 8, "every shape came back: {kinds:?}");
4925 let h: Vec<f64> = back.all_clips().filter_map(|(_, c)| c.shape.as_ref().map(|s| s.h.value)).collect();
4926 assert!(h.iter().all(|v| *v == -40.0), "the negative extent survived: {h:?}");
4927 }
4928
4929 #[test]
4932 fn effect_window_defaults_to_the_whole_clip() {
4933 let mut e = Effect::new(EffectKind::Blur);
4934 assert!(e.on_at(0.0) && e.on_at(1000.0));
4935 e.start = 1.0;
4936 e.len = 2.0;
4937 assert!(!e.on_at(0.5) && e.on_at(1.0) && e.on_at(3.0) && !e.on_at(3.5));
4938 e.enabled = false;
4939 assert!(!e.on_at(2.0), "disabled beats the window");
4940
4941 let old: Effect = serde_json::from_str(r#"{"kind":"Blur","params":[]}"#).unwrap();
4942 assert!(old.on_at(0.0) && old.on_at(1e6), "an old project keeps whole-clip effects");
4943 }
4944
4945 #[test]
4947 fn a_bare_graph_does_not_take_over_the_effect_stack() {
4948 let mut p = Project::from_media(asset(0, 10.0, 1));
4949 let id = p.tracks[0].clips[0].id;
4950 p.ensure_graph(id);
4951 assert_eq!(p.clip(id).unwrap().graph.as_ref().unwrap().nodes.len(), 2);
4952 assert!(!p.clip(id).unwrap().uses_graph(), "Input→Output says nothing the stack does not");
4953 assert!(p.add_node(id, NodeKind::Effect(Effect::new(EffectKind::Blur)), 0.0, 0.0).is_some());
4954 assert!(p.clip(id).unwrap().uses_graph(), "a real node makes the graph the renderer's truth");
4955 }
4956
4957 #[test]
4958 fn text_node_format_expands() {
4959 assert_eq!(expand_text("{n} / {frame}", 2.0, 1.0, 25.0), "25 / 50");
4961 assert_eq!(expand_text("{time}", 61.5, 0.0, 25.0), "01:01.50");
4962 assert_eq!(expand_text("{time}", 3661.0, 0.0, 25.0), "1:01:01.00");
4963 assert_eq!(expand_text("{frame} {x}", 1.0, 0.0, 0.0), "30 {x}");
4965 assert_eq!(expand_text("{n}", 0.0, -5.0, 30.0), "0");
4966 }
4967
4968 fn push(g: &mut NodeGraph, id: Id, kind: NodeKind) -> Id {
4970 g.nodes.push(Node { id, kind, x: 0.0, y: 0.0, enabled: true });
4971 id
4972 }
4973
4974 #[test]
4975 fn the_logic_nodes_evaluate_as_a_chain() {
4976 let mut next = 100;
4977 let mut nid = || {
4978 next += 1;
4979 next
4980 };
4981 let mut g = NodeGraph::new(&mut nid);
4982 let out = g.output().unwrap();
4983 let a = push(&mut g, nid(), NodeKind::Number(Animated::new(3.0)));
4984 let b = push(&mut g, nid(), NodeKind::Number(Animated::new(4.0)));
4985 let sum = push(&mut g, nid(), NodeKind::Math(MathOp::Add));
4986 let gt = push(&mut g, nid(), NodeKind::Compare(CmpOp::Gt));
4987 let not = push(&mut g, nid(), NodeKind::Logic(LogicOp::Not));
4988 let sel = push(&mut g, nid(), NodeKind::Select);
4989 assert!(g.connect(a, sum, 0) && g.connect(b, sum, 1));
4990 assert!(g.connect(sum, gt, 0) && g.connect(b, gt, 1)); assert!(g.connect(gt, not, 0));
4992 assert!(g.connect(not, sel, 0) && g.connect(a, sel, 1) && g.connect(b, sel, 2));
4993 assert!(g.connect(sel, out, 0));
4994 let v = g.eval_values(0.0, 30.0);
4995 assert_eq!(v[&sum], 7.0);
4996 assert_eq!(v[>], 1.0);
4997 assert_eq!(v[¬], 0.0);
4998 assert_eq!(v[&sel], 4.0, "the condition was negated, so Select takes b");
4999 g.node_mut(gt).unwrap().kind = NodeKind::Compare(CmpOp::Lt);
5001 let v = g.eval_values(0.0, 30.0);
5002 assert_eq!(v[&sel], 3.0);
5003 let loose = push(&mut g, nid(), NodeKind::Number(Animated::new(9.0)));
5005 assert!(!g.eval_values(0.0, 30.0).contains_key(&loose));
5006 g.node_mut(sum).unwrap().kind = NodeKind::Math(MathOp::Div);
5008 g.disconnect(sum, 1);
5009 assert_eq!(g.eval_values(0.0, 30.0)[&sum], 0.0);
5010 }
5011
5012 #[test]
5013 fn random_is_seeded_so_a_render_repeats() {
5014 let mut next = 200;
5015 let mut nid = || {
5016 next += 1;
5017 next
5018 };
5019 let mut g = NodeGraph::new(&mut nid);
5020 let out = g.output().unwrap();
5021 let r = push(&mut g, nid(), NodeKind::Random { seed: 7, min: -1.0, max: 1.0 });
5022 assert!(g.connect(r, out, 0));
5023 let first = g.eval_values(0.5, 30.0)[&r];
5024 assert_eq!(first, g.eval_values(0.5, 30.0)[&r], "same seed, same frame, same number");
5025 assert!((-1.0..=1.0).contains(&first), "{first} is outside min..max");
5026 assert_ne!(first, g.eval_values(1.5, 30.0)[&r], "a different frame draws again");
5027 g.node_mut(r).unwrap().kind = NodeKind::Random { seed: 8, min: -1.0, max: 1.0 };
5028 assert_ne!(first, g.eval_values(0.5, 30.0)[&r], "a different seed is a different sequence");
5029 }
5030
5031 #[test]
5032 fn an_old_text_node_loads_as_a_string_node() {
5033 let k = NodeKind::String(TextStyle { text: "hello".into(), ..Default::default() });
5034 let json = serde_json::to_string(&k).unwrap();
5035 assert!(json.starts_with("{\"String\""), "{json}");
5036 let old = json.replacen("\"String\"", "\"Text\"", 1);
5037 assert_eq!(serde_json::from_str::<NodeKind>(&old).unwrap(), k, "projects from before the rename still load");
5038 }
5039
5040 #[test]
5041 fn an_effect_node_takes_one_value_port_per_parameter() {
5042 let fx = NodeKind::Effect(Effect::new(EffectKind::Tint));
5043 assert_eq!(fx.inputs(), 1 + EffectKind::Tint.params().len());
5044 assert_eq!(fx.port_label(0), "in");
5045 assert_eq!(fx.port_label(1), EffectKind::Tint.params()[0].name);
5046 assert_eq!(NodeKind::Logic(LogicOp::Not).inputs(), 1);
5048 assert_eq!(NodeKind::Logic(LogicOp::And).inputs(), 2);
5049 assert_eq!(NodeKind::Select.port_label(0), "cond");
5050 }
5051}
5052
5053#[derive(Clone, Copy, Debug, PartialEq, Eq)]
5055pub struct AttrSet {
5056 pub transform: bool,
5057 pub opacity: bool,
5058 pub blend: bool,
5059 pub effects: bool,
5060 pub graph: bool,
5061 pub mask: bool,
5062 pub speed: bool,
5063 pub audio: bool,
5064 pub text: bool,
5065 pub shape: bool,
5066 pub label: bool,
5067 pub markers: bool,
5068}
5069
5070impl Default for AttrSet {
5071 fn default() -> Self {
5072 Self {
5073 transform: true,
5074 opacity: true,
5075 blend: true,
5076 effects: true,
5077 graph: true,
5078 mask: true,
5079 speed: false,
5080 audio: true,
5081 text: false,
5082 shape: false,
5083 label: true,
5084 markers: false,
5085 }
5086 }
5087}
5088
5089impl AttrSet {
5090 pub const NONE: AttrSet = AttrSet {
5091 transform: false,
5092 opacity: false,
5093 blend: false,
5094 effects: false,
5095 graph: false,
5096 mask: false,
5097 speed: false,
5098 audio: false,
5099 text: false,
5100 shape: false,
5101 label: false,
5102 markers: false,
5103 };
5104 pub fn fields(&mut self) -> Vec<(&'static str, &mut bool)> {
5106 vec![
5107 ("Transform (position, scale, rotation)", &mut self.transform),
5108 ("Opacity", &mut self.opacity),
5109 ("Blend mode", &mut self.blend),
5110 ("Effects", &mut self.effects),
5111 ("Node graph", &mut self.graph),
5112 ("Mask", &mut self.mask),
5113 ("Speed / reverse / freeze", &mut self.speed),
5114 ("Audio (volume, pan, fades, bus)", &mut self.audio),
5115 ("Text style", &mut self.text),
5116 ("Shape style", &mut self.shape),
5117 ("Colour label", &mut self.label),
5118 ("Markers", &mut self.markers),
5119 ]
5120 }
5121 pub fn any(&self) -> bool {
5122 let mut me = *self;
5123 me.fields().iter().any(|(_, v)| **v)
5124 }
5125}