wallr_core/animated/
mod.rs1use std::path::Path;
9use std::time::Duration;
10
11pub struct AnimatedImage {
13 frames: Vec<Vec<u8>>,
14 pub width: u32,
15 pub height: u32,
16 delays: Vec<Duration>,
17 total: Duration,
18}
19
20impl AnimatedImage {
21 pub fn decode(path: &Path) -> anyhow::Result<Option<Self>> {
24 use image::AnimationDecoder;
25 use image::ImageDecoder;
26
27 let format = image::ImageReader::open(path)?
28 .with_guessed_format()?
29 .format();
30 if format != Some(image::ImageFormat::Gif) {
31 return Ok(None);
32 }
33
34 let decoder = image::codecs::gif::GifDecoder::new(std::io::BufReader::new(
35 std::fs::File::open(path)?,
36 ))?;
37 let (width, height) = decoder.dimensions();
38 let mut frames = Vec::new();
39 let mut delays = Vec::new();
40 for frame in decoder.into_frames() {
41 let frame = frame?;
42 let (numer, denom) = frame.delay().numer_denom_ms();
44 let millis = numer.checked_div(denom).unwrap_or(100);
45 let delay = Duration::from_millis(millis as u64)
46 .clamp(Duration::from_millis(20), Duration::from_secs(5));
47 frames.push(frame.buffer().as_raw().clone());
48 delays.push(delay);
49 }
50 if frames.is_empty() {
51 return Ok(None);
52 }
53 let total = delays.iter().copied().sum();
54 Ok(Some(Self {
55 frames,
56 width,
57 height,
58 delays,
59 total,
60 }))
61 }
62
63 pub fn first_frame(&self) -> &[u8] {
65 &self.frames[0]
66 }
67
68 pub fn frame_at(&self, index: usize) -> &[u8] {
69 &self.frames[index.min(self.frames.len() - 1)]
70 }
71
72 pub fn frame_index_at(&self, elapsed: Duration) -> usize {
74 let total_ms = self.total.as_millis().max(1);
75 let mut t = elapsed.as_millis() % total_ms;
76 for (i, delay) in self.delays.iter().enumerate() {
77 let ms = delay.as_millis();
78 if t < ms {
79 return i;
80 }
81 t -= ms;
82 }
83 self.frames.len() - 1
84 }
85}
86
87#[cfg(test)]
88mod tests {
89 use super::AnimatedImage;
90 use std::time::Duration;
91
92 fn sample() -> AnimatedImage {
93 AnimatedImage {
94 frames: vec![vec![0; 4], vec![1; 4], vec![2; 4]],
95 width: 1,
96 height: 1,
97 delays: vec![
98 Duration::from_millis(100),
99 Duration::from_millis(200),
100 Duration::from_millis(300),
101 ],
102 total: Duration::from_millis(600),
103 }
104 }
105
106 #[test]
107 fn frame_index_tracks_delays() {
108 let anim = sample();
109 assert_eq!(anim.frame_index_at(Duration::ZERO), 0);
110 assert_eq!(anim.frame_index_at(Duration::from_millis(99)), 0);
111 assert_eq!(anim.frame_index_at(Duration::from_millis(100)), 1);
112 assert_eq!(anim.frame_index_at(Duration::from_millis(299)), 1);
113 assert_eq!(anim.frame_index_at(Duration::from_millis(300)), 2);
114 assert_eq!(anim.frame_index_at(Duration::from_millis(599)), 2);
115 }
116
117 #[test]
118 fn frame_index_loops() {
119 let anim = sample();
120 assert_eq!(anim.frame_index_at(Duration::from_millis(600)), 0);
122 assert_eq!(anim.frame_index_at(Duration::from_millis(610)), 0);
123 assert_eq!(anim.frame_index_at(Duration::from_millis(700)), 1);
124 assert_eq!(anim.frame_index_at(Duration::from_millis(3000)), 0);
125 }
126
127 #[test]
128 fn frame_at_clamps_out_of_range() {
129 let anim = sample();
130 assert_eq!(anim.frame_at(0), &[0, 0, 0, 0]);
131 assert_eq!(anim.frame_at(999), &[2, 2, 2, 2]);
132 assert_eq!(anim.first_frame(), &[0, 0, 0, 0]);
133 }
134}