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    /// A number counting from one value to another, as text.
348    Counter(Counter),
349}
350
351/// Children one after another.
352#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
353#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
354#[serde(deny_unknown_fields)]
355pub struct Sequence {
356    /// The node's name, for other nodes to refer to.
357    #[serde(default, skip_serializing_if = "Option::is_none")]
358    pub id: Option<String>,
359    /// The children, in play order.
360    pub children: Vec<Node>,
361    /// What joins neighbours: a transition overlaps them by its duration.
362    /// Default `none`. Only single pictures (media, text, color, layout)
363    /// can transition into each other.
364    #[serde(default, skip_serializing_if = "Option::is_none")]
365    pub transition: Option<TransitionChoice>,
366    /// Seconds from the parent's start (an overlay's: the video's). Ignored in a sequence.
367    #[serde(default, skip_serializing_if = "Option::is_none")]
368    pub at: Option<f64>,
369    /// Length in seconds. Default: the node's own length, else what its parent gives it.
370    #[serde(default, skip_serializing_if = "Option::is_none")]
371    pub duration: Option<f64>,
372    /// How it comes in at its start.
373    #[serde(default, skip_serializing_if = "Option::is_none")]
374    pub enter: Option<Motion>,
375    /// How it goes out, ending at its end.
376    #[serde(default, skip_serializing_if = "Option::is_none")]
377    pub exit: Option<Motion>,
378}
379
380/// Children over each other.
381#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
382#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
383#[serde(deny_unknown_fields)]
384pub struct Stack {
385    /// The node's name, for other nodes to refer to.
386    #[serde(default, skip_serializing_if = "Option::is_none")]
387    pub id: Option<String>,
388    /// The children, each drawn over the ones before and starting at its own `at`.
389    pub children: Vec<Node>,
390    /// Seconds from the parent's start (an overlay's: the video's). Ignored in a sequence.
391    #[serde(default, skip_serializing_if = "Option::is_none")]
392    pub at: Option<f64>,
393    /// Length in seconds. Default: the node's own length, else what its parent gives it.
394    #[serde(default, skip_serializing_if = "Option::is_none")]
395    pub duration: Option<f64>,
396    /// Pixels kept clear inside the parent's area: one number, or `[top, right, bottom, left]`.
397    #[serde(default, skip_serializing_if = "Option::is_none")]
398    pub inset: Option<Inset>,
399    /// How it comes in at its start.
400    #[serde(default, skip_serializing_if = "Option::is_none")]
401    pub enter: Option<Motion>,
402    /// How it goes out, ending at its end.
403    #[serde(default, skip_serializing_if = "Option::is_none")]
404    pub exit: Option<Motion>,
405    /// Seconds between the children's entrances. With it, the node's `enter` and `exit` move
406    /// each child rather than the whole, and child `i`'s entrance (its own `enter` too) starts
407    /// `i * stagger` after the child does.
408    #[serde(default, skip_serializing_if = "Option::is_none")]
409    pub stagger: Option<f64>,
410}
411
412/// Children in grid cells.
413#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
414#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
415#[serde(deny_unknown_fields)]
416pub struct Grid {
417    /// The node's name, for other nodes to refer to.
418    #[serde(default, skip_serializing_if = "Option::is_none")]
419    pub id: Option<String>,
420    /// The cells' contents, row by row.
421    pub children: Vec<Node>,
422    /// Columns. Default: as square a grid as the children make.
423    #[serde(default, skip_serializing_if = "Option::is_none")]
424    pub columns: Option<u32>,
425    /// Pixels between cells.
426    #[serde(default)]
427    pub gap: f32,
428    /// How long the grid lasts when its cells differ, and what a cell that ends early shows.
429    /// Default `shortest`.
430    #[serde(default)]
431    pub length: GridLength,
432    /// Seconds from the parent's start (an overlay's: the video's). Ignored in a sequence.
433    #[serde(default, skip_serializing_if = "Option::is_none")]
434    pub at: Option<f64>,
435    /// Length in seconds. Default: the node's own length, else what its parent gives it.
436    #[serde(default, skip_serializing_if = "Option::is_none")]
437    pub duration: Option<f64>,
438    /// Pixels kept clear inside the parent's area: one number, or `[top, right, bottom, left]`.
439    #[serde(default, skip_serializing_if = "Option::is_none")]
440    pub inset: Option<Inset>,
441    /// How it comes in at its start.
442    #[serde(default, skip_serializing_if = "Option::is_none")]
443    pub enter: Option<Motion>,
444    /// How it goes out, ending at its end.
445    #[serde(default, skip_serializing_if = "Option::is_none")]
446    pub exit: Option<Motion>,
447    /// Seconds between the children's entrances. With it, the node's `enter` and `exit` move
448    /// each child rather than the whole, and child `i`'s entrance (its own `enter` too) starts
449    /// `i * stagger` after the child does.
450    #[serde(default, skip_serializing_if = "Option::is_none")]
451    pub stagger: Option<f64>,
452}
453
454/// How long a grid of clips of different lengths lasts.
455#[derive(Clone, Copy, Debug, Default, Deserialize, Eq, PartialEq, Serialize)]
456#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
457#[serde(rename_all = "snake_case")]
458pub enum GridLength {
459    /// As long as its shortest cell.
460    #[default]
461    Shortest,
462    /// As long as its longest; shorter clips hold their last frame.
463    LongestFreeze,
464    /// As long as its longest; shorter clips play again from their start.
465    LongestLoop,
466}
467
468/// A video or still file.
469#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
470#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
471#[serde(deny_unknown_fields)]
472pub struct Media {
473    /// The node's name, for other nodes to refer to.
474    #[serde(default, skip_serializing_if = "Option::is_none")]
475    pub id: Option<String>,
476    /// The file. It becomes the source's locator and the export's asset.
477    pub path: String,
478    /// What the file holds. Default: by its extension.
479    #[serde(default, skip_serializing_if = "Option::is_none")]
480    pub kind: Option<FileKind>,
481    /// Where in the file a video starts, in seconds.
482    #[serde(default, skip_serializing_if = "Option::is_none")]
483    pub trim_start: Option<f64>,
484    /// Where in the file a video ends, in seconds. Default: its end.
485    #[serde(default, skip_serializing_if = "Option::is_none")]
486    pub trim_end: Option<f64>,
487    /// Playback speed of a video: 2 plays it twice as fast. Default 1.
488    #[serde(default, skip_serializing_if = "Option::is_none")]
489    pub speed: Option<f64>,
490    /// Keeps a video's sound at its pitch when `speed` is not 1 (time-stretched instead of
491    /// resampled). Default true.
492    #[serde(default, skip_serializing_if = "Option::is_none")]
493    pub preserve_pitch: Option<bool>,
494    /// Plays a video again from its start while the node lasts.
495    #[serde(default, skip_serializing_if = "std::ops::Not::not")]
496    pub r#loop: bool,
497    /// Linear gain of a video's sound. Default 1.
498    #[serde(default, skip_serializing_if = "Option::is_none")]
499    pub volume: Option<f32>,
500    /// Silences a video's sound.
501    #[serde(default, skip_serializing_if = "std::ops::Not::not")]
502    pub muted: bool,
503    /// Opacity, 0..=1. Default 1.
504    #[serde(default, skip_serializing_if = "Option::is_none")]
505    pub opacity: Option<f32>,
506    /// Seconds of fade in from transparent.
507    #[serde(default, skip_serializing_if = "Option::is_none")]
508    pub fade_in: Option<f64>,
509    /// Seconds of fade out to transparent.
510    #[serde(default, skip_serializing_if = "Option::is_none")]
511    pub fade_out: Option<f64>,
512    /// A slow zoom over the node's length, `[from, to]`, as multiples of the fitted size.
513    #[serde(default, skip_serializing_if = "Option::is_none")]
514    pub zoom: Option<[f32; 2]>,
515    /// A Gaussian blur's radius, as a fraction of the picture's shorter side.
516    #[serde(default, skip_serializing_if = "Option::is_none")]
517    pub blur: Option<f32>,
518    /// A label a comparison grid shows on the cell.
519    #[serde(default, skip_serializing_if = "Option::is_none")]
520    pub label: Option<String>,
521    /// Seconds from the parent's start (an overlay's: the video's). Ignored in a sequence.
522    #[serde(default, skip_serializing_if = "Option::is_none")]
523    pub at: Option<f64>,
524    /// Length in seconds. Default: the node's own length, else what its parent gives it.
525    #[serde(default, skip_serializing_if = "Option::is_none")]
526    pub duration: Option<f64>,
527    /// Pixels kept clear inside the parent's area: one number, or `[top, right, bottom, left]`.
528    #[serde(default, skip_serializing_if = "Option::is_none")]
529    pub inset: Option<Inset>,
530    /// How a picture fills its area. Default `contain`.
531    #[serde(default, skip_serializing_if = "Option::is_none")]
532    pub fit: Option<Fit>,
533    /// Where in its area the picture or text lies. Default `center`.
534    #[serde(default, skip_serializing_if = "Option::is_none")]
535    pub align: Option<Align>,
536    /// How it comes in at its start.
537    #[serde(default, skip_serializing_if = "Option::is_none")]
538    pub enter: Option<Motion>,
539    /// How it goes out, ending at its end.
540    #[serde(default, skip_serializing_if = "Option::is_none")]
541    pub exit: Option<Motion>,
542}
543
544impl Media {
545    /// A node of the file at `path`, everything else by default.
546    #[must_use]
547    pub fn new(path: impl Into<String>) -> Self {
548        Self {
549            id: None,
550            path: path.into(),
551            kind: None,
552            trim_start: None,
553            trim_end: None,
554            speed: None,
555            preserve_pitch: None,
556            r#loop: false,
557            volume: None,
558            muted: false,
559            opacity: None,
560            fade_in: None,
561            fade_out: None,
562            zoom: None,
563            blur: None,
564            label: None,
565            at: None,
566            duration: None,
567            inset: None,
568            fit: None,
569            align: None,
570            enter: None,
571            exit: None,
572        }
573    }
574}
575
576/// A file given by its path alone, or a media node.
577#[derive(Clone, Debug, PartialEq, Serialize)]
578#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
579#[serde(untagged)]
580pub enum MediaItem {
581    /// The file, everything else by default.
582    Path(String),
583    /// A `media` node.
584    Node(Box<Node>),
585}
586
587/// Text in one of the theme's styles.
588#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
589#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
590#[serde(deny_unknown_fields)]
591pub struct Text {
592    /// The node's name, for other nodes to refer to.
593    #[serde(default, skip_serializing_if = "Option::is_none")]
594    pub id: Option<String>,
595    /// The text; `\n` starts a line.
596    pub text: String,
597    /// The theme style it starts from. Default `body`.
598    #[serde(default)]
599    pub style: TextRole,
600    /// Font size in pixels. Default: the style's.
601    #[serde(default, skip_serializing_if = "Option::is_none")]
602    pub font_size: Option<f32>,
603    /// Family. Default: the theme's.
604    #[serde(default, skip_serializing_if = "Option::is_none")]
605    pub font_family: Option<String>,
606    /// Color. Default: the theme's text color.
607    #[serde(default, skip_serializing_if = "Option::is_none")]
608    pub color: Option<Rgba>,
609    /// A box of this color behind each line.
610    #[serde(default, skip_serializing_if = "Option::is_none")]
611    pub background: Option<Rgba>,
612    /// Bands around the glyphs, innermost first, widths as fractions of the font size (as a
613    /// text style's `strokes`).
614    #[serde(default, skip_serializing_if = "Vec::is_empty")]
615    pub strokes: Vec<TextStroke>,
616    /// Shadows of the glyphs, the first on top, lengths as fractions of the font size (as a
617    /// text style's `shadows`).
618    #[serde(default, skip_serializing_if = "Vec::is_empty")]
619    pub shadows: Vec<TextShadow>,
620    /// The most lines it may take; with fonts to measure by, longer text shrinks until it fits.
621    /// Default: no limit.
622    #[serde(default, skip_serializing_if = "Option::is_none")]
623    pub max_lines: Option<u32>,
624    /// Opacity, 0..=1. Default 1.
625    #[serde(default, skip_serializing_if = "Option::is_none")]
626    pub opacity: Option<f32>,
627    /// Seconds of fade in and out. Default none.
628    #[serde(default, skip_serializing_if = "Option::is_none")]
629    pub fade: Option<f64>,
630    /// Seconds from the parent's start (an overlay's: the video's). Ignored in a sequence.
631    #[serde(default, skip_serializing_if = "Option::is_none")]
632    pub at: Option<f64>,
633    /// Length in seconds. Default: the node's own length, else what its parent gives it.
634    #[serde(default, skip_serializing_if = "Option::is_none")]
635    pub duration: Option<f64>,
636    /// Pixels kept clear inside the parent's area: one number, or `[top, right, bottom, left]`.
637    #[serde(default, skip_serializing_if = "Option::is_none")]
638    pub inset: Option<Inset>,
639    /// Where in its area the picture or text lies. Default `center`.
640    #[serde(default, skip_serializing_if = "Option::is_none")]
641    pub align: Option<Align>,
642    /// How it comes in at its start.
643    #[serde(default, skip_serializing_if = "Option::is_none")]
644    pub enter: Option<Motion>,
645    /// How it goes out, ending at its end.
646    #[serde(default, skip_serializing_if = "Option::is_none")]
647    pub exit: Option<Motion>,
648    /// Reveals the text a word or a line at a time.
649    #[serde(default, skip_serializing_if = "Option::is_none")]
650    pub reveal: Option<Reveal>,
651}
652
653/// A theme text style.
654#[derive(Clone, Copy, Debug, Default, Deserialize, Eq, PartialEq, Serialize)]
655#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
656#[serde(rename_all = "snake_case")]
657pub enum TextRole {
658    /// The largest.
659    Title,
660    /// Under a title.
661    Subtitle,
662    /// Running text.
663    #[default]
664    Body,
665    /// A cell's label.
666    Label,
667}
668
669/// An area painted one color.
670#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
671#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
672#[serde(deny_unknown_fields)]
673pub struct ColorFill {
674    /// The node's name, for other nodes to refer to.
675    #[serde(default, skip_serializing_if = "Option::is_none")]
676    pub id: Option<String>,
677    /// The color. Default: the theme's surface.
678    #[serde(default, skip_serializing_if = "Option::is_none")]
679    pub color: Option<Rgba>,
680    /// Seconds from the parent's start (an overlay's: the video's). Ignored in a sequence.
681    #[serde(default, skip_serializing_if = "Option::is_none")]
682    pub at: Option<f64>,
683    /// Length in seconds. Default: the node's own length, else what its parent gives it.
684    #[serde(default, skip_serializing_if = "Option::is_none")]
685    pub duration: Option<f64>,
686    /// Pixels kept clear inside the parent's area: one number, or `[top, right, bottom, left]`.
687    #[serde(default, skip_serializing_if = "Option::is_none")]
688    pub inset: Option<Inset>,
689    /// How it comes in at its start.
690    #[serde(default, skip_serializing_if = "Option::is_none")]
691    pub enter: Option<Motion>,
692    /// How it goes out, ending at its end.
693    #[serde(default, skip_serializing_if = "Option::is_none")]
694    pub exit: Option<Motion>,
695}
696
697/// Timed text.
698#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
699#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
700#[cfg_attr(feature = "schema", schemars(rename = "CaptionsNode"))]
701#[serde(deny_unknown_fields)]
702pub struct Captions {
703    /// The node's name, for other nodes to refer to.
704    #[serde(default, skip_serializing_if = "Option::is_none")]
705    pub id: Option<String>,
706    /// Cues as given, in seconds from the node's start.
707    #[serde(default, skip_serializing_if = "Vec::is_empty")]
708    pub cues: Vec<Cue>,
709    /// Words with their times, as speech recognition gives them, grouped into cues of up to
710    /// `max_chars` characters.
711    #[serde(default, skip_serializing_if = "Vec::is_empty")]
712    pub words: Vec<Cue>,
713    /// A SubRip (`.srt`) or WebVTT (`.vtt`) file, read by the binding.
714    #[serde(default, skip_serializing_if = "Option::is_none")]
715    pub file: Option<String>,
716    /// SubRip text.
717    #[serde(default, skip_serializing_if = "Option::is_none")]
718    pub srt: Option<String>,
719    /// WebVTT text.
720    #[serde(default, skip_serializing_if = "Option::is_none")]
721    pub vtt: Option<String>,
722    /// How the captions look. Default `outline`.
723    #[serde(default)]
724    pub style: CaptionStyle,
725    /// The longest cue words are grouped into, in characters. Default 32 on a tall canvas, 42
726    /// on a wide one.
727    #[serde(default, skip_serializing_if = "Option::is_none")]
728    pub max_chars: Option<u32>,
729    /// Font size in pixels. Default: the theme's caption size.
730    #[serde(default, skip_serializing_if = "Option::is_none")]
731    pub font_size: Option<f32>,
732    /// Text color. Default: the theme's.
733    #[serde(default, skip_serializing_if = "Option::is_none")]
734    pub color: Option<Rgba>,
735    /// The color `karaoke` and `pop` give the word being said. Default: the theme's accent.
736    #[serde(default, skip_serializing_if = "Option::is_none")]
737    pub highlight: Option<Rgba>,
738    /// Each speaker's look, keyed by a WebVTT voice or a cue's `speaker`; it overrides the
739    /// style.
740    #[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
741    pub speakers: BTreeMap<String, SpeakerLook>,
742    /// Where the text's middle lies, as a fraction of the canvas height. Default depends on the
743    /// style and the canvas's shape.
744    #[serde(default, skip_serializing_if = "Option::is_none")]
745    pub position: Option<f32>,
746    /// Seconds from the parent's start (an overlay's: the video's). Ignored in a sequence.
747    #[serde(default, skip_serializing_if = "Option::is_none")]
748    pub at: Option<f64>,
749    /// Length in seconds. Default: the node's own length, else what its parent gives it.
750    #[serde(default, skip_serializing_if = "Option::is_none")]
751    pub duration: Option<f64>,
752}
753
754/// Text over an interval.
755#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
756#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
757#[serde(deny_unknown_fields)]
758pub struct Cue {
759    /// Start in seconds.
760    pub start: f64,
761    /// End in seconds.
762    pub end: f64,
763    /// The text.
764    pub text: String,
765    /// Who says it: a key of the captions' `speakers` gives the cue its look.
766    #[serde(default, skip_serializing_if = "Option::is_none")]
767    pub speaker: Option<String>,
768}
769
770/// How one speaker's captions look.
771#[derive(Clone, Debug, Default, Deserialize, PartialEq, Serialize)]
772#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
773#[serde(deny_unknown_fields)]
774pub struct SpeakerLook {
775    /// Their text color.
776    #[serde(default, skip_serializing_if = "Option::is_none")]
777    pub color: Option<Rgba>,
778    /// Where their text's middle lies, as fractions of the canvas, `(0, 0)` top-left.
779    #[serde(default, skip_serializing_if = "Option::is_none")]
780    pub position: Option<alpha2_model::Vec2>,
781}
782
783/// How captions look.
784#[derive(Clone, Copy, Debug, Default, Deserialize, Eq, PartialEq, Serialize)]
785#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
786#[serde(rename_all = "kebab-case")]
787pub enum CaptionStyle {
788    /// Text with a dark band around the glyphs, near the bottom.
789    #[default]
790    Outline,
791    /// Text on a panel behind each line, near the bottom.
792    Boxed,
793    /// Text centered on a translucent bar across the canvas near the bottom, shown as long as
794    /// the captions.
795    BarBottom,
796    /// Larger outlined text a little below the middle, as short-form video draws it.
797    BoldCenter,
798    /// White text with a black stroke and a soft shadow, near the bottom.
799    Bold,
800    /// `bold`, each word turning the highlight color as it is said. Needs word times (`words`
801    /// or WebVTT inline timestamps), else draws as `bold`.
802    Karaoke,
803    /// `bold`, the word being said growing a little in the highlight color. Needs word times.
804    Pop,
805}
806
807/// A layout generator.
808#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
809#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
810#[serde(deny_unknown_fields)]
811pub struct Layout {
812    /// The node's name, for other nodes to refer to.
813    #[serde(default, skip_serializing_if = "Option::is_none")]
814    pub id: Option<String>,
815    /// The root box, as a project's layout generator writes it.
816    pub root: Box<LayoutNode>,
817    /// Seconds from the parent's start (an overlay's: the video's). Ignored in a sequence.
818    #[serde(default, skip_serializing_if = "Option::is_none")]
819    pub at: Option<f64>,
820    /// Length in seconds. Default: the node's own length, else what its parent gives it.
821    #[serde(default, skip_serializing_if = "Option::is_none")]
822    pub duration: Option<f64>,
823    /// Pixels kept clear inside the parent's area: one number, or `[top, right, bottom, left]`.
824    #[serde(default, skip_serializing_if = "Option::is_none")]
825    pub inset: Option<Inset>,
826    /// Where in its area the picture or text lies. Default `center`.
827    #[serde(default, skip_serializing_if = "Option::is_none")]
828    pub align: Option<Align>,
829    /// How it comes in at its start.
830    #[serde(default, skip_serializing_if = "Option::is_none")]
831    pub enter: Option<Motion>,
832    /// How it goes out, ending at its end.
833    #[serde(default, skip_serializing_if = "Option::is_none")]
834    pub exit: Option<Motion>,
835}
836
837/// A sound file.
838#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
839#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
840#[serde(deny_unknown_fields)]
841pub struct Audio {
842    /// The node's name, for other nodes to refer to.
843    #[serde(default, skip_serializing_if = "Option::is_none")]
844    pub id: Option<String>,
845    /// The file.
846    pub path: String,
847    /// Where in the file it starts, in seconds.
848    #[serde(default, skip_serializing_if = "Option::is_none")]
849    pub trim_start: Option<f64>,
850    /// Where in the file it ends, in seconds. Default: its end.
851    #[serde(default, skip_serializing_if = "Option::is_none")]
852    pub trim_end: Option<f64>,
853    /// Plays the file again from its start while the node lasts.
854    #[serde(default, skip_serializing_if = "std::ops::Not::not")]
855    pub r#loop: bool,
856    /// Linear gain. Default 1.
857    #[serde(default, skip_serializing_if = "Option::is_none")]
858    pub volume: Option<f32>,
859    /// Seconds of fade in.
860    #[serde(default, skip_serializing_if = "Option::is_none")]
861    pub fade_in: Option<f64>,
862    /// Seconds of fade out.
863    #[serde(default, skip_serializing_if = "Option::is_none")]
864    pub fade_out: Option<f64>,
865    /// The `id` of a node (or `body`) whose sound this ducks under.
866    #[serde(default, skip_serializing_if = "Option::is_none")]
867    pub duck_under: Option<String>,
868    /// How far ducking lowers it, in dB (negative). Default -12.
869    #[serde(default, skip_serializing_if = "Option::is_none")]
870    pub duck_depth_db: Option<f32>,
871    /// Seconds from the parent's start (an overlay's: the video's). Ignored in a sequence.
872    #[serde(default, skip_serializing_if = "Option::is_none")]
873    pub at: Option<f64>,
874    /// Length in seconds. Default: the node's own length, else what its parent gives it.
875    #[serde(default, skip_serializing_if = "Option::is_none")]
876    pub duration: Option<f64>,
877}
878
879/// Part of a project at its own times.
880#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
881#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
882#[serde(deny_unknown_fields)]
883pub struct Raw {
884    /// Sources its tracks read. Ids must not collide with the compiler's (`s1`, `s2`, ...); a
885    /// `src-` prefix keeps them apart.
886    #[serde(default, skip_serializing_if = "Vec::is_empty")]
887    pub sources: Vec<MediaSource>,
888    /// Tracks drawn over what comes before, at timeline times.
889    #[serde(default, skip_serializing_if = "Vec::is_empty")]
890    pub tracks: Vec<Track>,
891    /// Compositions its clips show.
892    #[serde(default, skip_serializing_if = "Vec::is_empty")]
893    pub compositions: Vec<Composition>,
894    /// Each file a media source of the fragment reads, by source id: the export's assets.
895    #[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
896    pub assets: BTreeMap<String, String>,
897}
898
899/// Stills or clips in turn.
900#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
901#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
902#[serde(deny_unknown_fields)]
903pub struct Slideshow {
904    /// The node's name, for other nodes to refer to.
905    #[serde(default, skip_serializing_if = "Option::is_none")]
906    pub id: Option<String>,
907    /// The slides.
908    pub items: Vec<MediaItem>,
909    /// How long each slide shows, transitions included, in seconds. Default: `fit_to`'s length
910    /// shared out, else the theme's still length.
911    #[serde(default, skip_serializing_if = "Option::is_none")]
912    pub seconds: Option<f64>,
913    /// A sound file whose length the slides share out. It is not played; add it as a music bed.
914    #[serde(default, skip_serializing_if = "Option::is_none")]
915    pub fit_to: Option<String>,
916    /// What joins slides. Default `theme`.
917    #[serde(default, skip_serializing_if = "Option::is_none")]
918    pub transition: Option<TransitionChoice>,
919    /// How far each slide zooms in over its time, as a multiple (1.15 ends 15% larger). Default
920    /// none.
921    #[serde(default, skip_serializing_if = "Option::is_none")]
922    pub ken_burns: Option<f32>,
923    /// How slides fill the canvas. Default `cover`.
924    #[serde(default, skip_serializing_if = "Option::is_none")]
925    pub fit: Option<Fit>,
926    /// When it starts in its parent; its length is the slides'.
927    #[serde(default, skip_serializing_if = "Option::is_none")]
928    pub at: Option<f64>,
929}
930
931/// A video under its captions.
932#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
933#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
934#[serde(deny_unknown_fields)]
935pub struct CaptionedVideo {
936    /// The node's name, for other nodes to refer to. Its sound is the video's.
937    #[serde(default, skip_serializing_if = "Option::is_none")]
938    pub id: Option<String>,
939    /// The video.
940    pub video: MediaItem,
941    /// The captions, timed from the video's start.
942    pub captions: Box<Captions>,
943    /// How the video fills the canvas. Default `cover`.
944    #[serde(default, skip_serializing_if = "Option::is_none")]
945    pub fit: Option<Fit>,
946    /// When it starts in its parent; its length is the video's.
947    #[serde(default, skip_serializing_if = "Option::is_none")]
948    pub at: Option<f64>,
949}
950
951/// A title and subtitle.
952#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
953#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
954#[serde(deny_unknown_fields)]
955pub struct Title {
956    /// The node's name, for other nodes to refer to.
957    #[serde(default, skip_serializing_if = "Option::is_none")]
958    pub id: Option<String>,
959    /// The title.
960    pub text: String,
961    /// A smaller line under it.
962    #[serde(default, skip_serializing_if = "Option::is_none")]
963    pub subtitle: Option<String>,
964    /// Where it lies. Default `center`.
965    #[serde(default, skip_serializing_if = "Option::is_none")]
966    pub align: Option<Align>,
967    /// Seconds of fade in and out. Default: the theme's.
968    #[serde(default, skip_serializing_if = "Option::is_none")]
969    pub fade: Option<f64>,
970    /// A card's color. Default: the theme's surface.
971    #[serde(default, skip_serializing_if = "Option::is_none")]
972    pub background: Option<Rgba>,
973    /// Seconds from the parent's start (an overlay's: the video's). Ignored in a sequence.
974    #[serde(default, skip_serializing_if = "Option::is_none")]
975    pub at: Option<f64>,
976    /// Length in seconds. Default: the node's own length, else what its parent gives it.
977    #[serde(default, skip_serializing_if = "Option::is_none")]
978    pub duration: Option<f64>,
979    /// How it comes in at its start.
980    #[serde(default, skip_serializing_if = "Option::is_none")]
981    pub enter: Option<Motion>,
982    /// How it goes out, ending at its end.
983    #[serde(default, skip_serializing_if = "Option::is_none")]
984    pub exit: Option<Motion>,
985}
986
987/// A name and role in the lower left.
988#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
989#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
990#[serde(deny_unknown_fields)]
991pub struct LowerThird {
992    /// The node's name, for other nodes to refer to.
993    #[serde(default, skip_serializing_if = "Option::is_none")]
994    pub id: Option<String>,
995    /// The name.
996    pub text: String,
997    /// The role, under it.
998    #[serde(default, skip_serializing_if = "Option::is_none")]
999    pub subtitle: Option<String>,
1000    /// Seconds of fade in and out. Default: the theme's.
1001    #[serde(default, skip_serializing_if = "Option::is_none")]
1002    pub fade: Option<f64>,
1003    /// Seconds from the parent's start (an overlay's: the video's). Ignored in a sequence.
1004    #[serde(default, skip_serializing_if = "Option::is_none")]
1005    pub at: Option<f64>,
1006    /// Length in seconds. Default: the node's own length, else what its parent gives it.
1007    #[serde(default, skip_serializing_if = "Option::is_none")]
1008    pub duration: Option<f64>,
1009    /// How it comes in at its start.
1010    #[serde(default, skip_serializing_if = "Option::is_none")]
1011    pub enter: Option<Motion>,
1012    /// How it goes out, ending at its end.
1013    #[serde(default, skip_serializing_if = "Option::is_none")]
1014    pub exit: Option<Motion>,
1015}
1016
1017/// A closing card.
1018#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
1019#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1020#[serde(deny_unknown_fields)]
1021pub struct EndCard {
1022    /// The node's name, for other nodes to refer to.
1023    #[serde(default, skip_serializing_if = "Option::is_none")]
1024    pub id: Option<String>,
1025    /// The title.
1026    pub text: String,
1027    /// A smaller line under it.
1028    #[serde(default, skip_serializing_if = "Option::is_none")]
1029    pub subtitle: Option<String>,
1030    /// A picture above the title.
1031    #[serde(default, skip_serializing_if = "Option::is_none")]
1032    pub logo: Option<String>,
1033    /// The card's color. Default: the theme's surface.
1034    #[serde(default, skip_serializing_if = "Option::is_none")]
1035    pub background: Option<Rgba>,
1036    /// Seconds of fade in and out. Default: the theme's.
1037    #[serde(default, skip_serializing_if = "Option::is_none")]
1038    pub fade: Option<f64>,
1039    /// Seconds from the parent's start (an overlay's: the video's). Ignored in a sequence.
1040    #[serde(default, skip_serializing_if = "Option::is_none")]
1041    pub at: Option<f64>,
1042    /// Length in seconds. Default: the node's own length, else what its parent gives it.
1043    #[serde(default, skip_serializing_if = "Option::is_none")]
1044    pub duration: Option<f64>,
1045    /// How it comes in at its start.
1046    #[serde(default, skip_serializing_if = "Option::is_none")]
1047    pub enter: Option<Motion>,
1048    /// How it goes out, ending at its end.
1049    #[serde(default, skip_serializing_if = "Option::is_none")]
1050    pub exit: Option<Motion>,
1051}
1052
1053/// Clips in a labeled grid.
1054#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
1055#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1056#[serde(deny_unknown_fields)]
1057pub struct ComparisonGrid {
1058    /// The node's name, for other nodes to refer to.
1059    #[serde(default, skip_serializing_if = "Option::is_none")]
1060    pub id: Option<String>,
1061    /// The clips, row by row; each one's `label` names its cell.
1062    pub items: Vec<MediaItem>,
1063    /// Columns. Default: as square a grid as the items make.
1064    #[serde(default, skip_serializing_if = "Option::is_none")]
1065    pub columns: Option<u32>,
1066    /// Pixels between cells.
1067    #[serde(default)]
1068    pub gap: f32,
1069    /// How long the grid lasts when its clips differ. Default `shortest`.
1070    #[serde(default)]
1071    pub length: GridLength,
1072    /// Which item's sound plays, by index. Default: none.
1073    #[serde(default, skip_serializing_if = "Option::is_none")]
1074    pub audio_from: Option<u32>,
1075    /// Seconds from the parent's start (an overlay's: the video's). Ignored in a sequence.
1076    #[serde(default, skip_serializing_if = "Option::is_none")]
1077    pub at: Option<f64>,
1078    /// Length in seconds. Default: the node's own length, else what its parent gives it.
1079    #[serde(default, skip_serializing_if = "Option::is_none")]
1080    pub duration: Option<f64>,
1081}
1082
1083/// A video over a blurred copy of itself.
1084#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
1085#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1086#[serde(deny_unknown_fields)]
1087pub struct Reframe {
1088    /// The node's name, for other nodes to refer to. Its sound is the video's.
1089    #[serde(default, skip_serializing_if = "Option::is_none")]
1090    pub id: Option<String>,
1091    /// The video.
1092    pub video: MediaItem,
1093    /// The background's blur radius, as a fraction of its shorter side. Default 0.05.
1094    #[serde(default, skip_serializing_if = "Option::is_none")]
1095    pub blur: Option<f32>,
1096    /// When it starts in its parent; its length is the video's.
1097    #[serde(default, skip_serializing_if = "Option::is_none")]
1098    pub at: Option<f64>,
1099}
1100
1101/// A picture in a corner.
1102#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
1103#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1104#[serde(deny_unknown_fields)]
1105pub struct Logo {
1106    /// The node's name, for other nodes to refer to.
1107    #[serde(default, skip_serializing_if = "Option::is_none")]
1108    pub id: Option<String>,
1109    /// The picture.
1110    pub path: String,
1111    /// The corner. Default `top-right`.
1112    #[serde(default, skip_serializing_if = "Option::is_none")]
1113    pub corner: Option<Align>,
1114    /// Its longer side, in pixels. Default: 15% of the canvas's shorter side.
1115    #[serde(default, skip_serializing_if = "Option::is_none")]
1116    pub size: Option<f32>,
1117    /// Pixels from the edges. Default: the theme's margin.
1118    #[serde(default, skip_serializing_if = "Option::is_none")]
1119    pub margin: Option<f32>,
1120    /// Opacity, 0..=1. Default 1.
1121    #[serde(default, skip_serializing_if = "Option::is_none")]
1122    pub opacity: Option<f32>,
1123    /// Seconds from the parent's start (an overlay's: the video's). Ignored in a sequence.
1124    #[serde(default, skip_serializing_if = "Option::is_none")]
1125    pub at: Option<f64>,
1126    /// Length in seconds. Default: the node's own length, else what its parent gives it.
1127    #[serde(default, skip_serializing_if = "Option::is_none")]
1128    pub duration: Option<f64>,
1129    /// How it comes in at its start.
1130    #[serde(default, skip_serializing_if = "Option::is_none")]
1131    pub enter: Option<Motion>,
1132    /// How it goes out, ending at its end.
1133    #[serde(default, skip_serializing_if = "Option::is_none")]
1134    pub exit: Option<Motion>,
1135}
1136
1137/// Music under the video.
1138#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
1139#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1140#[serde(deny_unknown_fields)]
1141pub struct MusicBed {
1142    /// The node's name, for other nodes to refer to.
1143    #[serde(default, skip_serializing_if = "Option::is_none")]
1144    pub id: Option<String>,
1145    /// The file.
1146    pub path: String,
1147    /// Linear gain. Default 1.
1148    #[serde(default, skip_serializing_if = "Option::is_none")]
1149    pub volume: Option<f32>,
1150    /// Seconds of fade in. Default 1.
1151    #[serde(default, skip_serializing_if = "Option::is_none")]
1152    pub fade_in: Option<f64>,
1153    /// Seconds of fade out at the end. Default 2.
1154    #[serde(default, skip_serializing_if = "Option::is_none")]
1155    pub fade_out: Option<f64>,
1156    /// The `id` of a node (or `body`) whose sound it ducks under.
1157    #[serde(default, skip_serializing_if = "Option::is_none")]
1158    pub duck_under: Option<String>,
1159    /// How far ducking lowers it, in dB (negative). Default -12.
1160    #[serde(default, skip_serializing_if = "Option::is_none")]
1161    pub duck_depth_db: Option<f32>,
1162    /// Plays the file again from its start while the video lasts. Default true.
1163    #[serde(default = "yes")]
1164    pub r#loop: bool,
1165    /// Seconds from the parent's start (an overlay's: the video's). Ignored in a sequence.
1166    #[serde(default, skip_serializing_if = "Option::is_none")]
1167    pub at: Option<f64>,
1168    /// Length in seconds. Default: the node's own length, else what its parent gives it.
1169    #[serde(default, skip_serializing_if = "Option::is_none")]
1170    pub duration: Option<f64>,
1171}
1172
1173const fn yes() -> bool {
1174    true
1175}
1176
1177/// A timing curve: `linear`, a CSS name (`ease`, `ease-in`, `ease-out`, `ease-in-out`) or a
1178/// project easing object.
1179#[derive(Clone, Debug, PartialEq, Serialize)]
1180#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1181#[serde(untagged)]
1182pub enum EasingChoice {
1183    /// `linear`, `ease`, `ease-in`, `ease-out` or `ease-in-out`, with the CSS curves.
1184    Named(String),
1185    /// A project easing.
1186    Curve(Easing),
1187}
1188
1189/// An entrance or exit, timed from the node's own start or back from its own end.
1190#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
1191#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1192#[serde(deny_unknown_fields)]
1193pub struct Motion {
1194    /// What moves: `fade` (opacity alone), `rise` (a slide up), `slide` (toward `direction`) or
1195    /// `scale` (from `size` times its size). `rise`, `slide` and `scale` fade too unless `fade`
1196    /// is false.
1197    #[serde(rename = "type")]
1198    pub kind: MotionKind,
1199    /// Length in seconds. Default: the theme's fade length.
1200    #[serde(default, skip_serializing_if = "Option::is_none")]
1201    pub duration: Option<f64>,
1202    /// The timing curve. Default linear.
1203    #[serde(default, skip_serializing_if = "Option::is_none")]
1204    pub easing: Option<EasingChoice>,
1205    /// Which way a `slide` travels, entering into place and leaving out of it. Default `up`.
1206    #[serde(default, skip_serializing_if = "Option::is_none")]
1207    pub direction: Option<Direction>,
1208    /// How far a `rise` or `slide` travels, in pixels. Default: the theme's margin.
1209    #[serde(default, skip_serializing_if = "Option::is_none")]
1210    pub distance: Option<f32>,
1211    /// The size a `scale` enters from or exits to, as a multiple of its own. Default 0.
1212    #[serde(default, skip_serializing_if = "Option::is_none")]
1213    pub size: Option<f32>,
1214    /// Whether a `rise`, `slide` or `scale` also fades. Default true.
1215    #[serde(default, skip_serializing_if = "Option::is_none")]
1216    pub fade: Option<bool>,
1217}
1218
1219/// What an entrance or exit moves.
1220#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
1221#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1222#[serde(rename_all = "snake_case")]
1223pub enum MotionKind {
1224    /// Opacity alone.
1225    Fade,
1226    /// A slide up.
1227    Rise,
1228    /// A slide toward `direction`.
1229    Slide,
1230    /// A change of size about the node's own anchor.
1231    Scale,
1232}
1233
1234/// A direction of travel on the canvas.
1235#[derive(Clone, Copy, Debug, Default, Deserialize, Eq, PartialEq, Serialize)]
1236#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1237#[serde(rename_all = "snake_case")]
1238pub enum Direction {
1239    /// Toward the top.
1240    #[default]
1241    Up,
1242    /// Toward the bottom.
1243    Down,
1244    /// Toward the left.
1245    Left,
1246    /// Toward the right.
1247    Right,
1248}
1249
1250/// A text revealed a unit at a time. Each unit moves at a constant rate over `duration`.
1251#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
1252#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1253#[serde(deny_unknown_fields)]
1254pub struct Reveal {
1255    /// The unit: `word` (words holding a letter or digit) or `line` (laid-out lines).
1256    pub by: RevealUnit,
1257    /// How each unit appears: `fade`, or `rise` (fading in while moving up). Default `fade`.
1258    #[serde(rename = "type", default, skip_serializing_if = "Option::is_none")]
1259    pub kind: Option<RevealKind>,
1260    /// Seconds between one unit's start and the next's. Default half the duration.
1261    #[serde(default, skip_serializing_if = "Option::is_none")]
1262    pub stagger: Option<f64>,
1263    /// Seconds each unit takes. Default: the theme's fade length.
1264    #[serde(default, skip_serializing_if = "Option::is_none")]
1265    pub duration: Option<f64>,
1266    /// How far a `rise` moves, in pixels. Default half the font size.
1267    #[serde(default, skip_serializing_if = "Option::is_none")]
1268    pub distance: Option<f32>,
1269}
1270
1271/// The unit a text is revealed by.
1272#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
1273#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1274#[serde(rename_all = "snake_case")]
1275pub enum RevealUnit {
1276    /// Words that hold a letter or digit; other characters show throughout.
1277    Word,
1278    /// Laid-out lines.
1279    Line,
1280}
1281
1282/// How a revealed unit appears.
1283#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
1284#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1285#[serde(rename_all = "snake_case")]
1286pub enum RevealKind {
1287    /// Opacity alone.
1288    Fade,
1289    /// Fading in while moving up.
1290    Rise,
1291}
1292
1293/// A number counting from `start` to `end`, shown as text that changes on the frame grid.
1294#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
1295#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1296#[serde(deny_unknown_fields)]
1297pub struct Counter {
1298    /// The node's name, for other nodes to refer to.
1299    #[serde(default, skip_serializing_if = "Option::is_none")]
1300    pub id: Option<String>,
1301    /// The value it counts from.
1302    pub start: f64,
1303    /// The value it counts to.
1304    pub end: f64,
1305    /// Seconds the count takes from the node's start; it then holds `end`. Default: the node's
1306    /// whole length.
1307    #[serde(default, skip_serializing_if = "Option::is_none")]
1308    pub count_duration: Option<f64>,
1309    /// The timing curve of the count. Default linear.
1310    #[serde(default, skip_serializing_if = "Option::is_none")]
1311    pub easing: Option<EasingChoice>,
1312    /// How the number is written.
1313    #[serde(default, skip_serializing_if = "Option::is_none")]
1314    pub format: Option<NumberFormat>,
1315    /// The theme style it starts from. Default `title`.
1316    #[serde(default, skip_serializing_if = "Option::is_none")]
1317    pub style: Option<TextRole>,
1318    /// Font size in pixels. Default: the style's.
1319    #[serde(default, skip_serializing_if = "Option::is_none")]
1320    pub font_size: Option<f32>,
1321    /// Family. Default: the theme's.
1322    #[serde(default, skip_serializing_if = "Option::is_none")]
1323    pub font_family: Option<String>,
1324    /// Color. Default: the theme's text color.
1325    #[serde(default, skip_serializing_if = "Option::is_none")]
1326    pub color: Option<Rgba>,
1327    /// Bands around the glyphs, as a text node's `strokes`.
1328    #[serde(default, skip_serializing_if = "Vec::is_empty")]
1329    pub strokes: Vec<TextStroke>,
1330    /// Shadows of the glyphs, as a text node's `shadows`.
1331    #[serde(default, skip_serializing_if = "Vec::is_empty")]
1332    pub shadows: Vec<TextShadow>,
1333    /// Opacity, 0..=1. Default 1.
1334    #[serde(default, skip_serializing_if = "Option::is_none")]
1335    pub opacity: Option<f32>,
1336    /// Seconds from the parent's start (an overlay's: the video's). Ignored in a sequence.
1337    #[serde(default, skip_serializing_if = "Option::is_none")]
1338    pub at: Option<f64>,
1339    /// Length in seconds. Default: the node's own length, else what its parent gives it.
1340    #[serde(default, skip_serializing_if = "Option::is_none")]
1341    pub duration: Option<f64>,
1342    /// Pixels kept clear inside the parent's area: one number, or `[top, right, bottom, left]`.
1343    #[serde(default, skip_serializing_if = "Option::is_none")]
1344    pub inset: Option<Inset>,
1345    /// Where in its area the number lies. Default `center`.
1346    #[serde(default, skip_serializing_if = "Option::is_none")]
1347    pub align: Option<Align>,
1348    /// How it comes in at its start.
1349    #[serde(default, skip_serializing_if = "Option::is_none")]
1350    pub enter: Option<Motion>,
1351    /// How it goes out, ending at its end.
1352    #[serde(default, skip_serializing_if = "Option::is_none")]
1353    pub exit: Option<Motion>,
1354}
1355
1356/// How a counter writes its number.
1357#[derive(Clone, Debug, Default, Deserialize, PartialEq, Serialize)]
1358#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1359#[serde(deny_unknown_fields)]
1360pub struct NumberFormat {
1361    /// Digits after the decimal point, at most 6. Default 0.
1362    #[serde(default, skip_serializing_if = "Option::is_none")]
1363    pub decimals: Option<u32>,
1364    /// Put between each group of three integer digits, as `,`. Default none.
1365    #[serde(default, skip_serializing_if = "Option::is_none")]
1366    pub thousands: Option<String>,
1367    /// The decimal point. Default `.`.
1368    #[serde(default, skip_serializing_if = "Option::is_none")]
1369    pub point: Option<String>,
1370    /// Text before the number.
1371    #[serde(default, skip_serializing_if = "Option::is_none")]
1372    pub prefix: Option<String>,
1373    /// Text after the number.
1374    #[serde(default, skip_serializing_if = "Option::is_none")]
1375    pub suffix: Option<String>,
1376}