Merge pull request #2453 from VL420/feat/artistic-filters-1

G2: Effect › Artistic › Cutout, Neon Glow and Poster Edges
This commit is contained in:
BFlat
2026-10-17 17:08:03 +07:00
committed by GitHub
33 changed files with 717 additions and 27 deletions
+1 -1
View File
@@ -245,7 +245,7 @@ The scores are self-assessed from our code against Illustrator's public document
- reliable exchange of Illustrator's own `.ai` files (layer structure in real files; Illustrator's editing data isn't written);
- an interaction-fidelity pass covering every tool's modifiers and small behaviours;
- 3D and Materials beyond the initial live Revolve (Extrude & Bevel, Inflate, Rotate, materials);
- about half of the Photoshop-style raster effects (27 of 57 still missing; Film Grain is available under Artistic, Halftone Pattern under Sketch) and the Effect Gallery;
- about half of the Photoshop-style raster effects (24 of 57 still missing; Cutout, Film Grain, Neon Glow and Poster Edges are available under Artistic, Halftone Pattern under Sketch) and the Effect Gallery;
- CJK composition for vertical type (vertical type itself has initial support, with a Japanese interface);
- scripting, and the rest of Variables (image and graph kinds, dataset import);
- generative AI features.
+3 -3
View File
@@ -29,7 +29,7 @@ blocking it ([Alpha gate](docs/roadmap.md#alpha-gate)).
| | Value | Kind |
|---|---|---|
| **Feature breadth** (Illustrator's menu items, tools, effects, formats exist) | **~88%** | measured in part: menu items 348/375 wired (92.8%), tools 89/92, Illustrator effects 44/50, Photoshop effects 30/57, 3D 4/4 (initial types, no materials) |
| **Feature breadth** (Illustrator's menu items, tools, effects, formats exist) | **~88%** | measured in part: menu items 348/375 wired (92.8%), tools 89/92, Illustrator effects 44/50, Photoshop effects 33/57, 3D 4/4 (initial types, no materials) |
| **Feature depth** (weighted by use, scored by behaviour) | **~75%** | estimated |
| **To beta** | **~170–270 h** one agent · ~50–80 h with 4–6 agents | estimated |
| **To full parity** | **~360–590 h** one agent · ~95–165 h with 4–6 agents | estimated |
@@ -76,7 +76,7 @@ The dimension hours overlap (feature rows hold some UI, format and AI work), so
| Strokes, brushes, width profiles | 88% | 8–15 |
| Appearance, transparency, graphic styles, masks | 97% | 1–2 |
| Live vector effects | 85% | 6–10 |
| Raster effects (Effect Gallery) — [effects-parity.md](docs/effects-parity.md) | 49% | 23–39 |
| Raster effects (Effect Gallery) — [effects-parity.md](docs/effects-parity.md) | 49% | 21–35 |
| 3D and Materials | ~25% | 30–55 |
| Type core — [type-parity.md](docs/type-parity.md) | 80% | 14–19 |
| Type advanced (CJK, spelling, Touch Type) | 50% | 20–28 |
@@ -123,7 +123,7 @@ Ranked; detail in [docs/roadmap.md](docs/roadmap.md) and [docs/gaps.md](docs/gap
2. **`.ai` files from real users** (G9): layer structure, guides, speed: 10–20 h.
3. **Launch and stability on real machines** (G10): 20–35 h.
4. **Core-tool correctness** (G11: Shape Builder regions, Asset Export borders): 8–15 h.
5. **Raster effects** (G2) alongside: 28 filters and the Effect Gallery, 23–39 h.
5. **Raster effects** (G2) alongside: 24 filters and the Effect Gallery, 21–35 h.
6. **Then:** type core and advanced type (G4), the Illustrator 2026 additions (G13), 3D (G3).
## Progress log
+21
View File
@@ -336,6 +336,13 @@ pub fn effect_catalog() -> Vec<EffectInfo> {
),
r("stylize.feather", "Feather…", STYLIZE, "{radius: pt (5)}", json!({"radius": 5.0})),
r("blur.gaussian", "Gaussian Blur…", BLUR, "{radius: pt (5)}", json!({"radius": 5.0})),
r(
"artistic.cutout",
"Cutout…",
ARTISTIC,
"{levels: 2..8 (4; steps per colour channel: more levels, more flat colours), edgeSimplicity: 0..10 (4; simpler, smoother edges), edgeFidelity: 1..3 (2; keeps more of the edges' detail)} redraws the object as flat pieces of coloured paper, each piece painted in the average colour that falls into it, keeping its coverage. Original visual approximation; chosen VectorCraft defaults.",
json!({"levels": 4.0, "edgeSimplicity": 4.0, "edgeFidelity": 2.0}),
),
r(
"artistic.filmGrain",
"Film Grain…",
@@ -343,6 +350,20 @@ pub fn effect_catalog() -> Vec<EffectInfo> {
"{grain: 0..20 (4; monochrome noise in shadows and midtones), highlightArea: 0..20 (0; widens the bright highlight area), intensity: 0..10 (10; highlight strength and quieter highlight grain)}. Smooth document-space grain follows translations; minification uses approximate footprint filtering. Coverage and pure white are preserved; black takes light speckles, and saturated colour channels may clip. Grain 0 with either highlightArea 0 or intensity 0 leaves the art unchanged. Original visual approximation; chosen VectorCraft defaults.",
json!({"grain": 4.0, "highlightArea": 0.0, "intensity": 10.0}),
),
r(
"artistic.neonGlow",
"Neon Glow…",
ARTISTIC,
"{glowSize: -24..24 (5; above 0 the light areas glow, below 0 the dark ones; farther from 0 spreads the glow wider), glowBrightness: 0..50 (15; a stronger glow over dimmer greys), color: \"#rrggbb\" (\"#4da6ff\"; the glow's colour)} redraws the object in greys lit by a coloured glow, keeping its coverage. Original visual approximation; chosen VectorCraft defaults.",
json!({"glowSize": 5.0, "glowBrightness": 15.0, "color": "#4da6ff"}),
),
r(
"artistic.posterEdges",
"Poster Edges…",
ARTISTIC,
"{edgeThickness: 0..10 (2; wider lines), edgeIntensity: 0..10 (1; darker lines, on fainter edges too), posterization: 0..6 (2; posterization + 2 levels per colour channel)} reduces the object's colours and draws black lines on its edges, keeping its coverage. Original visual approximation; chosen VectorCraft defaults.",
json!({"edgeThickness": 2.0, "edgeIntensity": 1.0, "posterization": 2.0}),
),
r(
"blur.radial",
"Radial Blur…",
+136 -2
View File
@@ -1,11 +1,13 @@
//! Effect › Artistic. Original filters built from public behaviour documentation:
//! Effect › Artistic (Cutout, Film Grain, Neon Glow, Poster Edges). Original filters built from
//! public behaviour documentation:
//! <https://helpx.adobe.com/illustrator/using/summary-effects.html> and
//! <https://helpx.adobe.com/photoshop-elements/using/artistic-filters.html>.
//! These are visual approximations, not reproductions of a proprietary algorithm.
use super::PixelSpace;
use super::edges::sobel;
use super::pixelate::{Place, cell, rand};
use super::texture::{luma, premultiply, straight};
use super::{PixelSpace, blur_plane};
use vectorcraft_geom::Point;
const SALT_FILM: u64 = 256;
@@ -134,3 +136,135 @@ pub(super) fn film_grain(px: &mut [[u8; 4]], w: usize, h: usize, space: &PixelSp
*p = premultiply(c, f32::from(p[3]) / 255.0);
});
}
/// The coverage-weighted Gaussian softening of the planes `planes` (premultiplied by `alpha`) by
/// `sigma` pixels: each divided by the softened coverage, so transparency doesn't darken edges.
fn soften(planes: &mut [Vec<f32>], alpha: &[f32], w: usize, h: usize, sigma: f64) {
let mut a = alpha.to_vec();
blur_plane(&mut a, w, h, sigma);
for plane in planes {
for (v, c) in plane.iter_mut().zip(alpha) {
*v *= c;
}
blur_plane(plane, w, h, sigma);
for (v, c) in plane.iter_mut().zip(&a) {
*v = if *c > 1e-4 { (*v / c).clamp(0.0, 1.0) } else { 0.0 };
}
}
}
/// The pixels' straight colour channels and their coverage (0..1), as planes.
fn planes(px: &[[u8; 4]]) -> ([Vec<f32>; 3], Vec<f32>) {
let colours = px.iter().map(|p| straight(*p)).collect::<Vec<_>>();
(std::array::from_fn(|k| colours.iter().map(|c| c[k]).collect()), px.iter().map(|p| f32::from(p[3]) / 255.0).collect())
}
/// Cutout: the object as flat pieces of coloured paper. Its colours, softened as `simplicity`
/// (0..10) asks and less as `fidelity` (1..3) keeps detail, fall into `levels` (2..8) steps per
/// colour channel; each step's piece is painted in the average colour of what falls into it, so
/// colours keep their hue. Slivers (the blends along softened edges) take the nearest large
/// piece's colour, so the pieces meet with crisp edges. Coverage is kept.
pub(super) fn cutout(px: &mut [[u8; 4]], w: usize, h: usize, space: &PixelSpace, levels: f64, simplicity: f64, fidelity: f64) {
if ![levels, simplicity, fidelity].iter().all(|v| v.is_finite()) || !(space.px.is_finite() && space.px > 0.0) {
return;
}
let levels = levels.round().clamp(2.0, 8.0) as usize;
// Simpler edges come from softer colours; more fidelity softens them less.
let sigma = 0.6 * simplicity.clamp(0.0, 10.0) * (4.0 - fidelity.clamp(1.0, 3.0)) / 3.0 / space.px;
let (mut colour, alpha) = planes(px);
soften(&mut colour, &alpha, w, h, sigma);
let at = |k: usize| [0, 1, 2].map(|i| colour[i].get(k).copied().unwrap_or(0.0));
let piece = |c: [f32; 3]| c.iter().fold(0, |n, v| n * levels + ((v * levels as f32) as usize).min(levels - 1));
// Each piece's colour: the coverage-weighted mean of the softened colours in it.
let mut sums = vec![[0.0f64; 4]; levels * levels * levels];
for (k, a) in alpha.iter().enumerate() {
let c = at(k);
let Some(sum) = sums.get_mut(piece(c)) else { continue };
for (s, v) in sum.iter_mut().zip(c) {
*s += f64::from(v * a);
}
sum[3] += f64::from(*a);
}
let mean = |sum: &[f64; 4]| [0, 1, 2].map(|i| (sum[i] / sum[3]) as f32);
let total: f64 = sums.iter().map(|s| s[3]).sum();
let heaviest = sums.iter().map(|s| s[3]).fold(0.0, f64::max);
// Pieces of at least 1% of the object (and always the largest one) are kept.
let large: Vec<[f32; 3]> = sums.iter().filter(|s| s[3] > 0.0 && (s[3] >= 0.01 * total || s[3] >= heaviest)).map(mean).collect();
let paint: Vec<Option<[f32; 3]>> = sums
.iter()
.map(|s| {
let c = (s[3] > 0.0).then(|| mean(s))?;
let far = |o: &[f32; 3]| o.iter().zip(c).map(|(a, b)| (a - b) * (a - b)).sum::<f32>();
large.iter().min_by(|a, b| far(a).total_cmp(&far(b))).copied()
})
.collect();
for (k, p) in px.iter_mut().enumerate() {
if p[3] == 0 {
continue;
}
if let Some(Some(c)) = paint.get(piece(at(k))) {
*p = premultiply(*c, f32::from(p[3]) / 255.0);
}
}
}
/// Poster Edges: the object's colours posterized to `posterization` + 2 (2..8) levels per
/// channel, with black lines drawn on the edges of any of its colour channels and of its outline. `thickness` (0..10) widens the lines and
/// `intensity` (0..10) darkens them and lets fainter edges take a line. Coverage is kept.
pub(super) fn poster_edges(px: &mut [[u8; 4]], w: usize, h: usize, space: &PixelSpace, thickness: f64, intensity: f64, posterization: f64) {
if ![thickness, intensity, posterization].iter().all(|v| v.is_finite()) || !(space.px.is_finite() && space.px > 0.0) {
return;
}
let steps = (posterization.round().clamp(0.0, 6.0) + 1.0) as f32;
// The edges of each premultiplied channel (so the object's outline too), a step of 1 reading 1.
let mut edges = vec![0.0f32; px.len()];
for k in 0..4 {
let plane: Vec<f32> = px.iter().map(|p| f32::from(p[k]) / 255.0).collect();
for (e, s) in edges.iter_mut().zip(sobel(&plane, w, h)) {
*e = e.max(s / 4.0);
}
}
// Wider lines spread the edges; scale them back up so a spread line stays as dark.
let sigma = (0.3 + 0.25 * thickness.clamp(0.0, 10.0)) / space.px;
blur_plane(&mut edges, w, h, sigma);
let gain = ((0.3 + 0.25 * intensity.clamp(0.0, 10.0)) * (1.0 + 1.5 * sigma.max(0.0))) as f32;
for (p, e) in px.iter_mut().zip(edges) {
if p[3] == 0 {
continue;
}
let t = ((e * gain - 0.25) / 0.5).clamp(0.0, 1.0);
let ink = t * t * (3.0 - 2.0 * t);
let c = straight(*p).map(|v| (v * steps).round() / steps * (1.0 - ink));
*p = premultiply(c, f32::from(p[3]) / 255.0);
}
}
/// How far (document units) Neon Glow's glow spreads at `size` (-24..24).
pub(super) fn glow_spread(size: f64) -> f64 {
0.25 * size.abs().min(24.0)
}
/// Neon Glow: the object in dimmed greys, lit by `colour` glowing from its light areas (`size`
/// above 0) or from its dark areas (below 0), spread as far as |`size`| (0..24) says and as
/// strong as `brightness` (0..50). Coverage is kept.
pub(super) fn neon_glow(px: &mut [[u8; 4]], w: usize, h: usize, space: &PixelSpace, size: f64, brightness: f64, colour: [f32; 3]) {
if ![size, brightness].iter().all(|v| v.is_finite()) || !(space.px.is_finite() && space.px > 0.0) {
return;
}
let strength = (brightness / 50.0).clamp(0.0, 1.0) as f32;
let tones: Vec<f32> = px.iter().map(|p| luma(straight(*p))).collect();
let alpha: Vec<f32> = px.iter().map(|p| f32::from(p[3]) / 255.0).collect();
let mut glow = [tones.iter().map(|l| if size < 0.0 { 1.0 - l } else { *l }).collect::<Vec<_>>()];
soften(&mut glow, &alpha, w, h, glow_spread(size) / space.px);
let [glow] = glow;
for ((p, tone), g) in px.iter_mut().zip(tones).zip(glow) {
if p[3] == 0 {
continue;
}
// The greys dim as the glow brightens, and the glow is screened over them.
let base = tone * (1.0 - 0.6 * strength);
let lit = (3.0 * g * strength).clamp(0.0, 1.0);
let c = colour.map(|v| 1.0 - (1.0 - base) * (1.0 - v.clamp(0.0, 1.0) * lit));
*p = premultiply(c, f32::from(p[3]) / 255.0);
}
}
+44 -2
View File
@@ -1,4 +1,4 @@
//! Photoshop-style raster effects (Effect › Artistic › Film Grain, Blur › Radial Blur and Smart Blur, Brush Strokes ›
//! Photoshop-style raster effects (Effect › Artistic › Cutout, Film Grain, Neon Glow and Poster Edges, Blur › Radial Blur and Smart Blur, Brush Strokes ›
//! Accented Edges, Angled Strokes, Crosshatch, Dark Strokes, Ink Outlines, Spatter, Sprayed Strokes
//! and Sumi-e, Distort › Diffuse Glow, Glass and Ocean Ripple, Pixelate › Color Halftone,
//! Crystallize, Mezzotint and Pointillize, Sharpen › Unsharp Mask, Sketch › Halftone Pattern, Stylize ›
@@ -39,8 +39,11 @@ pub use sketch::{HALFTONE_TYPES, HalftonePattern};
pub use texture::{GRAIN_TYPES, Grain, LIGHT_DIRECTIONS, Light, TEXTURES, Texture};
/// The Photoshop-style effect ids (all raster effects, see [`crate::is_raster`]).
pub const PIXEL_EFFECTS: [&str; 29] = [
pub const PIXEL_EFFECTS: [&str; 32] = [
"artistic.cutout",
"artistic.filmGrain",
"artistic.neonGlow",
"artistic.posterEdges",
"blur.radial",
"blur.smart",
"brushStrokes.accentedEdges",
@@ -75,9 +78,19 @@ pub const PIXEL_EFFECTS: [&str; 29] = [
/// are in document units.
#[derive(Clone, Debug, PartialEq)]
pub enum PixelFx {
/// Artistic › Cutout: flat pieces of colour in `levels` (2..8) steps per channel, their
/// edges as simple as `simplicity` (0..10) and as faithful as `fidelity` (1..3) say.
Cutout { levels: f64, simplicity: f64, fidelity: f64 },
/// Artistic › Film Grain: monochrome grain in shadows and midtones, a wider bright
/// highlight area (0..20), and tonal intensity (0..10), preserving coverage.
FilmGrain { grain: f64, highlight: f64, intensity: f64 },
/// Artistic › Neon Glow: dimmed greys lit by `colour` (straight RGB, 0..1) glowing from the
/// light areas (`size` above 0) or the dark ones (below 0), spread by |`size`| (-24..24), as
/// strong as `brightness` (0..50).
NeonGlow { size: f64, brightness: f64, colour: [f32; 3] },
/// Artistic › Poster Edges: colours posterized to `posterization` + 2 levels (0..6) with black
/// lines on the edges, as thick as `thickness` (0..10) and as dark as `intensity` (0..10).
PosterEdges { thickness: f64, intensity: f64, posterization: f64 },
/// Blur › Radial Blur around the object's centre: along arcs (spin, an arc of `amount`°) or
/// along rays (zoom, the content scaled by up to about ±`amount`/2 %), averaging 2^`passes` samples.
RadialBlur { amount: f64, zoom: bool, passes: u32 },
@@ -209,6 +222,21 @@ pub(crate) fn parse(id: &str, p: &Value) -> Option<PixelFx> {
levels.iter().find(|(name, _)| name.eq_ignore_ascii_case(q)).map_or(default, |(_, v)| *v)
};
Some(match id {
"artistic.cutout" => PixelFx::Cutout {
levels: num(p, "levels", 4.0).round().clamp(2.0, 8.0),
simplicity: num(p, "edgeSimplicity", 4.0).clamp(0.0, 10.0),
fidelity: num(p, "edgeFidelity", 2.0).round().clamp(1.0, 3.0),
},
"artistic.neonGlow" => PixelFx::NeonGlow {
size: num(p, "glowSize", 5.0).clamp(-24.0, 24.0),
brightness: num(p, "glowBrightness", 15.0).clamp(0.0, 50.0),
colour: crate::raster::color_param(p, vectorcraft_doc::color::Color::rgb(0.3, 0.65, 1.0)).to_rgb(),
},
"artistic.posterEdges" => PixelFx::PosterEdges {
thickness: num(p, "edgeThickness", 2.0).clamp(0.0, 10.0),
intensity: num(p, "edgeIntensity", 1.0).clamp(0.0, 10.0),
posterization: num(p, "posterization", 2.0).round().clamp(0.0, 6.0),
},
"artistic.filmGrain" => PixelFx::FilmGrain {
grain: num(p, "grain", 4.0).clamp(0.0, 20.0),
highlight: num(p, "highlightArea", 0.0).clamp(0.0, 20.0),
@@ -367,6 +395,9 @@ impl PixelFx {
PixelFx::UnsharpMask { .. } | PixelFx::HalftonePattern { .. } => 0.0,
PixelFx::GlowingEdges { width, smoothness, .. } => width.max(smoothness * 3.0),
PixelFx::FilmGrain { .. }
| PixelFx::Cutout { .. }
| PixelFx::NeonGlow { .. }
| PixelFx::PosterEdges { .. }
| PixelFx::ColorHalftone { .. }
| PixelFx::Mezzotint { .. }
| PixelFx::Craquelure { .. }
@@ -409,6 +440,12 @@ impl PixelFx {
PixelFx::ColorHalftone { max_radius, .. } => Some(3.0 * std::f64::consts::SQRT_2 * max_radius),
PixelFx::Crystallize { cell } => Some(1.5 * cell),
PixelFx::FilmGrain { .. } | PixelFx::Mezzotint { .. } | PixelFx::HalftonePattern { .. } => Some(0.0),
// Cutout's palette comes from the whole object.
PixelFx::Cutout { .. } => None,
// The softened tones around the pixel.
PixelFx::NeonGlow { size, .. } => Some(3.0 * artistic::glow_spread(size)),
// The edges' lines, spread round them.
PixelFx::PosterEdges { thickness, .. } => Some(3.0 * (0.3 + 0.25 * thickness) + 1.0),
// The dots' points, and the softened colour around them.
PixelFx::Pointillize { cell } => Some(2.5 * cell),
// The softened tones around the pixel.
@@ -429,7 +466,12 @@ impl PixelFx {
let Some(px) = pixels(data, w, h) else { return };
let to_px = |len: f64| len / space.px.max(1e-9);
match *self {
PixelFx::Cutout { levels, simplicity, fidelity } => artistic::cutout(px, w, h, space, levels, simplicity, fidelity),
PixelFx::FilmGrain { grain, highlight, intensity } => artistic::film_grain(px, w, h, space, grain, highlight, intensity),
PixelFx::NeonGlow { size, brightness, colour } => artistic::neon_glow(px, w, h, space, size, brightness, colour),
PixelFx::PosterEdges { thickness, intensity, posterization } => {
artistic::poster_edges(px, w, h, space, thickness, intensity, posterization)
}
PixelFx::RadialBlur { amount, zoom, passes } => blur::radial(px, w, h, space, amount, zoom, passes),
PixelFx::SmartBlur { radius, threshold, samples, mode: SmartBlurMode::Normal } => {
blur::smart(px, w, h, to_px(radius), threshold, samples)
+153
View File
@@ -2093,3 +2093,156 @@ fn halftone_draws_in_black_and_white_only() {
}
}
}
/// The distinct colours of the covered pixels.
fn colours(d: &[u8]) -> std::collections::BTreeSet<[u8; 4]> {
d.as_chunks::<4>().0.iter().filter(|p| p[3] > 0).copied().collect()
}
/// A left-to-right rainbow ramp, half covered at its bottom rows.
fn ramp(w: usize, h: usize) -> Vec<u8> {
image(w, h, |x, y| {
let t = x * 255 / (w - 1);
let a = if y + 4 >= h { 128 } else { 255 };
let c = [t, 255 - t, (t * 3) % 256].map(|v| (v * a / 255) as u8);
[c[0], c[1], c[2], a as u8]
})
}
#[test]
fn artistic_params_take_defaults_and_stay_in_range() {
assert_eq!(fx("artistic.cutout", json!({})), PixelFx::Cutout { levels: 4.0, simplicity: 4.0, fidelity: 2.0 });
assert_eq!(
fx("artistic.cutout", json!({"levels": 1e308, "edgeSimplicity": -5, "edgeFidelity": "NaN"})),
PixelFx::Cutout { levels: 8.0, simplicity: 0.0, fidelity: 2.0 }
);
assert_eq!(fx("artistic.posterEdges", json!({})), PixelFx::PosterEdges { thickness: 2.0, intensity: 1.0, posterization: 2.0 });
assert_eq!(
fx("artistic.posterEdges", json!({"edgeThickness": 99, "edgeIntensity": -1, "posterization": 6.4})),
PixelFx::PosterEdges { thickness: 10.0, intensity: 0.0, posterization: 6.0 }
);
let PixelFx::NeonGlow { size, brightness, colour } = fx("artistic.neonGlow", json!({})) else { panic!() };
assert_eq!((size, brightness), (5.0, 15.0));
assert!((colour[0] - 0.3).abs() < 0.01 && (colour[1] - 0.65).abs() < 0.01 && colour[2] == 1.0, "{colour:?}");
assert_eq!(
fx("artistic.neonGlow", json!({"glowSize": -1e308, "glowBrightness": 80, "color": "#ff0000"})),
PixelFx::NeonGlow { size: -24.0, brightness: 50.0, colour: [1.0, 0.0, 0.0] }
);
for id in ["artistic.cutout", "artistic.neonGlow", "artistic.posterEdges"] {
assert_eq!(fx(id, json!({})).outset(Rect::new(0.0, 0.0, 40.0, 30.0)), 0.0, "{id} keeps the object's shape");
}
}
#[test]
fn cutout_paints_a_few_flat_colours_and_keeps_coverage() {
let (w, h) = (64, 24);
let src = ramp(w, h);
for levels in [2, 3, 5, 8] {
let mut out = src.clone();
fx("artistic.cutout", json!({"levels": levels})).apply(&mut out, w, h, &space(w, h));
let opaque: std::collections::BTreeSet<_> = colours(&out).into_iter().filter(|p| p[3] == 255).collect();
assert!(opaque.len() <= levels * levels * levels && opaque.len() >= 2, "{levels} levels: {} colours", opaque.len());
assert!(opaque.len() < 64, "flat pieces, not a ramp: {levels} levels, {} colours", opaque.len());
assert!(out.as_chunks::<4>().0.iter().zip(src.as_chunks::<4>().0).all(|(o, s)| o[3] == s[3]));
}
}
#[test]
fn cutout_simplicity_smooths_the_pieces_edges() {
// A noisy grey: simpler edges join the speckles into fewer changes between neighbours.
let (w, h) = (48, 48);
let src = image(w, h, |x, y| grey(if (x * 7 + y * 13) % 5 < 2 { 60 } else { 190 }));
let changes = |simplicity: f64| {
let mut out = src.clone();
fx("artistic.cutout", json!({"levels": 2, "edgeSimplicity": simplicity, "edgeFidelity": 1})).apply(&mut out, w, h, &space(w, h));
out.as_chunks::<4>().0.windows(2).filter(|p| p[0] != p[1]).count()
};
assert!(changes(10.0) * 4 < changes(0.0), "{} vs {}", changes(10.0), changes(0.0));
}
#[test]
fn poster_edges_draw_black_lines_on_edges_and_posterize_the_rest() {
let (w, h) = (40, 20);
// Two flat halves: a line where they meet, posterized colour away from it.
let src = image(w, h, |x, _| if x < w / 2 { [200, 90, 30, 255] } else { [30, 120, 210, 255] });
let mut out = src.clone();
fx("artistic.posterEdges", json!({"edgeThickness": 2, "edgeIntensity": 10, "posterization": 0})).apply(&mut out, w, h, &space(w, h));
let line = at(&out, w, w / 2, h / 2);
assert!(line[0] < 40 && line[1] < 40 && line[2] < 40, "a dark line on the edge: {line:?}");
// 2 levels per channel: every far pixel is 0 or 255 in each channel.
for x in [3, w - 4] {
let p = at(&out, w, x, h / 2);
assert!(p[..3].iter().all(|v| *v == 0 || *v == 255), "{x}: {p:?}");
}
// Inside a flat colour, away from the object's outline, there are no lines: it only posterizes.
let mut flat = image(w, h, |_, _| [200, 90, 30, 255]);
fx("artistic.posterEdges", json!({"edgeIntensity": 10, "posterization": 6})).apply(&mut flat, w, h, &space(w, h));
let inside: std::collections::BTreeSet<_> = (6..h - 6).flat_map(|y| (6..w - 6).map(move |x| (x, y))).map(|(x, y)| at(&flat, w, x, y)).collect();
assert_eq!(inside.len(), 1, "{inside:?}");
assert!(at(&flat, w, 0, h / 2)[0] < 60, "the outline takes a line");
}
#[test]
fn poster_edges_thickness_widens_the_lines() {
let (w, h) = (60, 10);
let src = image(w, h, |x, _| grey(if x < w / 2 { 30 } else { 230 }));
let dark = |thickness: f64| {
let mut out = src.clone();
fx("artistic.posterEdges", json!({"edgeThickness": thickness, "edgeIntensity": 10, "posterization": 6})).apply(&mut out, w, h, &space(w, h));
(0..w).filter(|x| at(&out, w, *x, h / 2)[0] < 20 && (*x >= w / 2 || at(&src, w, *x, h / 2)[0] > 20)).count()
};
assert!(dark(10.0) > dark(0.0), "{} vs {}", dark(10.0), dark(0.0));
}
#[test]
fn neon_glow_lights_the_bright_or_the_dark_areas_in_its_colour() {
let (w, h) = (40, 20);
let src = image(w, h, |x, _| grey(if x < w / 2 { 20 } else { 235 }));
let run = |size: f64| {
let mut out = src.clone();
fx("artistic.neonGlow", json!({"glowSize": size, "glowBrightness": 40, "color": "#ff0000"})).apply(&mut out, w, h, &space(w, h));
(at(&out, w, 3, h / 2), at(&out, w, w - 4, h / 2))
};
// Red glow: the red channel rises above green and blue where it glows.
let red = |p: [u8; 4]| i32::from(p[0]) - i32::from(p[1]);
let (dark, light) = run(8.0);
assert!(red(light) > 60 && red(light) > red(dark) + 40, "light areas glow: {dark:?} {light:?}");
let (dark, light) = run(-8.0);
assert!(red(dark) > 60 && red(dark) > red(light) + 40, "dark areas glow: {dark:?} {light:?}");
// No brightness: dimmed greys only.
let mut out = src.clone();
fx("artistic.neonGlow", json!({"glowBrightness": 0})).apply(&mut out, w, h, &space(w, h));
assert!(out.as_chunks::<4>().0.iter().all(|p| p[0] == p[1] && p[1] == p[2] && p[3] == 255));
}
#[test]
fn artistic_filters_look_the_same_at_any_resolution() {
// The same art at 1 and at 2 pixels per point: the filter at the coarse resolution matches
// the fine result sampled at the same points.
let (w, h) = (48, 32);
let art = |x: f64, y: f64| grey(if (x - 24.0).hypot(y - 16.0) < 10.0 { 230 } else { 40 });
let coarse = image(w, h, |x, y| art(x as f64 + 0.5, y as f64 + 0.5));
let fine = image(2 * w, 2 * h, |x, y| art((x as f64 + 0.5) / 2.0, (y as f64 + 0.5) / 2.0));
let mut half = space(2 * w, 2 * h);
half.to_doc = Affine::scale(0.5);
half.px = 0.5;
half.center = Point::new(w as f64 / 2.0, h as f64 / 2.0);
for (id, p) in [
("artistic.cutout", json!({"edgeSimplicity": 6})),
("artistic.neonGlow", json!({"glowSize": 12, "glowBrightness": 30})),
("artistic.posterEdges", json!({"edgeThickness": 6, "edgeIntensity": 6})),
] {
let (mut a, mut b) = (coarse.clone(), fine.clone());
fx(id, p.clone()).apply(&mut a, w, h, &space(w, h));
fx(id, p).apply(&mut b, 2 * w, 2 * h, &half);
let diff: f64 = (0..h)
.flat_map(|y| (0..w).map(move |x| (x, y)))
.map(|(x, y)| {
let (p, q) = (at(&a, w, x, y), at(&b, 2 * w, 2 * x, 2 * y));
(0..3).map(|k| f64::from(p[k].abs_diff(q[k]))).sum::<f64>() / 3.0
})
.sum::<f64>()
/ (w * h) as f64;
assert!(diff < 12.0, "{id}: mean difference {diff}");
}
}
+49 -3
View File
@@ -1,4 +1,4 @@
//! The Photoshop-style raster effects (Effect › Blur › Radial Blur and Smart Blur, Brush Strokes,
//! The Photoshop-style raster effects (Effect › Artistic, Blur › Radial Blur and Smart Blur, Brush Strokes,
//! Distort › Diffuse Glow, Glass and Ocean Ripple, Pixelate › Color Halftone, Crystallize, Mezzotint and Pointillize, Sharpen › Unsharp Mask, Sketch › Halftone Pattern, Texture ›
//! Craquelure, Grain, Mosaic Tiles, Patchwork, Stained Glass and Texturizer, Video): applied and
//! edited as commands, listed in the catalogue, drawn on the canvas, and written to PDF and SVG
@@ -30,8 +30,9 @@ const BRUSH_STROKES: [(&str, &str); 8] = [
("brushStrokes.sumiE", "{}"),
];
/// Effect › Artistic › Film Grain, at its defaults.
const ARTISTIC: [(&str, &str); 1] = [("artistic.filmGrain", "{}")];
/// Effect › Artistic, at their defaults.
const ARTISTIC: [(&str, &str); 4] =
[("artistic.cutout", "{}"), ("artistic.filmGrain", "{}"), ("artistic.neonGlow", "{}"), ("artistic.posterEdges", "{}")];
/// Effect › Distort (Diffuse Glow with its highlights glowing from a low brightness up, as the
/// striped art's reds are dark).
@@ -175,6 +176,51 @@ fn halftone_pattern_settings_survive_a_save_and_open() {
}
}
/// The Artistic filters' settings are saved with the document and draw the same when it opens
/// again; changing any one of them draws differently.
#[test]
fn artistic_settings_survive_a_save_and_open() {
for (id, set, changes) in [
(
"artistic.cutout",
json!({"levels": 3.0, "edgeSimplicity": 2.0, "edgeFidelity": 3.0}),
[json!({"levels": 2.0}), json!({"edgeSimplicity": 10.0})],
),
(
"artistic.neonGlow",
json!({"glowSize": -6.0, "glowBrightness": 30.0, "color": "#ff3300"}),
[json!({"glowSize": 6.0}), json!({"color": "#00ff00"})],
),
(
"artistic.posterEdges",
json!({"edgeThickness": 4.0, "edgeIntensity": 5.0, "posterization": 1.0}),
[json!({"edgeIntensity": 0.0}), json!({"posterization": 6.0})],
),
] {
let (mut s, g) = striped();
// Black and a grey beside the reds: Cutout puts the grey with black at 2 levels and apart
// from it at 3.
let mut ids = vec![g];
for (x, color) in [(55, "#000000"), (115, "#666666")] {
let id = s.execute("shape.ellipse", &json!({"x": x, "y": 60, "width": 30, "height": 30})).unwrap()["id"].as_u64().unwrap();
s.execute("paint.setFill", &json!({"color": color, "ids": [id]})).unwrap();
ids.push(id);
}
s.execute("select.set", &json!({"ids": ids})).unwrap();
let g = s.execute("object.group", &json!({})).unwrap()["id"].as_u64().unwrap();
s.execute("effect.apply", &json!({"effect": id, "ids": [g], "params": set})).unwrap();
let drawn = render(&s);
let doc = vectorcraft_format::load(&vectorcraft_format::save(&s.doc().unwrap().doc, false)).unwrap();
assert_eq!(doc.node(NodeId(g)).unwrap().appearance.effects[0].params, set, "{id}");
assert_eq!(Renderer::new().render_region(&doc, Rect::new(0.0, 0.0, 200.0, 200.0), 1.0, true).pixels, drawn.pixels, "{id}");
for change in changes {
s.execute("effect.setParams", &json!({"index": 0, "ids": [g], "params": change})).unwrap();
assert!(mean_diff(&drawn, &render(&s)) > 0.001, "{id}: {change}");
s.execute("effect.setParams", &json!({"index": 0, "ids": [g], "params": set})).unwrap();
}
}
}
/// A PNG of rings around its center, 12 × 10 pixels.
fn rings_png() -> Vec<u8> {
let img = image::RgbaImage::from_fn(12, 10, |x, y| {
+3
View File
@@ -329,6 +329,9 @@ fn structured_junk() {
("effect.apply", json!({"effect": "pixelate.crystallize", "params": {"cellSize": -1e308}})),
("effect.apply", json!({"effect": "pixelate.mezzotint", "params": {"type": 5}})),
("effect.apply", json!({"effect": "pixelate.pointillize", "params": {"cellSize": "NaN"}})),
("effect.apply", json!({"effect": "artistic.cutout", "params": {"levels": 1e308, "edgeSimplicity": "NaN", "edgeFidelity": -1e308}})),
("effect.apply", json!({"effect": "artistic.neonGlow", "params": {"glowSize": -1e308, "glowBrightness": "1e999", "color": [1]}})),
("effect.apply", json!({"effect": "artistic.posterEdges", "params": {"edgeThickness": 1e308, "edgeIntensity": [], "posterization": -7}})),
("effect.apply", json!({"effect": "sketch.halftonePattern", "params": {"size": -1e308, "contrast": "NaN", "patternType": 3}})),
("effect.apply", json!({"effect": "texture.craquelure", "params": {"crackSpacing": -1e308, "crackDepth": "NaN", "crackBrightness": 1e308}})),
("effect.apply", json!({"effect": "texture.grain", "params": {"intensity": 1e308, "contrast": [1], "grainType": 7}})),
+3
View File
@@ -198,6 +198,9 @@ fn effect_dialog_refuses_a_color_it_cannot_draw() {
#[test]
fn raster_filter_dialogs_preview_and_apply() {
for (effect, key, value) in [
("artistic.cutout", "levels", "6"),
("artistic.neonGlow", "color", "#ff00ff"),
("artistic.posterEdges", "edgeThickness", "5"),
("blur.radial", "method", "zoom"),
("blur.smart", "quality", "high"),
("blur.smart", "blurMode", "overlayEdge"),
+14
View File
@@ -4385,6 +4385,20 @@ lighting Light Светлина
Reset Ruler Origin Нулиране началото на линийката
Artistic Художествени
Film Grain… Филмова зърнистост…
Cutout… Апликация…
Cutout Апликация
Neon Glow… Неоново сияние…
Neon Glow Неоново сияние
Poster Edges… Плакатни ръбове…
Poster Edges Плакатни ръбове
Levels Нива
Edge Simplicity Простота на ръбовете
Edge Fidelity Точност на ръбовете
Glow Size Размер на сиянието
Glow Brightness Яркост на сиянието
Edge Thickness Дебелина на ръбовете
Edge Intensity Интензивност на ръбовете
Posterization Постеризация
Highlight Area Зона на осветяване
@msg the compressed file unpacks to more than VectorCraft reads ({_1} MB of document, {_2} MB per image) компресираният файл се разпакетира до повече, отколкото VectorCraft чете ({_1} МБ документ, {_2} МБ на изображение)
@msg the document is too large to save compressed ({0}): save it with Use Compression off документът е твърде голям за запазване в компресиран вид ({0}): запазете го с изключена опция „Използване на компресия“
Can't render this file because it contains an unexpected character in line 2 and column 73.
+14
View File
@@ -4864,6 +4864,20 @@ imageTrace Outline Obrys
DWG (use DXF) DWG (použijte DXF)
Artistic Umělecké
Film Grain… Filmové zrno…
Cutout… Výřez…
Cutout Výřez
Neon Glow… Neonová záře…
Neon Glow Neonová záře
Poster Edges… Plakátové okraje…
Poster Edges Plakátové okraje
Levels Úrovně
Edge Simplicity Jednoduchost okrajů
Edge Fidelity Věrnost okrajů
Glow Size Velikost záře
Glow Brightness Jas záře
Edge Thickness Tloušťka okrajů
Edge Intensity Intenzita okrajů
Posterization Posterizace
Highlight Area Oblast světel
# Czech render-site follow-up (AI drafts; native review requested).
Can't render this file because it contains an unexpected character in line 2 and column 73.
+14
View File
@@ -4434,6 +4434,20 @@ lighting Light Licht
Reset Ruler Origin Linealursprung zurücksetzen
Artistic Kunstfilter
Film Grain… Filmkörnung…
Cutout… Ausschnitt…
Cutout Ausschnitt
Neon Glow… Neonglühen…
Neon Glow Neonglühen
Poster Edges… Posterkanten…
Poster Edges Posterkanten
Levels Stufen
Edge Simplicity Kantenvereinfachung
Edge Fidelity Kantentreue
Glow Size Leuchtgröße
Glow Brightness Leuchthelligkeit
Edge Thickness Kantenstärke
Edge Intensity Kantenintensität
Posterization Tontrennung
Grain Körnung
Highlight Area Lichterbereich
@msg the compressed file unpacks to more than VectorCraft reads ({_1} MB of document, {_2} MB per image) die komprimierte Datei entpackt sich auf mehr, als VectorCraft liest ({_1} MB Dokument, {_2} MB pro Bild)
Can't render this file because it contains an unexpected character in line 2 and column 73.
+14
View File
@@ -2146,6 +2146,20 @@ lighting Light Φως
Left Diagonal Αριστερή διαγώνιος
Artistic Καλλιτεχνικό
Film Grain… Κόκκος φιλμ…
Cutout… Αποκοπή…
Cutout Αποκοπή
Neon Glow… Λάμψη νέον…
Neon Glow Λάμψη νέον
Poster Edges… Άκρες αφίσας…
Poster Edges Άκρες αφίσας
Levels Επίπεδα
Edge Simplicity Απλότητα άκρων
Edge Fidelity Πιστότητα άκρων
Glow Size Μέγεθος λάμψης
Glow Brightness Φωτεινότητα λάμψης
Edge Thickness Πάχος άκρων
Edge Intensity Ένταση άκρων
Posterization Αφισοποίηση
Highlight Area Περιοχή φωτεινών τόνων
Skew: Λοξότητα:
Inside Εσωτερικά
Can't render this file because it contains an unexpected character in line 1921 and column 19.
+14
View File
@@ -4452,6 +4452,20 @@ lighting Light Luz
Reset Ruler Origin Restablecer el origen de las reglas
Artistic Artístico
Film Grain… Granulado de película…
Cutout… Recorte…
Cutout Recorte
Neon Glow… Resplandor de neón…
Neon Glow Resplandor de neón
Poster Edges… Bordes de póster…
Poster Edges Bordes de póster
Levels Niveles
Edge Simplicity Simplicidad de bordes
Edge Fidelity Fidelidad de bordes
Glow Size Tamaño del resplandor
Glow Brightness Brillo del resplandor
Edge Thickness Grosor de bordes
Edge Intensity Intensidad de bordes
Posterization Posterización
Highlight Area Área de iluminaciones
@msg the compressed file unpacks to more than VectorCraft reads ({_1} MB of document, {_2} MB per image) el archivo comprimido ocupa al descomprimirse más de lo que VectorCraft lee ({_1} MB de documento, {_2} MB por imagen)
@msg the document is too large to save compressed ({0}): save it with Use Compression off el documento es demasiado grande para guardarlo comprimido ({0}): guárdelo con Usar compresión desactivado
Can't render this file because it contains an unexpected character in line 2 and column 73.
+14
View File
@@ -4772,6 +4772,20 @@ imageTrace Outline Ääriviiva
DWG (use DXF) DWG (käytä DXF:ää)
Artistic Taiteelliset
Film Grain… Filmin rakeisuus…
Cutout… Leikkaus…
Cutout Leikkaus
Neon Glow… Neonhehku…
Neon Glow Neonhehku
Poster Edges… Julisteen reunat…
Poster Edges Julisteen reunat
Levels Tasot
Edge Simplicity Reunojen yksinkertaisuus
Edge Fidelity Reunojen tarkkuus
Glow Size Hehkun koko
Glow Brightness Hehkun kirkkaus
Edge Thickness Reunojen paksuus
Edge Intensity Reunojen voimakkuus
Posterization Posterointi
Highlight Area Korostusalue
file-filter All readable files Kaikki luettavat tiedostot
file-filter All placeable files Kaikki sijoitettavat tiedostot
Can't render this file because it contains an unexpected character in line 2 and column 73.
+14
View File
@@ -4462,6 +4462,20 @@ lighting Light Lumière
Reset Ruler Origin Réinitialiser l’origine des règles
Artistic Artistique
Film Grain… Grain photographique…
Cutout… Découpage…
Cutout Découpage
Neon Glow… Néon…
Neon Glow Néon
Poster Edges… Bords d’affiche…
Poster Edges Bords d’affiche
Levels Niveaux
Edge Simplicity Simplicité des contours
Edge Fidelity Fidélité des contours
Glow Size Taille de la lueur
Glow Brightness Luminosité de la lueur
Edge Thickness Épaisseur des contours
Edge Intensity Intensité des contours
Posterization Postérisation
Highlight Area Zone des tons clairs
@msg the compressed file unpacks to more than VectorCraft reads ({_1} MB of document, {_2} MB per image) le fichier compressé dépasse une fois décompressé ce que VectorCraft lit ({_1} Mo de document, {_2} Mo par image)
@msg the document is too large to save compressed ({0}): save it with Use Compression off le document est trop volumineux pour être enregistré compressé ({0}) : enregistrez-le avec Utiliser la compression désactivé
Can't render this file because it contains an unexpected character in line 2 and column 73.
+14
View File
@@ -1556,6 +1556,20 @@ file-filter Templates Sablonok
Select Segments Szakaszok kijelölése
Artistic Művészi
Film Grain… Filmszemcse…
Cutout… Kivágás…
Cutout Kivágás
Neon Glow… Neonfény…
Neon Glow Neonfény
Poster Edges… Poszterszélek…
Poster Edges Poszterszélek
Levels Szintek
Edge Simplicity Élek egyszerűsége
Edge Fidelity Élek hűsége
Glow Size Fény mérete
Glow Brightness Fény fényereje
Edge Thickness Élek vastagsága
Edge Intensity Élek intenzitása
Posterization Poszterizálás
Highlight Area Csúcsfény-terület
English English
Graph Design Diagramminta
Can't render this file because it contains an unexpected character in line 2 and column 73.
+14
View File
@@ -4479,6 +4479,20 @@ lighting Light Luce
Reset Ruler Origin Ripristina origine dei righelli
Artistic Artistico
Film Grain… Grana della pellicola…
Cutout… Ritaglio…
Cutout Ritaglio
Neon Glow… Bagliore neon…
Neon Glow Bagliore neon
Poster Edges… Bordi poster…
Poster Edges Bordi poster
Levels Livelli
Edge Simplicity Semplicità dei bordi
Edge Fidelity Fedeltà dei bordi
Glow Size Dimensione del bagliore
Glow Brightness Luminosità del bagliore
Edge Thickness Spessore dei bordi
Edge Intensity Intensità dei bordi
Posterization Posterizzazione
Highlight Area Area luci
@msg the compressed file unpacks to more than VectorCraft reads ({_1} MB of document, {_2} MB per image) il file compresso, una volta decompresso, supera ciò che VectorCraft legge ({_1} MB di documento, {_2} MB per immagine)
@msg the document is too large to save compressed ({0}): save it with Use Compression off il documento è troppo grande per essere salvato compresso ({0}): salvalo con Usa compressione disattivato
Can't render this file because it contains an unexpected character in line 2 and column 73.
+14
View File
@@ -4519,6 +4519,20 @@ lighting Light ライト
Reset Ruler Origin 定規の原点をリセット
Artistic アーティスティック
Film Grain… フィルム粒子…
Cutout… カットアウト…
Cutout カットアウト
Neon Glow… ネオングロー…
Neon Glow ネオングロー
Poster Edges… ポスターエッジ…
Poster Edges ポスターエッジ
Levels レベル数
Edge Simplicity エッジの単純さ
Edge Fidelity エッジの正確さ
Glow Size 光彩のサイズ
Glow Brightness 光彩の明るさ
Edge Thickness エッジの太さ
Edge Intensity エッジの強さ
Posterization ポスタリゼーション
Highlight Area ハイライト領域
@msg the compressed file unpacks to more than VectorCraft reads ({_1} MB of document, {_2} MB per image) 圧縮ファイルを展開すると VectorCraft が読み込める量を超えます (ドキュメント {_1} MB、画像 1 枚あたり {_2} MB)
@msg the document is too large to save compressed ({0}): save it with Use Compression off ドキュメントが大きすぎて圧縮保存できません ({0}): 「圧縮を使用」をオフにして保存してください
Can't render this file because it contains an unexpected character in line 2 and column 73.
+14
View File
@@ -4443,6 +4443,20 @@ lighting Light 광원
X11 (XWayland) X11 (XWayland)
Artistic 예술 효과
Film Grain… 필름 그레인…
Cutout… 오려내기…
Cutout 오려내기
Neon Glow… 네온 글로…
Neon Glow 네온 글로
Poster Edges… 포스터 가장자리…
Poster Edges 포스터 가장자리
Levels 레벨 수
Edge Simplicity 가장자리 단순화
Edge Fidelity 가장자리 충실도
Glow Size 광선 크기
Glow Brightness 광선 밝기
Edge Thickness 가장자리 두께
Edge Intensity 가장자리 강도
Posterization 포스터화
Highlight Area 하이라이트 영역
@msg the compressed file unpacks to more than VectorCraft reads ({_1} MB of document, {_2} MB per image) 압축된 파일의 압축을 풀면 VectorCraft가 읽을 수 있는 한도를 넘습니다(문서 {_1}MB, 이미지당 {_2}MB)
@msg the document is too large to save compressed ({0}): save it with Use Compression off 문서가 너무 커서 압축 저장할 수 없습니다({0}): 압축 사용을 끄고 저장하세요
Can't render this file because it contains an unexpected character in line 2 and column 73.
+14
View File
@@ -4691,6 +4691,20 @@ imageTrace Outline Kontur
DWG (use DXF) DWG (użyj DXF)
Artistic Artystyczne
Film Grain… Ziarno filmu…
Cutout… Wycinanka…
Cutout Wycinanka
Neon Glow… Neonowa poświata…
Neon Glow Neonowa poświata
Poster Edges… Krawędzie plakatu…
Poster Edges Krawędzie plakatu
Levels Poziomy
Edge Simplicity Prostota krawędzi
Edge Fidelity Wierność krawędzi
Glow Size Rozmiar poświaty
Glow Brightness Jasność poświaty
Edge Thickness Grubość krawędzi
Edge Intensity Intensywność krawędzi
Posterization Posteryzacja
Highlight Area Obszar świateł
file-filter All readable files Wszystkie obsługiwane pliki
file-filter All placeable files Wszystkie pliki do umieszczenia
Can't render this file because it contains an unexpected character in line 2 and column 73.
+14
View File
@@ -3858,6 +3858,20 @@ axis Both Ambos
Reset Ruler Origin Redefinir origem das réguas
Artistic Artístico
Film Grain… Granulação de filme…
Cutout… Recorte…
Cutout Recorte
Neon Glow… Brilho neon…
Neon Glow Brilho neon
Poster Edges… Bordas de pôster…
Poster Edges Bordas de pôster
Levels Níveis
Edge Simplicity Simplicidade das bordas
Edge Fidelity Fidelidade das bordas
Glow Size Tamanho do brilho
Glow Brightness Intensidade do brilho
Edge Thickness Espessura das bordas
Edge Intensity Intensidade das bordas
Posterization Posterização
Grain Grão
Highlight Area Área de realces
@msg the compressed file unpacks to more than VectorCraft reads ({_1} MB of document, {_2} MB per image) o arquivo compactado se expande para mais do que o VectorCraft lê ({_1} MB de documento, {_2} MB por imagem)
Can't render this file because it contains an unexpected character in line 6 and column 47.
+14
View File
@@ -4475,6 +4475,20 @@ lighting Light Свет
Reset Ruler Origin Сбросить начало координат линеек
Artistic Художественные
Film Grain… Зернистость плёнки…
Cutout… Аппликация…
Cutout Аппликация
Neon Glow… Неоновое свечение…
Neon Glow Неоновое свечение
Poster Edges… Края плаката…
Poster Edges Края плаката
Levels Уровни
Edge Simplicity Простота краёв
Edge Fidelity Точность краёв
Glow Size Размер свечения
Glow Brightness Яркость свечения
Edge Thickness Толщина краёв
Edge Intensity Интенсивность краёв
Posterization Постеризация
Highlight Area Область светов
@msg the compressed file unpacks to more than VectorCraft reads ({_1} MB of document, {_2} MB per image) сжатый файл распаковывается в объём больше, чем читает VectorCraft ({_1} МБ документа, {_2} МБ на изображение)
@msg the document is too large to save compressed ({0}): save it with Use Compression off документ слишком велик для сохранения со сжатием ({0}): сохраните его с выключенным параметром «Использовать сжатие»
Can't render this file because it contains an unexpected character in line 2 and column 73.
+14
View File
@@ -4384,6 +4384,20 @@ lighting Light Ljus
Reset Ruler Origin Återställ linjalens origo
Artistic Konstnärligt
Film Grain… Filmkorn…
Cutout… Urklipp…
Cutout Urklipp
Neon Glow… Neonsken…
Neon Glow Neonsken
Poster Edges… Affischkanter…
Poster Edges Affischkanter
Levels Nivåer
Edge Simplicity Kantförenkling
Edge Fidelity Kanttrohet
Glow Size Skenets storlek
Glow Brightness Skenets ljusstyrka
Edge Thickness Kanttjocklek
Edge Intensity Kantintensitet
Posterization Posterisering
Highlight Area Högdagerområde
@msg the compressed file unpacks to more than VectorCraft reads ({_1} MB of document, {_2} MB per image) den komprimerade filen packas upp till mer än VectorCraft läser ({_1} MB dokument, {_2} MB per bild)
@msg the document is too large to save compressed ({0}): save it with Use Compression off dokumentet är för stort för att sparas komprimerat ({0}): spara det med Använd komprimering avstängt
Can't render this file because it contains an unexpected character in line 5 and column 25.
+14
View File
@@ -4459,6 +4459,20 @@ lighting Light Світло
Reset Ruler Origin Скинути початок координат лінійок
Artistic Художні
Film Grain… Зернистість плівки…
Cutout… Аплікація…
Cutout Аплікація
Neon Glow… Неонове сяйво…
Neon Glow Неонове сяйво
Poster Edges… Плакатні краї…
Poster Edges Плакатні краї
Levels Рівні
Edge Simplicity Простота країв
Edge Fidelity Точність країв
Glow Size Розмір сяйва
Glow Brightness Яскравість сяйва
Edge Thickness Товщина країв
Edge Intensity Інтенсивність країв
Posterization Постеризація
Highlight Area Область світлих тонів
@msg the compressed file unpacks to more than VectorCraft reads ({_1} MB of document, {_2} MB per image) стислий файл розпаковується в обсяг більший, ніж читає VectorCraft ({_1} МБ документа, {_2} МБ на зображення)
@msg the document is too large to save compressed ({0}): save it with Use Compression off документ завеликий для збереження зі стиском ({0}): збережіть його з вимкненим параметром «Використовувати стиск»
Can't render this file because it contains an unexpected character in line 722 and column 40.
+14
View File
@@ -1248,6 +1248,20 @@
Fill: click for swatches, Shift-click for the color mixer Tô: bấm để xem mẫu màu, Shift-bấm để trộn màu
Fills Vùng tô
Film Grain… Hạt phim…
Cutout… Cắt dán…
Cutout Cắt dán
Neon Glow… Ánh neon…
Neon Glow Ánh neon
Poster Edges… Viền áp phích…
Poster Edges Viền áp phích
Levels Số mức
Edge Simplicity Độ đơn giản của cạnh
Edge Fidelity Độ trung thực của cạnh
Glow Size Kích thước ánh sáng
Glow Brightness Độ sáng của ánh sáng
Edge Thickness Độ dày cạnh
Edge Intensity Cường độ cạnh
Posterization Áp phích hóa
Find Tìm
Find and Replace… Tìm và thay thế…
Find Font Tìm phông
1 # Vietnamese (vi) catalog. Same format as zh-hant.tsv: context <TAB> source <TAB> translation
1248
1249
1250
1251
1252
1253
1254
1255
1256
1257
1258
1259
1260
1261
1262
1263
1264
1265
1266
1267
+14
View File
@@ -4747,6 +4747,20 @@ lighting Light 光源
Reset Ruler Origin 重置标尺原点
Artistic 艺术效果
Film Grain… 胶片颗粒…
Cutout… 剪纸…
Cutout 剪纸
Neon Glow… 霓虹灯光…
Neon Glow 霓虹灯光
Poster Edges… 海报边缘…
Poster Edges 海报边缘
Levels 色阶数
Edge Simplicity 边缘简化度
Edge Fidelity 边缘逼真度
Glow Size 发光大小
Glow Brightness 发光亮度
Edge Thickness 边缘厚度
Edge Intensity 边缘强度
Posterization 海报化
Highlight Area 高光区域
Concentric Dividers: 同心分隔线:
Radial Dividers: 径向分隔线:
Can't render this file because it contains an unexpected character in line 2 and column 73.
+14
View File
@@ -3819,6 +3819,20 @@ lighting Light 光源
Reset Ruler Origin 重設尺標原點
Artistic 藝術效果
Film Grain… 底片顆粒…
Cutout… 剪紙…
Cutout 剪紙
Neon Glow… 霓虹光…
Neon Glow 霓虹光
Poster Edges… 海報邊緣…
Poster Edges 海報邊緣
Levels 色階數
Edge Simplicity 邊緣簡化度
Edge Fidelity 邊緣逼真度
Glow Size 光暈大小
Glow Brightness 光暈亮度
Edge Thickness 邊緣厚度
Edge Intensity 邊緣強度
Posterization 海報化
Highlight Area 亮部範圍
Concentric Dividers: 同心分隔線:
Radial Dividers: 徑向分隔線:
Can't render this file because it contains an unexpected character in line 6 and column 47.
+5 -4
View File
@@ -69,13 +69,14 @@ fn effect_menu_lists_outline_object_between_offset_path_and_outline_stroke() {
}
#[test]
fn effect_menu_lists_film_grain_in_artistic_before_blur() {
fn effect_menu_lists_the_artistic_filters_before_blur() {
let app = app();
let entries = menus::menu_entries(&app);
let artistic: Vec<_> = entries.iter().filter(|e| e.path == ["Effect", "Artistic"]).collect();
assert_eq!(artistic.len(), 1);
assert_eq!(artistic[0].label, "Film Grain…");
assert_eq!(artistic[0].command.as_deref(), Some("effect.dialog"));
let labels: Vec<&str> = artistic.iter().map(|e| e.label.as_str()).collect();
assert_eq!(labels, ["Cutout…", "Film Grain…", "Neon Glow…", "Poster Edges…"]);
assert!(artistic.iter().all(|e| e.command.as_deref() == Some("effect.dialog")));
assert_eq!(artistic[2].params, json!({"effect": "artistic.neonGlow"}));
let at = |name: &str| entries.iter().position(|e| e.path == ["Effect", name]).unwrap();
assert!(at("Artistic") < at("Blur"));
}
+18 -3
View File
@@ -12,7 +12,7 @@ Part of [target-app-parity.md](target-app-parity.md); open work is in [gaps.md](
| Group | Illustrator | VectorCraft | % | Remaining (h) |
|---|---:|---:|---:|---:|
| Illustrator effects (vector and stylize) | 50 | 45 | 90% | 6–10 |
| Photoshop effects (raster, Effect Gallery) | 57 | 30 | 53% | 22–38 |
| Photoshop effects (raster, Effect Gallery) | 57 | 33 | 58% | 21–35 |
| 3D and Materials | 4 (+3 Classic) | 4 (initial: Extrude & Bevel, Revolve, Inflate, Rotate) | ~25% | 25–45 |
| Beyond Illustrator: Effect › Color Adjustments (6), effect plug-ins (WebAssembly) | — | 6 + plug-ins | — | — |
@@ -38,7 +38,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 |
| 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 | 4/15 (approximate Cutout, Film Grain, Neon Glow, Poster Edges) | Colored Pencil, Dry Brush, Fresco, Paint Daubs, Palette Knife, Plastic Wrap, Rough Pastels, Smudge Stick, Sponge, Underpainting, Watercolor |
| 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 | — |
@@ -52,7 +52,7 @@ its resolution, PDF export warns once per object with the requested and the lowe
The pixel-filter pipeline (`vectorcraft_effects::pixel`) is in place: each new filter is one function, a
catalogue row, a menu entry and translations. Measured throughput: 0.7–1.2 agent hours per filter
(see [calibration](target-app-parity.md#calibration)), so the 27 missing filters are ~18–33 h and the Effect
(see [calibration](target-app-parity.md#calibration)), so the 24 missing filters are ~17–29 h and the Effect
Gallery dialog another 4–6 h. These parallelize well across agents.
Film Grain (`artistic.filmGrain`, Effect › Artistic) adds stable monochrome noise to shadows and
@@ -68,6 +68,21 @@ mid-grey to a quarter-strength floor at full Intensity before highlight headroom
soft grain, black takes light speckles and white stays clean. The tonal curve and endpoint behaviour
are interpretations, not verified proprietary behaviour. Saturated colour channels may clip. The parameter
ranges and defaults are chosen for VectorCraft, not asserted as measured proprietary defaults.
Cutout, Neon Glow and Poster Edges (`artistic.cutout`, `artistic.neonGlow`, `artistic.posterEdges`)
follow the same two public references, with original algorithms. Cutout softens the object's colours (Edge Simplicity, less with Edge
Fidelity), splits each colour channel into Number of Levels steps, and paints each resulting piece
in the average colour that falls into it, so hues survive. Poster Edges
posterizes each colour channel to Posterization + 2 levels and draws black lines on the edges of
any channel and of the object's outline, wider with Edge Thickness and darker, on fainter edges
too, with Edge Intensity. Neon Glow redraws the object in dimmed greys screened with a glow colour
from its light areas (positive Glow Size) or its dark ones (negative), spread by the size and as
strong as Glow Brightness; with no foreground and background colours in Illustrator's model, the
glow colour is the effect's own parameter. All three keep coverage, work in document units at any
Document Raster Effects resolution, and use the generic effect dialog with live preview. Ranges and
defaults are chosen for VectorCraft. Evidence: `crates/effects/src/pixel/artistic.rs`, the
`artistic_*`, `cutout_*`, `poster_edges_*` and `neon_glow_*` tests in `crates/effects/src/pixel/tests.rs`,
and `artistic_settings_survive_a_save_and_open` in `crates/engine/src/tests_rasterfilters.rs`.
Near white, highlight headroom can limit grain equally at every Intensity; highlight lift still
depends on Intensity when Highlight Area is nonzero.
The two rotated, variance-normalised noise layers can leave a faint diagonal weave at high
+5 -5
View File
@@ -22,7 +22,7 @@ stage ([ROADMAP.md](../ROADMAP.md)).
| 5 | [G11 Correctness bugs in core tools](#g11-correctness-bugs-in-core-tools) | 8–15 | yes | [target-app-parity.md](target-app-parity.md) |
| 6 | [G4 Advanced type](#g4-advanced-type) | 34–47 | core part | [type-parity.md](type-parity.md) |
| 7 | [G12 Preferences and panels that don't act yet](#g12-preferences-and-panels-that-dont-act-yet) | 13–20 | yes | [ui-parity.md](ui-parity.md) |
| 8 | [G2 Photoshop-style raster effects and the Effect Gallery](#g2-photoshop-style-raster-effects-and-the-effect-gallery) | 22–38 | no | [effects-parity.md](effects-parity.md) |
| 8 | [G2 Photoshop-style raster effects and the Effect Gallery](#g2-photoshop-style-raster-effects-and-the-effect-gallery) | 21–35 | no | [effects-parity.md](effects-parity.md) |
| 9 | [G13 Illustrator 2026 (30.x) additions](#g13-illustrator-2026-30x-additions) | 15–25 | no | [target-app-parity.md](target-app-parity.md) |
| 10 | [G14 Pen hardware beyond Windows pressure](#g14-pen-hardware-beyond-windows-pressure) | 8–16 | macOS pressure | [hardware-parity.md](hardware-parity.md) |
| 11 | [G5 Brushes, symbols and libraries](#g5-brushes-symbols-and-libraries) | 16–27 | no | [target-app-parity.md](target-app-parity.md) |
@@ -209,13 +209,13 @@ 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:** 27 of 57 filters (the remaining 14 Artistic filters and 13 of Sketch's 14), the Effect Gallery dialog, Load
- **Missing:** 24 of 57 filters (the remaining 11 Artistic filters and 13 of Sketch's 14), the Effect Gallery dialog, Load
Texture for Texturizer. Measured in
[effects-parity.md](effects-parity.md).
- **Done:** the pixel-filter pipeline and 30 filters (Artistic: Film Grain, Blur, Brush Strokes, Distort, Pixelate, Sharpen, Sketch:
Halftone Pattern, Stylize, Texture, Video), Brush Strokes on 2026-10-11, Film Grain on 2026-10-12, Halftone Pattern on 2026-10-16, and Smart Blur's Edge Only and Overlay Edge
- **Done:** the pixel-filter pipeline and 33 filters (Artistic: Cutout, Film Grain, Neon Glow, Poster Edges, Blur, Brush Strokes, Distort, Pixelate, Sharpen, Sketch:
Halftone Pattern, Stylize, Texture, Video), Brush Strokes on 2026-10-11, Film Grain on 2026-10-12, Halftone Pattern on 2026-10-16, Cutout, Neon Glow and Poster Edges on 2026-10-16, and Smart Blur's Edge Only and Overlay Edge
modes.
- **Estimate:** 22–38 h (measured 0.7–1.2 h per filter); parallelizes well.
- **Estimate:** 21–35 h (measured 0.7–1.2 h per filter); parallelizes well.
## G13 Illustrator 2026 (30.x) additions
+2 -2
View File
@@ -33,7 +33,7 @@ Checked 2026-10-10 from the code, tests and open issues; no workflow fails, so V
2. **`.ai` files from real users** (G9): layer structure (#951), guides (#779). 10–20 h.
3. **Launch and stability** (G10): the Windows and Linux start-up reports. 20–35 h.
4. **Core-tool bugs** (G11): Shape Builder regions (#937). Asset Export ghost borders closed (#983 / #1037). 8–15 h.
5. **Raster effects** (G2) continue alongside: Artistic (Film Grain done; 14 left), Sketch (Halftone Pattern done; 13 left), then the Effect Gallery. 22–38 h.
5. **Raster effects** (G2) continue alongside: Artistic (Cutout, Film Grain, Neon Glow and Poster Edges done; 11 left), Sketch (Halftone Pattern done; 13 left), then the Effect Gallery. 21–35 h.
## Next
@@ -59,7 +59,7 @@ Checked 2026-10-10 from the code, tests and open issues; no workflow fails, so V
| M6 | Path operations (Pathfinder, Shape Builder, offset…) | ✅ mostly done (single-result Pathfinder names and SVG IDs retained (#1293); Divide open-cutting-path geometry, appearance and history verified (#1307; existing segment cap remains); Simplify preview/counts and rewritten-path component selection verified (#1272; auto/slider/advanced UI remains open); Compound appearance and member fill-rule edits verified (#1197); Boolean precision on almost-horizontal edges fixed and the property tests made deterministic; Shape Builder gap detection, large-offset bug open) | 3–6 |
| M7 | Type (point/area/path, editing, styles, OpenType, threading, glyphs) | 🟡 Fullwidth `!` / `?` keep an upright fallback in vertical type; Document-point character controls after object/group scaling (#1034; optional font-size preservation remains separate), Character/Paragraph Styles (explicit Auto-leading resets and local leading overrides), per-paragraph paragraph attributes, mixed paragraph alignment indication (headless regressions, full integration CI and visual review pass), Area Type Options (with Width/Height, Auto Size and Shrink Text to Fit; overflow reported to agents and vertical Align), type areas resized by their handles and reshaped with Direct Selection, threaded text, Fit Headline, Glyphs, OpenType panel, Find Font, installed system fonts in every font menu (fonts registered with Windows outside its font folders, the XDG and Flatpak host font folders on Linux, and fonts installed while the app runs, listed when it comes back to the front; searchable; the Character and Properties menus show each family's sample of the selected text, preview it live under the pointer or ↑/↓, filter by script and kind, list a family's styles, and star favourites; the Glyphs panel and Find Font's Replace With use the same menu without the live preview) and found by name on open/import (full-name aliases and Missing Fonts searches verified, #1258; Windows font-service QA pending), variable fonts' named instances as styles, CJK names in the UI, input methods (marked text in place, candidate window at the caret; Japanese IME), Text Wrap (offset, invert, both sides of an object; follows edits), Type on a Path effects and Options with draggable start/centre/end brackets (slide, flip), tab stops + Tabs panel (Above Text floats it over the selected text, its ruler at the canvas zoom so stops line up with the columns, #729), inline symbols in text (beyond parity), Hebrew/Arabic bidirectional text, Paragraph Direction, Every-line/Single-line Composer per object (ragged text too) done; tab leaders, spell check, vertical type pending | 45–60 |
| M8 | Transform & distort (Puppet Warp, Liquify tools, Envelopes, Blends, Perspective Grid) | ✅ done (M8.1–M8.20 on 2026-10-06, fidelity pass): Liquify Tool Options with one shared brush, pen pressure, hold-to-apply, a safe scope and incremental strokes; tool options that last; Puppet Warp rotation, multi-select, Control bar and a rest-shape session that follows Undo; Width tool on compound paths; Envelope dialogs with preview, Reset with Warp/Mesh, full Envelope Options, type/images/symbols/appearance/gradients/patterns distorted on canvas and in every export, warps that keep their frame, Edit Contents, mesh handles and Control bar; Blend Options, anchor-targeted Blend tool, editable spine, keys picked on the canvas, faithful interpolation and knockout; Perspective Grid with Define Grid, presets, lock/snap/rulers, widgets, the 1–4 plane switch, movable planes, Perspective Selection scale/copy/perpendicular/Transform Again, and type and symbols truly projected with Edit Text. Left: Anti-Alias and Preserve Shape output, warp-envelope point editing, brush interpolation in blends, Free Transform/Scale/Reflect along a rotated box | 2–4 |
| M9 | Live effects (+ 3D & Materials) | 🟡 2D effects done incl. Effect → Pathfinder, Color Adjustments and Document Raster Effects Settings; PDF export warns when the pixel cap lowers raster-effect resolution; Artistic › Film Grain and Sketch › Halftone Pattern done (30/57 Photoshop filters); Revolve with automatic smooth vector shading, default-on circular caps and continuous rims, Extrude & Bevel, Inflate and Rotate; SVG Filters, remaining raster effects and 3D materials, lights and shadows pending | 81–130 |
| M9 | Live effects (+ 3D & Materials) | 🟡 2D effects done incl. Effect → Pathfinder, Color Adjustments and Document Raster Effects Settings; PDF export warns when the pixel cap lowers raster-effect resolution; Artistic › Cutout, Film Grain, Neon Glow and Poster Edges and Sketch › Halftone Pattern done (33/57 Photoshop filters); Revolve with automatic smooth vector shading, default-on circular caps and continuous rims, Extrude & Bevel, Inflate and Rotate; SVG Filters, remaining raster effects and 3D materials, lights and shadows pending | 81–130 |
| M10 | Brushes, symbols, patterns, Repeat | 🟡 pattern swatches (5 tile types, Pattern Options, editing mode, SVG `<pattern>`/PDF export) and live Repeat (radial/grid/mirror) done; brushes/symbols in progress | 23–37 |
| M11 | Artboards & views (selected-set deletion shared by Artboards menu, trash and Properties, retaining artboard 1 when all are selected; Artboard deletion keys keep the first of an all-selected set and protect artwork; history prunes invalid indices while preserving valid multiselection (history tests and full workspace CI pass); click-to-fit objects with the Artboard tool (#1227), artboard escape routing and document setup modal handoff done; artboard panel/tool with Move Artwork for X/Y fields and Artboard Options, Trim View, middle-button pan, print tiling and the status bar's artboard navigator with synchronized Artboard tool selection done; multiple windows, presentation polish) | 🟡 | 15–25 |
| M12 | Advanced color & art (CMYK/ICC, separations, Gradient Mesh, Live Paint, Image Trace, Graphs) | 🟡 Gradient Mesh, Live Paint, Image Trace (15 presets, the 14 documented ones each with an enhanced version; Ignore Color, Shapes, Auto Grouping and Gradients options; measured in `image-trace-quality.md`; the Flat Logo mode places edges to a fraction of a pixel, restores sharp corners, snaps lines and circles, and refuses gradients), Recolor Artwork, colour management (ICC, soft proofing, separations preview), Graphs (all 9 tools, Graph Data/Type, group-selected series colours persist through regenerate/save/open) done; Graph Design and Marker designs, Transpose and Switch x/y, `|` label line breaks, the pie options, drop shadows, the legend across the top, overlapping widths, First Row/Column in Front, filled lines, kept label formatting and the Graph Data worksheet done; graph Column designs pending | 6–10 |
+8 -2
View File
@@ -12,7 +12,7 @@ the work list made from it. Deep areas have their own checklists: [UI and intera
| Number | Value | Kind |
|---|---|---|
| 2026-10-15 | minor | Image Trace retains nonzero positioned boundaries when a fixed-end returning piece has fewer than two fitted cubics. Source-generated tests check survival, winding, shared-edge reversal and Noise controls; see `image-trace-quality.md`. Scores and hours unchanged. |
| **Feature breadth** (does each Illustrator menu item, tool, effect and format exist?) | **~88%** | measured in part: menu tree 353 of 375 items wired (94.1%), tools 89 of 92 (97%), vector effects 44 of 50 (88%), Photoshop-style effects 30 of 57 (53%), 3D 4 of 4 (initial: Extrude & Bevel, Revolve, Inflate, Rotate); blended by the area weights below |
| **Feature breadth** (does each Illustrator menu item, tool, effect and format exist?) | **~88%** | measured in part: menu tree 353 of 375 items wired (94.1%), tools 89 of 92 (97%), vector effects 44 of 50 (88%), Photoshop-style effects 33 of 57 (58%), 3D 4 of 4 (initial: Extrude & Bevel, Revolve, Inflate, Rotate); blended by the area weights below |
| **Feature depth** (each area scored by behaviour, weighted) | **~75%** | estimated, area table below |
| **Ready for real work, mainstream illustrator** | **~65%** (60–70%) | estimated, see [Ready for real work](#ready-for-real-work) |
| **Ready for real work, full Illustrator** (decides the stage) | **~62%** (57–67%) | estimated: additive weighted sum over the dimensions, see [Ready for real work](#ready-for-real-work) |
@@ -75,7 +75,7 @@ dominate; 3D, graphs and automation are specialist). They sum to 105. Feature de
| Strokes, brushes, width profiles | 5 | 88% | Shift-double-click adjoining adjustment is implemented; remaining: Art brush Overlap, Pattern brush auto corners, brush libraries, pen pressure on macOS, tilt/bearing/rotation | 8–15 | estimated; width-point creation and adjoining adjustment regression-tested; dialog layout reviewed; evidence below; finite-arithmetic follow-up regressions and full CI passed (evidence below) |
| Appearance, transparency, graphic styles, masks | 5 | 97% | raster effect reach in preview bounds | 1–2 | estimated |
| Live vector effects | 5 | 85% | Outline Object, Effect › Rasterize, Pathfinder Hard Mix/Soft Mix/Trap, SVG Filters, the Transform effect's missing options (#885) | 6–10 | measured count 44/50, depth estimated; Warp Options controls verified by focused checks and original-image review |
| Raster effects (Effect Gallery) | 4 | 49% | 27 Photoshop-style filters (14 Artistic, 13 Sketch), the Effect Gallery dialog, Load Texture | 22–38 | measured count 30/57; texture redraw distances optimized (#1257), pixels verified; depth estimated |
| Raster effects (Effect Gallery) | 4 | 49% | 24 Photoshop-style filters (11 Artistic, 13 Sketch), the Effect Gallery dialog, Load Texture | 21–35 | measured count 33/57; texture redraw distances optimized (#1257), pixels verified; depth estimated |
| 3D and Materials (and Turntable) | 4 | ~25% | live Revolve with automatic smooth vector shading and default-on continuous circular caps, Extrude & Bevel (twist, taper, caps, five bevel shapes), Inflate and Rotate with Position presets (other types use flat shading; one light, painter visibility); materials, several lights, shadows, ray tracing, 3D Classic, Turntable views, 3D export | 30–55 | estimated (Extrude & Bevel, Inflate and Rotate added 2026-10-12) |
| Type core | 9 | 80% | Auto-leading style resets are implemented; remaining: hyphenation and justification options, Optical Margin Alignment, Middle Eastern features beyond bidi layout, optional group font-size preservation; transformed character controls fixed (#1034), see [type-parity.md](type-parity.md) | 14–19 | estimated; full-name font matches regression-tested (#1258); mixed paragraph alignment indication implemented; headless regressions, full integration CI and visual review pass; localized legacy family-name fallback with loaded/catalog regressions (#2208) |
| Type advanced (CJK, spelling, Touch Type, Retype) | 4 | 50% | CJK vertical composition (ruby, mojikumi), variable font axes, tab leaders, spell check, Touch Type, Retype | 20–28 | estimated; upright fallback for fullwidth `!` / `?` regression-tested; score and hours unchanged |
@@ -291,6 +291,12 @@ its `film_grain_*` tests and the canvas/export cases in `crates/engine/src/tests
This raises the measured Photoshop filter count to 29/57; the depth estimate is conservatively 49%.
The full feature/readiness headline remains unchanged at its existing rounding.
Cutout, Neon Glow and Poster Edges (G2, 2026-10-16) follow as original Artistic filters with live
preview, undo, appearance editing and save/reopen, keeping coverage at any Document Raster Effects
resolution. Evidence: `crates/effects/src/pixel/artistic.rs`, its `cutout_*`, `neon_glow_*` and
`poster_edges_*` tests, and `artistic_settings_survive_a_save_and_open` in
`crates/engine/src/tests_rasterfilters.rs`. Measured Photoshop filter count 33/57; depth unchanged.
## Detailed missing items per area (2026-10-05 table, still current unless the area table above says otherwise)
> **Duplicated rows (2026-10-14):** this table lists "Selection, transform & align tools" three times and "Drawing tools"