mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-10-24 09:46:59 -06:00
Merge pull request #2443 from dotCooCoo/fix/smart-blur-radius-edge-search
Effects: Smart Blur's Radius widens the edge search in Edge Only and Overlay Edge
This commit is contained in:
@@ -354,7 +354,7 @@ pub fn effect_catalog() -> Vec<EffectInfo> {
|
||||
"blur.smart",
|
||||
"Smart Blur…",
|
||||
BLUR,
|
||||
"{radius: pt 0.1..100 (3), threshold: levels 0.1..100 (25; only colours closer than this blur together, so edges stay sharp), quality: \"low\"|\"medium\"|\"high\" (\"medium\"; 5, 7 or 9 samples across), blurMode: \"normal\"|\"edgeOnly\"|\"overlayEdge\" (\"normal\"; edgeOnly: the edges in white on black, overlayEdge: the edges in white over the blur)}. A pixel is on an edge when one of its four nearest pixels differs from it by more than the threshold in a channel and its channels (alpha included) add up to at least that pixel's; the white and the black keep each pixel's coverage, and the radius changes only the blur",
|
||||
"{radius: pt 0.1..100 (3), threshold: levels 0.1..100 (25; only colours closer than this blur together, so edges stay sharp), quality: \"low\"|\"medium\"|\"high\" (\"medium\"; 5, 7 or 9 samples across), blurMode: \"normal\"|\"edgeOnly\"|\"overlayEdge\" (\"normal\"; edgeOnly: the edges in white on black, overlayEdge: the edges in white over the blur)}. A pixel is on an edge when one of the pixels the search reads within the radius (all of them for a small radius, a grid of samples for a wide one) or one of its four nearest pixels differs from it by more than the threshold in a channel and its channels (alpha included) add up to at least that pixel's, so a step gives a line about as wide as the radius, and at least one pixel wide, on the side with the larger sum; the white and the black keep each pixel's coverage",
|
||||
json!({"radius": 3.0, "threshold": 25.0, "quality": "medium", "blurMode": "normal"}),
|
||||
),
|
||||
r(
|
||||
|
||||
@@ -102,6 +102,40 @@ fn close(a: &Px16, b: &Px16, t: i32) -> bool {
|
||||
a.iter().zip(b).all(|(a, b)| (*a as i32 - *b as i32).abs() <= t)
|
||||
}
|
||||
|
||||
/// The offsets Smart Blur looks at around a pixel out to about `r` pixels (up to round(`r`)),
|
||||
/// with the radius of the box blur of the copy they are read from (0: the pixels themselves):
|
||||
/// every pixel of the square of side 2 · round(`r`) + 1, or `samples` × `samples` points spread
|
||||
/// over the square once it is wider than `samples` pixels, read from a copy box-blurred over the
|
||||
/// gaps between them. The callers keep the offsets inside their disc.
|
||||
fn smart_grid(r: f64, samples: u32) -> (Vec<Vec2>, usize) {
|
||||
let n = samples.clamp(3, 15) as f64;
|
||||
let k = r.round();
|
||||
if 2.0 * k < n {
|
||||
let k = k as i32;
|
||||
((-k..=k).flat_map(|dy| (-k..=k).map(move |dx| Vec2::new(dx as f64, dy as f64))).collect(), 0)
|
||||
} else {
|
||||
let gap = 2.0 * r / (n - 1.0);
|
||||
let grid = (0..n as i32).map(|i| -r + gap * i as f64);
|
||||
(grid.clone().flat_map(|dy| grid.clone().map(move |dx| Vec2::new(dx.round(), dy.round()))).collect(), (gap / 2.0).floor() as usize)
|
||||
}
|
||||
}
|
||||
|
||||
/// How far from a pixel Smart Blur's edge modes ([`smart_edges`]) read, in the units of `radius`:
|
||||
/// `radius`, and the box blur of the copy that a wide radius's offsets are read from.
|
||||
pub(super) fn edge_reach(radius: f64, samples: u32) -> f64 {
|
||||
radius + radius / (samples.clamp(3, 15) as f64 - 1.0)
|
||||
}
|
||||
|
||||
/// `src` (rows of `w` pixels) box-blurred with radius `prefilter`, the copy that the offsets of
|
||||
/// [`smart_grid`] are read from; `None` when they are read from `src` itself.
|
||||
fn smart_near(src: &[Px16], w: usize, prefilter: usize) -> Option<Vec<Px16>> {
|
||||
(prefilter > 0).then(|| {
|
||||
let mut b = src.to_vec();
|
||||
box_blur(&mut b, w, prefilter);
|
||||
b
|
||||
})
|
||||
}
|
||||
|
||||
/// Smart Blur (Normal mode): each pixel averaged with itself and the neighbours within `radius`
|
||||
/// pixels whose premultiplied channels all lie within `threshold` levels of its own. Wide radii
|
||||
/// look at `samples` × `samples` neighbours spread over the disc, read from a copy box-blurred
|
||||
@@ -113,28 +147,12 @@ pub(super) fn smart(px: &mut [[u8; 4]], w: usize, h: usize, radius: f64, thresho
|
||||
return;
|
||||
}
|
||||
let r = radius.min(MAX_SMART);
|
||||
let n = samples.clamp(3, 15) as f64;
|
||||
let k = r.round();
|
||||
let (offsets, prefilter): (Vec<Vec2>, usize) = if 2.0 * k < n {
|
||||
let k = k as i32;
|
||||
((-k..=k).flat_map(|dy| (-k..=k).map(move |dx| Vec2::new(dx as f64, dy as f64))).collect(), 0)
|
||||
} else {
|
||||
let gap = 2.0 * r / (n - 1.0);
|
||||
let grid = (0..n as i32).map(|i| -r + gap * i as f64);
|
||||
(grid.clone().flat_map(|dy| grid.clone().map(move |dx| Vec2::new(dx.round(), dy.round()))).collect(), (gap / 2.0).floor() as usize)
|
||||
};
|
||||
let (offsets, prefilter) = smart_grid(r, samples);
|
||||
let offsets: Vec<(isize, isize)> =
|
||||
offsets.into_iter().filter(|o| o.hypot() <= r + 0.5 && *o != Vec2::ZERO).map(|o| (o.x as isize, o.y as isize)).collect();
|
||||
let src = to16(px);
|
||||
let blurred;
|
||||
let near: &[Px16] = if prefilter > 0 {
|
||||
let mut b = src.clone();
|
||||
box_blur(&mut b, w, prefilter);
|
||||
blurred = b;
|
||||
&blurred
|
||||
} else {
|
||||
&src
|
||||
};
|
||||
let blurred = smart_near(&src, w, prefilter);
|
||||
let near = blurred.as_deref().unwrap_or(&src);
|
||||
let t = levels(threshold);
|
||||
for (y, (row, out)) in src.chunks_exact(w).zip(px.chunks_exact_mut(w)).enumerate().take(h) {
|
||||
for (x, (c, o)) in row.iter().zip(out.iter_mut()).enumerate() {
|
||||
@@ -162,33 +180,41 @@ pub(super) fn smart(px: &mut [[u8; 4]], w: usize, h: usize, radius: f64, thresho
|
||||
|
||||
/// Smart Blur's Edge Only (`overlay` false) and Overlay Edge (`overlay` true) modes: the pixels on
|
||||
/// an edge drawn white, over black or over the Normal mode's result ([`smart`]). A pixel is on an
|
||||
/// edge when one of its four nearest neighbors (transparent beyond the raster) has a premultiplied
|
||||
/// channel more than `threshold` levels from its own and its channels add up to at least the
|
||||
/// neighbor's, so a step between two colors gives a line one pixel wide on the side whose channels
|
||||
/// add up to more, and two pixels wide when the sums are equal. The white and the black keep the
|
||||
/// pixel's coverage.
|
||||
/// edge when one of the pixels the search reads within `radius` pixels of it (transparent beyond
|
||||
/// the raster) has a premultiplied channel more than `threshold` levels from its own and its
|
||||
/// channels add up to at least that pixel's. The search reads the Normal mode's offsets ([`smart_grid`]) for the whole
|
||||
/// pixels of `radius`, kept within `radius`, from the pixels and, for a wide radius, from the
|
||||
/// box-blurred copy, and always the four nearest pixels. A straight step between two colors gives
|
||||
/// a line on the side whose channels add up to more, about `radius` wide (the whole pixels of
|
||||
/// `radius` while every pixel within it is read, and at least one pixel), and lines on both sides
|
||||
/// when the sums are equal. The white and the black keep the pixel's coverage.
|
||||
pub(super) fn smart_edges(px: &mut [[u8; 4]], w: usize, h: usize, radius: f64, threshold: f64, samples: u32, overlay: bool) {
|
||||
let src = to16(px);
|
||||
let t = levels(threshold);
|
||||
let sum = |p: &Px16| p.iter().map(|v| u32::from(*v)).sum::<u32>();
|
||||
let at = |x: usize, y: usize, dx: isize, dy: isize| -> Px16 {
|
||||
let (Some(x), Some(y)) = (x.checked_add_signed(dx), y.checked_add_signed(dy)) else { return [0; 4] };
|
||||
if x >= w || y >= h {
|
||||
return [0; 4];
|
||||
let r = if radius.is_nan() { 0.0 } else { radius.min(MAX_SMART) };
|
||||
// Spread over whole pixels, so that the outermost offsets lie within `r` and are kept.
|
||||
let (offsets, prefilter) = smart_grid(r.floor(), samples);
|
||||
let wide: Vec<(isize, isize)> = offsets.into_iter().filter(|o| o.hypot() > 1.0 && o.hypot() <= r).map(|o| (o.x as isize, o.y as isize)).collect();
|
||||
let nearest = [(1, 0), (-1, 0), (0, 1), (0, -1)];
|
||||
let blurred = smart_near(&src, w, prefilter);
|
||||
let mut edge = vec![false; src.len()];
|
||||
for (y, (row, out)) in src.chunks_exact(w).zip(edge.chunks_exact_mut(w)).enumerate().take(h) {
|
||||
for (x, (c, e)) in row.iter().zip(out.iter_mut()).enumerate() {
|
||||
let own = sum(c);
|
||||
let differs = |n: &Px16| !close(n, c, t) && own >= sum(n);
|
||||
// The pixel at the offset and, with `copy`, its blurred copy; transparent beyond the raster.
|
||||
let found = |(dx, dy): &(isize, isize), copy: bool| {
|
||||
let (sx, sy) = (x as isize + dx, y as isize + dy);
|
||||
if sx < 0 || sy < 0 || sx as usize >= w || sy as usize >= h {
|
||||
return differs(&[0; 4]);
|
||||
}
|
||||
let i = sy as usize * w + sx as usize;
|
||||
src.get(i).is_some_and(differs) || (copy && blurred.as_deref().and_then(|b| b.get(i)).is_some_and(differs))
|
||||
};
|
||||
*e = nearest.iter().any(|o| found(o, false)) || wide.iter().any(|o| found(o, true));
|
||||
}
|
||||
src.get(y * w + x).copied().unwrap_or([0; 4])
|
||||
};
|
||||
let edge: Vec<bool> = src
|
||||
.iter()
|
||||
.enumerate()
|
||||
.map(|(i, c)| {
|
||||
let (x, y) = (i % w.max(1), i / w.max(1));
|
||||
[(1, 0), (-1, 0), (0, 1), (0, -1)].iter().any(|(dx, dy)| {
|
||||
let n = at(x, y, *dx, *dy);
|
||||
!close(&n, c, t) && sum(c) >= sum(&n)
|
||||
})
|
||||
})
|
||||
.collect();
|
||||
}
|
||||
if overlay {
|
||||
smart(px, w, h, radius, threshold, samples);
|
||||
}
|
||||
|
||||
@@ -84,9 +84,8 @@ pub enum PixelFx {
|
||||
/// Blur › Smart Blur (Normal mode): each pixel averaged with the neighbours within `radius`
|
||||
/// whose colour differs from its own by no more than `threshold` levels, over a grid of
|
||||
/// `samples` × `samples` neighbours at most.
|
||||
/// The Edge Only `mode` draws the edges where neighboring pixels differ by more than
|
||||
/// `threshold` levels in white on black, and Overlay Edge draws them in white over the Normal
|
||||
/// result.
|
||||
/// The Edge Only `mode` draws the edges that `threshold` and `radius` find (`blur::smart_edges`)
|
||||
/// in white on black, and Overlay Edge draws them in white over the Normal result.
|
||||
SmartBlur { radius: f64, threshold: f64, samples: u32, mode: SmartBlurMode },
|
||||
/// Brush Strokes › Accented Edges: the edges, about `width` (1..14) wide, accented towards
|
||||
/// white chalk or black ink as `brightness` (0..50) says; `smoothness` (1..15) softens them.
|
||||
@@ -388,7 +387,8 @@ impl PixelFx {
|
||||
pub fn reach(&self) -> Option<f64> {
|
||||
match *self {
|
||||
PixelFx::RadialBlur { .. } => None,
|
||||
PixelFx::SmartBlur { radius, .. } => Some(radius),
|
||||
PixelFx::SmartBlur { radius, mode: SmartBlurMode::Normal, .. } => Some(radius),
|
||||
PixelFx::SmartBlur { radius, samples, .. } => Some(blur::edge_reach(radius, samples)),
|
||||
PixelFx::AccentedEdges { width, smoothness, .. } => Some(brushstrokes::accent_reach(width, smoothness)),
|
||||
// The strokes, and the softened tones that choose them.
|
||||
PixelFx::AngledStrokes { length, .. } => Some(brushstrokes::stroke_reach(length).max(brushstrokes::TONE_REACH)),
|
||||
|
||||
@@ -500,6 +500,9 @@ fn outsets_follow_the_object() {
|
||||
assert!((spin.outset(b) - (far - 25.0)).abs() < 1e-9);
|
||||
assert!(fx("blur.radial", json!({"method": "zoom", "amount": 100})).outset(b) > 0.4 * far);
|
||||
assert_eq!(fx("blur.smart", json!({"radius": 4})).outset(b), 4.0);
|
||||
assert_eq!(fx("blur.smart", json!({"radius": 4})).reach(), Some(4.0));
|
||||
// The edge modes also read the copy blurred over a wide radius's samples.
|
||||
assert_eq!(fx("blur.smart", json!({"radius": 4, "quality": "low", "blurMode": "edgeOnly"})).reach(), Some(5.0));
|
||||
assert_eq!(fx("sharpen.unsharpMask", json!({})).outset(b), 0.0);
|
||||
assert_eq!(spin.reach(), None);
|
||||
assert_eq!(fx("sharpen.unsharpMask", json!({"radius": 2})).reach(), Some(6.0));
|
||||
@@ -656,8 +659,8 @@ fn smart_blur_normal_mode_keeps_its_pixels() {
|
||||
|
||||
/// Smart Blur's test image: grays 56 and 64 in a checkerboard for x < 3 and 66 and 74 for x in 3..6
|
||||
/// (steps of 2 or 18 levels at x = 3, under the threshold), gray 200 from x = 6 on (steps of 126 or
|
||||
/// 134 levels). Its edges at threshold 25 are the pixels on the raster's border (transparency is
|
||||
/// beyond it) and column 6, the step's lighter side.
|
||||
/// 134 levels). Its edges at threshold 25 and radius 1 are the pixels on the raster's border
|
||||
/// (transparency is beyond it) and column 6, the step's lighter side.
|
||||
fn smart_blur_steps() -> (usize, usize, Vec<u8>, impl Fn(usize, usize) -> bool) {
|
||||
let (w, h) = (12, 8);
|
||||
let src = image(w, h, |x, y| match x {
|
||||
@@ -672,16 +675,16 @@ fn smart_blur_steps() -> (usize, usize, Vec<u8>, impl Fn(usize, usize) -> bool)
|
||||
fn smart_blur_edge_only_draws_the_edges_white_on_black() {
|
||||
let (w, h, src, edge) = smart_blur_steps();
|
||||
let mut d = src.clone();
|
||||
fx("blur.smart", json!({"blurMode": "edgeOnly"})).apply(&mut d, w, h, &space(w, h));
|
||||
fx("blur.smart", json!({"blurMode": "edgeOnly", "radius": 1})).apply(&mut d, w, h, &space(w, h));
|
||||
let want = image(w, h, |x, y| if edge(x, y) { [255; 4] } else { [0, 0, 0, 255] });
|
||||
assert_eq!(d, want);
|
||||
// The radius changes only the blur.
|
||||
let mut wide = src.clone();
|
||||
fx("blur.smart", json!({"blurMode": "edgeOnly", "radius": 40})).apply(&mut wide, w, h, &space(w, h));
|
||||
assert_eq!(wide, want);
|
||||
// Under one pixel the search still takes the four nearest pixels.
|
||||
let mut narrow = src.clone();
|
||||
fx("blur.smart", json!({"blurMode": "edgeOnly", "radius": 0.1})).apply(&mut narrow, w, h, &space(w, h));
|
||||
assert_eq!(narrow, want);
|
||||
// A threshold under the checkerboard's 8 levels makes its lighter squares edges too.
|
||||
let mut low = src.clone();
|
||||
fx("blur.smart", json!({"blurMode": "edgeOnly", "threshold": 5})).apply(&mut low, w, h, &space(w, h));
|
||||
fx("blur.smart", json!({"blurMode": "edgeOnly", "radius": 1, "threshold": 5})).apply(&mut low, w, h, &space(w, h));
|
||||
for y in 1..h - 1 {
|
||||
for x in 1..6 {
|
||||
let lighter = matches!(at(&src, w, x, y)[0], 64 | 74);
|
||||
@@ -692,7 +695,7 @@ fn smart_blur_edge_only_draws_the_edges_white_on_black() {
|
||||
let (w, h) = (8, 8);
|
||||
let shape = image(w, h, |x, y| if (2..6).contains(&x) && (2..6).contains(&y) { [64, 0, 0, 128] } else { [0; 4] });
|
||||
let mut s = shape.clone();
|
||||
fx("blur.smart", json!({"blurMode": "edgeOnly"})).apply(&mut s, w, h, &space(w, h));
|
||||
fx("blur.smart", json!({"blurMode": "edgeOnly", "radius": 1})).apply(&mut s, w, h, &space(w, h));
|
||||
let ring = |x: usize, y: usize| (2..6).contains(&x) && (2..6).contains(&y) && !((3..5).contains(&x) && (3..5).contains(&y));
|
||||
let inside = |x: usize, y: usize| (3..5).contains(&x) && (3..5).contains(&y);
|
||||
let want = image(w, h, |x, y| match (ring(x, y), inside(x, y)) {
|
||||
@@ -707,10 +710,10 @@ fn smart_blur_edge_only_draws_the_edges_white_on_black() {
|
||||
fn smart_blur_overlay_edge_draws_the_edges_white_over_the_blur() {
|
||||
let (w, h, src, edge) = smart_blur_steps();
|
||||
let mut normal = src.clone();
|
||||
fx("blur.smart", json!({})).apply(&mut normal, w, h, &space(w, h));
|
||||
fx("blur.smart", json!({"radius": 1})).apply(&mut normal, w, h, &space(w, h));
|
||||
assert_ne!(normal, src, "the checkerboard is blurred");
|
||||
let mut d = src.clone();
|
||||
fx("blur.smart", json!({"blurMode": "overlayEdge"})).apply(&mut d, w, h, &space(w, h));
|
||||
fx("blur.smart", json!({"blurMode": "overlayEdge", "radius": 1})).apply(&mut d, w, h, &space(w, h));
|
||||
for y in 0..h {
|
||||
for x in 0..w {
|
||||
let want = if edge(x, y) { [255; 4] } else { at(&normal, w, x, y) };
|
||||
@@ -726,6 +729,87 @@ fn smart_blur_overlay_edge_draws_the_edges_white_over_the_blur() {
|
||||
}
|
||||
}
|
||||
|
||||
/// The edges #2154 drew at any radius: a pixel is on an edge when one of its four nearest pixels
|
||||
/// (transparent beyond the raster) differs from it by more than `t` levels in a channel and its
|
||||
/// channels add up to at least that pixel's.
|
||||
fn four_neighbor_edges(src: &[u8], w: usize, h: usize, t: u8) -> Vec<bool> {
|
||||
let px = |x: isize, y: isize| if x < 0 || y < 0 || x as usize >= w || y as usize >= h { [0; 4] } else { at(src, w, x as usize, y as usize) };
|
||||
let sum = |p: [u8; 4]| p.iter().map(|v| u32::from(*v)).sum::<u32>();
|
||||
let mut edges = Vec::new();
|
||||
for y in 0..h as isize {
|
||||
for x in 0..w as isize {
|
||||
let c = px(x, y);
|
||||
edges.push([(1, 0), (-1, 0), (0, 1), (0, -1)].iter().any(|(dx, dy)| {
|
||||
let n = px(x + dx, y + dy);
|
||||
c.iter().zip(&n).any(|(a, b)| a.abs_diff(*b) > t) && sum(c) >= sum(n)
|
||||
}));
|
||||
}
|
||||
}
|
||||
edges
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn smart_blur_edges_at_radius_one_are_the_four_neighbor_edges() {
|
||||
let (w, h) = (48, 40);
|
||||
let src = image(w, h, |x, y| {
|
||||
let a = if x > 4 && y < 36 { 255 } else { 128 };
|
||||
[((x * 5 + y) as u8).min(a), ((y * 6) as u8).min(a), (((x * y) % 97) as u8).min(a), a]
|
||||
});
|
||||
for t in [5, 10, 25, 60] {
|
||||
let edges = four_neighbor_edges(&src, w, h, t);
|
||||
assert!(edges.contains(&true) && edges.contains(&false), "threshold {t}");
|
||||
let want: Vec<u8> = src.as_chunks::<4>().0.iter().zip(&edges).flat_map(|(p, e)| if *e { [p[3]; 4] } else { [0, 0, 0, p[3]] }).collect();
|
||||
for radius in [0.1, 0.5, 1.0] {
|
||||
let mut d = src.clone();
|
||||
fx("blur.smart", json!({"blurMode": "edgeOnly", "radius": radius, "threshold": t})).apply(&mut d, w, h, &space(w, h));
|
||||
assert_eq!(d, want, "threshold {t}, radius {radius}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn smart_blur_radius_widens_the_edge_search() {
|
||||
// Gray 60 left of x = 15 and gray 200 from it on: a step of 140 levels.
|
||||
let (w, h) = (30, 16);
|
||||
let src = image(w, h, |x, _| grey(if x < 15 { 60 } else { 200 }));
|
||||
for radius in [1.0, 1.5, 2.0, 2.5, 3.0, 3.5, 4.0, 4.5] {
|
||||
let mut d = src.clone();
|
||||
fx("blur.smart", json!({"blurMode": "edgeOnly", "radius": radius})).apply(&mut d, w, h, &space(w, h));
|
||||
// The pixels within `radius` of a darker one: the step's first `r` columns on its lighter
|
||||
// side, and a band `r` wide along the raster's border (transparency is beyond it). Up to
|
||||
// radius 4.5 the search reads no blurred copy: at 4 and 4.5 the medium quality's 7 samples
|
||||
// across are 4/3 pixels apart, under the 2 pixels a box blur of radius 1 needs.
|
||||
let r = radius as usize;
|
||||
let want =
|
||||
image(w, h, |x, y| if x < r || y < r || x >= w - r || y >= h - r || (15..15 + r).contains(&x) { [255; 4] } else { [0, 0, 0, 255] });
|
||||
assert_eq!(d, want, "radius {radius}");
|
||||
}
|
||||
// Overlay Edge draws the same edges over the blur.
|
||||
let mut normal = src.clone();
|
||||
fx("blur.smart", json!({"radius": 3})).apply(&mut normal, w, h, &space(w, h));
|
||||
let mut over = src.clone();
|
||||
fx("blur.smart", json!({"blurMode": "overlayEdge", "radius": 3})).apply(&mut over, w, h, &space(w, h));
|
||||
assert_eq!(at(&over, w, 17, 8), [255; 4]);
|
||||
assert_eq!(at(&over, w, 18, 8), at(&normal, w, 18, 8));
|
||||
// A wide radius reads a grid of samples, also through a blurred copy, and finds the step from
|
||||
// the radius away, on its lighter side only.
|
||||
let (w, h) = (260, 100);
|
||||
let src = image(w, h, |x, _| grey(if x < 100 { 60 } else { 200 }));
|
||||
let mut d = src.clone();
|
||||
fx("blur.smart", json!({"blurMode": "edgeOnly", "radius": 40})).apply(&mut d, w, h, &space(w, h));
|
||||
for (x, edge) in [(100, true), (130, true), (139, true), (170, false), (99, false), (70, false)] {
|
||||
assert_eq!(at(&d, w, x, 50)[0] == 255, edge, "({x}, 50)");
|
||||
}
|
||||
// A dark band narrower than the gaps between the samples is found through the blurred copy:
|
||||
// at x = 120 the samples fall on x = 107, 93 and 80, all light.
|
||||
let src = image(w, h, |x, _| grey(if (100..104).contains(&x) { 60 } else { 200 }));
|
||||
let mut d = src.clone();
|
||||
fx("blur.smart", json!({"blurMode": "edgeOnly", "radius": 40})).apply(&mut d, w, h, &space(w, h));
|
||||
for (x, edge) in [(104, true), (120, true), (102, false)] {
|
||||
assert_eq!(at(&d, w, x, 50)[0] == 255, edge, "band ({x}, 50)");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn unsharp_mask_overshoots_at_edges() {
|
||||
let (w, h) = (40, 10);
|
||||
|
||||
@@ -684,7 +684,7 @@ Moved from the ROADMAP's "Shipped so far" on 2026-10-10. Update it when a featur
|
||||
|
||||
- **Texture redraws (#1257):** Craquelure, Mosaic Tiles and Stained Glass compare squared distances when finding their nearest cells; rounding/tie boundaries use the original distance ordering. Brute-force candidate tests and three 1800×1500 viewport comparisons preserve the old pixels. A CMYK rectangle move at 190.07%/DPI 1.5 is reproducible with `cargo test -p vectorcraft-render texture_drag_perf -- --ignored --nocapture`; idle-machine Windows interaction is still unverified.
|
||||
|
||||
- **Smart Blur modes:** Effect › Blur › Smart Blur… has a Mode menu with Normal, Edge Only and Overlay Edge (`blurMode`: `normal`, `edgeOnly` or `overlayEdge`; an effect saved without it draws as Normal). Edge Only draws the edges in white on black, and Overlay Edge draws them in white over the Normal blur. A pixel is on an edge when one of its four nearest pixels differs from it by more than Threshold in a channel and its channels, alpha included, add up to at least that pixel's, so a color step gives a line one pixel wide on the side with the larger sum (two pixels when the sums are equal). The white and the black keep each pixel's coverage, and Radius changes only the blur. Behavior reference: [Summary of effects](https://helpx.adobe.com/illustrator/using/summary-effects.html).
|
||||
- **Smart Blur modes:** Effect › Blur › Smart Blur… has a Mode menu with Normal, Edge Only and Overlay Edge (`blurMode`: `normal`, `edgeOnly` or `overlayEdge`; an effect saved without it draws as Normal). Edge Only draws the edges in white on black, and Overlay Edge draws them in white over the Normal blur. A pixel is on an edge when one of the pixels the search reads within Radius of it differs from it by more than Threshold in a channel and its channels, alpha included, add up to at least that pixel's. The search reads neighbors as the Normal blur does, kept within Radius, and always the four nearest pixels, so a color step gives a line about as wide as Radius, and at least one pixel wide, on the side with the larger sum (lines on both sides when the sums are equal). The white and the black keep each pixel's coverage. Behavior reference: [Summary of effects](https://helpx.adobe.com/illustrator/using/summary-effects.html).
|
||||
|
||||
- **Moving objects with raster effects (#1257):** during a drag or a dialog's preview, an object whose content only moved draws its cached drop shadows, outer glows and filtered content (Gaussian Blur, Feather and the Photoshop-style filters) from the rasters of an earlier frame at the nearest whole pixel, without running the effects again. The renderer first draws the moved content where the old content lies in its raster and compares the pixels. De-Interlace runs again. The frame after the drag runs the effects again and draws what a fresh renderer draws; on the desktop, the canvas renders that frame on the render worker. `texture_drag_perf` times a drag's frames and the frame after it.
|
||||
|
||||
|
||||
Reference in New Issue
Block a user