1use crate::shader_reload::HotReloadRegistry;
8
9#[derive(Debug, Clone, Copy, PartialEq)]
16pub struct Rgba {
17 pub r: f32,
18 pub g: f32,
19 pub b: f32,
20 pub a: f32,
21}
22
23impl Rgba {
24 #[inline]
26 pub fn new(r: f32, g: f32, b: f32, a: f32) -> Self {
27 Self { r, g, b, a }
28 }
29
30 #[inline]
33 pub fn from_u8(r: u8, g: u8, b: u8, a: u8) -> Self {
34 Self {
35 r: r as f32 / 255.0,
36 g: g as f32 / 255.0,
37 b: b as f32 / 255.0,
38 a: a as f32 / 255.0,
39 }
40 }
41
42 #[inline]
44 pub fn to_u8(&self) -> (u8, u8, u8, u8) {
45 let clamp_u8 = |v: f32| (v.clamp(0.0, 1.0) * 255.0).round() as u8;
46 (
47 clamp_u8(self.r),
48 clamp_u8(self.g),
49 clamp_u8(self.b),
50 clamp_u8(self.a),
51 )
52 }
53
54 #[inline]
56 pub fn clamp(&self) -> Self {
57 Self {
58 r: self.r.clamp(0.0, 1.0),
59 g: self.g.clamp(0.0, 1.0),
60 b: self.b.clamp(0.0, 1.0),
61 a: self.a.clamp(0.0, 1.0),
62 }
63 }
64
65 #[inline]
67 pub fn premultiply(&self) -> Self {
68 Self {
69 r: self.r * self.a,
70 g: self.g * self.a,
71 b: self.b * self.a,
72 a: self.a,
73 }
74 }
75
76 #[inline]
80 pub fn unpremultiply(&self) -> Self {
81 if self.a < 1e-8 {
82 Self {
83 r: 0.0,
84 g: 0.0,
85 b: 0.0,
86 a: 0.0,
87 }
88 } else {
89 Self {
90 r: self.r / self.a,
91 g: self.g / self.a,
92 b: self.b / self.a,
93 a: self.a,
94 }
95 }
96 }
97
98 #[inline]
100 pub fn transparent() -> Self {
101 Self::new(0.0, 0.0, 0.0, 0.0)
102 }
103
104 #[inline]
106 pub fn white() -> Self {
107 Self::new(1.0, 1.0, 1.0, 1.0)
108 }
109
110 #[inline]
112 pub fn black() -> Self {
113 Self::new(0.0, 0.0, 0.0, 1.0)
114 }
115}
116
117#[derive(Debug, Clone, Copy, PartialEq, Eq)]
125pub enum BlendMode {
126 Normal,
128 Multiply,
129 Screen,
130 Overlay,
131 HardLight,
132 SoftLight,
133 Darken,
134 Lighten,
135 ColorDodge,
136 ColorBurn,
137 Difference,
138 Exclusion,
139 SrcOver,
141 SrcIn,
142 SrcOut,
143 SrcAtop,
144 DstOver,
145 DstIn,
146 DstOut,
147 DstAtop,
148 Xor,
149 Clear,
150}
151
152impl BlendMode {
153 pub fn blend(&self, src: Rgba, dst: Rgba) -> Rgba {
159 match self {
160 BlendMode::Normal | BlendMode::SrcOver => src_over(src, dst),
161 BlendMode::Multiply => separable_blend(src, dst, |s, d| s * d),
162 BlendMode::Screen => separable_blend(src, dst, |s, d| 1.0 - (1.0 - s) * (1.0 - d)),
163 BlendMode::Overlay => separable_blend(src, dst, |s, d| hard_light_channel(d, s)),
164 BlendMode::HardLight => separable_blend(src, dst, |s, d| hard_light_channel(s, d)),
165 BlendMode::SoftLight => separable_blend(src, dst, soft_light_channel),
166 BlendMode::Darken => separable_blend(src, dst, |s, d| s.min(d)),
167 BlendMode::Lighten => separable_blend(src, dst, |s, d| s.max(d)),
168 BlendMode::ColorDodge => separable_blend(src, dst, color_dodge_channel),
169 BlendMode::ColorBurn => separable_blend(src, dst, color_burn_channel),
170 BlendMode::Difference => separable_blend(src, dst, |s, d| (s - d).abs()),
171 BlendMode::Exclusion => separable_blend(src, dst, |s, d| s + d - 2.0 * s * d),
172 BlendMode::SrcIn => porter_duff(src, dst, src.a * dst.a, 0.0),
173 BlendMode::SrcOut => porter_duff(src, dst, src.a * (1.0 - dst.a), 0.0),
174 BlendMode::SrcAtop => porter_duff(src, dst, src.a * dst.a, dst.a * (1.0 - src.a)),
175 BlendMode::DstOver => src_over(dst, src),
176 BlendMode::DstIn => porter_duff(src, dst, 0.0, dst.a * src.a),
177 BlendMode::DstOut => porter_duff(src, dst, 0.0, dst.a * (1.0 - src.a)),
178 BlendMode::DstAtop => porter_duff(src, dst, src.a * (1.0 - dst.a), dst.a * src.a),
179 BlendMode::Xor => porter_duff(src, dst, src.a * (1.0 - dst.a), dst.a * (1.0 - src.a)),
180 BlendMode::Clear => Rgba::transparent(),
181 }
182 }
183}
184
185fn src_over(src: Rgba, dst: Rgba) -> Rgba {
189 let a_out = src.a + dst.a * (1.0 - src.a);
190 if a_out < 1e-8 {
191 return Rgba::transparent();
192 }
193 Rgba {
194 r: (src.r * src.a + dst.r * dst.a * (1.0 - src.a)) / a_out,
195 g: (src.g * src.a + dst.g * dst.a * (1.0 - src.a)) / a_out,
196 b: (src.b * src.a + dst.b * dst.a * (1.0 - src.a)) / a_out,
197 a: a_out,
198 }
199}
200
201fn separable_blend<F>(src: Rgba, dst: Rgba, f: F) -> Rgba
203where
204 F: Fn(f32, f32) -> f32,
205{
206 let blended = Rgba {
208 r: f(src.r, dst.r),
209 g: f(src.g, dst.g),
210 b: f(src.b, dst.b),
211 a: src.a,
212 };
213 src_over(blended, dst)
214}
215
216fn porter_duff(src: Rgba, dst: Rgba, src_factor: f32, dst_factor: f32) -> Rgba {
218 let a_out = src_factor + dst_factor;
219 if a_out < 1e-8 {
220 return Rgba::transparent();
221 }
222 Rgba {
223 r: (src.r * src_factor + dst.r * dst_factor) / a_out,
224 g: (src.g * src_factor + dst.g * dst_factor) / a_out,
225 b: (src.b * src_factor + dst.b * dst_factor) / a_out,
226 a: a_out,
227 }
228}
229
230#[inline]
232fn hard_light_channel(src: f32, dst: f32) -> f32 {
233 if src <= 0.5 {
234 2.0 * src * dst
235 } else {
236 1.0 - 2.0 * (1.0 - src) * (1.0 - dst)
237 }
238}
239
240#[inline]
242fn soft_light_channel(src: f32, dst: f32) -> f32 {
243 if src <= 0.5 {
244 dst - (1.0 - 2.0 * src) * dst * (1.0 - dst)
245 } else {
246 let d = if dst <= 0.25 {
247 ((16.0 * dst - 12.0) * dst + 4.0) * dst
248 } else {
249 dst.sqrt()
250 };
251 dst + (2.0 * src - 1.0) * (d - dst)
252 }
253}
254
255#[inline]
257fn color_dodge_channel(src: f32, dst: f32) -> f32 {
258 if (1.0 - src) < 1e-8 {
259 1.0
260 } else {
261 (dst / (1.0 - src)).min(1.0)
262 }
263}
264
265#[inline]
267fn color_burn_channel(src: f32, dst: f32) -> f32 {
268 if src < 1e-8 {
269 0.0
270 } else {
271 1.0 - ((1.0 - dst) / src).min(1.0)
272 }
273}
274
275pub struct Layer {
279 pub label: String,
280 pub pixels: Vec<Rgba>,
282 pub width: u32,
283 pub height: u32,
284 pub opacity: f32,
286 pub blend_mode: BlendMode,
287 pub visible: bool,
288 pub z_order: i32,
290}
291
292impl Layer {
293 pub fn new(label: impl Into<String>, width: u32, height: u32) -> Self {
295 let count = (width * height) as usize;
296 Self {
297 label: label.into(),
298 pixels: vec![Rgba::transparent(); count],
299 width,
300 height,
301 opacity: 1.0,
302 blend_mode: BlendMode::Normal,
303 visible: true,
304 z_order: 0,
305 }
306 }
307
308 pub fn fill(mut self, color: Rgba) -> Self {
310 self.pixels.fill(color);
311 self
312 }
313
314 pub fn pixel_at(&self, x: u32, y: u32) -> Option<Rgba> {
316 if x >= self.width || y >= self.height {
317 return None;
318 }
319 self.pixels.get((y * self.width + x) as usize).copied()
320 }
321
322 pub fn set_pixel(&mut self, x: u32, y: u32, color: Rgba) -> bool {
324 if x >= self.width || y >= self.height {
325 return false;
326 }
327 let idx = (y * self.width + x) as usize;
328 if let Some(p) = self.pixels.get_mut(idx) {
329 *p = color;
330 true
331 } else {
332 false
333 }
334 }
335}
336
337#[derive(Debug, Clone)]
341pub struct CompositeStats {
342 pub min_r: f32,
343 pub max_r: f32,
344 pub mean_r: f32,
345 pub min_g: f32,
346 pub max_g: f32,
347 pub mean_g: f32,
348 pub min_b: f32,
349 pub max_b: f32,
350 pub mean_b: f32,
351 pub min_a: f32,
352 pub max_a: f32,
353 pub mean_a: f32,
354 pub transparent_pixel_count: u64,
356}
357
358pub struct TileCompositor {
362 pub width: u32,
363 pub height: u32,
364 pub background: Rgba,
366}
367
368impl TileCompositor {
369 pub fn new(width: u32, height: u32, background: Rgba) -> Self {
371 Self {
372 width,
373 height,
374 background,
375 }
376 }
377
378 pub fn composite(&self, layers: &mut [Layer]) -> Vec<Rgba> {
383 let pixel_count = (self.width * self.height) as usize;
384
385 let mut canvas: Vec<Rgba> = vec![self.background; pixel_count];
387
388 layers.sort_by_key(|l| l.z_order);
390
391 for layer in layers.iter() {
392 if !layer.visible {
393 continue;
394 }
395 if layer.width != self.width || layer.height != self.height {
396 continue;
398 }
399
400 let opacity = layer.opacity.clamp(0.0, 1.0);
401
402 for (i, canvas_pixel) in canvas.iter_mut().enumerate() {
403 let src = match layer.pixels.get(i) {
404 Some(&p) => p,
405 None => continue,
406 };
407 let src = Rgba {
409 a: src.a * opacity,
410 ..src
411 };
412 *canvas_pixel = layer.blend_mode.blend(src, *canvas_pixel);
413 }
414 }
415
416 canvas
417 }
418
419 pub fn to_rgba_bytes(pixels: &[Rgba]) -> Vec<u8> {
421 let mut out = Vec::with_capacity(pixels.len() * 4);
422 for p in pixels {
423 let (r, g, b, a) = p.to_u8();
424 out.push(r);
425 out.push(g);
426 out.push(b);
427 out.push(a);
428 }
429 out
430 }
431
432 pub fn to_rgb_bytes(pixels: &[Rgba]) -> Vec<u8> {
435 let mut out = Vec::with_capacity(pixels.len() * 3);
436 for p in pixels {
437 let (r, g, b, _) = p.to_u8();
438 out.push(r);
439 out.push(g);
440 out.push(b);
441 }
442 out
443 }
444
445 pub fn stats(pixels: &[Rgba]) -> CompositeStats {
447 if pixels.is_empty() {
448 return CompositeStats {
449 min_r: 0.0,
450 max_r: 0.0,
451 mean_r: 0.0,
452 min_g: 0.0,
453 max_g: 0.0,
454 mean_g: 0.0,
455 min_b: 0.0,
456 max_b: 0.0,
457 mean_b: 0.0,
458 min_a: 0.0,
459 max_a: 0.0,
460 mean_a: 0.0,
461 transparent_pixel_count: 0,
462 };
463 }
464
465 let n = pixels.len();
466 let mut min_r = f32::MAX;
467 let mut max_r = f32::MIN;
468 let mut sum_r = 0.0_f64;
469 let mut min_g = f32::MAX;
470 let mut max_g = f32::MIN;
471 let mut sum_g = 0.0_f64;
472 let mut min_b = f32::MAX;
473 let mut max_b = f32::MIN;
474 let mut sum_b = 0.0_f64;
475 let mut min_a = f32::MAX;
476 let mut max_a = f32::MIN;
477 let mut sum_a = 0.0_f64;
478 let mut transparent_count: u64 = 0;
479
480 for p in pixels {
481 min_r = min_r.min(p.r);
482 max_r = max_r.max(p.r);
483 sum_r += p.r as f64;
484 min_g = min_g.min(p.g);
485 max_g = max_g.max(p.g);
486 sum_g += p.g as f64;
487 min_b = min_b.min(p.b);
488 max_b = max_b.max(p.b);
489 sum_b += p.b as f64;
490 min_a = min_a.min(p.a);
491 max_a = max_a.max(p.a);
492 sum_a += p.a as f64;
493 if p.a < 1e-4 {
494 transparent_count += 1;
495 }
496 }
497
498 let n_f64 = n as f64;
499 CompositeStats {
500 min_r,
501 max_r,
502 mean_r: (sum_r / n_f64) as f32,
503 min_g,
504 max_g,
505 mean_g: (sum_g / n_f64) as f32,
506 min_b,
507 max_b,
508 mean_b: (sum_b / n_f64) as f32,
509 min_a,
510 max_a,
511 mean_a: (sum_a / n_f64) as f32,
512 transparent_pixel_count: transparent_count,
513 }
514 }
515}
516
517#[derive(Debug, Clone, Copy)]
529pub struct ColorMatrix {
530 pub matrix: [[f32; 5]; 4],
532}
533
534impl ColorMatrix {
535 pub fn identity() -> Self {
537 Self {
538 matrix: [
539 [1.0, 0.0, 0.0, 0.0, 0.0],
540 [0.0, 1.0, 0.0, 0.0, 0.0],
541 [0.0, 0.0, 1.0, 0.0, 0.0],
542 [0.0, 0.0, 0.0, 1.0, 0.0],
543 ],
544 }
545 }
546
547 pub fn brightness(factor: f32) -> Self {
549 Self {
550 matrix: [
551 [factor, 0.0, 0.0, 0.0, 0.0],
552 [0.0, factor, 0.0, 0.0, 0.0],
553 [0.0, 0.0, factor, 0.0, 0.0],
554 [0.0, 0.0, 0.0, 1.0, 0.0],
555 ],
556 }
557 }
558
559 pub fn contrast(factor: f32) -> Self {
563 let offset = 0.5 * (1.0 - factor);
564 Self {
565 matrix: [
566 [factor, 0.0, 0.0, 0.0, offset],
567 [0.0, factor, 0.0, 0.0, offset],
568 [0.0, 0.0, factor, 0.0, offset],
569 [0.0, 0.0, 0.0, 1.0, 0.0],
570 ],
571 }
572 }
573
574 pub fn saturation(factor: f32) -> Self {
579 let lr = 0.2126_f32;
581 let lg = 0.7152_f32;
582 let lb = 0.0722_f32;
583
584 let sr = (1.0 - factor) * lr;
585 let sg = (1.0 - factor) * lg;
586 let sb = (1.0 - factor) * lb;
587
588 Self {
589 matrix: [
590 [sr + factor, sg, sb, 0.0, 0.0],
591 [sr, sg + factor, sb, 0.0, 0.0],
592 [sr, sg, sb + factor, 0.0, 0.0],
593 [0.0, 0.0, 0.0, 1.0, 0.0],
594 ],
595 }
596 }
597
598 pub fn hue_rotate(degrees: f32) -> Self {
604 let rad = degrees.to_radians();
605 let cos = rad.cos();
606 let sin = rad.sin();
607
608 let lr = 0.2126_f32;
610 let lg = 0.7152_f32;
611 let lb = 0.0722_f32;
612
613 Self {
614 matrix: [
615 [
616 lr + cos * (1.0 - lr) - sin * lr,
617 lg + cos * (-lg) - sin * lg,
618 lb + cos * (-lb) - sin * (1.0 - lb),
619 0.0,
620 0.0,
621 ],
622 [
623 lr + cos * (-lr) + sin * 0.143,
624 lg + cos * (1.0 - lg) + sin * 0.140,
625 lb + cos * (-lb) - sin * 0.283,
626 0.0,
627 0.0,
628 ],
629 [
630 lr + cos * (-lr) - sin * (1.0 - lr),
631 lg + cos * (-lg) + sin * lg,
632 lb + cos * (1.0 - lb) + sin * lb,
633 0.0,
634 0.0,
635 ],
636 [0.0, 0.0, 0.0, 1.0, 0.0],
637 ],
638 }
639 }
640
641 pub fn invert() -> Self {
643 Self {
644 matrix: [
645 [-1.0, 0.0, 0.0, 0.0, 1.0],
646 [0.0, -1.0, 0.0, 0.0, 1.0],
647 [0.0, 0.0, -1.0, 0.0, 1.0],
648 [0.0, 0.0, 0.0, 1.0, 0.0],
649 ],
650 }
651 }
652
653 pub fn grayscale() -> Self {
655 let lr = 0.2126_f32;
656 let lg = 0.7152_f32;
657 let lb = 0.0722_f32;
658 Self {
659 matrix: [
660 [lr, lg, lb, 0.0, 0.0],
661 [lr, lg, lb, 0.0, 0.0],
662 [lr, lg, lb, 0.0, 0.0],
663 [0.0, 0.0, 0.0, 1.0, 0.0],
664 ],
665 }
666 }
667
668 pub fn sepia() -> Self {
670 Self {
671 matrix: [
672 [0.393, 0.769, 0.189, 0.0, 0.0],
673 [0.349, 0.686, 0.168, 0.0, 0.0],
674 [0.272, 0.534, 0.131, 0.0, 0.0],
675 [0.0, 0.0, 0.0, 1.0, 0.0],
676 ],
677 }
678 }
679
680 pub fn apply(&self, pixel: Rgba) -> Rgba {
682 let m = &self.matrix;
683 let r =
684 m[0][0] * pixel.r + m[0][1] * pixel.g + m[0][2] * pixel.b + m[0][3] * pixel.a + m[0][4];
685 let g =
686 m[1][0] * pixel.r + m[1][1] * pixel.g + m[1][2] * pixel.b + m[1][3] * pixel.a + m[1][4];
687 let b =
688 m[2][0] * pixel.r + m[2][1] * pixel.g + m[2][2] * pixel.b + m[2][3] * pixel.a + m[2][4];
689 let a =
690 m[3][0] * pixel.r + m[3][1] * pixel.g + m[3][2] * pixel.b + m[3][3] * pixel.a + m[3][4];
691
692 Rgba::new(r, g, b, a).clamp()
693 }
694
695 pub fn compose(&self, other: &ColorMatrix) -> ColorMatrix {
701 let a = &self.matrix;
702 let b = &other.matrix;
703
704 let mut out = [[0.0_f32; 5]; 4];
705
706 for i in 0..4 {
707 for j in 0..5 {
708 let mut sum = 0.0_f32;
710 for k in 0..4 {
711 sum += a[i][k] * b[k][j];
712 }
713 if j == 4 {
716 sum += a[i][4];
717 }
718 out[i][j] = sum;
719 }
720 }
721
722 ColorMatrix { matrix: out }
723 }
724}
725
726pub struct TileRenderPipeline {
731 pub compositor: TileCompositor,
732 pub color_matrix: ColorMatrix,
734 pub shader_registry: HotReloadRegistry,
735}
736
737impl TileRenderPipeline {
738 pub fn new(width: u32, height: u32) -> Self {
740 Self {
741 compositor: TileCompositor::new(width, height, Rgba::transparent()),
742 color_matrix: ColorMatrix::identity(),
743 shader_registry: HotReloadRegistry::new(),
744 }
745 }
746
747 pub fn add_shader(&mut self, label: impl Into<String>, wgsl: impl Into<String>) {
749 let lbl: String = label.into();
750 self.shader_registry.watcher.add_inline(lbl, wgsl);
751 }
752
753 pub fn update_shader(&mut self, label: &str, new_wgsl: impl Into<String>) -> bool {
757 self.shader_registry.watcher.update_source(label, new_wgsl)
758 }
759
760 pub fn render(&self, layers: &mut [Layer]) -> Vec<u8> {
763 self.render_with_matrix(layers, &self.color_matrix)
764 }
765
766 pub fn render_with_matrix(&self, layers: &mut [Layer], matrix: &ColorMatrix) -> Vec<u8> {
768 let pixels = self.compositor.composite(layers);
769 let transformed: Vec<Rgba> = pixels.iter().map(|p| matrix.apply(*p)).collect();
770 TileCompositor::to_rgba_bytes(&transformed)
771 }
772}
773
774#[cfg(test)]
777mod tests {
778 use super::*;
779
780 const EPSILON: f32 = 1e-4;
781
782 fn approx_eq(a: f32, b: f32) -> bool {
783 (a - b).abs() < EPSILON
784 }
785
786 fn rgba_approx(a: Rgba, b: Rgba) -> bool {
787 approx_eq(a.r, b.r) && approx_eq(a.g, b.g) && approx_eq(a.b, b.b) && approx_eq(a.a, b.a)
788 }
789
790 #[test]
793 fn test_rgba_from_u8_round_trip() {
794 let (r, g, b, a) = (128_u8, 64_u8, 255_u8, 200_u8);
795 let px = Rgba::from_u8(r, g, b, a);
796 let (ro, go, bo, ao) = px.to_u8();
797 assert_eq!(ro, r);
798 assert_eq!(go, g);
799 assert_eq!(bo, b);
800 assert_eq!(ao, a);
801 }
802
803 #[test]
804 fn test_rgba_from_u8_black() {
805 let px = Rgba::from_u8(0, 0, 0, 255);
806 assert!(approx_eq(px.r, 0.0));
807 assert!(approx_eq(px.a, 1.0));
808 }
809
810 #[test]
811 fn test_rgba_from_u8_white() {
812 let px = Rgba::from_u8(255, 255, 255, 255);
813 assert!(approx_eq(px.r, 1.0));
814 assert!(approx_eq(px.g, 1.0));
815 }
816
817 #[test]
818 fn test_rgba_to_u8_clamps_over() {
819 let px = Rgba::new(1.5, -0.5, 0.5, 1.0);
820 let (r, _g, b, _a) = px.to_u8();
821 assert_eq!(r, 255);
822 assert_eq!(b, 128);
823 }
824
825 #[test]
826 fn test_rgba_clamp() {
827 let px = Rgba::new(1.5, -0.1, 0.5, 2.0).clamp();
828 assert!(approx_eq(px.r, 1.0));
829 assert!(approx_eq(px.g, 0.0));
830 assert!(approx_eq(px.b, 0.5));
831 assert!(approx_eq(px.a, 1.0));
832 }
833
834 #[test]
835 fn test_rgba_premultiply() {
836 let px = Rgba::new(1.0, 0.5, 0.25, 0.5);
837 let pre = px.premultiply();
838 assert!(approx_eq(pre.r, 0.5));
839 assert!(approx_eq(pre.g, 0.25));
840 assert!(approx_eq(pre.b, 0.125));
841 assert!(approx_eq(pre.a, 0.5));
842 }
843
844 #[test]
845 fn test_rgba_premultiply_full_alpha() {
846 let px = Rgba::new(0.3, 0.6, 0.9, 1.0);
847 let pre = px.premultiply();
848 assert!(approx_eq(pre.r, 0.3));
849 assert!(approx_eq(pre.g, 0.6));
850 assert!(approx_eq(pre.b, 0.9));
851 }
852
853 #[test]
854 fn test_rgba_unpremultiply() {
855 let px = Rgba::new(0.5, 0.25, 0.125, 0.5);
856 let straight = px.unpremultiply();
857 assert!(approx_eq(straight.r, 1.0));
858 assert!(approx_eq(straight.g, 0.5));
859 assert!(approx_eq(straight.b, 0.25));
860 }
861
862 #[test]
863 fn test_rgba_unpremultiply_zero_alpha() {
864 let px = Rgba::new(0.5, 0.5, 0.5, 0.0);
865 let straight = px.unpremultiply();
866 assert!(approx_eq(straight.r, 0.0));
867 assert!(approx_eq(straight.a, 0.0));
868 }
869
870 #[test]
871 fn test_rgba_premultiply_unpremultiply_round_trip() {
872 let px = Rgba::new(0.8, 0.4, 0.2, 0.6);
873 let recovered = px.premultiply().unpremultiply();
874 assert!(rgba_approx(px, recovered));
875 }
876
877 #[test]
880 fn test_blend_normal_src_over() {
881 let src = Rgba::new(1.0, 0.0, 0.0, 1.0);
882 let dst = Rgba::new(0.0, 1.0, 0.0, 1.0);
883 let result = BlendMode::Normal.blend(src, dst);
884 assert!(rgba_approx(result, src));
886 }
887
888 #[test]
889 fn test_blend_src_over_transparent_src() {
890 let src = Rgba::new(1.0, 0.0, 0.0, 0.0);
891 let dst = Rgba::new(0.0, 1.0, 0.0, 1.0);
892 let result = BlendMode::SrcOver.blend(src, dst);
893 assert!(rgba_approx(result, dst));
894 }
895
896 #[test]
897 fn test_blend_src_over_half_alpha() {
898 let src = Rgba::new(1.0, 0.0, 0.0, 0.5);
899 let dst = Rgba::new(0.0, 0.0, 1.0, 1.0);
900 let result = BlendMode::SrcOver.blend(src, dst);
901 assert!(approx_eq(result.a, 1.0));
903 assert!(approx_eq(result.r, 0.5));
905 assert!(approx_eq(result.b, 0.5));
907 }
908
909 #[test]
910 fn test_blend_multiply_black_src() {
911 let src = Rgba::new(0.0, 0.0, 0.0, 1.0);
912 let dst = Rgba::new(0.8, 0.5, 0.3, 1.0);
913 let result = BlendMode::Multiply.blend(src, dst);
914 assert!(approx_eq(result.r, 0.0));
915 assert!(approx_eq(result.g, 0.0));
916 assert!(approx_eq(result.b, 0.0));
917 }
918
919 #[test]
920 fn test_blend_multiply_white_src() {
921 let src = Rgba::new(1.0, 1.0, 1.0, 1.0);
922 let dst = Rgba::new(0.5, 0.5, 0.5, 1.0);
923 let result = BlendMode::Multiply.blend(src, dst);
924 assert!(approx_eq(result.r, 0.5));
926 }
927
928 #[test]
929 fn test_blend_screen_white_src() {
930 let src = Rgba::new(1.0, 1.0, 1.0, 1.0);
931 let dst = Rgba::new(0.5, 0.5, 0.5, 1.0);
932 let result = BlendMode::Screen.blend(src, dst);
934 assert!(approx_eq(result.r, 1.0));
935 }
936
937 #[test]
938 fn test_blend_darken_picks_darker() {
939 let src = Rgba::new(0.3, 0.7, 0.2, 1.0);
940 let dst = Rgba::new(0.5, 0.4, 0.8, 1.0);
941 let result = BlendMode::Darken.blend(src, dst);
942 assert!(result.r <= src.r + EPSILON && result.r <= dst.r + EPSILON);
944 assert!(result.g <= src.g + EPSILON && result.g <= dst.g + EPSILON);
945 assert!(result.b <= src.b + EPSILON && result.b <= dst.b + EPSILON);
946 }
947
948 #[test]
949 fn test_blend_lighten_picks_lighter() {
950 let src = Rgba::new(0.3, 0.7, 0.2, 1.0);
951 let dst = Rgba::new(0.5, 0.4, 0.8, 1.0);
952 let result = BlendMode::Lighten.blend(src, dst);
953 assert!(result.r >= src.r - EPSILON && result.r >= dst.r - EPSILON);
954 assert!(result.g >= src.g - EPSILON && result.g >= dst.g - EPSILON);
955 assert!(result.b >= src.b - EPSILON && result.b >= dst.b - EPSILON);
956 }
957
958 #[test]
959 fn test_blend_difference_same_color() {
960 let color = Rgba::new(0.5, 0.3, 0.8, 1.0);
961 let result = BlendMode::Difference.blend(color, color);
962 assert!(approx_eq(result.r, 0.0));
964 assert!(approx_eq(result.g, 0.0));
965 assert!(approx_eq(result.b, 0.0));
966 }
967
968 #[test]
969 fn test_blend_clear() {
970 let src = Rgba::new(1.0, 0.5, 0.0, 1.0);
971 let dst = Rgba::new(0.0, 1.0, 0.0, 1.0);
972 let result = BlendMode::Clear.blend(src, dst);
973 assert!(approx_eq(result.a, 0.0));
974 }
975
976 #[test]
977 fn test_blend_src_in() {
978 let src = Rgba::new(1.0, 0.0, 0.0, 0.5);
980 let dst = Rgba::new(0.0, 1.0, 0.0, 0.6);
981 let result = BlendMode::SrcIn.blend(src, dst);
982 assert!(approx_eq(result.a, 0.3));
984 }
985
986 #[test]
987 fn test_blend_src_out() {
988 let src = Rgba::new(1.0, 0.0, 0.0, 1.0);
989 let dst = Rgba::new(0.0, 1.0, 0.0, 1.0);
990 let result = BlendMode::SrcOut.blend(src, dst);
991 assert!(approx_eq(result.a, 0.0));
993 }
994
995 #[test]
996 fn test_blend_dst_over() {
997 let src = Rgba::new(1.0, 0.0, 0.0, 0.5);
999 let dst = Rgba::new(0.0, 0.0, 1.0, 1.0);
1000 let result = BlendMode::DstOver.blend(src, dst);
1001 assert!(rgba_approx(result, dst));
1003 }
1004
1005 #[test]
1006 fn test_blend_xor() {
1007 let src = Rgba::new(1.0, 0.0, 0.0, 1.0);
1009 let dst = Rgba::new(0.0, 1.0, 0.0, 1.0);
1010 let result = BlendMode::Xor.blend(src, dst);
1011 assert!(approx_eq(result.a, 0.0));
1013 }
1014
1015 #[test]
1016 fn test_blend_exclusion() {
1017 let src = Rgba::new(0.5, 0.5, 0.5, 1.0);
1018 let dst = Rgba::new(0.5, 0.5, 0.5, 1.0);
1019 let result = BlendMode::Exclusion.blend(src, dst);
1021 assert!(approx_eq(result.r, 0.5));
1022 }
1023
1024 #[test]
1027 fn test_layer_new_transparent() {
1028 let layer = Layer::new("base", 4, 4);
1029 for px in &layer.pixels {
1030 assert!(approx_eq(px.a, 0.0));
1031 }
1032 }
1033
1034 #[test]
1035 fn test_layer_fill() {
1036 let color = Rgba::new(0.5, 0.0, 1.0, 1.0);
1037 let layer = Layer::new("l", 2, 2).fill(color);
1038 for px in &layer.pixels {
1039 assert!(rgba_approx(*px, color));
1040 }
1041 }
1042
1043 #[test]
1044 fn test_layer_pixel_at_in_bounds() {
1045 let layer = Layer::new("l", 4, 4).fill(Rgba::white());
1046 let px = layer.pixel_at(2, 3);
1047 assert!(px.is_some());
1048 assert!(rgba_approx(px.expect("should be Some"), Rgba::white()));
1049 }
1050
1051 #[test]
1052 fn test_layer_pixel_at_out_of_bounds() {
1053 let layer = Layer::new("l", 4, 4);
1054 assert!(layer.pixel_at(4, 0).is_none());
1055 assert!(layer.pixel_at(0, 4).is_none());
1056 }
1057
1058 #[test]
1059 fn test_layer_set_pixel() {
1060 let mut layer = Layer::new("l", 4, 4);
1061 let ok = layer.set_pixel(1, 2, Rgba::white());
1062 assert!(ok);
1063 assert!(rgba_approx(
1064 layer.pixel_at(1, 2).expect("should be Some"),
1065 Rgba::white()
1066 ));
1067 }
1068
1069 #[test]
1070 fn test_layer_set_pixel_out_of_bounds() {
1071 let mut layer = Layer::new("l", 4, 4);
1072 assert!(!layer.set_pixel(10, 10, Rgba::white()));
1073 }
1074
1075 #[test]
1078 fn test_compositor_empty_layers() {
1079 let comp = TileCompositor::new(2, 2, Rgba::black());
1080 let mut layers: Vec<Layer> = vec![];
1081 let out = comp.composite(&mut layers);
1082 assert_eq!(out.len(), 4);
1083 for px in out {
1084 assert!(rgba_approx(px, Rgba::black()));
1085 }
1086 }
1087
1088 #[test]
1089 fn test_compositor_single_opaque_layer() {
1090 let comp = TileCompositor::new(2, 2, Rgba::transparent());
1091 let layer = Layer::new("l", 2, 2).fill(Rgba::white());
1092 let mut layers = vec![layer];
1093 let out = comp.composite(&mut layers);
1094 for px in out {
1095 assert!(rgba_approx(px, Rgba::white()));
1096 }
1097 }
1098
1099 #[test]
1100 fn test_compositor_z_order_respected() {
1101 let comp = TileCompositor::new(1, 1, Rgba::transparent());
1102 let bottom = Layer::new("bottom", 1, 1).fill(Rgba::new(0.0, 0.0, 1.0, 1.0));
1103 let top = {
1104 let mut l = Layer::new("top", 1, 1);
1105 l.z_order = 1;
1106 l.fill(Rgba::new(1.0, 0.0, 0.0, 1.0))
1107 };
1108 let mut layers = vec![top, bottom]; let out = comp.composite(&mut layers);
1110 assert!(approx_eq(out[0].r, 1.0));
1112 assert!(approx_eq(out[0].b, 0.0));
1113 }
1114
1115 #[test]
1116 fn test_compositor_invisible_layer_skipped() {
1117 let comp = TileCompositor::new(1, 1, Rgba::black());
1118 let mut invisible = Layer::new("inv", 1, 1).fill(Rgba::white());
1119 invisible.visible = false;
1120 let mut layers = vec![invisible];
1121 let out = comp.composite(&mut layers);
1122 assert!(rgba_approx(out[0], Rgba::black()));
1124 }
1125
1126 #[test]
1127 fn test_compositor_opacity_scales_alpha() {
1128 let comp = TileCompositor::new(1, 1, Rgba::transparent());
1129 let mut layer = Layer::new("l", 1, 1).fill(Rgba::new(1.0, 0.0, 0.0, 1.0));
1130 layer.opacity = 0.5;
1131 let mut layers = vec![layer];
1132 let out = comp.composite(&mut layers);
1133 assert!(approx_eq(out[0].a, 0.5));
1135 }
1136
1137 #[test]
1138 fn test_compositor_to_rgba_bytes_length() {
1139 let pixels = vec![Rgba::white(); 10];
1140 let bytes = TileCompositor::to_rgba_bytes(&pixels);
1141 assert_eq!(bytes.len(), 40);
1142 }
1143
1144 #[test]
1145 fn test_compositor_to_rgb_bytes_length() {
1146 let pixels = vec![Rgba::white(); 10];
1147 let bytes = TileCompositor::to_rgb_bytes(&pixels);
1148 assert_eq!(bytes.len(), 30);
1149 }
1150
1151 #[test]
1152 fn test_compositor_to_rgba_bytes_values() {
1153 let pixels = vec![Rgba::from_u8(100, 150, 200, 255)];
1154 let bytes = TileCompositor::to_rgba_bytes(&pixels);
1155 assert_eq!(bytes[0], 100);
1156 assert_eq!(bytes[1], 150);
1157 assert_eq!(bytes[2], 200);
1158 assert_eq!(bytes[3], 255);
1159 }
1160
1161 #[test]
1162 fn test_compositor_stats_mean_r() {
1163 let pixels = vec![Rgba::new(0.0, 0.0, 0.0, 1.0), Rgba::new(1.0, 0.0, 0.0, 1.0)];
1164 let s = TileCompositor::stats(&pixels);
1165 assert!(approx_eq(s.mean_r, 0.5));
1166 }
1167
1168 #[test]
1169 fn test_compositor_stats_transparent_count() {
1170 let pixels = vec![
1171 Rgba::new(0.0, 0.0, 0.0, 0.0),
1172 Rgba::new(0.0, 0.0, 0.0, 0.0),
1173 Rgba::new(1.0, 1.0, 1.0, 1.0),
1174 ];
1175 let s = TileCompositor::stats(&pixels);
1176 assert_eq!(s.transparent_pixel_count, 2);
1177 }
1178
1179 #[test]
1180 fn test_compositor_stats_empty() {
1181 let s = TileCompositor::stats(&[]);
1182 assert!(approx_eq(s.min_r, 0.0));
1183 assert!(approx_eq(s.mean_r, 0.0));
1184 }
1185
1186 #[test]
1187 fn test_compositor_stats_min_max() {
1188 let pixels = vec![Rgba::new(0.1, 0.2, 0.3, 0.4), Rgba::new(0.9, 0.8, 0.7, 0.6)];
1189 let s = TileCompositor::stats(&pixels);
1190 assert!(approx_eq(s.min_r, 0.1));
1191 assert!(approx_eq(s.max_r, 0.9));
1192 assert!(approx_eq(s.min_g, 0.2));
1193 assert!(approx_eq(s.max_g, 0.8));
1194 }
1195
1196 #[test]
1199 fn test_color_matrix_identity_noop() {
1200 let m = ColorMatrix::identity();
1201 let px = Rgba::new(0.4, 0.6, 0.2, 0.8);
1202 let out = m.apply(px);
1203 assert!(rgba_approx(out, px));
1204 }
1205
1206 #[test]
1207 fn test_color_matrix_brightness_doubles() {
1208 let m = ColorMatrix::brightness(2.0);
1209 let px = Rgba::new(0.2, 0.3, 0.4, 1.0);
1210 let out = m.apply(px);
1211 assert!(approx_eq(out.r, 0.4));
1213 assert!(approx_eq(out.g, 0.6));
1214 assert!(approx_eq(out.b, 0.8));
1215 }
1216
1217 #[test]
1218 fn test_color_matrix_brightness_zero() {
1219 let m = ColorMatrix::brightness(0.0);
1220 let px = Rgba::new(1.0, 1.0, 1.0, 1.0);
1221 let out = m.apply(px);
1222 assert!(approx_eq(out.r, 0.0));
1223 assert!(approx_eq(out.g, 0.0));
1224 assert!(approx_eq(out.b, 0.0));
1225 assert!(approx_eq(out.a, 1.0));
1226 }
1227
1228 #[test]
1229 fn test_color_matrix_invert() {
1230 let m = ColorMatrix::invert();
1231 let px = Rgba::new(0.2, 0.5, 0.8, 1.0);
1232 let out = m.apply(px);
1233 assert!(approx_eq(out.r, 0.8));
1234 assert!(approx_eq(out.g, 0.5));
1235 assert!(approx_eq(out.b, 0.2));
1236 assert!(approx_eq(out.a, 1.0));
1237 }
1238
1239 #[test]
1240 fn test_color_matrix_invert_twice_is_identity() {
1241 let m = ColorMatrix::invert().compose(&ColorMatrix::invert());
1242 let px = Rgba::new(0.3, 0.6, 0.9, 0.7);
1243 let out = m.apply(px);
1244 assert!(rgba_approx(out, px));
1245 }
1246
1247 #[test]
1248 fn test_color_matrix_grayscale_equal_channels() {
1249 let m = ColorMatrix::grayscale();
1250 let px = Rgba::new(0.6, 0.4, 0.2, 1.0);
1251 let out = m.apply(px);
1252 assert!(approx_eq(out.r, out.g));
1254 assert!(approx_eq(out.g, out.b));
1255 }
1256
1257 #[test]
1258 fn test_color_matrix_grayscale_white_stays_white() {
1259 let m = ColorMatrix::grayscale();
1260 let out = m.apply(Rgba::white());
1261 assert!(approx_eq(out.r, 1.0));
1262 assert!(approx_eq(out.g, 1.0));
1263 assert!(approx_eq(out.b, 1.0));
1264 }
1265
1266 #[test]
1267 fn test_color_matrix_compose_identity() {
1268 let any_m = ColorMatrix::brightness(1.5);
1269 let composed = any_m.compose(&ColorMatrix::identity());
1270 let px = Rgba::new(0.3, 0.4, 0.5, 1.0);
1271 let a = any_m.apply(px);
1272 let b = composed.apply(px);
1273 assert!(rgba_approx(a, b));
1274 }
1275
1276 #[test]
1277 fn test_color_matrix_compose_identity_left() {
1278 let any_m = ColorMatrix::contrast(1.5);
1279 let composed = ColorMatrix::identity().compose(&any_m);
1280 let px = Rgba::new(0.3, 0.4, 0.5, 1.0);
1281 let a = any_m.apply(px);
1282 let b = composed.apply(px);
1283 assert!(rgba_approx(a, b));
1284 }
1285
1286 #[test]
1287 fn test_color_matrix_saturation_zero_is_grayscale() {
1288 let gray = ColorMatrix::grayscale();
1289 let sat0 = ColorMatrix::saturation(0.0);
1290 let px = Rgba::new(0.5, 0.3, 0.7, 1.0);
1291 let a = gray.apply(px);
1292 let b = sat0.apply(px);
1293 assert!(approx_eq(a.r, b.r));
1295 }
1296
1297 #[test]
1298 fn test_color_matrix_contrast_identity() {
1299 let m = ColorMatrix::contrast(1.0);
1300 let px = Rgba::new(0.4, 0.6, 0.8, 1.0);
1301 assert!(rgba_approx(m.apply(px), px));
1302 }
1303
1304 #[test]
1305 fn test_color_matrix_sepia_non_zero() {
1306 let m = ColorMatrix::sepia();
1307 let px = Rgba::new(0.5, 0.5, 0.5, 1.0);
1308 let out = m.apply(px);
1309 assert!(out.r > 0.0);
1310 assert!(out.g > 0.0);
1311 assert!(out.b > 0.0);
1312 }
1313
1314 #[test]
1317 fn test_pipeline_render_byte_length() {
1318 let pipeline = TileRenderPipeline::new(4, 4);
1319 let mut layers = vec![Layer::new("l", 4, 4).fill(Rgba::white())];
1320 let bytes = pipeline.render(&mut layers);
1321 assert_eq!(bytes.len(), 4 * 4 * 4); }
1323
1324 #[test]
1325 fn test_pipeline_render_with_matrix() {
1326 let pipeline = TileRenderPipeline::new(2, 2);
1327 let mut layers = vec![Layer::new("l", 2, 2).fill(Rgba::new(0.5, 0.5, 0.5, 1.0))];
1328 let matrix = ColorMatrix::brightness(2.0);
1329 let bytes = pipeline.render_with_matrix(&mut layers, &matrix);
1330 assert_eq!(bytes.len(), 16);
1332 assert_eq!(bytes[0], 255);
1334 }
1335
1336 #[test]
1337 fn test_pipeline_add_shader() {
1338 let mut pipeline = TileRenderPipeline::new(4, 4);
1339 pipeline.add_shader("my_shader", "@compute fn main() {}");
1340 assert!(
1341 pipeline
1342 .shader_registry
1343 .watcher
1344 .get_source("my_shader")
1345 .is_some()
1346 );
1347 }
1348
1349 #[test]
1350 fn test_pipeline_update_shader() {
1351 let mut pipeline = TileRenderPipeline::new(4, 4);
1352 pipeline.add_shader("s", "@compute fn main() {}");
1353 let ok = pipeline.update_shader("s", "@compute fn main_v2() {}");
1354 assert!(ok);
1355 assert_eq!(
1356 pipeline.shader_registry.watcher.source_version("s"),
1357 Some(2)
1358 );
1359 }
1360
1361 #[test]
1362 fn test_pipeline_update_unknown_shader() {
1363 let mut pipeline = TileRenderPipeline::new(4, 4);
1364 assert!(!pipeline.update_shader("ghost", "@compute fn x() {}"));
1365 }
1366
1367 #[test]
1368 fn test_pipeline_render_empty_layers() {
1369 let pipeline = TileRenderPipeline::new(3, 3);
1370 let mut layers: Vec<Layer> = vec![];
1371 let bytes = pipeline.render(&mut layers);
1372 assert_eq!(bytes.len(), 3 * 3 * 4); }
1374}