mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-10-26 10:47:11 -06:00
A scan's paper is a tenth darker in one corner than in another. Flat Logo found each tone of it as a colour of its own and traced the bands and the specks between them: on the scanned inputs 470 paths, 21 px from the art on average. Two palette colours are now one when the pixels between them are no fewer than a third of those at the lesser of the two: flat colours leave only their shared edge between them, tones of one colour fill the gap. Whether the image is flat art at all is still asked of the tones as found. Flat Logo on its inputs, all sizes and variants: 3.106 -> 0.090 px off on average, 88.5 -> 0.82 px at most, 69.9 -> 3.9 paths; clean, aliased, blurred and noisy art as before.
430 lines
18 KiB
Rust
430 lines
18 KiB
Rust
//! End-to-end tests: synthetic logos made in code, with known geometry, traced and measured.
|
||
|
||
use crate::{Method, Mode, Raster, TraceError, TraceParams, TraceResult, trace_within};
|
||
use kurbo::{ParamCurve, Shape};
|
||
use vectorcraft_geom::Point;
|
||
|
||
/// A soft, anti-aliased render of `shade` (`Some(colour)` where the shape is), supersampled `ss × ss`
|
||
/// and blurred by `blur` pixels, on a transparent background.
|
||
fn render(w: u32, h: u32, blur: f64, shade: impl Fn(f64, f64) -> Option<[u8; 3]>) -> Raster {
|
||
let ss = 8usize;
|
||
let (wu, hu) = (w as usize, h as usize);
|
||
let mut px = vec![[0.0f64; 4]; wu * hu];
|
||
for y in 0..hu {
|
||
for x in 0..wu {
|
||
let mut acc = [0.0f64; 4];
|
||
for sy in 0..ss {
|
||
for sx in 0..ss {
|
||
let (fx, fy) = (x as f64 + (sx as f64 + 0.5) / ss as f64, y as f64 + (sy as f64 + 0.5) / ss as f64);
|
||
if let Some(c) = shade(fx, fy) {
|
||
acc[0] += f64::from(c[0]) / 255.0;
|
||
acc[1] += f64::from(c[1]) / 255.0;
|
||
acc[2] += f64::from(c[2]) / 255.0;
|
||
acc[3] += 1.0;
|
||
}
|
||
}
|
||
}
|
||
let n = (ss * ss) as f64;
|
||
px[y * wu + x] = [acc[0] / n, acc[1] / n, acc[2] / n, acc[3] / n];
|
||
}
|
||
}
|
||
if blur > 0.0 {
|
||
let r = (3.0 * blur).ceil() as i64;
|
||
let k: Vec<f64> = (-r..=r).map(|i| (-0.5 * (i as f64 / blur).powi(2)).exp()).collect();
|
||
let sum: f64 = k.iter().sum();
|
||
let k: Vec<f64> = k.iter().map(|v| v / sum).collect();
|
||
for horizontal in [true, false] {
|
||
let src = px.clone();
|
||
for y in 0..hu as i64 {
|
||
for x in 0..wu as i64 {
|
||
let mut acc = [0.0f64; 4];
|
||
for (j, kv) in k.iter().enumerate() {
|
||
let d = j as i64 - r;
|
||
let (xx, yy) = if horizontal { ((x + d).clamp(0, wu as i64 - 1), y) } else { (x, (y + d).clamp(0, hu as i64 - 1)) };
|
||
let p = src[(yy as usize) * wu + xx as usize];
|
||
for c in 0..4 {
|
||
acc[c] += kv * p[c];
|
||
}
|
||
}
|
||
px[(y as usize) * wu + x as usize] = acc;
|
||
}
|
||
}
|
||
}
|
||
}
|
||
let mut rgba = Vec::with_capacity(wu * hu * 4);
|
||
for p in px {
|
||
let a = p[3];
|
||
let un = |v: f64| if a > 1e-9 { ((v / a) * 255.0).round().clamp(0.0, 255.0) as u8 } else { 0 };
|
||
rgba.extend_from_slice(&[un(p[0]), un(p[1]), un(p[2]), (a * 255.0).round().clamp(0.0, 255.0) as u8]);
|
||
}
|
||
Raster::new(w, h, rgba)
|
||
}
|
||
|
||
const RED: [u8; 3] = [210, 40, 50];
|
||
const BLUE: [u8; 3] = [40, 80, 200];
|
||
|
||
fn logo_params() -> TraceParams {
|
||
TraceParams {
|
||
mode: Mode::Logo,
|
||
colors: 6,
|
||
paths: 60.0,
|
||
corners: 50.0,
|
||
noise: 6,
|
||
snap_curves_to_lines: true,
|
||
method: Method::Abutting,
|
||
..TraceParams::default()
|
||
}
|
||
}
|
||
|
||
fn trace(img: &Raster, p: &TraceParams) -> TraceResult {
|
||
trace_within(img, p, 100_000).expect("trace")
|
||
}
|
||
|
||
fn samples(res: &TraceResult) -> Vec<Point> {
|
||
let mut out = Vec::new();
|
||
for p in &res.paths {
|
||
for seg in p.path.to_bezpath().segments() {
|
||
out.extend((0..=40).map(|i| seg.eval(f64::from(i) / 40.0)));
|
||
}
|
||
}
|
||
out
|
||
}
|
||
|
||
fn covered(res: &TraceResult, p: Point) -> bool {
|
||
res.paths.iter().any(|t| t.path.to_bezpath().winding(p) != 0)
|
||
}
|
||
|
||
fn near(a: [u8; 3], b: [u8; 3], tol: i32) -> bool {
|
||
(0..3).all(|i| (i32::from(a[i]) - i32::from(b[i])).abs() <= tol)
|
||
}
|
||
|
||
#[test]
|
||
fn a_blurred_disc_becomes_a_true_circle_within_a_tenth_of_a_pixel() {
|
||
let (cx, cy, r) = (31.7, 32.4, 19.3);
|
||
let img = render(64, 64, 0.7, |x, y| ((x - cx).hypot(y - cy) <= r).then_some(RED));
|
||
let res = trace(&img, &logo_params());
|
||
assert_eq!(res.paths.len(), 1, "{} paths", res.paths.len());
|
||
assert!(near(res.paths[0].color, RED, 3), "colour {:?}", res.paths[0].color);
|
||
let errs: Vec<f64> = samples(&res).iter().map(|p| (p.distance(Point::new(cx, cy)) - r).abs()).collect();
|
||
let mean = errs.iter().sum::<f64>() / errs.len() as f64;
|
||
let max = errs.iter().copied().fold(0.0, f64::max);
|
||
assert!(mean < 0.06 && max < 0.15, "mean {mean:.3} max {max:.3}");
|
||
assert!(res.anchor_count() <= 6, "{} anchors for a circle", res.anchor_count());
|
||
}
|
||
|
||
#[test]
|
||
fn a_blurred_rectangle_keeps_square_corners_and_four_anchors() {
|
||
let (x0, x1, y0, y1) = (10.4, 77.9, 8.7, 38.2);
|
||
let img = render(90, 48, 0.8, |x, y| ((x0..=x1).contains(&x) && (y0..=y1).contains(&y)).then_some(BLUE));
|
||
let res = trace(&img, &logo_params());
|
||
assert_eq!(res.paths.len(), 1);
|
||
assert_eq!(res.anchor_count(), 4, "a rectangle is four corners");
|
||
let b = res.paths[0].path.bounds().expect("bounds");
|
||
for (got, want, what) in [(b.x0, x0, "left"), (b.x1, x1, "right"), (b.y0, y0, "top"), (b.y1, y1, "bottom")] {
|
||
// Sub-pixel: within 0.15 px of the true edge, from a blurred 90 px image.
|
||
assert!((got - want).abs() < 0.15, "{what} edge {got:.3} vs {want}");
|
||
}
|
||
}
|
||
|
||
#[test]
|
||
fn touching_colours_leave_no_gap_between_them() {
|
||
let edge = 40.35;
|
||
let img = render(80, 50, 0.7, |x, y| {
|
||
if !(10.2..=40.6).contains(&y) {
|
||
None
|
||
} else if (10.3..=edge).contains(&x) {
|
||
Some(RED)
|
||
} else if (edge..=70.6).contains(&x) {
|
||
Some(BLUE)
|
||
} else {
|
||
None
|
||
}
|
||
});
|
||
let res = trace(&img, &logo_params());
|
||
assert_eq!(res.paths.len(), 2);
|
||
assert!(!near(res.paths[0].color, res.paths[1].color, 20));
|
||
for i in 0..200 {
|
||
let y = 12.0 + 27.0 * f64::from(i) / 199.0;
|
||
for dx in [-0.03, 0.0, 0.03] {
|
||
assert!(covered(&res, Point::new(edge + dx, y)), "gap at ({}, {y})", edge + dx);
|
||
}
|
||
}
|
||
}
|
||
|
||
#[test]
|
||
fn letters_knocked_out_of_a_badge_become_white_or_holes() {
|
||
let ring = |x: f64, y: f64| {
|
||
let d = (x - 32.0).hypot(y - 32.0);
|
||
(12.0..=25.0).contains(&d).then_some(BLUE)
|
||
};
|
||
let img = render(64, 64, 0.7, ring);
|
||
// Default: the knockout is painted white, on top of the badge.
|
||
let res = trace(&img, &logo_params());
|
||
assert_eq!(res.paths.len(), 2, "badge + white knockout");
|
||
assert!(res.paths.iter().any(|p| p.color == [255, 255, 255]));
|
||
assert!(covered(&res, Point::new(32.0, 32.0)));
|
||
// Ignore white: the knockout is a real hole in the badge.
|
||
let res = trace(&img, &TraceParams { ignore_white: true, ..logo_params() });
|
||
assert_eq!(res.paths.len(), 1);
|
||
assert_eq!(res.paths[0].path.subpaths.len(), 2, "outer outline and a hole");
|
||
assert!(!covered(&res, Point::new(32.0, 32.0)), "the hole must be empty");
|
||
assert!(covered(&res, Point::new(32.0, 32.0 + 18.0)), "the ring itself is filled");
|
||
}
|
||
|
||
#[test]
|
||
fn a_gradient_is_refused_not_posterised() {
|
||
let img = Raster::from_fn(64, 32, |x, y| [(x * 4) as u8, 255 - (x * 4) as u8, (y * 6) as u8, 255]);
|
||
match trace_within(&img, &logo_params(), 100_000) {
|
||
Err(TraceError::NotFlat { fraction }) => assert!(fraction < 0.93),
|
||
other => panic!("expected NotFlat, got {other:?}"),
|
||
}
|
||
let msg = TraceError::NotFlat { fraction: 0.69 }.to_string();
|
||
assert!(msg.contains("69%") && msg.contains("Color mode"), "{msg}");
|
||
}
|
||
|
||
#[test]
|
||
fn a_white_background_logo_drops_the_white_when_asked() {
|
||
let img = Raster::from_fn(80, 60, |x, y| {
|
||
let d = (x as f64 + 0.5 - 40.0).hypot(y as f64 + 0.5 - 30.0);
|
||
if (10.0..=24.0).contains(&d) { [20, 20, 20, 255] } else { [255, 255, 255, 255] }
|
||
});
|
||
let kept = trace(&img, &logo_params());
|
||
assert!(kept.paths.iter().any(|p| p.color == [255, 255, 255]), "white background kept by default");
|
||
let dropped = trace(&img, &TraceParams { ignore_white: true, ..logo_params() });
|
||
assert!(dropped.paths.iter().all(|p| p.color != [255, 255, 255]));
|
||
assert!(!dropped.paths.is_empty());
|
||
}
|
||
|
||
#[test]
|
||
fn the_named_palette_is_used_exactly_and_a_bad_one_is_an_error() {
|
||
let img = render(64, 64, 0.7, |x, y| ((x - 32.0).hypot(y - 32.0) <= 20.0).then_some(RED));
|
||
let named = TraceParams { logo_colors: vec!["#d22832".into()], ..logo_params() };
|
||
let res = trace(&img, &named);
|
||
assert_eq!(res.paths[0].color, [0xd2, 0x28, 0x32]);
|
||
let bad = TraceParams { logo_colors: vec!["red".into()], ..logo_params() };
|
||
assert!(matches!(trace_within(&img, &bad, 1000), Err(TraceError::BadPalette(s)) if s == "red"));
|
||
}
|
||
|
||
#[test]
|
||
fn a_large_image_is_traced_at_reduced_size_and_scaled_back() {
|
||
let (cx, cy, r) = (1200.0, 600.0, 400.0);
|
||
let img = Raster::from_fn(2400, 1200, |x, y| {
|
||
if (f64::from(x) + 0.5 - cx).hypot(f64::from(y) + 0.5 - cy) <= r { [RED[0], RED[1], RED[2], 255] } else { [0, 0, 0, 0] }
|
||
});
|
||
let res = trace(&img, &logo_params());
|
||
assert_eq!(res.paths.len(), 1);
|
||
let b = res.paths[0].path.bounds().expect("bounds");
|
||
for (got, want) in [(b.x0, cx - r), (b.x1, cx + r), (b.y0, cy - r), (b.y1, cy + r)] {
|
||
assert!((got - want).abs() < 2.0, "{got} vs {want}");
|
||
}
|
||
}
|
||
|
||
/// Every anchor of every path is finite and inside (a small margin around) the image.
|
||
fn assert_sane(res: &TraceResult, w: u32, h: u32) {
|
||
for t in &res.paths {
|
||
for sp in &t.path.subpaths {
|
||
for a in &sp.anchors {
|
||
assert!(a.p.x.is_finite() && a.p.y.is_finite() && a.h_in.x.is_finite() && a.h_out.y.is_finite(), "non-finite anchor");
|
||
assert!(a.p.x > -2.0 && a.p.y > -2.0 && a.p.x < f64::from(w) + 2.0 && a.p.y < f64::from(h) + 2.0, "anchor {:?} outside {w}x{h}", a.p);
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
#[test]
|
||
fn degenerate_images_are_fine() {
|
||
let p = logo_params();
|
||
for (w, h, img) in [
|
||
(0, 0, Raster::new(0, 0, vec![])),
|
||
(1, 1, Raster::new(1, 1, vec![200, 30, 30, 255])),
|
||
(3, 3, Raster::new(3, 3, vec![0; 36])),
|
||
(2, 2, Raster::new(2, 2, vec![255; 16])),
|
||
(5, 1, Raster::new(5, 1, vec![9; 20])),
|
||
(1, 7, Raster::new(1, 7, vec![130; 28])),
|
||
] {
|
||
match trace_within(&img, &p, 1000) {
|
||
Ok(res) => assert_sane(&res, w, h),
|
||
Err(TraceError::NotFlat { .. }) => {}
|
||
Err(e) => panic!("unexpected error for {w}x{h}: {e}"),
|
||
}
|
||
}
|
||
}
|
||
|
||
#[test]
|
||
fn extreme_parameters_never_panic() {
|
||
let img = render(48, 48, 0.7, |x, y| ((x - 24.0).hypot(y - 24.0) <= 15.0).then_some(RED));
|
||
let weird = [f64::NAN, f64::INFINITY, f64::NEG_INFINITY, -50.0, 0.0, 100.0, 1e9];
|
||
for &paths in &weird {
|
||
for &corners in &weird {
|
||
let p = TraceParams { paths, corners, ..logo_params() };
|
||
let _ = trace_within(&img, &p, 10_000);
|
||
}
|
||
}
|
||
for colors in [0, 1, 2, 300, u32::MAX] {
|
||
for noise in [0, 1, 1_000_000, u32::MAX] {
|
||
let p = TraceParams { colors, noise, ..logo_params() };
|
||
let _ = trace_within(&img, &p, 10_000);
|
||
}
|
||
}
|
||
// A tiny anchor budget is reported as too complex, not exceeded.
|
||
let r = trace_within(&img, &logo_params(), 0);
|
||
assert!(r.is_err() || r.is_ok_and(|t| t.anchor_count() == 0));
|
||
}
|
||
|
||
#[test]
|
||
fn noise_specks_are_dropped() {
|
||
let img = render(64, 64, 0.5, |x, y| if (x - 32.0).hypot(y - 32.0) <= 18.0 || (x - 6.0).hypot(y - 6.0) <= 0.6 { Some(RED) } else { None });
|
||
let res = trace(&img, &TraceParams { noise: 20, ..logo_params() });
|
||
assert_eq!(res.paths.len(), 1, "the speck should be gone");
|
||
}
|
||
|
||
#[test]
|
||
fn nested_rings_keep_their_own_holes_under_ignore_white() {
|
||
// Red ring, white band, red ring, white centre.
|
||
let img = render(80, 80, 0.7, |x, y| {
|
||
let d = (x - 40.0).hypot(y - 40.0);
|
||
((22.0..=28.0).contains(&d) || (8.0..=16.0).contains(&d)).then_some(RED)
|
||
});
|
||
let res = trace(&img, &TraceParams { ignore_white: true, ..logo_params() });
|
||
assert_eq!(res.paths.len(), 2, "two red rings");
|
||
for p in &res.paths {
|
||
assert_eq!(p.path.subpaths.len(), 2, "each ring is an outline and its own hole, no more");
|
||
}
|
||
assert!(!covered(&res, Point::new(40.0, 40.0)), "centre is empty");
|
||
assert!(covered(&res, Point::new(52.0, 40.0)), "inner ring is filled");
|
||
assert!(!covered(&res, Point::new(59.0, 40.0)), "the band between the rings is empty");
|
||
assert!(covered(&res, Point::new(65.0, 40.0)), "outer ring is filled");
|
||
}
|
||
|
||
#[test]
|
||
fn too_many_outlines_are_refused_not_silently_truncated() {
|
||
let specks =
|
||
|x: f64, y: f64| (x.rem_euclid(5.0) >= 1.0 && x.rem_euclid(5.0) < 4.0 && y.rem_euclid(5.0) >= 1.0 && y.rem_euclid(5.0) < 4.0).then_some(RED);
|
||
let img = render(200, 200, 0.0, specks);
|
||
assert!(matches!(trace_within(&img, &logo_params(), 1_000_000), Err(TraceError::TooComplex { .. })));
|
||
// Specks under the noise threshold do not count towards the limit.
|
||
let res = trace_within(&img, &TraceParams { noise: 200, ..logo_params() }, 1_000_000).expect("trace");
|
||
assert!(res.paths.is_empty());
|
||
}
|
||
|
||
#[test]
|
||
fn a_large_odd_sized_image_never_overshoots_its_edges() {
|
||
// 1025 wide: the last working pixel is narrower than the others.
|
||
let img = Raster::from_fn(1025, 20, |x, y| if x >= 600 && (2..18).contains(&y) { [RED[0], RED[1], RED[2], 255] } else { [0, 0, 0, 0] });
|
||
let res = trace(&img, &logo_params());
|
||
assert_eq!(res.paths.len(), 1);
|
||
let b = res.paths[0].path.bounds().expect("bounds");
|
||
assert!(b.x1 <= 1025.15 && b.x1 >= 1024.7, "right edge {}", b.x1);
|
||
assert!((b.x0 - 600.0).abs() < 0.6 && (b.y0 - 2.0).abs() < 0.6 && (b.y1 - 18.0).abs() < 0.6, "{b:?}");
|
||
assert_sane(&res, 1025, 20);
|
||
}
|
||
|
||
#[test]
|
||
fn a_hostile_palette_is_bounded_and_near_duplicates_merge() {
|
||
let img = render(64, 64, 0.7, |x, y| ((x - 32.0).hypot(y - 32.0) <= 20.0).then_some(RED));
|
||
let many: Vec<String> = std::iter::once("#d22832".to_string()).chain((1..200_000u32).map(|i| format!("#{:06x}", i * 37 % 0xfffff0))).collect();
|
||
let started = std::time::Instant::now();
|
||
let res = trace_within(&img, &TraceParams { logo_colors: many, ..logo_params() }, 100_000);
|
||
assert!(started.elapsed().as_secs() < 20, "palette handling took {:?}", started.elapsed());
|
||
if let Ok(r) = res {
|
||
assert!(r.palette.len() <= 11);
|
||
}
|
||
// Ten shades within a couple of units of one red are one colour, so one path, not ten layers.
|
||
let shades: Vec<String> = (0..10).map(|i| format!("#{:02x}{:02x}{:02x}", 210 + i % 3, 40 + i % 2, 50 + i % 3)).collect();
|
||
let res = trace(&img, &TraceParams { logo_colors: shades, ..logo_params() });
|
||
assert_eq!(res.paths.len(), 1, "{} paths", res.paths.len());
|
||
// White cannot be named: naming only white leaves nothing to trace.
|
||
assert!(trace(&img, &TraceParams { logo_colors: vec!["#ffffff".into()], ..logo_params() }).paths.is_empty());
|
||
}
|
||
|
||
#[test]
|
||
fn a_small_accent_colour_is_not_lost() {
|
||
let img = render(100, 100, 0.6, |x, y| {
|
||
if (x - 50.0).hypot(y - 50.0) <= 40.0 {
|
||
Some(BLUE)
|
||
} else if (x - 90.0).hypot(y - 90.0) <= 3.5 {
|
||
Some(RED)
|
||
} else {
|
||
None
|
||
}
|
||
});
|
||
let res = trace(&img, &logo_params());
|
||
assert!(res.paths.iter().any(|p| near(p.color, RED, 12)), "palette {:?}", res.palette);
|
||
assert!(res.paths.iter().any(|p| near(p.color, BLUE, 12)));
|
||
}
|
||
|
||
#[test]
|
||
fn a_hard_edged_source_still_gets_smooth_circles() {
|
||
// No anti-aliasing at all: every pixel is exactly one colour.
|
||
let img = Raster::from_fn(80, 80, |x, y| {
|
||
let d = (f64::from(x) + 0.5 - 40.0).hypot(f64::from(y) + 0.5 - 40.0);
|
||
if (10.0..=28.0).contains(&d) { [RED[0], RED[1], RED[2], 255] } else { [0, 0, 0, 0] }
|
||
});
|
||
let res = trace(&img, &TraceParams { ignore_white: true, ..logo_params() });
|
||
assert!(res.anchor_count() <= 40, "{} anchors for two circles from a hard-edged source", res.anchor_count());
|
||
let errs: Vec<f64> = samples(&res)
|
||
.iter()
|
||
.map(|p| {
|
||
let d = p.distance(Point::new(40.0, 40.0));
|
||
(d - 28.0).abs().min((d - 10.0).abs())
|
||
})
|
||
.collect();
|
||
let mean = errs.iter().sum::<f64>() / errs.len() as f64;
|
||
assert!(mean < 0.3 && errs.iter().copied().fold(0.0, f64::max) < 0.8, "mean {mean:.3}");
|
||
}
|
||
|
||
#[test]
|
||
fn random_small_images_and_settings_never_panic() {
|
||
let mut state = 0x2545_f491_4f6c_dd1du64;
|
||
let mut next = move || {
|
||
state ^= state << 13;
|
||
state ^= state >> 7;
|
||
state ^= state << 17;
|
||
state
|
||
};
|
||
let weird = [f64::NAN, f64::INFINITY, -3.0, 0.0, 35.0, 60.0, 100.0, 1e12];
|
||
let colours = [[210u8, 40, 50], [40, 80, 200], [255, 255, 255], [20, 160, 70], [0, 0, 0]];
|
||
for _ in 0..250 {
|
||
let (w, h) = ((next() % 24 + 1) as u32, (next() % 24 + 1) as u32);
|
||
let mut rgba = Vec::with_capacity((w * h * 4) as usize);
|
||
for y in 0..h {
|
||
for x in 0..w {
|
||
let r = next();
|
||
let c = colours[((u64::from(x) / 3 + u64::from(y) / 4 + r % 2) % 5) as usize];
|
||
rgba.extend_from_slice(&match r % 11 {
|
||
0 => [0, 0, 0, 0],
|
||
1 => [(r >> 8) as u8, (r >> 16) as u8, (r >> 24) as u8, (r >> 32) as u8],
|
||
_ => [c[0], c[1], c[2], 255],
|
||
});
|
||
}
|
||
}
|
||
let img = Raster::new(w, h, rgba);
|
||
let p = TraceParams {
|
||
paths: weird[(next() % 8) as usize],
|
||
corners: weird[(next() % 8) as usize],
|
||
noise: [0, 1, 6, 1000, u32::MAX][(next() % 5) as usize],
|
||
colors: [0, 2, 6, 40][(next() % 4) as usize],
|
||
ignore_white: next() % 2 == 0,
|
||
snap_curves_to_lines: next() % 2 == 0,
|
||
..logo_params()
|
||
};
|
||
if let Ok(res) = trace_within(&img, &p, 20_000) {
|
||
assert_sane(&res, w, h);
|
||
}
|
||
}
|
||
}
|
||
|
||
/// A scan's paper is darker in one corner than in another: it is still one colour, not a band of
|
||
/// paths for each tone of it.
|
||
#[test]
|
||
fn uneven_paper_is_one_colour() {
|
||
let img = Raster::from_fn(160, 120, |x, y| {
|
||
let shade = 1.0 - 0.1 * f64::from(x) / 160.0 - 0.04 * (f64::from(y) / 40.0).sin().abs();
|
||
let ink = (40..120).contains(&x) && (30..90).contains(&y);
|
||
let c = if ink { [40.0, 60.0, 150.0] } else { [236.0, 230.0, 214.0] };
|
||
[(c[0] * shade) as u8, (c[1] * shade) as u8, (c[2] * shade) as u8, 255]
|
||
});
|
||
let res = trace(&img, &crate::preset("Flat Logo").unwrap());
|
||
assert_eq!(res.paths.len(), 2, "{:?}", res.paths.iter().map(|p| (p.color, p.pixels)).collect::<Vec<_>>());
|
||
}
|