use kui_core::{Color, Core, Gradient, GradientStop, NodeSpec, Quad, QuadKind, Side, Size, Vec2};
fn sample(core: &Core, u: f32, v: f32) -> [f32; 4] {
let size = core.atlas.size as i64;
let texel = |x: i64, y: i64| -> [f32; 4] {
let (x, y) = (x.clamp(0, size - 1), y.clamp(0, size - 1));
let at = ((y * size + x) * 4) as usize;
std::array::from_fn(|k| f32::from(core.atlas.pixels[at + k]) / 255.0)
};
let (fu, fv) = (u - 0.5, v - 0.5);
let (x0, y0) = (fu.floor(), fv.floor());
let (wx, wy) = (fu - x0, fv - y0);
let (x0, y0) = (x0 as i64, y0 as i64);
let (a, b, c, d) = (
texel(x0, y0),
texel(x0 + 1, y0),
texel(x0, y0 + 1),
texel(x0 + 1, y0 + 1),
);
std::array::from_fn(|k| {
(a[k] * (1.0 - wx) + b[k] * wx) * (1.0 - wy) + (c[k] * (1.0 - wx) + d[k] * wx) * wy
})
}
fn shade(core: &Core, q: &Quad, x: f32, y: f32) -> [f32; 4] {
let (fx, fy) = ((x - q.rect.x) / q.rect.w, (y - q.rect.y) / q.rect.h);
let t = sample(
core,
q.uv[0] as f32 + fx * q.uv[2] as f32,
q.uv[1] as f32 + fy * q.uv[3] as f32,
);
[
t[0] * q.color.r,
t[1] * q.color.g,
t[2] * q.color.b,
t[3] * q.color.a,
]
}
fn boxed(g: &Gradient, w: f32, h: f32) -> (Core, Quad) {
let mut core = Core::new();
let mut ui = core.frame(Size::new(w + 20.0, h + 80.0), 1.0);
let noise = |turns: f32| {
NodeSpec::column()
.size(8.0, 8.0)
.gradient(Gradient::angle(turns, [Color::WHITE, Color::BLACK]))
};
ui.leaf(noise(0.0));
ui.leaf(noise(0.3));
ui.leaf(NodeSpec::column().size(w, h).gradient(g.clone()));
ui.leaf(noise(0.25));
ui.finish();
let q = core
.output()
.0
.quads
.iter()
.filter(|q| q.kind == QuadKind::Image)
.nth(2)
.copied()
.expect("the box's image quad");
assert_eq!((q.rect.w, q.rect.h), (w, h));
(core, q)
}
fn worst(g: &Gradient, w: f32, h: f32) -> f32 {
let (core, q) = boxed(g, w, h);
let mut worst = 0.0f32;
for py in 0..h as u32 {
for px in 0..w as u32 {
let (x, y) = (px as f32 + 0.5, py as f32 + 0.5);
let got = shade(&core, &q, q.rect.x + x, q.rect.y + y);
let c = g.color_in(x / w, y / h);
let want = [c.r, c.g, c.b, c.a];
for k in 0..4 {
worst = worst.max((got[k] - want[k]).abs() * 255.0);
}
}
}
worst
}
const BLUE: u32 = 0x7f9cf5ff;
const PINK: u32 = 0xe07a8aff;
const DARK: u32 = 0x14161eff;
fn two(a: u32, b: u32) -> [Color; 2] {
[Color::hex(a), Color::hex(b)]
}
#[test]
fn a_strip_stretched_over_a_box_is_the_gradient_to_its_edges() {
for side in [Side::Right, Side::Left, Side::Bottom, Side::Top] {
let g = Gradient::to(side, two(BLUE, PINK));
let (w, h) = match side {
Side::Right | Side::Left => (1000.0, 40.0),
_ => (40.0, 1000.0),
};
let d = worst(&g, w, h);
println!("strip {side:?} over {w}x{h}: {d:.2} levels");
assert!(d <= 1.0, "{side:?}: {d} levels");
}
let g = Gradient::to(Side::Right, [Color::BLACK, Color::WHITE]);
let d = worst(&g, 1000.0, 40.0);
println!("strip black to white over 1000x40: {d:.2} levels");
assert!(d <= 1.0, "{d} levels");
}
#[test]
fn the_square_stretched_to_a_3_to_1_box_is_within_two_levels() {
let cases: [(&str, Gradient); 5] = [
("corner", Gradient::to(Side::BottomRight, two(BLUE, PINK))),
("angle 0.07", Gradient::angle(0.07, two(BLUE, PINK))),
(
"corner, full range",
Gradient::to(Side::TopRight, [Color::BLACK, Color::WHITE]),
),
("radial", Gradient::radial(two(BLUE, DARK))),
(
"radial from the top edge",
Gradient::radial_at(Vec2::new(0.5, 0.0), two(PINK, DARK)),
),
];
for (name, g) in cases {
let d = worst(&g, 600.0, 200.0);
println!("square, {name}, over 600x200: {d:.2} levels");
assert!(d <= 2.0, "{name}: {d} levels");
}
}
#[test]
fn a_middle_stop_s_kink_costs_a_few_levels_at_the_kink() {
let stops = [Color::hex(BLUE), Color::hex(0xffe066ff), Color::hex(PINK)];
let strip = worst(&Gradient::to(Side::Right, stops), 1000.0, 40.0);
let square = worst(&Gradient::to(Side::BottomRight, stops), 600.0, 200.0);
println!("three stops: strip {strip:.2} levels, square {square:.2} levels");
assert!(strip <= 2.0, "{strip}");
assert!(square <= 4.0, "{square}");
}
#[test]
fn a_hard_stop_on_a_strip_is_a_few_pixels_wide() {
let g = Gradient::to(
Side::Right,
[
GradientStop::from((Color::BLACK, 0.5)),
GradientStop::from((Color::WHITE, 0.5)),
],
);
let (core, q) = boxed(&g, 1000.0, 20.0);
let between = (0..1000)
.filter(|&px| {
let v = shade(&core, &q, q.rect.x + px as f32 + 0.5, q.rect.y + 10.0)[0];
v > 0.02 && v < 0.98
})
.count();
println!("a hard stop over 1000 px: {between} px between the two colours");
assert!((1..=5).contains(&between), "{between}");
}
#[test]
fn a_fade_to_transparent_keeps_its_colour_across_the_box() {
let g = Gradient::to(Side::Right, [Color::hex(0xff0000ff), Color::TRANSPARENT]);
let (core, q) = boxed(&g, 400.0, 20.0);
for px in [10.0, 100.0, 200.0, 300.0, 380.0] {
let c = shade(&core, &q, q.rect.x + px + 0.5, q.rect.y + 10.0);
let want = 1.0 - (px + 0.5) / 400.0;
assert!(c[0] > 0.99 && c[1] < 0.01, "at {px}: {c:?}");
assert!(
(c[3] - want).abs() < 0.01,
"alpha at {px}: {} vs {want}",
c[3]
);
}
}