1use std::io::Write;
2use std::path::Path;
3
4use crate::{Error, Result};
5
6pub type Color = [u8; 3];
7
8pub const BG: Color = [16, 19, 25];
9pub const TEXT: Color = [232, 235, 240];
10pub const DIM: Color = [138, 146, 160];
11pub const GRID: Color = [44, 50, 62];
12pub const SEP: Color = [70, 78, 92];
13pub const OURS: Color = [220, 110, 34];
14pub const THEIRS: Color = [74, 142, 230];
15pub const FASTER: Color = [74, 201, 125];
16pub const SLOWER: Color = [235, 88, 88];
17
18const GLYPHS: &[(char, [u8; 7])] = &[
19 ('A', [14, 17, 17, 31, 17, 17, 17]),
20 ('B', [30, 17, 17, 30, 17, 17, 30]),
21 ('C', [14, 17, 16, 16, 16, 17, 14]),
22 ('D', [30, 17, 17, 17, 17, 17, 30]),
23 ('E', [31, 16, 16, 30, 16, 16, 31]),
24 ('F', [31, 16, 16, 30, 16, 16, 16]),
25 ('G', [14, 17, 16, 23, 17, 17, 15]),
26 ('H', [17, 17, 17, 31, 17, 17, 17]),
27 ('I', [14, 4, 4, 4, 4, 4, 14]),
28 ('J', [7, 2, 2, 2, 2, 18, 12]),
29 ('K', [17, 18, 20, 24, 20, 18, 17]),
30 ('L', [16, 16, 16, 16, 16, 16, 31]),
31 ('M', [17, 27, 21, 21, 17, 17, 17]),
32 ('N', [17, 25, 21, 19, 17, 17, 17]),
33 ('O', [14, 17, 17, 17, 17, 17, 14]),
34 ('P', [30, 17, 17, 30, 16, 16, 16]),
35 ('Q', [14, 17, 17, 17, 21, 18, 13]),
36 ('R', [30, 17, 17, 30, 20, 18, 17]),
37 ('S', [15, 16, 16, 14, 1, 1, 30]),
38 ('T', [31, 4, 4, 4, 4, 4, 4]),
39 ('U', [17, 17, 17, 17, 17, 17, 14]),
40 ('V', [17, 17, 17, 17, 17, 10, 4]),
41 ('W', [17, 17, 17, 21, 21, 21, 10]),
42 ('X', [17, 17, 10, 4, 10, 17, 17]),
43 ('Y', [17, 17, 10, 4, 4, 4, 4]),
44 ('Z', [31, 1, 2, 4, 8, 16, 31]),
45 ('0', [14, 17, 19, 21, 25, 17, 14]),
46 ('1', [4, 12, 4, 4, 4, 4, 14]),
47 ('2', [14, 17, 1, 2, 4, 8, 31]),
48 ('3', [30, 1, 1, 14, 1, 1, 30]),
49 ('4', [2, 6, 10, 18, 31, 2, 2]),
50 ('5', [31, 16, 30, 1, 1, 17, 14]),
51 ('6', [6, 8, 16, 30, 17, 17, 14]),
52 ('7', [31, 1, 2, 4, 8, 8, 8]),
53 ('8', [14, 17, 17, 14, 17, 17, 14]),
54 ('9', [14, 17, 17, 15, 1, 2, 12]),
55 ('.', [0, 0, 0, 0, 0, 12, 12]),
56 (',', [0, 0, 0, 0, 12, 4, 8]),
57 (':', [0, 12, 12, 0, 12, 12, 0]),
58 ('-', [0, 0, 0, 31, 0, 0, 0]),
59 ('+', [0, 4, 4, 31, 4, 4, 0]),
60 ('/', [1, 1, 2, 4, 8, 16, 16]),
61 ('(', [2, 4, 8, 8, 8, 4, 2]),
62 (')', [8, 4, 2, 2, 2, 4, 8]),
63 ('%', [25, 26, 2, 4, 8, 11, 19]),
64 ('=', [0, 0, 31, 0, 31, 0, 0]),
65 ('<', [2, 4, 8, 16, 8, 4, 2]),
66 ('>', [8, 4, 2, 1, 2, 4, 8]),
67 ('\'', [4, 4, 8, 0, 0, 0, 0]),
68 ('_', [0, 0, 0, 0, 0, 0, 31]),
69 ('×', [0, 17, 10, 4, 10, 17, 0]),
70 ('*', [0, 21, 14, 31, 14, 21, 0]),
71 ('†', [4, 14, 4, 4, 4, 4, 0]),
72 (';', [0, 12, 12, 0, 12, 4, 8]),
73 ('|', [4, 4, 4, 4, 4, 4, 4]),
74 (' ', [0, 0, 0, 0, 0, 0, 0]),
75];
76
77fn find(c: char) -> Option<[u8; 7]> {
78 let c = c.to_ascii_uppercase();
79 GLYPHS.iter().find(|(g, _)| *g == c).map(|(_, rows)| *rows)
80}
81
82pub fn has_glyph(c: char) -> bool {
83 find(c).is_some()
84}
85
86fn glyph(c: char) -> [u8; 7] {
87 find(c).unwrap_or([0; 7])
88}
89
90pub struct Canvas {
91 width: usize,
92 height: usize,
93 px: Vec<u8>,
94}
95
96impl Canvas {
97 pub fn new(width: usize, height: usize) -> Self {
98 let mut px = Vec::with_capacity(width * height * 3);
99 for _ in 0..width * height {
100 px.extend_from_slice(&BG);
101 }
102 Self { width, height, px }
103 }
104
105 pub fn width(&self) -> usize {
106 self.width
107 }
108
109 pub fn height(&self) -> usize {
110 self.height
111 }
112
113 pub fn pixel(&self, x: usize, y: usize) -> Option<Color> {
114 if x >= self.width || y >= self.height {
115 return None;
116 }
117 let at = (y * self.width + x) * 3;
118 Some([self.px[at], self.px[at + 1], self.px[at + 2]])
119 }
120
121 pub fn rect(&mut self, x: i64, y: i64, w: i64, h: i64, c: Color) {
122 let x0 = x.clamp(0, self.width as i64) as usize;
123 let x1 = (x + w).clamp(0, self.width as i64) as usize;
124 let y0 = y.clamp(0, self.height as i64) as usize;
125 let y1 = (y + h).clamp(0, self.height as i64) as usize;
126 for row in y0..y1 {
127 for col in x0..x1 {
128 let at = (row * self.width + col) * 3;
129 self.px[at..at + 3].copy_from_slice(&c);
130 }
131 }
132 }
133
134 pub fn text_width(text: &str, scale: usize) -> i64 {
135 (text.chars().count() * 6 * scale) as i64 - scale as i64
136 }
137
138 pub fn text(&mut self, x: i64, y: i64, text: &str, scale: usize, c: Color) {
139 let s = scale as i64;
140 let mut pen = x;
141 for ch in text.chars() {
142 let rows = glyph(ch);
143 for (r, bits) in rows.iter().enumerate() {
144 for col in 0..5 {
145 if bits >> (4 - col) & 1 == 1 {
146 self.rect(pen + col as i64 * s, y + r as i64 * s, s, s, c);
147 }
148 }
149 }
150 pen += 6 * s;
151 }
152 }
153
154 pub fn write_png<W: Write>(&self, out: W) -> Result<()> {
155 let mut enc = png::Encoder::new(out, self.width as u32, self.height as u32);
156 enc.set_color(png::ColorType::Rgb);
157 enc.set_depth(png::BitDepth::Eight);
158 enc.set_compression(png::Compression::High);
159 let mut w = enc
160 .write_header()
161 .map_err(|e| Error::new(format!("cannot write the PNG: {e}")))?;
162 w.write_image_data(&self.px)
163 .map_err(|e| Error::new(format!("cannot write the PNG: {e}")))?;
164 w.finish()
165 .map_err(|e| Error::new(format!("cannot write the PNG: {e}")))
166 }
167
168 pub fn png(&self) -> Result<Vec<u8>> {
169 let mut out = Vec::new();
170 self.write_png(&mut out)?;
171 Ok(out)
172 }
173
174 pub fn save(&self, path: &Path) -> Result<()> {
175 let bytes = self.png()?;
176 if let Some(dir) = path.parent().filter(|d| !d.as_os_str().is_empty()) {
177 std::fs::create_dir_all(dir)
178 .map_err(|e| Error::new(format!("cannot create {}: {e}", dir.display())))?;
179 }
180 std::fs::write(path, bytes)
181 .map_err(|e| Error::new(format!("cannot write {}: {e}", path.display())))
182 }
183}