1pub use denise::Mask;
15
16use denise::Point;
17
18use crate::blend::{Paint, blend_span};
19use crate::canvas::Canvas;
20
21impl Canvas<'_> {
22 pub fn blit_mask(&mut self, at: Point, mask: &Mask<'_>, color: impl Into<Paint>) {
27 let bounds = mask.bounds_at(at);
28 let Some(visible) = self.visible(bounds) else {
29 return;
30 };
31 let paint = color.into();
32 if paint.alpha() == 0 {
33 return;
34 }
35 let opaque = paint.alpha() == 255;
36
37 for y in visible.y..visible.bottom() {
38 let row = mask.row(y - at.y);
39 let first = (visible.x - at.x) as usize;
40 let last = (visible.right() - at.x) as usize;
41 let mut x = first;
42 while x < last {
43 let value = row[x];
44 if value == 0 {
45 x += 1;
46 continue;
47 }
48 let mut end = x + 1;
52 while end < last && row[end] == value {
53 end += 1;
54 }
55 let start_x = at.x + x as i32;
56 let end_x = at.x + end as i32;
57 if value == 255 && opaque {
58 if let Some(span) = self.row_span(y, start_x, end_x) {
59 blend_span(span, paint);
60 }
61 } else {
62 let coverage = u32::from(value);
63 for px in start_x..end_x {
64 self.blend_at(px, y, paint, coverage);
65 }
66 }
67 x = end;
68 }
69 }
70 }
71}
72
73#[cfg(test)]
74mod tests {
75 use super::*;
76 use crate::testing::TestCanvas;
77 use denise::Color;
78 use denise::Rect;
79
80 fn ring() -> ([u8; 36], i32, i32) {
82 let mut data = [0u8; 36];
83 for y in 0..6 {
84 for x in 0..6 {
85 let edge = x == 0 || y == 0 || x == 5 || y == 5;
86 data[y * 6 + x] = if edge { 128 } else { 255 };
87 }
88 }
89 (data, 6, 6)
90 }
91
92 #[test]
93 fn geometry_is_validated() {
94 let data = [0u8; 10];
95 assert!(
96 Mask::packed(&data, 4, 4).is_none(),
97 "16 bytes needed, 10 given"
98 );
99 assert!(Mask::new(&data, 4, 2, 2).is_none(), "stride below width");
100 assert!(Mask::packed(&data, 0, 4).is_none());
101 assert!(Mask::packed(&data, 5, 2).is_some());
102 }
103
104 #[test]
105 fn solid_coverage_paints_the_colour_exactly() {
106 let data = [255u8; 16];
107 let mask = Mask::packed(&data, 4, 4).expect("mask");
108 let mut t = TestCanvas::new(8, 8);
109 t.canvas().blit_mask(Point::new(2, 2), &mask, Color::WHITE);
110 for y in 0..8usize {
111 for x in 0..8usize {
112 let inside = (2..6).contains(&x) && (2..6).contains(&y);
113 let expected = if inside { 0xFFFF_FFFF } else { 0 };
114 assert_eq!(t.pixels()[y * 8 + x], expected, "at {x},{y}");
115 }
116 }
117 }
118
119 #[test]
120 fn partial_coverage_lands_between_the_endpoints() {
121 let data = [128u8; 4];
122 let mask = Mask::packed(&data, 2, 2).expect("mask");
123 let mut t = TestCanvas::new(4, 4);
124 t.canvas().blit_mask(Point::ZERO, &mask, Color::WHITE);
125 let px = t.pixels()[0] & 0xFF;
126 assert!(
127 (120..=136).contains(&px),
128 "half coverage over black should be about half white, got {px}"
129 );
130 }
131
132 #[test]
133 fn coverage_multiplies_the_paint_alpha() {
134 let data = [128u8; 4];
135 let mask = Mask::packed(&data, 2, 2).expect("mask");
136 let mut t = TestCanvas::new(4, 4);
137 t.canvas()
138 .blit_mask(Point::ZERO, &mask, Color::rgba(255, 255, 255, 128));
139 let px = t.pixels()[0] & 0xFF;
140 assert!(
141 (56..=72).contains(&px),
142 "half alpha through half coverage should be about a quarter, got {px}"
143 );
144 }
145
146 #[test]
147 fn the_span_path_and_the_pixel_path_agree() {
148 let (data, w, h) = ring();
153 let mask = Mask::packed(&data, w, h).expect("mask");
154
155 let mut span = TestCanvas::new(8, 8);
156 span.canvas().blit_mask(Point::ZERO, &mask, Color::WHITE);
157 let mut pixel = TestCanvas::new(8, 8);
158 pixel
159 .canvas()
160 .blit_mask(Point::ZERO, &mask, Color::rgba(255, 255, 255, 254));
161
162 for i in 0..64 {
163 let a = span.pixels()[i] & 0xFF;
164 let b = pixel.pixels()[i] & 0xFF;
165 assert!(a.abs_diff(b) <= 2, "paths disagree at {i}: {a} vs {b}");
166 }
167 }
168
169 #[test]
170 fn zero_coverage_writes_nothing() {
171 let data = [0u8; 16];
172 let mask = Mask::packed(&data, 4, 4).expect("mask");
173 let mut t = TestCanvas::new(8, 8);
174 t.canvas().blit_mask(Point::ZERO, &mask, Color::WHITE);
175 assert!(t.pixels().iter().all(|&p| p == 0));
176 }
177
178 #[test]
179 fn a_transparent_paint_writes_nothing() {
180 let data = [255u8; 16];
181 let mask = Mask::packed(&data, 4, 4).expect("mask");
182 let mut t = TestCanvas::new(8, 8);
183 t.canvas()
184 .blit_mask(Point::ZERO, &mask, Color::rgba(255, 0, 0, 0));
185 assert!(t.pixels().iter().all(|&p| p == 0));
186 }
187
188 #[test]
189 fn a_mask_is_clipped_like_everything_else() {
190 let (data, w, h) = ring();
191 let mask = Mask::packed(&data, w, h).expect("mask");
192 let mut t = TestCanvas::new(16, 16);
193 {
194 let mut c = t.canvas();
195 let mut clipped = c.with_clip(Rect::new(4, 4, 4, 4));
196 clipped.blit_mask(Point::new(2, 2), &mask, Color::WHITE);
198 }
199 for y in 0..16usize {
200 for x in 0..16usize {
201 let inside = (4..8).contains(&x) && (4..8).contains(&y);
202 if !inside {
203 assert_eq!(t.pixels()[y * 16 + x], 0, "drew past the clip at {x},{y}");
204 }
205 }
206 }
207 assert_ne!(t.pixels()[4 * 16 + 4], 0, "nothing was drawn at all");
208 }
209
210 #[test]
211 fn a_mask_partly_off_the_top_left_draws_its_visible_part() {
212 let data = [255u8; 16];
216 let mask = Mask::packed(&data, 4, 4).expect("mask");
217 let mut t = TestCanvas::new(8, 8);
218 t.canvas()
219 .blit_mask(Point::new(-2, -2), &mask, Color::WHITE);
220 assert_eq!(t.pixels()[0], 0xFFFF_FFFF);
221 assert_eq!(t.pixels()[8 + 1], 0xFFFF_FFFF);
222 assert_eq!(t.pixels()[2 * 8 + 2], 0, "the mask is only 4 wide");
223 }
224
225 #[test]
226 fn a_padded_stride_reads_the_right_rows() {
227 let mut data = [7u8; 4 * 10];
230 for y in 0..4 {
231 for x in 0..3 {
232 data[y * 10 + x] = 255;
233 }
234 }
235 let mask = Mask::new(&data, 3, 4, 10).expect("mask");
236 let mut t = TestCanvas::new(8, 8);
237 t.canvas().blit_mask(Point::ZERO, &mask, Color::WHITE);
238 for y in 0..4usize {
239 for x in 0..8usize {
240 let expected = if x < 3 { 0xFFFF_FFFF } else { 0 };
241 assert_eq!(t.pixels()[y * 8 + x], expected, "at {x},{y}");
242 }
243 }
244 }
245}