use crate::renderer::AsciiBuffer;
use std::io::BufReader;
use std::path::Path;
use std::time::Duration;
struct GifFrame {
image: image::RgbaImage,
delay: Duration,
}
pub struct AnimatedGif {
frames: Vec<GifFrame>,
current_frame: usize,
elapsed: Duration,
pub name: String,
pub width: u32,
pub height: u32,
pub used_chafa: bool,
scale: f32,
}
impl AnimatedGif {
pub fn from_file(path: &Path) -> Result<Self, String> {
let name = path
.file_stem()
.and_then(|s| s.to_str())
.unwrap_or("Animation")
.to_string();
let file = std::fs::File::open(path).map_err(|e| e.to_string())?;
let reader = BufReader::new(file);
let decoder =
image::codecs::gif::GifDecoder::new(reader).map_err(|e| e.to_string())?;
use image::AnimationDecoder;
let raw_frames: Vec<image::Frame> = decoder
.into_frames()
.collect::<Result<Vec<_>, _>>()
.map_err(|e| e.to_string())?;
if raw_frames.is_empty() {
return Err("No frames in GIF".to_string());
}
let first = &raw_frames[0];
let width = first.buffer().width();
let height = first.buffer().height();
let frames: Vec<GifFrame> = raw_frames
.into_iter()
.map(|frame| {
let delay = Duration::from(frame.delay());
GifFrame {
image: frame.into_buffer(),
delay: if delay.as_millis() == 0 {
Duration::from_millis(100)
} else {
delay
},
}
})
.collect();
Ok(Self {
frames,
current_frame: 0,
elapsed: Duration::ZERO,
name,
width,
height,
used_chafa: false,
scale: 1.0,
})
}
pub fn set_scale(&mut self, scale: f32) {
self.scale = scale.clamp(0.3, 3.0);
}
pub fn get_scale(&self) -> f32 {
self.scale
}
pub fn update(&mut self, dt: f32) {
self.elapsed += Duration::from_secs_f32(dt);
if let Some(frame) = self.frames.get(self.current_frame) {
if self.elapsed >= frame.delay {
self.elapsed = Duration::ZERO;
self.current_frame = (self.current_frame + 1) % self.frames.len();
}
}
}
pub fn render(&self, buffer: &mut AsciiBuffer) {
let buf_width = buffer.width as usize;
let buf_height = buffer.height as usize;
if buf_width < 1 || buf_height < 1 {
return;
}
let Some(frame) = self.frames.get(self.current_frame) else {
return;
};
let img = &frame.image;
let img_width = img.width() as usize;
let img_height = img.height() as usize;
if img_width == 0 || img_height == 0 {
return;
}
let scale_x = img_width as f32 / buf_width as f32;
let scale_y = (img_height as f32 / buf_height as f32) * 0.5;
let scale = (scale_x.max(scale_y) / self.scale).max(0.1);
let out_width = ((img_width as f32 / scale) as usize).min(buf_width).max(1);
let out_height = ((img_height as f32 / scale * 0.5) as usize).min(buf_height).max(1);
let offset_x = buf_width.saturating_sub(out_width) / 2;
let offset_y = buf_height.saturating_sub(out_height) / 2;
for y in 0..out_height {
let buf_y = offset_y + y;
if buf_y >= buf_height {
break;
}
for x in 0..out_width {
let buf_x = offset_x + x;
if buf_x >= buf_width {
break;
}
let src_x = ((x as f32 * scale) as u32).min(img.width() - 1);
let src_y = ((y as f32 * scale * 2.0) as u32).min(img.height() - 1);
let pixel = img.get_pixel(src_x, src_y);
let [r, g, b, a] = pixel.0;
let lum = if a < 128 {
0.0 } else {
(0.299 * r as f32 + 0.587 * g as f32 + 0.114 * b as f32) / 255.0
};
buffer.plot(buf_x as u16, buf_y as u16, 100.0, lum);
}
}
}
pub fn frame_count(&self) -> usize {
self.frames.len()
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::path::Path;
#[test]
fn test_gif_scaling_logic() {
let img_width = 498usize;
let img_height = 498usize;
let test_cases = [
(80, 24, "small"),
(120, 40, "medium"),
(200, 60, "large"),
(40, 20, "tiny"),
];
for (buf_w, buf_h, _) in test_cases {
let scale_x = img_width as f32 / buf_w as f32;
let scale_y = (img_height as f32 / buf_h as f32) * 0.5;
let scale = scale_x.max(scale_y).max(1.0);
let out_w = ((img_width as f32 / scale) as usize).min(buf_w);
let out_h = ((img_height as f32 / scale * 0.5) as usize).min(buf_h);
assert!(out_w <= buf_w, "Output width exceeds buffer");
assert!(out_h <= buf_h, "Output height exceeds buffer");
assert!(out_w > buf_w / 4, "Output too small horizontally");
assert!(out_h > buf_h / 4, "Output too small vertically");
}
}
#[test]
fn test_load_and_render_gif() {
let gif_path = Path::new("samples/test.gif");
if !gif_path.exists() {
return; }
let gif = AnimatedGif::from_file(gif_path).expect("Failed to load GIF");
assert!(gif.frame_count() > 0, "GIF should have frames");
for (w, h) in [(80, 24), (120, 40), (40, 20)] {
let mut buffer = crate::renderer::AsciiBuffer::new(w, h);
gif.render(&mut buffer);
}
}
}