1use crate::Color;
2use fontdue::{Font, FontSettings, Metrics};
3use serde::Serialize;
4use std::{collections::HashMap, io::Write, path::Path};
5
6#[derive(Clone, Copy, Debug, Default, Serialize, PartialEq)]
7pub struct Rect {
8 pub x: f32,
9 pub y: f32,
10 pub w: f32,
11 pub h: f32,
12}
13impl Rect {
14 pub const fn new(x: f32, y: f32, w: f32, h: f32) -> Self {
15 Self { x, y, w, h }
16 }
17 pub fn contains(self, x: f32, y: f32) -> bool {
18 x >= self.x && y >= self.y && x < self.x + self.w && y < self.y + self.h
19 }
20 pub fn inset(self, n: f32) -> Self {
21 Self::new(
22 self.x + n,
23 self.y + n,
24 (self.w - 2.0 * n).max(0.0),
25 (self.h - 2.0 * n).max(0.0),
26 )
27 }
28 pub fn intersect(self, other: Self) -> Self {
29 let x = self.x.max(other.x);
30 let y = self.y.max(other.y);
31 Self::new(
32 x,
33 y,
34 (self.x + self.w).min(other.x + other.w).max(x) - x,
35 (self.y + self.h).min(other.y + other.h).max(y) - y,
36 )
37 }
38}
39pub struct Painter {
42 pub pixels: Vec<u32>,
43 pub width: u32,
44 pub height: u32,
45 pub scale: f32,
46 fonts: [Font; 2],
47 glyphs: HashMap<(char, u32, bool), (Metrics, Vec<u8>)>,
48 clip: Rect,
49}
50impl Default for Painter {
51 fn default() -> Self {
52 Self::new()
53 }
54}
55impl Painter {
56 pub fn new() -> Self {
57 Self {
58 pixels: vec![],
59 width: 0,
60 height: 0,
61 scale: 1.0,
62 fonts: [
63 Font::from_bytes(
64 include_bytes!("../assets/DejaVuSans.ttf") as &[u8],
65 FontSettings::default(),
66 )
67 .unwrap(),
68 Font::from_bytes(
69 include_bytes!("../assets/DejaVuSans-Bold.ttf") as &[u8],
70 FontSettings::default(),
71 )
72 .unwrap(),
73 ],
74 glyphs: HashMap::new(),
75 clip: Rect::default(),
76 }
77 }
78 pub fn resize(&mut self, width: u32, height: u32, scale: f32) {
79 self.width = width;
80 self.height = height;
81 self.scale = scale.max(0.25);
82 self.pixels.resize(width as usize * height as usize, 0);
83 self.clip = Rect::new(
84 0.0,
85 0.0,
86 width as f32 / self.scale,
87 height as f32 / self.scale,
88 );
89 }
90 pub fn clear(&mut self, c: Color) {
91 self.pixels.fill(c.0);
92 }
93 pub fn set_clip(&mut self, rect: Rect) {
94 self.clip = rect.intersect(Rect::new(
95 0.0,
96 0.0,
97 self.width as f32 / self.scale,
98 self.height as f32 / self.scale,
99 ));
100 }
101 pub fn clip(&self) -> Rect {
102 self.clip
103 }
104 pub fn measure(&self, text: &str, size: f32, bold: bool) -> f32 {
105 let f = &self.fonts[bold as usize];
106 let mut prev = None;
107 let mut x = 0.0;
108 for ch in text.chars() {
109 if let Some(p) = prev {
110 x += f.horizontal_kern(p, ch, size).unwrap_or(0.0);
111 }
112 x += f.metrics(ch, size).advance_width;
113 prev = Some(ch);
114 }
115 x
116 }
117 fn blend(&mut self, x: i32, y: i32, c: Color, alpha: u8) {
118 if x < 0
119 || y < 0
120 || x >= self.width as i32
121 || y >= self.height as i32
122 || !self
123 .clip
124 .contains((x as f32 + 0.5) / self.scale, (y as f32 + 0.5) / self.scale)
125 {
126 return;
127 }
128 let i = y as usize * self.width as usize + x as usize;
129 self.pixels[i] = Color(self.pixels[i]).mix(c, alpha as f32 / 255.0).0;
130 }
131 pub fn rect(&mut self, r: Rect, c: Color, radius: f32) {
132 let clip = r.intersect(self.clip);
133 let s = self.scale;
134 let rad = radius.max(0.0).min(r.w / 2.0).min(r.h / 2.0);
135 for y in (clip.y * s).floor().max(0.0) as i32
136 ..((clip.y + clip.h) * s).ceil().min(self.height as f32) as i32
137 {
138 for x in (clip.x * s).floor().max(0.0) as i32
139 ..((clip.x + clip.w) * s).ceil().min(self.width as f32) as i32
140 {
141 let px = (x as f32 + 0.5) / s;
142 let py = (y as f32 + 0.5) / s;
143 let cx = px.clamp(r.x + rad, r.x + r.w - rad);
144 let cy = py.clamp(r.y + rad, r.y + r.h - rad);
145 let d = ((px - cx).powi(2) + (py - cy).powi(2)).sqrt();
146 let a = if rad == 0.0 {
147 1.0
148 } else {
149 ((rad - d) * s + 0.5).clamp(0.0, 1.0)
150 };
151 if a > 0.0 {
152 self.blend(x, y, c, (a * 255.0) as u8);
153 }
154 }
155 }
156 }
157 pub fn outline(&mut self, r: Rect, c: Color) {
158 self.rect(Rect::new(r.x, r.y, r.w, 1.0), c, 0.0);
159 self.rect(Rect::new(r.x, r.y + r.h - 1.0, r.w, 1.0), c, 0.0);
160 self.rect(Rect::new(r.x, r.y, 1.0, r.h), c, 0.0);
161 self.rect(Rect::new(r.x + r.w - 1.0, r.y, 1.0, r.h), c, 0.0);
162 }
163 pub fn text(&mut self, text: &str, x: f32, y: f32, size: f32, c: Color, bold: bool) {
164 let s = self.scale;
165 let size_px = (size * s * 64.0).round() as u32;
166 let mut pen = x * s;
167 let baseline = (y + size) * s;
168 let mut prev = None;
169 for ch in text.chars() {
170 let f = &self.fonts[bold as usize];
171 if let Some(p) = prev {
172 pen += f
173 .horizontal_kern(p, ch, size_px as f32 / 64.0)
174 .unwrap_or(0.0);
175 }
176 let key = (ch, size_px, bold);
177 if !self.glyphs.contains_key(&key) {
178 if self.glyphs.len() > 8192 {
180 self.glyphs.clear();
181 }
182 self.glyphs.insert(
183 key,
184 self.fonts[bold as usize].rasterize(ch, size_px as f32 / 64.0),
185 );
186 }
187 let (m, bitmap) = &self.glyphs[&key];
188 let m = *m;
189 let gx = pen.round() as i32 + m.xmin;
190 let gy = baseline.round() as i32 - m.ymin - m.height as i32;
191 for yy in 0..m.height {
193 for xx in 0..m.width {
194 let px = gx + xx as i32;
195 let py = gy + yy as i32;
196 if px < 0
197 || py < 0
198 || px >= self.width as i32
199 || py >= self.height as i32
200 || !self
201 .clip
202 .contains((px as f32 + 0.5) / s, (py as f32 + 0.5) / s)
203 {
204 continue;
205 }
206 let a = bitmap[yy * m.width + xx];
207 let i = py as usize * self.width as usize + px as usize;
208 if a > 0 {
209 self.pixels[i] = Color(self.pixels[i]).mix(c, a as f32 / 255.0).0;
210 }
211 }
212 }
213 pen += m.advance_width;
214 prev = Some(ch);
215 }
216 }
217 pub fn save_ppm(&self, path: &Path) -> std::io::Result<()> {
219 let file = std::fs::OpenOptions::new()
220 .write(true)
221 .create_new(true)
222 .open(path)?;
223 let mut out = std::io::BufWriter::new(file);
224 write!(out, "P6\n{} {}\n255\n", self.width, self.height)?;
225 for p in &self.pixels {
226 out.write_all(&[(p >> 16) as u8, (p >> 8) as u8, *p as u8])?;
227 }
228 out.flush()
229 }
230}