Skip to main content

termplot_rs/
canvas.rs

1use colored::{Color, Colorize};
2
3pub struct BrailleCanvas {
4    pub width: usize,
5    pub height: usize,
6    // Matriz de máscaras de bits para la forma
7    grid: Vec<Vec<u8>>,
8    // Matriz paralela para almacenar el color de cada celda de carácter
9    color_grid: Vec<Vec<Option<Color>>>,
10    // Capa de texto
11    text_layer: Vec<Vec<Option<char>>>,
12}
13
14impl BrailleCanvas {
15    pub fn new(width: usize, height: usize) -> Self {
16        Self {
17            width,
18            height,
19            grid: vec![vec![0u8; width]; height],
20            // Inicialmente sin color (None se renderizará como blanco/por defecto)
21            color_grid: vec![vec![None; width]; height],
22            text_layer: vec![vec![None; width]; height],
23        }
24    }
25
26    pub fn clear(&mut self) {
27        for row in self.grid.iter_mut() {
28            row.fill(0);
29        }
30        for row in self.color_grid.iter_mut() {
31            row.fill(None);
32        }
33        for row in self.text_layer.iter_mut() {
34            row.fill(None);
35        }
36    }
37
38    /// Enciende un píxel virtual. Acepta un color opcional.
39    pub fn set_pixel(&mut self, px: usize, py: usize, color: Option<Color>) {
40        let pixel_width = self.width * 2;
41        let pixel_height = self.height * 4;
42
43        if px >= pixel_width || py >= pixel_height {
44            return;
45        }
46
47        // Coordenadas de la celda de carácter
48        let col_char = px / 2;
49        // Invertir Y para sistema cartesiano (0,0 abajo-izquierda)
50        let row_char = (pixel_height - 1 - py) / 4;
51
52        if row_char >= self.height || col_char >= self.width {
53            return;
54        }
55
56        // Coordenadas dentro del carácter Braille
57        let dx = px % 2;
58        let dy = (pixel_height - 1 - py) % 4;
59
60        // Máscaras Unicode Braille estándar
61        let mask = match (dx, dy) {
62            (0, 0) => 0x01,
63            (1, 0) => 0x08,
64            (0, 1) => 0x02,
65            (1, 1) => 0x10,
66            (0, 2) => 0x04,
67            (1, 2) => 0x20,
68            (0, 3) => 0x40,
69            (1, 3) => 0x80,
70            _ => 0,
71        };
72
73        self.grid[row_char][col_char] |= mask;
74
75        // Si se especifica un color, actualizamos el color de toda la celda
76        if let Some(c) = color {
77            self.color_grid[row_char][col_char] = Some(c);
78        }
79    }
80
81    /// Dibuja una línea (Bresenham) con color opcional
82    pub fn line(&mut self, x0: isize, y0: isize, x1: isize, y1: isize, color: Option<Color>) {
83        let dx = (x1 - x0).abs();
84        let dy = -(y1 - y0).abs();
85        let sx = if x0 < x1 { 1 } else { -1 };
86        let sy = if y0 < y1 { 1 } else { -1 };
87        let mut err = dx + dy;
88        let mut x = x0;
89        let mut y = y0;
90
91        loop {
92            if x >= 0 && y >= 0 {
93                self.set_pixel(x as usize, y as usize, color);
94            }
95            if x == x1 && y == y1 {
96                break;
97            }
98            let e2 = 2 * err;
99            if e2 >= dy {
100                err += dy;
101                x += sx;
102            }
103            if e2 <= dx {
104                err += dx;
105                y += sy;
106            }
107        }
108    }
109
110    /// Dibuja un círculo (Algoritmo de punto medio)
111    pub fn circle(&mut self, xc: isize, yc: isize, r: isize, color: Option<Color>) {
112        let mut x = 0;
113        let mut y = r;
114        let mut d = 3 - 2 * r;
115
116        // Función auxiliar para dibujar los 8 octantes simétricos
117        let mut draw_octants = |cx: isize, cy: isize, x: isize, y: isize| {
118            let points = [
119                (cx + x, cy + y),
120                (cx - x, cy + y),
121                (cx + x, cy - y),
122                (cx - x, cy - y),
123                (cx + y, cy + x),
124                (cx - y, cy + x),
125                (cx + y, cy - x),
126                (cx - y, cy - x),
127            ];
128            for (px, py) in points {
129                if px >= 0 && py >= 0 {
130                    self.set_pixel(px as usize, py as usize, color);
131                }
132            }
133        };
134
135        draw_octants(xc, yc, x, y);
136        while y >= x {
137            x += 1;
138            if d > 0 {
139                y -= 1;
140                d = d + 4 * (x - y) + 10;
141            } else {
142                d = d + 4 * x + 6;
143            }
144            draw_octants(xc, yc, x, y);
145        }
146    }
147
148    pub fn set_char(&mut self, char_x: usize, char_y: usize, c: char, color: Option<Color>) {
149        // Invertimos Y como siempre
150        let row = self.height.saturating_sub(1).saturating_sub(char_y);
151
152        if row < self.height && char_x < self.width {
153            self.text_layer[row][char_x] = Some(c);
154            if let Some(col) = color {
155                self.color_grid[row][char_x] = Some(col);
156            }
157        }
158    }
159
160    pub fn set_char_vertical(
161        &mut self,
162        char_x: usize,
163        char_y_start: usize,
164        text: &str,
165        color: Option<Color>,
166    ) {
167        for (i, ch) in text.chars().enumerate() {
168            let y = char_y_start.saturating_sub(i);
169            self.set_char(char_x, y, ch, color); // Reutiliza lógica de inversión de Y
170        }
171    }
172    pub fn render(&self) -> String {
173        self.render_with_options(true, None)
174    }
175
176    /// RENDER NO COLOR
177    pub fn render_no_color(&self) -> String {
178        let mut output = String::new();
179        for row_masks in &self.grid {
180            for &mask in row_masks {
181                let ch = std::char::from_u32(0x2800 + mask as u32).unwrap_or(' ');
182                output.push(ch);
183            }
184            output.push('\n');
185        }
186        output
187    }
188
189    /// Renderiza el canvas combinando forma y color con opciones extendidas
190    pub fn render_with_options(&self, show_border: bool, title: Option<&str>) -> String {
191        let mut output = String::new();
192
193        // Título opcional centrado
194        if let Some(t) = title {
195            output.push_str(&format!("{:^width$}\n", t, width = self.width + 2));
196        }
197
198        // Borde superior opcional
199        if show_border {
200            output.push_str(&format!("┌{}┐\n", "─".repeat(self.width)));
201        }
202
203        for (r_idx, row_masks) in self.grid.iter().enumerate() {
204            if show_border {
205                output.push('│');
206            }
207
208            for (c_idx, &mask) in row_masks.iter().enumerate() {
209                let char_to_print = if let Some(c) = self.text_layer[r_idx][c_idx] {
210                    c.to_string()
211                } else {
212                    std::char::from_u32(0x2800 + mask as u32)
213                        .unwrap_or(' ')
214                        .to_string()
215                };
216
217                if let Some(color) = self.color_grid[r_idx][c_idx] {
218                    output.push_str(&char_to_print.color(color).to_string());
219                } else {
220                    output.push_str(&char_to_print);
221                }
222            }
223
224            if show_border {
225                output.push('│');
226            }
227            output.push('\n');
228        }
229
230        // Borde inferior opcional
231        if show_border {
232            output.push_str(&format!("└{}┘", "─".repeat(self.width)));
233        }
234
235        output
236    }
237}