mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-10-25 10:17:02 -06:00
Image Trace: grayscale gradients use grey stops
In Grayscale mode with Gradients on, the trace could fill paths with coloured linear gradients (for example a blue-to-orange ramp) beside grey flat fills. Gradient detection ran on the RGB image, so the stops and the gradient's palette colour (its middle) were the source colours, while Grayscale's flat fills come from each pixel's luma. Grayscale now finds gradients, and settles their edges, in a copy of the image reduced to the same Rec. 601 luma the flat grays are quantised from. Every channel of that copy is equal, so the fitted stops, the ends carried to the area's edges and the palette entry are all grey. Color mode is unchanged: it still detects on the RGB image (trace output for gradient fixtures is byte-identical before and after). Adds grayscale_gradients_are_grey: a code-drawn sky ramp over flat shapes with a ramp rectangle, traced in Grayscale with 4 grays and Gradients on, must still produce a gradient, and every path colour, gradient stop and palette entry must be grey. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018tBcdwNhptqtmX32JCGFzB
This commit is contained in:
committed by
bflatastic
co-authored by
Claude Opus 5.5
parent
5d8b5ddc6c
commit
dbcd32d21a
+14
-2
@@ -291,6 +291,7 @@ pub struct TraceParams {
|
||||
pub auto_grouping: bool,
|
||||
/// Gradients (Grayscale and Color): an area whose colour changes evenly along one direction
|
||||
/// is one path filled with a linear gradient ([`TracedPath::gradient`]), not bands of colour.
|
||||
/// Grayscale finds them in the image's grays, and their stops are gray.
|
||||
pub gradients: bool,
|
||||
/// Create: trace areas as filled shapes.
|
||||
pub fills: bool,
|
||||
@@ -527,7 +528,17 @@ fn trace_paths(img: &Raster, params: &TraceParams, max_anchors: usize) -> Result
|
||||
let smoothed = (params.mode != Mode::BlackAndWhite && quantize::continuous_tone(img)).then(|| smooth::smoothed(img));
|
||||
let img = smoothed.as_ref().unwrap_or(img);
|
||||
// Gradients are found first and kept out of the palette: no colour is spent on bands of them.
|
||||
let found = (params.gradients && params.mode != Mode::BlackAndWhite).then(|| gradient::detect(img)).filter(|f| !f.1.is_empty());
|
||||
// Grayscale finds them in the image's grays (as its flat colours are), so their stops are gray.
|
||||
let grays = (params.gradients && params.mode == Mode::Grayscale).then(|| {
|
||||
let mut grays = img.clone();
|
||||
for p in grays.rgba.as_chunks_mut::<4>().0 {
|
||||
let g = quantize::luma(*p);
|
||||
p[..3].fill(g);
|
||||
}
|
||||
grays
|
||||
});
|
||||
let traced_as = grays.as_ref().unwrap_or(img);
|
||||
let found = (params.gradients && params.mode != Mode::BlackAndWhite).then(|| gradient::detect(traced_as)).filter(|f| !f.1.is_empty());
|
||||
let without = found.as_ref().map(|(of, _)| {
|
||||
let mut rest = img.clone();
|
||||
for (p, &g) in rest.rgba.as_chunks_mut::<4>().0.iter_mut().zip(of) {
|
||||
@@ -542,7 +553,7 @@ fn trace_paths(img: &Raster, params: &TraceParams, max_anchors: usize) -> Result
|
||||
// Each gradient is a palette entry of its own (its colour: the gradient's middle).
|
||||
let mut gradients: Vec<Option<TracedGradient>> = vec![None; q.palette.len()];
|
||||
if let Some((mut of, found)) = found {
|
||||
gradient::settle_edges(img, &mut of, &found, &mut q.labels);
|
||||
gradient::settle_edges(traced_as, &mut of, &found, &mut q.labels);
|
||||
let first = q.palette.len();
|
||||
for (l, &g) in q.labels.iter_mut().zip(&of) {
|
||||
if g != u32::MAX {
|
||||
@@ -552,6 +563,7 @@ fn trace_paths(img: &Raster, params: &TraceParams, max_anchors: usize) -> Result
|
||||
q.palette.extend(found.iter().map(|g| g.color_at(g.start.midpoint(g.end)).map(|v| v.round().clamp(0.0, 255.0) as u8)));
|
||||
gradients.extend(found.into_iter().map(Some));
|
||||
}
|
||||
drop(grays);
|
||||
denoise(&mut q.labels, w, h, params.noise as usize);
|
||||
// Pixel count per palette entry, and the box its pixels lie in.
|
||||
let mut counts = vec![0usize; q.palette.len()];
|
||||
|
||||
@@ -14,7 +14,8 @@ pub struct Quantized {
|
||||
pub palette: Vec<[u8; 3]>,
|
||||
}
|
||||
|
||||
fn luma(p: [u8; 4]) -> u8 {
|
||||
/// A pixel's gray (Rec. 601 luma): what Grayscale traces it as.
|
||||
pub(crate) fn luma(p: [u8; 4]) -> u8 {
|
||||
((p[0] as u32 * 299 + p[1] as u32 * 587 + p[2] as u32 * 114 + 500) / 1000) as u8
|
||||
}
|
||||
|
||||
|
||||
@@ -1295,3 +1295,36 @@ fn stroke_caps_are_square_or_round_as_the_ink_is() {
|
||||
assert!(line(true));
|
||||
assert!(!line(false));
|
||||
}
|
||||
|
||||
/// A blue-to-orange sky over flat colours, and a green-to-magenta ramp in a rectangle on them.
|
||||
fn sky_and_ramp() -> Raster {
|
||||
Raster::from_fn(240, 160, |x, y| {
|
||||
let (fx, fy) = (f64::from(x), f64::from(y));
|
||||
let mix = |a: [f64; 3], b: [f64; 3], t: f64| [0, 1, 2].map(|k| (a[k] + (b[k] - a[k]) * t).round() as u8);
|
||||
if (20..100).contains(&x) && (100..150).contains(&y) {
|
||||
let [r, g, b] = mix([40.0, 200.0, 60.0], [220.0, 40.0, 200.0], (fx - 20.0) / 80.0);
|
||||
[r, g, b, 255]
|
||||
} else if y >= 90 {
|
||||
if x >= 140 { [200, 30, 40, 255] } else { [30, 120, 50, 255] }
|
||||
} else {
|
||||
let [r, g, b] = mix([30.0, 60.0, 200.0], [250.0, 140.0, 30.0], fy / 90.0);
|
||||
[r, g, b, 255]
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn grayscale_gradients_are_grey() {
|
||||
let img = sky_and_ramp();
|
||||
let params = TraceParams { mode: Mode::Grayscale, colors: 4, gradients: true, ..TraceParams::default() };
|
||||
let res = trace(&img, ¶ms);
|
||||
let grey = |c: [u8; 3]| c[0] == c[1] && c[1] == c[2];
|
||||
assert!(res.paths.iter().any(|p| p.gradient.is_some()), "a gradient is still found");
|
||||
for p in &res.paths {
|
||||
assert!(grey(p.color), "{:?}", p.color);
|
||||
for stop in p.gradient.iter().flat_map(|g| &g.stops) {
|
||||
assert!(grey(stop.1), "{:?}", p.gradient);
|
||||
}
|
||||
}
|
||||
assert!(res.palette.iter().all(|&c| grey(c)), "{:?}", res.palette);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user