1use alloc::vec;
25use alloc::vec::Vec;
26
27use retroglyph_core::color::Color;
28use retroglyph_core::symbols::braille;
29use retroglyph_core::symbols::{Glyph, quantize_half_block, quantize_quadrant, quantize_sextant};
30
31#[derive(Debug, Clone)]
37struct SubCanvas {
38 width: usize,
40 height: usize,
42 pixels: Vec<Color>,
43 clear: Color,
44}
45
46impl SubCanvas {
47 fn new(width: usize, height: usize, clear: Color) -> Self {
48 Self {
49 width,
50 height,
51 pixels: vec![clear; width * height],
52 clear,
53 }
54 }
55
56 fn clear(&mut self) {
57 self.pixels.fill(self.clear);
58 }
59
60 fn set(&mut self, x: i32, y: i32, color: Color) {
61 if x < 0 || y < 0 {
62 return;
63 }
64 #[allow(clippy::cast_sign_loss)] let (x, y) = (x as usize, y as usize);
66 if x >= self.width || y >= self.height {
67 return;
68 }
69 self.pixels[y * self.width + x] = color;
70 }
71
72 fn get(&self, x: usize, y: usize) -> Color {
73 self.pixels
74 .get(y * self.width + x)
75 .copied()
76 .unwrap_or(self.clear)
77 }
78
79 fn rgb_at(&self, x: usize, y: usize) -> (u8, u8, u8) {
86 let fallback = self.clear.resolve_rgb((0, 0, 0));
87 self.get(x, y).resolve_rgb(fallback)
88 }
89}
90
91#[derive(Debug, Clone)]
98pub struct HalfBlockCanvas {
99 canvas: SubCanvas,
100 cols: usize,
101 rows: usize,
102}
103
104impl HalfBlockCanvas {
105 #[must_use]
107 pub fn new(cols: u16, rows: u16, clear: Color) -> Self {
108 let (cols, rows) = (cols as usize, rows as usize);
109 Self {
110 canvas: SubCanvas::new(cols, rows * 2, clear),
111 cols,
112 rows,
113 }
114 }
115
116 #[must_use]
118 pub const fn size(&self) -> (usize, usize) {
119 (self.canvas.width, self.canvas.height)
120 }
121
122 pub fn clear(&mut self) {
124 self.canvas.clear();
125 }
126
127 pub fn plot(&mut self, x: i32, y: i32, color: Color) {
130 self.canvas.set(x, y, color);
131 }
132
133 pub fn cells(&self) -> impl Iterator<Item = (u16, u16, Glyph)> + '_ {
135 (0..self.rows).flat_map(move |row| {
136 (0..self.cols).map(move |col| {
137 let glyph = quantize_half_block([
138 self.canvas.rgb_at(col, row * 2),
139 self.canvas.rgb_at(col, row * 2 + 1),
140 ]);
141 (
142 u16::try_from(col).unwrap_or(u16::MAX),
143 u16::try_from(row).unwrap_or(u16::MAX),
144 glyph,
145 )
146 })
147 })
148 }
149}
150
151#[derive(Debug, Clone)]
157pub struct QuadrantCanvas {
158 canvas: SubCanvas,
159 cols: usize,
160 rows: usize,
161}
162
163impl QuadrantCanvas {
164 #[must_use]
166 pub fn new(cols: u16, rows: u16, clear: Color) -> Self {
167 let (cols, rows) = (cols as usize, rows as usize);
168 Self {
169 canvas: SubCanvas::new(cols * 2, rows * 2, clear),
170 cols,
171 rows,
172 }
173 }
174
175 #[must_use]
177 pub const fn size(&self) -> (usize, usize) {
178 (self.canvas.width, self.canvas.height)
179 }
180
181 pub fn clear(&mut self) {
183 self.canvas.clear();
184 }
185
186 pub fn plot(&mut self, x: i32, y: i32, color: Color) {
188 self.canvas.set(x, y, color);
189 }
190
191 pub fn cells(&self) -> impl Iterator<Item = (u16, u16, Glyph)> + '_ {
193 (0..self.rows).flat_map(move |row| {
194 (0..self.cols).map(move |col| {
195 let (x, y) = (col * 2, row * 2);
196 let glyph = quantize_quadrant([
197 self.canvas.rgb_at(x, y),
198 self.canvas.rgb_at(x + 1, y),
199 self.canvas.rgb_at(x, y + 1),
200 self.canvas.rgb_at(x + 1, y + 1),
201 ]);
202 (
203 u16::try_from(col).unwrap_or(u16::MAX),
204 u16::try_from(row).unwrap_or(u16::MAX),
205 glyph,
206 )
207 })
208 })
209 }
210}
211
212#[derive(Debug, Clone)]
219pub struct SextantCanvas {
220 canvas: SubCanvas,
221 cols: usize,
222 rows: usize,
223}
224
225impl SextantCanvas {
226 #[must_use]
228 pub fn new(cols: u16, rows: u16, clear: Color) -> Self {
229 let (cols, rows) = (cols as usize, rows as usize);
230 Self {
231 canvas: SubCanvas::new(cols * 2, rows * 3, clear),
232 cols,
233 rows,
234 }
235 }
236
237 #[must_use]
239 pub const fn size(&self) -> (usize, usize) {
240 (self.canvas.width, self.canvas.height)
241 }
242
243 pub fn clear(&mut self) {
245 self.canvas.clear();
246 }
247
248 pub fn plot(&mut self, x: i32, y: i32, color: Color) {
250 self.canvas.set(x, y, color);
251 }
252
253 pub fn cells(&self) -> impl Iterator<Item = (u16, u16, Glyph)> + '_ {
255 (0..self.rows).flat_map(move |row| {
256 (0..self.cols).map(move |col| {
257 let (x, y) = (col * 2, row * 3);
258 let glyph = quantize_sextant([
259 self.canvas.rgb_at(x, y),
260 self.canvas.rgb_at(x + 1, y),
261 self.canvas.rgb_at(x, y + 1),
262 self.canvas.rgb_at(x + 1, y + 1),
263 self.canvas.rgb_at(x, y + 2),
264 self.canvas.rgb_at(x + 1, y + 2),
265 ]);
266 (
267 u16::try_from(col).unwrap_or(u16::MAX),
268 u16::try_from(row).unwrap_or(u16::MAX),
269 glyph,
270 )
271 })
272 })
273 }
274}
275
276#[derive(Debug, Clone)]
289pub struct BrailleCanvas {
290 cols: usize,
291 rows: usize,
292 dots: Vec<u8>,
294}
295
296impl BrailleCanvas {
297 #[must_use]
299 pub fn new(cols: u16, rows: u16) -> Self {
300 let (cols, rows) = (cols as usize, rows as usize);
301 Self {
302 cols,
303 rows,
304 dots: vec![0; cols * rows],
305 }
306 }
307
308 #[must_use]
310 pub const fn size(&self) -> (usize, usize) {
311 (self.cols * 2, self.rows * 4)
312 }
313
314 pub fn clear(&mut self) {
316 self.dots.fill(0);
317 }
318
319 pub fn plot(&mut self, x: i32, y: i32) {
322 if let Some((index, mask)) = self.locate(x, y) {
323 self.dots[index] |= mask;
324 }
325 }
326
327 pub fn unplot(&mut self, x: i32, y: i32) {
329 if let Some((index, mask)) = self.locate(x, y) {
330 self.dots[index] &= !mask;
331 }
332 }
333
334 #[must_use]
336 pub fn get(&self, x: i32, y: i32) -> bool {
337 self.locate(x, y)
338 .is_some_and(|(index, mask)| self.dots[index] & mask != 0)
339 }
340
341 const fn locate(&self, x: i32, y: i32) -> Option<(usize, u8)> {
347 if x < 0 || y < 0 {
348 return None;
349 }
350 #[allow(clippy::cast_sign_loss)] let (x, y) = (x as usize, y as usize);
352 let (cell_x, cell_y) = (x / 2, y / 4);
353 if cell_x >= self.cols || cell_y >= self.rows {
354 return None;
355 }
356 Some((cell_y * self.cols + cell_x, braille::DOTS[y % 4][x % 2]))
357 }
358
359 pub fn line(&mut self, x0: i32, y0: i32, x1: i32, y1: i32) {
365 let (dx, dy) = ((x1 - x0).abs(), -(y1 - y0).abs());
366 let (sx, sy) = (if x0 < x1 { 1 } else { -1 }, if y0 < y1 { 1 } else { -1 });
367 let (mut x, mut y) = (x0, y0);
368 let mut err = dx + dy;
369 loop {
370 self.plot(x, y);
371 if x == x1 && y == y1 {
372 break;
373 }
374 let e2 = 2 * err;
375 if e2 >= dy {
376 err += dy;
377 x += sx;
378 }
379 if e2 <= dx {
380 err += dx;
381 y += sy;
382 }
383 }
384 }
385
386 #[must_use]
388 pub fn glyph(&self, col: usize, row: usize) -> char {
389 let bits = if col < self.cols && row < self.rows {
390 self.dots[row * self.cols + col]
391 } else {
392 0
393 };
394 braille::glyph(bits)
395 }
396
397 pub fn cells(&self) -> impl Iterator<Item = (u16, u16, char)> + '_ {
399 (0..self.rows).flat_map(move |row| {
400 (0..self.cols).map(move |col| {
401 (
402 u16::try_from(col).unwrap_or(u16::MAX),
403 u16::try_from(row).unwrap_or(u16::MAX),
404 self.glyph(col, row),
405 )
406 })
407 })
408 }
409}
410
411#[cfg(test)]
412mod tests {
413 use alloc::vec;
414 use alloc::vec::Vec;
415
416 use super::{BrailleCanvas, Glyph, HalfBlockCanvas, QuadrantCanvas, SextantCanvas, braille};
417 use retroglyph_core::color::Color;
418 use retroglyph_core::symbols::{HALF_BLOCKS, QUADRANTS};
419
420 const RED: Color = Color::rgb(255, 0, 0);
421 const BLUE: Color = Color::rgb(0, 0, 255);
422 const BLACK: Color = Color::rgb(0, 0, 0);
423
424 #[test]
425 fn half_block_canvas_has_double_vertical_resolution() {
426 let canvas = HalfBlockCanvas::new(10, 5, BLACK);
427 assert_eq!(canvas.size(), (10, 10));
428 assert_eq!(canvas.cells().count(), 50);
429 }
430
431 fn resolve(glyph: Glyph, bank: &[char]) -> Vec<Color> {
438 let mask = bank
439 .iter()
440 .position(|&c| c == glyph.ch)
441 .expect("glyph must come from its own bank");
442 let sub_cells = bank.len().trailing_zeros() as usize;
445 (0..sub_cells)
446 .map(|i| {
447 if mask & (1 << i) != 0 {
448 glyph.fg
449 } else {
450 glyph.bg
451 }
452 })
453 .collect()
454 }
455
456 #[test]
457 fn half_block_renders_the_half_that_was_plotted() {
458 let mut canvas = HalfBlockCanvas::new(1, 1, BLACK);
459 canvas.plot(0, 0, RED);
460 let (_, _, glyph) = canvas.cells().next().expect("one cell");
461 assert_eq!(resolve(glyph, &HALF_BLOCKS), vec![RED, BLACK], "top set");
462
463 canvas.clear();
464 canvas.plot(0, 1, RED);
465 let (_, _, glyph) = canvas.cells().next().expect("one cell");
466 assert_eq!(resolve(glyph, &HALF_BLOCKS), vec![BLACK, RED], "bottom set");
467 }
468
469 #[test]
470 fn quadrant_canvas_has_double_resolution_on_both_axes() {
471 let canvas = QuadrantCanvas::new(8, 4, BLACK);
472 assert_eq!(canvas.size(), (16, 8));
473 assert_eq!(canvas.cells().count(), 32);
474 }
475
476 #[test]
477 fn quadrant_renders_the_corner_that_was_plotted() {
478 for (x, y, index) in [(0, 0, 0), (1, 0, 1), (0, 1, 2), (1, 1, 3)] {
479 let mut canvas = QuadrantCanvas::new(1, 1, BLACK);
480 canvas.plot(x, y, RED);
481 let (_, _, glyph) = canvas.cells().next().expect("one cell");
482 let mut expected = vec![BLACK; 4];
483 expected[index] = RED;
484 assert_eq!(resolve(glyph, &QUADRANTS), expected, "corner ({x}, {y})");
485 }
486 }
487
488 #[test]
489 fn canvases_preserve_plotted_colors_exactly() {
490 let mut canvas = HalfBlockCanvas::new(1, 1, BLACK);
493 canvas.plot(0, 0, RED);
494 canvas.plot(0, 1, BLUE);
495 let (_, _, glyph) = canvas.cells().next().expect("one cell");
496 assert_eq!(resolve(glyph, &HALF_BLOCKS), vec![RED, BLUE]);
497 }
498
499 #[test]
500 fn sextant_canvas_is_two_by_three() {
501 let canvas = SextantCanvas::new(6, 3, BLACK);
502 assert_eq!(canvas.size(), (12, 9));
503 assert_eq!(canvas.cells().count(), 18);
504 }
505
506 #[test]
507 fn canvases_ignore_out_of_bounds_plots() {
508 let mut half = HalfBlockCanvas::new(2, 2, BLACK);
509 let mut quad = QuadrantCanvas::new(2, 2, BLACK);
510 let mut sext = SextantCanvas::new(2, 2, BLACK);
511 for (x, y) in [(-1, 0), (0, -1), (999, 0), (0, 999), (-999, -999)] {
512 half.plot(x, y, RED);
513 quad.plot(x, y, RED);
514 sext.plot(x, y, RED);
515 }
516 assert!(half.cells().all(|(_, _, g)| g.ch == ' ' || g.fg == BLACK));
518 assert!(quad.cells().all(|(_, _, g)| g.ch == ' ' || g.fg == BLACK));
519 assert!(sext.cells().all(|(_, _, g)| g.ch == ' ' || g.fg == BLACK));
520 }
521
522 #[test]
523 fn clearing_a_canvas_erases_everything() {
524 let mut canvas = QuadrantCanvas::new(2, 2, BLACK);
525 canvas.plot(0, 0, RED);
526 canvas.clear();
527 let glyphs: Vec<char> = canvas.cells().map(|(_, _, g)| g.ch).collect();
528 assert!(glyphs.iter().all(|&c| c == ' '), "left over: {glyphs:?}");
529 }
530
531 #[test]
534 fn braille_canvas_is_two_by_four() {
535 let canvas = BrailleCanvas::new(20, 10);
536 assert_eq!(canvas.size(), (40, 40));
537 assert_eq!(canvas.cells().count(), 200);
538 }
539
540 #[test]
541 fn braille_plot_round_trips_through_get() {
542 let mut canvas = BrailleCanvas::new(4, 2);
543 for y in 0..8 {
544 for x in 0..8 {
545 assert!(!canvas.get(x, y));
546 canvas.plot(x, y);
547 assert!(canvas.get(x, y), "dot ({x}, {y}) did not stick");
548 canvas.unplot(x, y);
549 assert!(!canvas.get(x, y), "dot ({x}, {y}) did not clear");
550 }
551 }
552 }
553
554 #[test]
555 fn braille_glyph_is_blank_when_empty_and_full_when_saturated() {
556 let mut canvas = BrailleCanvas::new(1, 1);
557 assert_eq!(canvas.glyph(0, 0), braille::BLANK);
558 for y in 0..4 {
559 for x in 0..2 {
560 canvas.plot(x, y);
561 }
562 }
563 assert_eq!(canvas.glyph(0, 0), '\u{28FF}', "all 8 dots set");
564 }
565
566 #[test]
567 fn braille_glyphs_stay_inside_the_unicode_block() {
568 let mut canvas = BrailleCanvas::new(3, 3);
569 canvas.line(0, 0, 5, 11);
570 for (_, _, glyph) in canvas.cells() {
571 let code = glyph as u32;
572 assert!(
573 (0x2800..0x2900).contains(&code),
574 "{glyph:?} is outside the Braille block"
575 );
576 }
577 }
578
579 #[test]
580 fn braille_out_of_bounds_is_ignored_not_panicked() {
581 let mut canvas = BrailleCanvas::new(2, 2);
582 for (x, y) in [(-1, 0), (0, -1), (999, 0), (0, 999)] {
583 canvas.plot(x, y);
584 canvas.unplot(x, y);
585 assert!(!canvas.get(x, y));
586 }
587 assert!(canvas.cells().all(|(_, _, g)| g == braille::BLANK));
588 }
589
590 #[test]
594 fn dot_bit_positions_match_symbols_braille_dots() {
595 let mut canvas = BrailleCanvas::new(1, 1);
596 for y in 0..4u8 {
597 for x in 0..2u8 {
598 canvas.plot(i32::from(x), i32::from(y));
599 let bit = braille::DOTS[y as usize][x as usize];
600 assert_eq!(canvas.glyph(0, 0), braille::glyph(bit), "dot ({x}, {y})");
601 canvas.unplot(i32::from(x), i32::from(y));
602 }
603 }
604 }
605}