1use colored::{Color, Colorize};
2use std::fmt::Write;
3
4pub struct BrailleCanvas {
5 pub width: usize,
6 pub height: usize,
7 buffer: Vec<u8>,
9 colors: Vec<Option<Color>>,
11 text_layer: Vec<Option<char>>,
13 }
15
16impl BrailleCanvas {
17 pub fn new(width: usize, height: usize) -> Self {
18 let size = width * height;
19 Self {
20 width,
21 height,
22 buffer: vec![0u8; size],
23 colors: vec![None; size],
24 text_layer: vec![None; size],
25 }
26 }
27
28 #[inline]
30 pub fn pixel_width(&self) -> usize {
31 self.width * 2
32 }
33
34 #[inline]
36 pub fn pixel_height(&self) -> usize {
37 self.height * 4
38 }
39
40 pub fn clear(&mut self) {
41 self.buffer.fill(0);
42 self.colors.fill(None);
43 self.text_layer.fill(None);
44 }
45
46 #[inline]
49 fn idx(&self, col: usize, row: usize) -> usize {
50 row * self.width + col
51 }
52
53 fn set_pixel_impl(&mut self, px: usize, py: usize, color: Option<Color>) {
54 if px >= self.pixel_width() || py >= self.pixel_height() {
55 return;
56 }
57
58 let col = px / 2;
59 let row = py / 4;
60
61 let sub_x = px % 2;
62 let sub_y = py % 4;
63
64 let mask = match (sub_x, sub_y) {
65 (0, 0) => 0x01, (1, 0) => 0x08,
66 (0, 1) => 0x02, (1, 1) => 0x10,
67 (0, 2) => 0x04, (1, 2) => 0x20,
68 (0, 3) => 0x40, (1, 3) => 0x80,
69 _ => 0,
70 };
71
72 let index = self.idx(col, row);
73 self.buffer[index] |= mask;
74
75 if let Some(c) = color {
76 self.colors[index] = Some(c);
77 }
78 }
79
80 pub fn set_pixel(&mut self, x: usize, y: usize, color: Option<Color>) {
84 let inverted_y = self.pixel_height().saturating_sub(1).saturating_sub(y);
85 self.set_pixel_impl(x, inverted_y, color);
86 }
87
88 pub fn set_pixel_screen(&mut self, x: usize, y: usize, color: Option<Color>) {
90 self.set_pixel_impl(x, y, color);
91 }
92
93 pub fn line(&mut self, x0: isize, y0: isize, x1: isize, y1: isize, color: Option<Color>) {
95 self.bresenham(x0, y0, x1, y1, color, true);
96 }
97
98 pub fn line_screen(&mut self, x0: isize, y0: isize, x1: isize, y1: isize, color: Option<Color>) {
100 self.bresenham(x0, y0, x1, y1, color, false);
101 }
102
103 fn bresenham(&mut self, x0: isize, y0: isize, x1: isize, y1: isize, color: Option<Color>, cartesian: bool) {
104 let dx = (x1 - x0).abs();
105 let dy = -(y1 - y0).abs();
106 let sx = if x0 < x1 { 1 } else { -1 };
107 let sy = if y0 < y1 { 1 } else { -1 };
108 let mut err = dx + dy;
109 let mut x = x0;
110 let mut y = y0;
111
112 loop {
113 if x >= 0 && y >= 0 {
114 if cartesian {
115 self.set_pixel(x as usize, y as usize, color);
116 } else {
117 self.set_pixel_screen(x as usize, y as usize, color);
118 }
119 }
120 if x == x1 && y == y1 { break; }
121 let e2 = 2 * err;
122 if e2 >= dy { err += dy; x += sx; }
123 if e2 <= dx { err += dx; y += sy; }
124 }
125 }
126
127 pub fn circle(&mut self, xc: isize, yc: isize, r: isize, color: Option<Color>) {
128 let mut x = 0;
129 let mut y = r;
130 let mut d = 3 - 2 * r;
131
132 let mut draw_octants = |cx: isize, cy: isize, x: isize, y: isize| {
133 let points = [
134 (cx + x, cy + y), (cx - x, cy + y), (cx + x, cy - y), (cx - x, cy - y),
135 (cx + y, cy + x), (cx - y, cy + x), (cx + y, cy - x), (cx - y, cy - x),
136 ];
137 for (px, py) in points {
138 if px >= 0 && py >= 0 {
139 self.set_pixel(px as usize, py as usize, color);
140 }
141 }
142 };
143
144 draw_octants(xc, yc, x, y);
145 while y >= x {
146 x += 1;
147 if d > 0 {
148 y -= 1;
149 d = d + 4 * (x - y) + 10;
150 } else {
151 d = d + 4 * x + 6;
152 }
153 draw_octants(xc, yc, x, y);
154 }
155 }
156
157 pub fn set_char(&mut self, col: usize, row: usize, c: char, color: Option<Color>) {
158 let inverted_row = self.height.saturating_sub(1).saturating_sub(row);
160
161 if col < self.width && inverted_row < self.height {
162 let idx = self.idx(col, inverted_row);
163 self.text_layer[idx] = Some(c);
164 if let Some(col_val) = color {
165 self.colors[idx] = Some(col_val);
166 }
167 }
168 }
169
170 pub fn render(&self) -> String {
173 self.render_with_options(true, None)
174 }
175
176 pub fn render_no_color(&self) -> String {
177 let mut output = String::with_capacity(self.width * self.height + self.height);
178 for row in 0..self.height {
179 for col in 0..self.width {
180 let idx = self.idx(col, row);
181 let mask = self.buffer[idx];
182 let ch = std::char::from_u32(0x2800 + mask as u32).unwrap_or(' ');
183 output.push(ch);
184 }
185 output.push('\n');
186 }
187 output
188 }
189
190 pub fn render_with_options(&self, show_border: bool, title: Option<&str>) -> String {
191 let mut out = String::with_capacity(self.width * self.height * 15);
192
193 if let Some(t) = title {
194 let _ = writeln!(out, "{:^width$}", t, width = self.width + 2);
195 }
196
197 if show_border {
198 out.push('┌');
199 for _ in 0..self.width { out.push('─'); }
200 out.push('┐');
201 out.push('\n');
202 }
203
204 let mut last_color: Option<Color> = None;
205
206 for row in 0..self.height {
207 if show_border { out.push('│'); }
208
209 for col in 0..self.width {
210 let idx = self.idx(col, row);
211
212 let char_to_print = if let Some(c) = self.text_layer[idx] {
213 c
214 } else {
215 let mask = self.buffer[idx];
216 std::char::from_u32(0x2800 + mask as u32).unwrap_or(' ')
217 };
218
219 let current_color = self.colors[idx];
220
221 if current_color != last_color {
223 match current_color {
224 Some(c) => {
225 let ansi_full = format!("{}", "".color(c));
229 let ansi_prefix = ansi_full.replace("\x1b[0m", "");
230 out.push_str(&ansi_prefix);
231 },
232 None => {
233 out.push_str("\x1b[0m");
234 }
235 }
236 last_color = current_color;
237 }
238
239 out.push(char_to_print);
240 }
241
242 if last_color.is_some() {
244 out.push_str("\x1b[0m");
245 last_color = None;
246 }
247
248 if show_border { out.push('│'); }
249 out.push('\n');
250 }
251
252 if show_border {
253 out.push('└');
254 for _ in 0..self.width { out.push('─'); }
255 out.push('┘');
256 }
257
258 out
259 }
260}
261#[cfg(test)]
263mod tests {
264 use super::*;
265
266 #[test]
267 fn test_coordinate_mapping() {
268 let canvas = BrailleCanvas::new(10, 5);
269 assert_eq!(canvas.idx(0, 0), 0);
271 assert_eq!(canvas.idx(0, 1), 10);
273 }
274
275 #[test]
276 fn test_braille_bitmask() {
277 let mut canvas = BrailleCanvas::new(1, 1);
279
280 canvas.set_pixel_screen(0, 0, None);
282 assert_eq!(canvas.buffer[0], 0x01);
283
284 canvas.set_pixel_screen(1, 0, None);
286 assert_eq!(canvas.buffer[0], 0x01 | 0x08); canvas.set_pixel_screen(0, 1, None);
290 assert_eq!(canvas.buffer[0], 0x09 | 0x02); }
292
293 #[test]
294 fn test_clear() {
295 let mut canvas = BrailleCanvas::new(2, 2);
296 canvas.set_pixel(0, 0, None);
297 assert!(canvas.buffer.iter().any(|&x| x != 0));
298
299 canvas.clear();
300 assert!(canvas.buffer.iter().all(|&x| x == 0));
301 assert!(canvas.colors.iter().all(|x| x.is_none()));
302 }
303}