Skip to main content

standard_plugin/draw/
pixels.rs

1//! Shapes in the graphics model: straight-alpha RGBA, antialiased edges.
2
3use super::font::{GLYPH_ADVANCE, GLYPH_WIDTH, glyph_or_box};
4use super::{Area, Shapes, clip_segment, unit};
5use crate::colour::Rgba;
6use crate::surface::{Canvas, Pixels, Surface};
7
8/// `v` clamped to 0..=1 as a coverage byte.
9fn coverage(v: f32) -> u8 {
10    (v.clamp(0.0, 1.0) * 255.0 + 0.5) as u8
11}
12
13/// The signed distance from `(px, py)` to a box of half-size `(bx, by)`
14/// centred on the origin with corners of radius `r` (negative inside).
15fn rounded_box_distance(px: f32, py: f32, bx: f32, by: f32, r: f32) -> f32 {
16    let qx = libm::fabsf(px) - (bx - r);
17    let qy = libm::fabsf(py) - (by - r);
18    let outside = libm::sqrtf(qx.max(0.0) * qx.max(0.0) + qy.max(0.0) * qy.max(0.0));
19    outside + qx.max(qy).min(0.0) - r
20}
21
22/// Paints every pixel of the clipped box `[x0, x1) x [y0, y1)` whose
23/// coverage `shade(px, py)` (pixel centres) is not zero.
24fn shade(
25    canvas: &mut Canvas<'_>,
26    (x0, y0, x1, y1): (i64, i64, i64, i64),
27    colour: Rgba,
28    shade: impl Fn(f32, f32) -> f32,
29) {
30    let (left, right) = (x0.max(0), x1.min(i64::from(canvas.width)));
31    let (top, bottom) = (y0.max(0), y1.min(i64::from(canvas.height)));
32    for y in top..bottom {
33        for x in left..right {
34            let amount = coverage(shade(x as f32 + 0.5, y as f32 + 0.5));
35            if amount > 0 {
36                canvas.blend(x, y, colour, amount);
37            }
38        }
39    }
40    canvas.touch(left, top, right, bottom);
41}
42
43fn rounded(canvas: &mut Canvas<'_>, area: Area, radius: u32, colour: Rgba, fill: bool) {
44    if area.w == 0 || area.h == 0 {
45        return;
46    }
47    let (bx, by) = (area.w as f32 / 2.0, area.h as f32 / 2.0);
48    let (cx, cy) = (area.left() as f32 + bx, area.top() as f32 + by);
49    let r = (radius as f32).min(bx).min(by);
50    let bounds = (area.left(), area.top(), area.right(), area.bottom());
51    shade(canvas, bounds, colour, |px, py| {
52        let d = rounded_box_distance(px - cx, py - cy, bx, by, r);
53        if fill {
54            0.5 - d
55        } else {
56            // A one-pixel ring just inside the edge.
57            (0.5 - d).clamp(0.0, 1.0) - (-0.5 - d).clamp(0.0, 1.0)
58        }
59    });
60}
61
62/// A finite coordinate as the canvas's integer space can hold it.
63fn finite(v: f32) -> f32 {
64    if v.is_nan() {
65        0.0
66    } else {
67        v.clamp(-1.0e9, 1.0e9)
68    }
69}
70
71fn disc(canvas: &mut Canvas<'_>, centre: (f32, f32), radius: f32, colour: Rgba, fill: bool) {
72    let (cx, cy) = (finite(centre.0), finite(centre.1));
73    let r = finite(radius).clamp(0.0, 1.0e6);
74    let reach = libm::ceilf(r) as i64 + 2;
75    let (x, y) = (libm::floorf(cx) as i64, libm::floorf(cy) as i64);
76    let bounds = (x - reach, y - reach, x + reach + 1, y + reach + 1);
77    shade(canvas, bounds, colour, |px, py| {
78        let d = libm::sqrtf((px - cx) * (px - cx) + (py - cy) * (py - cy)) - r;
79        if fill { 0.5 - d } else { 1.0 - libm::fabsf(d) }
80    });
81}
82
83/// Xiaolin Wu's antialiased line between pixel centres.
84fn wu_line(canvas: &mut Canvas<'_>, from: (i64, i64), to: (i64, i64), colour: Rgba) {
85    let ((x0, y0), (x1, y1)) = (from, to);
86    let steep = (y1 - y0).abs() > (x1 - x0).abs();
87    let (mut a0, mut b0, mut a1, mut b1) = if steep {
88        (y0, x0, y1, x1)
89    } else {
90        (x0, y0, x1, y1)
91    };
92    if a0 > a1 {
93        core::mem::swap(&mut a0, &mut a1);
94        core::mem::swap(&mut b0, &mut b1);
95    }
96    let gradient = if a1 == a0 {
97        0.0
98    } else {
99        (b1 - b0) as f32 / (a1 - a0) as f32
100    };
101    let mut b = b0 as f32;
102    for a in a0..=a1 {
103        let base = libm::floorf(b);
104        let fraction = b - base;
105        let (near, far) = (coverage(1.0 - fraction), coverage(fraction));
106        let base = base as i64;
107        if steep {
108            canvas.blend(base, a, colour, near);
109            canvas.blend(base + 1, a, colour, far);
110        } else {
111            canvas.blend(a, base, colour, near);
112            canvas.blend(a, base + 1, colour, far);
113        }
114        b += gradient;
115    }
116    let (min_x, max_x) = (x0.min(x1), x0.max(x1));
117    let (min_y, max_y) = (y0.min(y1), y0.max(y1));
118    canvas.touch(min_x - 1, min_y - 1, max_x + 2, max_y + 2);
119}
120
121/// An RGBA8 image to draw with [`Surface::blit_clipped`]: `width` by
122/// `height` pixels, `width * 4` bytes per row, straight alpha.
123#[derive(Clone, Copy, Debug)]
124pub struct Image<'a> {
125    pub width: u32,
126    pub height: u32,
127    pub rgba: &'a [u8],
128}
129
130impl<'a> Image<'a> {
131    pub const fn new(width: u32, height: u32, rgba: &'a [u8]) -> Self {
132        Self {
133            width,
134            height,
135            rgba,
136        }
137    }
138}
139
140impl Surface<Pixels> {
141    /// Draws `image` with its top-left corner at `(x, y)`, clipped to `clip`
142    /// and to the surface: only pixels inside both are written and marked
143    /// dirty, so a sprite drawn into a viewport never spills out of it.
144    /// With `blend` each pixel is drawn over what is there with its alpha;
145    /// without, it replaces it. A short `rgba` draws the rows it holds.
146    pub fn blit_clipped(&mut self, x: i32, y: i32, image: Image<'_>, clip: Area, blend: bool) {
147        self.with_canvas(|canvas| {
148            let (x0, y0) = (i64::from(x), i64::from(y));
149            let left = x0.max(clip.left()).max(0);
150            let right = (x0 + i64::from(image.width))
151                .min(clip.right())
152                .min(i64::from(canvas.width));
153            let top = y0.max(clip.top()).max(0);
154            let bottom = (y0 + i64::from(image.height))
155                .min(clip.bottom())
156                .min(i64::from(canvas.height));
157            if left >= right || top >= bottom {
158                return;
159            }
160            let stride = image.width as usize * 4;
161            let columns = (right - left) as usize;
162            let skip = (left - x0) as usize;
163            let mut last_row = top;
164            for target_y in top..bottom {
165                let start = (target_y - y0) as usize * stride + skip * 4;
166                let Some(source) = image.rgba.get(start..start + columns * 4) else {
167                    break;
168                };
169                let offset = target_y as usize * canvas.width as usize + left as usize;
170                let target = &mut canvas.slot[offset..offset + columns];
171                for (word, pixel) in target.iter_mut().zip(source.chunks_exact(4)) {
172                    let colour = Rgba::new(pixel[0], pixel[1], pixel[2], pixel[3]);
173                    *word = if blend {
174                        colour.over(Rgba::from_word(*word), 255).to_word()
175                    } else {
176                        colour.to_word()
177                    };
178                }
179                last_row = target_y + 1;
180            }
181            canvas.touch(left, top, right, last_row);
182        });
183    }
184
185    /// A rectangle at a fractional position and size, its edges
186    /// antialiased: a paddle or a ball that moves by less than a pixel per
187    /// frame moves smoothly.
188    pub fn fill_rect_at(&mut self, x: f32, y: f32, w: f32, h: f32, colour: Rgba) {
189        let (x, y) = (finite(x), finite(y));
190        let (w, h) = (finite(w).max(0.0), finite(h).max(0.0));
191        if w == 0.0 || h == 0.0 {
192            return;
193        }
194        let (right, bottom) = (x + w, y + h);
195        let bounds = (
196            libm::floorf(x) as i64,
197            libm::floorf(y) as i64,
198            libm::ceilf(right) as i64,
199            libm::ceilf(bottom) as i64,
200        );
201        self.with_canvas(|canvas| {
202            shade(canvas, bounds, colour, |px, py| {
203                // The overlap of the pixel [px - 0.5, px + 0.5) with the box.
204                let across = (px + 0.5).min(right) - (px - 0.5).max(x);
205                let down = (py + 0.5).min(bottom) - (py - 0.5).max(y);
206                across.clamp(0.0, 1.0) * down.clamp(0.0, 1.0)
207            });
208        });
209    }
210
211    /// Text in the built-in 5x7 font at an integer `scale` (1 is 5x7
212    /// pixels per glyph). Returns the pixels advanced.
213    pub fn label_scaled(&mut self, x: i32, y: i32, text: &str, colour: Rgba, scale: u32) -> u32 {
214        let scale = scale.max(1);
215        let advance = i64::from(GLYPH_ADVANCE) * i64::from(scale);
216        self.with_canvas(|canvas| {
217            let mut pen = i64::from(x);
218            let top = i64::from(y);
219            let size = i64::from(scale);
220            for character in text.chars() {
221                if pen >= i64::from(canvas.width) {
222                    break;
223                }
224                if pen + advance > 0 {
225                    for (row, bits) in glyph_or_box(character).iter().enumerate() {
226                        for column in 0..GLYPH_WIDTH {
227                            if bits & (1 << (GLYPH_WIDTH - 1 - column)) != 0 {
228                                let gx = pen + i64::from(column) * size;
229                                let gy = top + row as i64 * size;
230                                canvas.fill(gx, gy, gx + size, gy + size, colour);
231                            }
232                        }
233                    }
234                }
235                pen += advance;
236            }
237            (pen - i64::from(x)).clamp(0, i64::from(u32::MAX)) as u32
238        })
239        .unwrap_or(0)
240    }
241}
242
243impl Shapes for Surface<Pixels> {
244    type Ink = Rgba;
245
246    fn stroke_rect(&mut self, area: Area, ink: Rgba) {
247        if area.w == 0 || area.h == 0 {
248            return;
249        }
250        self.with_canvas(|canvas| {
251            let (l, t, r, b) = (area.left(), area.top(), area.right(), area.bottom());
252            canvas.fill(l, t, r, t + 1, ink);
253            if b - t > 1 {
254                canvas.fill(l, b - 1, r, b, ink);
255            }
256            canvas.fill(l, t + 1, l + 1, b - 1, ink);
257            if r - l > 1 {
258                canvas.fill(r - 1, t + 1, r, b - 1, ink);
259            }
260        });
261    }
262
263    fn fill_rect(&mut self, area: Area, ink: Rgba) {
264        self.with_canvas(|canvas| {
265            canvas.fill(area.left(), area.top(), area.right(), area.bottom(), ink);
266        });
267    }
268
269    fn line(&mut self, from: (i32, i32), to: (i32, i32), ink: Rgba) {
270        self.with_canvas(|canvas| {
271            let from = (i64::from(from.0), i64::from(from.1));
272            let to = (i64::from(to.0), i64::from(to.1));
273            if let Some((a, b)) = clip_segment(from, to, canvas.width, canvas.height) {
274                wu_line(canvas, a, b, ink);
275            }
276        });
277    }
278
279    fn bar(&mut self, area: Area, fraction: f32, fill: Rgba, track: Rgba) {
280        self.with_canvas(|canvas| {
281            let (l, t, r, b) = (area.left(), area.top(), area.right(), area.bottom());
282            canvas.fill(l, t, r, b, track);
283            let filled = unit(fraction) * area.w as f32;
284            let whole = libm::floorf(filled) as i64;
285            canvas.fill(l, t, l + whole, b, fill);
286            let partial = coverage(filled - whole as f32);
287            if partial > 0 && whole < i64::from(area.w) {
288                for y in t..b {
289                    canvas.blend(l + whole, y, fill, partial);
290                }
291            }
292        });
293    }
294
295    fn rounded_rect(&mut self, area: Area, radius: u32, ink: Rgba) {
296        self.with_canvas(|canvas| rounded(canvas, area, radius, ink, false));
297    }
298
299    fn fill_rounded_rect(&mut self, area: Area, radius: u32, ink: Rgba) {
300        self.with_canvas(|canvas| rounded(canvas, area, radius, ink, true));
301    }
302
303    fn circle(&mut self, centre: (f32, f32), radius: f32, ink: Rgba) {
304        self.with_canvas(|canvas| disc(canvas, centre, radius, ink, false));
305    }
306
307    fn fill_circle(&mut self, centre: (f32, f32), radius: f32, ink: Rgba) {
308        self.with_canvas(|canvas| disc(canvas, centre, radius, ink, true));
309    }
310
311    fn label(&mut self, x: i32, y: i32, text: &str, ink: Rgba) -> u32 {
312        self.label_scaled(x, y, text, ink, 1)
313    }
314}