vectorcraft/crates/render/src/tests_strokegradient.rs

115 lines
5.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.

//! Gradients along and across strokes: positions along the path and across the stroke, no seams
//! between the slices, translucent stops painted once.
use vectorcraft_color::{Gradient, GradientPaint, Paint};
use vectorcraft_doc::{Appearance, AppearanceItem, Node, StrokeGradientMode, StrokeLayer};
use vectorcraft_geom::PathData;
use super::*;
const SIZE: u32 = 200;
/// `bp` stroked 20 pt with `g` laid `mode`, on a 200×200 page (white, or transparent).
fn render(bp: &BezPath, g: Gradient, mode: StrokeGradientMode, white: bool) -> Rendered {
let mut st = StrokeLayer::new(Paint::Gradient(Box::new(GradientPaint::new(g))), 20.0);
st.gradient_mode = mode;
let mut d = Document::new(SIZE as f64, SIZE as f64);
let n = Node::path(d.alloc_id(), PathData::from_bezpath(bp), Appearance { items: vec![AppearanceItem::Stroke(st)], ..Default::default() });
let l = d.layers[0].id;
d.insert(Some(l), 0, n).unwrap();
let background = white.then_some([255, 255, 255, 255]);
Renderer::new().render(&d, SIZE, SIZE, Affine::IDENTITY, &RenderOptions { background, ..Default::default() })
}
fn line(a: (f64, f64), b: (f64, f64)) -> BezPath {
let mut p = BezPath::new();
p.move_to(a);
p.line_to(b);
p
}
/// Grey level (0..1) at the centre of pixel (x, y).
fn grey(img: &Rendered, x: f64, y: f64) -> f64 {
img.pixel(x as u32, y as u32)[0] as f64 / 255.0
}
#[test]
fn along_a_quarter_of_the_length_is_a_quarter_of_the_gradient_in_any_direction() {
for (a, b) in [((20.0, 100.0), (180.0, 100.0)), ((100.0, 180.0), (100.0, 20.0)), ((30.0, 30.0), (170.0, 170.0))] {
let img = render(&line(a, b), Gradient::default(), StrokeGradientMode::Along, true);
for (f, off) in [(0.25, 0.0), (0.25, 6.0), (0.75, -6.0), (0.5, 0.0)] {
let (dx, dy) = (b.0 - a.0, b.1 - a.1);
let len = (dx * dx + dy * dy).sqrt();
let (x, y) = (a.0 + dx * f - dy / len * off, a.1 + dy * f + dx / len * off);
let want = 1.0 - f;
assert!((grey(&img, x, y) - want).abs() < 0.03, "{a:?}→{b:?} at {f}: {} vs {want}", grey(&img, x, y));
}
}
}
#[test]
fn across_runs_from_the_left_edge_to_the_right() {
// Travelling right, the left edge is the top one (y 90), the right edge the bottom (y 110).
let img = render(&line((20.0, 100.0), (180.0, 100.0)), Gradient::default(), StrokeGradientMode::Across, true);
for (y, want) in [(91.0, 0.925), (95.0, 0.725), (100.0, 0.475), (108.0, 0.075)] {
for x in [40.0, 100.0, 160.0] {
assert!((grey(&img, x, y) - want).abs() < 0.03, "({x}, {y}): {} vs {want}", grey(&img, x, y));
}
}
// Within (the default) the gradient lies on the page: left to right here.
let within = render(&line((20.0, 100.0), (180.0, 100.0)), Gradient::default(), StrokeGradientMode::Within, true);
assert!(grey(&within, 40.0, 91.0) > grey(&within, 160.0, 91.0) + 0.5);
}
#[test]
fn slices_meet_without_seams_and_translucent_stops_paint_once() {
let circle = kurbo::Circle::new((100.0, 100.0), 60.0).to_path(0.01);
let mut half = Gradient::default();
for s in &mut half.stops {
s.opacity = 0.5;
}
for mode in [StrokeGradientMode::Along, StrokeGradientMode::Across] {
let solid = render(&circle, Gradient::default(), mode, false);
let translucent = render(&circle, half.clone(), mode, false);
for k in 0..720 {
let a = k as f64 * std::f64::consts::TAU / 720.0;
for r in [55.0, 60.0, 65.0] {
let (x, y) = (100.0 + r * a.cos(), 100.0 + r * a.sin());
assert_eq!(solid.pixel(x as u32, y as u32)[3], 255, "{mode:?}: a seam at ({x:.1}, {y:.1})");
let alpha = translucent.pixel(x as u32, y as u32)[3] as i32;
assert!((alpha - 128).abs() <= 2, "{mode:?}: alpha {alpha} at ({x:.1}, {y:.1})");
}
}
}
}
#[test]
fn corners_and_tight_turns_have_no_holes() {
let mut corner = line((40.0, 40.0), (160.0, 40.0));
corner.line_to((160.0, 160.0));
corner.line_to((60.0, 120.0));
let tight = kurbo::Circle::new((100.0, 100.0), 6.0).to_path(0.01);
let mut half = Gradient::default();
for s in &mut half.stops {
s.opacity = 0.5;
}
for mode in [StrokeGradientMode::Within, StrokeGradientMode::Along, StrokeGradientMode::Across] {
for (bp, probes) in [
(
&corner,
vec![(160.0, 40.0), (156.0, 36.0), (165.0, 45.0), (163.0, 37.0), (157.0, 43.0), (160.0, 160.0), (166.0, 158.0), (152.0, 157.0)],
),
// The stroke's inside edge folds over the centre: a 4 pt hole stays there.
(&tight, vec![(105.5, 100.0), (95.0, 103.0), (108.0, 100.0), (100.0, 113.0), (91.0, 92.0), (100.0, 94.0)]),
] {
let solid = render(bp, Gradient::default(), mode, false);
let translucent = render(bp, half.clone(), mode, false);
for (x, y) in probes {
assert_eq!(solid.pixel(x as u32, y as u32)[3], 255, "{mode:?}: a hole at ({x}, {y})");
let alpha = translucent.pixel(x as u32, y as u32)[3] as i32;
assert!((alpha - 128).abs() <= 2, "{mode:?}: alpha {alpha} at ({x}, {y})");
}
}
}
}