///|
/// Non-destructive timeline clips for editors and reusable export scenes.
pub(all) struct AnimationClip {
source : Animation
start_cs : Int
end_cs : Int
speed_per_mille : Int
loop_count : Int
} derive(Debug)
///|
pub fn AnimationClip::new(
source : Animation,
start_cs : Int,
end_cs : Int,
) -> AnimationClip {
let start = if start_cs < 0 { 0 } else { start_cs }
let end = if end_cs < start { start } else { end_cs }
{ source, start_cs: start, end_cs: end, speed_per_mille: 1000, loop_count: 1 }
}
///|
pub fn AnimationClip::source(self : AnimationClip) -> Animation {
self.source
}
///|
pub fn AnimationClip::range(self : AnimationClip) -> (Int, Int) {
(self.start_cs, self.end_cs)
}
///|
pub fn AnimationClip::speed(
self : AnimationClip,
per_mille : Int,
) -> AnimationClip {
{ ..self, speed_per_mille: if per_mille < 1 { 1 } else { per_mille } }
}
///|
pub fn AnimationClip::loops(self : AnimationClip, count : Int) -> AnimationClip {
{ ..self, loop_count: if count < 1 { 1 } else { count } }
}
///|
pub fn AnimationClip::duration(self : AnimationClip) -> Int {
(self.end_cs - self.start_cs) * 1000 / self.speed_per_mille
}
///|
/// Render a clip into a fresh animation while preserving sampled timing.
pub fn AnimationClip::render(self : AnimationClip) -> Animation {
let duration = self.duration()
let sampled = self.source.sample(
if self.source.frame_count() < 1 {
1
} else {
self.source.frame_count()
},
)
let frames : Array[Frame] = []
for frame in sampled.frames {
if frames.length() == 0 || frames.length() < self.source.frame_count() {
frames.push(
frame.with_delay(
delay_cs=if sampled.frame_count() == 0 {
0
} else {
duration / sampled.frame_count()
},
),
)
}
}
let one = Animation::{ ..self.source, frames, }
one.repeat(self.loop_count)
}
///|
/// Render multiple clips back-to-back with no implicit timing gap.
pub fn render_clip_sequence(clips : ArrayView[AnimationClip]) -> Animation? {
if clips.length() == 0 {
None
} else {
let first = clips[0].render()
let mut result = first
for i in 1.. AnimationClip {
{ ..self, source: self.source.reverse() }
}
///|
/// Set an explicit frame rate for clip playback.
pub fn AnimationClip::fps(
self : AnimationClip,
frames_per_second : Int,
) -> AnimationClip {
let safe = if frames_per_second < 1 { 1 } else { frames_per_second }
let frame_count = self.source.frame_count()
let duration = if frame_count == 0 { 0 } else { frame_count * 100 / safe }
{ ..self, end_cs: self.start_cs + duration }
}
///|
/// Extract a clip directly from a frame range and annotate its timing.
pub fn Animation::clip_frames(
self : Animation,
start : Int,
end : Int,
) -> AnimationClip {
let safe_start = if start < 0 {
0
} else if start >= self.frame_count() {
self.frame_count()
} else {
start
}
let safe_end = if end < safe_start {
safe_start
} else if end > self.frame_count() {
self.frame_count()
} else {
end
}
let first_time = if safe_start >= self.frame_count() {
self.duration_cs()
} else {
self.timeline_starts()[safe_start]
}
let final_time = if safe_end >= self.frame_count() {
self.duration_cs()
} else {
self.timeline_starts()[safe_end]
}
AnimationClip::new(self, first_time, final_time)
}
///|
/// Return a clip that includes a small pre-roll and post-roll around a marker.
pub fn Animation::clip_around(
self : Animation,
center_cs : Int,
padding_cs : Int,
) -> AnimationClip {
let safe_padding = if padding_cs < 0 { 0 } else { padding_cs }
AnimationClip::new(self, center_cs - safe_padding, center_cs + safe_padding)
}
///|
/// Concatenate clips and normalize delays to a target frame duration.
pub fn render_clips_at_rate(
clips : ArrayView[AnimationClip],
delay_cs : Int,
) -> Animation? {
match render_clip_sequence(clips) {
None => None
Some(animation) =>
Some(animation.with_delay(if delay_cs < 0 { 0 } else { delay_cs }))
}
}