From c0562b5043cfc89233d55018f15bbae7fde24a89 Mon Sep 17 00:00:00 2001 From: Jan Bergen Date: Mon, 9 Mar 2026 20:24:31 +0100 Subject: [PATCH] add ascii mode --- termshrek/.gitignore | 1 + termshrek/Cargo.lock | 448 ++++++++++++++++++++++++++++++++++++++++++ termshrek/Cargo.toml | 9 + termshrek/src/main.rs | 284 ++++++++++++++++++++++++++ 4 files changed, 742 insertions(+) create mode 100644 termshrek/.gitignore create mode 100644 termshrek/Cargo.lock create mode 100644 termshrek/Cargo.toml create mode 100644 termshrek/src/main.rs diff --git a/termshrek/.gitignore b/termshrek/.gitignore new file mode 100644 index 0000000..eb5a316 --- /dev/null +++ b/termshrek/.gitignore @@ -0,0 +1 @@ +target diff --git a/termshrek/Cargo.lock b/termshrek/Cargo.lock new file mode 100644 index 0000000..7044dca --- /dev/null +++ b/termshrek/Cargo.lock @@ -0,0 +1,448 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "aho-corasick" +version = "1.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ddd31a130427c27518df266943a5308ed92d4b226cc639f5a8f1002816174301" +dependencies = [ + "memchr", +] + +[[package]] +name = "anstream" +version = "0.6.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "43d5b281e737544384e969a5ccad3f1cdd24b48086a0fc1b2a5262a26b8f4f4a" +dependencies = [ + "anstyle", + "anstyle-parse", + "anstyle-query", + "anstyle-wincon", + "colorchoice", + "is_terminal_polyfill", + "utf8parse", +] + +[[package]] +name = "anstyle" +version = "1.0.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5192cca8006f1fd4f7237516f40fa183bb07f8fbdfedaa0036de5ea9b0b45e78" + +[[package]] +name = "anstyle-parse" +version = "0.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4e7644824f0aa2c7b9384579234ef10eb7efb6a0deb83f9630a49594dd9c15c2" +dependencies = [ + "utf8parse", +] + +[[package]] +name = "anstyle-query" +version = "1.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "40c48f72fd53cd289104fc64099abca73db4166ad86ea0b4341abe65af83dadc" +dependencies = [ + "windows-sys", +] + +[[package]] +name = "anstyle-wincon" +version = "3.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "291e6a250ff86cd4a820112fb8898808a366d8f9f58ce16d1f538353ad55747d" +dependencies = [ + "anstyle", + "once_cell_polyfill", + "windows-sys", +] + +[[package]] +name = "bindgen" +version = "0.72.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "993776b509cfb49c750f11b8f07a46fa23e0a1386ffc01fb1e7d343efc387895" +dependencies = [ + "bitflags", + "cexpr", + "clang-sys", + "itertools", + "proc-macro2", + "quote", + "regex", + "rustc-hash", + "shlex", + "syn", +] + +[[package]] +name = "bitflags" +version = "2.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "843867be96c8daad0d758b57df9392b6d8d271134fce549de6ce169ff98a92af" + +[[package]] +name = "cc" +version = "1.2.56" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aebf35691d1bfb0ac386a69bac2fde4dd276fb618cf8bf4f5318fe285e821bb2" +dependencies = [ + "find-msvc-tools", + "shlex", +] + +[[package]] +name = "cexpr" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6fac387a98bb7c37292057cffc56d62ecb629900026402633ae9160df93a8766" +dependencies = [ + "nom", +] + +[[package]] +name = "cfg-if" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" + +[[package]] +name = "clang-sys" +version = "1.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b023947811758c97c59bf9d1c188fd619ad4718dcaa767947df1cadb14f39f4" +dependencies = [ + "glob", + "libc", + "libloading", +] + +[[package]] +name = "clap" +version = "4.5.60" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2797f34da339ce31042b27d23607e051786132987f595b02ba4f6a6dffb7030a" +dependencies = [ + "clap_builder", + "clap_derive", +] + +[[package]] +name = "clap_builder" +version = "4.5.60" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24a241312cea5059b13574bb9b3861cabf758b879c15190b37b6d6fd63ab6876" +dependencies = [ + "anstream", + "anstyle", + "clap_lex", + "strsim", +] + +[[package]] +name = "clap_derive" +version = "4.5.55" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a92793da1a46a5f2a02a6f4c46c6496b28c43638adea8306fcb0caa1634f24e5" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "clap_lex" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3a822ea5bc7590f9d40f1ba12c0dc3c2760f3482c6984db1573ad11031420831" + +[[package]] +name = "colorchoice" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b05b61dc5112cbb17e4b6cd61790d9845d13888356391624cbe7e41efeac1e75" + +[[package]] +name = "either" +version = "1.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719" + +[[package]] +name = "ffmpeg-next" +version = "8.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d658424d233cbd993a972dd73a66ca733acd12a494c68995c9ac32ae1fe65b40" +dependencies = [ + "bitflags", + "ffmpeg-sys-next", + "libc", +] + +[[package]] +name = "ffmpeg-sys-next" +version = "8.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9bca20aa4ee774fe384c2490096c122b0b23cf524a9910add0686691003d797b" +dependencies = [ + "bindgen", + "cc", + "libc", + "num_cpus", + "pkg-config", + "vcpkg", +] + +[[package]] +name = "find-msvc-tools" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582" + +[[package]] +name = "glob" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0cc23270f6e1808e30a928bdc84dea0b9b4136a8bc82338574f23baf47bbd280" + +[[package]] +name = "heck" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" + +[[package]] +name = "hermit-abi" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc0fef456e4baa96da950455cd02c081ca953b141298e41db3fc7e36b1da849c" + +[[package]] +name = "is_terminal_polyfill" +version = "1.70.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695" + +[[package]] +name = "itertools" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "413ee7dfc52ee1a4949ceeb7dbc8a33f2d6c088194d9f922fb8318faf1f01186" +dependencies = [ + "either", +] + +[[package]] +name = "libc" +version = "0.2.182" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6800badb6cb2082ffd7b6a67e6125bb39f18782f793520caee8cb8846be06112" + +[[package]] +name = "libloading" +version = "0.8.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7c4b02199fee7c5d21a5ae7d8cfa79a6ef5bb2fc834d6e9058e89c825efdc55" +dependencies = [ + "cfg-if", + "windows-link", +] + +[[package]] +name = "memchr" +version = "2.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79" + +[[package]] +name = "minimal-lexical" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" + +[[package]] +name = "nom" +version = "7.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a" +dependencies = [ + "memchr", + "minimal-lexical", +] + +[[package]] +name = "num_cpus" +version = "1.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91df4bbde75afed763b708b7eee1e8e7651e02d97f6d5dd763e89367e957b23b" +dependencies = [ + "hermit-abi", + "libc", +] + +[[package]] +name = "once_cell_polyfill" +version = "1.70.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe" + +[[package]] +name = "pkg-config" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c" + +[[package]] +name = "proc-macro2" +version = "1.0.106" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41f2619966050689382d2b44f664f4bc593e129785a36d6ee376ddf37259b924" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "regex" +version = "1.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e10754a14b9137dd7b1e3e5b0493cc9171fdd105e0ab477f51b72e7f3ac0e276" +dependencies = [ + "aho-corasick", + "memchr", + "regex-automata", + "regex-syntax", +] + +[[package]] +name = "regex-automata" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e1dd4122fc1595e8162618945476892eefca7b88c52820e74af6262213cae8f" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + +[[package]] +name = "regex-syntax" +version = "0.8.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc897dd8d9e8bd1ed8cdad82b5966c3e0ecae09fb1907d58efaa013543185d0a" + +[[package]] +name = "rustc-hash" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "357703d41365b4b27c590e3ed91eabb1b663f07c4c084095e60cbed4362dff0d" + +[[package]] +name = "shlex" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" + +[[package]] +name = "strsim" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" + +[[package]] +name = "syn" +version = "2.0.117" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e665b8803e7b1d2a727f4023456bbbbe74da67099c585258af0ad9c5013b9b99" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "term_size" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e4129646ca0ed8f45d09b929036bafad5377103edd06e50bf574b353d2b08d9" +dependencies = [ + "libc", + "winapi", +] + +[[package]] +name = "termshrek" +version = "0.1.0" +dependencies = [ + "clap", + "ffmpeg-next", + "term_size", +] + +[[package]] +name = "unicode-ident" +version = "1.0.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" + +[[package]] +name = "utf8parse" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" + +[[package]] +name = "vcpkg" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" + +[[package]] +name = "winapi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" +dependencies = [ + "winapi-i686-pc-windows-gnu", + "winapi-x86_64-pc-windows-gnu", +] + +[[package]] +name = "winapi-i686-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" + +[[package]] +name = "winapi-x86_64-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" + +[[package]] +name = "windows-link" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" + +[[package]] +name = "windows-sys" +version = "0.61.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" +dependencies = [ + "windows-link", +] diff --git a/termshrek/Cargo.toml b/termshrek/Cargo.toml new file mode 100644 index 0000000..166819d --- /dev/null +++ b/termshrek/Cargo.toml @@ -0,0 +1,9 @@ +[package] +name = "termshrek" +version = "0.1.0" +edition = "2024" + +[dependencies] +clap = { version = "4.5.60", features = ["derive"] } +ffmpeg-next = "8.0.0" +term_size = "0.3.2" diff --git a/termshrek/src/main.rs b/termshrek/src/main.rs new file mode 100644 index 0000000..edeb236 --- /dev/null +++ b/termshrek/src/main.rs @@ -0,0 +1,284 @@ +// TODO CLI tool, read resolution of term + +use std::{ + io::{self, Write}, + path::Path, + thread, + time::{Duration, Instant}, +}; + +use clap::{Parser, ValueEnum}; +use ffmpeg::software::scaling::{context::Context as ScalingContext, flag::Flags}; +use ffmpeg::util::format::pixel::Pixel; +use ffmpeg_next as ffmpeg; + +// const ASCII_GRADIENT: &[char] = &[' ', '░', '▒', '▓', '█']; +const ASCII_GRADIENT: &[char] = &[' ', '▁', '▂', '▃', '▄', '▅', '▆', '▇', '█']; + +#[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord, ValueEnum)] +enum ArtStyle { + Pixel, + Ascii, +} + +#[derive(Parser)] +struct Args { + #[arg(short, long, default_value_t = true)] + keep_ar: bool, + #[arg(short, long, default_value_t = false)] + loop_video: bool, + #[arg(short, long, value_enum, default_value_t =ArtStyle::Pixel)] + art_style: ArtStyle, +} + +struct VideoReader { + input: ffmpeg::format::context::Input, + fps: f32, + aspect_ratio: f32, + dims: (u32, u32), + decoder: ffmpeg::decoder::Video, + scaler: ScalingContext, + stream_index: usize, + frame: ffmpeg::util::frame::Video, + rgb_frame: ffmpeg::util::frame::Video, + loop_video: bool, +} + +impl VideoReader { + fn new(path: &Path, loop_video: bool, term_width: usize, term_height: usize) -> Self { + ffmpeg::init().unwrap(); + + let input = ffmpeg::format::input(path).unwrap(); + + let stream = input + .streams() + .best(ffmpeg::media::Type::Video) + .expect("video stream not found"); + + let framerate = stream.avg_frame_rate(); + let framerate_f32 = framerate.numerator() as f32 / framerate.denominator() as f32; + + let stream_index = stream.index(); + + let codec_context = + ffmpeg::codec::context::Context::from_parameters(stream.parameters()).unwrap(); + + let decoder = codec_context.decoder().video().unwrap(); + + // Find aspect ratio + let width = decoder.width(); + let height = decoder.height(); + + let sar = decoder.aspect_ratio(); + + let sar_f32 = if sar.denominator() != 0 { + sar.numerator() as f32 / sar.denominator() as f32 + } else { + 1.0 + }; + + let dar = (width as f32 / height as f32) * sar_f32; + + // Find target resolution + let target_width; + let target_height; + + let prep_term_height = (term_height - 2) * 2; + + if dar > term_width as f32 / (prep_term_height) as f32 { + target_height = (term_width as f32 / dar).floor() as u32; + target_width = term_width as u32; + } else { + target_height = (prep_term_height) as u32; + target_width = ((prep_term_height) as f32 * dar).floor() as u32; + } + + let scaler = ScalingContext::get( + decoder.format(), + width, + height, + Pixel::RGB24, + target_width, + target_height.div_euclid(2) * 2, + Flags::BILINEAR, + ) + .unwrap(); + + Self { + input, + fps: framerate_f32, + aspect_ratio: dar, + dims: (target_width, target_height.div_euclid(2) * 2), + decoder, + scaler, + stream_index, + frame: ffmpeg::util::frame::Video::empty(), + rgb_frame: ffmpeg::util::frame::Video::empty(), + loop_video, + } + } + + fn extract_frame_data(&mut self) -> Vec<[u8; 2]> { + self.scaler.run(&self.frame, &mut self.rgb_frame).unwrap(); + + let width = self.rgb_frame.width() as usize; + let height = self.rgb_frame.height() as usize; + let stride = self.rgb_frame.stride(0); + + let mut rgb: Vec<[u8; 2]> = Vec::with_capacity(width * height * 3); + + let mut y = 0; + while y < height { + let upper = y * stride; + let lower = (y + 1) * stride; + let row_upper = &self.rgb_frame.data(0)[upper..upper + width * 3]; + let row_lower = &self.rgb_frame.data(0)[lower..lower + width * 3]; + let dualrow: Vec<[u8; 2]> = row_upper + .iter() + .zip(row_lower.iter()) + .map(|(&x, &y)| [x, y]) + .collect(); + rgb.extend(dualrow); + y += 2 + } + + return rgb; + } + + fn next_frame(&mut self) -> Option> { + let mut packets_iter = self.input.packets(); + loop { + if let Some((stream, packet)) = packets_iter.next() { + if stream.index() != self.stream_index { + continue; + } + + self.decoder.send_packet(&packet).unwrap(); + + if let Ok(_) = self.decoder.receive_frame(&mut self.frame) { + return Some(self.extract_frame_data()); + } + } else { + self.decoder.flush(); + self.input.seek(0, ..).unwrap(); + packets_iter = self.input.packets(); + } + } + } +} + +fn render_pixel_art(frame: &Vec<[u8; 2]>, reader: &VideoReader, buf: &mut Vec) { + for row in 0..reader.dims.1 / 2 { + for col in 0..reader.dims.0 { + let idx = 3 * (row * reader.dims.0 + col); + let reds = frame.get(idx as usize).unwrap_or_else(|| { + panic!("Index: {idx} is out of bounds at row {row} and col {col}. ") + }); + let greens = frame.get((idx + 1) as usize).unwrap_or_else(|| { + panic!("Index: {idx} is out of bounds at row {row} and col {col}. ") + }); + + let blues = frame.get((idx + 2) as usize).unwrap_or_else(|| { + panic!("Index: {idx} is out of bounds at row {row} and col {col}. ") + }); + + buf.extend_from_slice( + format!( + "\x1b[48;2;{br};{bg};{bb}m\x1b[38;2;{tr};{tg};{tb}m▀", + br = reds[1], + bg = greens[1], + bb = blues[1], + tr = reds[0], + tg = greens[0], + tb = blues[0], + ) + .as_bytes(), + ); + } + buf.extend_from_slice(b"\x1b[0m\n"); + } +} + +fn luminance(r: u8, g: u8, b: u8) -> f32 { + let res = 0.2126 * f32::try_from(r).unwrap_or_else(|e| unreachable!("{e}")) + + 0.7152 * f32::try_from(g).unwrap_or_else(|e| unreachable!("{e}")) + + 0.0722 * f32::try_from(b).unwrap_or_else(|e| unreachable!("{e}")); + res +} + +fn render_ascii_art(frame: &Vec<[u8; 2]>, reader: &VideoReader, buf: &mut Vec) { + for row in 0..reader.dims.1 / 2 { + for col in 0..reader.dims.0 { + let idx = 3 * (row * reader.dims.0 + col); + let reds = frame.get(idx as usize).unwrap_or_else(|| { + panic!("Index: {idx} is out of bounds at row {row} and col {col}. ") + }); + let greens = frame.get((idx + 1) as usize).unwrap(); + let blues = frame.get((idx + 2) as usize).unwrap(); + + // color + // buf.extend_from_slice( + // format!( + // "\x1b[38;2;{tr};{tg};{tb}m", + // tr = reds[0], + // tg = greens[0], + // tb = blues[0], + // ) + // .as_bytes(), + // ); + let mut extend = [0u8; 4]; + buf.extend_from_slice( + ASCII_GRADIENT[(luminance(reds[0], greens[0], blues[0])) as usize + * (ASCII_GRADIENT.len() - 1) + / 255] + .encode_utf8(&mut extend) + .as_bytes(), + ); + } + buf.extend_from_slice(b"\x1b[0m\n"); + } +} + +fn main() { + let args = Args::parse(); + + let term_dims = + term_size::dimensions().unwrap_or_else(|| panic!("Could not find terminal dimensions")); + + let mut reader = VideoReader::new( + Path::new("/home/jan/Videos/rotating_panda.mkv"), + args.loop_video, + term_dims.0, + term_dims.1, + ); + + let frame_duration_micros: Duration = + Duration::from_micros((1_000_000.0 / reader.fps).floor() as u64); + + let mut out = io::stdout().lock(); + let mut buf: Vec = Vec::new(); + let start_time = Instant::now(); + + // Clear screen and hide cursor + let _ = out.write_all(b"\x1b[2J\x1b[?25l"); + + while let Some(frame) = reader.next_frame() { + let time = start_time.elapsed(); + + buf.extend_from_slice(b"\x1b[J\x1b[H"); // move to start + match args.art_style { + ArtStyle::Pixel => render_pixel_art(&frame, &reader, &mut buf), + ArtStyle::Ascii => render_ascii_art(&frame, &reader, &mut buf), + } + + let _ = out.write_all(&buf); + let _ = out.flush(); + + buf.clear(); + + let sleep_time = frame_duration_micros - (start_time.elapsed() - time); + if sleep_time.as_micros() > 0 { + thread::sleep(sleep_time); + } + } +}