drawille/
lib.rs

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