use alloc::vec;
use alloc::vec::Vec;
use retroglyph_core::color::Color;
use retroglyph_core::symbols::braille;
use retroglyph_core::symbols::{Glyph, quantize_half_block, quantize_quadrant, quantize_sextant};
#[derive(Debug, Clone)]
struct SubCanvas {
width: usize,
height: usize,
pixels: Vec<Color>,
clear: Color,
}
impl SubCanvas {
fn new(width: usize, height: usize, clear: Color) -> Self {
Self {
width,
height,
pixels: vec![clear; width * height],
clear,
}
}
fn clear(&mut self) {
self.pixels.fill(self.clear);
}
fn set(&mut self, x: i32, y: i32, color: Color) {
if x < 0 || y < 0 {
return;
}
#[allow(clippy::cast_sign_loss)] let (x, y) = (x as usize, y as usize);
if x >= self.width || y >= self.height {
return;
}
self.pixels[y * self.width + x] = color;
}
fn get(&self, x: usize, y: usize) -> Color {
self.pixels
.get(y * self.width + x)
.copied()
.unwrap_or(self.clear)
}
fn rgb_at(&self, x: usize, y: usize) -> (u8, u8, u8) {
let fallback = self.clear.resolve_rgb((0, 0, 0));
self.get(x, y).resolve_rgb(fallback)
}
}
#[derive(Debug, Clone)]
pub struct HalfBlockCanvas {
canvas: SubCanvas,
cols: usize,
rows: usize,
}
impl HalfBlockCanvas {
#[must_use]
pub fn new(cols: u16, rows: u16, clear: Color) -> Self {
let (cols, rows) = (cols as usize, rows as usize);
Self {
canvas: SubCanvas::new(cols, rows * 2, clear),
cols,
rows,
}
}
#[must_use]
pub const fn size(&self) -> (usize, usize) {
(self.canvas.width, self.canvas.height)
}
pub fn clear(&mut self) {
self.canvas.clear();
}
pub fn plot(&mut self, x: i32, y: i32, color: Color) {
self.canvas.set(x, y, color);
}
pub fn cells(&self) -> impl Iterator<Item = (u16, u16, Glyph)> + '_ {
(0..self.rows).flat_map(move |row| {
(0..self.cols).map(move |col| {
let glyph = quantize_half_block([
self.canvas.rgb_at(col, row * 2),
self.canvas.rgb_at(col, row * 2 + 1),
]);
(
u16::try_from(col).unwrap_or(u16::MAX),
u16::try_from(row).unwrap_or(u16::MAX),
glyph,
)
})
})
}
}
#[derive(Debug, Clone)]
pub struct QuadrantCanvas {
canvas: SubCanvas,
cols: usize,
rows: usize,
}
impl QuadrantCanvas {
#[must_use]
pub fn new(cols: u16, rows: u16, clear: Color) -> Self {
let (cols, rows) = (cols as usize, rows as usize);
Self {
canvas: SubCanvas::new(cols * 2, rows * 2, clear),
cols,
rows,
}
}
#[must_use]
pub const fn size(&self) -> (usize, usize) {
(self.canvas.width, self.canvas.height)
}
pub fn clear(&mut self) {
self.canvas.clear();
}
pub fn plot(&mut self, x: i32, y: i32, color: Color) {
self.canvas.set(x, y, color);
}
pub fn cells(&self) -> impl Iterator<Item = (u16, u16, Glyph)> + '_ {
(0..self.rows).flat_map(move |row| {
(0..self.cols).map(move |col| {
let (x, y) = (col * 2, row * 2);
let glyph = quantize_quadrant([
self.canvas.rgb_at(x, y),
self.canvas.rgb_at(x + 1, y),
self.canvas.rgb_at(x, y + 1),
self.canvas.rgb_at(x + 1, y + 1),
]);
(
u16::try_from(col).unwrap_or(u16::MAX),
u16::try_from(row).unwrap_or(u16::MAX),
glyph,
)
})
})
}
}
#[derive(Debug, Clone)]
pub struct SextantCanvas {
canvas: SubCanvas,
cols: usize,
rows: usize,
}
impl SextantCanvas {
#[must_use]
pub fn new(cols: u16, rows: u16, clear: Color) -> Self {
let (cols, rows) = (cols as usize, rows as usize);
Self {
canvas: SubCanvas::new(cols * 2, rows * 3, clear),
cols,
rows,
}
}
#[must_use]
pub const fn size(&self) -> (usize, usize) {
(self.canvas.width, self.canvas.height)
}
pub fn clear(&mut self) {
self.canvas.clear();
}
pub fn plot(&mut self, x: i32, y: i32, color: Color) {
self.canvas.set(x, y, color);
}
pub fn cells(&self) -> impl Iterator<Item = (u16, u16, Glyph)> + '_ {
(0..self.rows).flat_map(move |row| {
(0..self.cols).map(move |col| {
let (x, y) = (col * 2, row * 3);
let glyph = quantize_sextant([
self.canvas.rgb_at(x, y),
self.canvas.rgb_at(x + 1, y),
self.canvas.rgb_at(x, y + 1),
self.canvas.rgb_at(x + 1, y + 1),
self.canvas.rgb_at(x, y + 2),
self.canvas.rgb_at(x + 1, y + 2),
]);
(
u16::try_from(col).unwrap_or(u16::MAX),
u16::try_from(row).unwrap_or(u16::MAX),
glyph,
)
})
})
}
}
#[derive(Debug, Clone)]
pub struct BrailleCanvas {
cols: usize,
rows: usize,
dots: Vec<u8>,
}
impl BrailleCanvas {
#[must_use]
pub fn new(cols: u16, rows: u16) -> Self {
let (cols, rows) = (cols as usize, rows as usize);
Self {
cols,
rows,
dots: vec![0; cols * rows],
}
}
#[must_use]
pub const fn size(&self) -> (usize, usize) {
(self.cols * 2, self.rows * 4)
}
pub fn clear(&mut self) {
self.dots.fill(0);
}
pub fn plot(&mut self, x: i32, y: i32) {
if let Some((index, mask)) = self.locate(x, y) {
self.dots[index] |= mask;
}
}
pub fn unplot(&mut self, x: i32, y: i32) {
if let Some((index, mask)) = self.locate(x, y) {
self.dots[index] &= !mask;
}
}
#[must_use]
pub fn get(&self, x: i32, y: i32) -> bool {
self.locate(x, y)
.is_some_and(|(index, mask)| self.dots[index] & mask != 0)
}
const fn locate(&self, x: i32, y: i32) -> Option<(usize, u8)> {
if x < 0 || y < 0 {
return None;
}
#[allow(clippy::cast_sign_loss)] let (x, y) = (x as usize, y as usize);
let (cell_x, cell_y) = (x / 2, y / 4);
if cell_x >= self.cols || cell_y >= self.rows {
return None;
}
Some((cell_y * self.cols + cell_x, braille::DOTS[y % 4][x % 2]))
}
pub fn line(&mut self, x0: i32, y0: i32, x1: i32, y1: i32) {
let (dx, dy) = ((x1 - x0).abs(), -(y1 - y0).abs());
let (sx, sy) = (if x0 < x1 { 1 } else { -1 }, if y0 < y1 { 1 } else { -1 });
let (mut x, mut y) = (x0, y0);
let mut err = dx + dy;
loop {
self.plot(x, y);
if x == x1 && y == y1 {
break;
}
let e2 = 2 * err;
if e2 >= dy {
err += dy;
x += sx;
}
if e2 <= dx {
err += dx;
y += sy;
}
}
}
#[must_use]
pub fn glyph(&self, col: usize, row: usize) -> char {
let bits = if col < self.cols && row < self.rows {
self.dots[row * self.cols + col]
} else {
0
};
braille::glyph(bits)
}
pub fn cells(&self) -> impl Iterator<Item = (u16, u16, char)> + '_ {
(0..self.rows).flat_map(move |row| {
(0..self.cols).map(move |col| {
(
u16::try_from(col).unwrap_or(u16::MAX),
u16::try_from(row).unwrap_or(u16::MAX),
self.glyph(col, row),
)
})
})
}
}
#[cfg(test)]
mod tests {
use alloc::vec;
use alloc::vec::Vec;
use super::{BrailleCanvas, Glyph, HalfBlockCanvas, QuadrantCanvas, SextantCanvas, braille};
use retroglyph_core::color::Color;
use retroglyph_core::symbols::{HALF_BLOCKS, QUADRANTS};
const RED: Color = Color::rgb(255, 0, 0);
const BLUE: Color = Color::rgb(0, 0, 255);
const BLACK: Color = Color::rgb(0, 0, 0);
#[test]
fn half_block_canvas_has_double_vertical_resolution() {
let canvas = HalfBlockCanvas::new(10, 5, BLACK);
assert_eq!(canvas.size(), (10, 10));
assert_eq!(canvas.cells().count(), 50);
}
fn resolve(glyph: Glyph, bank: &[char]) -> Vec<Color> {
let mask = bank
.iter()
.position(|&c| c == glyph.ch)
.expect("glyph must come from its own bank");
let sub_cells = bank.len().trailing_zeros() as usize;
(0..sub_cells)
.map(|i| {
if mask & (1 << i) != 0 {
glyph.fg
} else {
glyph.bg
}
})
.collect()
}
#[test]
fn half_block_renders_the_half_that_was_plotted() {
let mut canvas = HalfBlockCanvas::new(1, 1, BLACK);
canvas.plot(0, 0, RED);
let (_, _, glyph) = canvas.cells().next().expect("one cell");
assert_eq!(resolve(glyph, &HALF_BLOCKS), vec![RED, BLACK], "top set");
canvas.clear();
canvas.plot(0, 1, RED);
let (_, _, glyph) = canvas.cells().next().expect("one cell");
assert_eq!(resolve(glyph, &HALF_BLOCKS), vec![BLACK, RED], "bottom set");
}
#[test]
fn quadrant_canvas_has_double_resolution_on_both_axes() {
let canvas = QuadrantCanvas::new(8, 4, BLACK);
assert_eq!(canvas.size(), (16, 8));
assert_eq!(canvas.cells().count(), 32);
}
#[test]
fn quadrant_renders_the_corner_that_was_plotted() {
for (x, y, index) in [(0, 0, 0), (1, 0, 1), (0, 1, 2), (1, 1, 3)] {
let mut canvas = QuadrantCanvas::new(1, 1, BLACK);
canvas.plot(x, y, RED);
let (_, _, glyph) = canvas.cells().next().expect("one cell");
let mut expected = vec![BLACK; 4];
expected[index] = RED;
assert_eq!(resolve(glyph, &QUADRANTS), expected, "corner ({x}, {y})");
}
}
#[test]
fn canvases_preserve_plotted_colors_exactly() {
let mut canvas = HalfBlockCanvas::new(1, 1, BLACK);
canvas.plot(0, 0, RED);
canvas.plot(0, 1, BLUE);
let (_, _, glyph) = canvas.cells().next().expect("one cell");
assert_eq!(resolve(glyph, &HALF_BLOCKS), vec![RED, BLUE]);
}
#[test]
fn sextant_canvas_is_two_by_three() {
let canvas = SextantCanvas::new(6, 3, BLACK);
assert_eq!(canvas.size(), (12, 9));
assert_eq!(canvas.cells().count(), 18);
}
#[test]
fn canvases_ignore_out_of_bounds_plots() {
let mut half = HalfBlockCanvas::new(2, 2, BLACK);
let mut quad = QuadrantCanvas::new(2, 2, BLACK);
let mut sext = SextantCanvas::new(2, 2, BLACK);
for (x, y) in [(-1, 0), (0, -1), (999, 0), (0, 999), (-999, -999)] {
half.plot(x, y, RED);
quad.plot(x, y, RED);
sext.plot(x, y, RED);
}
assert!(half.cells().all(|(_, _, g)| g.ch == ' ' || g.fg == BLACK));
assert!(quad.cells().all(|(_, _, g)| g.ch == ' ' || g.fg == BLACK));
assert!(sext.cells().all(|(_, _, g)| g.ch == ' ' || g.fg == BLACK));
}
#[test]
fn clearing_a_canvas_erases_everything() {
let mut canvas = QuadrantCanvas::new(2, 2, BLACK);
canvas.plot(0, 0, RED);
canvas.clear();
let glyphs: Vec<char> = canvas.cells().map(|(_, _, g)| g.ch).collect();
assert!(glyphs.iter().all(|&c| c == ' '), "left over: {glyphs:?}");
}
#[test]
fn braille_canvas_is_two_by_four() {
let canvas = BrailleCanvas::new(20, 10);
assert_eq!(canvas.size(), (40, 40));
assert_eq!(canvas.cells().count(), 200);
}
#[test]
fn braille_plot_round_trips_through_get() {
let mut canvas = BrailleCanvas::new(4, 2);
for y in 0..8 {
for x in 0..8 {
assert!(!canvas.get(x, y));
canvas.plot(x, y);
assert!(canvas.get(x, y), "dot ({x}, {y}) did not stick");
canvas.unplot(x, y);
assert!(!canvas.get(x, y), "dot ({x}, {y}) did not clear");
}
}
}
#[test]
fn braille_glyph_is_blank_when_empty_and_full_when_saturated() {
let mut canvas = BrailleCanvas::new(1, 1);
assert_eq!(canvas.glyph(0, 0), braille::BLANK);
for y in 0..4 {
for x in 0..2 {
canvas.plot(x, y);
}
}
assert_eq!(canvas.glyph(0, 0), '\u{28FF}', "all 8 dots set");
}
#[test]
fn braille_glyphs_stay_inside_the_unicode_block() {
let mut canvas = BrailleCanvas::new(3, 3);
canvas.line(0, 0, 5, 11);
for (_, _, glyph) in canvas.cells() {
let code = glyph as u32;
assert!(
(0x2800..0x2900).contains(&code),
"{glyph:?} is outside the Braille block"
);
}
}
#[test]
fn braille_out_of_bounds_is_ignored_not_panicked() {
let mut canvas = BrailleCanvas::new(2, 2);
for (x, y) in [(-1, 0), (0, -1), (999, 0), (0, 999)] {
canvas.plot(x, y);
canvas.unplot(x, y);
assert!(!canvas.get(x, y));
}
assert!(canvas.cells().all(|(_, _, g)| g == braille::BLANK));
}
#[test]
fn dot_bit_positions_match_symbols_braille_dots() {
let mut canvas = BrailleCanvas::new(1, 1);
for y in 0..4u8 {
for x in 0..2u8 {
canvas.plot(i32::from(x), i32::from(y));
let bit = braille::DOTS[y as usize][x as usize];
assert_eq!(canvas.glyph(0, 0), braille::glyph(bit), "dot ({x}, {y})");
canvas.unplot(i32::from(x), i32::from(y));
}
}
}
}