standard-plugin-sdk 0.1.0

Write Standard Code plugins in Rust: wasm components against standard:plugin@2.0.0
Documentation
//! Shapes in the graphics model: straight-alpha RGBA, antialiased edges.

use super::font::{GLYPH_ADVANCE, GLYPH_WIDTH, glyph_or_box};
use super::{Area, Shapes, clip_segment, unit};
use crate::colour::Rgba;
use crate::surface::{Canvas, Pixels, Surface};

/// `v` clamped to 0..=1 as a coverage byte.
fn coverage(v: f32) -> u8 {
    (v.clamp(0.0, 1.0) * 255.0 + 0.5) as u8
}

/// The signed distance from `(px, py)` to a box of half-size `(bx, by)`
/// centred on the origin with corners of radius `r` (negative inside).
fn rounded_box_distance(px: f32, py: f32, bx: f32, by: f32, r: f32) -> f32 {
    let qx = libm::fabsf(px) - (bx - r);
    let qy = libm::fabsf(py) - (by - r);
    let outside = libm::sqrtf(qx.max(0.0) * qx.max(0.0) + qy.max(0.0) * qy.max(0.0));
    outside + qx.max(qy).min(0.0) - r
}

/// Paints every pixel of the clipped box `[x0, x1) x [y0, y1)` whose
/// coverage `shade(px, py)` (pixel centres) is not zero.
fn shade(
    canvas: &mut Canvas<'_>,
    (x0, y0, x1, y1): (i64, i64, i64, i64),
    colour: Rgba,
    shade: impl Fn(f32, f32) -> f32,
) {
    let (left, right) = (x0.max(0), x1.min(i64::from(canvas.width)));
    let (top, bottom) = (y0.max(0), y1.min(i64::from(canvas.height)));
    for y in top..bottom {
        for x in left..right {
            let amount = coverage(shade(x as f32 + 0.5, y as f32 + 0.5));
            if amount > 0 {
                canvas.blend(x, y, colour, amount);
            }
        }
    }
    canvas.touch(left, top, right, bottom);
}

fn rounded(canvas: &mut Canvas<'_>, area: Area, radius: u32, colour: Rgba, fill: bool) {
    if area.w == 0 || area.h == 0 {
        return;
    }
    let (bx, by) = (area.w as f32 / 2.0, area.h as f32 / 2.0);
    let (cx, cy) = (area.left() as f32 + bx, area.top() as f32 + by);
    let r = (radius as f32).min(bx).min(by);
    let bounds = (area.left(), area.top(), area.right(), area.bottom());
    shade(canvas, bounds, colour, |px, py| {
        let d = rounded_box_distance(px - cx, py - cy, bx, by, r);
        if fill {
            0.5 - d
        } else {
            // A one-pixel ring just inside the edge.
            (0.5 - d).clamp(0.0, 1.0) - (-0.5 - d).clamp(0.0, 1.0)
        }
    });
}

/// A finite coordinate as the canvas's integer space can hold it.
fn finite(v: f32) -> f32 {
    if v.is_nan() {
        0.0
    } else {
        v.clamp(-1.0e9, 1.0e9)
    }
}

fn disc(canvas: &mut Canvas<'_>, centre: (f32, f32), radius: f32, colour: Rgba, fill: bool) {
    let (cx, cy) = (finite(centre.0), finite(centre.1));
    let r = finite(radius).clamp(0.0, 1.0e6);
    let reach = libm::ceilf(r) as i64 + 2;
    let (x, y) = (libm::floorf(cx) as i64, libm::floorf(cy) as i64);
    let bounds = (x - reach, y - reach, x + reach + 1, y + reach + 1);
    shade(canvas, bounds, colour, |px, py| {
        let d = libm::sqrtf((px - cx) * (px - cx) + (py - cy) * (py - cy)) - r;
        if fill { 0.5 - d } else { 1.0 - libm::fabsf(d) }
    });
}

