#![allow(clippy::too_many_arguments)]
use ab_glyph::{point, Font, FontRef, PxScale, ScaleFont};
use super::backend::{
DrawBackend, FillPattern, FontFace, LineStyle, PointStyle, RectStyle, TextAnchor, TextStyle,
};
use super::{Rect, RenderError};
const FONT: &[u8] = include_bytes!("../../assets/fonts/DejaVuSans.ttf");
pub struct PixelBackend {
width: usize,
height: usize,
buf: Vec<u8>,
plot_area: Rect,
total_area: Rect,
font: FontRef<'static>,
}
impl PixelBackend {
pub fn new(width: u32, height: u32, plot_area: Rect) -> Self {
let (w, h) = (width as usize, height as usize);
PixelBackend {
width: w,
height: h,
buf: vec![255; w * h * 4], plot_area,
total_area: Rect {
x: 0.0,
y: 0.0,
width: width as f64,
height: height as f64,
},
font: FontRef::try_from_slice(FONT).expect("bundled font parses"),
}
}
pub fn dimensions(&self) -> (u32, u32) {
(self.width as u32, self.height as u32)
}
pub fn into_rgba(self) -> Vec<u8> {
self.buf
}
pub fn into_png(self) -> Result<Vec<u8>, RenderError> {
let img = image::RgbaImage::from_raw(self.width as u32, self.height as u32, self.buf)
.ok_or_else(|| RenderError::BackendError("buffer size mismatch".into()))?;
let mut out = std::io::Cursor::new(Vec::new());
img.write_to(&mut out, image::ImageOutputFormat::Png)
.map_err(|e| RenderError::BackendError(format!("{e:?}")))?;
Ok(out.into_inner())
}
}
fn blend(buf: &mut [u8], w: usize, h: usize, x: i32, y: i32, (r, g, b): (u8, u8, u8), a: f64) {
if x < 0 || y < 0 || x as usize >= w || y as usize >= h {
return;
}
let a = a.clamp(0.0, 1.0);
if a <= 0.0 {
return;
}
let i = (y as usize * w + x as usize) * 4;
let inv = 1.0 - a;
buf[i] = (r as f64 * a + buf[i] as f64 * inv).round() as u8;
buf[i + 1] = (g as f64 * a + buf[i + 1] as f64 * inv).round() as u8;
buf[i + 2] = (b as f64 * a + buf[i + 2] as f64 * inv).round() as u8;
buf[i + 3] = 255;
}
fn fill_circle(
buf: &mut [u8],
w: usize,
h: usize,
cx: f64,
cy: f64,
r: f64,
c: (u8, u8, u8),
a: f64,
) {
let r = r.max(0.5);
let (x0, x1) = ((cx - r - 1.0).floor() as i32, (cx + r + 1.0).ceil() as i32);
let (y0, y1) = ((cy - r - 1.0).floor() as i32, (cy + r + 1.0).ceil() as i32);
for y in y0..=y1 {
for x in x0..=x1 {
let d = ((x as f64 + 0.5 - cx).powi(2) + (y as f64 + 0.5 - cy).powi(2)).sqrt();
let cov = (r - d + 0.5).clamp(0.0, 1.0); if cov > 0.0 {
blend(buf, w, h, x, y, c, cov * a);
}
}
}
}
#[allow(clippy::too_many_arguments)]
fn segment(
buf: &mut [u8],
w: usize,
h: usize,
x0: f64,
y0: f64,
x1: f64,
y1: f64,
c: (u8, u8, u8),
width: f64,
a: f64,
) {
let r = (width / 2.0).max(0.5);
let (dx, dy) = (x1 - x0, y1 - y0);
let len = (dx * dx + dy * dy).sqrt().max(1.0);
let steps = len.ceil() as i32;
for s in 0..=steps {
let t = s as f64 / steps as f64;
fill_circle(buf, w, h, x0 + dx * t, y0 + dy * t, r, c, a);
}
}
fn fill_polygon(buf: &mut [u8], w: usize, h: usize, pts: &[(f64, f64)], c: (u8, u8, u8), a: f64) {
fill_polygon_rings(buf, w, h, &[pts], c, a);
}
fn fill_polygon_rings(
buf: &mut [u8],
w: usize,
h: usize,
rings: &[&[(f64, f64)]],
c: (u8, u8, u8),
a: f64,
) {
fill_polygon_rings_with(buf, w, h, rings, a, |_, _| c);
}
fn fill_polygon_rings_pattern(
buf: &mut [u8],
w: usize,
h: usize,
rings: &[&[(f64, f64)]],
pattern: &FillPattern,
a: f64,
) {
fill_polygon_rings_with(buf, w, h, rings, a, |x, y| pattern.color_at(x, y));
}
fn fill_polygon_rings_with(
buf: &mut [u8],
w: usize,
h: usize,
rings: &[&[(f64, f64)]],
a: f64,
color_at: impl Fn(f64, f64) -> (u8, u8, u8),
) {
let (mut y0, mut y1) = (f64::INFINITY, f64::NEG_INFINITY);
for ring in rings {
if ring.len() < 3 {
continue;
}
for p in ring.iter() {
y0 = y0.min(p.1);
y1 = y1.max(p.1);
}
}
if !(y0.is_finite() && y1.is_finite()) {
return;
}
let (y0, y1) = (y0.floor() as i32, y1.ceil() as i32);
for y in y0..=y1 {
let yc = y as f64 + 0.5;
let mut xs: Vec<f64> = Vec::new();
for ring in rings {
let n = ring.len();
if n < 3 {
continue;
}
for i in 0..n {
let (ax, ay) = ring[i];
let (bx, by) = ring[(i + 1) % n];
if (ay <= yc && by > yc) || (by <= yc && ay > yc) {
xs.push(ax + (yc - ay) / (by - ay) * (bx - ax));
}
}
}
xs.sort_by(|p, q| p.total_cmp(q));
for span in xs.chunks(2) {
if let [xa, xb] = span {
for x in xa.round() as i32..=xb.round() as i32 {
blend(buf, w, h, x, y, color_at(x as f64, yc), a);
}
}
}
}
}
impl DrawBackend for PixelBackend {
fn plot_area(&self) -> Rect {
self.plot_area.clone()
}
fn total_area(&self) -> Rect {
self.total_area.clone()
}
fn draw_circle(
&mut self,
(cx, cy): (f64, f64),
radius: f64,
style: &PointStyle,
) -> Result<(), RenderError> {
fill_circle(
&mut self.buf,
self.width,
self.height,
cx,
cy,
radius,
style.color,
style.alpha,
);
Ok(())
}
fn draw_line(&mut self, pts: &[(f64, f64)], style: &LineStyle) -> Result<(), RenderError> {
for w in pts.windows(2) {
segment(
&mut self.buf,
self.width,
self.height,
w[0].0,
w[0].1,
w[1].0,
w[1].1,
style.color,
style.width,
style.alpha,
);
}
Ok(())
}
fn draw_rect(
&mut self,
(x0, y0): (f64, f64),
(x1, y1): (f64, f64),
style: &RectStyle,
) -> Result<(), RenderError> {
let r = if style.corner_radius > 0.0 {
super::backend::rounded_rect_points(x0, y0, x1, y1, style.corner_radius)
} else {
vec![(x0, y0), (x1, y0), (x1, y1), (x0, y1)]
};
if let Some(fill) = style.fill {
fill_polygon(
&mut self.buf,
self.width,
self.height,
&r,
fill,
style.alpha,
);
}
if let Some(stroke) = style.stroke {
let n = r.len();
for i in 0..n {
let (a, b) = (r[i], r[(i + 1) % n]);
segment(
&mut self.buf,
self.width,
self.height,
a.0,
a.1,
b.0,
b.1,
stroke,
style.stroke_width,
style.alpha,
);
}
}
Ok(())
}
fn draw_polygon(&mut self, pts: &[(f64, f64)], style: &RectStyle) -> Result<(), RenderError> {
if let Some(fill) = style.fill {
fill_polygon(
&mut self.buf,
self.width,
self.height,
pts,
fill,
style.alpha,
);
}
if let Some(stroke) = style.stroke {
for w in pts.windows(2) {
segment(
&mut self.buf,
self.width,
self.height,
w[0].0,
w[0].1,
w[1].0,
w[1].1,
stroke,
style.stroke_width,
style.alpha,
);
}
if let (Some(first), Some(last)) = (pts.first(), pts.last()) {
segment(
&mut self.buf,
self.width,
self.height,
last.0,
last.1,
first.0,
first.1,
stroke,
style.stroke_width,
style.alpha,
);
}
}
Ok(())
}
fn draw_polygon_rings(
&mut self,
rings: &[Vec<(f64, f64)>],
style: &RectStyle,
) -> Result<(), RenderError> {
if let Some(fill) = style.fill {
let ring_refs: Vec<&[(f64, f64)]> = rings.iter().map(Vec::as_slice).collect();
fill_polygon_rings(
&mut self.buf,
self.width,
self.height,
&ring_refs,
fill,
style.alpha,
);
}
if let Some(stroke) = style.stroke {
for ring in rings {
if ring.len() < 2 {
continue;
}
for w in ring.windows(2) {
segment(
&mut self.buf,
self.width,
self.height,
w[0].0,
w[0].1,
w[1].0,
w[1].1,
stroke,
style.stroke_width,
style.alpha,
);
}
if let (Some(first), Some(last)) = (ring.first(), ring.last()) {
segment(
&mut self.buf,
self.width,
self.height,
last.0,
last.1,
first.0,
first.1,
stroke,
style.stroke_width,
style.alpha,
);
}
}
}
Ok(())
}
fn draw_rect_pattern(
&mut self,
(x0, y0): (f64, f64),
(x1, y1): (f64, f64),
pattern: &FillPattern,
style: &RectStyle,
) -> Result<(), RenderError> {
let r = if style.corner_radius > 0.0 {
super::backend::rounded_rect_points(x0, y0, x1, y1, style.corner_radius)
} else {
vec![(x0, y0), (x1, y0), (x1, y1), (x0, y1)]
};
fill_polygon_rings_pattern(
&mut self.buf,
self.width,
self.height,
&[&r],
pattern,
style.alpha,
);
if let Some(stroke) = style.stroke {
let n = r.len();
for i in 0..n {
let (a, b) = (r[i], r[(i + 1) % n]);
segment(
&mut self.buf,
self.width,
self.height,
a.0,
a.1,
b.0,
b.1,
stroke,
style.stroke_width,
style.alpha,
);
}
}
Ok(())
}
fn draw_polygon_pattern(
&mut self,
pts: &[(f64, f64)],
pattern: &FillPattern,
style: &RectStyle,
) -> Result<(), RenderError> {
fill_polygon_rings_pattern(
&mut self.buf,
self.width,
self.height,
&[pts],
pattern,
style.alpha,
);
if let Some(stroke) = style.stroke {
for w in pts.windows(2) {
segment(
&mut self.buf,
self.width,
self.height,
w[0].0,
w[0].1,
w[1].0,
w[1].1,
stroke,
style.stroke_width,
style.alpha,
);
}
if let (Some(first), Some(last)) = (pts.first(), pts.last()) {
segment(
&mut self.buf,
self.width,
self.height,
last.0,
last.1,
first.0,
first.1,
stroke,
style.stroke_width,
style.alpha,
);
}
}
Ok(())
}
fn draw_polygon_rings_pattern(
&mut self,
rings: &[Vec<(f64, f64)>],
pattern: &FillPattern,
style: &RectStyle,
) -> Result<(), RenderError> {
let ring_refs: Vec<&[(f64, f64)]> = rings.iter().map(Vec::as_slice).collect();
fill_polygon_rings_pattern(
&mut self.buf,
self.width,
self.height,
&ring_refs,
pattern,
style.alpha,
);
if let Some(stroke) = style.stroke {
for ring in rings {
if ring.len() < 2 {
continue;
}
for w in ring.windows(2) {
segment(
&mut self.buf,
self.width,
self.height,
w[0].0,
w[0].1,
w[1].0,
w[1].1,
stroke,
style.stroke_width,
style.alpha,
);
}
if let (Some(first), Some(last)) = (ring.first(), ring.last()) {
segment(
&mut self.buf,
self.width,
self.height,
last.0,
last.1,
first.0,
first.1,
stroke,
style.stroke_width,
style.alpha,
);
}
}
}
Ok(())
}
fn draw_text(
&mut self,
text: &str,
(x, y): (f64, f64),
style: &TextStyle,
) -> Result<(), RenderError> {
let scale = PxScale::from(style.size as f32);
let font = &self.font;
let scaled = font.as_scaled(scale);
let width: f32 = text
.chars()
.map(|c| scaled.h_advance(font.glyph_id(c)))
.sum();
let start_x = match style.anchor {
TextAnchor::Start => x,
TextAnchor::Middle => x - width as f64 / 2.0,
TextAnchor::End => x - width as f64,
};
let baseline = (y + style.size * 0.35) as f32;
let bold = style.face == FontFace::Bold;
let (bw, bh, color) = (self.width, self.height, style.color);
let mut pen = start_x as f32;
for ch in text.chars() {
let gid = font.glyph_id(ch);
let glyph = gid.with_scale_and_position(scale, point(pen, baseline));
if let Some(outlined) = font.outline_glyph(glyph) {
let bounds = outlined.px_bounds();
outlined.draw(|gx, gy, cov| {
let a = if bold {
(cov as f64 * 1.4).min(1.0)
} else {
cov as f64
};
blend(
&mut self.buf,
bw,
bh,
bounds.min.x as i32 + gx as i32,
bounds.min.y as i32 + gy as i32,
color,
a,
);
});
}
pen += scaled.h_advance(gid);
}
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::*;
fn pixel(buf: &[u8], w: usize, x: i32, y: i32) -> (u8, u8, u8) {
let i = (y as usize * w + x as usize) * 4;
(buf[i], buf[i + 1], buf[i + 2])
}
#[test]
fn fill_polygon_rings_cuts_out_a_hole() {
let (w, h) = (20usize, 20usize);
let mut buf = vec![255u8; w * h * 4];
let exterior: Vec<(f64, f64)> = vec![(1.0, 1.0), (19.0, 1.0), (19.0, 19.0), (1.0, 19.0)];
let hole: Vec<(f64, f64)> = vec![(7.0, 7.0), (13.0, 7.0), (13.0, 13.0), (7.0, 13.0)];
fill_polygon_rings(&mut buf, w, h, &[&exterior, &hole], (200, 50, 50), 1.0);
assert_eq!(pixel(&buf, w, 10, 10), (255, 255, 255), "hole stays blank");
assert_eq!(
pixel(&buf, w, 3, 3),
(200, 50, 50),
"ring between is filled"
);
let mut filled = vec![255u8; w * h * 4];
fill_polygon_rings(&mut filled, w, h, &[&exterior], (200, 50, 50), 1.0);
assert_eq!(pixel(&filled, w, 10, 10), (200, 50, 50));
}
#[test]
fn fill_polygon_rings_handles_disjoint_rings() {
let (w, h) = (20usize, 20usize);
let mut buf = vec![255u8; w * h * 4];
let a: Vec<(f64, f64)> = vec![(1.0, 1.0), (5.0, 1.0), (5.0, 5.0), (1.0, 5.0)];
let b: Vec<(f64, f64)> = vec![(14.0, 14.0), (18.0, 14.0), (18.0, 18.0), (14.0, 18.0)];
fill_polygon_rings(&mut buf, w, h, &[&a, &b], (10, 20, 30), 1.0);
assert_eq!(pixel(&buf, w, 3, 3), (10, 20, 30));
assert_eq!(pixel(&buf, w, 16, 16), (10, 20, 30));
assert_eq!(
pixel(&buf, w, 10, 10),
(255, 255, 255),
"gap between stays blank"
);
}
#[test]
fn draw_polygon_rings_trait_method_fills_and_strokes_every_ring() {
let pa = Rect {
x: 0.0,
y: 0.0,
width: 20.0,
height: 20.0,
};
let mut backend = PixelBackend::new(20, 20, pa);
let dyn_backend: &mut dyn DrawBackend = &mut backend;
let exterior = vec![(1.0, 1.0), (19.0, 1.0), (19.0, 19.0), (1.0, 19.0)];
let hole = vec![(7.0, 7.0), (13.0, 7.0), (13.0, 13.0), (7.0, 13.0)];
dyn_backend
.draw_polygon_rings(
&[exterior, hole],
&RectStyle {
fill: Some((200, 50, 50)),
stroke: Some((0, 0, 0)),
stroke_width: 1.0,
alpha: 1.0,
clip: true,
corner_radius: 0.0,
},
)
.unwrap();
let buf = backend.into_rgba();
assert_eq!(pixel(&buf, 20, 10, 10), (255, 255, 255), "hole stays blank");
assert_eq!(
pixel(&buf, 20, 3, 3),
(200, 50, 50),
"ring between is filled"
);
let near_hole_edge = pixel(&buf, 20, 7, 10);
assert_ne!(near_hole_edge, (255, 255, 255), "hole outline is stroked");
}
#[test]
fn draw_polygon_rings_skips_a_degenerate_ring_when_stroking() {
let pa = Rect {
x: 0.0,
y: 0.0,
width: 20.0,
height: 20.0,
};
let mut backend = PixelBackend::new(20, 20, pa);
let dyn_backend: &mut dyn DrawBackend = &mut backend;
let degenerate: Vec<(f64, f64)> = vec![(5.0, 5.0)];
let real = vec![(1.0, 1.0), (10.0, 1.0), (10.0, 10.0), (1.0, 10.0)];
dyn_backend
.draw_polygon_rings(
&[degenerate, real],
&RectStyle {
fill: None,
stroke: Some((0, 0, 0)),
stroke_width: 1.0,
alpha: 1.0,
clip: true,
corner_radius: 0.0,
},
)
.unwrap();
let buf = backend.into_rgba();
assert_ne!(pixel(&buf, 20, 5, 1), (255, 255, 255));
}
fn stripe() -> FillPattern {
FillPattern::Stripe {
fg: (10, 20, 30),
bg: (200, 210, 220),
size_px: 4.0,
angle_deg: 0.0, }
}
#[test]
fn a_pattern_fill_rasterises_both_colours_not_a_flat_one() {
let pa = Rect {
x: 0.0,
y: 0.0,
width: 20.0,
height: 20.0,
};
let mut backend = PixelBackend::new(20, 20, pa);
let dyn_backend: &mut dyn DrawBackend = &mut backend;
let square = vec![(0.0, 0.0), (20.0, 0.0), (20.0, 20.0), (0.0, 20.0)];
dyn_backend
.draw_polygon_pattern(&square, &stripe(), &RectStyle::default())
.unwrap();
let buf = backend.into_rgba();
let pixels: std::collections::HashSet<(u8, u8, u8)> =
(0..20).map(|x| pixel(&buf, 20, x, 10)).collect();
assert!(pixels.contains(&(10, 20, 30)), "{pixels:?}");
assert!(pixels.contains(&(200, 210, 220)), "{pixels:?}");
}
#[test]
fn pattern_fill_still_cuts_out_a_hole() {
let (w, h) = (20usize, 20usize);
let mut buf = vec![255u8; w * h * 4];
let exterior: Vec<(f64, f64)> = vec![(1.0, 1.0), (19.0, 1.0), (19.0, 19.0), (1.0, 19.0)];
let hole: Vec<(f64, f64)> = vec![(7.0, 7.0), (13.0, 7.0), (13.0, 13.0), (7.0, 13.0)];
fill_polygon_rings_pattern(&mut buf, w, h, &[&exterior, &hole], &stripe(), 1.0);
assert_eq!(pixel(&buf, w, 10, 10), (255, 255, 255), "hole stays blank");
}
#[test]
fn draw_rect_pattern_also_strokes_normally() {
let pa = Rect {
x: 0.0,
y: 0.0,
width: 20.0,
height: 20.0,
};
let mut backend = PixelBackend::new(20, 20, pa);
let dyn_backend: &mut dyn DrawBackend = &mut backend;
dyn_backend
.draw_rect_pattern(
(2.0, 2.0),
(18.0, 18.0),
&stripe(),
&RectStyle {
fill: None,
stroke: Some((0, 0, 0)),
stroke_width: 1.0,
alpha: 1.0,
clip: true,
corner_radius: 0.0,
},
)
.unwrap();
let buf = backend.into_rgba();
assert_ne!(pixel(&buf, 20, 2, 10), (255, 255, 255), "left edge stroked");
}
#[test]
fn draw_rect_pattern_still_rounds_its_corners() {
let pa = Rect {
x: 0.0,
y: 0.0,
width: 20.0,
height: 20.0,
};
let mut backend = PixelBackend::new(20, 20, pa);
let dyn_backend: &mut dyn DrawBackend = &mut backend;
dyn_backend
.draw_rect_pattern(
(0.0, 0.0),
(20.0, 20.0),
&stripe(),
&RectStyle {
fill: None,
stroke: None,
stroke_width: 0.0,
alpha: 1.0,
clip: true,
corner_radius: 8.0,
},
)
.unwrap();
let buf = backend.into_rgba();
assert_eq!(
pixel(&buf, 20, 0, 0),
(255, 255, 255),
"corner rounded away"
);
assert_ne!(
pixel(&buf, 20, 10, 10),
(255, 255, 255),
"centre still filled"
);
}
}