///|
/// A clipped rectangle in logical-screen coordinates.
pub(all) struct GifRect {
left : Int
top : Int
width : Int
height : Int
} derive(Eq, Debug)
///|
pub fn GifRect::new(
left~ : Int,
top~ : Int,
width~ : Int,
height~ : Int,
) -> GifRect {
{ left, top, width, height }
}
///|
pub fn GifRect::left(self : GifRect) -> Int {
self.left
}
///|
pub fn GifRect::top(self : GifRect) -> Int {
self.top
}
///|
pub fn GifRect::width(self : GifRect) -> Int {
self.width
}
///|
pub fn GifRect::height(self : GifRect) -> Int {
self.height
}
///|
pub fn GifRect::area(self : GifRect) -> Int {
self.width * self.height
}
///|
pub fn GifRect::right(self : GifRect) -> Int {
self.left + self.width
}
///|
pub fn GifRect::bottom(self : GifRect) -> Int {
self.top + self.height
}
///|
/// Find changed pixels using a squared RGBA distance threshold.
pub fn changed_rect(left : Canvas, right : Canvas, threshold : Int) -> GifRect? {
if left.width != right.width || left.height != right.height {
Some(GifRect::{ left: 0, top: 0, width: right.width, height: right.height })
} else {
let limit = if threshold < 0 { 0 } else { threshold }
let mut min_x = left.width
let mut min_y = left.height
let mut max_x = -1
let mut max_y = -1
for y in 0.. limit {
if x < min_x {
min_x = x
}
if y < min_y {
min_y = y
}
if x > max_x {
max_x = x
}
if y > max_y {
max_y = y
}
}
}
}
if max_x < 0 {
None
} else {
Some(GifRect::{
left: min_x,
top: min_y,
width: max_x - min_x + 1,
height: max_y - min_y + 1,
})
}
}
}
///|
pub(all) struct DeltaStats {
frame_count : Int
full_pixels : Int
delta_pixels : Int
changed_frames : Int
} derive(Debug)
///|
pub fn DeltaStats::frame_count(self : DeltaStats) -> Int {
self.frame_count
}
///|
pub fn DeltaStats::full_pixels(self : DeltaStats) -> Int {
self.full_pixels
}
///|
pub fn DeltaStats::delta_pixels(self : DeltaStats) -> Int {
self.delta_pixels
}
///|
pub fn DeltaStats::changed_frames(self : DeltaStats) -> Int {
self.changed_frames
}
///|
pub fn DeltaStats::saving_per_mille(self : DeltaStats) -> Int {
if self.full_pixels <= 0 {
0
} else {
(self.full_pixels - self.delta_pixels) * 1000 / self.full_pixels
}
}
///|
/// Estimate the amount of pixel data saved by frame differencing.
pub fn Animation::delta_stats(
self : Animation,
threshold? : Int = 0,
) -> DeltaStats {
let full_pixels = self.width * self.height * self.frames.length()
if self.frames.length() == 0 {
DeltaStats::{
frame_count: 0,
full_pixels,
delta_pixels: 0,
changed_frames: 0,
}
} else {
let mut previous = self.background_canvas()
let mut delta_pixels = 0
let mut changed_frames = 0
for frame in self.frames {
match changed_rect(previous, frame.canvas, threshold) {
None => delta_pixels += 1
Some(rect) => {
delta_pixels += rect.area()
changed_frames += 1
}
}
previous = frame.canvas
}
DeltaStats::{
frame_count: self.frames.length(),
full_pixels,
delta_pixels,
changed_frames,
}
}
}
///|
fn Animation::background_canvas(self : Animation) -> Canvas {
Canvas::new(width=self.width, height=self.height, fill=self.background)
}
///|
/// Encode unchanged frames as transparent 1x1 frames and crop changed frames
/// to their bounding rectangles. This is an optimization pass; it preserves
/// timing and keeps the logical screen size unchanged.
pub fn Animation::delta_encode(
self : Animation,
threshold? : Int = 0,
) -> Animation {
if self.frames.length() == 0 {
self
} else {
let transparent = rgba(0, 0, 0, 0)
let frames : Array[Frame] = []
let mut previous = self.background_canvas()
for frame in self.frames {
match changed_rect(previous, frame.canvas, threshold) {
None => {
let empty = Canvas::new(width=1, height=1, fill=transparent)
frames.push(
Frame::new(canvas=empty, delay_cs=frame.delay_cs).with_transparent(
color=transparent,
),
)
}
Some(rect) => {
let cropped = frame.canvas.crop(
x=rect.left,
y=rect.top,
width=rect.width,
height=rect.height,
)
let optimized = Frame::new(canvas=cropped, delay_cs=frame.delay_cs)
.with_offset(left=rect.left, top=rect.top)
.with_disposal(disposal=frame.disposal)
let final_frame = match frame.transparent {
None => optimized
Some(color) => optimized.with_transparent(color~)
}
frames.push(final_frame)
}
}
previous = frame.canvas
}
Animation::{ ..self, frames, }
}
}
///|
/// Alias emphasizing that the pass is safe to apply before encoding.
pub fn Animation::optimize_frames(self : Animation) -> Animation {
self.delta_encode()
}