1use denise::{Point, Rect};
15
16use crate::blend::{Paint, blend_span};
17use crate::canvas::Canvas;
18
19#[derive(Clone, Copy, Debug)]
21pub struct Mask<'a> {
22 data: &'a [u8],
23 width: i32,
24 height: i32,
25 stride: usize,
26}
27
28impl<'a> Mask<'a> {
29 pub fn new(data: &'a [u8], width: i32, height: i32, stride: usize) -> Option<Self> {
32 if width <= 0 || height <= 0 || stride < width as usize {
33 return None;
34 }
35 let required = stride * (height as usize - 1) + width as usize;
36 (data.len() >= required).then_some(Self {
37 data,
38 width,
39 height,
40 stride,
41 })
42 }
43
44 pub fn packed(data: &'a [u8], width: i32, height: i32) -> Option<Self> {
46 Self::new(data, width, height, width.max(0) as usize)
47 }
48
49 #[inline]
51 pub const fn width(&self) -> i32 {
52 self.width
53 }
54
55 #[inline]
57 pub const fn height(&self) -> i32 {
58 self.height
59 }
60
61 #[inline]
63 pub const fn bounds_at(&self, at: Point) -> Rect {
64 Rect::new(at.x, at.y, self.width, self.height)
65 }
66
67 #[inline]
68 fn row(&self, y: i32) -> &'a [u8] {
69 let base = y as usize * self.stride;
70 &self.data[base..base + self.width as usize]
71 }
72}
73
74impl Canvas<'_> {
75 pub fn blit_mask(&mut self, at: Point, mask: &Mask<'_>, color: impl Into<Paint>) {
80 let bounds = mask.bounds_at(at);
81 let Some(visible) = self.visible(bounds) else {
82 return;
83 };
84 let paint = color.into();
85 if paint.alpha() == 0 {
86 return;
87 }
88 let opaque = paint.alpha() == 255;
89
90 for y in visible.y..visible.bottom() {
91 let row = mask.row(y - at.y);
92 let first = (visible.x - at.x) as usize;
93 let last = (visible.right() - at.x) as usize;
94 let mut x = first;
95 while x < last {
96 let value = row[x];
97 if value == 0 {
98 x += 1;
99 continue;
100 }
101 let mut end = x + 1;
105 while end < last && row[end] == value {
106 end += 1;
107 }
108 let start_x = at.x + x as i32;
109 let end_x = at.x + end as i32;
110 if value == 255 && opaque {
111 if let Some(span) = self.row_span(y, start_x, end_x) {
112 blend_span(span, paint);
113 }
114 } else {
115 let coverage = u32::from(value);
116 for px in start_x..end_x {
117 self.blend_at(px, y, paint, coverage);
118 }
119 }
120 x = end;
121 }
122 }
123 }
124}
125
126#[cfg(test)]
127mod tests {
128 use super::*;
129 use crate::testing::TestCanvas;
130 use denise::Color;
131
132 fn ring() -> ([u8; 36], i32, i32) {
134 let mut data = [0u8; 36];
135 for y in 0..6 {
136 for x in 0..6 {
137 let edge = x == 0 || y == 0 || x == 5 || y == 5;
138 data[y * 6 + x] = if edge { 128 } else { 255 };
139 }
140 }
141 (data, 6, 6)
142 }
143
144 #[test]
145 fn geometry_is_validated() {
146 let data = [0u8; 10];
147 assert!(
148 Mask::packed(&data, 4, 4).is_none(),
149 "16 bytes needed, 10 given"
150 );
151 assert!(Mask::new(&data, 4, 2, 2).is_none(), "stride below width");
152 assert!(Mask::packed(&data, 0, 4).is_none());
153 assert!(Mask::packed(&data, 5, 2).is_some());
154 }
155
156 #[test]
157 fn solid_coverage_paints_the_colour_exactly() {
158 let data = [255u8; 16];
159 let mask = Mask::packed(&data, 4, 4).expect("mask");
160 let mut t = TestCanvas::new(8, 8);
161 t.canvas().blit_mask(Point::new(2, 2), &mask, Color::WHITE);
162 for y in 0..8usize {
163 for x in 0..8usize {
164 let inside = (2..6).contains(&x) && (2..6).contains(&y);
165 let expected = if inside { 0xFFFF_FFFF } else { 0 };
166 assert_eq!(t.pixels()[y * 8 + x], expected, "at {x},{y}");
167 }
168 }
169 }
170
171 #[test]
172 fn partial_coverage_lands_between_the_endpoints() {
173 let data = [128u8; 4];
174 let mask = Mask::packed(&data, 2, 2).expect("mask");
175 let mut t = TestCanvas::new(4, 4);
176 t.canvas().blit_mask(Point::ZERO, &mask, Color::WHITE);
177 let px = t.pixels()[0] & 0xFF;
178 assert!(
179 (120..=136).contains(&px),
180 "half coverage over black should be about half white, got {px}"
181 );
182 }
183
184 #[test]
185 fn coverage_multiplies_the_paint_alpha() {
186 let data = [128u8; 4];
187 let mask = Mask::packed(&data, 2, 2).expect("mask");
188 let mut t = TestCanvas::new(4, 4);
189 t.canvas()
190 .blit_mask(Point::ZERO, &mask, Color::rgba(255, 255, 255, 128));
191 let px = t.pixels()[0] & 0xFF;
192 assert!(
193 (56..=72).contains(&px),
194 "half alpha through half coverage should be about a quarter, got {px}"
195 );
196 }
197
198 #[test]
199 fn the_span_path_and_the_pixel_path_agree() {
200 let (data, w, h) = ring();
205 let mask = Mask::packed(&data, w, h).expect("mask");
206
207 let mut span = TestCanvas::new(8, 8);
208 span.canvas().blit_mask(Point::ZERO, &mask, Color::WHITE);
209 let mut pixel = TestCanvas::new(8, 8);
210 pixel
211 .canvas()
212 .blit_mask(Point::ZERO, &mask, Color::rgba(255, 255, 255, 254));
213
214 for i in 0..64 {
215 let a = span.pixels()[i] & 0xFF;
216 let b = pixel.pixels()[i] & 0xFF;
217 assert!(a.abs_diff(b) <= 2, "paths disagree at {i}: {a} vs {b}");
218 }
219 }
220
221 #[test]
222 fn zero_coverage_writes_nothing() {
223 let data = [0u8; 16];
224 let mask = Mask::packed(&data, 4, 4).expect("mask");
225 let mut t = TestCanvas::new(8, 8);
226 t.canvas().blit_mask(Point::ZERO, &mask, Color::WHITE);
227 assert!(t.pixels().iter().all(|&p| p == 0));
228 }
229
230 #[test]
231 fn a_transparent_paint_writes_nothing() {
232 let data = [255u8; 16];
233 let mask = Mask::packed(&data, 4, 4).expect("mask");
234 let mut t = TestCanvas::new(8, 8);
235 t.canvas()
236 .blit_mask(Point::ZERO, &mask, Color::rgba(255, 0, 0, 0));
237 assert!(t.pixels().iter().all(|&p| p == 0));
238 }
239
240 #[test]
241 fn a_mask_is_clipped_like_everything_else() {
242 let (data, w, h) = ring();
243 let mask = Mask::packed(&data, w, h).expect("mask");
244 let mut t = TestCanvas::new(16, 16);
245 {
246 let mut c = t.canvas();
247 let mut clipped = c.with_clip(Rect::new(4, 4, 4, 4));
248 clipped.blit_mask(Point::new(2, 2), &mask, Color::WHITE);
250 }
251 for y in 0..16usize {
252 for x in 0..16usize {
253 let inside = (4..8).contains(&x) && (4..8).contains(&y);
254 if !inside {
255 assert_eq!(t.pixels()[y * 16 + x], 0, "drew past the clip at {x},{y}");
256 }
257 }
258 }
259 assert_ne!(t.pixels()[4 * 16 + 4], 0, "nothing was drawn at all");
260 }
261
262 #[test]
263 fn a_mask_partly_off_the_top_left_draws_its_visible_part() {
264 let data = [255u8; 16];
268 let mask = Mask::packed(&data, 4, 4).expect("mask");
269 let mut t = TestCanvas::new(8, 8);
270 t.canvas()
271 .blit_mask(Point::new(-2, -2), &mask, Color::WHITE);
272 assert_eq!(t.pixels()[0], 0xFFFF_FFFF);
273 assert_eq!(t.pixels()[8 + 1], 0xFFFF_FFFF);
274 assert_eq!(t.pixels()[2 * 8 + 2], 0, "the mask is only 4 wide");
275 }
276
277 #[test]
278 fn a_padded_stride_reads_the_right_rows() {
279 let mut data = [7u8; 4 * 10];
282 for y in 0..4 {
283 for x in 0..3 {
284 data[y * 10 + x] = 255;
285 }
286 }
287 let mask = Mask::new(&data, 3, 4, 10).expect("mask");
288 let mut t = TestCanvas::new(8, 8);
289 t.canvas().blit_mask(Point::ZERO, &mask, Color::WHITE);
290 for y in 0..4usize {
291 for x in 0..8usize {
292 let expected = if x < 3 { 0xFFFF_FFFF } else { 0 };
293 assert_eq!(t.pixels()[y * 8 + x], expected, "at {x},{y}");
294 }
295 }
296 }
297}