Files
ghostty-rs/crates/ghostty-font/src/sprite/canvas.rs
T
ddidderr 64aa8b6793 feat: visual parity with the original Ghostty (pixel-diff pipeline)
Adds scripts/visual-parity.sh (`just visual-parity`): launches the
installed Ghostty 1.3.1 and the Rust build in an isolated headless
nested weston (GL renderer, Mesa forced, weston-screenshooter via
--debug), with identical isolated config, a deterministic test page
(ASCII, ligatures, all SGR styles and underline kinds, 16/256/truecolor,
box/block/braille/powerline/nerd icons, CJK, emoji, wide chars,
cursor) and a tabs+splits scenario driven through Xwayland, then diffs
the captures (terminal area and chrome separately, heatmaps).

Fixes found by comparing against the original:
- A pane stayed blank after a nested split: the GTK render path called
  draw_frame(false) where Ghostty uses drawFrame(true) (a re-realized
  GLArea has a new swap chain that nothing else redraws).
- Sprite glyph antialiasing now ports z2d's default 4x4 multisample
  rasterizer instead of tiny-skia's analytic coverage (pixels differing
  from Ghostty's reference sprite sheets: 5.76% -> 1.20%, none off by
  more than 96 levels); the sheet test tolerance is tightened to 0.1%.

Result vs Ghostty 1.3.1: test page 0.020% of pixels differ (196 px at
curly-underline ends and diagonal box strokes, max delta 86 levels);
tabs+splits and headerbar/tab bar are bit-identical. Details in
docs/rewrite/notes/visual-parity-VP.md.

Test Plan:
- just visual-parity (needs weston, weston-screenshooter, xdotool)
- cargo test -p ghostty-font (sprite sheet comparison)
2026-10-12 13:52:22 +02:00

1033 lines
30 KiB
Rust

