use super::Scene;
use crate::{
Color, Duration, Error, FrameRate, Node, NodeModifiers, SceneContext, Shape, Size,
TICKS_PER_SECOND, Time,
};
use std::{fmt, sync::Arc};
mod builder;
mod diagnostics;
mod resolved;
mod soundtrack;
#[cfg(test)]
mod tests;
mod track;
pub use builder::TimelineBuilder;
pub use diagnostics::{BoundaryAdjustment, BoundaryEdge, Diagnostics, TimelineWarning};
pub use resolved::ResolvedAudio;
pub use soundtrack::Soundtrack;
pub use track::{Clip, Track, TrackBuilder};
const MAX_TRACKS: usize = 256;
const MAX_CLIPS: usize = 16_384;
const MAX_ADJUSTMENTS: usize = 65_536;
fn invalid(message: impl Into<String>) -> Error {
Error::Timeline {
timeline: "<native>".into(),
message: message.into(),
}
}
fn on_track(track: usize, message: impl fmt::Display) -> Error {
invalid(format!("track[{track}]: {message}"))
}
fn add_time(first: Time, second: Duration) -> crate::Result<Time> {
first
.checked_add(second)
.ok_or_else(|| invalid("Scheduled time exceeds signed ticks"))
}
fn align(time: Time, rate: FrameRate) -> crate::Result<(Time, i64)> {
let quantum = i128::from(TICKS_PER_SECOND) * i128::from(rate.denominator());
let numerator = i128::from(rate.numerator());
let lower = (i128::from(time.ticks()) * numerator).div_euclid(quantum);
let boundary = |frame: i128| {
let ticks = frame * quantum;
ticks.div_euclid(numerator) + i128::from(ticks.rem_euclid(numerator) != 0)
};
let left = boundary(lower);
let right = boundary(lower + 1);
let frame = if i128::from(time.ticks()) - left < right - i128::from(time.ticks()) {
lower
} else {
lower + 1
};
let ticks = i64::try_from(boundary(frame))
.map_err(|_| invalid("Nearest frame boundary exceeds signed ticks"))?;
let frame =
i64::try_from(frame).map_err(|_| invalid("Nearest frame index exceeds signed range"))?;
Ok((Time::from_ticks(ticks), frame))
}
#[derive(Clone, Debug)]
pub struct Timeline {
size: Size,
rate: FrameRate,
background: Option<Color>,
tracks: Arc<Vec<Track>>,
duration: Duration,
inferred_duration: bool,
diagnostics: Diagnostics,
soundtrack: Soundtrack,
}
impl Timeline {
#[allow(clippy::new_ret_no_self)]
pub fn new(size: Size, rate: FrameRate) -> TimelineBuilder {
match TimelineBuilder::checked(size, rate) {
Ok(builder) => builder,
Err(error) => TimelineBuilder::empty(Size::new(1.0, 1.0), rate).failed(error),
}
}
pub fn try_new(size: Size, rate: FrameRate) -> crate::Result<TimelineBuilder> {
TimelineBuilder::checked(size, rate)
}
pub fn to_builder(&self) -> TimelineBuilder {
TimelineBuilder::from_timeline(self)
}
pub fn size(&self) -> Size {
self.size
}
pub fn frame_rate(&self) -> FrameRate {
self.rate
}
pub fn background(&self) -> Option<Color> {
self.background
}
pub fn duration(&self) -> Duration {
self.duration
}
pub fn frame_count(&self) -> u64 {
self.duration.to_frame(self.rate)
}
pub fn tracks(&self) -> &[Track] {
&self.tracks
}
pub fn soundtrack(&self) -> &Soundtrack {
&self.soundtrack
}
pub fn diagnostics(&self) -> &Diagnostics {
&self.diagnostics
}
pub fn resolve_audio(
&self,
mut resolve: impl FnMut(&crate::AudioClip) -> crate::Result<crate::AudioClip>,
) -> crate::Result<ResolvedAudio> {
let clips = self
.soundtrack
.clips
.iter()
.map(&mut resolve)
.collect::<crate::Result<Vec<_>>>()?;
let mut duration = self.duration;
if self.inferred_duration {
for clip in &clips {
duration = duration.max(self.soundtrack.clip_end(clip, self.rate)?);
}
}
Ok(ResolvedAudio { clips, duration })
}
pub fn visual(&self) -> TimelineVisual {
TimelineVisual(self.clone())
}
pub fn render(&self, context: &SceneContext<'_>) -> Node {
self.render_until(context, self.duration)
}
pub fn render_until(&self, context: &SceneContext<'_>, end: Duration) -> Node {
if context.time() < Time::ZERO || context.time() >= end.after_origin() {
return Node::empty();
}
let parent_size = context.size();
let nested = context.is_nested_scene();
let context = context.for_timeline(self.size, self.rate);
let background = self
.background
.map(|color| Node::from(self.size.to_rect().fill(color)));
let clips = self
.tracks
.iter()
.filter_map(|track| track.render(&context));
let node = Node::group(background.into_iter().chain(clips));
if parent_size == self.size {
return if nested {
node.clip_with(self.size.to_rect(), crate::ClipMode::Geometric)
} else {
node
};
}
let scale =
(parent_size.width / self.size.width).min(parent_size.height / self.size.height);
let scale = if scale.is_finite() && scale > 0.0 {
scale
} else {
f64::NAN
};
node.clip_with(self.size.to_rect(), crate::ClipMode::Geometric)
.pivot(crate::Pivot::Point(crate::Point::ORIGIN))
.scale(scale)
.translate(
(parent_size.width - self.size.width * scale) * 0.5,
(parent_size.height - self.size.height * scale) * 0.5,
)
}
}
#[derive(Clone, Debug)]
pub struct TimelineVisual(Timeline);
impl Scene for TimelineVisual {
fn render(&self, context: &SceneContext<'_>) -> Node {
self.0.render(context)
}
fn duration(&self) -> Option<Duration> {
Some(self.0.duration)
}
}