Skip to main content

dinamika_cpu/raster/
mask.rs

1//! [`Mask`] — a clipping mask (single-channel alpha coverage).
2//!
3//! A mask defines an `overflow: hidden` region: during fill and stroke the
4//! coverage of each pixel is multiplied by the mask value, so the shape is
5//! visible only where the mask is non-zero. A typical scenario is clipping
6//! children by the rounded contour of the parent.
7//!
8//! The mask size must match the target [`Pixmap`](crate::Pixmap): clipping is
9//! done pixel-by-pixel in canvas coordinates.
10
11use crate::geometry::Transform;
12use crate::path::{FillRule, Path};
13use crate::pixmap::FLATTEN_TOLERANCE;
14use crate::raster::Rasterizer;
15
16/// A clipping mask: `width × height` coverage values `0..=255`.
17#[derive(Clone)]
18pub struct Mask {
19    width: u32,
20    height: u32,
21    data: Vec<u8>,
22}
23
24impl Mask {
25    /// Creates an empty (fully transparent — everything is clipped) mask. `None`
26    /// for zero dimensions or overflow.
27    pub fn new(width: u32, height: u32) -> Option<Mask> {
28        if width == 0 || height == 0 {
29            return None;
30        }
31        let len = (width as usize).checked_mul(height as usize)?;
32        Some(Mask { width, height, data: vec![0; len] })
33    }
34
35    /// Creates a mask from a contour: inside the path — one, outside — zero.
36    pub fn from_path(
37        width: u32,
38        height: u32,
39        path: &Path,
40        fill_rule: FillRule,
41        anti_alias: bool,
42        transform: Transform,
43    ) -> Option<Mask> {
44        let mut mask = Mask::new(width, height)?;
45        mask.fill_path(path, fill_rule, anti_alias, transform);
46        Some(mask)
47    }
48
49    #[inline]
50    pub fn width(&self) -> u32 {
51        self.width
52    }
53
54    #[inline]
55    pub fn height(&self) -> u32 {
56        self.height
57    }
58
59    /// Raw coverage values (`0..=255`), one per pixel.
60    #[inline]
61    pub fn data(&self) -> &[u8] {
62        &self.data
63    }
64
65    /// Mask coverage at pixel `(x, y)` in the range `0.0..=1.0`. Outside the
66    /// mask — zero.
67    #[inline]
68    pub(crate) fn coverage_at(&self, x: usize, y: usize) -> f32 {
69        if x >= self.width as usize || y >= self.height as usize {
70            return 0.0;
71        }
72        self.data[y * self.width as usize + x] as f32 / 255.0
73    }
74
75    /// Adds a contour to the mask (union: the maximum coverage is taken).
76    pub fn fill_path(
77        &mut self,
78        path: &Path,
79        fill_rule: FillRule,
80        anti_alias: bool,
81        transform: Transform,
82    ) {
83        self.rasterize(path, fill_rule, anti_alias, transform, |old, cov| {
84            old.max(cov)
85        });
86    }
87
88    /// Intersects the mask with a contour (coverage is multiplied). Useful for
89    /// nested clipping regions.
90    pub fn intersect_path(
91        &mut self,
92        path: &Path,
93        fill_rule: FillRule,
94        anti_alias: bool,
95        transform: Transform,
96    ) {
97        // First compute the contour coverage into a separate buffer, then
98        // multiply it with the whole mask — pixels outside the contour are zeroed.
99        let coverage = match Mask::from_path(self.width, self.height, path, fill_rule, anti_alias, transform)
100        {
101            Some(m) => m,
102            None => return,
103        };
104        for (dst, &src) in self.data.iter_mut().zip(coverage.data.iter()) {
105            *dst = ((*dst as u16 * src as u16 + 127) / 255) as u8;
106        }
107    }
108
109    /// Rasterizes a contour and updates the mask values via `combine(old, cov)`.
110    fn rasterize<F: Fn(f32, f32) -> f32>(
111        &mut self,
112        path: &Path,
113        fill_rule: FillRule,
114        anti_alias: bool,
115        transform: Transform,
116        combine: F,
117    ) {
118        let tol = FLATTEN_TOLERANCE / transform.max_scale().max(1e-3);
119        let contours = path.to_contours(transform, tol);
120        if contours.is_empty() {
121            return;
122        }
123
124        // Bounding box of the shape — the rasterizer buffer is taken only for
125        // the intersection with the mask (as in `Pixmap::fill_polys`).
126        let (mut min_x, mut min_y) = (f32::INFINITY, f32::INFINITY);
127        let (mut max_x, mut max_y) = (f32::NEG_INFINITY, f32::NEG_INFINITY);
128        for c in contours.iter() {
129            for p in &c.points {
130                min_x = min_x.min(p.x);
131                min_y = min_y.min(p.y);
132                max_x = max_x.max(p.x);
133                max_y = max_y.max(p.y);
134            }
135        }
136        if !(min_x <= max_x && min_y <= max_y) {
137            return;
138        }
139
140        let mw = self.width as i32;
141        let mh = self.height as i32;
142        let x0 = (min_x.floor() as i32 - 1).clamp(0, mw);
143        let y0 = (min_y.floor() as i32 - 1).clamp(0, mh);
144        let x1 = (max_x.ceil() as i32 + 1).clamp(0, mw);
145        let y1 = (max_y.ceil() as i32 + 1).clamp(0, mh);
146        let bw = (x1 - x0) as usize;
147        let bh = (y1 - y0) as usize;
148        if bw == 0 || bh == 0 {
149            return;
150        }
151
152        let mut rast = Rasterizer::new(x0, y0, bw, bh);
153        for c in contours.iter() {
154            let n = c.points.len();
155            if n < 2 {
156                continue;
157            }
158            for i in 0..n {
159                rast.add_line(c.points[i], c.points[(i + 1) % n]);
160            }
161        }
162
163        let width = self.width as usize;
164        let data = &mut self.data;
165        rast.for_each_pixel(fill_rule, |x, y, coverage| {
166            let cov = if anti_alias {
167                coverage
168            } else if coverage >= 0.5 {
169                1.0
170            } else {
171                0.0
172            };
173            let idx = y * width + x;
174            let old = data[idx] as f32 / 255.0;
175            let new = combine(old, cov).clamp(0.0, 1.0);
176            data[idx] = (new * 255.0 + 0.5) as u8;
177        });
178    }
179}
180
181#[cfg(test)]
182mod tests {
183    use super::*;
184    use crate::geometry::Rect;
185    use crate::path::PathBuilder;
186
187    #[test]
188    fn rect_mask_covers_interior_only() {
189        let path = PathBuilder::from_rect(Rect::from_xywh(2.0, 2.0, 6.0, 6.0).unwrap());
190        let mask = Mask::from_path(10, 10, &path, FillRule::NonZero, true, Transform::identity())
191            .unwrap();
192        assert!(mask.coverage_at(5, 5) > 0.99, "inside");
193        assert!(mask.coverage_at(0, 0) < 0.01, "outside");
194    }
195
196    #[test]
197    fn intersect_keeps_overlap_only() {
198        let left = PathBuilder::from_rect(Rect::from_xywh(0.0, 0.0, 6.0, 10.0).unwrap());
199        let mut mask =
200            Mask::from_path(10, 10, &left, FillRule::NonZero, true, Transform::identity()).unwrap();
201        let right = PathBuilder::from_rect(Rect::from_xywh(4.0, 0.0, 6.0, 10.0).unwrap());
202        mask.intersect_path(&right, FillRule::NonZero, true, Transform::identity());
203        // Intersection [4,6): pixel 5 is inside both, pixel 1 — only the left one.
204        assert!(mask.coverage_at(5, 5) > 0.99, "intersection");
205        assert!(mask.coverage_at(1, 5) < 0.01, "outside the intersection");
206    }
207}