1use std::collections::HashMap;
10use std::sync::Arc;
11
12use serde::{Deserialize, Serialize};
13
14use yog_gfx::Color;
15use yog_ui::text::LINE_H;
16
17use super::assets::{Image, Resources};
18use super::font::{Font, Glyph, styled};
19
20#[derive(Debug, Clone, Serialize, Deserialize)]
22#[serde(tag = "op", rename_all = "snake_case")]
23pub enum DrawOp {
24 Rect {
25 x1: f32,
26 y1: f32,
27 x2: f32,
28 y2: f32,
29 color: u32,
30 },
31 Gradient {
32 x1: f32,
33 y1: f32,
34 x2: f32,
35 y2: f32,
36 top: u32,
37 bottom: u32,
38 },
39 Text {
40 text: String,
41 font: String,
42 x: f32,
43 y: f32,
44 color: u32,
45 shadow: bool,
46 },
47 TextScaled {
49 text: String,
50 font: String,
51 x: f32,
52 y: f32,
53 scale: f32,
54 color: u32,
55 shadow: bool,
56 },
57 Item {
58 id: String,
59 x: f32,
60 y: f32,
61 size: f32,
62 },
63 Blocks {
66 list: String,
67 x: f32,
68 y: f32,
69 w: f32,
70 h: f32,
71 yaw: f32,
72 pitch: f32,
73 },
74 Image {
76 source: String,
77 x: f32,
78 y: f32,
79 w: f32,
80 h: f32,
81 region: [f32; 4],
82 tint: u32,
83 },
84 Svg {
86 data: String,
87 x: f32,
88 y: f32,
89 w: f32,
90 h: f32,
91 },
92 Entity {
95 entity: String,
96 x: f32,
97 y: f32,
98 w: f32,
99 h: f32,
100 },
101 Scissor {
102 x: i32,
103 y: i32,
104 w: i32,
105 h: i32,
106 },
107 ClearScissor,
108}
109
110pub struct Canvas<'r> {
112 pub w: u32,
113 pub h: u32,
114 pub rgba: Vec<u8>,
115 scale: f32,
116 clip: Option<(i32, i32, i32, i32)>,
117 res: &'r Resources,
118 fonts: HashMap<String, Arc<Font>>,
119}
120
121fn blend(dst: &mut [u8], src: [f32; 4]) {
123 let sa = src[3];
124 if sa <= 0.0 {
125 return;
126 }
127 let d = Color::from_rgba8([dst[0], dst[1], dst[2], dst[3]]).to_f32();
128 let oa = sa + d[3] * (1.0 - sa);
129 let mut o = [0.0, 0.0, 0.0, oa];
130 for i in 0..3 {
131 o[i] = (src[i] * sa + d[i] * d[3] * (1.0 - sa)) / oa;
132 }
133 dst[..4].copy_from_slice(&Color::from_f32(o).to_rgba8());
134}
135
136fn argb(c: u32) -> [f32; 4] {
137 Color(c).to_f32()
138}
139
140const TEXT_MIN_ALPHA: u8 = 4;
142
143fn text_color(c: u32) -> u32 {
145 let c = Color(c);
146 if c.a() < TEXT_MIN_ALPHA { c.with_alpha(u8::MAX).0 } else { c.0 }
147}
148
149fn shadow_color(c: u32) -> u32 {
151 const QUARTER_MASK: u32 = 0x00FC_FCFC;
152 const QUARTER_SHIFT: u32 = 2;
153 (c & Color::BLACK.0) | ((c & QUARTER_MASK) >> QUARTER_SHIFT)
154}
155
156const UNDERLINE: (f32, f32) = (LINE_H, LINE_H - 1.0);
159const STRIKE: (f32, f32) = (LINE_H / 2.0, LINE_H / 2.0 - 1.0);
160
161impl<'r> Canvas<'r> {
162 pub fn new(gui_w: u32, gui_h: u32, scale: u32, res: &'r Resources) -> Self {
163 let (w, h) = (gui_w * scale, gui_h * scale);
164 Canvas {
165 w,
166 h,
167 rgba: vec![0; w as usize * h as usize * 4],
168 scale: scale as f32,
169 clip: None,
170 res,
171 fonts: HashMap::new(),
172 }
173 }
174
175 pub fn font(&mut self, id: &str) -> Arc<Font> {
176 let res = self.res;
177 self.fonts
178 .entry(id.to_string())
179 .or_insert_with(|| Font::load(res, id))
180 .clone()
181 }
182
183 fn span(&self, a: f32, b: f32, vertical: bool) -> (i32, i32) {
185 let max = if vertical { self.h } else { self.w } as i32;
186 let (mut lo, mut hi) = (
187 (a * self.scale).floor() as i32,
188 (b * self.scale).floor() as i32,
189 );
190 if let Some((cx, cy, cw, ch)) = self.clip {
191 let (c0, c1) = if vertical {
192 (cy, cy + ch)
193 } else {
194 (cx, cx + cw)
195 };
196 lo = lo.max((c0 as f32 * self.scale) as i32);
197 hi = hi.min((c1 as f32 * self.scale) as i32);
198 }
199 (lo.max(0), hi.min(max))
200 }
201
202 fn px(&mut self, x: i32, y: i32) -> &mut [u8] {
203 let i = ((y as u32 * self.w + x as u32) * 4) as usize;
204 &mut self.rgba[i..i + 4]
205 }
206
207 pub(super) fn quad(&mut self, img: &Image, o: [f32; 2], a: [f32; 2], b: [f32; 2], shade: f32) {
211 let det = a[0] * b[1] - a[1] * b[0];
212 if det.abs() < 1e-6 {
213 return;
214 }
215 let xs = [o[0], o[0] + a[0], o[0] + b[0], o[0] + a[0] + b[0]];
216 let ys = [o[1], o[1] + a[1], o[1] + b[1], o[1] + a[1] + b[1]];
217 let min = |v: [f32; 4]| v.iter().copied().fold(f32::MAX, f32::min);
218 let max = |v: [f32; 4]| v.iter().copied().fold(f32::MIN, f32::max);
219 let (xa, xb) = self.span(min(xs), max(xs) + 1.0 / self.scale, false);
220 let (ya, yb) = self.span(min(ys), max(ys) + 1.0 / self.scale, true);
221 for py in ya..yb {
222 let gy = (py as f32 + 0.5) / self.scale - o[1];
223 for px in xa..xb {
224 let gx = (px as f32 + 0.5) / self.scale - o[0];
225 let u = (gx * b[1] - gy * b[0]) / det;
226 let v = (a[0] * gy - a[1] * gx) / det;
227 if !(0.0..1.0).contains(&u) || !(0.0..1.0).contains(&v) {
228 continue;
229 }
230 let t = img.pixel((u * img.width as f32) as u32, (v * img.width.min(img.height) as f32) as u32).to_rgba8();
231 if t[3] == 0 {
232 continue;
233 }
234 let [r, g, b, a] = Color::from_rgba8(t).to_f32();
235 let c = [r * shade, g * shade, b * shade, a];
236 blend(self.px(px, py), c);
237 }
238 }
239 }
240
241 pub(super) fn resources(&self) -> &Resources {
242 self.res
243 }
244
245 pub fn fill(&mut self, x1: f32, y1: f32, x2: f32, y2: f32, top: u32, bottom: u32) {
246 let (x1, x2) = (
247 (x1 as i32).min(x2 as i32) as f32,
248 (x1 as i32).max(x2 as i32) as f32,
249 );
250 let (y1, y2) = (
251 (y1 as i32).min(y2 as i32) as f32,
252 (y1 as i32).max(y2 as i32) as f32,
253 );
254 let (xa, xb) = self.span(x1, x2, false);
255 let (ya, yb) = self.span(y1, y2, true);
256 let (t, b) = (argb(top), argb(bottom));
257 let height = ((y2 - y1) * self.scale).max(1.0);
258 for py in ya..yb {
259 let f = ((py as f32 + 0.5 - y1 * self.scale) / height).clamp(0.0, 1.0);
260 let c = [0, 1, 2, 3].map(|i| t[i] + (b[i] - t[i]) * f);
261 for px in xa..xb {
262 blend(self.px(px, py), c);
263 }
264 }
265 }
266
267 #[allow(clippy::too_many_arguments)]
270 pub(super) fn blit(
271 &mut self,
272 img: &Image,
273 x: f32,
274 y: f32,
275 w: f32,
276 h: f32,
277 uv: [f32; 4],
278 tint: [f32; 4],
279 ) {
280 let (xa, xb) = self.span(x, x + w, false);
281 let (ya, yb) = self.span(y, y + h, true);
282 let [u0, v0, u1, v1] = uv;
283 for py in ya..yb {
284 let fy = ((py as f32 + 0.5) / self.scale - y) / h;
285 let ty = ((v0 + (v1 - v0) * fy) * img.height as f32).floor() as i64;
286 if ty < 0 || ty >= img.height as i64 {
287 continue;
288 }
289 for px in xa..xb {
290 let fx = ((px as f32 + 0.5) / self.scale - x) / w;
291 let tx = ((u0 + (u1 - u0) * fx) * img.width as f32).floor() as i64;
292 if tx < 0 || tx >= img.width as i64 {
293 continue;
294 }
295 let t = img.pixel(tx as u32, ty as u32).to_rgba8();
296 let t = Color::from_rgba8(t).to_f32();
297 let c = [0, 1, 2, 3].map(|i| t[i] * tint[i]);
298 blend(self.px(px, py), c);
299 }
300 }
301 }
302
303 fn glyph(&mut self, g: &Glyph, x: f32, y: f32, k: f32, color: u32) {
304 let Some(b) = &g.bitmap else { return };
305 let img = Image {
306 width: b.w,
307 height: b.h,
308 rgba: b.rgba.clone(),
309 };
310 self.blit(
311 &img,
312 x,
313 y + b.top * k,
314 b.w as f32 / b.oversample * k,
315 b.h as f32 / b.oversample * k,
316 [0.0, 0.0, 1.0, 1.0],
317 argb(color),
318 );
319 }
320
321 pub fn text(&mut self, text: &str, font: &str, x: f32, y: f32, color: u32, shadow: bool) {
323 self.text_scaled(text, font, (x as i32) as f32, (y as i32) as f32, 1.0, color, shadow);
324 }
325
326 #[allow(clippy::too_many_arguments)]
329 pub fn text_scaled(&mut self, text: &str, font: &str, x: f32, y: f32, k: f32, color: u32, shadow: bool) {
330 let font = self.font(font);
331 let chars = styled(text, text_color(color));
332 let passes: &[bool] = if shadow { &[true, false] } else { &[false] };
333 for &is_shadow in passes {
334 let mut pen = x;
335 for s in &chars {
336 let g = font.glyph(s.ch);
337 let c = if is_shadow {
338 shadow_color(s.color)
339 } else {
340 s.color
341 };
342 let off = if is_shadow { g.shadow_offset * k } else { 0.0 };
343 self.glyph(&g, pen + off, y + off, k, c);
344 let mut adv = g.advance * k;
345 if s.bold {
346 self.glyph(&g, pen + off + g.bold_offset * k, y + off, k, c);
347 adv += g.bold_offset * k;
348 }
349 if s.underline {
350 self.fill(
351 pen + off - 1.0,
352 y + off + UNDERLINE.0,
353 pen + off + adv,
354 y + off + UNDERLINE.1,
355 c,
356 c,
357 );
358 }
359 if s.strike {
360 self.fill(
361 pen + off - 1.0,
362 y + off + STRIKE.0,
363 pen + off + adv,
364 y + off + STRIKE.1,
365 c,
366 c,
367 );
368 }
369 pen += adv;
370 }
371 }
372 }
373
374 pub fn draw(&mut self, op: &DrawOp) {
375 match op {
376 DrawOp::Rect {
377 x1,
378 y1,
379 x2,
380 y2,
381 color,
382 } => self.fill(*x1, *y1, *x2, *y2, *color, *color),
383 DrawOp::Gradient {
384 x1,
385 y1,
386 x2,
387 y2,
388 top,
389 bottom,
390 } => self.fill(*x1, *y1, *x2, *y2, *top, *bottom),
391 DrawOp::Text {
392 text,
393 font,
394 x,
395 y,
396 color,
397 shadow,
398 } => self.text(text, font, *x, *y, *color, *shadow),
399 DrawOp::TextScaled { text, font, x, y, scale, color, shadow } => {
400 self.text_scaled(text, font, *x, *y, *scale, *color, *shadow)
401 }
402 DrawOp::Blocks { list, x, y, w, h, yaw, pitch } => {
403 super::blocks3d::draw(self, list, [*x, *y, *w, *h], *yaw, *pitch)
404 }
405 DrawOp::Item { id, x, y, size } => {
406 let source = format!("item:{id}");
409 super::images::draw(self, &source, [*x, *y, *size, *size], [0.0; 4], Color::WHITE.0);
410 }
411 DrawOp::Image {
412 source,
413 x,
414 y,
415 w,
416 h,
417 region,
418 tint,
419 } => super::images::draw(self, source, [*x, *y, *w, *h], *region, *tint),
420 DrawOp::Svg { data, x, y, w, h } => super::images::draw_svg(self, data, [*x, *y, *w, *h], Color::WHITE.0),
421 DrawOp::Entity { entity, x, y, w, h } => super::images::draw_entity(self, entity, [*x, *y, *w, *h]),
422 DrawOp::Scissor { x, y, w, h } => self.clip = Some((*x, *y, *w, *h)),
423 DrawOp::ClearScissor => self.clip = None,
424 }
425 }
426
427 pub fn png(&self) -> Result<Vec<u8>, String> {
428 let mut out = Vec::new();
429 let mut enc = png::Encoder::new(&mut out, self.w, self.h);
430 enc.set_color(png::ColorType::Rgba);
431 enc.set_depth(png::BitDepth::Eight);
432 enc.set_compression(png::Compression::Fast);
433 let mut writer = enc.write_header().map_err(|e| e.to_string())?;
434 writer
435 .write_image_data(&self.rgba)
436 .map_err(|e| e.to_string())?;
437 writer.finish().map_err(|e| e.to_string())?;
438 Ok(out)
439 }
440}
441
442const MAX_PIXELS: u64 = 1 << 26;
444
445pub fn fit_scale(gui_w: u32, gui_h: u32, scale: u32) -> u32 {
447 let gui = u64::from(gui_w) * u64::from(gui_h);
448 let mut scale = scale.clamp(1, 8);
449 while scale > 1 && gui.saturating_mul(u64::from(scale * scale)) > MAX_PIXELS {
450 scale -= 1;
451 }
452 scale
453}
454
455pub fn rasterize<'r>(
457 ops: &[DrawOp],
458 gui_w: u32,
459 gui_h: u32,
460 scale: u32,
461 res: &'r Resources,
462) -> Canvas<'r> {
463 let mut canvas = Canvas::new(gui_w, gui_h, fit_scale(gui_w, gui_h, scale), res);
464 for op in ops {
465 canvas.draw(op);
466 }
467 canvas
468}
469
470#[cfg(test)]
471mod tests {
472 use super::*;
473
474 fn res() -> Resources {
475 Resources {
476 mod_root: None,
477 vanilla: None,
478 }
479 }
480
481 #[test]
482 fn huge_canvases_lower_their_scale() {
483 assert_eq!(fit_scale(400, 300, 3), 3);
484 assert_eq!(fit_scale(400, 300, 0), 1);
485 assert_eq!(fit_scale(4096, 4096, 8), 2);
486 assert_eq!(fit_scale(u32::MAX, u32::MAX, 8), 1);
487 }
488
489 #[test]
490 fn rects_truncate_to_gui_pixels_and_blend() {
491 let r = res();
492 let c = rasterize(
493 &[DrawOp::Rect {
494 x1: 1.7,
495 y1: 0.0,
496 x2: 3.2,
497 y2: 1.0,
498 color: 0xFF_FF0000,
499 }],
500 4,
501 1,
502 2,
503 &r,
504 );
505 let px = |x: usize| &c.rgba[x * 4..x * 4 + 4];
507 assert_eq!(px(1), &[0, 0, 0, 0]);
508 assert_eq!(px(2), &[255, 0, 0, 255]);
509 assert_eq!(px(5), &[255, 0, 0, 255]);
510 assert_eq!(px(6), &[0, 0, 0, 0]);
511 }
512
513 #[test]
514 fn scissor_clips() {
515 let r = res();
516 let ops = [
517 DrawOp::Scissor {
518 x: 0,
519 y: 0,
520 w: 1,
521 h: 1,
522 },
523 DrawOp::Rect {
524 x1: 0.0,
525 y1: 0.0,
526 x2: 2.0,
527 y2: 1.0,
528 color: 0xFF_FFFFFF,
529 },
530 ];
531 let c = rasterize(&ops, 2, 1, 1, &r);
532 assert_eq!(c.rgba[3], 255);
533 assert_eq!(c.rgba[7], 0);
534 }
535
536 #[test]
537 fn missing_font_draws_boxes_with_the_shadow_first() {
538 let r = res();
539 let mut c = Canvas::new(16, 10, 1, &r);
540 c.text("A", "", 0.0, 0.0, 0xFF_FFFFFF, true);
541 assert_eq!(&c.rgba[0..4], &[255, 255, 255, 255]);
543 let i = (16 + 1) * 4;
544 assert_eq!(&c.rgba[i..i + 4], &[63, 63, 63, 255]);
545 }
546}