///|
/// Timeline marker for editors, captions, and export orchestration.
pub(all) struct TimelineMarker {
name : String
time_cs : Int
frame_index : Int
color : Rgba
} derive(Debug)
///|
pub fn TimelineMarker::new(
name : String,
time_cs : Int,
color : Rgba,
) -> TimelineMarker {
{
name,
time_cs: if time_cs < 0 {
0
} else {
time_cs
},
frame_index: -1,
color,
}
}
///|
pub fn TimelineMarker::name(self : TimelineMarker) -> String {
self.name
}
///|
pub fn TimelineMarker::time(self : TimelineMarker) -> Int {
self.time_cs
}
///|
pub fn TimelineMarker::frame(self : TimelineMarker) -> Int {
self.frame_index
}
///|
pub fn TimelineMarker::color(self : TimelineMarker) -> Rgba {
self.color
}
///|
pub(all) struct AnimationTimeline {
animation : Animation
markers : Array[TimelineMarker]
} derive(Debug)
///|
pub fn AnimationTimeline::new(animation : Animation) -> AnimationTimeline {
{ animation, markers: [] }
}
///|
pub fn AnimationTimeline::animation(self : AnimationTimeline) -> Animation {
self.animation
}
///|
pub fn AnimationTimeline::markers(
self : AnimationTimeline,
) -> ArrayView[TimelineMarker] {
self.markers[:]
}
///|
pub fn AnimationTimeline::mark(
self : AnimationTimeline,
marker : TimelineMarker,
) -> AnimationTimeline {
let position = self.animation.position_at(marker.time())
let frame_index = match position {
None => -1
Some(value) => value.index()
}
self.markers.push({ ..marker, frame_index, })
self
}
///|
pub fn AnimationTimeline::mark_frame(
self : AnimationTimeline,
name : String,
index : Int,
color : Rgba,
) -> AnimationTimeline {
let safe_index = if index < 0 {
0
} else if index >= self.animation.frame_count() {
self.animation.frame_count() - 1
} else {
index
}
let time = if safe_index < 0 {
0
} else {
self.animation.timeline_starts()[safe_index]
}
self.mark({ name, time_cs: time, frame_index: safe_index, color })
}
///|
pub fn AnimationTimeline::at_time(
self : AnimationTimeline,
time_cs : Int,
) -> Array[TimelineMarker] {
let result : Array[TimelineMarker] = []
for marker in self.markers {
if marker.time() == time_cs {
result.push(marker)
}
}
result
}
///|
pub fn AnimationTimeline::between(
self : AnimationTimeline,
start_cs : Int,
end_cs : Int,
) -> Array[TimelineMarker] {
let low = if start_cs < end_cs { start_cs } else { end_cs }
let high = if start_cs < end_cs { end_cs } else { start_cs }
let result : Array[TimelineMarker] = []
for marker in self.markers {
if marker.time() >= low && marker.time() <= high {
result.push(marker)
}
}
result
}
///|
/// Move all markers by a time offset while clamping at zero.
pub fn AnimationTimeline::shift(
self : AnimationTimeline,
offset_cs : Int,
) -> AnimationTimeline {
let markers : Array[TimelineMarker] = []
for marker in self.markers {
let time = if marker.time() + offset_cs < 0 {
0
} else {
marker.time() + offset_cs
}
markers.push({ ..marker, time_cs: time })
}
{ ..self, markers, }
}
///|
/// Scale all marker positions using the same integer tempo rule as retiming.
pub fn AnimationTimeline::scale_time(
self : AnimationTimeline,
per_mille : Int,
) -> AnimationTimeline {
let safe = if per_mille < 0 { 0 } else { per_mille }
let markers : Array[TimelineMarker] = []
for marker in self.markers {
markers.push({ ..marker, time_cs: marker.time() * safe / 1000 })
}
{ ..self, markers, }
}
///|
/// Render a marker strip aligned with the animation timeline.
pub fn AnimationTimeline::marker_strip(
self : AnimationTimeline,
width : Int,
height : Int,
) -> Canvas {
let safe_width = if width < 1 { 1 } else { width }
let safe_height = if height < 1 { 1 } else { height }
let result = Canvas::new(
width=safe_width,
height=safe_height,
fill=rgba(0, 0, 0, 0),
)
let duration = self.animation.duration_cs()
for marker in self.markers {
let x = if duration <= 0 {
0
} else {
marker.time() * (safe_width - 1) / duration
}
ignore(
result.draw_line(
x0=x,
y0=0,
x1=x,
y1=safe_height - 1,
color=marker.color(),
),
)
}
result
}
///|
/// Attach marker-derived delays to a sequence of frames.
pub fn AnimationTimeline::split_at_markers(
self : AnimationTimeline,
) -> Animation {
let result = self.animation
if self.markers.length() == 0 {
result
} else {
let starts = result.timeline_starts()
for marker in self.markers {
let mut nearest = 0
for index, start in starts {
if start <= marker.time() {
nearest = index
}
}
if nearest >= 0 && nearest < result.frames.length() {
let frame = result.frames[nearest]
let remaining = if marker.time() > starts[nearest] {
marker.time() - starts[nearest]
} else {
0
}
result.frames[nearest] = frame.with_delay(delay_cs=remaining)
}
}
result
}
}
///|
/// Return marker names in timeline order.
pub fn AnimationTimeline::names(self : AnimationTimeline) -> Array[String] {
let result : Array[String] = []
for marker in self.markers {
result.push(marker.name())
}
result
}
///|
/// Remove duplicate markers at the same time and name.
pub fn AnimationTimeline::deduplicate(
self : AnimationTimeline,
) -> AnimationTimeline {
let result : Array[TimelineMarker] = []
for marker in self.markers {
let mut present = false
for existing in result {
if existing.time() == marker.time() && existing.name() == marker.name() {
present = true
}
}
if !present {
result.push(marker)
}
}
{ ..self, markers: result }
}