use super::{BoundaryAdjustment, BoundaryEdge, MAX_CLIPS, Scene, add_time, align, on_track};
use crate::ErrorReport;
use crate::authoring::{scene, transition};
use crate::{AudioClip, Duration, FrameRate, Node, SceneContext, Time, Timeline};
use std::{fmt, sync::Arc};
#[derive(Clone)]
pub struct Clip {
pub(super) id: usize,
pub(super) start: Time,
pub(super) duration: Duration,
pub(super) offset: Duration,
pub(super) scene: Arc<dyn Scene>,
pub(super) transition_in: Option<TransitionIn>,
}
#[derive(Clone)]
pub(super) struct TransitionIn {
pub(super) effect: Arc<dyn transition::Transition>,
pub(super) duration: Duration,
}
impl fmt::Debug for Clip {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
formatter
.debug_struct("Clip")
.field("id", &self.id)
.field("start", &self.start)
.field("duration", &self.duration)
.finish_non_exhaustive()
}
}
impl Clip {
pub fn id(&self) -> usize {
self.id
}
pub fn start(&self) -> Time {
self.start
}
pub fn duration(&self) -> Duration {
self.duration
}
pub fn offset(&self) -> Duration {
self.offset
}
pub fn end(&self) -> Time {
self.start + self.duration
}
pub fn scene(&self) -> &dyn Scene {
self.scene.as_ref()
}
pub fn transition_in(&self) -> Option<(&dyn transition::Transition, Duration)> {
self.transition_in
.as_ref()
.map(|value| (value.effect.as_ref(), value.duration))
}
}
#[derive(Clone, Debug)]
pub struct Track {
pub(super) clips: Arc<[Clip]>,
pub(super) duration: Duration,
}
impl Track {
pub fn clips(&self) -> &[Clip] {
&self.clips
}
pub fn duration(&self) -> Duration {
self.duration
}
pub fn active_clip(&self, time: Time) -> Option<&Clip> {
let index = self
.clips
.partition_point(|clip| clip.start <= time)
.checked_sub(1)?;
self.clips.get(index).filter(|clip| time < clip.end())
}
pub fn render(&self, context: &SceneContext<'_>) -> Option<Node> {
let index = self
.clips
.partition_point(|clip| clip.start <= context.time())
.checked_sub(1)?;
let clip = &self.clips[index];
if context.time() >= clip.end() {
return None;
}
let render = |clip: &Clip| {
context.during(clip.start, clip.duration, |local| {
if clip.offset == Duration::ZERO {
clip.scene.render(local)
} else {
clip.scene
.render(&local.shifted((Time::ZERO - clip.offset).since_origin()))
}
})
};
let node = if let Some(transition) = clip
.transition_in
.as_ref()
.filter(|transition| context.time() < clip.start + transition.duration)
{
let outgoing = render(&self.clips[index - 1]);
let incoming = render(clip);
let animation = crate::Tween::new(0.0f64, 1.0, transition.duration)
.easing(transition.effect.easing());
let progress =
crate::Signal::sample(&animation, context.time() - clip.start.since_origin());
if progress.is_finite() {
let local = context
.shifted(clip.start.since_origin())
.with_clip_duration(transition.duration);
transition
.effect
.compose(progress.clamp(0.0, 1.0), outgoing, incoming, &local)
} else {
transition::failure("easing", "Transition easing must produce finite progress")
}
} else {
render(clip)
};
Some(Node::group([node]))
}
}
pub struct TrackBuilder {
pub(super) index: usize,
pub(super) rate: FrameRate,
pub(super) clips: Vec<Clip>,
pub(super) cursor: Time,
pub(super) adjustments: Vec<BoundaryAdjustment>,
pub(super) pending: Option<Arc<dyn transition::Transition>>,
pub(super) errors: ErrorReport,
pub(super) audio: Vec<AudioClip>,
}
impl TrackBuilder {
fn defer(mut self, operation: impl FnOnce(&mut Self) -> crate::Result<()>) -> Self {
if let Err(error) = operation(&mut self) {
self.errors.record(on_track(
self.index,
format!("clip[{}]: {error}", self.clips.len()),
));
}
self
}
pub fn push(self, scene: impl Scene + 'static) -> Self {
let cursor = self.cursor;
self.defer(|builder| builder.place_checked(cursor, None, Duration::ZERO, scene))
}
pub fn scene<Output: scene::IntoSceneOutput + 'static>(
self,
duration: Duration,
build: impl Fn(&SceneContext<'_>) -> Output + Send + Sync + 'static,
) -> Self {
self.push(scene::scene(build).with_duration(duration))
}
pub fn push_for(self, duration: Duration, scene: impl Scene + 'static) -> Self {
let cursor = self.cursor;
self.defer(|builder| builder.place_checked(cursor, Some(duration), Duration::ZERO, scene))
}
pub fn place_for(self, start: Time, duration: Duration, scene: impl Scene + 'static) -> Self {
self.defer(|builder| builder.place_checked(start, Some(duration), Duration::ZERO, scene))
}
pub fn push_trimmed(
self,
offset: Duration,
duration: Duration,
scene: impl Scene + 'static,
) -> Self {
let cursor = self.cursor;
self.defer(|builder| builder.place_checked(cursor, Some(duration), offset, scene))
}
pub fn try_push_for(
mut self,
duration: Duration,
scene: impl Scene + 'static,
) -> crate::Result<Self> {
let cursor = self.cursor;
self.place_checked(cursor, Some(duration), Duration::ZERO, scene)?;
Ok(self)
}
pub fn try_place_for(
mut self,
start: Time,
duration: Duration,
scene: impl Scene + 'static,
) -> crate::Result<Self> {
self.place_checked(start, Some(duration), Duration::ZERO, scene)?;
Ok(self)
}
pub fn try_push_trimmed(
mut self,
offset: Duration,
duration: Duration,
scene: impl Scene + 'static,
) -> crate::Result<Self> {
let cursor = self.cursor;
self.place_checked(cursor, Some(duration), offset, scene)?;
Ok(self)
}
pub fn nest(self, child: Timeline) -> Self {
let cursor = self.cursor;
self.defer(|builder| builder.nest_checked(cursor, None, child))
}
pub fn nest_for(self, duration: Duration, child: Timeline) -> Self {
let cursor = self.cursor;
self.defer(|builder| builder.nest_checked(cursor, Some(duration), child))
}
pub fn place_timeline(self, start: Time, child: Timeline) -> Self {
self.defer(|builder| builder.nest_checked(start, None, child))
}
pub fn try_nest(mut self, child: Timeline) -> crate::Result<Self> {
let cursor = self.cursor;
self.nest_checked(cursor, None, child)?;
Ok(self)
}
pub fn try_nest_for(mut self, duration: Duration, child: Timeline) -> crate::Result<Self> {
let cursor = self.cursor;
self.nest_checked(cursor, Some(duration), child)?;
Ok(self)
}
pub fn try_place_timeline(mut self, start: Time, child: Timeline) -> crate::Result<Self> {
self.nest_checked(start, None, child)?;
Ok(self)
}
fn nest_checked(
&mut self,
start: Time,
duration: Option<Duration>,
child: Timeline,
) -> crate::Result<()> {
let id = self.clips.len();
match duration {
Some(duration) => {
self.place_checked(start, Some(duration), Duration::ZERO, child.visual())?
}
None => self.place_checked(start, None, Duration::ZERO, child.visual())?,
}
let clip = self
.clips
.iter()
.find(|clip| clip.id == id)
.ok_or_else(|| on_track(self.index, "Nested clip was not placed"))?;
let (start, duration) = (clip.start, clip.duration);
let soundtrack = child.soundtrack();
let sources =
if soundtrack.master() != crate::Master::default() || !soundtrack.buses().is_empty() {
vec![AudioClip::composition(child.clone()).duration(duration)]
} else {
soundtrack.clips().to_vec()
};
for source in sources {
if let Some(source) = source.relocated(start.since_origin().magnitude(), duration)? {
self.audio.push(source);
}
}
Ok(())
}
pub fn transition(
self,
effect: impl transition::Transition + 'static,
duration: Duration,
) -> Self {
self.defer(|builder| builder.transition_checked(effect, duration))
}
pub fn place(self, start: Time, scene: impl Scene + 'static) -> Self {
self.defer(|builder| builder.place_checked(start, None, Duration::ZERO, scene))
}
pub fn gap(self, duration: Duration) -> Self {
self.defer(|builder| builder.gap_checked(duration))
}
pub fn try_transition(
mut self,
effect: impl transition::Transition + 'static,
duration: Duration,
) -> crate::Result<Self> {
self.transition_checked(effect, duration)?;
Ok(self)
}
pub fn try_place(mut self, start: Time, scene: impl Scene + 'static) -> crate::Result<Self> {
self.place_checked(start, None, Duration::ZERO, scene)?;
Ok(self)
}
pub fn try_gap(mut self, duration: Duration) -> crate::Result<Self> {
self.gap_checked(duration)?;
Ok(self)
}
pub fn try_push(self, scene: impl Scene + 'static) -> crate::Result<Self> {
let cursor = self.cursor;
self.try_place(cursor, scene)
}
fn transition_checked(
&mut self,
effect: impl transition::Transition + 'static,
duration: Duration,
) -> crate::Result<()> {
if self.pending.is_some() || duration <= Duration::ZERO {
return Err(on_track(
self.index,
"Transition needs positive duration and no pending transition",
));
}
let previous = self
.clips
.last()
.ok_or_else(|| on_track(self.index, "Transition requires a preceding clip"))?;
if align(self.cursor, self.rate)?.0 != previous.end() {
return Err(on_track(self.index, "Transition cannot follow a gap"));
}
let start = self
.cursor
.checked_sub(duration)
.filter(|time| *time >= Time::ZERO)
.ok_or_else(|| on_track(self.index, "Transition exceeds the preceding clip"))?;
let aligned = align(start, self.rate)?.0;
if aligned < previous.start
|| aligned >= previous.end()
|| previous
.transition_in
.as_ref()
.is_some_and(|incoming| aligned < previous.start + incoming.duration)
{
return Err(on_track(
self.index,
"Transition would be empty or overlap another transition",
));
}
self.cursor = start;
self.pending = Some(Arc::new(effect));
Ok(())
}
fn place_checked(
&mut self,
start: Time,
duration: Option<Duration>,
offset: Duration,
scene: impl Scene + 'static,
) -> crate::Result<()> {
if self.clips.len() >= MAX_CLIPS {
return Err(on_track(self.index, "At most 16384 clips are allowed"));
}
if start < Time::ZERO {
return Err(on_track(self.index, "Clip start must be nonnegative"));
}
let duration = duration.or_else(|| scene.duration()).ok_or_else(|| {
on_track(
self.index,
"Scene has no finite duration; use push_for or with_duration",
)
})?;
if duration <= Duration::ZERO {
return Err(on_track(self.index, "Clip duration must be positive"));
}
let requested_end =
add_time(start, duration).map_err(|error| on_track(self.index, error))?;
let (aligned_start, start_frame) =
align(start, self.rate).map_err(|error| on_track(self.index, error))?;
let (aligned_end, end_frame) =
align(requested_end, self.rate).map_err(|error| on_track(self.index, error))?;
if aligned_end <= aligned_start {
return Err(on_track(
self.index,
"Clip becomes empty after frame alignment; increase its duration",
));
}
let index = self
.clips
.partition_point(|clip| clip.start <= aligned_start);
let id = self.clips.len();
let overlaps = index
.checked_sub(1)
.and_then(|previous| self.clips.get(previous))
.filter(|clip| clip.end() > aligned_start)
.or_else(|| {
self.clips
.get(index)
.filter(|clip| clip.start < aligned_end)
});
let transition_in = if let Some(effect) = &self.pending {
let previous = self
.clips
.last()
.ok_or_else(|| on_track(self.index, "Transition requires a preceding clip"))?;
if start != self.cursor
|| index != self.clips.len()
|| aligned_start < previous.start
|| aligned_start >= previous.end()
|| aligned_end < previous.end()
|| previous
.transition_in
.as_ref()
.is_some_and(|incoming| aligned_start < previous.start + incoming.duration)
{
return Err(on_track(
self.index,
"Incoming clip must cover the adjacent overlap without a triple overlap",
));
}
Some(TransitionIn {
effect: effect.clone(),
duration: (previous.end() - aligned_start).magnitude(),
})
} else {
None
};
if let Some(other) = overlaps.filter(|_| transition_in.is_none()) {
return Err(on_track(
self.index,
format!(
"clip[{id}] [{}, {}) overlaps clip[{}] [{}, {}); use an explicit adjacent transition or separate track",
aligned_start.ticks(),
aligned_end.ticks(),
other.id,
other.start.ticks(),
other.end().ticks()
),
));
}
for (edge, requested, aligned, frame) in [
(BoundaryEdge::Start, start, aligned_start, start_frame),
(BoundaryEdge::End, requested_end, aligned_end, end_frame),
] {
if requested != aligned {
self.adjustments.push(BoundaryAdjustment {
track: Some(self.index),
clip: Some(id),
edge,
requested,
aligned,
frame,
});
}
}
self.clips.insert(
index,
Clip {
id,
start: aligned_start,
duration: (aligned_end - aligned_start).magnitude(),
offset,
scene: Arc::new(scene),
transition_in,
},
);
self.cursor = self.cursor.max(requested_end);
self.pending = None;
Ok(())
}
fn gap_checked(&mut self, duration: Duration) -> crate::Result<()> {
if self.pending.is_some() {
return Err(on_track(
self.index,
"A pending transition must be followed by a clip, not a gap",
));
}
self.cursor =
add_time(self.cursor, duration).map_err(|error| on_track(self.index, error))?;
Ok(())
}
pub fn cursor(&self) -> Time {
self.cursor
}
pub(super) fn finish(
mut self,
) -> Result<(Track, Vec<BoundaryAdjustment>, Vec<AudioClip>), ErrorReport> {
if !self.errors.is_empty() {
return Err(self.errors);
}
if self.pending.is_some() {
return Err(on_track(self.index, "Pending transition has no incoming clip").into());
}
let (end, frame) =
align(self.cursor, self.rate).map_err(|error| on_track(self.index, error))?;
if end != self.cursor {
self.adjustments.push(BoundaryAdjustment {
track: Some(self.index),
clip: None,
edge: BoundaryEdge::Cursor,
requested: self.cursor,
aligned: end,
frame,
});
}
Ok((
Track {
clips: self.clips.into(),
duration: end.since_origin().magnitude(),
},
self.adjustments,
self.audio,
))
}
}