Skip to main content

alpha2_model/
timeline.rs

1use crate::{
2    AudioProperties, Captions, ClipBlendMode, ClipComposition, ClipEffect, ClipId, Easing, Matte,
3    MediaKind, ShapeMask, TimeMap, TrackAudio, TrackId, VisualProperties,
4};
5use alpha2_media::SourceId;
6use alpha2_time::TimeRange;
7use serde::{Deserialize, Serialize};
8
9/// Timeline tracks ordered from back to front for visual tracks.
10#[derive(Clone, Debug, Default, Deserialize, PartialEq, Serialize)]
11#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
12#[serde(deny_unknown_fields)]
13pub struct Timeline {
14    /// Tracks in composition order.
15    pub tracks: Vec<Track>,
16}
17
18impl Timeline {
19    /// The smallest interval holding everything the timeline shows: every
20    /// clip, and every caption cue, which a caption track holds instead of
21    /// clips. `None` for a timeline that shows nothing. What a whole-project
22    /// render or export covers.
23    #[must_use]
24    pub fn extent(&self) -> Option<TimeRange> {
25        let mut ranges = self.tracks.iter().flat_map(|track| {
26            track.clips.iter().map(|clip| clip.timeline_range).chain(
27                track
28                    .captions
29                    .iter()
30                    .flat_map(|captions| captions.cues.iter().map(|cue| cue.timeline_range)),
31            )
32        });
33        let first = ranges.next()?;
34        let (start, end) = ranges.fold((first.start(), first.end()), |(start, end), range| {
35            (start.min(range.start()), end.max(range.end()))
36        });
37        TimeRange::new(start, end).ok()
38    }
39
40    /// Whether any track solos its bus, which silences every bus that does
41    /// not ([`TrackAudio::is_silenced`]).
42    #[must_use]
43    pub fn any_solo(&self) -> bool {
44        self.tracks.iter().any(|track| track.audio.solo)
45    }
46
47    /// The clips that are another clip's matte. Such a clip is drawn only
48    /// into the mattes that name it, never composited itself, wherever on
49    /// the timeline either clip is.
50    #[must_use]
51    pub fn matte_sources(&self) -> std::collections::HashSet<&ClipId> {
52        self.tracks
53            .iter()
54            .flat_map(|track| &track.clips)
55            .filter_map(|clip| clip.matte.as_ref().map(|matte| &matte.source))
56            .collect()
57    }
58}
59
60/// A homogeneous visual or audio timeline lane.
61#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
62#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
63#[serde(deny_unknown_fields)]
64#[non_exhaustive]
65pub struct Track {
66    /// Stable track identity.
67    pub id: TrackId,
68    /// User-visible track name.
69    pub name: String,
70    /// Controls source compatibility and evaluation behavior.
71    pub kind: TrackKind,
72    /// Clips on this track. Ordering is authoring order; evaluation sorts by time.
73    /// A caption track has none.
74    #[serde(default)]
75    pub clips: Vec<Clip>,
76    /// Explicit transitions between overlapping clips on this track.
77    #[serde(default)]
78    pub transitions: Vec<ClipTransition>,
79    /// Bus controls applied to this track's mixed audio.
80    #[serde(default)]
81    pub audio: TrackAudio,
82    /// The cues and style of a caption track; present exactly for that kind.
83    #[serde(default)]
84    pub captions: Option<Captions>,
85}
86
87impl Track {
88    /// Creates an empty track with default bus controls.
89    #[must_use]
90    pub fn new(id: TrackId, name: impl Into<String>, kind: TrackKind) -> Self {
91        Self {
92            id,
93            name: name.into(),
94            kind,
95            clips: Vec::new(),
96            transitions: Vec::new(),
97            audio: TrackAudio::default(),
98            captions: (kind == TrackKind::Caption).then(Captions::default),
99        }
100    }
101}
102
103/// The media domain accepted by a track.
104#[derive(Copy, Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
105#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
106#[serde(rename_all = "snake_case")]
107#[non_exhaustive]
108pub enum TrackKind {
109    /// A composited visual track.
110    Video,
111    /// A mixed audio track.
112    Audio,
113    /// Timed text, drawn over the tracks before it and under the ones after.
114    Caption,
115}
116
117impl TrackKind {
118    pub(crate) const fn accepts(self, source: MediaKind) -> bool {
119        matches!(
120            (self, source),
121            (
122                Self::Video,
123                MediaKind::Video | MediaKind::Image | MediaKind::Generated | MediaKind::Composition
124            ) | (Self::Audio, MediaKind::Audio)
125        )
126    }
127}
128
129/// A reference to a source over independent timeline and source ranges.
130#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
131#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
132#[serde(deny_unknown_fields)]
133#[non_exhaustive]
134pub struct Clip {
135    /// Stable clip identity.
136    pub id: ClipId,
137    /// Source consumed by this clip.
138    pub source_id: SourceId,
139    /// Destination interval on the project timeline.
140    pub timeline_range: TimeRange,
141    /// Interval read from the source timeline.
142    pub source_range: TimeRange,
143    /// Resolution-independent visual presentation controls.
144    #[serde(default)]
145    pub visual: VisualProperties,
146    /// The composition graph that builds the clip's picture from its source,
147    /// with the clip's time mapping onto it. Without one the clip shows its
148    /// source as it is. Visual clips only.
149    #[serde(default)]
150    pub composition: Option<ClipComposition>,
151    /// Artistic operation used when this clip is composited over earlier layers.
152    #[serde(default)]
153    pub blend_mode: ClipBlendMode,
154    /// Shapes that limit where the clip is visible, applied in order after
155    /// its composition and before its transform. At most
156    /// [`crate::MAX_CLIP_MASKS`]; visual clips only.
157    #[serde(default)]
158    pub masks: Vec<ShapeMask>,
159    /// Another visual clip whose picture sets where this clip is visible.
160    #[serde(default)]
161    pub matte: Option<Matte>,
162    /// How timeline time maps onto the source range.
163    #[serde(default)]
164    pub time_map: TimeMap,
165    /// Presentation controls applied to this clip's audio.
166    #[serde(default)]
167    pub audio: AudioProperties,
168    /// Whether a runtime may keep and reuse the clip's rendered picture.
169    /// Never changes what renders: a kept picture is the one a live render
170    /// draws.
171    #[serde(default, skip_serializing_if = "CacheIntent::is_auto")]
172    pub cache: CacheIntent,
173}
174
175/// Whether a runtime may keep and reuse a clip's rendered picture.
176#[derive(Copy, Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)]
177#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
178#[serde(rename_all = "snake_case")]
179pub enum CacheIntent {
180    /// The runtime decides, keeping pictures whose inputs it can identify.
181    #[default]
182    Auto,
183    /// Keep it ahead of other pictures, and bake its range when a host bakes
184    /// render caches.
185    Pin,
186    /// Always render it live: nothing it draws is read from or kept in a
187    /// cache.
188    Bypass,
189}
190
191impl CacheIntent {
192    /// Whether it is [`CacheIntent::Auto`].
193    #[must_use]
194    pub const fn is_auto(&self) -> bool {
195        matches!(self, Self::Auto)
196    }
197}
198
199impl Clip {
200    /// Creates a clip with default visual, blend, and audio controls and no
201    /// composition.
202    #[must_use]
203    pub fn new(
204        id: ClipId,
205        source_id: SourceId,
206        timeline_range: TimeRange,
207        source_range: TimeRange,
208    ) -> Self {
209        Self {
210            id,
211            source_id,
212            timeline_range,
213            source_range,
214            visual: VisualProperties::default(),
215            composition: None,
216            blend_mode: ClipBlendMode::default(),
217            masks: Vec::new(),
218            matte: None,
219            time_map: TimeMap::Forward,
220            audio: AudioProperties::default(),
221            cache: CacheIntent::Auto,
222        }
223    }
224}
225
226/// A two-input transition over the overlap of two visual clips.
227#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
228#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
229#[serde(deny_unknown_fields)]
230pub struct ClipTransition {
231    /// Clip sampled as transition progress zero.
232    pub from_clip: ClipId,
233    /// Clip sampled as transition progress one.
234    pub to_clip: ClipId,
235    /// Half-open timeline interval over which both clips must be active.
236    pub timeline_range: TimeRange,
237    /// Timing curve applied to the transition's linear progress. The eased
238    /// progress is clamped to `0..=1`, the domain transition shaders define.
239    pub easing: Easing,
240    /// Versioned transition shader and named parameter values.
241    pub effect: ClipEffect,
242}