motion_core/authoring/
timeline.rs1use super::Scene;
30use crate::{
31 Color, Duration, Error, FrameRate, Node, NodeModifiers, SceneContext, Shape, Size,
32 TICKS_PER_SECOND, Time,
33};
34use std::{fmt, sync::Arc};
35
36mod builder;
37mod diagnostics;
38mod resolved;
39mod soundtrack;
40#[cfg(test)]
41mod tests;
42mod track;
43
44pub use builder::TimelineBuilder;
45pub use diagnostics::{BoundaryAdjustment, BoundaryEdge, Diagnostics, TimelineWarning};
46pub use resolved::ResolvedAudio;
47pub use soundtrack::Soundtrack;
48pub use track::{Clip, Track, TrackBuilder};
49
50const MAX_TRACKS: usize = 256;
51
52const MAX_CLIPS: usize = 16_384;
53
54const MAX_ADJUSTMENTS: usize = 65_536;
55
56fn invalid(message: impl Into<String>) -> Error {
57 Error::Timeline {
58 timeline: "<native>".into(),
59 message: message.into(),
60 }
61}
62
63fn on_track(track: usize, message: impl fmt::Display) -> Error {
64 invalid(format!("track[{track}]: {message}"))
65}
66
67fn add_time(first: Time, second: Duration) -> crate::Result<Time> {
68 first
69 .checked_add(second)
70 .ok_or_else(|| invalid("Scheduled time exceeds signed ticks"))
71}
72
73fn align(time: Time, rate: FrameRate) -> crate::Result<(Time, i64)> {
74 let quantum = i128::from(TICKS_PER_SECOND) * i128::from(rate.denominator());
75 let numerator = i128::from(rate.numerator());
76 let lower = (i128::from(time.ticks()) * numerator).div_euclid(quantum);
77 let boundary = |frame: i128| {
78 let ticks = frame * quantum;
79 ticks.div_euclid(numerator) + i128::from(ticks.rem_euclid(numerator) != 0)
80 };
81 let left = boundary(lower);
82 let right = boundary(lower + 1);
83 let frame = if i128::from(time.ticks()) - left < right - i128::from(time.ticks()) {
84 lower
85 } else {
86 lower + 1
87 };
88 let ticks = i64::try_from(boundary(frame))
89 .map_err(|_| invalid("Nearest frame boundary exceeds signed ticks"))?;
90 let frame =
91 i64::try_from(frame).map_err(|_| invalid("Nearest frame index exceeds signed range"))?;
92 Ok((Time::from_ticks(ticks), frame))
93}
94
95#[derive(Clone, Debug)]
104pub struct Timeline {
105 size: Size,
106 rate: FrameRate,
107 background: Option<Color>,
108 tracks: Arc<Vec<Track>>,
109 duration: Duration,
110 inferred_duration: bool,
111 diagnostics: Diagnostics,
112 soundtrack: Soundtrack,
113}
114
115impl Timeline {
116 #[allow(clippy::new_ret_no_self)]
118 pub fn new(size: Size, rate: FrameRate) -> TimelineBuilder {
119 match TimelineBuilder::checked(size, rate) {
120 Ok(builder) => builder,
121 Err(error) => TimelineBuilder::empty(Size::new(1.0, 1.0), rate).failed(error),
122 }
123 }
124
125 pub fn try_new(size: Size, rate: FrameRate) -> crate::Result<TimelineBuilder> {
128 TimelineBuilder::checked(size, rate)
129 }
130
131 pub fn to_builder(&self) -> TimelineBuilder {
134 TimelineBuilder::from_timeline(self)
135 }
136
137 pub fn size(&self) -> Size {
139 self.size
140 }
141
142 pub fn frame_rate(&self) -> FrameRate {
144 self.rate
145 }
146
147 pub fn background(&self) -> Option<Color> {
149 self.background
150 }
151
152 pub fn duration(&self) -> Duration {
155 self.duration
156 }
157
158 pub fn frame_count(&self) -> u64 {
160 self.duration.to_frame(self.rate)
161 }
162
163 pub fn tracks(&self) -> &[Track] {
165 &self.tracks
166 }
167
168 pub fn soundtrack(&self) -> &Soundtrack {
170 &self.soundtrack
171 }
172
173 pub fn diagnostics(&self) -> &Diagnostics {
175 &self.diagnostics
176 }
177
178 pub fn resolve_audio(
182 &self,
183 mut resolve: impl FnMut(&crate::AudioClip) -> crate::Result<crate::AudioClip>,
184 ) -> crate::Result<ResolvedAudio> {
185 let clips = self
186 .soundtrack
187 .clips
188 .iter()
189 .map(&mut resolve)
190 .collect::<crate::Result<Vec<_>>>()?;
191 let mut duration = self.duration;
192 if self.inferred_duration {
193 for clip in &clips {
194 duration = duration.max(self.soundtrack.clip_end(clip, self.rate)?);
195 }
196 }
197 Ok(ResolvedAudio { clips, duration })
198 }
199
200 pub fn visual(&self) -> TimelineVisual {
203 TimelineVisual(self.clone())
204 }
205
206 pub fn render(&self, context: &SceneContext<'_>) -> Node {
209 self.render_until(context, self.duration)
210 }
211
212 pub fn render_until(&self, context: &SceneContext<'_>, end: Duration) -> Node {
215 if context.time() < Time::ZERO || context.time() >= end.after_origin() {
216 return Node::empty();
217 }
218 let parent_size = context.size();
219 let nested = context.is_nested_scene();
220 let context = context.for_timeline(self.size, self.rate);
221 let background = self
222 .background
223 .map(|color| Node::from(self.size.to_rect().fill(color)));
224 let clips = self
225 .tracks
226 .iter()
227 .filter_map(|track| track.render(&context));
228 let node = Node::group(background.into_iter().chain(clips));
229 if parent_size == self.size {
230 return if nested {
231 node.clip_with(self.size.to_rect(), crate::ClipMode::Geometric)
232 } else {
233 node
234 };
235 }
236 let scale =
237 (parent_size.width / self.size.width).min(parent_size.height / self.size.height);
238 let scale = if scale.is_finite() && scale > 0.0 {
239 scale
240 } else {
241 f64::NAN
242 };
243 node.clip_with(self.size.to_rect(), crate::ClipMode::Geometric)
244 .pivot(crate::Pivot::Point(crate::Point::ORIGIN))
245 .scale(scale)
246 .translate(
247 (parent_size.width - self.size.width * scale) * 0.5,
248 (parent_size.height - self.size.height * scale) * 0.5,
249 )
250 }
251}
252
253#[derive(Clone, Debug)]
258pub struct TimelineVisual(Timeline);
259
260impl Scene for TimelineVisual {
261 fn render(&self, context: &SceneContext<'_>) -> Node {
262 self.0.render(context)
263 }
264
265 fn duration(&self) -> Option<Duration> {
266 Some(self.0.duration)
267 }
268}