Skip to main content

yog_graphics/painting/
raster.rs

1//! A canvas to draw on in pixels (`y` down): shapes filled with signed-area
2//! coverage (anti-aliased, non-zero winding), strokes as the union of
3//! their segments with round joins and caps, images sampled into place.
4
5use crate::geometry::math::Vec2;
6use crate::geometry::rect::Rect;
7use crate::pixels::color::Color;
8use crate::pixels::image::Image;
9use crate::pixels::mask::Mask;
10
11/// The thinnest stroke's half-width: thinner ones vanish between pixels.
12const MIN_STROKE_RADIUS: f32 = 0.35;
13/// How far inside the right edge a line is kept, so it lands in the last column.
14const EDGE_INSET: f32 = 0.001;
15/// The middle of a line's second cell, in cells from its first cell's left edge.
16const SECOND_CELL_MIDDLE: f32 = 1.5;
17
18/// Pixels being drawn, premultiplied, in floats.
19#[derive(Debug, Clone)]
20pub struct Canvas {
21    w: usize,
22    h: usize,
23    px: Vec<[f32; 4]>,
24}
25
26impl Canvas {
27    /// A transparent `w × h` canvas.
28    pub fn new(w: u32, h: u32) -> Canvas {
29        Canvas {
30            w: w as usize,
31            h: h as usize,
32            px: vec![[0.0; 4]; (w * h) as usize],
33        }
34    }
35
36    pub fn width(&self) -> u32 {
37        self.w as u32
38    }
39
40    pub fn height(&self) -> u32 {
41        self.h as u32
42    }
43
44    /// The inside of `contours` (closed polylines; an open one is closed)
45    /// painted `color`.
46    pub fn fill(&mut self, contours: &[Vec<Vec2>], color: Color) {
47        let lines = contours.iter().flat_map(|c| {
48            c.iter()
49                .zip(c.iter().cycle().skip(1))
50                .map(|(a, b)| (*a, *b))
51        });
52        let cov = coverage(self.w, self.h, lines);
53        self.paint(&cov, color);
54    }
55
56    /// `polylines` drawn `width` wide in `color`, round-joined and capped.
57    pub fn stroke(&mut self, polylines: &[Vec<Vec2>], width: f32, color: Color) {
58        let r = (width / 2.0).max(MIN_STROKE_RADIUS);
59        let mut pieces: Vec<Vec<Vec2>> = Vec::new();
60        for line in polylines {
61            for w in line.windows(2) {
62                pieces.extend(segment_quad(w[0], w[1], r));
63            }
64            for &p in line {
65                pieces.push(circle(p, r));
66            }
67        }
68        let lines = pieces.iter().flat_map(|c| {
69            c.iter()
70                .zip(c.iter().cycle().skip(1))
71                .map(|(a, b)| (*a, *b))
72        });
73        let cov = coverage(self.w, self.h, lines);
74        self.paint(&cov, color);
75    }
76
77    /// `image` sampled (nearest) into `dst`, its colors multiplied by
78    /// `tint` if given.
79    pub fn image(&mut self, image: &Image, dst: Rect, tint: Option<Color>) {
80        if image.is_empty() {
81            return;
82        }
83        let tint = tint.map(Color::to_f32);
84        self.sampled(image.width, image.height, dst, |u, v| {
85            let c = image.pixel(u, v).to_f32();
86            Some(match tint {
87                Some(t) => [c[0] * t[0], c[1] * t[1], c[2] * t[2], c[3] * t[3]],
88                None => c,
89            })
90        });
91    }
92
93    /// `mask` in box `dst` (pixels), its set pixels `color`.
94    pub fn mask(&mut self, mask: &Mask, dst: Rect, color: Color) {
95        let c = color.to_f32();
96        self.sampled(mask.width, mask.height, dst, |u, v| {
97            mask.get(u, v).then_some(c)
98        });
99    }
100
101    /// A `width × height` picture in box `dst`: each canvas pixel in it
102    /// takes the picture's pixel under its centre, `sample` giving its color
103    /// (none: nothing there).
104    fn sampled(
105        &mut self,
106        width: u32,
107        height: u32,
108        dst: Rect,
109        sample: impl Fn(u32, u32) -> Option<[f32; 4]>,
110    ) {
111        if width == 0 || height == 0 || dst.w <= 0.0 || dst.h <= 0.0 {
112            return;
113        }
114        for py in
115            (dst.y.floor().max(0.0) as usize)..(dst.bottom().ceil().min(self.h as f32) as usize)
116        {
117            for px in
118                (dst.x.floor().max(0.0) as usize)..(dst.right().ceil().min(self.w as f32) as usize)
119            {
120                let u = ((px as f32 + 0.5 - dst.x) / dst.w * width as f32).floor();
121                let v = ((py as f32 + 0.5 - dst.y) / dst.h * height as f32).floor();
122                if u < 0.0 || v < 0.0 || u >= width as f32 || v >= height as f32 {
123                    continue;
124                }
125                if let Some(c) = sample(u as u32, v as u32) {
126                    over(&mut self.px[py * self.w + px], c);
127                }
128            }
129        }
130    }
131
132    /// `color` over the canvas where `cov` (0..1 a pixel) covers it.
133    fn paint(&mut self, cov: &[f32], color: Color) {
134        let c = color.to_f32();
135        for (px, &k) in self.px.iter_mut().zip(cov) {
136            if k > 0.0 {
137                over(px, [c[0], c[1], c[2], c[3] * k]);
138            }
139        }
140    }
141
142    /// The drawing, straight alpha.
143    pub fn into_image(self) -> Image {
144        let mut rgba = Vec::with_capacity(self.w * self.h * 4);
145        for p in &self.px {
146            let a = p[3];
147            let un = |c: f32| if a > 0.0 { c / a } else { 0.0 };
148            rgba.extend(Color::from_f32([un(p[0]), un(p[1]), un(p[2]), a]).to_rgba8());
149        }
150        Image {
151            width: self.w as u32,
152            height: self.h as u32,
153            rgba,
154        }
155    }
156}
157
158/// Straight `src` over premultiplied `dst`.
159fn over(dst: &mut [f32; 4], src: [f32; 4]) {
160    let a = src[3];
161    for i in 0..3 {
162        dst[i] = src[i] * a + dst[i] * (1.0 - a);
163    }
164    dst[3] = a + dst[3] * (1.0 - a);
165}
166
167/// A rectangle around segment `a → b`, `r` to each side, wound one way.
168fn segment_quad(a: Vec2, b: Vec2, r: f32) -> Option<Vec<Vec2>> {
169    let n = (b - a).normalize().perp() * r;
170    (a != b).then(|| oriented(vec![a + n, b + n, b - n, a - n]))
171}
172
173/// A circle as a polygon, wound one way.
174fn circle(c: Vec2, r: f32) -> Vec<Vec2> {
175    let n = ((r * 2.0).ceil() as usize * 4).clamp(8, 48);
176    oriented(
177        (0..n)
178            .map(|i| {
179                let t = i as f32 / n as f32 * std::f32::consts::TAU;
180                c + Vec2::new(t.cos(), t.sin()) * r
181            })
182            .collect(),
183    )
184}
185
186/// `poly` wound the same way as every other stroke piece, so where they
187/// overlap they add up instead of cancelling.
188fn oriented(mut poly: Vec<Vec2>) -> Vec<Vec2> {
189    let area: f32 = poly
190        .iter()
191        .zip(poly.iter().cycle().skip(1))
192        .map(|(a, b)| a.x * b.y - b.x * a.y)
193        .sum();
194    if area < 0.0 {
195        poly.reverse();
196    }
197    poly
198}
199
200/// Coverage of the shape `lines` bound (closed contours, pixels), each
201/// pixel 0..1: signed area accumulated along each row.
202fn coverage(w: usize, h: usize, lines: impl Iterator<Item = (Vec2, Vec2)>) -> Vec<f32> {
203    let mut acc = vec![0.0f32; w * h + 4];
204    if w == 0 || h == 0 {
205        return Vec::new();
206    }
207    for (p0, p1) in lines {
208        accumulate(&mut acc, w, h, p0, p1);
209    }
210    let mut sum = 0.0;
211    acc.truncate(w * h);
212    for a in acc.iter_mut() {
213        sum += *a;
214        *a = sum.abs().min(1.0);
215    }
216    acc
217}
218
219/// Adds line `p0 → p1`'s signed area to the rows it crosses.
220fn accumulate(acc: &mut [f32], w: usize, h: usize, p0: Vec2, p1: Vec2) {
221    if (p0.y - p1.y).abs() <= f32::EPSILON {
222        return;
223    }
224    let (dir, p0, p1) = if p0.y < p1.y {
225        (1.0, p0, p1)
226    } else {
227        (-1.0, p1, p0)
228    };
229    let clamp_x = |x: f32| x.clamp(0.0, w as f32 - EDGE_INSET);
230    let dxdy = (p1.x - p0.x) / (p1.y - p0.y);
231    let mut x = p0.x;
232    if p0.y < 0.0 {
233        x -= p0.y * dxdy;
234    }
235    let y_start = p0.y.max(0.0) as usize;
236    let y_end = (p1.y.ceil().max(0.0) as usize).min(h);
237    for y in y_start..y_end {
238        let row = y * w;
239        let dy = ((y + 1) as f32).min(p1.y) - (y as f32).max(p0.y);
240        let xnext = x + dxdy * dy;
241        let d = dy * dir;
242        let (x0, x1) = if x < xnext {
243            (clamp_x(x), clamp_x(xnext))
244        } else {
245            (clamp_x(xnext), clamp_x(x))
246        };
247        let x0f = x0.floor();
248        let x0i = x0f as usize;
249        let x1i = x1.ceil() as usize;
250        if x1i <= x0i + 1 {
251            let xm = 0.5 * (x0 + x1) - x0f;
252            acc[row + x0i] += d - d * xm;
253            acc[row + x0i + 1] += d * xm;
254        } else {
255            let s = 1.0 / (x1 - x0);
256            let x0r = x0 - x0f;
257            let a0 = 0.5 * s * (1.0 - x0r) * (1.0 - x0r);
258            let x1r = x1 - x1.ceil() + 1.0;
259            let am = 0.5 * s * x1r * x1r;
260            acc[row + x0i] += d * a0;
261            if x1i == x0i + 2 {
262                acc[row + x0i + 1] += d * (1.0 - a0 - am);
263            } else {
264                let a1 = s * (SECOND_CELL_MIDDLE - x0r);
265                acc[row + x0i + 1] += d * (a1 - a0);
266                for xi in x0i + 2..x1i - 1 {
267                    acc[row + xi] += d * s;
268                }
269                let a2 = a1 + (x1i - x0i - 3) as f32 * s;
270                acc[row + x1i - 1] += d * (1.0 - a2 - am);
271            }
272            acc[row + x1i] += d * am;
273        }
274        x = xnext;
275    }
276}
277
278#[cfg(test)]
279mod tests {
280    use super::*;
281
282    #[test]
283    fn shapes_fill_and_strokes_round() {
284        let mut c = Canvas::new(20, 20);
285        c.fill(
286            &[vec![
287                Vec2::new(2.0, 2.0),
288                Vec2::new(10.0, 2.0),
289                Vec2::new(10.0, 10.0),
290                Vec2::new(2.0, 10.0),
291            ]],
292            Color::WHITE,
293        );
294        c.stroke(
295            &[vec![Vec2::new(4.0, 15.0), Vec2::new(16.0, 15.0)]],
296            2.0,
297            Color::rgb(0xff0000),
298        );
299        let img = c.into_image();
300        assert_eq!(img.pixel(5, 5), Color::WHITE);
301        assert_eq!(img.pixel(11, 5).a(), 0);
302        assert_eq!(img.pixel(10, 15), Color::rgb(0xff0000));
303        assert_eq!(img.pixel(10, 17).a(), 0);
304    }
305
306    #[test]
307    fn a_square_with_a_hole_keeps_it() {
308        let mut c = Canvas::new(12, 12);
309        let outer = vec![
310            Vec2::new(1.0, 1.0),
311            Vec2::new(11.0, 1.0),
312            Vec2::new(11.0, 11.0),
313            Vec2::new(1.0, 11.0),
314        ];
315        let inner = vec![
316            Vec2::new(4.0, 4.0),
317            Vec2::new(4.0, 8.0),
318            Vec2::new(8.0, 8.0),
319            Vec2::new(8.0, 4.0),
320        ];
321        c.fill(&[outer, inner], Color::WHITE);
322        let img = c.into_image();
323        assert_eq!(img.pixel(2, 2), Color::WHITE);
324        assert_eq!(img.pixel(6, 6).a(), 0);
325    }
326}