mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-10-25 02:07:09 -06:00
Effects: Smart Blur's Mode menu has Edge Only and Overlay Edge
Effect › Blur › Smart Blur had only its Normal mode: the dialog and
blur.smart took Radius, Threshold and Quality and drew the blur.
Smart Blur now takes a `blurMode` param ("normal", "edgeOnly" or
"overlayEdge"; "normal" by default), which its dialog shows as a Mode
menu. 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 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. Normal draws the same pixels as before, and an effect saved
without `blurMode` draws as Normal.
Docs: docs/effects-parity.md, docs/gaps.md, docs/target-app-parity.md.
This commit is contained in:
@@ -345,8 +345,8 @@ 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)} (Normal mode)",
|
||||
json!({"radius": 3.0, "threshold": 25.0, "quality": "medium"}),
|
||||
"{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",
|
||||
json!({"radius": 3.0, "threshold": 25.0, "quality": "medium", "blurMode": "normal"}),
|
||||
),
|
||||
r(
|
||||
"brushStrokes.accentedEdges",
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
//! Blur › Radial Blur and Smart Blur.
|
||||
//! Blur › Radial Blur and Smart Blur (Normal, Edge Only and Overlay Edge).
|
||||
|
||||
use vectorcraft_geom::{Affine, Point, Vec2};
|
||||
|
||||
use super::{PixelSpace, Px16, box_blur, to8, to16};
|
||||
use super::{PixelSpace, Px16, box_blur, pick, to8, to16};
|
||||
|
||||
/// Zoom's total spread, in log scale: `amount` 100 scales the content between 1/√2 and √2.
|
||||
pub(super) fn zoom_extent(amount: f64) -> f64 {
|
||||
@@ -69,6 +69,39 @@ pub(super) fn radial(px: &mut [[u8; 4]], w: usize, h: usize, space: &PixelSpace,
|
||||
/// The largest radius (pixels) Smart Blur looks over: farther neighbours are as good as gone.
|
||||
const MAX_SMART: f64 = 4096.0;
|
||||
|
||||
/// Smart Blur's modes, in its Mode menu's order: (label, parameter value).
|
||||
pub const SMART_BLUR_MODES: [(&str, &str); 3] = [("Normal", "normal"), ("Edge Only", "edgeOnly"), ("Overlay Edge", "overlayEdge")];
|
||||
|
||||
/// What Smart Blur draws ([`SMART_BLUR_MODES`]).
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
pub enum SmartBlurMode {
|
||||
/// The object blurred.
|
||||
Normal,
|
||||
/// The edges in white on black.
|
||||
EdgeOnly,
|
||||
/// The edges in white over the object blurred.
|
||||
OverlayEdge,
|
||||
}
|
||||
|
||||
impl SmartBlurMode {
|
||||
const ALL: [SmartBlurMode; 3] = [SmartBlurMode::Normal, SmartBlurMode::EdgeOnly, SmartBlurMode::OverlayEdge];
|
||||
|
||||
/// The mode of `value` (a [`SMART_BLUR_MODES`] value, any case); Normal otherwise.
|
||||
pub fn parse(value: &str) -> Self {
|
||||
pick(&SMART_BLUR_MODES, &Self::ALL, value, SmartBlurMode::Normal)
|
||||
}
|
||||
}
|
||||
|
||||
/// Smart Blur's `threshold` (8-bit levels) in the levels of [`Px16`].
|
||||
fn levels(threshold: f64) -> i32 {
|
||||
(threshold.clamp(0.0, 255.0) * 257.0) as i32
|
||||
}
|
||||
|
||||
/// Do all of `a`'s channels lie within `t` levels of `b`'s?
|
||||
fn close(a: &Px16, b: &Px16, t: i32) -> bool {
|
||||
a.iter().zip(b).all(|(a, b)| (*a as i32 - *b as i32).abs() <= t)
|
||||
}
|
||||
|
||||
/// 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
|
||||
@@ -102,7 +135,7 @@ pub(super) fn smart(px: &mut [[u8; 4]], w: usize, h: usize, radius: f64, thresho
|
||||
} else {
|
||||
&src
|
||||
};
|
||||
let t = (threshold.clamp(0.0, 255.0) * 257.0) as i32;
|
||||
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() {
|
||||
let mut acc = c.map(u64::from);
|
||||
@@ -115,8 +148,7 @@ pub(super) fn smart(px: &mut [[u8; 4]], w: usize, h: usize, radius: f64, thresho
|
||||
let i = sy as usize * w + sx as usize;
|
||||
let (Some(s), Some(own)) = (near.get(i), src.get(i)) else { continue };
|
||||
// The neighbour itself and its surroundings both close enough: edges stay sharp.
|
||||
let close = |p: &Px16| p.iter().zip(c).all(|(a, b)| (*a as i32 - *b as i32).abs() <= t);
|
||||
if close(own) && close(s) {
|
||||
if close(own, c, t) && close(s, c, t) {
|
||||
for (a, v) in acc.iter_mut().zip(s) {
|
||||
*a += *v as u64;
|
||||
}
|
||||
@@ -127,3 +159,45 @@ 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.
|
||||
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];
|
||||
}
|
||||
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);
|
||||
}
|
||||
for ((p, c), on_edge) in px.iter_mut().zip(&src).zip(&edge) {
|
||||
let a = to8(c[3]);
|
||||
if *on_edge {
|
||||
*p = [a; 4];
|
||||
} else if !overlay {
|
||||
*p = [0, 0, 0, a];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -27,6 +27,7 @@ use vectorcraft_geom::{Affine, Point, Rect};
|
||||
|
||||
use crate::util::{flag, num, text};
|
||||
|
||||
pub use blur::{SMART_BLUR_MODES, SmartBlurMode};
|
||||
pub use brushstrokes::{STROKE_DIRECTIONS, StrokeDirection};
|
||||
pub use distort::{GLASS_TEXTURES, GlassTexture};
|
||||
pub use pixelate::{MEZZOTINT_TYPES, Mezzotint};
|
||||
@@ -77,7 +78,10 @@ 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.
|
||||
SmartBlur { radius: f64, threshold: f64, samples: u32 },
|
||||
/// 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.
|
||||
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.
|
||||
AccentedEdges { width: f64, brightness: f64, smoothness: f64 },
|
||||
@@ -211,6 +215,7 @@ pub(crate) fn parse(id: &str, p: &Value) -> Option<PixelFx> {
|
||||
radius: num(p, "radius", 3.0).clamp(0.1, 100.0),
|
||||
threshold: num(p, "threshold", 25.0).clamp(0.1, 100.0),
|
||||
samples: quality([("low", 5), ("medium", 7), ("high", 9)], 7),
|
||||
mode: SmartBlurMode::parse(text(p, "blurMode", "normal")),
|
||||
},
|
||||
"brushStrokes.accentedEdges" => PixelFx::AccentedEdges {
|
||||
width: num(p, "edgeWidth", 2.0).clamp(1.0, 14.0),
|
||||
@@ -412,7 +417,12 @@ impl PixelFx {
|
||||
match *self {
|
||||
PixelFx::FilmGrain { grain, highlight, intensity } => artistic::film_grain(px, w, h, space, grain, highlight, intensity),
|
||||
PixelFx::RadialBlur { amount, zoom, passes } => blur::radial(px, w, h, space, amount, zoom, passes),
|
||||
PixelFx::SmartBlur { radius, threshold, samples } => blur::smart(px, w, h, to_px(radius), threshold, samples),
|
||||
PixelFx::SmartBlur { radius, threshold, samples, mode: SmartBlurMode::Normal } => {
|
||||
blur::smart(px, w, h, to_px(radius), threshold, samples)
|
||||
}
|
||||
PixelFx::SmartBlur { radius, threshold, samples, mode } => {
|
||||
blur::smart_edges(px, w, h, to_px(radius), threshold, samples, mode == SmartBlurMode::OverlayEdge)
|
||||
}
|
||||
PixelFx::AccentedEdges { width, brightness, smoothness } => brushstrokes::accented_edges(px, w, h, space, width, brightness, smoothness),
|
||||
PixelFx::AngledStrokes { balance, length, sharpness } => brushstrokes::angled_strokes(px, w, h, space, balance, length, sharpness),
|
||||
PixelFx::Crosshatch { length, sharpness, strength } => brushstrokes::crosshatch(px, w, h, space, length, sharpness, strength),
|
||||
|
||||
@@ -465,11 +465,22 @@ fn params_take_defaults_and_stay_in_range() {
|
||||
PixelFx::RadialBlur { amount: 100.0, zoom: true, passes: 8 }
|
||||
);
|
||||
assert_eq!(fx("blur.radial", json!({"amount": -5, "quality": "nonsense"})), PixelFx::RadialBlur { amount: 1.0, zoom: false, passes: 6 });
|
||||
assert_eq!(fx("blur.smart", json!({})), PixelFx::SmartBlur { radius: 3.0, threshold: 25.0, samples: 7 });
|
||||
assert_eq!(fx("blur.smart", json!({})), PixelFx::SmartBlur { radius: 3.0, threshold: 25.0, samples: 7, mode: SmartBlurMode::Normal });
|
||||
assert_eq!(
|
||||
fx("blur.smart", json!({"radius": "1e999", "threshold": -1, "quality": "low"})),
|
||||
PixelFx::SmartBlur { radius: 3.0, threshold: 0.1, samples: 5 }
|
||||
fx("blur.smart", json!({"radius": "1e999", "threshold": -1, "quality": "low", "blurMode": "EDGEONLY"})),
|
||||
PixelFx::SmartBlur { radius: 3.0, threshold: 0.1, samples: 5, mode: SmartBlurMode::EdgeOnly }
|
||||
);
|
||||
assert_eq!(
|
||||
fx("blur.smart", json!({"blurMode": "overlayEdge"})),
|
||||
PixelFx::SmartBlur { radius: 3.0, threshold: 25.0, samples: 7, mode: SmartBlurMode::OverlayEdge }
|
||||
);
|
||||
assert_eq!(fx("blur.smart", json!({"blurMode": "overlay"})), fx("blur.smart", json!({})));
|
||||
// Every Mode menu value is documented in the catalogue and parses to a mode of its own.
|
||||
let doc = crate::effect_info("blur.smart").unwrap().params;
|
||||
for (i, (_, v)) in SMART_BLUR_MODES.iter().enumerate() {
|
||||
assert!(doc.contains(&format!("\"{v}\"")), "{v}");
|
||||
assert_eq!(SmartBlurMode::parse(v), [SmartBlurMode::Normal, SmartBlurMode::EdgeOnly, SmartBlurMode::OverlayEdge][i], "{v}");
|
||||
}
|
||||
assert_eq!(fx("sharpen.unsharpMask", json!({})), PixelFx::UnsharpMask { amount: 0.5, radius: 1.0, threshold: 0.0 });
|
||||
assert_eq!(
|
||||
fx("sharpen.unsharpMask", json!({"amount": 1e9, "radius": 0, "threshold": 1e9})),
|
||||
@@ -616,6 +627,105 @@ fn smart_blur_smooths_noise_and_keeps_edges() {
|
||||
assert_eq!(at(&all, w, 20, 10)[0], 255, "a 155-level step is still past 100");
|
||||
}
|
||||
|
||||
/// FNV-1a of `bytes`.
|
||||
fn digest(bytes: &[u8]) -> u64 {
|
||||
bytes.iter().fold(0xcbf2_9ce4_8422_2325, |h, b| (h ^ u64::from(*b)).wrapping_mul(0x0100_0000_01b3))
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn smart_blur_normal_mode_keeps_its_pixels() {
|
||||
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]
|
||||
});
|
||||
let mut sums = Vec::new();
|
||||
for p in [json!({}), json!({"radius": 2, "threshold": 10, "quality": "high"}), json!({"radius": 12, "threshold": 40, "quality": "low"})] {
|
||||
let mut d = src.clone();
|
||||
fx("blur.smart", p.clone()).apply(&mut d, w, h, &space(w, h));
|
||||
let mut normal = src.clone();
|
||||
let mut p = p;
|
||||
p["blurMode"] = json!("normal");
|
||||
fx("blur.smart", p).apply(&mut normal, w, h, &space(w, h));
|
||||
assert_eq!(d, normal);
|
||||
sums.push(digest(&d));
|
||||
}
|
||||
// The pixels Smart Blur gave before it had a mode.
|
||||
assert_eq!(sums, [11495339692959753241, 14281410954765351056, 5111933201350526354]);
|
||||
}
|
||||
|
||||
/// 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.
|
||||
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 {
|
||||
0..3 => grey(if (x + y) % 2 == 0 { 56 } else { 64 }),
|
||||
3..6 => grey(if (x + y) % 2 == 0 { 66 } else { 74 }),
|
||||
_ => grey(200),
|
||||
});
|
||||
(w, h, src, move |x, y| x == 0 || y == 0 || x == w - 1 || y == h - 1 || x == 6)
|
||||
}
|
||||
|
||||
#[test]
|
||||
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));
|
||||
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);
|
||||
// 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));
|
||||
for y in 1..h - 1 {
|
||||
for x in 1..6 {
|
||||
let lighter = matches!(at(&src, w, x, y)[0], 64 | 74);
|
||||
assert_eq!(at(&low, w, x, y)[0] == 255, lighter, "({x}, {y})");
|
||||
}
|
||||
}
|
||||
// White and black keep each pixel's coverage, and transparency stays.
|
||||
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));
|
||||
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)) {
|
||||
(true, _) => [128; 4],
|
||||
(_, true) => [0, 0, 0, 128],
|
||||
_ => [0; 4],
|
||||
});
|
||||
assert_eq!(s, want);
|
||||
}
|
||||
|
||||
#[test]
|
||||
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));
|
||||
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));
|
||||
for y in 0..h {
|
||||
for x in 0..w {
|
||||
let want = if edge(x, y) { [255; 4] } else { at(&normal, w, x, y) };
|
||||
assert_eq!(at(&d, w, x, y), want, "({x}, {y})");
|
||||
}
|
||||
}
|
||||
// Parameters past their ranges, at any resolution, still finish.
|
||||
for mode in ["edgeOnly", "overlayEdge"] {
|
||||
for s in [PixelSpace { px: 1e-9, ..space(w, h) }, PixelSpace { px: 1e9, ..space(w, h) }] {
|
||||
let mut e = src.clone();
|
||||
fx("blur.smart", json!({"blurMode": mode, "radius": 1e308, "threshold": 1e308, "quality": "high"})).apply(&mut e, w, h, &s);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn unsharp_mask_overshoots_at_edges() {
|
||||
let (w, h) = (40, 10);
|
||||
|
||||
@@ -193,6 +193,7 @@ fn choices(effect: &str, key: &str) -> Option<form::Choices> {
|
||||
("blur.radial", "method") => Some(&[("Spin", "spin"), ("Zoom", "zoom")]),
|
||||
("blur.radial", "quality") => Some(&[("Draft", "draft"), ("Good", "good"), ("Best", "best")]),
|
||||
("blur.smart", "quality") => Some(&[("Low", "low"), ("Medium", "medium"), ("High", "high")]),
|
||||
("blur.smart", "blurMode") => Some(&vectorcraft_effects::pixel::SMART_BLUR_MODES),
|
||||
("brushStrokes.sprayedStrokes", "strokeDirection") => Some(&vectorcraft_effects::pixel::STROKE_DIRECTIONS),
|
||||
("distort.glass", "texture") => Some(&vectorcraft_effects::pixel::GLASS_TEXTURES),
|
||||
("pixelate.mezzotint", "type") => Some(&vectorcraft_effects::pixel::MEZZOTINT_TYPES),
|
||||
|
||||
@@ -425,6 +425,7 @@ pub(super) fn humanize(k: &str) -> String {
|
||||
"Radius1" => "Radius 1".into(),
|
||||
"Radius2" => "Radius 2".into(),
|
||||
"Max Radius" => "Max. Radius:".into(),
|
||||
"Blur Mode" => "Mode:".into(),
|
||||
"Channel1" => "Channel 1:".into(),
|
||||
"Channel2" => "Channel 2:".into(),
|
||||
"Channel3" => "Channel 3:".into(),
|
||||
|
||||
@@ -179,6 +179,7 @@ fn raster_filter_dialogs_preview_and_apply() {
|
||||
for (effect, key, value) in [
|
||||
("blur.radial", "method", "zoom"),
|
||||
("blur.smart", "quality", "high"),
|
||||
("blur.smart", "blurMode", "overlayEdge"),
|
||||
("sharpen.unsharpMask", "amount", "120"),
|
||||
("pixelate.colorHalftone", "channel1", "30"),
|
||||
("pixelate.crystallize", "cellSize", "20"),
|
||||
@@ -207,14 +208,15 @@ fn raster_filter_dialogs_preview_and_apply() {
|
||||
}
|
||||
// Fields follow the effect's documented order (its dialog's), not the alphabet.
|
||||
let doc = vectorcraft_effects::effect_info("blur.smart").unwrap().params;
|
||||
let ranks: Vec<usize> = ["radius", "threshold", "quality"].iter().map(|k| super::effect::doc_rank(doc, k)).collect();
|
||||
assert!(ranks.windows(2).all(|w| w[0] < w[1]), "{ranks:?}");
|
||||
let ranks: Vec<usize> = ["radius", "threshold", "quality", "blurMode"].iter().map(|k| super::effect::doc_rank(doc, k)).collect();
|
||||
assert!(ranks.windows(2).all(|w| w[0] < w[1]) && ranks[3] < usize::MAX, "{ranks:?}");
|
||||
assert_eq!(super::effect::doc_rank(doc, "nope"), usize::MAX);
|
||||
let doc = vectorcraft_effects::effect_info("pixelate.colorHalftone").unwrap().params;
|
||||
let ranks: Vec<usize> = ["maxRadius", "channel1", "channel2", "channel3", "channel4"].iter().map(|k| super::effect::doc_rank(doc, k)).collect();
|
||||
assert!(ranks.windows(2).all(|w| w[0] < w[1]) && ranks[4] < usize::MAX, "{ranks:?}");
|
||||
// Labels as the dialogs show them.
|
||||
assert_eq!(super::form::humanize("maxRadius"), "Max. Radius:");
|
||||
assert_eq!(super::form::humanize("blurMode"), "Mode:");
|
||||
assert_eq!(super::form::humanize("channel3"), "Channel 3:");
|
||||
assert_eq!(super::form::humanize("cellSize"), "Cell Size:");
|
||||
assert_eq!(super::form::humanize("lightDirection"), "Light Direction:");
|
||||
|
||||
@@ -1972,6 +1972,10 @@ lighting Light Světlo
|
||||
Spin Otáčení
|
||||
Good Dobrá
|
||||
Best Nejlepší
|
||||
Mode Režim
|
||||
Blur Mode Režim rozostření
|
||||
Edge Only Jen okraje
|
||||
Overlay Edge Překrýt okraje
|
||||
Amount Míra
|
||||
Contrast Kontrast
|
||||
Edit the applied effect, or add another one? Upravit použitý efekt, nebo přidat další?
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -39,6 +39,10 @@
|
||||
Draft Entwurf
|
||||
Good Gut
|
||||
Best Sehr gut
|
||||
Mode Modus
|
||||
Blur Mode Weichzeichnermodus
|
||||
Edge Only Nur Kanten
|
||||
Overlay Edge Kanten überlagern
|
||||
|
||||
# Status and error messages too (`@msg` rows at the end, `COMPLETE_MESSAGES` in the tests).
|
||||
# A native speaker's review is welcome: keep the placeholders ({name}), the trailing "…" and the
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -57,6 +57,10 @@
|
||||
Draft Borrador
|
||||
Good Buena
|
||||
Best Óptima
|
||||
Mode Modo
|
||||
Blur Mode Modo de desenfoque
|
||||
Edge Only Solo bordes
|
||||
Overlay Edge Superponer bordes
|
||||
|
||||
# Status and error messages too (`@msg` rows at the end, `COMPLETE_MESSAGES` in the tests).
|
||||
# A native speaker's review is welcome: keep the placeholders ({name}), the trailing "…" and the
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -58,6 +58,10 @@
|
||||
Draft Brouillon
|
||||
Good Bonne
|
||||
Best Optimale
|
||||
Mode Mode
|
||||
Blur Mode Mode de flou
|
||||
Edge Only Bords uniquement
|
||||
Overlay Edge Bords en incrustation
|
||||
|
||||
# Status and error messages too (`@msg` rows at the end, `COMPLETE_MESSAGES` in the tests).
|
||||
# A native speaker's review is welcome: keep the placeholders ({name}), the trailing "…" and the
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -424,6 +424,7 @@
|
||||
Blocks Kockák
|
||||
Blue Kék
|
||||
Blur Elmosás
|
||||
Blur Mode Elmosási mód
|
||||
Bold Félkövér
|
||||
Border Thickness Szegélyvastagság
|
||||
Bottom Alsó
|
||||
@@ -1113,6 +1114,7 @@
|
||||
East Asian Type Kelet-ázsiai szöveg
|
||||
Edge Él
|
||||
Edge Brightness Él fényessége
|
||||
Edge Only Csak élek
|
||||
Edge Width Élszélesség
|
||||
Edit Szerkesztés
|
||||
Edit Artboards Rajztáblák szerkesztése
|
||||
@@ -2114,6 +2116,7 @@
|
||||
Mixed Appearances Vegyes megjelenések
|
||||
Mixed Objects Vegyes objektumok
|
||||
Mobile Mobil
|
||||
Mode Mód
|
||||
Mode: Mód:
|
||||
Model Modell
|
||||
Models Modellek
|
||||
@@ -2449,6 +2452,7 @@
|
||||
Overlap: Átfedés:
|
||||
Overlapping Átfedő
|
||||
Overlay Átfedés
|
||||
Overlay Edge Élek átfedése
|
||||
Overprint Felülnyomás
|
||||
Overprint and Transparency Flattener Felülnyomás és átlátszóság-lapítás
|
||||
Overprint and Transparency Flattener Options Felülnyomás és átlátszóság-lapítás beállításai
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -57,6 +57,10 @@
|
||||
Draft Bozza
|
||||
Good Buona
|
||||
Best Ottima
|
||||
Mode Modalità
|
||||
Blur Mode Modalità sfocatura
|
||||
Edge Only Solo bordi
|
||||
Overlay Edge Sovrapponi bordi
|
||||
|
||||
# Status and error messages too (`@msg` rows at the end, `COMPLETE_MESSAGES` in the tests).
|
||||
# A native speaker's review is welcome: keep the placeholders ({name}), the trailing "…" and the
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -3520,6 +3520,10 @@ theme Appearance 外観
|
||||
Draft ドラフト
|
||||
Good 標準
|
||||
Best 高品質
|
||||
Mode モード
|
||||
Blur Mode ぼかしモード
|
||||
Edge Only エッジのみ
|
||||
Overlay Edge エッジをオーバーレイ
|
||||
|
||||
# Status and error messages (`@msg`), translated where the status bar shows them; the message itself
|
||||
# stays English for agents and tests. `{_1}`, `{_2}` … stand for the `{}` of the format string, in order
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -3505,6 +3505,10 @@ theme Appearance 외관
|
||||
Draft 초안
|
||||
Good 좋음
|
||||
Best 최상
|
||||
Mode 모드
|
||||
Blur Mode 흐림 모드
|
||||
Edge Only 가장자리만
|
||||
Overlay Edge 가장자리 오버레이
|
||||
@msg Reset {_1} {_1}을(를) 재설정했습니다
|
||||
@msg Saving 저장 중
|
||||
@msg Saving {_1} {_1} 저장 중
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -1880,6 +1880,10 @@ lighting Light Światło
|
||||
Spin Wirowanie
|
||||
Good Dobra
|
||||
Best Najlepsza
|
||||
Mode Tryb
|
||||
Blur Mode Tryb rozmycia
|
||||
Edge Only Tylko krawędzie
|
||||
Overlay Edge Nakładka krawędzi
|
||||
Amount Wartość
|
||||
Contrast Kontrast
|
||||
Edit the applied effect, or add another one? Edytować zastosowany efekt czy dodać kolejny?
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -55,6 +55,10 @@
|
||||
Draft Черновое
|
||||
Good Хорошее
|
||||
Best Наилучшее
|
||||
Mode Режим
|
||||
Blur Mode Режим размытия
|
||||
Edge Only Только края
|
||||
Overlay Edge Наложение краёв
|
||||
|
||||
# Status and error messages too (`@msg` rows at the end, `COMPLETE_MESSAGES` in the tests).
|
||||
# A native speaker's review is welcome: keep the placeholders ({name}), the trailing "…" and the
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -56,6 +56,10 @@
|
||||
Draft Чорнове
|
||||
Good Гарне
|
||||
Best Найкраще
|
||||
Mode Режим
|
||||
Blur Mode Режим розмиття
|
||||
Edge Only Лише краї
|
||||
Overlay Edge Накладання країв
|
||||
— missing — відсутній
|
||||
to search every command для пошуку будь-якої команди
|
||||
(closest) (найближчий)
|
||||
|
||||
|
@@ -345,6 +345,7 @@
|
||||
Blocks Khối
|
||||
Blue Xanh lam
|
||||
Blur Mờ
|
||||
Blur Mode Chế độ làm mờ
|
||||
Bold Đậm
|
||||
Border Thickness Độ dày viền
|
||||
Both sides Cả hai mặt
|
||||
@@ -996,6 +997,7 @@
|
||||
East Asian Type Chữ Đông Á
|
||||
Edge Cạnh
|
||||
Edge Brightness Độ sáng cạnh
|
||||
Edge Only Chỉ cạnh
|
||||
Edge Width Độ rộng cạnh
|
||||
Edit Chỉnh sửa
|
||||
Edit Artboards Chỉnh sửa các bảng vẽ
|
||||
@@ -1911,6 +1913,7 @@
|
||||
Miter Limit: Giới hạn góc nhọn:
|
||||
Mixed Appearances Diện mạo hỗn hợp
|
||||
Mixed Objects Đối tượng hỗn hợp
|
||||
Mode Chế độ
|
||||
Mode: Chế độ:
|
||||
Model Mô hình
|
||||
Models Mô hình AI
|
||||
@@ -2243,6 +2246,7 @@
|
||||
Overlap: Chồng lấp:
|
||||
Overlapping Chồng lấp
|
||||
Overlay Phủ lên
|
||||
Overlay Edge Cạnh phủ lên
|
||||
Overprint In chồng
|
||||
Overprint and Transparency Flattener In chồng và làm phẳng trong suốt
|
||||
Overprint and Transparency Flattener Options Tùy chọn in chồng và làm phẳng trong suốt
|
||||
|
||||
|
@@ -3762,6 +3762,10 @@ axis Both 两者
|
||||
Method 方法
|
||||
Quality 质量
|
||||
Radius 半径
|
||||
Mode 模式
|
||||
Blur Mode 模糊模式
|
||||
Edge Only 仅边缘
|
||||
Overlay Edge 叠加边缘
|
||||
Spin 旋转
|
||||
Draft 草稿
|
||||
Good 良好
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -3546,6 +3546,10 @@ axis Both 兩者
|
||||
Method 方法
|
||||
Quality 品質
|
||||
Radius 半徑
|
||||
Mode 模式
|
||||
Blur Mode 模糊模式
|
||||
Edge Only 僅邊緣
|
||||
Overlay Edge 覆蓋邊緣
|
||||
Spin 旋轉
|
||||
Draft 草稿
|
||||
Good 良好
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 6 and column 47.
|
@@ -39,7 +39,7 @@ its resolution, PDF export warns once per object with the requested and the lowe
|
||||
| Submenu | Items | Ours | Missing |
|
||||
|---|---|---:|---|
|
||||
| Artistic | Colored Pencil, Cutout, Dry Brush, Film Grain, Fresco, Neon Glow, Paint Daubs, Palette Knife, Plastic Wrap, Poster Edges, Rough Pastels, Smudge Stick, Sponge, Underpainting, Watercolor | 1/15 (approximate Film Grain) | Colored Pencil, Cutout, Dry Brush, Fresco, Neon Glow, Paint Daubs, Palette Knife, Plastic Wrap, Poster Edges, Rough Pastels, Smudge Stick, Sponge, Underpainting, Watercolor |
|
||||
| Blur | Gaussian Blur, Radial Blur, Smart Blur | 3/3 | Smart Blur's Edge Only and Overlay Edge modes |
|
||||
| Blur | Gaussian Blur, Radial Blur, Smart Blur | 3/3 | — |
|
||||
| Brush Strokes | Accented Edges, Angled Strokes, Crosshatch, Dark Strokes, Ink Outlines, Spatter, Sprayed Strokes, Sumi-e | 8/8 | — |
|
||||
| Distort | Diffuse Glow, Glass, Ocean Ripple | 3/3 | Glass's Load Texture |
|
||||
| Pixelate | Color Halftone, Crystallize, Mezzotint, Pointillize | 4/4 | — |
|
||||
|
||||
+3
-2
@@ -189,10 +189,11 @@ from G14 and the budget run of G15: **~170–270 h** one agent, ~50–80 h wall
|
||||
## G2 Photoshop-style raster effects and the Effect Gallery
|
||||
|
||||
- **Missing:** 28 of 57 filters (the remaining 14 Artistic filters and all of Sketch), the Effect Gallery dialog, Load
|
||||
Texture for Glass and Texturizer, Smart Blur's Edge Only and Overlay Edge. Measured in
|
||||
Texture for Glass and Texturizer. Measured in
|
||||
[effects-parity.md](effects-parity.md).
|
||||
- **Done:** the pixel-filter pipeline and 29 filters (Artistic: Film Grain, Blur, Brush Strokes, Distort, Pixelate, Sharpen, Stylize,
|
||||
Texture, Video), Brush Strokes on 2026-10-11 and Film Grain on 2026-10-12.
|
||||
Texture, Video), Brush Strokes on 2026-10-11, Film Grain on 2026-10-12, and Smart Blur's Edge Only and Overlay Edge
|
||||
modes.
|
||||
- **Estimate:** 23–39 h (measured 0.7–1.2 h per filter); parallelizes well.
|
||||
|
||||
## G13 Illustrator 2026 (30.x) additions
|
||||
|
||||
@@ -320,7 +320,7 @@ about 1–1.5 agent-hours each, so the other rows (estimated on the older scale)
|
||||
| Strokes, brushes, width profiles | 5 | 90% | the Art brush's Overlap, auto-generated Pattern brush corners and new tile art in Pattern Brush Options (Brush Options for every brush type done), brush libraries (generated in code), pen pressure on macOS (the windowing layer reports none there), brushes driven by pen tilt, bearing and barrel rotation (6D art pens) | 8–15 |
|
||||
| Appearance, transparency, graphic styles, masks | 5 | 98% | raster effect reach in preview bounds (flattener presets apply in PDF, EPS and print) | 1–2 |
|
||||
| Live vector effects | 5 | 85% | Outline Object, Pathfinder Hard/Soft Mix and Trap, SVG Filters, the Transform effect's Transform Objects/Patterns and Scale Strokes & Effects options | 6–10 |
|
||||
| Raster effects (Effect Gallery, Document Raster Effects Settings) | 4 | 35% | ~36 Photoshop-style filters (Artistic, Brush Strokes, Sketch, Stylize), Smart Blur's Edge Only / Overlay Edge modes, Glass's and Texturizer's Load Texture and the Effect Gallery; Document Raster Effects Settings, raster effects in PDF, the pixel-filter pipeline, Radial Blur, Smart Blur, Unsharp Mask, Glowing Edges, Distort (Diffuse Glow, Glass, Ocean Ripple), Pixelate (Color Halftone, Crystallize, Mezzotint, Pointillize), Texture (Craquelure, Grain, Mosaic Tiles, Patchwork, Stained Glass, Texturizer) and Video (De-Interlace, NTSC Colors) are done | 19–31 |
|
||||
| Raster effects (Effect Gallery, Document Raster Effects Settings) | 4 | 35% | ~36 Photoshop-style filters (Artistic, Brush Strokes, Sketch, Stylize), Glass's and Texturizer's Load Texture and the Effect Gallery; Document Raster Effects Settings, raster effects in PDF, the pixel-filter pipeline, Radial Blur, Smart Blur (Normal, Edge Only and Overlay Edge), Unsharp Mask, Glowing Edges, Distort (Diffuse Glow, Glass, Ocean Ripple), Pixelate (Color Halftone, Crystallize, Mezzotint, Pointillize), Texture (Craquelure, Grain, Mosaic Tiles, Patchwork, Stained Glass, Texturizer) and Video (De-Interlace, NTSC Colors) are done | 19–31 |
|
||||
| 3D and Materials | 4 | ~25% | Initial Revolve, Extrude & Bevel, Inflate and Rotate, with simple lighting; materials, several lights, shadows, ray tracing, 3D Classic and more complete visibility remain | 25–45 |
|
||||
| Type core | 9 | 80% | Middle Eastern features beyond bidi layout and Paragraph Direction (Character Direction, kashidas, the Middle Eastern composers, editable bidirectional SVG text, a split caret where directions meet); Hyphenation… and Justification… options (Every-line Composer for justified and ragged text, per object, is done; it is tuned by eye, not yet compared side by side with Illustrator; mixed type sizes in a paragraph still break single-line), Optical Margin Alignment, paragraph spacing limit for vertically justified area type, hidden characters, paragraph attributes in the Type tool's copy and paste (the characters keep their styles, the paragraphs take the destination's), mixed-value display in the Paragraph panel for a selection across differing paragraphs, partial selection of type-area anchors by marquee, optional font-size preservation during group scaling (scaled character controls fixed in #1034) | 14–19 |
|
||||
| Type advanced | 4 | 50% | CJK composition for vertical type (ruby, mojikumi beyond Line-end Punctuation Half Width — the other sets (and their paragraph-start bracket and indent rules), custom kinsoku sets (beyond Hard, Soft and None) and the Mojikumi Settings dialog —, manual tate-chu-yoko), variable font axis sliders (named instances are styles), tab leaders, spell check (open dictionary), Touch Type, Retype; vertical point/area/path type and Type Orientation have initial support (beyond parity, not scored: inline symbols in text) | 20–28 |
|
||||
@@ -612,6 +612,8 @@ 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).
|
||||
|
||||
- **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.
|
||||
|
||||
- **Raster effects after document and view changes:** a cached drop shadow, outer glow or filtered content is made again when something its content reads from outside the object changes: a pattern's art or tiling, a symbol shown inside another symbol, a brush definition, an image's pixels, the raster resolution (De-Interlace), Page Knockout Group, Document Setup's substitution highlights, a layer's Dim Images, the color settings, the fonts, Pixel Preview, Anti-aliased Artwork, Overprint Preview and isolation mode. A thumbnail under Color Halftone or Mezzotint is made again when the document's color mode changes. Raster effects (blur, shadow, glow, grain) drawn after a pattern, radial or freeform gradient fill export at their place and size (#1541, fixes #1536).
|
||||
|
||||
Reference in New Issue
Block a user