//! This exposes primitives to draw 2D graphics and export the graphic to
//! a font atlas. Port of `font/sprite/canvas.zig`.
//!
//! Ghostty uses the `z2d` library for antialiased path drawing. This port
//! reproduces z2d's default `multisample_4x` rasterizer (4x4 samples per
//! pixel, scanline spans, alpha `coverage * 16 - 1`) in [`Canvas::rasterize`];
//! stroke outlines come from `tiny-skia`'s stroker.
use std::ops::Sub;
use tiny_skia::{LineCap, LineJoin, PathBuilder, PathSegment, Stroke};
use crate::atlas::{self, Atlas, Format, Region};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub struct Point<T> {
pub x: T,
pub y: T,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub struct Line<T> {
pub p0: Point<T>,
pub p1: Point<T>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub struct Box<T> {
pub p0: Point<T>,
pub p1: Point<T>,
}
impl<T: Copy + PartialOrd + Sub<Output = T>> Box<T> {
pub fn rect(&self) -> Rect<T> {
let min = |a: T, b: T| if b < a { b } else { a };
let max = |a: T, b: T| if b > a { b } else { a };
let tl_x = min(self.p0.x, self.p1.x);
let tl_y = min(self.p0.y, self.p1.y);
let br_x = max(self.p0.x, self.p1.x);
let br_y = max(self.p0.y, self.p1.y);
Rect {
x: tl_x,
y: tl_y,
width: br_x - tl_x,
height: br_y - tl_y,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub struct Rect<T> {
pub x: T,
pub y: T,
pub width: T,
pub height: T,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub struct Triangle<T> {
pub p0: Point<T>,
pub p1: Point<T>,
pub p2: Point<T>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub struct Quad<T> {
pub p0: Point<T>,
pub p1: Point<T>,
pub p2: Point<T>,
pub p3: Point<T>,
}
/// We only use alpha-channel so a pixel is an 8-bit alpha value. Zig models
/// this as a non-exhaustive `enum(u8)`; `ON` = 255, `OFF` = 0, and any other
/// value is a partial alpha (used for shades).
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub struct Color(pub u8);
impl Color {
pub const ON: Color = Color(255);
pub const OFF: Color = Color(0);
}
/// Errors that drawing functions can produce.
#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
pub enum DrawError {
/// Something went wrong while doing math.
#[error("math error")]
MathError,
#[error("atlas error: {0}")]
Atlas(#[from] atlas::Error),
#[error("path error")]
Path,
}
/// A node of a [`Path`].
#[derive(Debug, Clone, Copy, PartialEq)]
enum PathNode {
MoveTo(f64, f64),
LineTo(f64, f64),
CurveTo(f64, f64, f64, f64, f64, f64),
ClosePath,
}
/// A path in cell-relative coordinates (the canvas applies the padding
/// translation when the path is filled or stroked, like z2d's
/// `staticPath` transformation).
#[derive(Debug, Clone, Default)]
pub struct Path {
nodes: Vec<PathNode>,
current: Option<(f64, f64)>,
start: Option<(f64, f64)>,
}
impl Path {
pub fn new() -> Path {
Path::default()
}
pub fn move_to(&mut self, x: f64, y: f64) {
self.nodes.push(PathNode::MoveTo(x, y));
self.current = Some((x, y));
self.start = Some((x, y));
}
pub fn line_to(&mut self, x: f64, y: f64) {
if self.current.is_none() {
self.move_to(x, y);
return;
}
self.nodes.push(PathNode::LineTo(x, y));
self.current = Some((x, y));
}
pub fn curve_to(&mut self, x1: f64, y1: f64, x2: f64, y2: f64, x3: f64, y3: f64) {
if self.current.is_none() {
self.move_to(x1, y1);
}
self.nodes.push(PathNode::CurveTo(x1, y1, x2, y2, x3, y3));
self.current = Some((x3, y3));
}
/// Close the current subpath. Like z2d, the current point returns to
/// the start of the subpath.
pub fn close(&mut self) {
if self.current.is_none() {
return;
}
self.nodes.push(PathNode::ClosePath);
self.current = self.start;
}
/// Add a circular arc centered at `(x, y)` from angle `a0` to `a1`
/// (radians, clockwise in canvas space with +y down). If there is a
/// current point, a line is drawn to the start of the arc, otherwise
/// the arc starts a new subpath.
pub fn arc(&mut self, x: f64, y: f64, radius: f64, a0: f64, a1: f64) {
use std::f64::consts::FRAC_PI_2;
let (sx, sy) = (x + radius * a0.cos(), y + radius * a0.sin());
if self.current.is_some() {
self.line_to(sx, sy);
} else {
self.move_to(sx, sy);
}
let mut sweep = a1 - a0;
// Split into segments of at most a quarter turn.
let segs = ((sweep.abs() / FRAC_PI_2).ceil() as usize).max(1);
sweep /= segs as f64;
let k = 4.0 / 3.0 * (sweep / 4.0).tan();
let mut a = a0;
for _ in 0..segs {
let (s0, c0) = a.sin_cos();
let (s1, c1) = (a + sweep).sin_cos();
self.curve_to(
x + radius * (c0 - k * s0),
y + radius * (s0 + k * c0),
x + radius * (c1 + k * s1),
y + radius * (s1 - k * c1),
x + radius * c1,
y + radius * s1,
);
a += sweep;
}
}
fn to_skia(&self, dx: f64, dy: f64) -> Option<tiny_skia::Path> {
let mut pb = PathBuilder::new();
let f = |v: f64, d: f64| (v + d) as f32;
for n in &self.nodes {
match *n {
PathNode::MoveTo(x, y) => pb.move_to(f(x, dx), f(y, dy)),
PathNode::LineTo(x, y) => pb.line_to(f(x, dx), f(y, dy)),
PathNode::CurveTo(x1, y1, x2, y2, x3, y3) => pb.cubic_to(
f(x1, dx),
f(y1, dy),
f(x2, dx),
f(y2, dy),
f(x3, dx),
f(y3, dy),
),
PathNode::ClosePath => pb.close(),
}
}
pb.finish()
}
}
/// Line cap modes (subset of z2d's).
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum CapMode {
#[default]
Butt,
Round,
Square,
}
/// Stroke options (subset of z2d's `StrokeOptions`).
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct StrokeOptions {
pub line_width: f64,
pub line_cap_mode: CapMode,
}
impl Default for StrokeOptions {
fn default() -> Self {
StrokeOptions {
line_width: 2.0,
line_cap_mode: CapMode::Butt,
}
}
}
/// The canvas is an 8-bit alpha surface with padding on all sides, onto
/// which sprite glyphs are drawn.
#[derive(Debug)]
pub struct Canvas {
/// The underlying alpha8 surface, row-major, `sfc_width * sfc_height`.
pub sfc: Vec<u8>,
pub sfc_width: u32,
pub sfc_height: u32,
pub padding_x: u32,
pub padding_y: u32,
pub clip_top: u32,
pub clip_left: u32,
pub clip_right: u32,
pub clip_bottom: u32,
/// Coverage mask (alpha8, same layout as `sfc`) of the last rasterized path.
cov: Vec<u8>,
}
impl Canvas {
pub fn new(width: u32, height: u32, padding_x: u32, padding_y: u32) -> Canvas {
// We add padding to both sides (hence `2 *`)
let w = width + 2 * padding_x;
let h = height + 2 * padding_y;
Canvas {
sfc: vec![0; w as usize * h as usize],
sfc_width: w,
sfc_height: h,
padding_x,
padding_y,
clip_top: 0,
clip_left: 0,
clip_right: 0,
clip_bottom: 0,
cov: vec![0; w as usize * h as usize],
}
}
/// Write the data in this drawing to the atlas.
pub fn write_atlas(&mut self, atlas: &mut Atlas) -> Result<Region, atlas::Error> {
assert_eq!(atlas.format, Format::Grayscale);
self.trim();
let sfc_width = self.sfc_width;
let sfc_height = self.sfc_height;
// Subtract our clip margins from the
// width and height to get region size.
let region_width = sfc_width
.saturating_sub(self.clip_left)
.saturating_sub(self.clip_right);
let region_height = sfc_height
.saturating_sub(self.clip_top)
.saturating_sub(self.clip_bottom);
// Allocate our texture atlas region
let region = atlas.reserve(region_width, region_height)?;
if region.width > 0 && region.height > 0 {
// Write the glyph information into the atlas
assert_eq!(region.width, region_width);
assert_eq!(region.height, region_height);
atlas.set_from_larger(region, &self.sfc, sfc_width, self.clip_left, self.clip_top);
}
Ok(region)
}
/// Adjust clip boundaries to trim off any fully transparent rows or columns.
fn trim(&mut self) {
let width = self.sfc_width;
let height = self.sfc_height;
let buf = &self.sfc;
let px = |x: u32, y: u32| buf[y as usize * width as usize + x as usize];
while self.clip_top < height - self.clip_bottom {
let y = self.clip_top;
let x0 = self.clip_left;
let x1 = width - self.clip_right;
if (x0..x1).any(|x| px(x, y) != 0) {
break;
}
self.clip_top += 1;
}
while self.clip_bottom < height - self.clip_top {
let y = (height - self.clip_bottom).saturating_sub(1);
let x0 = self.clip_left;
let x1 = width - self.clip_right;
if (x0..x1).any(|x| px(x, y) != 0) {
break;
}
self.clip_bottom += 1;
}
while self.clip_left < width - self.clip_right {
let x = self.clip_left;
let y0 = self.clip_top;
let y1 = height - self.clip_bottom;
if (y0..y1).any(|y| px(x, y) != 0) {
break;
}
self.clip_left += 1;
}
while self.clip_right < width - self.clip_left {
let x = (width - self.clip_right).saturating_sub(1);
let y0 = self.clip_top;
let y1 = height - self.clip_bottom;
if (y0..y1).any(|y| px(x, y) != 0) {
break;
}
self.clip_right += 1;
}
}
/// Only really useful for test purposes, since the clipping region is
/// automatically excluded when writing to an atlas with `write_atlas`.
pub fn clear_clipping_regions(&mut self) {
let width = self.sfc_width as usize;
let height = self.sfc_height as usize;
for y in 0..height {
for x in 0..self.clip_left as usize {
self.sfc[y * width + x] = 0;
}
}
for y in 0..height {
for x in (width - self.clip_right as usize)..width {
self.sfc[y * width + x] = 0;
}
}
for y in 0..self.clip_top as usize {
for x in 0..width {
self.sfc[y * width + x] = 0;
}
}
for y in (height - self.clip_bottom as usize)..height {
for x in 0..width {
self.sfc[y * width + x] = 0;
}
}
}
/// Reset the canvas (pixels and clip margins) for reuse. Used by tests
/// that render many glyphs onto a single canvas.
pub fn reset(&mut self) {
self.sfc.fill(0);
self.clip_top = 0;
self.clip_left = 0;
self.clip_right = 0;
self.clip_bottom = 0;
}
/// Draw and fill a single pixel (out-of-bounds pixels are ignored).
pub fn pixel(&mut self, x: i32, y: i32, color: Color) {
let x = i64::from(x) + i64::from(self.padding_x);
let y = i64::from(y) + i64::from(self.padding_y);
if x < 0 || y < 0 || x >= i64::from(self.sfc_width) || y >= i64::from(self.sfc_height) {
return;
}
self.sfc[y as usize * self.sfc_width as usize + x as usize] = color.0;
}
/// Draw and fill a rectangle. This is the main primitive for drawing
/// lines as well (which are just generally skinny rectangles...)
pub fn rect(&mut self, v: Rect<i32>, color: Color) {
let mut y = v.y;
while y < v.y + v.height {
let mut x = v.x;
while x < v.x + v.width {
self.pixel(x, y, color);
x += 1;
}
y += 1;
}
}
/// Convenience wrapper for `Canvas::rect`
pub fn box_(&mut self, x0: i32, y0: i32, x1: i32, y1: i32, color: Color) {
self.rect(
Box {
p0: Point { x: x0, y: y0 },
p1: Point { x: x1, y: y1 },
}
.rect(),
color,
);
}
/// Draw and fill a quad.
pub fn quad(&mut self, q: Quad<f64>, color: Color) -> Result<(), DrawError> {
let mut path = Path::new();
path.move_to(q.p0.x, q.p0.y);
path.line_to(q.p1.x, q.p1.y);
path.line_to(q.p2.x, q.p2.y);
path.line_to(q.p3.x, q.p3.y);
path.close();
self.fill_path(&path, color)
}
/// Draw and fill a triangle.
pub fn triangle(&mut self, t: Triangle<f64>, color: Color) -> Result<(), DrawError> {
let mut path = Path::new();
path.move_to(t.p0.x, t.p0.y);
path.line_to(t.p1.x, t.p1.y);
path.line_to(t.p2.x, t.p2.y);
path.close();
self.fill_path(&path, color)
}
/// Stroke a line.
pub fn line(&mut self, l: Line<f64>, thickness: f64, color: Color) -> Result<(), DrawError> {
let mut path = Path::new();
path.move_to(l.p0.x, l.p0.y);
path.line_to(l.p1.x, l.p1.y);
self.stroke_path(
&path,
StrokeOptions {
line_cap_mode: CapMode::Butt,
line_width: thickness,
},
color,
)
}
/// Rasterize `path` (filled or stroked) into the coverage mask `self.cov`
/// with z2d's default multisample anti-aliasing.
fn rasterize(&mut self, path: &Path, stroke: Option<StrokeOptions>) -> bool {
self.cov.fill(0);
let Some(p) = path.to_skia(f64::from(self.padding_x), f64::from(self.padding_y)) else {
return false;
};
let p = match stroke {
None => p,
Some(opts) => {
let s = Stroke {
width: opts.line_width as f32,
miter_limit: 10.0,
line_cap: match opts.line_cap_mode {
CapMode::Butt => LineCap::Butt,
CapMode::Round => LineCap::Round,
CapMode::Square => LineCap::Square,
},
line_join: LineJoin::Miter,
dash: None,
};
match p.stroke(&s, 1.0) {
Some(o) => o,
None => return false,
}
}
};
let edges = flatten_to_edges(&p);
multisample_fill(
&edges,
self.sfc_width as usize,
self.sfc_height as usize,
&mut self.cov,
);
true
}
/// Composite the coverage mask onto the canvas using `src_over`
/// with an opaque pattern of alpha `color`.
fn composite_scratch(&mut self, color: Color) {
let a = u32::from(color.0);
for (dst, px) in self.sfc.iter_mut().zip(self.cov.iter()) {
let cov = u32::from(*px);
if cov == 0 {
continue;
}
let src = (cov * a + 127) / 255;
let d = u32::from(*dst);
*dst = (src + (d * (255 - src) + 127) / 255).min(255) as u8;
}
}
/// Stroke a path.
pub fn stroke_path(
&mut self,
path: &Path,
opts: StrokeOptions,
color: Color,
) -> Result<(), DrawError> {
if self.rasterize(path, Some(opts)) {
self.composite_scratch(color);
}
Ok(())
}
/// Do an inner stroke on a path: the shape is filled to make a mask,
/// then stroked with double width, and the two are multiplied so only
/// the part of the stroke inside the shape remains.
pub fn inner_stroke_path(
&mut self,
path: &Path,
opts: StrokeOptions,
color: Color,
) -> Result<(), DrawError> {
// Make a closed version of the path for our fill, so
// that we can support open paths for inner stroke.
let mut closed = path.clone();
closed.close();
let mut fill = vec![0u8; self.sfc.len()];
if self.rasterize(&closed, None) {
for (d, px) in fill.iter_mut().zip(self.cov.iter()) {
*d = *px;
}
}
// Stroke the shape with double the desired width.
let mut stroke = vec![0u8; self.sfc.len()];
let mut mut_opts = opts;
mut_opts.line_width *= 2.0;
if self.rasterize(path, Some(mut_opts)) {
let a = u32::from(color.0);
for (d, px) in stroke.iter_mut().zip(self.cov.iter()) {
*d = ((u32::from(*px) * a + 127) / 255) as u8;
}
}
// Multiply the stroke onto the fill, then composite over the canvas.
for ((dst, f), s) in self.sfc.iter_mut().zip(&fill).zip(&stroke) {
let m = (255.0 * (f64::from(*s) / 255.0) * (f64::from(*f) / 255.0)).round() as u32;
let d = u32::from(*dst);
*dst = (m + (d * (255 - m) + 127) / 255).min(255) as u8;
}
Ok(())
}
/// Fill a path (non-zero winding).
pub fn fill_path(&mut self, path: &Path, color: Color) -> Result<(), DrawError> {
if self.rasterize(path, None) {
self.composite_scratch(color);
}
Ok(())
}
/// Invert all pixels on the canvas.
pub fn invert(&mut self) {
for v in &mut self.sfc {
*v = 255 - *v;
}
}
/// Mirror the canvas horizontally.
pub fn flip_horizontal(&mut self) {
let width = self.sfc_width as usize;
for row in self.sfc.chunks_exact_mut(width) {
row.reverse();
}
std::mem::swap(&mut self.clip_left, &mut self.clip_right);
}
/// Mirror the canvas vertically.
pub fn flip_vertical(&mut self) {
let width = self.sfc_width as usize;
let height = self.sfc_height as usize;
for y in 0..height / 2 {
let (top, bottom) = self.sfc.split_at_mut((height - y - 1) * width);
top[y * width..(y + 1) * width].swap_with_slice(&mut bottom[..width]);
}
std::mem::swap(&mut self.clip_top, &mut self.clip_bottom);
}
}
// ---------------------------------------------------------------------------
// z2d multisample rasterizer port (`z2d/src/internal/raster/multisample.zig`,
// `tess/Polygon.zig`, `tess/Spline.zig`).
const MS_SCALE: i32 = 4;
#[derive(Clone, Copy)]
struct Edge {
y0: f64,
y1: f64,
x_start: f64,
x_inc: f64,
}
impl Edge {
fn top(&self) -> f64 {
if self.y0 < self.y1 { self.y0 } else { self.y1 }
}
fn bottom(&self) -> f64 {
if self.y0 < self.y1 { self.y1 } else { self.y0 }
}
fn dir(&self) -> i32 {
if self.y0 < self.y1 { -1 } else { 1 }
}
}
fn add_edge(edges: &mut Vec<Edge>, p0: (f64, f64), p1: (f64, f64)) {
let s = f64::from(MS_SCALE);
let (x0, y0, x1, y1) = (p0.0 * s, p0.1 * s, p1.0 * s, p1.1 * s);
if y0 < y1 {
edges.push(Edge {
y0,
y1,
x_start: x0,
x_inc: (x1 - x0) / (y1 - y0),
});
} else if y0 > y1 {
edges.push(Edge {
y0,
y1,
x_start: x1,
x_inc: (x0 - x1) / (y0 - y1),
});
}
}
fn lerp_half(a: (f64, f64), b: (f64, f64)) -> (f64, f64) {
(a.0 + (b.0 - a.0) / 2.0, a.1 + (b.1 - a.1) / 2.0)
}
type Knots = [(f64, f64); 4];
#[allow(clippy::float_cmp)] // exact comparison, like z2d
fn knots_error_sq(k: &Knots) -> f64 {
let [a, b, c, d] = *k;
let (mut bx, mut by) = (b.0 - a.0, b.1 - a.1);
let (mut cx, mut cy) = (c.0 - a.0, c.1 - a.1);
if a.0 != d.0 || a.1 != d.1 {
let (dx, dy) = (d.0 - a.0, d.1 - a.1);
let dd = dx * dx + dy * dy;
let bd = bx * dx + by * dy;
if bd >= dd {
bx -= dx;
by -= dy;
} else {
bx -= bd / dd * dx;
by -= bd / dd * dy;
}
let cd = cx * dx + cy * dy;
if cd >= dd {
cx -= dx;
cy -= dy;
} else {
cx -= cd / dd * dx;
cy -= cd / dd * dy;
}
}
(bx * bx + by * by).max(cx * cx + cy * cy)
}
fn de_casteljau(k: &mut Knots) -> Knots {
let [a, b, c, d] = *k;
let ab = lerp_half(a, b);
let bc = lerp_half(b, c);
let cd = lerp_half(c, d);
let abbc = lerp_half(ab, bc);
let bccd = lerp_half(bc, cd);
let fin = lerp_half(abbc, bccd);
*k = [a, ab, abbc, fin];
[fin, bccd, cd, d]
}
fn decompose_into(k: &mut Knots, start: (f64, f64), tol_sq: f64, out: &mut Vec<(f64, f64)>) {
if knots_error_sq(k) < tol_sq {
if k[0] != start {
out.push(k[0]);
}
return;
}
let mut s2 = de_casteljau(k);
decompose_into(k, start, tol_sq, out);
decompose_into(&mut s2, start, tol_sq, out);
}
/// Flatten a cubic (z2d spline, tolerance 0.1), pushing every point after `a`.
fn flatten_cubic(
a: (f64, f64),
b: (f64, f64),
c: (f64, f64),
d: (f64, f64),
out: &mut Vec<(f64, f64)>,
) {
if a == b && c == d {
out.push(d);
return;
}
let mut k: Knots = [a, b, c, d];
decompose_into(&mut k, a, 0.1 * 0.1, out);
out.push(d);
}
/// Flatten a tiny-skia path into scaled polygon edges. Every subpath is
/// implicitly closed (fill semantics).
fn flatten_to_edges(p: &tiny_skia::Path) -> Vec<Edge> {
let mut edges = Vec::new();
let mut pts: Vec<(f64, f64)> = Vec::new();
let flush = |pts: &mut Vec<(f64, f64)>, edges: &mut Vec<Edge>| {
if pts.len() > 1 {
for w in pts.windows(2) {
add_edge(edges, w[0], w[1]);
}
add_edge(edges, pts[pts.len() - 1], pts[0]);
}
pts.clear();
};
let f = |p: tiny_skia::Point| (f64::from(p.x), f64::from(p.y));
let mut cur = (0.0, 0.0);
for seg in p.segments() {
match seg {
PathSegment::MoveTo(q) => {
flush(&mut pts, &mut edges);
cur = f(q);
pts.push(cur);
}
PathSegment::LineTo(q) => {
cur = f(q);
pts.push(cur);
}
PathSegment::QuadTo(c, q) => {
let (c, q) = (f(c), f(q));
// Elevate to a cubic.
let c1 = (
cur.0 + 2.0 / 3.0 * (c.0 - cur.0),
cur.1 + 2.0 / 3.0 * (c.1 - cur.1),
);
let c2 = (q.0 + 2.0 / 3.0 * (c.0 - q.0), q.1 + 2.0 / 3.0 * (c.1 - q.1));
flatten_cubic(cur, c1, c2, q, &mut pts);
cur = q;
}
PathSegment::CubicTo(c1, c2, q) => {
let q = f(q);
flatten_cubic(cur, f(c1), f(c2), q, &mut pts);
cur = q;
}
PathSegment::Close => {
flush(&mut pts, &mut edges);
}
}
}
flush(&mut pts, &mut edges);
edges
}
/// Port of `multisample.run` with a non-zero fill rule and an opaque source:
/// writes the per-pixel alpha mask (`coverage * 16 - 1`, full = 255).
fn multisample_fill(edges: &[Edge], width: usize, height: usize, cov: &mut [u8]) {
if edges.is_empty() || width == 0 || height == 0 {
return;
}
let scale = MS_SCALE as usize;
let full = (scale * scale) as u32;
let mut counts = vec![0u32; width];
let mut xs: Vec<(i32, i32)> = Vec::new();
let max_x = (width * scale) as i32;
for y in 0..height {
counts.fill(0);
let mut any = false;
for sub in 0..scale {
let ys = (y * scale + sub) as i32;
let mid = f64::from(ys) + 0.5;
xs.clear();
for e in edges {
if e.top() < mid && e.bottom() >= mid {
let x = (e.x_start + e.x_inc * (mid - e.top())).round() as i32;
xs.push((x, e.dir()));
}
}
if xs.is_empty() {
continue;
}
// Stable sort by x like pdqsort on x (ties are order-insensitive
// for the winding filter outcome in practice).
xs.sort_by_key(|v| v.0);
// Non-zero filter.
let mut winding = 0;
let mut filtered: Vec<i32> = Vec::with_capacity(xs.len());
for &(x, d) in &xs {
if winding == 0 {
winding += d;
filtered.push(x);
} else {
winding += d;
if winding == 0 {
filtered.push(x);
}
}
}
let mut x_min = 0;
for pair in filtered.as_chunks::<2>().0 {
let start = pair[0].max(x_min);
if start >= max_x {
break;
}
let end = pair[1].clamp(start, max_x);
if end > start {
any = true;
// Count samples per pixel.
let (mut sx, ex) = (start, end);
while sx < ex {
let px = (sx / MS_SCALE) as usize;
let next = ((px as i32 + 1) * MS_SCALE).min(ex);
counts[px] += (next - sx) as u32;
sx = next;
}
}
x_min = end;
}
}
if any {
let row = &mut cov[y * width..(y + 1) * width];
for (o, &c) in row.iter_mut().zip(&counts) {
let c = c.min(full);
*o = match c {
0 => 0,
c if c == full => 255,
c => (c * (256 / full) - 1).min(255) as u8,
};
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
fn at(c: &Canvas, x: u32, y: u32) -> u8 {
c.sfc[y as usize * c.sfc_width as usize + x as usize]
}
#[test]
fn box_rect_normalizes() {
let b = Box {
p0: Point { x: 5, y: 9 },
p1: Point { x: 2, y: 3 },
};
assert_eq!(
Rect {
x: 2,
y: 3,
width: 3,
height: 6
},
b.rect()
);
}
#[test]
fn rect_and_padding() {
let mut c = Canvas::new(4, 4, 1, 1);
assert_eq!((6, 6), (c.sfc_width, c.sfc_height));
c.rect(
Rect {
x: 0,
y: 0,
width: 2,
height: 1,
},
Color::ON,
);
assert_eq!(255, at(&c, 1, 1));
assert_eq!(255, at(&c, 2, 1));
assert_eq!(0, at(&c, 3, 1));
assert_eq!(0, at(&c, 1, 2));
// out of bounds is ignored
c.rect(
Rect {
x: -10,
y: -10,
width: 100,
height: 100,
},
Color::OFF,
);
assert!(c.sfc.iter().all(|v| *v == 0));
}
#[test]
fn trim_and_write_atlas() {
let mut c = Canvas::new(8, 8, 2, 2);
c.rect(
Rect {
x: 1,
y: 2,
width: 3,
height: 2,
},
Color::ON,
);
let mut atlas = Atlas::new(32, Format::Grayscale);
let r = c.write_atlas(&mut atlas).unwrap();
assert_eq!((3, 2), (r.width, r.height));
assert_eq!(2 + 1, c.clip_left);
assert_eq!(2 + 2, c.clip_top);
assert_eq!(atlas.data[(r.y * 32 + r.x) as usize], 255);
}
#[test]
fn empty_canvas_writes_empty_region() {
let mut c = Canvas::new(8, 8, 2, 2);
let mut atlas = Atlas::new(32, Format::Grayscale);
let r = c.write_atlas(&mut atlas).unwrap();
assert_eq!((0, 0), (r.width, r.height));
}
#[test]
fn flips() {
let mut c = Canvas::new(4, 2, 0, 0);
c.pixel(0, 0, Color::ON);
c.flip_horizontal();
assert_eq!(255, at(&c, 3, 0));
c.flip_vertical();
assert_eq!(255, at(&c, 3, 1));
}
#[test]
fn invert_all() {
let mut c = Canvas::new(2, 2, 0, 0);
c.pixel(0, 0, Color::ON);
c.invert();
assert_eq!(0, at(&c, 0, 0));
assert_eq!(255, at(&c, 1, 1));
}
#[test]
fn fill_triangle_covers_interior() {
let mut c = Canvas::new(20, 20, 5, 5);
c.triangle(
Triangle {
p0: Point { x: 0.0, y: 0.0 },
p1: Point { x: 20.0, y: 0.0 },
p2: Point { x: 0.0, y: 20.0 },
},
Color::ON,
)
.unwrap();
assert_eq!(255, at(&c, 5 + 2, 5 + 2));
assert_eq!(0, at(&c, 5 + 18, 5 + 18));
}
#[test]
fn stroke_line_thickness() {
let mut c = Canvas::new(20, 20, 5, 5);
c.line(
Line {
p0: Point { x: 0.0, y: 10.0 },
p1: Point { x: 20.0, y: 10.0 },
},
4.0,
Color::ON,
)
.unwrap();
// rows 8..12 inclusive of padding offset
assert_eq!(255, at(&c, 15, 5 + 8));
assert_eq!(255, at(&c, 15, 5 + 11));
assert_eq!(0, at(&c, 15, 5 + 7));
assert_eq!(0, at(&c, 15, 5 + 12));
}
#[test]
fn arc_full_circle_fills() {
let mut c = Canvas::new(20, 20, 0, 0);
let mut p = Path::new();
p.arc(10.0, 10.0, 8.0, 0.0, std::f64::consts::TAU);
p.close();
c.fill_path(&p, Color::ON).unwrap();
assert_eq!(255, at(&c, 10, 10));
assert_eq!(0, at(&c, 0, 0));
}
#[test]
fn inner_stroke_stays_inside() {
let mut c = Canvas::new(20, 20, 0, 0);
let mut p = Path::new();
p.move_to(2.0, 2.0);
p.line_to(18.0, 2.0);
p.line_to(18.0, 18.0);
p.line_to(2.0, 18.0);
p.close();
c.inner_stroke_path(
&p,
StrokeOptions {
line_width: 2.0,
..Default::default()
},
Color::ON,
)
.unwrap();
assert_eq!(0, at(&c, 1, 10)); // outside
assert_eq!(255, at(&c, 3, 10)); // inside stroke
assert_eq!(0, at(&c, 10, 10)); // interior
}
}