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:
dotCooCoo
2026-10-16 04:04:51 +07:00
committed by bflatastic
parent 580aa8daa5
commit 6f9ee7825b
24 changed files with 275 additions and 18 deletions
+2 -2
View File
@@ -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",
+79 -5
View File
@@ -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];
}
}
}
+12 -2
View File
@@ -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),
+113 -3
View File
@@ -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);
+1
View File
@@ -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),
+1
View File
@@ -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(),
+4 -2
View File
@@ -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:");
+4
View File
@@ -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.
+4
View File
@@ -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.
+4
View File
@@ -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.
+4
View File
@@ -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.
+4
View File
@@ -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.
+4
View File
@@ -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.
+4
View File
@@ -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.
+4
View File
@@ -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.
+4
View File
@@ -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.
+4
View File
@@ -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.
+4
View File
@@ -56,6 +56,10 @@
Draft Чорнове
Good Гарне
Best Найкраще
Mode Режим
Blur Mode Режим розмиття
Edge Only Лише краї
Overlay Edge Накладання країв
— missing — відсутній
to search every command для пошуку будь-якої команди
(closest) (найближчий)
1 # Ukrainian (uk) catalog. Same format as zh-hant.tsv: context <TAB> source <TAB> translation.
56
57
58
59
60
61
62
63
64
65
+4
View File
@@ -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
1 # Vietnamese (vi) catalog. Same format as zh-hant.tsv: context <TAB> source <TAB> translation
345
346
347
348
349
350
351
997
998
999
1000
1001
1002
1003
1913
1914
1915
1916
1917
1918
1919
2246
2247
2248
2249
2250
2251
2252
+4
View File
@@ -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.
+4
View File
@@ -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.
+1 -1
View File
@@ -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
View File
@@ -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
+3 -1
View File
@@ -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).