/// Xiaolin Wu's antialiased line between pixel centres.
fn wu_line(canvas: &mut Canvas<'_>, from: (i64, i64), to: (i64, i64), colour: Rgba) {
    let ((x0, y0), (x1, y1)) = (from, to);
    let steep = (y1 - y0).abs() > (x1 - x0).abs();
    let (mut a0, mut b0, mut a1, mut b1) = if steep {
        (y0, x0, y1, x1)
    } else {
        (x0, y0, x1, y1)
    };
    if a0 > a1 {
        core::mem::swap(&mut a0, &mut a1);
        core::mem::swap(&mut b0, &mut b1);
    }
    let gradient = if a1 == a0 {
        0.0
    } else {
        (b1 - b0) as f32 / (a1 - a0) as f32
    };
    let mut b = b0 as f32;
    for a in a0..=a1 {
        let base = libm::floorf(b);
        let fraction = b - base;
        let (near, far) = (coverage(1.0 - fraction), coverage(fraction));
        let base = base as i64;
        if steep {
            canvas.blend(base, a, colour, near);
            canvas.blend(base + 1, a, colour, far);
        } else {
            canvas.blend(a, base, colour, near);
            canvas.blend(a, base + 1, colour, far);
        }
        b += gradient;
    }
    let (min_x, max_x) = (x0.min(x1), x0.max(x1));
    let (min_y, max_y) = (y0.min(y1), y0.max(y1));
    canvas.touch(min_x - 1, min_y - 1, max_x + 2, max_y + 2);
}

/// An RGBA8 image to draw with [`Surface::blit_clipped`]: `width` by
/// `height` pixels, `width * 4` bytes per row, straight alpha.
#[derive(Clone, Copy, Debug)]
pub struct Image<'a> {
    pub width: u32,
    pub height: u32,
    pub rgba: &'a [u8],
}

impl<'a> Image<'a> {
    pub const fn new(width: u32, height: u32, rgba: &'a [u8]) -> Self {
        Self {
            width,
            height,
            rgba,
        }
    }
}

impl Surface<Pixels> {
    /// Draws `image` with its top-left corner at `(x, y)`, clipped to `clip`
    /// and to the surface: only pixels inside both are written and marked
    /// dirty, so a sprite drawn into a viewport never spills out of it.
    /// With `blend` each pixel is drawn over what is there with its alpha;
    /// without, it replaces it. A short `rgba` draws the rows it holds.
    pub fn blit_clipped(&mut self, x: i32, y: i32, image: Image<'_>, clip: Area, blend: bool) {
        self.with_canvas(|canvas| {
            let (x0, y0) = (i64::from(x), i64::from(y));
            let left = x0.max(clip.left()).max(0);
            let right = (x0 + i64::from(image.width))
                .min(clip.right())
                .min(i64::from(canvas.width));
            let top = y0.max(clip.top()).max(0);
            let bottom = (y0 + i64::from(image.height))
                .min(clip.bottom())
                .min(i64::from(canvas.height));
            if left >= right || top >= bottom {
                return;
            }
            let stride = image.width as usize * 4;
            let columns = (right - left) as usize;
            let skip = (left - x0) as usize;
            let mut last_row = top;
            for target_y in top..bottom {
                let start = (target_y - y0) as usize * stride + skip * 4;
                let Some(source) = image.rgba.get(start..start + columns * 4) else {
                    break;
                };
                let offset = target_y as usize * canvas.width as usize + left as usize;
                let target = &mut canvas.slot[offset..offset + columns];
                for (word, pixel) in target.iter_mut().zip(source.chunks_exact(4)) {
                    let colour = Rgba::new(pixel[0], pixel[1], pixel[2], pixel[3]);
                    *word = if blend {
                        colour.over(Rgba::from_word(*word), 255).to_word()
                    } else {
                        colour.to_word()
                    };
                }
                last_row = target_y + 1;
            }
            canvas.touch(left, top, right, last_row);
        });
    }

    /// A rectangle at a fractional position and size, its edges
    /// antialiased: a paddle or a ball that moves by less than a pixel per
    /// frame moves smoothly.
    pub fn fill_rect_at(&mut self, x: f32, y: f32, w: f32, h: f32, colour: Rgba) {
        let (x, y) = (finite(x), finite(y));
        let (w, h) = (finite(w).max(0.0), finite(h).max(0.0));
        if w == 0.0 || h == 0.0 {
            return;
        }
        let (right, bottom) = (x + w, y + h);
        let bounds = (
            libm::floorf(x) as i64,
            libm::floorf(y) as i64,
            libm::ceilf(right) as i64,
            libm::ceilf(bottom) as i64,
        );
        self.with_canvas(|canvas| {
            shade(canvas, bounds, colour, |px, py| {
                // The overlap of the pixel [px - 0.5, px + 0.5) with the box.
                let across = (px + 0.5).min(right) - (px - 0.5).max(x);
                let down = (py + 0.5).min(bottom) - (py - 0.5).max(y);
                across.clamp(0.0, 1.0) * down.clamp(0.0, 1.0)
            });
        });
    }

