drawille_nostd/
lib.rs

1//! `drawille-nostd` – a `no_std` version of the [drawille](https://github.com/ftxqxd/drawille-rs)
2//! crate. This crate provides an interface for utilising Braille characters to draw a picture to a
3//! terminal, allowing no colour support.
4//!
5//! # Example
6//!
7//! ```
8//! extern crate drawille_nostd;
9//!
10//! use drawille_nostd::Canvas;
11//!
12//! fn main() {
13//!     let mut canvas = Canvas::new(10, 10);
14//!     canvas.set(5, 4);
15//!     canvas.line(2, 2, 8, 8);
16//!     assert_eq!(canvas.frame(), [
17//! " ⢄    ",
18//! "  ⠙⢄  ",
19//! "    ⠁ "].join("\n"));
20//! }
21//! ```
22
23#![cfg_attr(not(test), no_std)]
24
25use core::cmp;
26use core::{char, f32};
27
28extern crate alloc;
29
30use alloc::collections::BTreeMap;
31use alloc::format;
32use alloc::string::String;
33use alloc::vec::Vec;
34
35#[allow(unused_imports)]
36use num_traits::real::Real;
37
38static PIXEL_MAP: [[u8; 2]; 4] = [[0x01, 0x08], [0x02, 0x10], [0x04, 0x20], [0x40, 0x80]];
39
40/// A canvas object that can be used to draw to the terminal using Braille characters.
41#[derive(Clone, Debug, PartialEq, Eq)]
42pub struct Canvas {
43    chars: BTreeMap<(u16, u16), (u8, char, bool)>,
44    width: u16,
45    height: u16,
46}
47
48impl Canvas {
49    /// Creates a new `Canvas` with the given width and height.
50    ///
51    /// Note that the `Canvas` can still draw outside the given dimensions (expanding the canvas)
52    /// if a pixel is set outside the dimensions.
53    pub fn new(width: u32, height: u32) -> Canvas {
54        Canvas {
55            chars: BTreeMap::default(),
56            width: (width / 2) as u16,
57            height: (height / 4) as u16,
58        }
59    }
60
61    /// Clears the canvas.
62    pub fn clear(&mut self) {
63        self.chars.clear();
64    }
65
66    /// Sets a pixel at the specified coordinates.
67    pub fn set(&mut self, x: u32, y: u32) {
68        let (row, col) = ((x / 2) as u16, (y / 4) as u16);
69        let a = self.chars.entry((row, col)).or_insert((0, ' ', false));
70        a.0 |= PIXEL_MAP[y as usize % 4][x as usize % 2];
71        a.1 = ' ';
72        a.2 = false;
73    }
74
75    /// Sets a letter at the specified coordinates.
76    pub fn set_char(&mut self, x: u32, y: u32, c: char) {
77        let (row, col) = ((x / 2) as u16, (y / 4) as u16);
78        let a = self.chars.entry((row, col)).or_insert((0, ' ', false));
79        a.0 = 0;
80        a.1 = c;
81        a.2 = false;
82    }
83
84    /// Draws text at the specified coordinates (top-left of the text) up to max_width length
85    pub fn text(&mut self, x: u32, y: u32, max_width: u32, text: &str) {
86        for (i, c) in text.chars().enumerate() {
87            let w = i as u32 * 2;
88            if w > max_width {
89                return;
90            }
91            self.set_char(x + w, y, c);
92        }
93    }
94
95    /// Deletes a pixel at the specified coordinates.
96    pub fn unset(&mut self, x: u32, y: u32) {
97        let (row, col) = ((x / 2) as u16, (y / 4) as u16);
98        let a = self.chars.entry((row, col)).or_insert((0, ' ', false));
99        a.0 &= !PIXEL_MAP[y as usize % 4][x as usize % 2];
100    }
101
102    /// Toggles a pixel at the specified coordinates.
103    pub fn toggle(&mut self, x: u32, y: u32) {
104        let (row, col) = ((x / 2) as u16, (y / 4) as u16);
105        let a = self.chars.entry((row, col)).or_insert((0, ' ', false));
106        a.0 ^= PIXEL_MAP[y as usize % 4][x as usize % 2];
107    }
108
109    /// Detects whether the pixel at the given coordinates is set.
110    pub fn get(&self, x: u32, y: u32) -> bool {
111        let (row, col) = ((x / 2) as u16, (y / 4) as u16);
112        self.chars.get(&(row, col)).map_or(false, |a| {
113            let dot_index = PIXEL_MAP[y as usize % 4][x as usize % 2];
114            a.0 & dot_index != 0
115        })
116    }
117
118    /// Returns a `Vec` of each row of the `Canvas`.
119    ///
120    /// Note that each row is actually four pixels high due to the fact that a single Braille
121    /// character spans two by four pixels.
122    pub fn rows(&self) -> Vec<String> {
123        let mut maxrow = self.width;
124        let mut maxcol = self.height;
125        for &(x, y) in self.chars.keys() {
126            if x > maxrow {
127                maxrow = x;
128            }
129            if y > maxcol {
130                maxcol = y;
131            }
132        }
133
134        let mut result = Vec::with_capacity(maxcol as usize + 1);
135        for y in 0..=maxcol {
136            let mut row = String::with_capacity(maxrow as usize + 1);
137            for x in 0..=maxrow {
138                let cell = self.chars.get(&(x, y)).cloned().unwrap_or((0, ' ', false));
139                match cell {
140                    (0, _, _) => row.push(cell.1),
141                    (_, _, false) => row.push(char::from_u32(0x2800 + cell.0 as u32).unwrap()),
142                    (_, _, true) => {
143                        row = format!(
144                            "{0}{1}",
145                            row,
146                            String::from(char::from_u32(0x2800 + cell.0 as u32).unwrap())
147                        )
148                    }
149                };
150            }
151            result.push(row);
152        }
153        result
154    }
155
156    /// Draws the canvas to a `String` and returns it.
157    pub fn frame(&self) -> String {
158        self.rows().join("\n")
159    }
160
161    /// Draws a line from `(x1, y1)` to `(x2, y2)` onto the `Canvas`.
162    pub fn line(&mut self, x1: u32, y1: u32, x2: u32, y2: u32) {
163        let xdiff = cmp::max(x1, x2) - cmp::min(x1, x2);
164        let ydiff = cmp::max(y1, y2) - cmp::min(y1, y2);
165        let xdir = if x1 <= x2 { 1 } else { -1 };
166        let ydir = if y1 <= y2 { 1 } else { -1 };
167
168        let r = cmp::max(xdiff, ydiff);
169
170        for i in 0..=r {
171            let mut x = x1 as i32;
172            let mut y = y1 as i32;
173
174            if ydiff != 0 {
175                y += ((i * ydiff) / r) as i32 * ydir;
176            }
177            if xdiff != 0 {
178                x += ((i * xdiff) / r) as i32 * xdir;
179            }
180
181            self.set(x as u32, y as u32);
182        }
183    }
184}
185
186/// A ‘turtle’ that can walk around a canvas drawing lines.
187pub struct Turtle {
188    pub x: f32,
189    pub y: f32,
190    pub brush: bool,
191    pub rotation: f32,
192    pub cvs: Canvas,
193}
194
195impl Turtle {
196    /// Create a new `Turtle`, starting at the given coordinates.
197    ///
198    /// The turtle starts with its brush down, facing right.
199    pub fn new(x: f32, y: f32) -> Turtle {
200        Turtle {
201            cvs: Canvas::new(0, 0),
202            x,
203            y,
204            brush: true,
205            rotation: 0.0,
206        }
207    }
208
209    /// Creates a new `Turtle` with the provided `Canvas`, starting at the given coordinates.
210    ///
211    /// The turtle starts with its brush down, facing right.
212    pub fn from_canvas(x: f32, y: f32, cvs: Canvas) -> Turtle {
213        Turtle {
214            cvs,
215            x,
216            y,
217            brush: true,
218            rotation: 0.0,
219        }
220    }
221
222    /// Sets the width of a `Turtle`’s `Canvas`, and return it for use again.
223    pub fn width(mut self, width: u32) -> Turtle {
224        self.cvs.width = width as u16;
225        self
226    }
227
228    /// Sets the height of a `Turtle`’s `Canvas`, and return it for use again.
229    pub fn height(mut self, height: u32) -> Turtle {
230        self.cvs.height = height as u16;
231        self
232    }
233
234    /// Lifts the `Turtle`’s brush.
235    pub fn up(&mut self) {
236        self.brush = false;
237    }
238
239    /// Puts down the `Turtle`’s brush.
240    pub fn down(&mut self) {
241        self.brush = true;
242    }
243
244    /// Toggles the `Turtle`’s brush.
245    pub fn toggle(&mut self) {
246        self.brush = !self.brush;
247    }
248
249    /// Moves the `Turtle` forward by `dist` steps.
250    pub fn forward(&mut self, dist: f32) {
251        let x = self.x + degrees_to_radians(self.rotation).cos() * dist;
252        let y = self.y + degrees_to_radians(self.rotation).sin() * dist;
253        self.teleport(x, y);
254    }
255
256    /// Moves the `Turtle` backward by `dist` steps.
257    pub fn back(&mut self, dist: f32) {
258        self.forward(-dist);
259    }
260
261    /// Teleports the `Turtle` to the given coordinates.
262    ///
263    /// Note that this draws a line between the old position and the new one if the `Turtle`’s
264    /// brush is down.
265    pub fn teleport(&mut self, x: f32, y: f32) {
266        if self.brush {
267            self.cvs.line(
268                cmp::max(0, self.x.round() as i32) as u32,
269                cmp::max(0, self.y.round() as i32) as u32,
270                cmp::max(0, x.round() as i32) as u32,
271                cmp::max(0, y.round() as i32) as u32,
272            );
273        }
274        self.x = x;
275        self.y = y;
276    }
277
278    /// Turns the `Turtle` right (clockwise) by `angle` degrees.
279    pub fn right(&mut self, angle: f32) {
280        self.rotation += angle;
281    }
282
283    /// Turns the `Turtle` left (clockwise) by `angle` degrees.
284    pub fn left(&mut self, angle: f32) {
285        self.rotation -= angle;
286    }
287
288    /// Writes the `Turtle`’s `Canvas` to a `String` and returns it.
289    pub fn frame(&self) -> String {
290        self.cvs.frame()
291    }
292}
293
294fn degrees_to_radians(deg: f32) -> f32 {
295    deg * (f32::consts::PI / 180.0f32)
296}