1use alloc::collections::BTreeMap;
31
32use serde::{Deserialize, Serialize};
33use shuyi_assets::font::{AtlasRect, FALLBACK_GLYPH, FontAtlasDef};
34use shuyi_assets::image::ImageId;
35
36use crate::prelude::*;
37
38use crate::frame::color::Rgb;
39use crate::frame::glyph_buffer::{Backdrop, GlyphBuffer, Item, Layer, Paint, PxRect, Rect, Sprite};
40use crate::frame::onebit::{self, Frame};
41use crate::frame::painted::{Painted, PaintedSprite, PaintedTable};
42
43pub mod card;
44pub mod test_card;
45
46pub const MISSING_COLOR: Rgb = Rgb::new(255, 0, 255);
50
51pub fn one_bit_picture(frame: &Frame, scale: u32) -> Image {
53 let (width, height) = (onebit::WIDTH.unsigned_abs(), onebit::HEIGHT.unsigned_abs());
54 let mut image = Image::filled(width * scale, height * scale, Rgb::new(0, 0, 0));
55 let s = i64::from(scale);
56 let at = |v: i64| i32::try_from(v / s).ok();
57 for y in 0..i64::from(image.height) {
58 for x in 0..i64::from(image.width) {
59 if at(x)
60 .zip(at(y))
61 .is_some_and(|(x, y)| onebit::is_white(frame, x, y))
62 {
63 blend(&mut image, x, y, [1.0; 3], 1.0);
64 }
65 }
66 }
67 image
68}
69
70pub use shuyi_assets::image::Image;
74
75pub fn of_painted(picture: &Painted) -> Image {
77 let (width, height) = picture.size();
78 let mut rgba = Vec::new();
79 picture.rgba_into(&mut rgba);
80 Image {
81 width: u32::from(width),
82 height: u32::from(height),
83 rgba,
84 }
85}
86
87fn texel(image: &Image, x: i64, y: i64) -> [u8; 4] {
90 if image.width == 0 || image.height == 0 {
91 return [0; 4];
92 }
93 let clamp = |v: i64, size: u32| usize::try_from(v.clamp(0, i64::from(size) - 1)).unwrap_or(0);
94 let i = (clamp(y, image.height) * image.width as usize + clamp(x, image.width)) * 4;
95 [
96 image.rgba[i],
97 image.rgba[i + 1],
98 image.rgba[i + 2],
99 image.rgba[i + 3],
100 ]
101}
102
103fn blend(image: &mut Image, x: i64, y: i64, color: [f32; 3], alpha: f32) {
107 let (Ok(x), Ok(y)) = (u32::try_from(x), u32::try_from(y)) else {
108 return;
109 };
110 if x >= image.width || y >= image.height {
111 return;
112 }
113 let i = (y as usize * image.width as usize + x as usize) * 4;
114 for (c, &src) in color.iter().enumerate() {
115 let dst = f32::from(image.rgba[i + c]) / 255.0;
116 let out = src * alpha + dst * (1.0 - alpha);
117 #[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)] let byte = (out * 255.0).round().clamp(0.0, 255.0) as u8;
119 image.rgba[i + c] = byte;
120 }
121 image.rgba[i + 3] = 255;
122}
123
124fn fill(image: &mut Image, (x, y, w, h): (i64, i64, i64, i64), color: Rgb) {
126 for py in y..y + h {
127 for px in x..x + w {
128 blend(image, px, py, unit(color), 1.0);
129 }
130 }
131}
132
133fn unit(c: Rgb) -> [f32; 3] {
135 [c.r, c.g, c.b].map(|v| f32::from(v) / 255.0)
136}
137
138pub struct Painter {
141 atlas: FontAtlasDef,
142 atlas_image: Image,
143 clear: Rgb,
146 images: BTreeMap<ImageId, Image>,
148}
149
150impl Painter {
151 pub fn new(
156 atlas: FontAtlasDef,
157 atlas_image: Image,
158 images: BTreeMap<ImageId, Image>,
159 clear: Rgb,
160 ) -> Self {
161 Self {
162 atlas,
163 atlas_image,
164 clear,
165 images,
166 }
167 }
168
169 pub fn render(&self, buf: &GlyphBuffer, scale: u32) -> Image {
173 let s = i64::from(scale);
174 let spec = buf.spec();
175 let (cell_w, cell_h) = (i64::from(spec.cell_w) * s, i64::from(spec.cell_h) * s);
176 let width = u32::try_from(i64::from(buf.width()) * cell_w).unwrap_or(0);
177 let height = u32::try_from(i64::from(buf.height()) * cell_h).unwrap_or(0);
178 let mut out = Image::filled(width, height, self.clear);
179 let pictures = buf.pictures();
180 if let Some(backdrop) = buf.backdrop() {
181 self.draw_backdrop(&mut out, backdrop, pictures, scale);
182 }
183 let all_fills = buf.backdrop().is_some();
185 self.draw_layers(&mut out, (buf, pictures), (0, 0), s, all_fills);
186 out
187 }
188
189 fn draw_backdrop(
195 &self,
196 out: &mut Image,
197 backdrop: &Backdrop,
198 pictures: &PaintedTable,
199 scale: u32,
200 ) {
201 let s = i64::from(scale);
202 let clip = backdrop.clip_px();
203 let size = |v: i32| u32::try_from(i64::from(v) * s).unwrap_or(0);
204 let mut window = Image::filled(size(clip.w), size(clip.h), self.clear);
205 let scene_px = i64::from(backdrop.zoom()) * s;
206 self.draw_layers(
207 &mut window,
208 (backdrop.scene(), pictures),
209 backdrop.scene_offset(scale),
210 scene_px,
211 false,
212 );
213 let (left, top) = (i64::from(clip.x) * s, i64::from(clip.y) * s);
214 for y in 0..i64::from(window.height) {
215 for x in 0..i64::from(window.width) {
216 let [r, g, b, _] = texel(&window, x, y);
217 blend(out, left + x, top + y, unit(Rgb::new(r, g, b)), 1.0);
218 }
219 }
220 }
221
222 fn draw_layers(
228 &self,
229 out: &mut Image,
230 (buf, pictures): (&GlyphBuffer, &PaintedTable),
231 corner: (i64, i64),
232 px: i64,
233 all_fills: bool,
234 ) {
235 let spec = buf.spec();
236 let (cell_w, cell_h) = (i64::from(spec.cell_w) * px, i64::from(spec.cell_h) * px);
237 let at = |x: i32, y: i32| {
238 (
239 corner.0 + i64::from(x) * cell_w,
240 corner.1 + i64::from(y) * cell_h,
241 )
242 };
243 let cells = || {
244 (0..i32::from(buf.height())).flat_map(move |y| {
245 (0..i32::from(buf.width()))
246 .filter_map(move |x| buf.get(x, y).map(|cell| (at(x, y), cell)))
247 })
248 };
249 for ((x, y), cell) in cells() {
250 if !cell.see_through && (all_fills || cell.bg != self.clear) {
251 fill(out, (x, y, cell_w, cell_h), cell.bg);
252 }
253 }
254 self.draw_items(out, (buf, pictures), Layer::Under, corner, px);
255 for (cx, cy, cell, wide) in buf.glyphs() {
256 if cell.glyph == ' ' {
257 continue;
258 }
259 let set = buf.widths().wide_font();
260 let Some((rect, fg, cells)) = self.atlas_cell(cell.glyph, cell.fg, wide, set) else {
261 continue;
262 };
263 let fg = unit(fg);
264 #[allow(clippy::cast_precision_loss)] let src = [rect.x, rect.y, rect.w, rect.h].map(|v| v as f32);
266 let (x, y) = at(cx, cy);
267 blit(
268 out,
269 &self.atlas_image,
270 src,
271 (x, y, cell_w * cells, cell_h),
272 false,
273 Paint::Image,
274 [fg[0], fg[1], fg[2], 1.0],
275 );
276 }
277 self.draw_items(out, (buf, pictures), Layer::Over, corner, px);
278 }
279
280 fn draw_items(
283 &self,
284 out: &mut Image,
285 (buf, pictures): (&GlyphBuffer, &PaintedTable),
286 layer: Layer,
287 corner: (i64, i64),
288 scale: i64,
289 ) {
290 for item in buf.items().iter().filter(|item| item.layer() == layer) {
291 let to_px = |r: PxRect| {
292 (
293 corner.0 + i64::from(r.x) * scale,
294 corner.1 + i64::from(r.y) * scale,
295 i64::from(r.w) * scale,
296 i64::from(r.h) * scale,
297 )
298 };
299 match item {
300 Item::Rect(rect) => fill(out, to_px(rect.rect), rect.color),
301 Item::Sprite(sprite) => self.draw_sprite(out, sprite, to_px(sprite.clip)),
302 Item::Painted(painted) => draw_painted(out, painted, pictures, to_px(painted.clip)),
303 Item::Gizmo(gizmo) => gizmo.for_each_run(|y, x0, x1| {
305 fill(out, to_px(Rect::new(x0, y, x1 - x0, 1)), gizmo.color);
306 }),
307 }
308 }
309 }
310
311 fn draw_sprite(&self, out: &mut Image, sprite: &Sprite, dest: (i64, i64, i64, i64)) {
314 let Some(image) = self.images.get(&sprite.image) else {
315 fill(out, dest, MISSING_COLOR);
316 return;
317 };
318 let opacity = f32::from(sprite.opacity) / 255.0;
319 let bright = f32::from(sprite.brightness) / 255.0;
321 let src = sprite.clipped_src();
322 blit(
323 out,
324 image,
325 src,
326 dest,
327 sprite.flip_x,
328 sprite.paint,
329 [bright, bright, bright, opacity],
330 );
331 }
332
333 fn atlas_cell(
339 &self,
340 glyph: char,
341 fg: Rgb,
342 wide: bool,
343 set: Option<&str>,
344 ) -> Option<(AtlasRect, Rgb, i64)> {
345 if let Some(rect) = self.atlas.wide_rect_in(set, glyph).filter(|_| wide) {
346 return Some((rect, fg, 2));
347 }
348 if let Some(rect) = self.atlas.glyph_rect(glyph) {
349 return Some((rect, fg, 1));
350 }
351 Some((self.atlas.glyph_rect(FALLBACK_GLYPH)?, MISSING_COLOR, 1))
352 }
353}
354
355fn draw_painted(
359 out: &mut Image,
360 painted: &PaintedSprite,
361 pictures: &PaintedTable,
362 dest: (i64, i64, i64, i64),
363) {
364 let Some(picture) = pictures.get(painted.picture) else {
365 fill(out, dest, MISSING_COLOR);
366 return;
367 };
368 let bright = f32::from(painted.brightness) / 255.0;
369 blit(
370 out,
371 &of_painted(picture),
372 painted.clipped_src(),
373 dest,
374 false,
375 Paint::Image,
376 [bright, bright, bright, 1.0],
377 );
378}
379
380#[allow(clippy::cast_precision_loss)] fn blit(
387 out: &mut Image,
388 image: &Image,
389 src: [f32; 4],
390 (left, top, width, height): (i64, i64, i64, i64),
391 flip_x: bool,
392 paint: Paint,
393 tint: [f32; 4],
394) {
395 let [src_x, src_y, src_w, src_h] = src;
396 for dy in 0..height {
397 let tex_y = src_y + (dy as f32 + 0.5) / height as f32 * src_h;
398 for dx in 0..width {
399 let across = (dx as f32 + 0.5) / width as f32;
400 let tex_x = if flip_x {
401 src_x + src_w * (1.0 - across)
402 } else {
403 src_x + src_w * across
404 };
405 #[allow(clippy::cast_possible_truncation)] let texel = texel(image, tex_x.floor() as i64, tex_y.floor() as i64);
407 let [red, green, blue] = unit(paint.apply(Rgb::new(texel[0], texel[1], texel[2])));
408 let color = [red * tint[0], green * tint[1], blue * tint[2]];
409 let alpha = f32::from(texel[3]) / 255.0;
410 blend(out, left + dx, top + dy, color, alpha * tint[3]);
411 }
412 }
413}
414
415#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
419#[serde(default, deny_unknown_fields)]
420#[non_exhaustive]
422pub struct Tolerance {
423 pub channel: u8,
426 pub pixels: u32,
428}
429
430impl Tolerance {
431 pub const fn new(channel: u8, pixels: u32) -> Self {
434 Self { channel, pixels }
435 }
436}
437
438impl Default for Tolerance {
439 fn default() -> Self {
441 Self::new(2, 0)
442 }
443}
444
445#[derive(Debug, Clone, PartialEq, Eq)]
448#[non_exhaustive]
450pub struct Comparison {
451 pub same_size: bool,
453 pub over: u32,
456 pub worst: u8,
458 pub first: Option<(u32, u32)>,
460 pub diff: Image,
463 pub tolerance: Tolerance,
465}
466
467impl Comparison {
468 pub fn passes(&self) -> bool {
471 self.same_size && self.over <= self.tolerance.pixels
472 }
473}
474
475pub fn compare(window: &Image, reference: &Image, tolerance: Tolerance) -> Comparison {
478 let width = window.width.min(reference.width);
479 let height = window.height.min(reference.height);
480 let mut diff = Image {
481 width,
482 height,
483 rgba: Vec::with_capacity(width as usize * height as usize * 4),
484 };
485 let (mut over, mut worst, mut first) = (0, 0, None);
486 for y in 0..height {
487 for x in 0..width {
488 let (Some(seen), Some(want)) = (window.pixel(x, y), reference.pixel(x, y)) else {
489 continue;
490 };
491 let most = (0..3).map(|c| seen[c].abs_diff(want[c])).max().unwrap_or(0);
492 worst = worst.max(most);
493 if most > tolerance.channel {
494 over += 1;
495 first = first.or(Some((x, y)));
496 let Rgb { r, g, b } = MISSING_COLOR;
497 diff.rgba.extend([r, g, b, 255]);
498 } else {
499 diff.rgba
500 .extend([want[0] / 2, want[1] / 2, want[2] / 2, 255]);
501 }
502 }
503 }
504 Comparison {
505 same_size: (window.width, window.height) == (reference.width, reference.height),
506 over,
507 worst,
508 first,
509 diff,
510 tolerance,
511 }
512}
513
514#[cfg(test)]
515mod tests;