///|
/// GIF uses an explicit logical interval. Full samples a finite plan's complete
/// duration, or one period of an illustrative plan.
pub(all) enum GifInterval {
Full
Interval(start_ms~ : Int64, end_ms~ : Int64)
} derive(Eq, Debug)
///|
pub extend GifInterval with Eq::{equal, not_equal}
///|
pub extend GifInterval with @debug.Debug::{to_repr}
///|
/// Repeat is the number of additional plays after the first, from 1 to 65535.
/// Once omits the Netscape extension; Forever writes its zero loop count.
pub(all) enum GifLoop {
Once
Forever
Repeat(Int32)
} derive(Eq, Debug)
///|
pub extend GifLoop with Eq::{equal, not_equal}
///|
pub extend GifLoop with @debug.Debug::{to_repr}
///|
/// GIF composites onto an opaque background. Frame rate ranges from 1 to 100.
/// Sampling includes the exact final logical view with one final frame hold.
/// Delays are rounded deterministically to centiseconds, with a 10ms minimum.
pub(all) struct GifOptions {
size : RasterSize
background : Rgba
theme : ExportTheme
interval : GifInterval
frame_rate : Int32
loop_behavior : GifLoop
} derive(Eq, Debug)
///|
pub extend GifOptions with Eq::{equal, not_equal}
///|
pub extend GifOptions with @debug.Debug::{to_repr}
///|
pub fn GifOptions::new(
size~ : RasterSize = Original,
background~ : Rgba = { red: 255, green: 255, blue: 255, alpha: 255, },
theme~ : ExportTheme = Light,
interval~ : GifInterval = Full,
frame_rate~ : Int32 = 10,
loop_behavior~ : GifLoop = Once,
) -> GifOptions {
{ size, background, theme, interval, frame_rate, loop_behavior, }
}
///|
fn gif_times(
duration : Int64,
options : GifOptions,
limits : RasterLimits,
pixels~ : Int64 = 1L,
) -> Array[(Int64, Int32)] raise {
guard options.frame_rate >= 1 && options.frame_rate <= 100 else {
raise GifInvalid("GIF frame rate must be between 1 and 100")
}
match options.loop_behavior {
Repeat(count) if count < 1 || count > 65535 =>
raise GifInvalid("GIF repeat count must be between 1 and 65535")
_ => ()
}
guard options.background.alpha == 255 &&
[options.background.red, options.background.green, options.background.blue].all(value => {
value >= 0 && value <= 255
},
) else {
raise GifInvalid("GIF background must be an opaque RGB color")
}
let (start, end) = match options.interval {
Full => (0L, duration)
Interval(start_ms~, end_ms~) => (start_ms, end_ms)
}
guard start >= 0L && end > start else {
raise GifInvalid("GIF requires a nonempty nonnegative logical interval")
}
guard limits.max_frames >= 2 else {
raise GifInvalid("GIF frame budget must allow at least two frames")
}
let elapsed = end - start
let fps = options.frame_rate.to_int64()
// This guard bounds the later multiplication and the endpoint-inclusive count.
guard elapsed <= (limits.max_frames.to_int64() - 1L) * 1000L / fps else {
raise GifInvalid("GIF frame budget exhausted")
}
let intervals = (elapsed * fps + 999L) / 1000L
guard intervals + 1L <= limits.max_frames.to_int64() else {
raise GifInvalid("GIF frame budget exhausted")
}
let count = intervals + 1L
guard pixels > 0L && count <= limits.max_cumulative_pixels / pixels else {
raise raster_fail("raster/cumulative-pixel-limit", "GIF exceeds its cumulative pixel budget")
}
guard limits.max_output_bytes.to_int64() >= 782L + count * 20L else {
raise raster_fail("raster/output-limit", "GIF byte budget cannot hold its headers and frames")
}
let result : Array[(Int64, Int32)] = []
for i in 0L.. pair.1).unwrap_or(1)
result.push((end, hold))
result
}