///|
/// A half-open range of frame indices.
pub(all) struct FrameRange {
start : Int
end : Int
} derive(Eq, Debug)
///|
pub fn FrameRange::new(start~ : Int, end~ : Int) -> FrameRange {
{
start: if start < 0 {
0
} else {
start
},
end: if end < start {
start
} else {
end
},
}
}
///|
pub fn FrameRange::start(self : FrameRange) -> Int {
self.start
}
///|
pub fn FrameRange::end(self : FrameRange) -> Int {
self.end
}
///|
pub fn FrameRange::length(self : FrameRange) -> Int {
self.end - self.start
}
///|
pub fn FrameRange::contains(self : FrameRange, index : Int) -> Bool {
index >= self.start && index < self.end
}
///|
pub(all) enum TimelinePolicy {
KeepDurations
Uniform
PreserveTotal
ClampToGif
} derive(Eq, Debug)
///|
pub(all) struct EditReport {
input_frames : Int
output_frames : Int
removed_frames : Int
duration_before : Int
duration_after : Int
} derive(Debug)
///|
pub fn EditReport::input_frames(self : EditReport) -> Int {
self.input_frames
}
///|
pub fn EditReport::output_frames(self : EditReport) -> Int {
self.output_frames
}
///|
pub fn EditReport::removed_frames(self : EditReport) -> Int {
self.removed_frames
}
///|
pub fn EditReport::duration_before(self : EditReport) -> Int {
self.duration_before
}
///|
pub fn EditReport::duration_after(self : EditReport) -> Int {
self.duration_after
}
///|
fn animation_report(before : Animation, after : Animation) -> EditReport {
EditReport::{
input_frames: before.frame_count(),
output_frames: after.frame_count(),
removed_frames: if before.frame_count() > after.frame_count() {
before.frame_count() - after.frame_count()
} else {
0
},
duration_before: before.duration_cs(),
duration_after: after.duration_cs(),
}
}
///|
/// Remove a half-open frame range and return the resulting animation.
pub fn Animation::remove_range(
self : Animation,
range : FrameRange,
) -> Animation {
let result : Array[Frame] = []
for i, frame in self.frames {
if !range.contains(i) {
result.push(frame)
}
}
Animation::{ ..self, frames: result }
}
///|
/// Replace a range with a new sequence while preserving logical dimensions.
pub fn Animation::replace_range(
self : Animation,
range : FrameRange,
replacement : ArrayView[Frame],
) -> Animation {
let result : Array[Frame] = []
for i, frame in self.frames {
if i == range.start() {
for item in replacement {
result.push(item)
}
}
if !range.contains(i) {
result.push(frame)
}
}
if range.start() >= self.frames.length() {
for item in replacement {
result.push(item)
}
}
Animation::{ ..self, frames: result }
}
///|
/// Insert a frame before the requested index. Out-of-range indices append.
pub fn Animation::insert_frame(
self : Animation,
index : Int,
frame : Frame,
) -> Animation {
let result : Array[Frame] = []
let target = if index < 0 {
0
} else if index > self.frames.length() {
self.frames.length()
} else {
index
}
for i, existing in self.frames {
if i == target {
result.push(frame)
}
result.push(existing)
}
if target == self.frames.length() {
result.push(frame)
}
Animation::{ ..self, frames: result }
}
///|
/// Insert `count` transparent frames with the requested delay.
pub fn Animation::insert_gap(
self : Animation,
index : Int,
count : Int,
delay_cs~ : Int,
) -> Animation {
let mut result = self
let safe_count = if count < 0 { 0 } else { count }
for _ in 0.. Animation {
if range.length() <= 0 || range.start() >= self.frames.length() {
self
} else {
let extracted : Array[Frame] = []
let remaining : Array[Frame] = []
for i, frame in self.frames {
if range.contains(i) {
extracted.push(frame)
} else {
remaining.push(frame)
}
}
let safe_destination = if destination < 0 {
0
} else if destination > remaining.length() {
remaining.length()
} else {
destination
}
let result : Array[Frame] = []
for i, frame in remaining {
if i == safe_destination {
for item in extracted {
result.push(item)
}
}
result.push(frame)
}
if safe_destination == remaining.length() {
for item in extracted {
result.push(item)
}
}
Animation::{ ..self, frames: result }
}
}
///|
/// Split every frame into a fixed number of duration-preserving copies.
pub fn Animation::split_frames(self : Animation, pieces : Int) -> Animation {
if pieces <= 1 {
self
} else {
let result : Array[Frame] = []
for frame in self.frames {
let each = frame.delay_cs() / pieces
let remainder = frame.delay_cs() - each * pieces
for part in 0.. Animation {
let result : Array[Frame] = []
let mut previous : Canvas? = None
for frame in self.frames {
let keep = match previous {
None => true
Some(canvas) => changed_rect(canvas, frame.canvas(), threshold) is Some(_)
}
if keep {
result.push(frame)
previous = Some(frame.canvas())
} else if result.length() > 0 {
let last = result[result.length() - 1]
result[result.length() - 1] = last.with_delay(
delay_cs=last.delay_cs() + frame.delay_cs(),
)
}
}
Animation::{ ..self, frames: result }
}
///|
/// Merge adjacent frames when their rendered pixels are identical.
pub fn Animation::merge_equal_frames(self : Animation) -> Animation {
if self.frames.length() <= 1 {
self
} else {
let result : Array[Frame] = [self.frames[0]]
for i in 1.. Animation {
if tick_cs <= 0 || self.frames.length() == 0 {
self
} else {
let result : Array[Frame] = []
let mut assigned = 0
for i, frame in self.frames {
let delay = match policy {
KeepDurations => frame.delay_cs()
Uniform => tick_cs
PreserveTotal =>
if i == self.frames.length() - 1 {
let target = self.duration_cs()
if target > assigned {
target - assigned
} else {
1
}
} else {
let units = (frame.delay_cs() + tick_cs / 2) / tick_cs
if units <= 0 {
tick_cs
} else {
units * tick_cs
}
}
ClampToGif =>
if frame.delay_cs() > 65535 {
65535
} else if frame.delay_cs() < 0 {
0
} else {
frame.delay_cs()
}
}
assigned += delay
result.push(frame.with_delay(delay_cs=delay))
}
Animation::{ ..self, frames: result }
}
}
///|
/// Normalize a time range into a sequence of uniformly sampled snapshots.
pub fn Animation::resample(
self : Animation,
frame_count : Int,
policy : TimelinePolicy,
) -> Animation {
if frame_count <= 0 || self.frames.length() == 0 {
Animation::{ ..self, frames: [] }
} else {
let result : Array[Frame] = []
let duration = if self.duration_cs() <= 0 {
self.frames.length()
} else {
self.duration_cs()
}
let delay = if policy == Uniform { duration / frame_count } else { 0 }
for i in 0.. ()
Some(frame) =>
result.push(
frame.with_delay(
delay_cs=if delay <= 0 { frame.delay_cs() } else { delay },
),
)
}
}
Animation::{ ..self, frames: result }
}
}
///|
/// A small non-destructive editor with undo snapshots for CLI and UI clients.
pub(all) struct AnimationEditor {
mut current : Animation
mut history : Array[Animation]
mut cursor : Int
} derive(Debug)
///|
pub fn AnimationEditor::new(animation : Animation) -> AnimationEditor {
{ current: animation, history: [], cursor: 0 }
}
///|
pub fn AnimationEditor::current(self : AnimationEditor) -> Animation {
self.current
}
///|
pub fn AnimationEditor::can_undo(self : AnimationEditor) -> Bool {
self.history.length() > 0
}
///|
pub fn AnimationEditor::can_redo(self : AnimationEditor) -> Bool {
self.cursor + 1 < self.history.length()
}
///|
fn AnimationEditor::record(
self : AnimationEditor,
next : Animation,
) -> AnimationEditor {
self.history.push(self.current)
self.cursor = self.history.length()
self.current = next
self
}
///|
pub fn AnimationEditor::remove(
self : AnimationEditor,
range : FrameRange,
) -> AnimationEditor {
self.record(self.current.remove_range(range))
}
///|
pub fn AnimationEditor::insert(
self : AnimationEditor,
index : Int,
frame : Frame,
) -> AnimationEditor {
self.record(self.current.insert_frame(index, frame))
}
///|
pub fn AnimationEditor::transform(
self : AnimationEditor,
transform : AnimationTransform,
) -> AnimationEditor {
self.record(self.current.map_transform(transform))
}
///|
pub fn AnimationEditor::undo(self : AnimationEditor) -> AnimationEditor {
if self.history.length() == 0 {
self
} else {
let previous = self.history[self.history.length() - 1]
ignore(self.history.remove(self.history.length() - 1))
self.current = previous
self
}
}
///|
pub fn AnimationEditor::build(self : AnimationEditor) -> Animation {
self.current
}
///|
/// Return a report for an edit operation without hiding the before/after data.
pub fn edit_report(before : Animation, after : Animation) -> EditReport {
animation_report(before, after)
}
///|
/// Split a timeline at every delay boundary and return the generated ranges.
pub fn Animation::ranges(self : Animation) -> Array[FrameRange] {
let result : Array[FrameRange] = []
for i in 0.. Animation {
let replacement : Array[Frame] = []
let start = if range.start() < 0 { 0 } else { range.start() }
let end = if range.end() > self.frames.length() {
self.frames.length()
} else {
range.end()
}
if start < end {
for i in start.. Animation {
if cycles <= 0 || self.frames.length() == 0 {
Animation::{ ..self, frames: [] }
} else {
let result : Array[Frame] = []
for _ in 0..