1use std::collections::HashMap;
10use yog_sync::Arc;
11
12use yog_gfx::Color;
13use yog_gfx::draw2d::TextStyle;
14use yog_ui::text::LINE_H;
15
16use super::assets::{Image, Resources};
17use super::font::{Font, Glyph, styled};
18
19#[derive(Debug, Clone, yog_data::Schema)]
21#[data(tag = "op")]
22pub enum DrawOp {
23 Rect {
24 x1: f32,
25 y1: f32,
26 x2: f32,
27 y2: f32,
28 color: u32,
29 },
30 Gradient {
31 x1: f32,
32 y1: f32,
33 x2: f32,
34 y2: f32,
35 top: u32,
36 bottom: u32,
37 },
38 Text {
39 text: String,
40 font: String,
41 x: f32,
42 y: f32,
43 color: u32,
44 shadow: bool,
45 },
46 TextScaled {
48 text: String,
49 font: String,
50 x: f32,
51 y: f32,
52 scale: f32,
53 color: u32,
54 shadow: bool,
55 },
56 Item {
57 id: String,
58 x: f32,
59 y: f32,
60 size: f32,
61 },
62 Blocks {
65 list: String,
66 x: f32,
67 y: f32,
68 w: f32,
69 h: f32,
70 yaw: f32,
71 pitch: f32,
72 },
73 Image {
75 source: String,
76 x: f32,
77 y: f32,
78 w: f32,
79 h: f32,
80 region: [f32; 4],
81 tint: u32,
82 },
83 Svg {
85 data: String,
86 x: f32,
87 y: f32,
88 w: f32,
89 h: f32,
90 },
91 Entity {
94 entity: String,
95 x: f32,
96 y: f32,
97 w: f32,
98 h: f32,
99 },
100 Scissor {
101 x: i32,
102 y: i32,
103 w: i32,
104 h: i32,
105 },
106 ClearScissor,
107}
108
109impl DrawOp {
110 pub fn moved(mut self, dx: f32, dy: f32) -> DrawOp {
113 match &mut self {
114 DrawOp::Rect { x1, y1, x2, y2, .. } | DrawOp::Gradient { x1, y1, x2, y2, .. } => {
115 (*x1, *x2, *y1, *y2) = (*x1 + dx, *x2 + dx, *y1 + dy, *y2 + dy);
116 }
117 DrawOp::Text { x, y, .. }
118 | DrawOp::TextScaled { x, y, .. }
119 | DrawOp::Item { x, y, .. }
120 | DrawOp::Blocks { x, y, .. }
121 | DrawOp::Image { x, y, .. }
122 | DrawOp::Svg { x, y, .. }
123 | DrawOp::Entity { x, y, .. } => (*x, *y) = (*x + dx, *y + dy),
124 DrawOp::Scissor { .. } | DrawOp::ClearScissor => {
125 return DrawOp::Rect {
126 x1: 0.0,
127 y1: 0.0,
128 x2: 0.0,
129 y2: 0.0,
130 color: 0,
131 };
132 }
133 }
134 self
135 }
136}
137
138pub struct Canvas<'r> {
140 pub w: u32,
141 pub h: u32,
142 pub rgba: Vec<u8>,
143 scale: f32,
144 clip: Option<(i32, i32, i32, i32)>,
145 res: &'r Resources,
146 fonts: HashMap<String, Arc<Font>>,
147}
148
149fn blend(dst: &mut [u8], src: [f32; 4]) {
151 let sa = src[3];
152 if sa <= 0.0 {
153 return;
154 }
155 let d = Color::from_rgba8([dst[0], dst[1], dst[2], dst[3]]).to_f32();
156 let oa = sa + d[3] * (1.0 - sa);
157 let mut o = [0.0, 0.0, 0.0, oa];
158 for i in 0..3 {
159 o[i] = (src[i] * sa + d[i] * d[3] * (1.0 - sa)) / oa;
160 }
161 dst[..4].copy_from_slice(&Color::from_f32(o).to_rgba8());
162}
163
164fn argb(c: u32) -> [f32; 4] {
165 Color(c).to_f32()
166}
167
168const TEXT_MIN_ALPHA: u8 = 4;
170
171fn text_color(c: u32) -> u32 {
173 let c = Color(c);
174 if c.a() < TEXT_MIN_ALPHA {
175 c.with_alpha(u8::MAX).0
176 } else {
177 c.0
178 }
179}
180
181fn shadow_color(c: u32) -> u32 {
183 const QUARTER_MASK: u32 = 0x00FC_FCFC;
184 const QUARTER_SHIFT: u32 = 2;
185 (c & Color::BLACK.0) | ((c & QUARTER_MASK) >> QUARTER_SHIFT)
186}
187
188const UNDERLINE: (f32, f32) = (LINE_H, LINE_H - 1.0);
191const STRIKE: (f32, f32) = (LINE_H / 2.0, LINE_H / 2.0 - 1.0);
192
193impl<'r> Canvas<'r> {
194 pub fn new(gui_w: u32, gui_h: u32, scale: u32, res: &'r Resources) -> Self {
195 let (w, h) = (gui_w * scale, gui_h * scale);
196 Canvas {
197 w,
198 h,
199 rgba: vec![0; w as usize * h as usize * 4],
200 scale: scale as f32,
201 clip: None,
202 res,
203 fonts: HashMap::new(),
204 }
205 }
206
207 pub fn font(&mut self, id: &str) -> Arc<Font> {
208 let res = self.res;
209 self.fonts
210 .entry(id.to_string())
211 .or_insert_with(|| Font::load(res, id))
212 .clone()
213 }
214
215 fn span(&self, a: f32, b: f32, vertical: bool) -> (i32, i32) {
217 let max = if vertical { self.h } else { self.w } as i32;
218 let (mut lo, mut hi) = (
219 (a * self.scale).floor() as i32,
220 (b * self.scale).floor() as i32,
221 );
222 if let Some((cx, cy, cw, ch)) = self.clip {
223 let (c0, c1) = if vertical {
224 (cy, cy + ch)
225 } else {
226 (cx, cx + cw)
227 };
228 lo = lo.max((c0 as f32 * self.scale) as i32);
229 hi = hi.min((c1 as f32 * self.scale) as i32);
230 }
231 (lo.max(0), hi.min(max))
232 }
233
234 fn px(&mut self, x: i32, y: i32) -> &mut [u8] {
235 let i = ((y as u32 * self.w + x as u32) * 4) as usize;
236 &mut self.rgba[i..i + 4]
237 }
238
239 pub(super) fn quad(&mut self, img: &Image, o: [f32; 2], a: [f32; 2], b: [f32; 2], shade: f32) {
243 let det = a[0] * b[1] - a[1] * b[0];
244 if det.abs() < 1e-6 {
245 return;
246 }
247 let xs = [o[0], o[0] + a[0], o[0] + b[0], o[0] + a[0] + b[0]];
248 let ys = [o[1], o[1] + a[1], o[1] + b[1], o[1] + a[1] + b[1]];
249 let min = |v: [f32; 4]| v.iter().copied().fold(f32::MAX, f32::min);
250 let max = |v: [f32; 4]| v.iter().copied().fold(f32::MIN, f32::max);
251 let (xa, xb) = self.span(min(xs), max(xs) + 1.0 / self.scale, false);
252 let (ya, yb) = self.span(min(ys), max(ys) + 1.0 / self.scale, true);
253 for py in ya..yb {
254 let gy = (py as f32 + 0.5) / self.scale - o[1];
255 for px in xa..xb {
256 let gx = (px as f32 + 0.5) / self.scale - o[0];
257 let u = (gx * b[1] - gy * b[0]) / det;
258 let v = (a[0] * gy - a[1] * gx) / det;
259 if !(0.0..1.0).contains(&u) || !(0.0..1.0).contains(&v) {
260 continue;
261 }
262 let t = img
263 .pixel(
264 (u * img.width as f32) as u32,
265 (v * img.width.min(img.height) as f32) as u32,
266 )
267 .to_rgba8();
268 if t[3] == 0 {
269 continue;
270 }
271 let [r, g, b, a] = Color::from_rgba8(t).to_f32();
272 let c = [r * shade, g * shade, b * shade, a];
273 blend(self.px(px, py), c);
274 }
275 }
276 }
277
278 pub(super) fn resources(&self) -> &Resources {
279 self.res
280 }
281
282 pub fn fill(&mut self, [x1, y1, x2, y2]: [f32; 4], top: u32, bottom: u32) {
284 let (x1, x2) = (
285 (x1 as i32).min(x2 as i32) as f32,
286 (x1 as i32).max(x2 as i32) as f32,
287 );
288 let (y1, y2) = (
289 (y1 as i32).min(y2 as i32) as f32,
290 (y1 as i32).max(y2 as i32) as f32,
291 );
292 let (xa, xb) = self.span(x1, x2, false);
293 let (ya, yb) = self.span(y1, y2, true);
294 let (t, b) = (argb(top), argb(bottom));
295 let height = ((y2 - y1) * self.scale).max(1.0);
296 for py in ya..yb {
297 let f = ((py as f32 + 0.5 - y1 * self.scale) / height).clamp(0.0, 1.0);
298 let c = [0, 1, 2, 3].map(|i| t[i] + (b[i] - t[i]) * f);
299 for px in xa..xb {
300 blend(self.px(px, py), c);
301 }
302 }
303 }
304
305 pub(super) fn blit(
308 &mut self,
309 img: &Image,
310 [x, y, w, h]: [f32; 4],
311 uv: [f32; 4],
312 tint: [f32; 4],
313 ) {
314 let (xa, xb) = self.span(x, x + w, false);
315 let (ya, yb) = self.span(y, y + h, true);
316 let [u0, v0, u1, v1] = uv;
317 for py in ya..yb {
318 let fy = ((py as f32 + 0.5) / self.scale - y) / h;
319 let ty = ((v0 + (v1 - v0) * fy) * img.height as f32).floor() as i64;
320 if ty < 0 || ty >= img.height as i64 {
321 continue;
322 }
323 for px in xa..xb {
324 let fx = ((px as f32 + 0.5) / self.scale - x) / w;
325 let tx = ((u0 + (u1 - u0) * fx) * img.width as f32).floor() as i64;
326 if tx < 0 || tx >= img.width as i64 {
327 continue;
328 }
329 let t = img.pixel(tx as u32, ty as u32).to_rgba8();
330 let t = Color::from_rgba8(t).to_f32();
331 let c = [0, 1, 2, 3].map(|i| t[i] * tint[i]);
332 blend(self.px(px, py), c);
333 }
334 }
335 }
336
337 fn glyph(&mut self, g: &Glyph, x: f32, y: f32, k: f32, color: u32) {
338 let Some(b) = &g.bitmap else { return };
339 let img = Image {
340 width: b.w,
341 height: b.h,
342 rgba: b.rgba.clone(),
343 };
344 let at = [
345 x,
346 y + b.top * k,
347 b.w as f32 / b.oversample * k,
348 b.h as f32 / b.oversample * k,
349 ];
350 self.blit(&img, at, [0.0, 0.0, 1.0, 1.0], argb(color));
351 }
352
353 pub fn text(&mut self, text: &str, at: [f32; 2], style: &TextStyle) {
357 let k = style.scale;
358 let [x, y] = if k == 1.0 {
359 at.map(|v| (v as i32) as f32)
360 } else {
361 at
362 };
363 let font = self.font(style.font);
364 let chars = styled(text, text_color(style.color.0));
365 let passes: &[bool] = if style.shadow {
366 &[true, false]
367 } else {
368 &[false]
369 };
370 for &is_shadow in passes {
371 let mut pen = x;
372 for s in &chars {
373 let g = font.glyph(s.ch);
374 let c = if is_shadow {
375 shadow_color(s.color)
376 } else {
377 s.color
378 };
379 let off = if is_shadow { g.shadow_offset * k } else { 0.0 };
380 self.glyph(&g, pen + off, y + off, k, c);
381 let mut adv = g.advance * k;
382 if s.bold {
383 self.glyph(&g, pen + off + g.bold_offset * k, y + off, k, c);
384 adv += g.bold_offset * k;
385 }
386 for (on, (top, bottom)) in [(s.underline, UNDERLINE), (s.strike, STRIKE)] {
387 if on {
388 self.fill(
389 [
390 pen + off - 1.0,
391 y + off + top,
392 pen + off + adv,
393 y + off + bottom,
394 ],
395 c,
396 c,
397 );
398 }
399 }
400 pen += adv;
401 }
402 }
403 }
404
405 pub fn draw(&mut self, op: &DrawOp) {
406 match op {
407 DrawOp::Rect {
408 x1,
409 y1,
410 x2,
411 y2,
412 color,
413 } => self.fill([*x1, *y1, *x2, *y2], *color, *color),
414 DrawOp::Gradient {
415 x1,
416 y1,
417 x2,
418 y2,
419 top,
420 bottom,
421 } => self.fill([*x1, *y1, *x2, *y2], *top, *bottom),
422 DrawOp::Text {
423 text,
424 font,
425 x,
426 y,
427 color,
428 shadow,
429 } => self.text(
430 text,
431 [*x, *y],
432 &TextStyle {
433 font,
434 scale: 1.0,
435 color: Color(*color),
436 shadow: *shadow,
437 },
438 ),
439 DrawOp::TextScaled {
440 text,
441 font,
442 x,
443 y,
444 scale,
445 color,
446 shadow,
447 } => self.text(
448 text,
449 [*x, *y],
450 &TextStyle {
451 font,
452 scale: *scale,
453 color: Color(*color),
454 shadow: *shadow,
455 },
456 ),
457 DrawOp::Scissor { x, y, w, h } => self.clip = Some((*x, *y, *w, *h)),
458 DrawOp::ClearScissor => self.clip = None,
459 picture => self.draw_picture(picture),
460 }
461 }
462
463 fn draw_picture(&mut self, op: &DrawOp) {
465 match op {
466 DrawOp::Blocks {
467 list,
468 x,
469 y,
470 w,
471 h,
472 yaw,
473 pitch,
474 } => super::blocks3d::draw(self, list, [*x, *y, *w, *h], *yaw, *pitch),
475 DrawOp::Item { id, x, y, size } => {
476 let source = format!(
479 "{}{}{id}",
480 yog_ids::image_scheme::ITEM,
481 yog_ids::sep::NAMESPACE
482 );
483 super::images::draw(
484 self,
485 &source,
486 [*x, *y, *size, *size],
487 [0.0; 4],
488 Color::WHITE.0,
489 );
490 }
491 DrawOp::Image {
492 source,
493 x,
494 y,
495 w,
496 h,
497 region,
498 tint,
499 } => super::images::draw(self, source, [*x, *y, *w, *h], *region, *tint),
500 DrawOp::Svg { data, x, y, w, h } => {
501 super::images::draw_svg(self, data, [*x, *y, *w, *h], Color::WHITE.0)
502 }
503 DrawOp::Entity { entity, x, y, w, h } => {
504 super::images::draw_entity(self, entity, [*x, *y, *w, *h])
505 }
506 _ => {}
507 }
508 }
509
510 pub fn png(&self) -> Result<Vec<u8>, String> {
511 let mut out = Vec::new();
512 let mut enc = png::Encoder::new(&mut out, self.w, self.h);
513 enc.set_color(png::ColorType::Rgba);
514 enc.set_depth(png::BitDepth::Eight);
515 enc.set_compression(png::Compression::Fast);
516 let mut writer = enc.write_header().map_err(|e| e.to_string())?;
517 writer
518 .write_image_data(&self.rgba)
519 .map_err(|e| e.to_string())?;
520 writer.finish().map_err(|e| e.to_string())?;
521 Ok(out)
522 }
523
524 pub fn png_url(&self) -> Option<String> {
526 self.png().ok().map(|png| png_url(&png))
527 }
528}
529
530pub const PNG_URL_PREFIX: &str = "data:image/png;base64,";
532
533pub fn png_url(png: &[u8]) -> String {
535 use base64::Engine as _;
536 format!(
537 "{PNG_URL_PREFIX}{}",
538 base64::engine::general_purpose::STANDARD.encode(png)
539 )
540}
541
542const MAX_PIXELS: u64 = 1 << 26;
544
545pub fn fit_scale(gui_w: u32, gui_h: u32, scale: u32) -> u32 {
547 let gui = u64::from(gui_w) * u64::from(gui_h);
548 let mut scale = scale.clamp(1, 8);
549 while scale > 1 && gui.saturating_mul(u64::from(scale * scale)) > MAX_PIXELS {
550 scale -= 1;
551 }
552 scale
553}
554
555pub fn rasterize<'r>(
557 ops: &[DrawOp],
558 gui_w: u32,
559 gui_h: u32,
560 scale: u32,
561 res: &'r Resources,
562) -> Canvas<'r> {
563 let mut canvas = Canvas::new(gui_w, gui_h, fit_scale(gui_w, gui_h, scale), res);
564 for op in ops {
565 canvas.draw(op);
566 }
567 canvas
568}
569
570#[cfg(test)]
571mod tests {
572 use super::*;
573
574 fn res() -> Resources {
575 Resources {
576 mod_root: None,
577 vanilla: None,
578 }
579 }
580
581 #[test]
582 fn huge_canvases_lower_their_scale() {
583 assert_eq!(fit_scale(400, 300, 3), 3);
584 assert_eq!(fit_scale(400, 300, 0), 1);
585 assert_eq!(fit_scale(4096, 4096, 8), 2);
586 assert_eq!(fit_scale(u32::MAX, u32::MAX, 8), 1);
587 }
588
589 #[test]
590 fn rects_truncate_to_gui_pixels_and_blend() {
591 let r = res();
592 let c = rasterize(
593 &[DrawOp::Rect {
594 x1: 1.7,
595 y1: 0.0,
596 x2: 3.2,
597 y2: 1.0,
598 color: 0xFF_FF0000,
599 }],
600 4,
601 1,
602 2,
603 &r,
604 );
605 let px = |x: usize| &c.rgba[x * 4..x * 4 + 4];
607 assert_eq!(px(1), &[0, 0, 0, 0]);
608 assert_eq!(px(2), &[255, 0, 0, 255]);
609 assert_eq!(px(5), &[255, 0, 0, 255]);
610 assert_eq!(px(6), &[0, 0, 0, 0]);
611 }
612
613 #[test]
614 fn scissor_clips() {
615 let r = res();
616 let ops = [
617 DrawOp::Scissor {
618 x: 0,
619 y: 0,
620 w: 1,
621 h: 1,
622 },
623 DrawOp::Rect {
624 x1: 0.0,
625 y1: 0.0,
626 x2: 2.0,
627 y2: 1.0,
628 color: 0xFF_FFFFFF,
629 },
630 ];
631 let c = rasterize(&ops, 2, 1, 1, &r);
632 assert_eq!(c.rgba[3], 255);
633 assert_eq!(c.rgba[7], 0);
634 }
635
636 #[test]
637 fn missing_font_draws_boxes_with_the_shadow_first() {
638 let r = res();
639 let mut c = Canvas::new(16, 10, 1, &r);
640 c.text(
641 "A",
642 [0.0, 0.0],
643 &TextStyle {
644 font: "",
645 scale: 1.0,
646 color: Color::WHITE,
647 shadow: true,
648 },
649 );
650 assert_eq!(&c.rgba[0..4], &[255, 255, 255, 255]);
652 let i = (16 + 1) * 4;
653 assert_eq!(&c.rgba[i..i + 4], &[63, 63, 63, 255]);
654 }
655}