use kui_core::{Color, Core, NodeSpec, Path, Quad, QuadKind, Size, Vec2};
fn coverage(core: &Core, q: &Quad, x: f32, y: f32) -> Option<f32> {
if x < q.rect.x || y < q.rect.y || x >= q.rect.x + q.rect.w || y >= q.rect.y + q.rect.h {
return None;
}
let tx = q.uv[0] + (x - q.rect.x) as u32;
let ty = q.uv[1] + (y - q.rect.y) as u32;
let at = ((ty * core.atlas.size + tx) * 4 + 3) as usize;
Some(f32::from(core.atlas.pixels[at]) / 255.0 * q.color.a)
}
fn masks(build: impl FnOnce(&mut kui_core::Ui<'_>), scale: f32) -> (Core, Vec<Quad>) {
let mut core = Core::new();
let mut ui = core.frame(Size::new(200.0, 100.0), scale);
build(&mut ui);
ui.finish();
let quads: Vec<Quad> = core
.output()
.0
.quads
.iter()
.filter(|q| q.kind == QuadKind::GlyphMask)
.copied()
.collect();
(core, quads)
}
#[test]
fn a_square_is_opaque_inside_half_on_its_edge_and_nothing_past_it() {
let (core, q) = masks(
|ui| {
ui.path(
&Path::parse("M20 20 H60 V60 H20 Z").unwrap(),
NodeSpec::column().bg(Color::rgba8(0, 0, 0, 255)),
);
},
1.0,
);
assert_eq!(q.len(), 1);
let q = &q[0];
let cov = |x: f32, y: f32| coverage(&core, q, x, y).unwrap_or(0.0);
assert!(cov(40.5, 40.5) > 0.99, "opaque inside: {}", cov(40.5, 40.5));
assert!(cov(20.5, 40.5) > 0.99, "solid on the first pixel in");
let bleed = cov(19.5, 40.5);
assert!((0.3..0.7).contains(&bleed), "half on the bleed: {bleed}");
assert!(cov(18.5, 40.5) < 0.01, "nothing two pixels out");
assert!(cov(q.rect.x + 0.5, 40.5) < 0.01);
assert!(cov(q.rect.x + q.rect.w - 0.5, 40.5) < 0.01);
assert!(cov(40.5, q.rect.y + 0.5) < 0.01);
assert!(cov(40.5, q.rect.y + q.rect.h - 0.5) < 0.01);
}
#[test]
fn two_wedges_sharing_an_edge_leave_no_seam() {
let (core, q) = masks(
|ui| {
ui.path(
&Path::sector(100.0, 50.0, 40.0, 0.0, 0.0, 0.25),
NodeSpec::column().bg(Color::rgba8(255, 0, 0, 255)),
);
ui.path(
&Path::sector(100.0, 50.0, 40.0, 0.0, 0.25, 0.25),
NodeSpec::column().bg(Color::rgba8(0, 0, 255, 255)),
);
},
1.0,
);
assert_eq!(q.len(), 2);
for y in [60.5, 70.5, 80.5] {
for x in [99.5, 100.5] {
let a = coverage(&core, &q[0], x, y).unwrap_or(0.0);
let b = coverage(&core, &q[1], x, y).unwrap_or(0.0);
let over = a + b * (1.0 - a);
assert!(over > 0.99, "at ({x}, {y}): {a} under {b} leaves {over}");
}
}
let a = (0.125f32) * std::f32::consts::TAU;
let inside = coverage(&core, &q[0], 100.0 + 37.0 * a.cos(), 50.0 + 37.0 * a.sin());
let past = coverage(&core, &q[0], 100.0 + 43.0 * a.cos(), 50.0 + 43.0 * a.sin());
assert!(inside.unwrap_or(0.0) > 0.95, "{inside:?}");
assert!(past.unwrap_or(0.0) < 0.05, "{past:?}");
}
#[test]
fn the_mask_is_rasterized_at_the_physical_scale() {
let (core, q) = masks(
|ui| {
ui.path(
&Path::parse("M20 20 H60 V60 H20 Z").unwrap(),
NodeSpec::column().bg(Color::rgba8(0, 0, 0, 255)),
);
},
2.0,
);
let q = &q[0];
let cov = |x: f32, y: f32| coverage(&core, q, x, y).unwrap_or(0.0);
assert!(cov(80.5, 80.5) > 0.99);
assert!(
cov(40.5, 80.5) > 0.99,
"solid on the first physical pixel in"
);
assert!(cov(38.5, 80.5) < 0.01, "nothing two physical pixels out");
assert_eq!(q.rect.x.fract(), 0.0, "on whole pixels");
}
#[test]
fn a_fractional_position_is_baked_into_the_mask() {
let (core, q) = masks(
|ui| {
ui.path(
&Path::parse("M20.5 20 H60.5 V60 H20.5 Z").unwrap(),
NodeSpec::column().bg(Color::rgba8(0, 0, 0, 255)),
);
},
1.0,
);
let q = &q[0];
assert_eq!(q.rect.x.fract(), 0.0);
let cov = |x: f32, y: f32| coverage(&core, q, x, y).unwrap_or(0.0);
assert!(cov(21.5, 40.5) > 0.99, "{}", cov(21.5, 40.5));
assert!(cov(20.5, 40.5) > 0.95, "{}", cov(20.5, 40.5));
assert!(cov(19.5, 40.5) < 0.05, "{}", cov(19.5, 40.5));
assert!(Vec2::new(q.rect.x, q.rect.y).x <= 19.0);
}
fn turned_coverage(core: &Core, q: &Quad, x: f32, y: f32) -> f32 {
let (cx, cy) = (q.rect.x + q.rect.w * 0.5, q.rect.y + q.rect.h * 0.5);
let (sin, cos) = (-q.blur).sin_cos();
let (dx, dy) = (x - cx, y - cy);
let lx = dx * cos - dy * sin + q.rect.w * 0.5;
let ly = dx * sin + dy * cos + q.rect.h * 0.5;
if lx < 0.0 || ly < 0.0 || lx >= q.rect.w || ly >= q.rect.h {
return 0.0;
}
let texel = |tx: i32, ty: i32| -> f32 {
let tx = (q.uv[0] as i32 + tx.clamp(0, q.uv[2] as i32 - 1)) as u32;
let ty = (q.uv[1] as i32 + ty.clamp(0, q.uv[3] as i32 - 1)) as u32;
f32::from(core.atlas.pixels[((ty * core.atlas.size + tx) * 4 + 3) as usize]) / 255.0
};
let (fx, fy) = (lx - 0.5, ly - 0.5);
let (x0, y0) = (fx.floor(), fy.floor());
let (ax, ay) = (fx - x0, fy - y0);
let (x0, y0) = (x0 as i32, y0 as i32);
let top = texel(x0, y0) * (1.0 - ax) + texel(x0 + 1, y0) * ax;
let bottom = texel(x0, y0 + 1) * (1.0 - ax) + texel(x0 + 1, y0 + 1) * ax;
(top * (1.0 - ay) + bottom * ay) * q.color.a
}
fn turn_error(
turned: impl FnOnce(&mut kui_core::Ui<'_>),
reference: impl FnOnce(&mut kui_core::Ui<'_>),
) -> (f32, f32) {
let (tc, tq) = masks(turned, 1.0);
let (rc, rq) = masks(reference, 1.0);
assert_eq!((tq.len(), rq.len()), (1, 1));
let (mut worst, mut worst_flat) = (0.0f32, 0.0f32);
for y in 0..100 {
for x in 0..200 {
let (px, py) = (x as f32 + 0.5, y as f32 + 0.5);
let want = coverage(&rc, &rq[0], px, py).unwrap_or(0.0);
let got = turned_coverage(&tc, &tq[0], px, py);
let d = (want - got).abs();
worst = worst.max(d);
let flat = [(-1.0, 0.0), (1.0, 0.0), (0.0, -1.0), (0.0, 1.0)]
.iter()
.all(|(ox, oy)| {
(coverage(&rc, &rq[0], px + ox, py + oy).unwrap_or(0.0) - want).abs() < 0.01
});
if flat {
worst_flat = worst_flat.max(d);
}
}
}
(worst, worst_flat)
}
#[test]
fn a_wedge_turned_by_its_quad_is_the_wedge_rasterized_turned() {
let black = || NodeSpec::column().bg(Color::rgba8(0, 0, 0, 255));
for degrees in [7.0f32, 45.0, 90.0] {
let turns = degrees / 360.0;
let (worst, flat) = turn_error(
|ui| {
ui.path(
&Path::sector(100.0, 50.0, 40.0, 0.0, 0.0, 0.3)
.pivot(100.0, 50.0)
.rotated(turns),
black(),
);
},
|ui| {
ui.path(&Path::sector(100.0, 50.0, 40.0, 0.0, turns, 0.3), black());
},
);
println!("wedge {degrees}°: edge {worst:.3}, flat {flat:.3}");
assert!(
flat < 0.02,
"{degrees}°: {flat} off where the wedge is flat"
);
assert!(worst < 0.35, "{degrees}°: {worst} off on an edge");
}
}
#[test]
fn a_thin_arc_turned_by_its_quad_keeps_its_line() {
let arc = |from: f32| {
let (a0, a1) = (
from * std::f32::consts::TAU,
(from + 0.3) * std::f32::consts::TAU,
);
Path::new()
.move_to(100.0 + 30.0 * a0.cos(), 50.0 + 30.0 * a0.sin())
.arc_to(
30.0,
30.0,
0.0,
false,
true,
100.0 + 30.0 * a1.cos(),
50.0 + 30.0 * a1.sin(),
)
.stroked(kui_core::Stroke::new(1.5, Color::rgba8(0, 0, 0, 255)))
};
for degrees in [7.0f32, 45.0, 90.0] {
let turns = degrees / 360.0;
let (worst, _) = turn_error(
|ui| {
ui.path(
&arc(0.0).pivot(100.0, 50.0).rotated(turns),
NodeSpec::column(),
);
},
|ui| ui.path(&arc(turns), NodeSpec::column()),
);
println!("1.5 px arc {degrees}°: {worst:.3}");
assert!(worst < 0.5, "{degrees}°: {worst} off along the line");
}
}
#[test]
fn two_wedges_turned_together_leave_no_seam() {
let turns = 0.07;
let (core, q) = masks(
|ui| {
for (from, color) in [
(0.0, Color::rgba8(255, 0, 0, 255)),
(0.25, Color::rgba8(0, 0, 255, 255)),
] {
ui.path(
&Path::sector(100.0, 50.0, 40.0, 0.0, from, 0.25)
.pivot(100.0, 50.0)
.rotated(turns),
NodeSpec::column().bg(color),
);
}
},
1.0,
);
assert_eq!(q.len(), 2);
let a = (0.25 + turns) * std::f32::consts::TAU;
let (nx, ny) = (-a.sin(), a.cos());
let mut least = 1.0f32;
for r in [10.0f32, 20.0, 30.0] {
for off in [-0.5f32, 0.0, 0.5] {
let x = 100.0 + r * a.cos() + off * nx;
let y = 50.0 + r * a.sin() + off * ny;
let (x, y) = (x.floor() + 0.5, y.floor() + 0.5);
let c0 = turned_coverage(&core, &q[0], x, y);
let c1 = turned_coverage(&core, &q[1], x, y);
least = least.min(c0 + c1 * (1.0 - c0));
}
}
println!("turned seam: least {least:.3}");
assert!(least > 0.9, "the seam shows: {least}");
}