1use crate::Sink;
6use std::io::{self, Write as _};
7
8use katui_proto::kitty::{
9 Animate, AnimationState, Canvas, Delete, FrameTransmit, ImageId as KittyImage,
10};
11use katui_scene::placement::CellRect;
12use katui_scene::surface::diff_tiles;
13use katui_scene::surface::{PixelRect, Surface};
14
15use super::patched::tile_pixels;
16use super::{damage_is_dense, write_whole_image};
17use crate::kitty::crop_rect;
18
19const RESET_EVERY: u32 = 96;
22
23#[derive(Debug)]
36pub struct AnimatedKitty {
37 base: u32,
38 current: u32,
39 tile_side: u32,
40 budget: usize,
41 gap_ms: i32,
42 frame: u32,
43 since_reset: u32,
44 prev: Option<Surface>,
45}
46
47impl AnimatedKitty {
48 #[must_use]
53 pub fn new(base: u32, frames_per_second: u32, tile_side: u32, budget: usize) -> Self {
54 Self {
55 base,
56 current: base,
57 tile_side: tile_side.max(1),
58 budget,
59 gap_ms: (1000 / frames_per_second.max(1)).max(1).cast_signed(),
60 frame: 0,
61 since_reset: 0,
62 prev: None,
63 }
64 }
65
66 pub fn repaint(&mut self) {
69 self.prev = None;
70 }
71
72 pub fn free<W: Sink>(&mut self, out: &mut W) -> io::Result<()> {
74 for image in [self.base, self.base + 1] {
75 out.write_all(&Delete::Free(KittyImage(image)).encode())?;
76 }
77 self.repaint();
78 Ok(())
79 }
80
81 pub fn render_frame<W: Sink>(
83 &mut self,
84 out: &mut W,
85 surface: &Surface,
86 area: CellRect,
87 ) -> io::Result<usize> {
88 let same_size = self.prev.as_ref().is_some_and(|prev| {
89 prev.width() == surface.width() && prev.height() == surface.height()
90 });
91 if !same_size {
92 return self.start_reel(out, surface, area);
93 }
94 if self.since_reset >= RESET_EVERY {
95 return self.rotate_reel(out, surface, area);
96 }
97 let damage = self
98 .prev
99 .as_ref()
100 .map(|prev| diff_tiles(prev, surface, self.tile_side))
101 .unwrap_or_default();
102 self.prev = Some(surface.clone());
103 if damage.is_empty() {
104 return Ok(0);
105 }
106 let next = self.frame + 1;
107 let mut bytes = Vec::new();
108 let dense = damage_is_dense(surface, self.tile_side, damage.len());
109 if !dense && damage.len() <= self.budget {
110 self.edited_frame(&mut bytes, surface, &damage, next)?;
111 } else {
112 self.whole_frame(&mut bytes, surface)?;
113 }
114 self.frame = next;
115 self.since_reset += 1;
116 let written = bytes.len();
117 out.write_all(&bytes)?;
118 Ok(written)
119 }
120
121 fn rotate_reel<W: Sink>(
124 &mut self,
125 out: &mut W,
126 surface: &Surface,
127 area: CellRect,
128 ) -> io::Result<usize> {
129 let finished = self.current;
130 self.current = if finished == self.base {
131 self.base + 1
132 } else {
133 self.base
134 };
135 let written = self.start_reel(out, surface, area)?;
136 Self::stop_reel(out, finished)?;
137 Ok(written)
138 }
139
140 fn edited_frame(
143 &self,
144 bytes: &mut Vec<u8>,
145 surface: &Surface,
146 damage: &[PixelRect],
147 next: u32,
148 ) -> io::Result<()> {
149 let anchor = surface.pixels().get(..4).unwrap_or(&[0, 0, 0, 255]);
150 let create = FrameTransmit {
151 id: KittyImage(self.current),
152 edit: None,
153 canvas: Canvas::Frame(self.frame),
154 rect: Some(crop_rect(PixelRect {
155 x: 0,
156 y: 0,
157 width: 1,
158 height: 1,
159 })),
160 width: 1,
161 height: 1,
162 payload: anchor,
163 compressed: false,
164 gap_ms: Some(self.gap_ms),
165 };
166 for frame in create.encode()? {
167 bytes.write_all(&frame)?;
168 }
169 for rect in damage {
170 let pixels = tile_pixels(surface, *rect);
171 let edit = FrameTransmit {
172 id: KittyImage(self.current),
173 edit: Some(next),
174 canvas: Canvas::Blank,
175 rect: Some(crop_rect(*rect)),
176 width: rect.width,
177 height: rect.height,
178 payload: &pixels,
179 compressed: true,
180 gap_ms: None,
181 };
182 for frame in edit.encode()? {
183 bytes.write_all(&frame)?;
184 }
185 }
186 Ok(())
187 }
188
189 fn whole_frame(&self, bytes: &mut Vec<u8>, surface: &Surface) -> io::Result<()> {
191 let whole = FrameTransmit {
192 id: KittyImage(self.current),
193 edit: None,
194 canvas: Canvas::Blank,
195 rect: None,
196 width: surface.width(),
197 height: surface.height(),
198 payload: surface.pixels(),
199 compressed: true,
200 gap_ms: Some(self.gap_ms),
201 };
202 for frame in whole.encode()? {
203 bytes.write_all(&frame)?;
204 }
205 Ok(())
206 }
207
208 fn start_reel<W: Sink>(
211 &mut self,
212 out: &mut W,
213 surface: &Surface,
214 area: CellRect,
215 ) -> io::Result<usize> {
216 let mut bytes = Vec::new();
217 write_whole_image(&mut bytes, KittyImage(self.current), surface, area)?;
218 let run = Animate {
219 id: KittyImage(self.current),
220 state: Some(AnimationState::Loading),
221 frame_gap: Some((1, self.gap_ms)),
222 };
223 bytes.write_all(&run.encode())?;
224 self.prev = Some(surface.clone());
225 self.frame = 1;
226 self.since_reset = 1;
227 let written = bytes.len();
228 out.write_all(&bytes)?;
229 Ok(written)
230 }
231
232 fn stop_reel<W: Sink>(out: &mut W, image: u32) -> io::Result<()> {
234 let stop = Animate {
235 id: KittyImage(image),
236 state: Some(AnimationState::Stop),
237 frame_gap: None,
238 };
239 out.write_all(&stop.encode())?;
240 out.write_all(&Delete::Free(KittyImage(image)).encode())?;
241 Ok(())
242 }
243}
244
245#[cfg(test)]
246mod tests {
247 use super::AnimatedKitty;
248
249 fn plain_surface(width: u32, height: u32) -> Surface {
250 crate::surface::test_support::solid(width, height, Rgba::solid(10, 10, 30))
251 }
252
253 use katui_scene::surface::{Rgba, Surface};
254
255 #[test]
256 fn the_first_frame_is_a_root_frame_placed_and_playing() {
257 let mut presenter = AnimatedKitty::new(90, 20, 8, 16);
258 let mut out = Vec::new();
259 presenter
260 .render_frame(
261 &mut out,
262 &plain_surface(64, 64),
263 crate::surface::test_support::area(),
264 )
265 .expect("writing to a Vec cannot fail");
266 let text = String::from_utf8_lossy(&out);
267 assert!(
268 text.starts_with("\x1b_Ga=t,"),
269 "an image must exist before kitty accepts frames for it"
270 );
271 assert!(!text.contains("a=f"), "the root is no animation frame");
272 assert!(text.contains("o=z"), "compressed");
273 assert!(text.contains("a=p,i=90"), "placed once");
274 assert!(text.contains("a=a,i=90,s=2"), "playback is terminal-driven");
275 assert!(text.contains("r=1,z=50"), "the root gap is 50ms at 20fps");
276 }
277
278 #[test]
279 fn a_changed_frame_is_a_copy_plus_its_rectangles() {
280 let mut presenter = AnimatedKitty::new(90, 20, 8, 16);
281 let mut out = Vec::new();
282 presenter
283 .render_frame(
284 &mut out,
285 &plain_surface(64, 64),
286 crate::surface::test_support::area(),
287 )
288 .expect("writing to a Vec cannot fail");
289 let mut surface = plain_surface(64, 64);
290 surface.set(
291 katui_scene::surface::PixelPos::new(40, 40),
292 Rgba::solid(255, 255, 255),
293 );
294 let mut second = Vec::new();
295 presenter
296 .render_frame(&mut second, &surface, crate::surface::test_support::area())
297 .expect("writing to a Vec cannot fail");
298 let text = String::from_utf8_lossy(&second);
299 assert!(
300 !text.contains("s=64,v=64,"),
301 "the whole surface does not travel again"
302 );
303 assert!(
304 text.contains("c=1,x=0,y=0,s=1,v=1"),
305 "the frame is born a copy of its predecessor with an anchor pixel"
306 );
307 assert!(
308 text.contains("r=2,x=40,y=40,s=8,v=8"),
309 "the damaged tile edits frame two in place"
310 );
311 }
312
313 #[test]
314 fn an_unchanged_frame_writes_nothing() {
315 let mut presenter = AnimatedKitty::new(90, 20, 8, 16);
316 let mut out = Vec::new();
317 let surface = plain_surface(64, 64);
318 presenter
319 .render_frame(&mut out, &surface, crate::surface::test_support::area())
320 .expect("writing to a Vec cannot fail");
321 let mut second = Vec::new();
322 let written = presenter
323 .render_frame(&mut second, &surface, crate::surface::test_support::area())
324 .expect("writing to a Vec cannot fail");
325 assert_eq!(written, 0);
326 assert!(second.is_empty());
327 }
328
329 #[test]
330 fn a_reel_resets_after_its_frame_budget() {
331 let mut presenter = AnimatedKitty::new(90, 20, 8, 16);
332 let mut surface = plain_surface(32, 32);
333 let mut out = Vec::new();
334 presenter
335 .render_frame(&mut out, &surface, crate::surface::test_support::area())
336 .expect("writing to a Vec cannot fail");
337 for _ in 0..super::RESET_EVERY {
338 surface.set(
339 katui_scene::surface::PixelPos::new(4, 4),
340 Rgba::solid(
341 (std::time::SystemTime::now()
342 .duration_since(std::time::UNIX_EPOCH)
343 .map_or(1, |d| d.subsec_nanos()) as u8)
344 .saturating_mul(0)
345 + 200,
346 40,
347 40,
348 ),
349 );
350 surface.set(
351 katui_scene::surface::PixelPos::new(5, 5),
352 Rgba::solid(40, 200, 40),
353 );
354 presenter
355 .render_frame(&mut out, &surface, crate::surface::test_support::area())
356 .expect("writing to a Vec cannot fail");
357 surface.set(
358 katui_scene::surface::PixelPos::new(4, 4),
359 Rgba::solid(40, 40, 200),
360 );
361 presenter
362 .render_frame(&mut out, &surface, crate::surface::test_support::area())
363 .expect("writing to a Vec cannot fail");
364 }
365 let text = String::from_utf8_lossy(&out);
366 assert!(text.contains("a=a,i=90,s=1"), "the finished reel stops");
367 assert!(
368 text.contains("a=t,f=32,t=d,o=z,s=32,v=32,i=91"),
369 "a fresh root frame starts the next reel"
370 );
371 }
372}