vectorcraft/crates/render/src/tests_blend.rs

110 lines
4.2 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

//! Blend-mode accuracy: every mode matches the reference formulas ([`blend_rgb`]) within 1/255
//! (see [`tolerance`]), whether it is an object's blend mode, a fill's or a group's.
use super::*;
use vectorcraft_color::blend::blend_rgb;
use vectorcraft_color::{BlendMode as Blend, Color, Paint};
use vectorcraft_doc::{Appearance, Node};
use vectorcraft_geom::shapes;
/// Backdrop / source pairs (8-bit, so the expected values carry no input rounding) that take every
/// branch of the formulas: dark and light backdrops, sources on both sides of ½, greys, extremes.
const PAIRS: [([u8; 3], [u8; 3]); 6] = [
([51, 128, 230], [153, 64, 128]),
([230, 26, 102], [77, 204, 179]),
([0, 255, 128], [255, 0, 127]),
([200, 180, 40], [20, 60, 250]),
([128, 128, 128], [240, 30, 90]),
([10, 90, 160], [128, 128, 128]),
];
fn rect(d: &mut Document, r: Rect, rgb: [u8; 3]) -> Node {
Node::path(d.alloc_id(), shapes::rectangle(r), Appearance::basic(Paint::solid(Color::rgb8(rgb[0], rgb[1], rgb[2])), Paint::None, 0.0))
}
/// Where the blend is set: on the object, on its fill, or on a group around it.
#[derive(Clone, Copy, Debug)]
enum Level {
Object,
Fill,
Group,
}
/// An opaque backdrop with the source on top of it, blended with `mode` at `level`.
fn doc(mode: Blend, level: Level, backdrop: [u8; 3], source: [u8; 3]) -> Document {
let mut d = Document::new(20.0, 20.0);
let below = rect(&mut d, Rect::new(0.0, 0.0, 20.0, 20.0), backdrop);
let mut top = rect(&mut d, Rect::new(5.0, 5.0, 15.0, 15.0), source);
match level {
Level::Object => top.blend = mode,
Level::Fill => {
if let Some(vectorcraft_doc::AppearanceItem::Fill(f)) = top.appearance.items.first_mut() {
f.blend = mode;
}
}
Level::Group => {}
}
let top = match level {
Level::Group => {
let mut g = Node::group(d.alloc_id(), vec![Arc::new(top)]);
g.blend = mode;
g
}
_ => top,
};
let l = d.layers[0].id;
d.insert(Some(l), usize::MAX, below).unwrap();
d.insert(Some(l), usize::MAX, top).unwrap();
d
}
pub(crate) fn expected(mode: Blend, backdrop: [u8; 3], source: [u8; 3]) -> [u8; 3] {
let f = |c: [u8; 3]| c.map(|v| v as f32 / 255.0);
blend_rgb(mode, f(backdrop), f(source)).map(|v| (v.clamp(0.0, 1.0) * 255.0).round() as u8)
}
/// Whether `a` is within `tol`/255 of `b` in every channel.
pub(crate) fn within(a: [u8; 3], b: [u8; 3], tol: i32) -> bool {
a.iter().zip(b).all(|(x, y)| (*x as i32 - y as i32).abs() <= tol)
}
/// Tolerance of `mode` on screen: 1/255, except Exclusion, which the rasteriser's 8-bit pipeline
/// rounds twice (b + s − 2bs).
fn tolerance(mode: Blend) -> i32 {
if mode == Blend::Exclusion { 2 } else { 1 }
}
#[test]
fn every_mode_matches_the_reference_formulas() {
let mut r = Renderer::new();
for mode in Blend::ALL {
for level in [Level::Object, Level::Fill, Level::Group] {
for (b, s) in PAIRS {
let px = r.render(&doc(mode, level, b, s), 20, 20, Affine::IDENTITY, &RenderOptions::default()).pixel(10, 10);
let want = expected(mode, b, s);
assert!(
within([px[0], px[1], px[2]], want, tolerance(mode)) && px[3] == 255,
"{mode:?} at {level:?}, {b:?} under {s:?}: {px:?}, want {want:?}"
);
}
}
}
}
#[test]
fn a_translucent_source_mixes_the_blend_by_its_opacity() {
let mut r = Renderer::new();
for mode in Blend::ALL {
let (b, s) = PAIRS[0];
let mut d = doc(mode, Level::Object, b, s);
let top = d.layers[0].children().unwrap()[1].id;
d.node_mut(top).unwrap().opacity = 0.6;
let px = r.render(&d, 20, 20, Affine::IDENTITY, &RenderOptions::default()).pixel(10, 10);
let f = |c: [u8; 3], a: f32| [c[0] as f32 / 255.0, c[1] as f32 / 255.0, c[2] as f32 / 255.0, a];
let o = vectorcraft_color::blend::composite(mode, f(b, 1.0), f(s, 0.6));
let want = [o[0], o[1], o[2]].map(|v| (v * 255.0).round() as u8);
// The opacity is one more 8-bit step.
assert!(within([px[0], px[1], px[2]], want, tolerance(mode) + 1), "{mode:?}: {px:?}, want {want:?}");
}
}