1use crate::Sink;
6use std::fmt::Write as _;
7
8#[derive(Debug, Clone, Copy, PartialEq, Eq)]
10pub struct Rgb {
11 pub r: u8,
12 pub g: u8,
13 pub b: u8,
14}
15
16impl Rgb {
17 fn distance_squared(self, other: Rgb) -> u32 {
18 let dr = u32::from(self.r.abs_diff(other.r));
19 let dg = u32::from(self.g.abs_diff(other.g));
20 let db = u32::from(self.b.abs_diff(other.b));
21 dr * dr + dg * dg + db * db
22 }
23
24 fn luminance(self) -> u32 {
25 2_126 * u32::from(self.r) + 7_152 * u32::from(self.g) + 722 * u32::from(self.b)
26 }
27}
28
29#[derive(Debug, Clone, Copy, PartialEq, Eq)]
31pub struct Block {
32 pub glyph: char,
34 pub fg: Rgb,
35 pub bg: Rgb,
36}
37
38const QUARTERS: [char; 16] = [
41 ' ', '▘', '▝', '▀', '▖', '▌', '▞', '▛', '▗', '▚', '▐', '▜', '▄', '▙', '▟', '█',
42];
43
44#[must_use]
51pub fn quarter(pixels: [[Rgb; 2]; 2]) -> Block {
52 let [[top_left, top_right], [bottom_left, bottom_right]] = pixels;
53 let corners = [
54 (top_left, 1u8),
55 (top_right, 2),
56 (bottom_left, 4),
57 (bottom_right, 8),
58 ];
59 let mut best: Option<(Rgb, Rgb, u32)> = None;
60 for (first, (pixel, _)) in corners.iter().enumerate() {
61 for (other, _) in corners.iter().skip(first + 1) {
62 let distance = pixel.distance_squared(*other);
63 let improved = best.is_none_or(|(_, _, best_distance)| distance > best_distance);
64 if improved {
65 best = Some((*pixel, *other, distance));
66 }
67 }
68 }
69 let (left, right) = match best {
70 Some((first, second, _)) => (first, second),
71 None => (top_left, top_right),
72 };
73 let (fg, bg) = if left.luminance() >= right.luminance() {
74 (left, right)
75 } else {
76 (right, left)
77 };
78 let mut mask = 0u8;
79 for (pixel, bit) in corners {
80 if pixel.distance_squared(fg) <= pixel.distance_squared(bg) {
81 mask |= bit;
82 }
83 }
84 let glyph = QUARTERS.get(usize::from(mask)).copied().unwrap_or(' ');
85 Block { glyph, fg, bg }
86}
87
88#[must_use]
90pub fn half(top: Rgb, bottom: Rgb) -> Block {
91 if top == bottom {
92 return Block {
93 glyph: ' ',
94 fg: top,
95 bg: top,
96 };
97 }
98 Block {
99 glyph: '▀',
100 fg: top,
101 bg: bottom,
102 }
103}
104
105pub fn render_line<W: Sink>(out: &mut W, top: &[Rgb], bottom: &[Rgb]) -> std::io::Result<()> {
110 let mut text = String::new();
111 for (top_pair, bottom_pair) in top.chunks(2).zip(bottom.chunks(2)) {
112 let block = match (top_pair, bottom_pair) {
113 ([tl, tr], [bl, br]) => quarter([[*tl, *tr], [*bl, *br]]),
114 (odd_top, odd_bottom) => match (odd_top.first(), odd_bottom.first()) {
115 (Some(top), Some(bottom)) => half(*top, *bottom),
116 _ => break,
117 },
118 };
119 let _ = write!(
120 text,
121 "\x1b[38;2;{r};{g};{b}m\x1b[48;2;{r2};{g2};{b2}m{glyph}\x1b[0m",
122 r = block.fg.r,
123 g = block.fg.g,
124 b = block.fg.b,
125 r2 = block.bg.r,
126 g2 = block.bg.g,
127 b2 = block.bg.b,
128 glyph = block.glyph,
129 );
130 }
131 out.write_all(text.as_bytes())
132}
133
134#[cfg(test)]
135mod tests {
136 use super::{Block, Rgb, half, quarter};
137
138 const WHITE: Rgb = Rgb {
139 r: 255,
140 g: 255,
141 b: 255,
142 };
143 const BLACK: Rgb = Rgb { r: 0, g: 0, b: 0 };
144
145 #[test]
146 fn four_equal_pixels_collapse_to_a_full_block() {
147 let red = Rgb {
148 r: 200,
149 g: 30,
150 b: 30,
151 };
152 assert_eq!(
153 quarter([[red; 2]; 2]),
154 Block {
155 glyph: '█',
156 fg: red,
157 bg: red
158 }
159 );
160 }
161
162 #[test]
163 fn a_split_row_makes_the_upper_half() {
164 let cell = quarter([[WHITE, WHITE], [BLACK, BLACK]]);
165 assert_eq!(
166 cell,
167 Block {
168 glyph: '▀',
169 fg: WHITE,
170 bg: BLACK
171 }
172 );
173 }
174
175 #[test]
176 fn the_darker_pair_member_takes_the_background() {
177 let cell = quarter([[BLACK, BLACK], [WHITE, WHITE]]);
178 assert_eq!(cell.glyph, '▄');
179 assert_eq!(cell.fg, WHITE);
180 assert_eq!(cell.bg, BLACK);
181 }
182
183 #[test]
184 fn a_checkerboard_picks_the_diagonal_glyph() {
185 let cell = quarter([[WHITE, BLACK], [BLACK, WHITE]]);
186 assert_eq!(cell.glyph, '▚');
187 assert_eq!(cell.fg, WHITE);
188 assert_eq!(cell.bg, BLACK);
189 }
190
191 #[test]
192 fn the_other_diagonal_picks_its_own_glyph() {
193 let cell = quarter([[BLACK, WHITE], [WHITE, BLACK]]);
194 assert_eq!(cell.glyph, '▞');
195 assert_eq!(cell.fg, WHITE);
196 assert_eq!(cell.bg, BLACK);
197 }
198
199 #[test]
200 fn a_left_column_makes_the_left_half() {
201 let cell = quarter([[WHITE, BLACK], [WHITE, BLACK]]);
202 assert_eq!(cell.glyph, '▌');
203 }
204
205 #[test]
206 fn each_quadrant_joins_its_nearer_color() {
207 let near_black = Rgb { r: 5, g: 5, b: 5 };
208 let cell = quarter([[WHITE, near_black], [near_black, WHITE]]);
209 assert_eq!(cell.glyph, '▚');
210 assert_eq!(cell.fg, WHITE);
211 assert_eq!(cell.bg, near_black);
212 }
213
214 #[test]
215 fn half_blocks_split_or_fill() {
216 assert_eq!(
217 half(WHITE, BLACK),
218 Block {
219 glyph: '▀',
220 fg: WHITE,
221 bg: BLACK
222 }
223 );
224 assert_eq!(half(BLACK, BLACK).glyph, ' ');
225 }
226
227 #[test]
228 fn render_line_writes_golden_sgr_bytes() {
229 let mut out = Vec::new();
230 super::render_line(&mut out, &[WHITE, BLACK], &[BLACK, WHITE])
231 .expect("writing to a Vec cannot fail");
232 assert_eq!(
233 String::from_utf8_lossy(&out),
234 "\x1b[38;2;255;255;255m\x1b[48;2;0;0;0m▚\x1b[0m"
235 );
236 }
237
238 #[test]
239 fn a_trailing_odd_column_renders_a_half_block() {
240 let mut out = Vec::new();
241 super::render_line(&mut out, &[WHITE, BLACK, WHITE], &[BLACK, WHITE, BLACK])
242 .expect("writing to a Vec cannot fail");
243 let text = String::from_utf8_lossy(&out);
244 assert!(text.contains('▀'), "the odd column becomes a half block");
245 assert_eq!(text.chars().filter(|c| *c == '▀').count(), 1);
246 }
247
248 #[test]
249 fn empty_rows_render_nothing() {
250 let mut out = Vec::new();
251 super::render_line(&mut out, &[], &[]).expect("writing to a Vec cannot fail");
252 assert!(out.is_empty());
253 }
254}