Skip to main content

alpha2_compose/
spec.rs

1//! The compose spec: a video described by intent, which [`crate::compile`] turns into a
2//! project document.
3//!
4//! Times are seconds, rounded to the nearest tick (1/120 000 s). Lengths are canvas pixels.
5//! Colors are CSS hex strings or the project's `{red, green, blue, alpha}` object. Nodes are
6//! tagged by `type`, and a node with an `id` can be named by another (a music bed ducks under
7//! it).
8
9use std::collections::BTreeMap;
10
11use alpha2_model::{
12    Animated, Composition, Easing, EffectValue, LayoutNode, MediaSource, Rgba, TextShadow,
13    TextStroke, Track,
14};
15use serde::{Deserialize, Serialize};
16
17/// The newest compose spec version this build reads.
18pub const COMPOSE_VERSION: u32 = 1;
19
20/// A whole video: a body, what is drawn over it, and what plays under it.
21#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
22#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
23#[serde(deny_unknown_fields)]
24pub struct VideoSpec {
25    /// Spec version. This build reads only [`COMPOSE_VERSION`].
26    #[serde(default = "compose_version")]
27    #[cfg_attr(feature = "schema", schemars(extend("const" = COMPOSE_VERSION)))]
28    pub compose_version: u32,
29    /// The project's name. Default `video`.
30    #[serde(default, skip_serializing_if = "Option::is_none")]
31    pub name: Option<String>,
32    /// Size, rate and background. Default `youtube-1080p`.
33    #[serde(default)]
34    pub canvas: Canvas,
35    /// Fonts, colors, sizes and default timings. Default `clean-dark`.
36    #[serde(default)]
37    pub theme: ThemeChoice,
38    /// The main picture, from time zero. Its length is the video's unless `duration` says
39    /// otherwise.
40    pub body: Node,
41    /// Nodes drawn over the body in order, placed by their own `at` (default 0) and lasting to
42    /// the end unless they say otherwise.
43    #[serde(default, skip_serializing_if = "Vec::is_empty")]
44    pub overlays: Vec<Node>,
45    /// Sound beside the body's: `audio` and `music_bed` nodes, placed as overlays are.
46    #[serde(default, skip_serializing_if = "Vec::is_empty")]
47    pub audio: Vec<Node>,
48    /// The video's length in seconds. Default: the body's.
49    #[serde(default, skip_serializing_if = "Option::is_none")]
50    pub duration: Option<f64>,
51    /// The loudness an export normalizes the mix to. Returned as an export option, not stored
52    /// in the project.
53    #[serde(default, skip_serializing_if = "Option::is_none")]
54    pub loudness: Option<Loudness>,
55}
56
57const fn compose_version() -> u32 {
58    COMPOSE_VERSION
59}
60
61/// The canvas: a preset name, or its parts.
62#[derive(Clone, Debug, PartialEq, Serialize)]
63#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
64#[serde(untagged)]
65pub enum Canvas {
66    /// A preset at 30 fps: `youtube-1080p` (1920x1080), `youtube-4k` (3840x2160), `youtube-720p`
67    /// (1280x720), `shorts` (1080x1920), `square` (1080x1080) or `portrait` (1080x1350).
68    Preset(String),
69    /// A preset with some parts replaced, or every part given.
70    Custom(CustomCanvas),
71}
72
73impl Default for Canvas {
74    fn default() -> Self {
75        Self::Preset("youtube-1080p".to_owned())
76    }
77}
78
79/// A canvas by its parts.
80#[derive(Clone, Debug, Default, Deserialize, PartialEq, Serialize)]
81#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
82#[serde(deny_unknown_fields)]
83pub struct CustomCanvas {
84    /// The preset the other parts replace parts of. Default `youtube-1080p`.
85    #[serde(default, skip_serializing_if = "Option::is_none")]
86    pub preset: Option<String>,
87    /// Width in pixels.
88    #[serde(default, skip_serializing_if = "Option::is_none")]
89    pub width: Option<u32>,
90    /// Height in pixels.
91    #[serde(default, skip_serializing_if = "Option::is_none")]
92    pub height: Option<u32>,
93    /// Frames per second: an integer, 23.976, 29.97 or 59.94, or the project's exact
94    /// `{num, den}`.
95    #[serde(default, skip_serializing_if = "Option::is_none")]
96    pub frame_rate: Option<FrameRateChoice>,
97    /// The opaque color under everything. Default: the theme's.
98    #[serde(default, skip_serializing_if = "Option::is_none")]
99    pub background: Option<Rgba>,
100}
101
102/// A frame rate, as a number or exactly.
103#[derive(Clone, Copy, Debug, PartialEq, Serialize)]
104#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
105#[serde(untagged)]
106pub enum FrameRateChoice {
107    /// Frames per second: an integer, or 23.976, 29.97 or 59.94 for the NTSC rates.
108    Number(f64),
109    /// The project's exact rate.
110    Exact(alpha2_time::FrameRate),
111}
112
113/// A theme by name, or one built on a named theme.
114#[derive(Clone, Debug, PartialEq, Serialize)]
115#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
116#[serde(untagged)]
117pub enum ThemeChoice {
118    /// A built-in theme: `clean-dark` or `clean-light`.
119    Named(String),
120    /// A theme of its own.
121    Custom(Box<Theme>),
122}
123
124impl Default for ThemeChoice {
125    fn default() -> Self {
126        Self::Named("clean-dark".to_owned())
127    }
128}
129
130/// How text, panels and timings look. Parts left out come from the base theme. Sizes are
131/// fractions of the canvas's shorter side, so a theme looks the same on wide and tall canvases.
132#[derive(Clone, Debug, Default, Deserialize, PartialEq, Serialize)]
133#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
134#[serde(deny_unknown_fields)]
135pub struct Theme {
136    /// The built-in theme this one changes. Default `clean-dark`.
137    #[serde(default, skip_serializing_if = "Option::is_none")]
138    pub base: Option<String>,
139    /// Font family, as the font names it. Unset, text draws in the first font an export
140    /// registers.
141    #[serde(default, skip_serializing_if = "Option::is_none")]
142    pub font_family: Option<String>,
143    /// Text color.
144    #[serde(default, skip_serializing_if = "Option::is_none")]
145    pub text: Option<Rgba>,
146    /// The canvas background.
147    #[serde(default, skip_serializing_if = "Option::is_none")]
148    pub background: Option<Rgba>,
149    /// What title and end cards are drawn on.
150    #[serde(default, skip_serializing_if = "Option::is_none")]
151    pub surface: Option<Rgba>,
152    /// The boxes behind labels, lower thirds and boxed captions.
153    #[serde(default, skip_serializing_if = "Option::is_none")]
154    pub panel: Option<Rgba>,
155    /// A highlight: a lower third's bar.
156    #[serde(default, skip_serializing_if = "Option::is_none")]
157    pub accent: Option<Rgba>,
158    /// Title size.
159    #[serde(default, skip_serializing_if = "Option::is_none")]
160    pub title_size: Option<f32>,
161    /// Subtitle size.
162    #[serde(default, skip_serializing_if = "Option::is_none")]
163    pub subtitle_size: Option<f32>,
164    /// Body text size.
165    #[serde(default, skip_serializing_if = "Option::is_none")]
166    pub body_size: Option<f32>,
167    /// Caption size.
168    #[serde(default, skip_serializing_if = "Option::is_none")]
169    pub caption_size: Option<f32>,
170    /// Label size: a grid cell's.
171    #[serde(default, skip_serializing_if = "Option::is_none")]
172    pub label_size: Option<f32>,
173    /// The space kept clear at the canvas's edges.
174    #[serde(default, skip_serializing_if = "Option::is_none")]
175    pub margin: Option<f32>,
176    /// The transition a slideshow, or a sequence asking for `theme`, puts between children.
177    #[serde(default, skip_serializing_if = "Option::is_none")]
178    pub transition: Option<Transition>,
179    /// How long a still shows when nothing says, in seconds.
180    #[serde(default, skip_serializing_if = "Option::is_none")]
181    pub still_seconds: Option<f64>,
182    /// How long a card or text shows when nothing says, in seconds.
183    #[serde(default, skip_serializing_if = "Option::is_none")]
184    pub text_seconds: Option<f64>,
185    /// How long text and cards fade in and out, in seconds.
186    #[serde(default, skip_serializing_if = "Option::is_none")]
187    pub fade_seconds: Option<f64>,
188}
189
190/// The loudness an export normalizes to: a target, or a target and a ceiling.
191#[derive(Clone, Copy, Debug, PartialEq, Serialize)]
192#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
193#[serde(untagged)]
194pub enum Loudness {
195    /// Integrated loudness in LUFS, under a -1 dBTP ceiling.
196    Lufs(f64),
197    /// Integrated loudness and a true-peak ceiling.
198    Target(crate::LoudnessTarget),
199}
200
201/// The transition between two neighbours of a sequence.
202#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
203#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
204#[serde(deny_unknown_fields)]
205pub struct Transition {
206    /// `crossfade`, a built-in transition's short name (`wipe-left`, `circle-open`, ...), or a
207    /// full effect id (`alpha2.transition.fade`).
208    #[serde(rename = "type")]
209    pub kind: String,
210    /// How long the neighbours overlap, in seconds.
211    pub duration: f64,
212    /// The timing curve. Default linear.
213    #[serde(default, skip_serializing_if = "Option::is_none")]
214    pub easing: Option<Easing>,
215    /// The effect's parameters by name.
216    #[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
217    pub params: BTreeMap<String, Animated<EffectValue>>,
218    /// The effect definition's version. Default 1.
219    #[serde(default, skip_serializing_if = "Option::is_none")]
220    pub version: Option<u32>,
221}
222
223/// A sequence's transitions: none, the theme's, or one given.
224#[derive(Clone, Debug, PartialEq, Serialize)]
225#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
226#[serde(untagged)]
227pub enum TransitionChoice {
228    /// `none` for cuts, `theme` for the theme's transition, or a transition name at the theme's
229    /// duration.
230    Named(String),
231    /// A transition.
232    Given(Transition),
233}
234
235/// Space kept clear inside an area.
236#[derive(Clone, Copy, Debug, PartialEq, Serialize)]
237#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
238#[serde(untagged)]
239pub enum Inset {
240    /// The same on every side.
241    Uniform(f32),
242    /// `[top, right, bottom, left]`.
243    Sides([f32; 4]),
244}
245
246/// How a picture fills its area.
247#[derive(Clone, Copy, Debug, Default, Deserialize, Eq, PartialEq, Serialize)]
248#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
249#[serde(rename_all = "snake_case")]
250pub enum Fit {
251    /// All of it, as large as fits; the rest of the area stays empty.
252    #[default]
253    Contain,
254    /// All of the area, cropped to it.
255    Cover,
256    /// Stretched to the area.
257    Fill,
258    /// At its own size in pixels, cropped to the area.
259    None,
260}
261
262/// A point of an area: its center, an edge's middle or a corner.
263#[derive(Clone, Copy, Debug, Default, Deserialize, Eq, PartialEq, Serialize)]
264#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
265#[serde(rename_all = "kebab-case")]
266pub enum Align {
267    /// The center.
268    #[default]
269    Center,
270    /// The top edge's middle.
271    Top,
272    /// The bottom edge's middle.
273    Bottom,
274    /// The left edge's middle.
275    Left,
276    /// The right edge's middle.
277    Right,
278    /// The top-left corner.
279    TopLeft,
280    /// The top-right corner.
281    TopRight,
282    /// The bottom-left corner.
283    BottomLeft,
284    /// The bottom-right corner.
285    BottomRight,
286}
287
288/// What a media file holds, when its extension does not say.
289#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
290#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
291#[serde(rename_all = "snake_case")]
292pub enum FileKind {
293    /// Moving pictures, perhaps with sound.
294    Video,
295    /// A still picture.
296    Image,
297    /// Sound alone.
298    Audio,
299}
300
301/// One part of a video.
302#[derive(Clone, Debug, PartialEq, Serialize)]
303#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
304#[serde(tag = "type", rename_all = "snake_case", deny_unknown_fields)]
305pub enum Node {
306    /// Children one after another, with transitions between them.
307    Sequence(Sequence),
308    /// Children over each other, each over the ones before.
309    Stack(Stack),
310    /// Children in the cells of a grid, row by row.
311    Grid(Grid),
312    /// A video or still file.
313    Media(Media),
314    /// Text.
315    Text(Text),
316    /// An area painted one color.
317    Color(ColorFill),
318    /// Timed text from cues, a SubRip or WebVTT file, or word timestamps.
319    Captions(Captions),
320    /// A layout generator: boxes, text and shapes laid out by CSS flexbox and grid.
321    Layout(Layout),
322    /// A sound file.
323    Audio(Audio),
324    /// Part of a project (sources, tracks, compositions) at its own times. Overlays only.
325    Raw(Raw),
326    /// Stills or clips in turn, with transitions and slow zooms.
327    Slideshow(Slideshow),
328    /// A video under its captions.
329    CaptionedVideo(CaptionedVideo),
330    /// A title (and subtitle) over what is under it, laid out on the whole canvas.
331    Title(Title),
332    /// A title (and subtitle) on the theme's surface, over the whole canvas.
333    TitleCard(Title),
334    /// A name and role in the canvas's lower left.
335    LowerThird(LowerThird),
336    /// A closing card over the whole canvas: a logo, a title and a subtitle on the theme's
337    /// surface.
338    EndCard(EndCard),
339    /// Clips side by side in a grid, each labeled.
340    ComparisonGrid(ComparisonGrid),
341    /// A video fitted to a canvas of another shape, over a blurred copy of itself.
342    Reframe(Reframe),
343    /// A picture in a corner.
344    Logo(Logo),
345    /// Music under the video, faded in and out and ducked under speech.
346    MusicBed(MusicBed),
347}
348
349/// Children one after another.
350#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
351#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
352#[serde(deny_unknown_fields)]
353pub struct Sequence {
354    /// The node's name, for other nodes to refer to.
355    #[serde(default, skip_serializing_if = "Option::is_none")]
356    pub id: Option<String>,
357    /// The children, in play order.
358    pub children: Vec<Node>,
359    /// What joins neighbours: a transition overlaps them by its duration.
360    /// Default `none`. Only single pictures (media, text, color, layout)
361    /// can transition into each other.
362    #[serde(default, skip_serializing_if = "Option::is_none")]
363    pub transition: Option<TransitionChoice>,
364    /// Seconds from the parent's start (an overlay's: the video's). Ignored in a sequence.
365    #[serde(default, skip_serializing_if = "Option::is_none")]
366    pub at: Option<f64>,
367    /// Length in seconds. Default: the node's own length, else what its parent gives it.
368    #[serde(default, skip_serializing_if = "Option::is_none")]
369    pub duration: Option<f64>,
370}
371
372/// Children over each other.
373#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
374#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
375#[serde(deny_unknown_fields)]
376pub struct Stack {
377    /// The node's name, for other nodes to refer to.
378    #[serde(default, skip_serializing_if = "Option::is_none")]
379    pub id: Option<String>,
380    /// The children, each drawn over the ones before and starting at its own `at`.
381    pub children: Vec<Node>,
382    /// Seconds from the parent's start (an overlay's: the video's). Ignored in a sequence.
383    #[serde(default, skip_serializing_if = "Option::is_none")]
384    pub at: Option<f64>,
385    /// Length in seconds. Default: the node's own length, else what its parent gives it.
386    #[serde(default, skip_serializing_if = "Option::is_none")]
387    pub duration: Option<f64>,
388    /// Pixels kept clear inside the parent's area: one number, or `[top, right, bottom, left]`.
389    #[serde(default, skip_serializing_if = "Option::is_none")]
390    pub inset: Option<Inset>,
391}
392
393/// Children in grid cells.
394#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
395#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
396#[serde(deny_unknown_fields)]
397pub struct Grid {
398    /// The node's name, for other nodes to refer to.
399    #[serde(default, skip_serializing_if = "Option::is_none")]
400    pub id: Option<String>,
401    /// The cells' contents, row by row.
402    pub children: Vec<Node>,
403    /// Columns. Default: as square a grid as the children make.
404    #[serde(default, skip_serializing_if = "Option::is_none")]
405    pub columns: Option<u32>,
406    /// Pixels between cells.
407    #[serde(default)]
408    pub gap: f32,
409    /// How long the grid lasts when its cells differ, and what a cell that ends early shows.
410    /// Default `shortest`.
411    #[serde(default)]
412    pub length: GridLength,
413    /// Seconds from the parent's start (an overlay's: the video's). Ignored in a sequence.
414    #[serde(default, skip_serializing_if = "Option::is_none")]
415    pub at: Option<f64>,
416    /// Length in seconds. Default: the node's own length, else what its parent gives it.
417    #[serde(default, skip_serializing_if = "Option::is_none")]
418    pub duration: Option<f64>,
419    /// Pixels kept clear inside the parent's area: one number, or `[top, right, bottom, left]`.
420    #[serde(default, skip_serializing_if = "Option::is_none")]
421    pub inset: Option<Inset>,
422}
423
424/// How long a grid of clips of different lengths lasts.
425#[derive(Clone, Copy, Debug, Default, Deserialize, Eq, PartialEq, Serialize)]
426#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
427#[serde(rename_all = "snake_case")]
428pub enum GridLength {
429    /// As long as its shortest cell.
430    #[default]
431    Shortest,
432    /// As long as its longest; shorter clips hold their last frame.
433    LongestFreeze,
434    /// As long as its longest; shorter clips play again from their start.
435    LongestLoop,
436}
437
438/// A video or still file.
439#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
440#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
441#[serde(deny_unknown_fields)]
442pub struct Media {
443    /// The node's name, for other nodes to refer to.
444    #[serde(default, skip_serializing_if = "Option::is_none")]
445    pub id: Option<String>,
446    /// The file. It becomes the source's locator and the export's asset.
447    pub path: String,
448    /// What the file holds. Default: by its extension.
449    #[serde(default, skip_serializing_if = "Option::is_none")]
450    pub kind: Option<FileKind>,
451    /// Where in the file a video starts, in seconds.
452    #[serde(default, skip_serializing_if = "Option::is_none")]
453    pub trim_start: Option<f64>,
454    /// Where in the file a video ends, in seconds. Default: its end.
455    #[serde(default, skip_serializing_if = "Option::is_none")]
456    pub trim_end: Option<f64>,
457    /// Playback speed of a video: 2 plays it twice as fast. Default 1.
458    #[serde(default, skip_serializing_if = "Option::is_none")]
459    pub speed: Option<f64>,
460    /// Keeps a video's sound at its pitch when `speed` is not 1 (time-stretched instead of
461    /// resampled). Default true.
462    #[serde(default, skip_serializing_if = "Option::is_none")]
463    pub preserve_pitch: Option<bool>,
464    /// Plays a video again from its start while the node lasts.
465    #[serde(default, skip_serializing_if = "std::ops::Not::not")]
466    pub r#loop: bool,
467    /// Linear gain of a video's sound. Default 1.
468    #[serde(default, skip_serializing_if = "Option::is_none")]
469    pub volume: Option<f32>,
470    /// Silences a video's sound.
471    #[serde(default, skip_serializing_if = "std::ops::Not::not")]
472    pub muted: bool,
473    /// Opacity, 0..=1. Default 1.
474    #[serde(default, skip_serializing_if = "Option::is_none")]
475    pub opacity: Option<f32>,
476    /// Seconds of fade in from transparent.
477    #[serde(default, skip_serializing_if = "Option::is_none")]
478    pub fade_in: Option<f64>,
479    /// Seconds of fade out to transparent.
480    #[serde(default, skip_serializing_if = "Option::is_none")]
481    pub fade_out: Option<f64>,
482    /// A slow zoom over the node's length, `[from, to]`, as multiples of the fitted size.
483    #[serde(default, skip_serializing_if = "Option::is_none")]
484    pub zoom: Option<[f32; 2]>,
485    /// A Gaussian blur's radius, as a fraction of the picture's shorter side.
486    #[serde(default, skip_serializing_if = "Option::is_none")]
487    pub blur: Option<f32>,
488    /// A label a comparison grid shows on the cell.
489    #[serde(default, skip_serializing_if = "Option::is_none")]
490    pub label: Option<String>,
491    /// Seconds from the parent's start (an overlay's: the video's). Ignored in a sequence.
492    #[serde(default, skip_serializing_if = "Option::is_none")]
493    pub at: Option<f64>,
494    /// Length in seconds. Default: the node's own length, else what its parent gives it.
495    #[serde(default, skip_serializing_if = "Option::is_none")]
496    pub duration: Option<f64>,
497    /// Pixels kept clear inside the parent's area: one number, or `[top, right, bottom, left]`.
498    #[serde(default, skip_serializing_if = "Option::is_none")]
499    pub inset: Option<Inset>,
500    /// How a picture fills its area. Default `contain`.
501    #[serde(default, skip_serializing_if = "Option::is_none")]
502    pub fit: Option<Fit>,
503    /// Where in its area the picture or text lies. Default `center`.
504    #[serde(default, skip_serializing_if = "Option::is_none")]
505    pub align: Option<Align>,
506}
507
508impl Media {
509    /// A node of the file at `path`, everything else by default.
510    #[must_use]
511    pub fn new(path: impl Into<String>) -> Self {
512        Self {
513            id: None,
514            path: path.into(),
515            kind: None,
516            trim_start: None,
517            trim_end: None,
518            speed: None,
519            preserve_pitch: None,
520            r#loop: false,
521            volume: None,
522            muted: false,
523            opacity: None,
524            fade_in: None,
525            fade_out: None,
526            zoom: None,
527            blur: None,
528            label: None,
529            at: None,
530            duration: None,
531            inset: None,
532            fit: None,
533            align: None,
534        }
535    }
536}
537
538/// A file given by its path alone, or a media node.
539#[derive(Clone, Debug, PartialEq, Serialize)]
540#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
541#[serde(untagged)]
542pub enum MediaItem {
543    /// The file, everything else by default.
544    Path(String),
545    /// A `media` node.
546    Node(Box<Node>),
547}
548
549/// Text in one of the theme's styles.
550#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
551#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
552#[serde(deny_unknown_fields)]
553pub struct Text {
554    /// The node's name, for other nodes to refer to.
555    #[serde(default, skip_serializing_if = "Option::is_none")]
556    pub id: Option<String>,
557    /// The text; `\n` starts a line.
558    pub text: String,
559    /// The theme style it starts from. Default `body`.
560    #[serde(default)]
561    pub style: TextRole,
562    /// Font size in pixels. Default: the style's.
563    #[serde(default, skip_serializing_if = "Option::is_none")]
564    pub font_size: Option<f32>,
565    /// Family. Default: the theme's.
566    #[serde(default, skip_serializing_if = "Option::is_none")]
567    pub font_family: Option<String>,
568    /// Color. Default: the theme's text color.
569    #[serde(default, skip_serializing_if = "Option::is_none")]
570    pub color: Option<Rgba>,
571    /// A box of this color behind each line.
572    #[serde(default, skip_serializing_if = "Option::is_none")]
573    pub background: Option<Rgba>,
574    /// Bands around the glyphs, innermost first, widths as fractions of the font size (as a
575    /// text style's `strokes`).
576    #[serde(default, skip_serializing_if = "Vec::is_empty")]
577    pub strokes: Vec<TextStroke>,
578    /// Shadows of the glyphs, the first on top, lengths as fractions of the font size (as a
579    /// text style's `shadows`).
580    #[serde(default, skip_serializing_if = "Vec::is_empty")]
581    pub shadows: Vec<TextShadow>,
582    /// The most lines it may take; with fonts to measure by, longer text shrinks until it fits.
583    /// Default: no limit.
584    #[serde(default, skip_serializing_if = "Option::is_none")]
585    pub max_lines: Option<u32>,
586    /// Opacity, 0..=1. Default 1.
587    #[serde(default, skip_serializing_if = "Option::is_none")]
588    pub opacity: Option<f32>,
589    /// Seconds of fade in and out. Default none.
590    #[serde(default, skip_serializing_if = "Option::is_none")]
591    pub fade: Option<f64>,
592    /// Seconds from the parent's start (an overlay's: the video's). Ignored in a sequence.
593    #[serde(default, skip_serializing_if = "Option::is_none")]
594    pub at: Option<f64>,
595    /// Length in seconds. Default: the node's own length, else what its parent gives it.
596    #[serde(default, skip_serializing_if = "Option::is_none")]
597    pub duration: Option<f64>,
598    /// Pixels kept clear inside the parent's area: one number, or `[top, right, bottom, left]`.
599    #[serde(default, skip_serializing_if = "Option::is_none")]
600    pub inset: Option<Inset>,
601    /// Where in its area the picture or text lies. Default `center`.
602    #[serde(default, skip_serializing_if = "Option::is_none")]
603    pub align: Option<Align>,
604}
605
606/// A theme text style.
607#[derive(Clone, Copy, Debug, Default, Deserialize, Eq, PartialEq, Serialize)]
608#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
609#[serde(rename_all = "snake_case")]
610pub enum TextRole {
611    /// The largest.
612    Title,
613    /// Under a title.
614    Subtitle,
615    /// Running text.
616    #[default]
617    Body,
618    /// A cell's label.
619    Label,
620}
621
622/// An area painted one color.
623#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
624#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
625#[serde(deny_unknown_fields)]
626pub struct ColorFill {
627    /// The node's name, for other nodes to refer to.
628    #[serde(default, skip_serializing_if = "Option::is_none")]
629    pub id: Option<String>,
630    /// The color. Default: the theme's surface.
631    #[serde(default, skip_serializing_if = "Option::is_none")]
632    pub color: Option<Rgba>,
633    /// Seconds from the parent's start (an overlay's: the video's). Ignored in a sequence.
634    #[serde(default, skip_serializing_if = "Option::is_none")]
635    pub at: Option<f64>,
636    /// Length in seconds. Default: the node's own length, else what its parent gives it.
637    #[serde(default, skip_serializing_if = "Option::is_none")]
638    pub duration: Option<f64>,
639    /// Pixels kept clear inside the parent's area: one number, or `[top, right, bottom, left]`.
640    #[serde(default, skip_serializing_if = "Option::is_none")]
641    pub inset: Option<Inset>,
642}
643
644/// Timed text.
645#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
646#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
647#[cfg_attr(feature = "schema", schemars(rename = "CaptionsNode"))]
648#[serde(deny_unknown_fields)]
649pub struct Captions {
650    /// The node's name, for other nodes to refer to.
651    #[serde(default, skip_serializing_if = "Option::is_none")]
652    pub id: Option<String>,
653    /// Cues as given, in seconds from the node's start.
654    #[serde(default, skip_serializing_if = "Vec::is_empty")]
655    pub cues: Vec<Cue>,
656    /// Words with their times, as speech recognition gives them, grouped into cues of up to
657    /// `max_chars` characters.
658    #[serde(default, skip_serializing_if = "Vec::is_empty")]
659    pub words: Vec<Cue>,
660    /// A SubRip (`.srt`) or WebVTT (`.vtt`) file, read by the binding.
661    #[serde(default, skip_serializing_if = "Option::is_none")]
662    pub file: Option<String>,
663    /// SubRip text.
664    #[serde(default, skip_serializing_if = "Option::is_none")]
665    pub srt: Option<String>,
666    /// WebVTT text.
667    #[serde(default, skip_serializing_if = "Option::is_none")]
668    pub vtt: Option<String>,
669    /// How the captions look. Default `outline`.
670    #[serde(default)]
671    pub style: CaptionStyle,
672    /// The longest cue words are grouped into, in characters. Default 32 on a tall canvas, 42
673    /// on a wide one.
674    #[serde(default, skip_serializing_if = "Option::is_none")]
675    pub max_chars: Option<u32>,
676    /// Font size in pixels. Default: the theme's caption size.
677    #[serde(default, skip_serializing_if = "Option::is_none")]
678    pub font_size: Option<f32>,
679    /// Text color. Default: the theme's.
680    #[serde(default, skip_serializing_if = "Option::is_none")]
681    pub color: Option<Rgba>,
682    /// The color `karaoke` and `pop` give the word being said. Default: the theme's accent.
683    #[serde(default, skip_serializing_if = "Option::is_none")]
684    pub highlight: Option<Rgba>,
685    /// Each speaker's look, keyed by a WebVTT voice or a cue's `speaker`; it overrides the
686    /// style.
687    #[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
688    pub speakers: BTreeMap<String, SpeakerLook>,
689    /// Where the text's middle lies, as a fraction of the canvas height. Default depends on the
690    /// style and the canvas's shape.
691    #[serde(default, skip_serializing_if = "Option::is_none")]
692    pub position: Option<f32>,
693    /// Seconds from the parent's start (an overlay's: the video's). Ignored in a sequence.
694    #[serde(default, skip_serializing_if = "Option::is_none")]
695    pub at: Option<f64>,
696    /// Length in seconds. Default: the node's own length, else what its parent gives it.
697    #[serde(default, skip_serializing_if = "Option::is_none")]
698    pub duration: Option<f64>,
699}
700
701/// Text over an interval.
702#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
703#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
704#[serde(deny_unknown_fields)]
705pub struct Cue {
706    /// Start in seconds.
707    pub start: f64,
708    /// End in seconds.
709    pub end: f64,
710    /// The text.
711    pub text: String,
712    /// Who says it: a key of the captions' `speakers` gives the cue its look.
713    #[serde(default, skip_serializing_if = "Option::is_none")]
714    pub speaker: Option<String>,
715}
716
717/// How one speaker's captions look.
718#[derive(Clone, Debug, Default, Deserialize, PartialEq, Serialize)]
719#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
720#[serde(deny_unknown_fields)]
721pub struct SpeakerLook {
722    /// Their text color.
723    #[serde(default, skip_serializing_if = "Option::is_none")]
724    pub color: Option<Rgba>,
725    /// Where their text's middle lies, as fractions of the canvas, `(0, 0)` top-left.
726    #[serde(default, skip_serializing_if = "Option::is_none")]
727    pub position: Option<alpha2_model::Vec2>,
728}
729
730/// How captions look.
731#[derive(Clone, Copy, Debug, Default, Deserialize, Eq, PartialEq, Serialize)]
732#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
733#[serde(rename_all = "kebab-case")]
734pub enum CaptionStyle {
735    /// Text with a dark band around the glyphs, near the bottom.
736    #[default]
737    Outline,
738    /// Text on a panel behind each line, near the bottom.
739    Boxed,
740    /// Text centered on a translucent bar across the canvas near the bottom, shown as long as
741    /// the captions.
742    BarBottom,
743    /// Larger outlined text a little below the middle, as short-form video draws it.
744    BoldCenter,
745    /// White text with a black stroke and a soft shadow, near the bottom.
746    Bold,
747    /// `bold`, each word turning the highlight color as it is said. Needs word times (`words`
748    /// or WebVTT inline timestamps), else draws as `bold`.
749    Karaoke,
750    /// `bold`, the word being said growing a little in the highlight color. Needs word times.
751    Pop,
752}
753
754/// A layout generator.
755#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
756#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
757#[serde(deny_unknown_fields)]
758pub struct Layout {
759    /// The node's name, for other nodes to refer to.
760    #[serde(default, skip_serializing_if = "Option::is_none")]
761    pub id: Option<String>,
762    /// The root box, as a project's layout generator writes it.
763    pub root: Box<LayoutNode>,
764    /// Seconds from the parent's start (an overlay's: the video's). Ignored in a sequence.
765    #[serde(default, skip_serializing_if = "Option::is_none")]
766    pub at: Option<f64>,
767    /// Length in seconds. Default: the node's own length, else what its parent gives it.
768    #[serde(default, skip_serializing_if = "Option::is_none")]
769    pub duration: Option<f64>,
770    /// Pixels kept clear inside the parent's area: one number, or `[top, right, bottom, left]`.
771    #[serde(default, skip_serializing_if = "Option::is_none")]
772    pub inset: Option<Inset>,
773    /// Where in its area the picture or text lies. Default `center`.
774    #[serde(default, skip_serializing_if = "Option::is_none")]
775    pub align: Option<Align>,
776}
777
778/// A sound file.
779#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
780#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
781#[serde(deny_unknown_fields)]
782pub struct Audio {
783    /// The node's name, for other nodes to refer to.
784    #[serde(default, skip_serializing_if = "Option::is_none")]
785    pub id: Option<String>,
786    /// The file.
787    pub path: String,
788    /// Where in the file it starts, in seconds.
789    #[serde(default, skip_serializing_if = "Option::is_none")]
790    pub trim_start: Option<f64>,
791    /// Where in the file it ends, in seconds. Default: its end.
792    #[serde(default, skip_serializing_if = "Option::is_none")]
793    pub trim_end: Option<f64>,
794    /// Plays the file again from its start while the node lasts.
795    #[serde(default, skip_serializing_if = "std::ops::Not::not")]
796    pub r#loop: bool,
797    /// Linear gain. Default 1.
798    #[serde(default, skip_serializing_if = "Option::is_none")]
799    pub volume: Option<f32>,
800    /// Seconds of fade in.
801    #[serde(default, skip_serializing_if = "Option::is_none")]
802    pub fade_in: Option<f64>,
803    /// Seconds of fade out.
804    #[serde(default, skip_serializing_if = "Option::is_none")]
805    pub fade_out: Option<f64>,
806    /// The `id` of a node (or `body`) whose sound this ducks under.
807    #[serde(default, skip_serializing_if = "Option::is_none")]
808    pub duck_under: Option<String>,
809    /// How far ducking lowers it, in dB (negative). Default -12.
810    #[serde(default, skip_serializing_if = "Option::is_none")]
811    pub duck_depth_db: Option<f32>,
812    /// Seconds from the parent's start (an overlay's: the video's). Ignored in a sequence.
813    #[serde(default, skip_serializing_if = "Option::is_none")]
814    pub at: Option<f64>,
815    /// Length in seconds. Default: the node's own length, else what its parent gives it.
816    #[serde(default, skip_serializing_if = "Option::is_none")]
817    pub duration: Option<f64>,
818}
819
820/// Part of a project at its own times.
821#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
822#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
823#[serde(deny_unknown_fields)]
824pub struct Raw {
825    /// Sources its tracks read. Ids must not collide with the compiler's (`s1`, `s2`, ...); a
826    /// `src-` prefix keeps them apart.
827    #[serde(default, skip_serializing_if = "Vec::is_empty")]
828    pub sources: Vec<MediaSource>,
829    /// Tracks drawn over what comes before, at timeline times.
830    #[serde(default, skip_serializing_if = "Vec::is_empty")]
831    pub tracks: Vec<Track>,
832    /// Compositions its clips show.
833    #[serde(default, skip_serializing_if = "Vec::is_empty")]
834    pub compositions: Vec<Composition>,
835    /// Each file a media source of the fragment reads, by source id: the export's assets.
836    #[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
837    pub assets: BTreeMap<String, String>,
838}
839
840/// Stills or clips in turn.
841#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
842#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
843#[serde(deny_unknown_fields)]
844pub struct Slideshow {
845    /// The node's name, for other nodes to refer to.
846    #[serde(default, skip_serializing_if = "Option::is_none")]
847    pub id: Option<String>,
848    /// The slides.
849    pub items: Vec<MediaItem>,
850    /// How long each slide shows, transitions included, in seconds. Default: `fit_to`'s length
851    /// shared out, else the theme's still length.
852    #[serde(default, skip_serializing_if = "Option::is_none")]
853    pub seconds: Option<f64>,
854    /// A sound file whose length the slides share out. It is not played; add it as a music bed.
855    #[serde(default, skip_serializing_if = "Option::is_none")]
856    pub fit_to: Option<String>,
857    /// What joins slides. Default `theme`.
858    #[serde(default, skip_serializing_if = "Option::is_none")]
859    pub transition: Option<TransitionChoice>,
860    /// How far each slide zooms in over its time, as a multiple (1.15 ends 15% larger). Default
861    /// none.
862    #[serde(default, skip_serializing_if = "Option::is_none")]
863    pub ken_burns: Option<f32>,
864    /// How slides fill the canvas. Default `cover`.
865    #[serde(default, skip_serializing_if = "Option::is_none")]
866    pub fit: Option<Fit>,
867    /// When it starts in its parent; its length is the slides'.
868    #[serde(default, skip_serializing_if = "Option::is_none")]
869    pub at: Option<f64>,
870}
871
872/// A video under its captions.
873#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
874#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
875#[serde(deny_unknown_fields)]
876pub struct CaptionedVideo {
877    /// The node's name, for other nodes to refer to. Its sound is the video's.
878    #[serde(default, skip_serializing_if = "Option::is_none")]
879    pub id: Option<String>,
880    /// The video.
881    pub video: MediaItem,
882    /// The captions, timed from the video's start.
883    pub captions: Box<Captions>,
884    /// How the video fills the canvas. Default `cover`.
885    #[serde(default, skip_serializing_if = "Option::is_none")]
886    pub fit: Option<Fit>,
887    /// When it starts in its parent; its length is the video's.
888    #[serde(default, skip_serializing_if = "Option::is_none")]
889    pub at: Option<f64>,
890}
891
892/// A title and subtitle.
893#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
894#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
895#[serde(deny_unknown_fields)]
896pub struct Title {
897    /// The node's name, for other nodes to refer to.
898    #[serde(default, skip_serializing_if = "Option::is_none")]
899    pub id: Option<String>,
900    /// The title.
901    pub text: String,
902    /// A smaller line under it.
903    #[serde(default, skip_serializing_if = "Option::is_none")]
904    pub subtitle: Option<String>,
905    /// Where it lies. Default `center`.
906    #[serde(default, skip_serializing_if = "Option::is_none")]
907    pub align: Option<Align>,
908    /// Seconds of fade in and out. Default: the theme's.
909    #[serde(default, skip_serializing_if = "Option::is_none")]
910    pub fade: Option<f64>,
911    /// A card's color. Default: the theme's surface.
912    #[serde(default, skip_serializing_if = "Option::is_none")]
913    pub background: Option<Rgba>,
914    /// Seconds from the parent's start (an overlay's: the video's). Ignored in a sequence.
915    #[serde(default, skip_serializing_if = "Option::is_none")]
916    pub at: Option<f64>,
917    /// Length in seconds. Default: the node's own length, else what its parent gives it.
918    #[serde(default, skip_serializing_if = "Option::is_none")]
919    pub duration: Option<f64>,
920}
921
922/// A name and role in the lower left.
923#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
924#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
925#[serde(deny_unknown_fields)]
926pub struct LowerThird {
927    /// The node's name, for other nodes to refer to.
928    #[serde(default, skip_serializing_if = "Option::is_none")]
929    pub id: Option<String>,
930    /// The name.
931    pub text: String,
932    /// The role, under it.
933    #[serde(default, skip_serializing_if = "Option::is_none")]
934    pub subtitle: Option<String>,
935    /// Seconds of fade in and out. Default: the theme's.
936    #[serde(default, skip_serializing_if = "Option::is_none")]
937    pub fade: Option<f64>,
938    /// Seconds from the parent's start (an overlay's: the video's). Ignored in a sequence.
939    #[serde(default, skip_serializing_if = "Option::is_none")]
940    pub at: Option<f64>,
941    /// Length in seconds. Default: the node's own length, else what its parent gives it.
942    #[serde(default, skip_serializing_if = "Option::is_none")]
943    pub duration: Option<f64>,
944}
945
946/// A closing card.
947#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
948#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
949#[serde(deny_unknown_fields)]
950pub struct EndCard {
951    /// The node's name, for other nodes to refer to.
952    #[serde(default, skip_serializing_if = "Option::is_none")]
953    pub id: Option<String>,
954    /// The title.
955    pub text: String,
956    /// A smaller line under it.
957    #[serde(default, skip_serializing_if = "Option::is_none")]
958    pub subtitle: Option<String>,
959    /// A picture above the title.
960    #[serde(default, skip_serializing_if = "Option::is_none")]
961    pub logo: Option<String>,
962    /// The card's color. Default: the theme's surface.
963    #[serde(default, skip_serializing_if = "Option::is_none")]
964    pub background: Option<Rgba>,
965    /// Seconds of fade in and out. Default: the theme's.
966    #[serde(default, skip_serializing_if = "Option::is_none")]
967    pub fade: Option<f64>,
968    /// Seconds from the parent's start (an overlay's: the video's). Ignored in a sequence.
969    #[serde(default, skip_serializing_if = "Option::is_none")]
970    pub at: Option<f64>,
971    /// Length in seconds. Default: the node's own length, else what its parent gives it.
972    #[serde(default, skip_serializing_if = "Option::is_none")]
973    pub duration: Option<f64>,
974}
975
976/// Clips in a labeled grid.
977#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
978#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
979#[serde(deny_unknown_fields)]
980pub struct ComparisonGrid {
981    /// The node's name, for other nodes to refer to.
982    #[serde(default, skip_serializing_if = "Option::is_none")]
983    pub id: Option<String>,
984    /// The clips, row by row; each one's `label` names its cell.
985    pub items: Vec<MediaItem>,
986    /// Columns. Default: as square a grid as the items make.
987    #[serde(default, skip_serializing_if = "Option::is_none")]
988    pub columns: Option<u32>,
989    /// Pixels between cells.
990    #[serde(default)]
991    pub gap: f32,
992    /// How long the grid lasts when its clips differ. Default `shortest`.
993    #[serde(default)]
994    pub length: GridLength,
995    /// Which item's sound plays, by index. Default: none.
996    #[serde(default, skip_serializing_if = "Option::is_none")]
997    pub audio_from: Option<u32>,
998    /// Seconds from the parent's start (an overlay's: the video's). Ignored in a sequence.
999    #[serde(default, skip_serializing_if = "Option::is_none")]
1000    pub at: Option<f64>,
1001    /// Length in seconds. Default: the node's own length, else what its parent gives it.
1002    #[serde(default, skip_serializing_if = "Option::is_none")]
1003    pub duration: Option<f64>,
1004}
1005
1006/// A video over a blurred copy of itself.
1007#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
1008#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1009#[serde(deny_unknown_fields)]
1010pub struct Reframe {
1011    /// The node's name, for other nodes to refer to. Its sound is the video's.
1012    #[serde(default, skip_serializing_if = "Option::is_none")]
1013    pub id: Option<String>,
1014    /// The video.
1015    pub video: MediaItem,
1016    /// The background's blur radius, as a fraction of its shorter side. Default 0.05.
1017    #[serde(default, skip_serializing_if = "Option::is_none")]
1018    pub blur: Option<f32>,
1019    /// When it starts in its parent; its length is the video's.
1020    #[serde(default, skip_serializing_if = "Option::is_none")]
1021    pub at: Option<f64>,
1022}
1023
1024/// A picture in a corner.
1025#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
1026#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1027#[serde(deny_unknown_fields)]
1028pub struct Logo {
1029    /// The node's name, for other nodes to refer to.
1030    #[serde(default, skip_serializing_if = "Option::is_none")]
1031    pub id: Option<String>,
1032    /// The picture.
1033    pub path: String,
1034    /// The corner. Default `top-right`.
1035    #[serde(default, skip_serializing_if = "Option::is_none")]
1036    pub corner: Option<Align>,
1037    /// Its longer side, in pixels. Default: 15% of the canvas's shorter side.
1038    #[serde(default, skip_serializing_if = "Option::is_none")]
1039    pub size: Option<f32>,
1040    /// Pixels from the edges. Default: the theme's margin.
1041    #[serde(default, skip_serializing_if = "Option::is_none")]
1042    pub margin: Option<f32>,
1043    /// Opacity, 0..=1. Default 1.
1044    #[serde(default, skip_serializing_if = "Option::is_none")]
1045    pub opacity: Option<f32>,
1046    /// Seconds from the parent's start (an overlay's: the video's). Ignored in a sequence.
1047    #[serde(default, skip_serializing_if = "Option::is_none")]
1048    pub at: Option<f64>,
1049    /// Length in seconds. Default: the node's own length, else what its parent gives it.
1050    #[serde(default, skip_serializing_if = "Option::is_none")]
1051    pub duration: Option<f64>,
1052}
1053
1054/// Music under the video.
1055#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
1056#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1057#[serde(deny_unknown_fields)]
1058pub struct MusicBed {
1059    /// The node's name, for other nodes to refer to.
1060    #[serde(default, skip_serializing_if = "Option::is_none")]
1061    pub id: Option<String>,
1062    /// The file.
1063    pub path: String,
1064    /// Linear gain. Default 1.
1065    #[serde(default, skip_serializing_if = "Option::is_none")]
1066    pub volume: Option<f32>,
1067    /// Seconds of fade in. Default 1.
1068    #[serde(default, skip_serializing_if = "Option::is_none")]
1069    pub fade_in: Option<f64>,
1070    /// Seconds of fade out at the end. Default 2.
1071    #[serde(default, skip_serializing_if = "Option::is_none")]
1072    pub fade_out: Option<f64>,
1073    /// The `id` of a node (or `body`) whose sound it ducks under.
1074    #[serde(default, skip_serializing_if = "Option::is_none")]
1075    pub duck_under: Option<String>,
1076    /// How far ducking lowers it, in dB (negative). Default -12.
1077    #[serde(default, skip_serializing_if = "Option::is_none")]
1078    pub duck_depth_db: Option<f32>,
1079    /// Plays the file again from its start while the video lasts. Default true.
1080    #[serde(default = "yes")]
1081    pub r#loop: bool,
1082    /// Seconds from the parent's start (an overlay's: the video's). Ignored in a sequence.
1083    #[serde(default, skip_serializing_if = "Option::is_none")]
1084    pub at: Option<f64>,
1085    /// Length in seconds. Default: the node's own length, else what its parent gives it.
1086    #[serde(default, skip_serializing_if = "Option::is_none")]
1087    pub duration: Option<f64>,
1088}
1089
1090const fn yes() -> bool {
1091    true
1092}