Skip to main content

alpha2_compose/
spec.rs

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