    /// Text in the built-in 5x7 font at an integer `scale` (1 is 5x7
    /// pixels per glyph). Returns the pixels advanced.
    pub fn label_scaled(&mut self, x: i32, y: i32, text: &str, colour: Rgba, scale: u32) -> u32 {
        let scale = scale.max(1);
        let advance = i64::from(GLYPH_ADVANCE) * i64::from(scale);
        self.with_canvas(|canvas| {
            let mut pen = i64::from(x);
            let top = i64::from(y);
            let size = i64::from(scale);
            for character in text.chars() {
                if pen >= i64::from(canvas.width) {
                    break;
                }
                if pen + advance > 0 {
                    for (row, bits) in glyph_or_box(character).iter().enumerate() {
                        for column in 0..GLYPH_WIDTH {
                            if bits & (1 << (GLYPH_WIDTH - 1 - column)) != 0 {
                                let gx = pen + i64::from(column) * size;
                                let gy = top + row as i64 * size;
                                canvas.fill(gx, gy, gx + size, gy + size, colour);
                            }
                        }
                    }
                }
                pen += advance;
            }
            (pen - i64::from(x)).clamp(0, i64::from(u32::MAX)) as u32
        })
        .unwrap_or(0)
    }
}

impl Shapes for Surface<Pixels> {
    type Ink = Rgba;

    fn stroke_rect(&mut self, area: Area, ink: Rgba) {
        if area.w == 0 || area.h == 0 {
            return;
        }
        self.with_canvas(|canvas| {
            let (l, t, r, b) = (area.left(), area.top(), area.right(), area.bottom());
            canvas.fill(l, t, r, t + 1, ink);
            if b - t > 1 {
                canvas.fill(l, b - 1, r, b, ink);
            }
            canvas.fill(l, t + 1, l + 1, b - 1, ink);
            if r - l > 1 {
                canvas.fill(r - 1, t + 1, r, b - 1, ink);
            }
        });
    }

    fn fill_rect(&mut self, area: Area, ink: Rgba) {
        self.with_canvas(|canvas| {
            canvas.fill(area.left(), area.top(), area.right(), area.bottom(), ink);
        });
    }

    fn line(&mut self, from: (i32, i32), to: (i32, i32), ink: Rgba) {
        self.with_canvas(|canvas| {
            let from = (i64::from(from.0), i64::from(from.1));
            let to = (i64::from(to.0), i64::from(to.1));
            if let Some((a, b)) = clip_segment(from, to, canvas.width, canvas.height) {
                wu_line(canvas, a, b, ink);
            }
        });
    }

    fn bar(&mut self, area: Area, fraction: f32, fill: Rgba, track: Rgba) {
        self.with_canvas(|canvas| {
            let (l, t, r, b) = (area.left(), area.top(), area.right(), area.bottom());
            canvas.fill(l, t, r, b, track);
            let filled = unit(fraction) * area.w as f32;
            let whole = libm::floorf(filled) as i64;
            canvas.fill(l, t, l + whole, b, fill);
            let partial = coverage(filled - whole as f32);
            if partial > 0 && whole < i64::from(area.w) {
                for y in t..b {
                    canvas.blend(l + whole, y, fill, partial);
                }
            }
        });
    }

    fn rounded_rect(&mut self, area: Area, radius: u32, ink: Rgba) {
        self.with_canvas(|canvas| rounded(canvas, area, radius, ink, false));
    }

    fn fill_rounded_rect(&mut self, area: Area, radius: u32, ink: Rgba) {
        self.with_canvas(|canvas| rounded(canvas, area, radius, ink, true));
    }

    fn circle(&mut self, centre: (f32, f32), radius: f32, ink: Rgba) {
        self.with_canvas(|canvas| disc(canvas, centre, radius, ink, false));
    }

    fn fill_circle(&mut self, centre: (f32, f32), radius: f32, ink: Rgba) {
        self.with_canvas(|canvas| disc(canvas, centre, radius, ink, true));
    }

    fn label(&mut self, x: i32, y: i32, text: &str, ink: Rgba) -> u32 {
        self.label_scaled(x, y, text, ink, 1)
    }
}