Lightningbeam/lightningbeam-ui/lightningbeam-core/src/raster_draw.rs

195 lines
7.0 KiB
Rust

//! CPU-side raster drawing primitives for geometric shapes on raster layers.
//!
//! All coordinates are in canvas pixels (f32). The pixel buffer is RGBA u8,
//! 4 bytes per pixel, row-major, top-left origin.
/// RGBA color as `[R, G, B, A]` bytes.
pub type Rgba = [u8; 4];
// ---------------------------------------------------------------------------
// Internal helpers
// ---------------------------------------------------------------------------
/// Alpha-composite `color` (RGBA) onto `pixels[idx..idx+4]` with an extra
/// `coverage` factor (0.0 = transparent, 1.0 = full color alpha).
#[inline]
fn blend_at(pixels: &mut [u8], idx: usize, color: Rgba, coverage: f32) {
let a = (color[3] as f32 / 255.0) * coverage;
if a <= 0.0 { return; }
let inv = 1.0 - a;
pixels[idx] = (color[0] as f32 * a + pixels[idx] as f32 * inv) as u8;
pixels[idx + 1] = (color[1] as f32 * a + pixels[idx + 1] as f32 * inv) as u8;
pixels[idx + 2] = (color[2] as f32 * a + pixels[idx + 2] as f32 * inv) as u8;
pixels[idx + 3] = ((a + pixels[idx + 3] as f32 / 255.0 * inv) * 255.0).min(255.0) as u8;
}
/// Write a pixel at integer canvas coordinates, clipped to canvas bounds.
#[inline]
fn put(pixels: &mut [u8], w: u32, h: u32, x: i32, y: i32, color: Rgba, coverage: f32) {
if x < 0 || y < 0 || x >= w as i32 || y >= h as i32 { return; }
let idx = (y as u32 * w + x as u32) as usize * 4;
blend_at(pixels, idx, color, coverage);
}
/// Draw an anti-aliased filled disk at (`cx`, `cy`) with the given `radius`.
fn draw_disk(pixels: &mut [u8], w: u32, h: u32, cx: f32, cy: f32, radius: f32, color: Rgba) {
let r = (radius + 1.0) as i32;
let ix = cx as i32;
let iy = cy as i32;
for dy in -r..=r {
for dx in -r..=r {
let px = ix + dx;
let py = iy + dy;
let dist = ((px as f32 - cx).powi(2) + (py as f32 - cy).powi(2)).sqrt();
let cov = (radius + 0.5 - dist).clamp(0.0, 1.0);
if cov > 0.0 {
put(pixels, w, h, px, py, color, cov);
}
}
}
}
// ---------------------------------------------------------------------------
// Public API
// ---------------------------------------------------------------------------
/// Draw a thick line from (`x0`, `y0`) to (`x1`, `y1`) by stamping
/// anti-aliased disks of radius `thickness / 2` at every half-pixel step.
pub fn draw_line(
pixels: &mut [u8], w: u32, h: u32,
x0: f32, y0: f32, x1: f32, y1: f32,
color: Rgba, thickness: f32,
) {
let radius = (thickness / 2.0).max(0.5);
let dx = x1 - x0;
let dy = y1 - y0;
let len = (dx * dx + dy * dy).sqrt();
if len < 0.5 {
draw_disk(pixels, w, h, x0, y0, radius, color);
return;
}
let steps = ((len * 2.0).ceil() as i32).max(1);
for i in 0..=steps {
let t = i as f32 / steps as f32;
draw_disk(pixels, w, h, x0 + dx * t, y0 + dy * t, radius, color);
}
}
/// Draw a rectangle with corners (`x0`, `y0`) and (`x1`, `y1`).
///
/// `stroke` draws the four edges; `fill` fills the interior. Either may be
/// `None` to skip that part.
pub fn draw_rect(
pixels: &mut [u8], w: u32, h: u32,
x0: f32, y0: f32, x1: f32, y1: f32,
stroke: Option<Rgba>, fill: Option<Rgba>, thickness: f32,
) {
let (lx, rx) = if x0 <= x1 { (x0, x1) } else { (x1, x0) };
let (ty, by) = if y0 <= y1 { (y0, y1) } else { (y1, y0) };
if let Some(fc) = fill {
let px0 = lx.ceil() as i32;
let py0 = ty.ceil() as i32;
let px1 = rx.floor() as i32;
let py1 = by.floor() as i32;
for py in py0..=py1 {
for px in px0..=px1 {
put(pixels, w, h, px, py, fc, 1.0);
}
}
}
if let Some(sc) = stroke {
draw_line(pixels, w, h, lx, ty, rx, ty, sc, thickness); // top
draw_line(pixels, w, h, rx, ty, rx, by, sc, thickness); // right
draw_line(pixels, w, h, rx, by, lx, by, sc, thickness); // bottom
draw_line(pixels, w, h, lx, by, lx, ty, sc, thickness); // left
}
}
/// Draw an ellipse centred at (`cx`, `cy`) with semi-axes `rx` and `ry`.
///
/// `stroke` draws the outline; `fill` fills the interior via scanline.
pub fn draw_ellipse(
pixels: &mut [u8], w: u32, h: u32,
cx: f32, cy: f32, rx: f32, ry: f32,
stroke: Option<Rgba>, fill: Option<Rgba>, thickness: f32,
) {
if rx <= 0.0 || ry <= 0.0 { return; }
if let Some(fc) = fill {
let py0 = (cy - ry).ceil() as i32;
let py1 = (cy + ry).floor() as i32;
for py in py0..=py1 {
let dy = py as f32 - cy;
let t = 1.0 - (dy / ry).powi(2);
if t <= 0.0 { continue; }
let x_ext = rx * t.sqrt();
let px0 = (cx - x_ext).ceil() as i32;
let px1 = (cx + x_ext).floor() as i32;
for px in px0..=px1 {
put(pixels, w, h, px, py, fc, 1.0);
}
}
}
if let Some(sc) = stroke {
let radius = (thickness / 2.0).max(0.5);
// Ramanujan's perimeter approximation for step count.
let perim = std::f32::consts::PI
* (3.0 * (rx + ry) - ((3.0 * rx + ry) * (rx + 3.0 * ry)).sqrt());
let steps = ((perim * 2.0).ceil() as i32).max(16);
for i in 0..steps {
let t = i as f32 / steps as f32 * std::f32::consts::TAU;
draw_disk(pixels, w, h, cx + rx * t.cos(), cy + ry * t.sin(), radius, sc);
}
}
}
/// Draw a closed polygon given world-space `vertices` (at least 2).
///
/// `stroke` draws the outline; `fill` fills the interior via scanline.
pub fn draw_polygon(
pixels: &mut [u8], w: u32, h: u32,
vertices: &[(f32, f32)],
stroke: Option<Rgba>, fill: Option<Rgba>, thickness: f32,
) {
let n = vertices.len();
if n < 2 { return; }
if let Some(fc) = fill {
let min_y = vertices.iter().map(|v| v.1).fold(f32::MAX, f32::min).ceil() as i32;
let max_y = vertices.iter().map(|v| v.1).fold(f32::MIN, f32::max).floor() as i32;
let mut xs: Vec<f32> = Vec::with_capacity(n);
for py in min_y..=max_y {
xs.clear();
let scan_y = py as f32 + 0.5;
for i in 0..n {
let (x0, y0) = vertices[i];
let (x1, y1) = vertices[(i + 1) % n];
if (y0 <= scan_y && scan_y < y1) || (y1 <= scan_y && scan_y < y0) {
xs.push(x0 + (scan_y - y0) / (y1 - y0) * (x1 - x0));
}
}
xs.sort_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal));
let mut j = 0;
while j + 1 < xs.len() {
let px0 = xs[j].ceil() as i32;
let px1 = xs[j + 1].floor() as i32;
for px in px0..=px1 {
put(pixels, w, h, px, py, fc, 1.0);
}
j += 2;
}
}
}
if let Some(sc) = stroke {
for i in 0..n {
let (x0, y0) = vertices[i];
let (x1, y1) = vertices[(i + 1) % n];
draw_line(pixels, w, h, x0, y0, x1, y1, sc, thickness);
}
}
}