Merge pull request #2525 from vangel76/feat/2435-artistic-filters

Effects: Artistic: Smudge Stick, Sponge and Watercolor (#2435)
This commit is contained in:
BFlat
2026-10-20 06:41:08 +07:00
committed by GitHub
38 changed files with 748 additions and 43 deletions
+1 -1
View File
@@ -246,7 +246,7 @@ The scores are self-assessed from our code against public documentation;
- reliable exchange of .ai files (layer structure in real files; proprietary 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 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;
- about half of the raster effects (21 of 57 still missing; Cutout, Film Grain, Neon Glow, Poster Edges, Smudge Stick, Sponge and Watercolor 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 46/50, Photoshop effects 33/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 46/50, Photoshop effects 42/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% | 21–35 |
| Raster effects (Effect Gallery) — [effects-parity.md](docs/effects-parity.md) | 49% | 14–24 |
| 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: 24 filters and the Effect Gallery, 21–35 h.
5. **Raster effects** (G2) alongside: 15 filters (7 Artistic, 8 Sketch) and the Effect Gallery, 14–24 h.
6. **Then:** type core and advanced type (G4), the Illustrator 2026 additions (G13), 3D (G3).
## Progress log
+21
View File
@@ -377,6 +377,27 @@ pub fn effect_catalog() -> Vec<EffectInfo> {
"{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(
"artistic.smudgeStick",
"Smudge Stick…",
ARTISTIC,
"{strokeLength: 0..10 (2; length of the short diagonal strokes), highlightArea: 0..20 (12; widens the area counted as light), intensity: 0..10 (10; how far the light areas are lightened)} smudges the dark areas along short diagonal strokes; the lighter areas become brighter and lose detail. Coverage is kept. Original visual approximation; chosen VectorCraft defaults.",
json!({"strokeLength": 2.0, "highlightArea": 12.0, "intensity": 10.0}),
),
r(
"artistic.sponge",
"Sponge…",
ARTISTIC,
"{brushSize: 0..10 (2; size of the sponge's pores), definition: 0..25 (12; how strongly lighter and darker blobs contrast), smoothness: 1..15 (5; softer blob edges)} paints the object as if dabbed with a sponge, in highly textured areas of contrasting colour. Coverage is kept. Original visual approximation; chosen VectorCraft defaults.",
json!({"brushSize": 2.0, "definition": 12.0, "smoothness": 5.0}),
),
r(
"artistic.watercolor",
"Watercolor…",
ARTISTIC,
"{brushDetail: 1..14 (9; the larger, the more detail stays), shadowIntensity: 0..10 (1; deepens the shadows), texture: 1..3 (1; paper grain, whole numbers)} paints the object in a watercolor style: detail simplified, colour saturated and darker where tones change at edges. Coverage is kept. Original visual approximation; chosen VectorCraft defaults.",
json!({"brushDetail": 9.0, "shadowIntensity": 1.0, "texture": 1.0}),
),
r(
"blur.radial",
"Radial Blur…",
+14 -14
View File
@@ -64,9 +64,9 @@ impl StrokeDirection {
}
/// Along a right diagonal stroke, rising to the right (y down).
const RIGHT: Vec2 = Vec2::new(FRAC_1_SQRT_2, -FRAC_1_SQRT_2);
pub(super) const RIGHT: Vec2 = Vec2::new(FRAC_1_SQRT_2, -FRAC_1_SQRT_2);
/// Along a left diagonal stroke, falling to the right.
const LEFT: Vec2 = Vec2::new(FRAC_1_SQRT_2, FRAC_1_SQRT_2);
pub(super) const LEFT: Vec2 = Vec2::new(FRAC_1_SQRT_2, FRAC_1_SQRT_2);
/// The most samples a stroke averages, whatever its length.
const MAX_SAMPLES: usize = 16;
@@ -75,7 +75,7 @@ const MAX_SAMPLES: usize = 16;
const JITTER: f64 = 0.25;
/// How soft (points, the σ) the tones are that decide which strokes a pixel gets.
const TONE_SOFTNESS: f64 = 1.0;
pub(super) const TONE_SOFTNESS: f64 = 1.0;
/// Dark Strokes' short dark strokes and long light ones (points).
pub(super) const DARK_LENGTH: f64 = 3.0;
@@ -86,7 +86,7 @@ const INK_SOFTNESS: f64 = 0.6;
/// A pixel at 72 ppi (points): how far the neighbours of the Sobel operator and of sampling
/// between pixels add to a filter's reach.
const MARGIN: f64 = 1.0;
pub(super) const MARGIN: f64 = 1.0;
/// How far (document units) strokes `len` long reach: half their length, their random shift, the
/// softening between their samples and the pixels a sample is taken between.
@@ -137,18 +137,18 @@ pub(super) fn spray_shift(radius: f64) -> f64 {
/// The object's colours averaged along strokes `len` long: [`MAX_SAMPLES`] at most, the object
/// softened first when they lie more than a pixel apart.
struct Smear<'a> {
pub(super) struct Smear<'a> {
/// Premultiplied colours (0..1).
src: Vec<[f32; 4]>,
w: usize,
h: usize,
place: &'a Place,
len: f64,
pub(super) len: f64,
n: usize,
}
impl<'a> Smear<'a> {
fn new(px: &[[u8; 4]], w: usize, h: usize, place: &'a Place, len: f64) -> Self {
pub(super) fn new(px: &[[u8; 4]], w: usize, h: usize, place: &'a Place, len: f64) -> Self {
let len = if len.is_finite() { len.max(0.0) } else { 0.0 };
let span = len / place.px;
let n = if span.is_finite() { (span.ceil() as usize).saturating_add(1).clamp(1, MAX_SAMPLES) } else { MAX_SAMPLES };
@@ -218,7 +218,7 @@ impl<'a> Smear<'a> {
}
/// The stroke of pattern `salt` through `q` along `dir`, its rows `width` apart.
fn stroke(&self, salt: u64, q: Point, dir: Vec2, width: f64) -> [f32; 4] {
pub(super) fn stroke(&self, salt: u64, q: Point, dir: Vec2, width: f64) -> [f32; 4] {
self.at(q + dir * stroke_shift(salt, q, dir, self.len, width), dir)
}
}
@@ -242,37 +242,37 @@ fn stroke_shift(salt: u64, q: Point, dir: Vec2, len: f64, width: f64) -> f64 {
}
/// Smooth noise in [-1, 1) streaked along `dir`: blobs `along` long and `across` wide.
fn streak(salt: u64, q: Point, dir: Vec2, along: f64, across: f64) -> f64 {
pub(super) fn streak(salt: u64, q: Point, dir: Vec2, along: f64, across: f64) -> f64 {
let (u, v) = frame(q, dir);
2.0 * value_noise(salt, u / along.max(1e-3), v / across.max(1e-3)) - 1.0
}
/// The straight colour and coverage of premultiplied `pm` (0..1).
fn unpremultiply(pm: [f32; 4]) -> ([f32; 3], f32) {
pub(super) fn unpremultiply(pm: [f32; 4]) -> ([f32; 3], f32) {
let a = pm[3].clamp(0.0, 1.0);
if a > 1e-6 { ([pm[0], pm[1], pm[2]].map(|v| (v / a).clamp(0.0, 1.0)), a) } else { ([0.0; 3], 0.0) }
}
/// `a` + (`b` − `a`) × `t`, channel by channel.
fn mix<const N: usize>(a: [f32; N], b: [f32; N], t: f32) -> [f32; N] {
pub(super) fn mix<const N: usize>(a: [f32; N], b: [f32; N], t: f32) -> [f32; N] {
std::array::from_fn(|k| a[k] + (b[k] - a[k]) * t)
}
/// A smooth step from 0 at `t` ≤ 0 to 1 at `t` ≥ 1.
fn ramp(t: f64) -> f64 {
pub(super) fn ramp(t: f64) -> f64 {
let t = if t.is_finite() { t.clamp(0.0, 1.0) } else { 0.0 };
t * t * (3.0 - 2.0 * t)
}
/// Pixel `p` (premultiplied 0..255) as premultiplied 0..1.
fn unit(p: [u8; 4]) -> [f32; 4] {
pub(super) fn unit(p: [u8; 4]) -> [f32; 4] {
p.map(|v| f32::from(v) / 255.0)
}
/// The object's edges after softening it by `sigma` (document units): about 1 across a step from
/// black to white or from clear to opaque, the same at any resolution. Empty when the sizes don't
/// match.
fn edges(px: &[[u8; 4]], w: usize, h: usize, place: &Place, sigma: f64) -> Vec<f32> {
pub(super) fn edges(px: &[[u8; 4]], w: usize, h: usize, place: &Place, sigma: f64) -> Vec<f32> {
let mut tone: Vec<f32> = px.iter().map(|p| luma(straight(*p)) * f32::from(p[3]) / 255.0).collect();
let mut cover: Vec<f32> = px.iter().map(|p| f32::from(p[3]) / 255.0).collect();
let s = sigma / place.px;
+41 -2
View File
@@ -1,5 +1,5 @@
//! Photoshop-style raster effects (Effect › Artistic › Cutout, Film Grain, Neon Glow, Plastic Wrap and
//! Poster Edges, Blur › Radial Blur and Smart Blur, Brush Strokes › Accented Edges, Angled Strokes,
//! Poster Edges, Smudge Stick, Sponge and Watercolor, 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 › Chalk & Charcoal, Graphic Pen, Halftone Pattern, Plaster and
@@ -21,6 +21,7 @@ mod brushstrokes;
mod distort;
mod edges;
mod loaded;
mod paint;
mod pixelate;
mod sharpen;
mod sketch;
@@ -41,12 +42,15 @@ 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; 38] = [
pub const PIXEL_EFFECTS: [&str; 41] = [
"artistic.cutout",
"artistic.filmGrain",
"artistic.neonGlow",
"artistic.plasticWrap",
"artistic.posterEdges",
"artistic.smudgeStick",
"artistic.sponge",
"artistic.watercolor",
"blur.radial",
"blur.smart",
"brushStrokes.accentedEdges",
@@ -92,6 +96,16 @@ pub enum PixelFx {
/// 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 › Smudge Stick: the dark areas smudged along short diagonal strokes `stroke`
/// (0..10) long, the light areas (widened by `highlight`, 0..20) smudged longer and lightened
/// by up to `intensity` (0..10).
SmudgeStick { stroke: f64, highlight: f64, intensity: f64 },
/// Artistic › Sponge: soft blobs of lighter and darker colour about `brush` (0..10) large,
/// contrasting as `definition` (0..25) says, their edges as soft as `smoothness` (1..15).
Sponge { brush: f64, definition: f64, smoothness: f64 },
/// Artistic › Watercolor: detail simplified by a brush of `detail` (1..14), edges saturated,
/// shadows deepened by `shadow` (0..10), `texture` (1..3) grains of paper showing.
Watercolor { detail: f64, shadow: f64, texture: u32 },
/// 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).
@@ -268,6 +282,21 @@ pub(crate) fn parse(id: &str, p: &Value) -> Option<PixelFx> {
highlight: num(p, "highlightArea", 0.0).clamp(0.0, 20.0),
intensity: num(p, "intensity", 10.0).clamp(0.0, 10.0),
},
"artistic.smudgeStick" => PixelFx::SmudgeStick {
stroke: num(p, "strokeLength", 2.0).clamp(0.0, 10.0),
highlight: num(p, "highlightArea", 12.0).clamp(0.0, 20.0),
intensity: num(p, "intensity", 10.0).clamp(0.0, 10.0),
},
"artistic.sponge" => PixelFx::Sponge {
brush: num(p, "brushSize", 2.0).clamp(0.0, 10.0),
definition: num(p, "definition", 12.0).clamp(0.0, 25.0),
smoothness: num(p, "smoothness", 5.0).clamp(1.0, 15.0),
},
"artistic.watercolor" => PixelFx::Watercolor {
detail: num(p, "brushDetail", 9.0).clamp(1.0, 14.0),
shadow: num(p, "shadowIntensity", 1.0).clamp(0.0, 10.0),
texture: num(p, "texture", 1.0).clamp(1.0, 3.0).round() as u32,
},
"artistic.plasticWrap" => PixelFx::PlasticWrap {
highlight: num(p, "highlightStrength", 20.0).clamp(0.0, 20.0),
detail: num(p, "detail", 12.0).clamp(0.0, 15.0),
@@ -453,6 +482,8 @@ impl PixelFx {
| PixelFx::Plaster { .. }
| PixelFx::Reticulation { .. } => 0.0,
PixelFx::GlowingEdges { width, smoothness, .. } => width.max(smoothness * 3.0),
// These three keep the object's coverage.
PixelFx::SmudgeStick { .. } | PixelFx::Sponge { .. } | PixelFx::Watercolor { .. } => 0.0,
PixelFx::FilmGrain { .. }
| PixelFx::Cutout { .. }
| PixelFx::NeonGlow { .. }
@@ -480,6 +511,9 @@ impl PixelFx {
PixelFx::RadialBlur { .. } => None,
PixelFx::SmartBlur { radius, mode: SmartBlurMode::Normal, .. } => Some(radius),
PixelFx::SmartBlur { radius, samples, .. } => Some(blur::edge_reach(radius, samples)),
PixelFx::SmudgeStick { stroke, .. } => Some(paint::smudge_reach(stroke)),
PixelFx::Sponge { brush, smoothness, .. } => Some(paint::sponge_reach(brush, smoothness)),
PixelFx::Watercolor { detail, .. } => Some(paint::watercolor_reach(detail)),
PixelFx::AccentedEdges { width, smoothness, .. } => Some(brushstrokes::accent_reach(width, smoothness)),
// The strokes, and the softened tones that choose them.
PixelFx::AngledStrokes { length, .. } | PixelFx::GraphicPen { length, .. } => {
@@ -534,6 +568,9 @@ impl PixelFx {
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::SmudgeStick { stroke, highlight, intensity } => paint::smudge_stick(px, w, h, space, stroke, highlight, intensity),
PixelFx::Sponge { brush, definition, smoothness } => paint::sponge(px, w, h, space, brush, definition, smoothness),
PixelFx::Watercolor { detail, shadow, texture } => paint::watercolor(px, w, h, space, detail, shadow, texture),
PixelFx::PlasticWrap { highlight, detail, smoothness } => artistic::plastic_wrap(px, w, h, space, highlight, detail, smoothness),
PixelFx::NeonGlow { size, brightness, colour } => artistic::neon_glow(px, w, h, space, size, brightness, colour),
PixelFx::PosterEdges { thickness, intensity, posterization } => {
@@ -814,3 +851,5 @@ pub fn gaussian_rgba(data: &mut [u8], w: usize, h: usize, sigma: f64) {
#[cfg(test)]
mod tests;
#[cfg(test)]
mod tests_paint;
+173
View File
@@ -0,0 +1,173 @@
//! Effect › Artistic › Smudge Stick, Sponge and Watercolor. 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> (Smudge Stick smudges the
//! dark areas with short diagonal strokes and brightens the light ones, Sponge paints highly
//! textured areas of contrasting colour, Watercolor simplifies detail with a medium brush loaded
//! with water and saturates the colour where tones change at edges). Visual approximations, not
//! reproductions of a proprietary algorithm; the ranges and defaults are chosen for VectorCraft.
//!
//! All three keep the object's coverage, so they paint nothing outside it. Their patterns lie in
//! document space around the object's centre, as the Brush Strokes' and Texture filters' do.
use super::blur;
use super::brushstrokes::{MARGIN, RIGHT, Smear, TONE_SOFTNESS, edges, mix, ramp, streak, stroke_reach, unpremultiply};
use super::pixelate::Place;
use super::texture::{Tones, luma, premultiply, smooth_noise, straight};
use super::{PixelSpace, Px16, gaussian, to16};
const SALT_SMUDGE: u64 = 320;
const SALT_SPONGE: u64 = 328;
const SALT_WATER: u64 = 336;
/// The length (points) of Smudge Stick's short strokes for Stroke Length `stroke` (0..10); the
/// light areas are smudged with strokes twice as long.
fn smudge_length(stroke: f64) -> f64 {
1.0 + 1.5 * stroke.clamp(0.0, 10.0)
}
/// How far (document units) Smudge Stick reaches for Stroke Length `stroke`: its strokes and the
/// softened tones that choose them.
pub(super) fn smudge_reach(stroke: f64) -> f64 {
stroke_reach(2.0 * smudge_length(stroke)).max(3.0 * TONE_SOFTNESS + MARGIN)
}
/// The σ (points) Sponge softens the object by, for Brush Size `brush` (0..10) and Smoothness
/// `smoothness` (1..15).
fn sponge_softness(brush: f64, smoothness: f64) -> f64 {
(0.15 + 0.04 * smoothness.clamp(1.0, 15.0)) * sponge_pore(brush)
}
/// The size (points) of Sponge's pores for Brush Size `brush` (0..10).
fn sponge_pore(brush: f64) -> f64 {
2.0 + 1.5 * brush.clamp(0.0, 10.0)
}
/// How far (document units) Sponge reaches: the softened colours around a pixel.
pub(super) fn sponge_reach(brush: f64, smoothness: f64) -> f64 {
3.0 * sponge_softness(brush, smoothness) + MARGIN
}
/// The radius (points) of the brush that simplifies detail for Brush Detail `detail` (1..14): the
/// less detail, the wider the brush.
fn water_brush(detail: f64) -> f64 {
0.5 * (15.0 - detail.clamp(1.0, 14.0))
}
/// The σ (points) of the softening that finds Watercolor's edges.
const WATER_EDGE: f64 = 0.6;
/// How far (document units) Watercolor reaches for Brush Detail `detail`.
pub(super) fn watercolor_reach(detail: f64) -> f64 {
water_brush(detail) + 3.0 * WATER_EDGE + MARGIN
}
/// Smudge Stick: the dark areas smudged along short diagonal strokes `stroke` (0..10) long, the
/// light areas smudged with longer strokes and lightened towards white as strongly as `intensity`
/// (0..10) says, so they lose their detail; `highlight` (0..20) widens the area counted as light.
pub(super) fn smudge_stick(px: &mut [[u8; 4]], w: usize, h: usize, space: &PixelSpace, stroke: f64, highlight: f64, intensity: f64) {
if ![stroke, highlight, intensity].iter().all(|v| v.is_finite()) {
return;
}
let Some(place) = Place::new(space, w, h) else { return };
let len = smudge_length(stroke);
let (short, long) = (Smear::new(px, w, h, &place, len), Smear::new(px, w, h, &place, 2.0 * len));
let tones = Tones::new(px, w, h, TONE_SOFTNESS / place.px);
// Tones above the threshold count as light; a wider highlight area lowers it.
let threshold = 0.9 - 0.5 * (highlight / 20.0).clamp(0.0, 1.0);
let lift = (0.7 * intensity / 10.0).clamp(0.0, 0.7) as f32;
let row = (0.25 * len).max(1.0);
place.each(px, |x, y, p| {
if p[3] == 0 {
return;
}
let q = place.at(x, y);
let own = straight(*p);
let paint = |smear: &Smear, salt: u64| {
let (c, a) = unpremultiply(smear.stroke(salt, q, RIGHT, row));
if a > 0.0 { c } else { own }
};
// Bristle marks along the dark strokes.
let bristle = (1.0 + 0.08 * streak(SALT_SMUDGE + 2, q, RIGHT, len, 0.5)) as f32;
let dark = paint(&short, SALT_SMUDGE).map(|v| v * bristle);
let light = paint(&long, SALT_SMUDGE + 1);
let t = ramp((tones.get(x, y) - threshold) / 0.2 + 0.5) as f32;
let bright = light.map(|v| v + (1.0 - v) * lift);
*p = premultiply(mix(dark, bright, t), f32::from(p[3]) / 255.0);
});
}
/// Sponge: the object as if dabbed with a sponge, soft blobs of lighter and darker colour about
/// `brush` (0..10) large; `definition` (0..25) sets how strongly they contrast and `smoothness`
/// (1..15) how soft their edges are.
pub(super) fn sponge(px: &mut [[u8; 4]], w: usize, h: usize, space: &PixelSpace, brush: f64, definition: f64, smoothness: f64) {
if ![brush, definition, smoothness].iter().all(|v| v.is_finite()) {
return;
}
let Some(place) = Place::new(space, w, h) else { return };
let mut soft: Vec<Px16> = to16(px);
gaussian(&mut soft, w, sponge_softness(brush, smoothness) / place.px);
let pore = sponge_pore(brush);
let edge = 0.12 + 0.03 * smoothness.clamp(1.0, 15.0);
let amount = (definition / 25.0).clamp(0.0, 1.0);
let gain = 1.0 + 1.2 * amount;
place.each(px, |x, y, p| {
if p[3] == 0 {
return;
}
let alpha = f32::from(p[3]) / 255.0;
let own = straight(*p);
let (c, a) = unpremultiply(soft.get(y * w + x).map_or([0.0; 4], |s| s.map(|v| f32::from(v) / 65535.0)));
let c = if a > 0.0 { c } else { own };
let q = place.at(x, y);
let n = 0.65 * smooth_noise(SALT_SPONGE, q, pore) + 0.35 * smooth_noise(SALT_SPONGE + 1, q, 0.45 * pore);
// -1 inside a dark pore, 1 on a light bridge between pores.
let blob = 2.0 * ramp(0.5 + n / (2.0 * edge)) - 1.0;
let l = f64::from(luma(c));
let target = (0.5 + (l + 0.55 * amount * blob - 0.5) * gain).clamp(0.0, 1.0);
// Darker towards black, lighter towards white: the hue stays.
let out = if target <= l {
let k = if l > 1e-3 { (target / l) as f32 } else { 0.0 };
c.map(|v| v * k)
} else {
let k = ((target - l) / (1.0 - l).max(1e-3)).clamp(0.0, 1.0) as f32;
c.map(|v| v + (1.0 - v) * k)
};
*p = premultiply(out, alpha);
});
}
/// Watercolor: the object's detail simplified by a brush whose size `detail` (1..14) sets (the
/// larger, the more detail stays), the colour saturated and its pigment pooled where tones change
/// at edges, the shadows deepened as strongly as `shadow` (0..10) says, and `texture` (1..3)
/// grains of paper showing through.
pub(super) fn watercolor(px: &mut [[u8; 4]], w: usize, h: usize, space: &PixelSpace, detail: f64, shadow: f64, texture: u32) {
if ![detail, shadow].iter().all(|v| v.is_finite()) {
return;
}
let Some(place) = Place::new(space, w, h) else { return };
let coverage: Vec<u8> = px.iter().map(|p| p[3]).collect();
// Simplify: average within areas of similar colour, keeping the bolder edges; a coarser brush
// (less detail) also merges colours further apart.
blur::smart(px, w, h, water_brush(detail) / place.px, 20.0 + 6.0 * (14.0 - detail.clamp(1.0, 14.0)), 7);
let found = edges(px, w, h, &place, WATER_EDGE);
let shadow = shadow.clamp(0.0, 10.0);
let paper = 0.02 * f64::from(texture.clamp(1, 3));
let grain = 1.0 + 0.5 * f64::from(texture.clamp(1, 3));
place.each(px, |x, y, p| {
let alpha = coverage.get(y * w + x).copied().unwrap_or(0);
if alpha == 0 {
*p = [0; 4];
return;
}
let c = straight(*p);
let e = (2.0 * f64::from(found.get(y * w + x).copied().unwrap_or(0.0))).clamp(0.0, 1.0) as f32;
let l = luma(c);
// Saturated colour and darker pigment where the tones change.
let c = c.map(|v| (l + (v - l) * (1.0 + 0.9 * e)) * (1.0 - 0.12 * e));
let deep = ((1.0 - l / 0.5).clamp(0.0, 1.0) * 0.07 * shadow as f32).min(0.8);
let q = place.at(x, y);
let n = 0.6 * smooth_noise(SALT_WATER, q, grain) + 0.4 * smooth_noise(SALT_WATER + 1, q, 0.5 * grain);
let n = (paper * n) as f32;
*p = premultiply(c.map(|v| v * (1.0 - deep) + n), f32::from(alpha) / 255.0);
});
}
+189
View File
@@ -0,0 +1,189 @@
//! Artistic › Smudge Stick, Sponge and Watercolor.
use serde_json::json;
use vectorcraft_doc::Effect;
use vectorcraft_geom::{Affine, Point};
use super::*;
use crate::{RasterFx, raster_effects};
/// A `w` × `h` raster whose pixels are document points (origin top-left), of an object that fills it.
fn space(w: usize, h: usize) -> PixelSpace {
let (w, h) = (w as f64, h as f64);
PixelSpace {
to_doc: Affine::IDENTITY,
px: 1.0,
center: Point::new(w / 2.0, h / 2.0),
channels: Channels::Rgb,
line: 1.0,
extent: 0.5 * w.hypot(h),
}
}
fn image(w: usize, h: usize, f: impl Fn(usize, usize) -> [u8; 4]) -> Vec<u8> {
(0..h).flat_map(|y| (0..w).map(move |x| (x, y))).flat_map(|(x, y)| f(x, y)).collect()
}
fn fx(id: &str, p: serde_json::Value) -> PixelFx {
let e = Effect { id: id.into(), params: p, visible: true };
match raster_effects(&[e]).first() {
Some(RasterFx::Pixel(p)) => p.clone(),
other => panic!("{id}: {other:?}"),
}
}
fn run(id: &str, p: serde_json::Value, src: &[u8], w: usize, h: usize) -> Vec<u8> {
let mut out = src.to_vec();
fx(id, p).apply(&mut out, w, h, &space(w, h));
out
}
fn grey(v: u8) -> [u8; 4] {
[v, v, v, 255]
}
/// The mean of channel 0 over the columns `x0..x1`.
fn mean(d: &[u8], w: usize, x0: usize, x1: usize) -> f64 {
let rows = d.len() / 4 / w;
let sum: f64 = (0..rows).flat_map(|y| (x0..x1).map(move |x| (x, y))).map(|(x, y)| f64::from(d[(y * w + x) * 4])).sum();
sum / (rows * (x1 - x0)) as f64
}
/// The summed difference between horizontal neighbours of channel 0 over columns `x0..x1`.
fn roughness(d: &[u8], w: usize, x0: usize, x1: usize) -> f64 {
let rows = d.len() / 4 / w;
(0..rows)
.flat_map(|y| (x0..x1 - 1).map(move |x| (x, y)))
.map(|(x, y)| (f64::from(d[(y * w + x) * 4]) - f64::from(d[(y * w + x + 1) * 4])).abs())
.sum()
}
fn spread(d: &[u8]) -> f64 {
let v: Vec<f64> = d.as_chunks::<4>().0.iter().map(|p| f64::from(p[0])).collect();
let m = v.iter().sum::<f64>() / v.len() as f64;
(v.iter().map(|x| (x - m).powi(2)).sum::<f64>() / v.len() as f64).sqrt()
}
/// No panic and the coverage kept on tiny, huge and non-finite spaces and parameters.
fn survives(id: &str, params: &[serde_json::Value]) {
let (w, h) = (9, 7);
let src = image(w, h, |x, y| [(x * 25) as u8, (y * 30) as u8, 90, if x == 0 { 0 } else { 255 }]);
let tiny = PixelSpace { px: 1e-9, ..space(w, h) };
let huge = PixelSpace { px: 1e9, center: Point::new(f64::MAX, -1e300), ..space(w, h) };
let nan = PixelSpace { px: f64::NAN, ..space(w, h) };
for p in params {
for s in [tiny, huge, nan, space(w, h)] {
let mut out = src.clone();
fx(id, p.clone()).apply(&mut out, w, h, &s);
assert_eq!(out.len(), src.len());
}
}
// A size that doesn't match, and an empty raster, do nothing.
let mut odd = vec![1u8; 10];
fx(id, json!({})).apply(&mut odd, 3, 3, &space(3, 3));
assert_eq!(odd, vec![1u8; 10]);
fx(id, json!({})).apply(&mut [], 0, 0, &space(1, 1));
}
#[test]
fn smudge_stick_smudges_the_dark_and_lightens_the_light() {
assert_eq!(fx("artistic.smudgeStick", json!({})), PixelFx::SmudgeStick { stroke: 2.0, highlight: 12.0, intensity: 10.0 });
assert_eq!(
fx("artistic.smudgeStick", json!({"strokeLength": 1e308, "highlightArea": -5, "intensity": "x"})),
PixelFx::SmudgeStick { stroke: 10.0, highlight: 0.0, intensity: 10.0 }
);
// A fine checkerboard of dark tones on the left, of light tones on the right, and a half
// transparent bottom.
let (w, h) = (64, 48);
let src = image(w, h, |x, y| {
let on = (x + y) % 2 == 0;
let v = match (x < 32, on) {
(true, true) => 20,
(true, false) => 70,
(false, true) => 190,
(false, false) => 240,
};
if y < 40 { grey(v) } else { [v / 2, v / 2, v / 2, 128] }
});
let out = run("artistic.smudgeStick", json!({}), &src, w, h);
assert_eq!(out, run("artistic.smudgeStick", json!({}), &src, w, h), "draws the same twice");
assert!(out.as_chunks::<4>().0.iter().zip(src.as_chunks::<4>().0).all(|(o, s)| o[3] == s[3]), "coverage kept");
// The dark detail is smudged away, the light areas brighter.
assert!(roughness(&out, w, 4, 28) < 0.5 * roughness(&src, w, 4, 28));
assert!(mean(&out, w, 36, 60) > mean(&src, w, 36, 60) + 10.0);
// Intensity 0 leaves the light areas unlightened.
let flat = run("artistic.smudgeStick", json!({"intensity": 0}), &src, w, h);
assert!(mean(&flat, w, 36, 60) < mean(&out, w, 36, 60) - 5.0);
survives(
"artistic.smudgeStick",
&[json!({}), json!({"strokeLength": 1e308, "highlightArea": 1e308, "intensity": 1e308}), json!({"strokeLength": "NaN"})],
);
}
#[test]
fn sponge_dabs_contrasting_blobs_and_keeps_the_hue() {
assert_eq!(fx("artistic.sponge", json!({})), PixelFx::Sponge { brush: 2.0, definition: 12.0, smoothness: 5.0 });
assert_eq!(
fx("artistic.sponge", json!({"brushSize": -1, "definition": 1e308, "smoothness": 0})),
PixelFx::Sponge { brush: 0.0, definition: 25.0, smoothness: 1.0 }
);
let (w, h) = (80, 60);
let flat = image(w, h, |_, y| if y < 50 { [200, 90, 60, 255] } else { [100, 45, 30, 128] });
let out = run("artistic.sponge", json!({"definition": 25}), &flat, w, h);
assert_eq!(out, run("artistic.sponge", json!({"definition": 25}), &flat, w, h), "draws the same twice");
assert!(out.as_chunks::<4>().0.iter().zip(flat.as_chunks::<4>().0).all(|(o, s)| o[3] == s[3]), "coverage kept");
// A flat colour breaks up into lighter and darker blobs of the same hue.
let top = &out[..w * 50 * 4];
assert!(spread(top) > 15.0, "{}", spread(top));
assert!(top.as_chunks::<4>().0.iter().all(|p| p[0] >= p[1] && p[1] >= p[2]), "red stays the strongest channel");
// Definition 0 leaves a flat colour as it is; more definition, more contrast.
let none = run("artistic.sponge", json!({"definition": 0}), &flat, w, h);
assert!(spread(&none[..w * 50 * 4]) < 1.0);
let some = run("artistic.sponge", json!({"definition": 8}), &flat, w, h);
assert!(spread(&some[..w * 50 * 4]) < spread(top));
survives("artistic.sponge", &[json!({}), json!({"brushSize": 1e308, "definition": 1e308, "smoothness": 1e308}), json!({"brushSize": "NaN"})]);
}
#[test]
fn watercolor_simplifies_detail_and_saturates_edges() {
assert_eq!(fx("artistic.watercolor", json!({})), PixelFx::Watercolor { detail: 9.0, shadow: 1.0, texture: 1 });
assert_eq!(
fx("artistic.watercolor", json!({"brushDetail": 99, "shadowIntensity": -1, "texture": 2.6})),
PixelFx::Watercolor { detail: 14.0, shadow: 0.0, texture: 3 }
);
// Noisy grey on the left, a step from light red to dark blue on the right, a transparent row.
let (w, h) = (64, 48);
let src = image(w, h, |x, y| {
if y >= 44 {
return [0; 4];
}
if x < 32 {
grey(if (x * 7 + y * 13) % 5 < 2 { 90 } else { 150 })
} else if x < 48 {
[200, 120, 120, 255]
} else {
[60, 70, 110, 255]
}
});
let few = run("artistic.watercolor", json!({"brushDetail": 1}), &src, w, h);
let many = run("artistic.watercolor", json!({"brushDetail": 14}), &src, w, h);
assert_eq!(few, run("artistic.watercolor", json!({"brushDetail": 1}), &src, w, h), "draws the same twice");
assert!(few.as_chunks::<4>().0.iter().zip(src.as_chunks::<4>().0).all(|(o, s)| o[3] == s[3]), "coverage kept, nothing painted outside");
assert!(roughness(&few, w, 4, 28) < 0.5 * roughness(&src, w, 4, 28), "less detail, simpler");
assert!(roughness(&few, w, 4, 28) < roughness(&many, w, 4, 28));
// Colour is saturated where the tones change, and shadows deepen with Shadow Intensity.
let chroma = |d: &[u8], x: usize, y: usize| {
let i = (y * w + x) * 4;
f64::from(d[i].max(d[i + 1]).max(d[i + 2])) - f64::from(d[i].min(d[i + 1]).min(d[i + 2]))
};
assert!(chroma(&few, 47, 20) > chroma(&src, 47, 20) || chroma(&few, 48, 20) > chroma(&src, 48, 20));
assert!(mean(&run("artistic.watercolor", json!({"shadowIntensity": 10}), &src, w, h), w, 4, 28) < mean(&few, w, 4, 28));
// Texture adds paper grain to a flat colour.
let flat = image(w, h, |_, _| grey(128));
let (t1, t3) = (run("artistic.watercolor", json!({"texture": 1}), &flat, w, h), run("artistic.watercolor", json!({"texture": 3}), &flat, w, h));
assert!(spread(&t3) > spread(&t1) && spread(&t1) > 0.2);
survives(
"artistic.watercolor",
&[json!({}), json!({"brushDetail": -1e308, "shadowIntensity": 1e308, "texture": 1e308}), json!({"brushDetail": "NaN"})],
);
}
+4 -1
View File
@@ -31,12 +31,15 @@ const BRUSH_STROKES: [(&str, &str); 8] = [
];
/// Effect › Artistic, at their defaults.
const ARTISTIC: [(&str, &str); 5] = [
const ARTISTIC: [(&str, &str); 8] = [
("artistic.cutout", "{}"),
("artistic.filmGrain", "{}"),
("artistic.neonGlow", "{}"),
("artistic.plasticWrap", "{}"),
("artistic.posterEdges", "{}"),
("artistic.smudgeStick", "{}"),
("artistic.sponge", "{}"),
("artistic.watercolor", "{}"),
];
/// Effect › Distort (Diffuse Glow with its highlights glowing from a low brightness up, as the
+3
View File
@@ -331,6 +331,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.smudgeStick", "params": {"strokeLength": 1e308, "highlightArea": "NaN", "intensity": [1]}})),
("effect.apply", json!({"effect": "artistic.sponge", "params": {"brushSize": -1e308, "definition": 1e308, "smoothness": null}})),
("effect.apply", json!({"effect": "artistic.watercolor", "params": {"brushDetail": "1e999", "shadowIntensity": -1, "texture": 1e308}})),
("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}})),
+3
View File
@@ -202,6 +202,9 @@ fn raster_filter_dialogs_preview_and_apply() {
("artistic.cutout", "levels", "6"),
("artistic.neonGlow", "color", "#ff00ff"),
("artistic.posterEdges", "edgeThickness", "5"),
("artistic.smudgeStick", "strokeLength", "6"),
("artistic.sponge", "definition", "20"),
("artistic.watercolor", "texture", "2"),
("blur.radial", "method", "zoom"),
("blur.smart", "quality", "high"),
("blur.smart", "blurMode", "overlayEdge"),
+10
View File
@@ -5165,6 +5165,16 @@ theme Appearance Външен вид
Movie Clip Филмов клип
@msg name is empty името е празно
@msg no symbol named `{n}` няма символ с име `{n}`
Smudge Stick… Размазваща пръчка…
Smudge Stick Размазваща пръчка
Sponge… Гъба…
Sponge Гъба
Watercolor… Акварел…
Watercolor Акварел
Brush Size Размер на четката
Definition Дефиниция
Brush Detail Детайл на четката
Shadow Intensity Интензивност на сенките
# Links and Attributes: files and URLs handed to other apps.
@msg Edit Original opens image and document files only; “{name}” is not one „Редактиране на оригинал“ отваря само файлове с изображения и документи; „{name}“ не е такъв файл
@msg only http, https and mailto URLs open in the browser, not “{url}” в браузъра се отварят само URL адреси с http, https и mailto, не „{url}“
Can't render this file because it contains an unexpected character in line 2 and column 73.
+10
View File
@@ -5192,6 +5192,16 @@ mark-style Japanese Japonské
Movie Clip Filmový klip
@msg name is empty název je prázdný
@msg no symbol named `{n}` žádný symbol s názvem `{n}`
Smudge Stick… Rozmazávací tyčinka…
Smudge Stick Rozmazávací tyčinka
Sponge… Houba…
Sponge Houba
Watercolor… Akvarel…
Watercolor Akvarel
Brush Size Velikost štětce
Definition Definice
Brush Detail Detail štětce
Shadow Intensity Intenzita stínů
# Links and Attributes: files and URLs handed to other apps.
@msg Edit Original opens image and document files only; “{name}” is not one Upravit originál otevírá pouze soubory obrázků a dokumentů; „{name}“ mezi ně nepatří
@msg only http, https and mailto URLs open in the browser, not “{url}” v prohlížeči se otevírají pouze adresy URL http, https a mailto, ne „{url}“
Can't render this file because it contains an unexpected character in line 2 and column 73.
+10
View File
@@ -5211,6 +5211,16 @@ swatch Bright Violet Kraftig violet
Justification Justering
# Select by Color (select.color)
Select by Color Markér efter farve
Smudge Stick… Udtværingsstift…
Smudge Stick Udtværingsstift
Sponge… Svamp…
Sponge Svamp
Watercolor… Akvarel…
Watercolor Akvarel
Brush Size Penselstørrelse
Definition Definition
Brush Detail Penseldetalje
Shadow Intensity Skyggeintensitet
Move Anchor Point Flyt ankerpunkt
Chalk & Charcoal… Kridt og kul…
Chalk & Charcoal Kridt og kul
Can't render this file because it contains an unexpected character in line 2 and column 73.
+10
View File
@@ -4771,6 +4771,16 @@ effect Soft Weich
Movie Clip Movieclip
@msg name is empty Name ist leer
@msg no symbol named `{n}` kein Symbol namens `{n}`
Smudge Stick… Wischstift…
Smudge Stick Wischstift
Sponge… Schwamm…
Sponge Schwamm
Watercolor… Aquarell…
Watercolor Aquarell
Brush Size Pinselgröße
Definition Definition
Brush Detail Pinseldetail
Shadow Intensity Schattenintensität
# Links and Attributes: files and URLs handed to other apps.
@msg Edit Original opens image and document files only; “{name}” is not one Original bearbeiten öffnet nur Bild- und Dokumentdateien; „{name}“ gehört nicht dazu
@msg only http, https and mailto URLs open in the browser, not “{url}” nur http-, https- und mailto-URLs werden im Browser geöffnet, nicht „{url}“
Can't render this file because it contains an unexpected character in line 2 and column 73.
+10
View File
@@ -5144,6 +5144,16 @@ effect Points Σημεία
Movie Clip Κλιπ ταινίας
@msg name is empty το όνομα είναι κενό
@msg no symbol named `{n}` κανένα σύμβολο με όνομα `{n}`
Smudge Stick… Μουτζούρα…
Smudge Stick Μουτζούρα
Sponge… Σφουγγάρι…
Sponge Σφουγγάρι
Watercolor… Υδατογραφία…
Watercolor Υδατογραφία
Brush Size Μέγεθος πινέλου
Definition Ευκρίνεια
Brush Detail Λεπτομέρεια πινέλου
Shadow Intensity Ένταση σκιών
# Links and Attributes: files and URLs handed to other apps.
@msg Edit Original opens image and document files only; “{name}” is not one η Επεξεργασία πρωτοτύπου ανοίγει μόνο αρχεία εικόνων και εγγράφων· το «{name}» δεν είναι τέτοιο αρχείο
@msg only http, https and mailto URLs open in the browser, not “{url}” στο πρόγραμμα περιήγησης ανοίγουν μόνο URL http, https και mailto, όχι το «{url}»
Can't render this file because it contains an unexpected character in line 2016 and column 19.
+10
View File
@@ -5134,6 +5134,16 @@ mark-style Japanese Japonesas
Movie Clip Clip de película
@msg name is empty el nombre está vacío
@msg no symbol named `{n}` no hay ningún símbolo llamado `{n}`
Smudge Stick… Difuminar con barra…
Smudge Stick Difuminar con barra
Sponge… Esponja…
Sponge Esponja
Watercolor… Acuarela…
Watercolor Acuarela
Brush Size Tamaño del pincel
Definition Definición
Brush Detail Detalle del pincel
Shadow Intensity Intensidad de sombras
# Links and Attributes: files and URLs handed to other apps.
@msg Edit Original opens image and document files only; “{name}” is not one Editar original solo abre archivos de imagen y de documento; “{name}” no lo es
@msg only http, https and mailto URLs open in the browser, not “{url}” solo se abren en el navegador las URL http, https y mailto, no “{url}”
Can't render this file because it contains an unexpected character in line 2 and column 73.
+10
View File
@@ -5156,6 +5156,16 @@ mark-style Japanese Japanilainen
Movie Clip Elokuvaleike
@msg name is empty nimi on tyhjä
@msg no symbol named `{n}` symbolia nimeltä `{n}` ei ole
Smudge Stick… Sumennuskynä…
Smudge Stick Sumennuskynä
Sponge… Sieni…
Sponge Sieni
Watercolor… Vesiväri…
Watercolor Vesiväri
Brush Size Siveltimen koko
Definition Terävyys
Brush Detail Siveltimen yksityiskohta
Shadow Intensity Varjojen voimakkuus
# Links and Attributes: files and URLs handed to other apps.
@msg Edit Original opens image and document files only; “{name}” is not one Muokkaa alkuperäistä avaa vain kuva- ja asiakirjatiedostoja; “{name}” ei ole sellainen
@msg only http, https and mailto URLs open in the browser, not “{url}” selaimessa avataan vain http-, https- ja mailto-URL-osoitteet, ei “{url}”
Can't render this file because it contains an unexpected character in line 2 and column 73.
+10
View File
@@ -5191,6 +5191,16 @@ theme Appearance Apparence
Movie Clip Clip
@msg name is empty le nom est vide
@msg no symbol named `{n}` aucun symbole nommé `{n}`
Smudge Stick… Estompe…
Smudge Stick Estompe
Sponge… Éponge…
Sponge Éponge
Watercolor… Aquarelle…
Watercolor Aquarelle
Brush Size Taille du pinceau
Definition Définition
Brush Detail Détail du pinceau
Shadow Intensity Intensité des ombres
# Links and Attributes: files and URLs handed to other apps.
@msg Edit Original opens image and document files only; “{name}” is not one Modifier l’original n’ouvre que les fichiers d’image et de document ; « {name} » n’en est pas un
@msg only http, https and mailto URLs open in the browser, not “{url}” seules les URL http, https et mailto s’ouvrent dans le navigateur, pas « {url} »
Can't render this file because it contains an unexpected character in line 2 and column 73.
+10
View File
@@ -5178,6 +5178,16 @@ imageTrace Outline Körvonal
Movie Clip Mozgóklip
@msg name is empty a név üres
@msg no symbol named `{n}` nincs `{n}` nevű szimbólum
Smudge Stick… Elmosó rúd…
Smudge Stick Elmosó rúd
Sponge… Szivacs…
Sponge Szivacs
Watercolor… Akvarell…
Watercolor Akvarell
Brush Size Ecsetméret
Definition Kidolgozottság
Brush Detail Ecsetrészletesség
Shadow Intensity Árnyék intenzitása
# Links and Attributes: files and URLs handed to other apps.
@msg Edit Original opens image and document files only; “{name}” is not one az Eredeti szerkesztése csak kép- és dokumentumfájlokat nyit meg; ez nem ilyen: „{name}”
@msg only http, https and mailto URLs open in the browser, not “{url}” a böngészőben csak a http-, https- és mailto-URL-ek nyílnak meg; ez nem ilyen: „{url}”
Can't render this file because it contains an unexpected character in line 2 and column 73.
+10
View File
@@ -5211,6 +5211,16 @@ swatch Bright Violet Violet cerah
Justification Justifikasi
# Select by Color (select.color)
Select by Color Pilih berdasarkan warna
Smudge Stick… Tongkat olesan…
Smudge Stick Tongkat olesan
Sponge… Spons…
Sponge Spons
Watercolor… Cat air…
Watercolor Cat air
Brush Size Ukuran kuas
Definition Definisi
Brush Detail Detail kuas
Shadow Intensity Intensitas bayangan
Move Anchor Point Pindahkan titik jangkar
Chalk & Charcoal… Kapur dan arang…
Chalk & Charcoal Kapur dan arang
Can't render this file because it contains an unexpected character in line 3772 and column 19.
+10
View File
@@ -4805,6 +4805,16 @@ lighting Light Luce
Movie Clip Clip filmato
@msg name is empty il nome è vuoto
@msg no symbol named `{n}` nessun simbolo denominato `{n}`
Smudge Stick… Sfumino…
Smudge Stick Sfumino
Sponge… Spugna…
Sponge Spugna
Watercolor… Acquerello…
Watercolor Acquerello
Brush Size Dimensione pennello
Definition Definizione
Brush Detail Dettaglio pennello
Shadow Intensity Intensità ombre
# Links and Attributes: files and URLs handed to other apps.
@msg Edit Original opens image and document files only; “{name}” is not one Modifica originale apre solo file di immagini e documenti; “{name}” non lo è
@msg only http, https and mailto URLs open in the browser, not “{url}” nel browser si aprono solo URL http, https e mailto, non “{url}”
Can't render this file because it contains an unexpected character in line 2 and column 73.
+10
View File
@@ -4845,6 +4845,16 @@ lighting Light ライト
Movie Clip ムービークリップ
@msg name is empty 名前が空です
@msg no symbol named `{n}` `{n}` という名前のシンボルはありません
Smudge Stick… スマッジスティック…
Smudge Stick スマッジスティック
Sponge… スポンジ…
Sponge スポンジ
Watercolor… 水彩…
Watercolor 水彩
Brush Size ブラシサイズ
Definition 精細度
Brush Detail ブラシの細かさ
Shadow Intensity シャドウの強さ
# Links and Attributes: files and URLs handed to other apps.
@msg Edit Original opens image and document files only; “{name}” is not one 「オリジナルを編集」で開けるのは画像ファイルとドキュメントファイルだけです: 「{name}」はどちらでもありません
@msg only http, https and mailto URLs open in the browser, not “{url}” ブラウザーで開けるのは http、https、mailto の URL だけです: 「{url}」は開けません
Can't render this file because it contains an unexpected character in line 2 and column 73.
+10
View File
@@ -5137,6 +5137,16 @@ imageTrace Outline კონტური
# Select by Color (select.color)
Select by Color მონიშვნა ფერის მიხედვით
@msg thumbnail: no page {n} (the file has 1) thumbnail: გვერდი {n} არ არსებობს (ფაილს 1 გვერდი აქვს)
Smudge Stick… დასაზელი ჯოხი…
Smudge Stick დასაზელი ჯოხი
Sponge… ღრუბელი…
Sponge ღრუბელი
Watercolor… აკვარელი…
Watercolor აკვარელი
Brush Size ფუნჯის ზომა
Definition განსაზღვრულობა
Brush Detail ფუნჯის დეტალი
Shadow Intensity ჩრდილის ინტენსივობა
Move Anchor Point საყრდენი წერტილის გადაადგილება
Alt key toggles to Smooth Tool Alt კლავიში ჩართავს გაგლუვების ხელსაწყოს
@msg {label} keeps 1 of the document's {n} artboards: the file reopens with one. Turn on Preserve Editing Capabilities to keep them all. {label} ინახავს დოკუმენტის {n} სამუშაო დაფიდან 1-ს: ფაილი ხელახლა ერთით გაიხსნება. ყველას შესანარჩუნებლად ჩართეთ „რედაქტირების შესაძლებლობების შენარჩუნება“.
Can't render this file because it contains an unexpected character in line 2 and column 73.
+10
View File
@@ -4768,6 +4768,16 @@ lighting Light 광원
Movie Clip 무비 클립
@msg name is empty 이름이 비어 있습니다
@msg no symbol named `{n}` 이름이 `{n}`인 심볼이 없습니다
Smudge Stick… 문지르기 막대…
Smudge Stick 문지르기 막대
Sponge… 스펀지…
Sponge 스펀지
Watercolor… 수채화…
Watercolor 수채화
Brush Size 브러시 크기
Definition 선명도
Brush Detail 브러시 세부 묘사
Shadow Intensity 그림자 강도
# Links and Attributes: files and URLs handed to other apps.
@msg Edit Original opens image and document files only; “{name}” is not one 원본 편집은 이미지 및 문서 파일만 엽니다. “{name}”은(는) 해당하지 않습니다
@msg only http, https and mailto URLs open in the browser, not “{url}” 브라우저에서는 http, https, mailto URL만 열립니다. “{url}”은(는) 열 수 없습니다
Can't render this file because it contains an unexpected character in line 2 and column 73.
+10
View File
@@ -5253,6 +5253,16 @@ swatch Bright Violet Felviolet
Justification Uitvullen
# Select by Color (select.color)
Select by Color Selecteren op kleur
Smudge Stick… Uitsmeerstift…
Smudge Stick Uitsmeerstift
Sponge… Spons…
Sponge Spons
Watercolor… Aquarel…
Watercolor Aquarel
Brush Size Penseelgrootte
Definition Definitie
Brush Detail Penseeldetail
Shadow Intensity Schaduwintensiteit
Move Anchor Point Ankerpunt verplaatsen
Chalk & Charcoal… Krijt en houtskool…
Chalk & Charcoal Krijt en houtskool
Can't render this file because it contains an unexpected character in line 2 and column 73.
+10
View File
@@ -5044,6 +5044,16 @@ mark-style Japanese Japońskie
Movie Clip Klip filmowy
@msg name is empty nazwa jest pusta
@msg no symbol named `{n}` brak symbolu o nazwie `{n}`
Smudge Stick… Rozmazywacz…
Smudge Stick Rozmazywacz
Sponge… Gąbka…
Sponge Gąbka
Watercolor… Akwarela…
Watercolor Akwarela
Brush Size Rozmiar pędzla
Definition Wyrazistość
Brush Detail Szczegółowość pędzla
Shadow Intensity Intensywność cieni
# Links and Attributes: files and URLs handed to other apps.
@msg Edit Original opens image and document files only; “{name}” is not one Edytuj oryginał otwiera tylko pliki obrazów i dokumentów; „{name}” do nich nie należy
@msg only http, https and mailto URLs open in the browser, not “{url}” w przeglądarce otwierają się tylko adresy URL http, https i mailto, a nie „{url}”
Can't render this file because it contains an unexpected character in line 2 and column 73.
+10
View File
@@ -5109,6 +5109,16 @@ mark-style Japanese Japonesas
# Symbol Options › Export Type
@msg name is empty o nome está vazio
@msg no symbol named `{n}` nenhum símbolo chamado `{n}`
Smudge Stick… Esfuminho…
Smudge Stick Esfuminho
Sponge… Esponja…
Sponge Esponja
Watercolor… Aquarela…
Watercolor Aquarela
Brush Size Tamanho do pincel
Definition Definição
Brush Detail Detalhe do pincel
Shadow Intensity Intensidade da sombra
# Links and Attributes: files and URLs handed to other apps.
@msg Edit Original opens image and document files only; “{name}” is not one Editar Original abre apenas arquivos de imagem e de documento; “{name}” não é um deles
@msg only http, https and mailto URLs open in the browser, not “{url}” apenas URLs http, https e mailto abrem no navegador, não “{url}”
Can't render this file because it contains an unexpected character in line 6 and column 47.
+10
View File
@@ -4801,6 +4801,16 @@ lighting Light Свет
Movie Clip Видеоклип
@msg name is empty имя пустое
@msg no symbol named `{n}` нет символа с именем `{n}`
Smudge Stick… Растушёвка…
Smudge Stick Растушёвка
Sponge… Губка…
Sponge Губка
Watercolor… Акварель…
Watercolor Акварель
Brush Size Размер кисти
Definition Чёткость
Brush Detail Детализация кисти
Shadow Intensity Интенсивность теней
# Links and Attributes: files and URLs handed to other apps.
@msg Edit Original opens image and document files only; “{name}” is not one «Редактировать оригинал» открывает только файлы изображений и документов; «{name}» к ним не относится
@msg only http, https and mailto URLs open in the browser, not “{url}” в браузере открываются только URL с http, https и mailto, а не «{url}»
Can't render this file because it contains an unexpected character in line 2 and column 73.
+10
View File
@@ -5148,6 +5148,16 @@ theme Appearance Utseende
Movie Clip Filmklipp
@msg name is empty namnet är tomt
@msg no symbol named `{n}` ingen symbol med namnet `{n}`
Smudge Stick… Utsmetning…
Smudge Stick Utsmetning
Sponge… Svamp…
Sponge Svamp
Watercolor… Akvarell…
Watercolor Akvarell
Brush Size Penselstorlek
Definition Skärpa
Brush Detail Penseldetalj
Shadow Intensity Skuggintensitet
# Links and Attributes: files and URLs handed to other apps.
@msg Edit Original opens image and document files only; “{name}” is not one Redigera original öppnar bara bild- och dokumentfiler; ”{name}” är ingen sådan
@msg only http, https and mailto URLs open in the browser, not “{url}” bara http-, https- och mailto-URL:er öppnas i webbläsaren, inte ”{url}”
Can't render this file because it contains an unexpected character in line 5 and column 25.
+10
View File
@@ -4785,6 +4785,16 @@ lighting Light Світло
Movie Clip Кліп
@msg name is empty ім’я порожнє
@msg no symbol named `{n}` немає символу з назвою `{n}`
Smudge Stick… Розтушовування…
Smudge Stick Розтушовування
Sponge… Губка…
Sponge Губка
Watercolor… Акварель…
Watercolor Акварель
Brush Size Розмір пензля
Definition Чіткість
Brush Detail Деталізація пензля
Shadow Intensity Інтенсивність тіней
# Links and Attributes: files and URLs handed to other apps.
@msg Edit Original opens image and document files only; “{name}” is not one «Редагувати оригінал» відкриває лише файли зображень і документів; «{name}» до них не належить
@msg only http, https and mailto URLs open in the browser, not “{url}” у браузері відкриваються лише URL з http, https і mailto, а не «{url}»
Can't render this file because it contains an unexpected character in line 725 and column 40.
+10
View File
@@ -4785,6 +4785,16 @@ theme Appearance Giao diện
Movie Clip Clip phim
@msg name is empty tên trống
@msg no symbol named `{n}` không có biểu tượng nào tên `{n}`
Smudge Stick… Bút chì xóa mờ…
Smudge Stick Bút chì xóa mờ
Sponge… Bọt biển…
Sponge Bọt biển
Watercolor… Màu nước…
Watercolor Màu nước
Brush Size Kích thước cọ
Definition Độ nét
Brush Detail Chi tiết cọ
Shadow Intensity Cường độ bóng
# Links and Attributes: files and URLs handed to other apps.
@msg Edit Original opens image and document files only; “{name}” is not one Chỉnh sửa bản gốc chỉ mở tệp hình ảnh và tài liệu; “{name}” không thuộc loại đó
@msg only http, https and mailto URLs open in the browser, not “{url}” chỉ URL http, https và mailto mới được mở trong trình duyệt, không mở “{url}”
Can't render this file because it contains an unexpected character in line 3962 and column 10.
+10
View File
@@ -5025,6 +5025,16 @@ lighting Light 光源
Movie Clip 影片剪辑
@msg name is empty 名称为空
@msg no symbol named `{n}` 没有名为 `{n}` 的符号
Smudge Stick… 涂抹棒…
Smudge Stick 涂抹棒
Sponge… 海绵…
Sponge 海绵
Watercolor… 水彩…
Watercolor 水彩
Brush Size 画笔大小
Definition 清晰度
Brush Detail 画笔细节
Shadow Intensity 阴影强度
Enable Guides for 9-Slice Scaling 启用 9-slice 缩放参考线
@msg cancelled; nothing was changed 已取消;未做任何更改
@msg Save writes {_1}, not {_2}: use file.saveAsTemplate to write it 保存会写入 {_1},而不是 {_2}:要写入该格式请使用 file.saveAsTemplate
Can't render this file because it contains an unexpected character in line 2 and column 73.
+10
View File
@@ -4835,6 +4835,16 @@ lighting Light 光源
Movie Clip 影片片段
@msg name is empty 名稱為空
@msg no symbol named `{n}` 沒有名為 `{n}` 的符號
Smudge Stick… 塗抹棒…
Smudge Stick 塗抹棒
Sponge… 海綿…
Sponge 海綿
Watercolor… 水彩…
Watercolor 水彩
Brush Size 筆刷大小
Definition 清晰度
Brush Detail 筆刷細節
Shadow Intensity 陰影強度
Enable Guides for 9-Slice Scaling 啟用 9-slice 縮放參考線
Persistent 持久
Library Panel Options 資料庫面板選項
Can't render this file because it contains an unexpected character in line 6 and column 47.
+1 -1
View File
@@ -86,7 +86,7 @@ fn effect_menu_lists_the_artistic_filters_before_blur() {
let entries = menus::menu_entries(&app);
let artistic: Vec<_> = entries.iter().filter(|e| e.path == ["Effect", "Artistic"]).collect();
let labels: Vec<&str> = artistic.iter().map(|e| e.label.as_str()).collect();
assert_eq!(labels, ["Cutout…", "Film Grain…", "Neon Glow…", "Plastic Wrap…", "Poster Edges…"]);
assert_eq!(labels, ["Cutout…", "Film Grain…", "Neon Glow…", "Plastic Wrap…", "Poster Edges…", "Smudge Stick…", "Sponge…", "Watercolor…"]);
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();
+45 -6
View File
@@ -1,6 +1,6 @@
# Effects parity
> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-19 · **Change:** minor (G2: Sketch › Chrome; Sketch › Chalk & Charcoal, Graphic Pen and Reticulation and Artistic › Plastic Wrap, #2972, #2967, #2976, #3034: 37/57 Photoshop effects; 3D lighting: one to ten colored lights, smooth colored Inflate, legacy white-light appearance preserved; surface batching and restored seam minimum; scores and estimates unchanged; Zig Zag's Smooth points make a continuous wave, #2343; G2: Effect › Sketch › Halftone Pattern; Photoshop filters 30/57, 27 remaining (22–38 h); Round Corners also rounds corners where a curve meets a line or another curve, with Live Corners' cut; straight corners unchanged; score unchanged; Revolve: automatic smooth shading and default-on Cap with continuous rims; wide shadows and glows at high zoom, #1706; stacked Transform effects, #1705; 3D on nonzero compound paths, #1724; pattern tiles redraw, #1723; scores unchanged; Roughen's Smooth turn-backs, #1643; Scribble per region, #1648; score unchanged; Warp Arc bends in the object's proportions, #1578; Zig Zag ridges evenly spaced, #1558; effect parameters checked, #1592; score unchanged; raster effects after a pattern, radial or freeform gradient fill export at their place and size, #1536; score unchanged; 3D and Materials on outlined type and other shapes of many small curves, #1481; Twist 0° to 360°, #1524; twisted walls keep their faces, #1525; score unchanged; 3D and Materials: Inflate takes Twist, Taper and Cap; score unchanged; 3D and Materials: Perspective as the documented 0°–160° angle; 3D and Materials: the documented light settings) · **Target:** Adobe Illustrator 2026 (30.x)
> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-20 · **Change:** minor (G2: Effect › Artistic › Smudge Stick, Sponge and Watercolor; Photoshop filters 42/57, 15 remaining (14–24 h); G2: Sketch › Chrome; Sketch › Chalk & Charcoal, Graphic Pen and Reticulation and Artistic › Plastic Wrap, #2972, #2967, #2976, #3034: 37/57 Photoshop effects; 3D lighting: one to ten colored lights, smooth colored Inflate, legacy white-light appearance preserved; surface batching and restored seam minimum; scores and estimates unchanged; Zig Zag's Smooth points make a continuous wave, #2343; G2: Effect › Sketch › Halftone Pattern; Photoshop filters 42/57, 15 remaining (14–24 h); Round Corners also rounds corners where a curve meets a line or another curve, with Live Corners' cut; straight corners unchanged; score unchanged; Revolve: automatic smooth shading and default-on Cap with continuous rims; wide shadows and glows at high zoom, #1706; stacked Transform effects, #1705; 3D on nonzero compound paths, #1724; pattern tiles redraw, #1723; scores unchanged; Roughen's Smooth turn-backs, #1643; Scribble per region, #1648; score unchanged; Warp Arc bends in the object's proportions, #1578; Zig Zag ridges evenly spaced, #1558; effect parameters checked, #1592; score unchanged; raster effects after a pattern, radial or freeform gradient fill export at their place and size, #1536; score unchanged; 3D and Materials on outlined type and other shapes of many small curves, #1481; Twist 0° to 360°, #1524; twisted walls keep their faces, #1525; score unchanged; 3D and Materials: Inflate takes Twist, Taper and Cap; score unchanged; 3D and Materials: Perspective as the documented 0°–160° angle; 3D and Materials: the documented light settings) · **Target:** Adobe Illustrator 2026 (30.x)
Every entry of Illustrator's Effect menu, by its public feature name, against VectorCraft's effects catalogue
(`effect_catalog()` in `crates/effects/src/lib.rs`) and the Effect menu built from it
@@ -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 | 46 | 92% | 6–10 |
| Photoshop effects (raster, Effect Gallery) | 57 | 36 | 63% | 21–35 |
| Photoshop effects (raster, Effect Gallery) | 57 | 42 | 74% | 14–24 |
| 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,13 +38,13 @@ 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 | 5/15 (approximate Cutout, Film Grain, Neon Glow, Plastic Wrap, Poster Edges) | Colored Pencil, Dry Brush, Fresco, Paint Daubs, Palette Knife, 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 | 8/15 (approximate Cutout, Film Grain, Neon Glow, Plastic Wrap, Poster Edges, Smudge Stick, Sponge, Watercolor) | Colored Pencil, Dry Brush, Fresco, Paint Daubs, Palette Knife, Rough Pastels, Underpainting |
| 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 | — |
| Pixelate | Color Halftone, Crystallize, Mezzotint, Pointillize | 4/4 | — |
| Sharpen | Unsharp Mask | 1/1 | — |
| Sketch | Bas Relief, Chalk & Charcoal, Charcoal, Chrome, Conté Crayon, Graphic Pen, Halftone Pattern, Note Paper, Photocopy, Plaster, Reticulation, Stamp, Torn Edges, Water Paper | 5/14 (approximate Chalk & Charcoal, Chrome, Graphic Pen, Halftone Pattern, Reticulation; Chalk & Charcoal and Graphic Pen defaults unverified) | Bas Relief, Charcoal, Conté Crayon, Note Paper, Photocopy, Plaster, Stamp, Torn Edges, Water Paper |
| Sketch | Bas Relief, Chalk & Charcoal, Charcoal, Chrome, Conté Crayon, Graphic Pen, Halftone Pattern, Note Paper, Photocopy, Plaster, Reticulation, Stamp, Torn Edges, Water Paper | 6/14 (approximate Chalk & Charcoal, Chrome, Graphic Pen, Halftone Pattern, Plaster, Reticulation; Chalk & Charcoal and Graphic Pen defaults unverified) | Bas Relief, Charcoal, Conté Crayon, Note Paper, Photocopy, Stamp, Torn Edges, Water Paper |
| Stylize | Glowing Edges | 1/1 | — |
| Texture | Craquelure, Grain, Mosaic Tiles, Patchwork, Stained Glass, Texturizer | 6/6 | Texturizer's Load Texture |
| Video | De-Interlace, NTSC Colors | 2/2 | — |
@@ -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 24 missing filters are ~17–29 h and the Effect
(see [calibration](target-app-parity.md#calibration)), so the 15 missing filters are ~10–18 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
@@ -83,6 +83,25 @@ Document Raster Effects resolution, and use the generic effect dialog with live
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`.
Smudge Stick (`artistic.smudgeStick`), Sponge (`artistic.sponge`) and Watercolor (`artistic.watercolor`),
Effect › Artistic, are original visual approximations from the public descriptions in
[Illustrator's effects reference](https://helpx.adobe.com/illustrator/using/summary-effects.html) and the
[public Artistic filters guide](https://helpx.adobe.com/photoshop-elements/using/artistic-filters.html), not
pixel-identical copies. Each has live preview, appearance editing, document-unit patterns that follow the
object, bounded work per pixel and kept coverage (they paint nothing outside the object), and bakes to an
image in SVG and PDF. Ranges and defaults are chosen for VectorCraft, not asserted as measured.
- Smudge Stick (Stroke Length 0–10, Highlight Area 0–20, Intensity 0–10): dark areas smudged along short
diagonal strokes; light areas smudged with strokes twice as long and lightened towards white.
- Sponge (Brush Size 0–10, Definition 0–25, Smoothness 1–15): the object softened, then lighter and darker
blobs of the same hue, their contrast set by Definition, their edges by Smoothness.
- Watercolor (Brush Detail 1–14, Shadow Intensity 0–10, Texture 1–3): detail simplified by an edge-keeping
average whose brush widens as Brush Detail falls, colour saturated and pooled darker where tones change at
edges, shadows deepened, a faint paper grain. Edges are found from tone only, so a change of hue at equal
brightness is not saturated.
Tests: `pixel::tests_paint::*`, `tests_rasterfilters`, `dialogs::tests::raster_filter_dialogs_*`,
`tests_labels::effect_menu_lists_the_artistic_filters_*` and the `command_sweep` structured junk.
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
@@ -93,7 +112,26 @@ Calibration on uniform grey at Grain 20, using 32×32 fine samples per coarse pi
grain standard deviation within about 15% of the dense area average at pixel widths of 2, 4
and 8 document units (ratios 1.13, 0.89, 0.85). This is evidence for the bounded approximation,
not an accuracy guarantee for arbitrary artwork. Tests: `pixel::tests::film_grain_*`,
`tests_rasterfilters`, `tests_adjust::film_grain_dialog_*` and `tests_labels::effect_menu_lists_film_grain_*`.
`tests_rasterfilters`, `tests_adjust::film_grain_dialog_*` and `tests_labels::effect_menu_lists_the_artistic_filters_*`.
Smudge Stick (`artistic.smudgeStick`), Sponge (`artistic.sponge`) and Watercolor (`artistic.watercolor`),
Effect › Artistic, are original visual approximations from the public descriptions in
[Illustrator's effects reference](https://helpx.adobe.com/illustrator/using/summary-effects.html) and the
[public Artistic filters guide](https://helpx.adobe.com/photoshop-elements/using/artistic-filters.html), not
pixel-identical copies. Each has live preview, appearance editing, document-unit patterns that follow the
object, bounded work per pixel and kept coverage (they paint nothing outside the object), and bakes to an
image in SVG and PDF. Ranges and defaults are chosen for VectorCraft, not asserted as measured.
- Smudge Stick (Stroke Length 0–10, Highlight Area 0–20, Intensity 0–10): dark areas smudged along short
diagonal strokes; light areas smudged with strokes twice as long and lightened towards white.
- Sponge (Brush Size 0–10, Definition 0–25, Smoothness 1–15): the object softened, then lighter and darker
blobs of the same hue, their contrast set by Definition, their edges by Smoothness.
- Watercolor (Brush Detail 1–14, Shadow Intensity 0–10, Texture 1–3): detail simplified by an edge-keeping
average whose brush widens as Brush Detail falls, colour saturated and pooled darker where tones change at
edges, shadows deepened, a faint paper grain. Edges are found from tone only, so a change of hue at equal
brightness is not saturated.
Tests: `pixel::tests_paint::*`, `tests_rasterfilters`, `dialogs::tests::raster_filter_dialogs_*`,
`tests_labels::effect_menu_lists_the_artistic_filters_*` and the `command_sweep` structured junk.
## 3D and Materials
@@ -116,6 +154,7 @@ angular cut caps, exact visibility, 3D Classic, Turntable, 3D export) is 25–45
|---|---|---|
| 2026-10-19 | minor | Sketch › Chalk & Charcoal, Graphic Pen and Reticulation and Artistic › Plastic Wrap landed (#2972, #2967, #2976, #3034): 36/57 Photoshop effects (estimated; defaults unverified) |
| 2026-10-16 | minor | Zig Zag with Smooth points draws a continuous wave: every peak and valley is a smooth point with handles along the path, a third of the way to the next ridge, so each span follows a half cosine through the path instead of a straight diagonal with rounded tips, also at the ends of open paths and for ridges much taller than they are apart (#2343); score unchanged (estimated) |
| 2026-10-16 | minor | G2: Effect › Artistic › Smudge Stick, Sponge and Watercolor (live, with their documented options; original approximations); measured Photoshop filters 36/57 with Cutout, Neon Glow and Poster Edges, 21 remaining (19–31 h). |
| 2026-10-16 | minor | G2: Effect › Sketch › Halftone Pattern (Size, Contrast, Pattern Type Circle, Dot or Line; black ink on white paper, Size in points at any Document Raster Effects resolution); measured Photoshop filters 30/57, 27 remaining (22–38 h). |
| 2026-10-17 | minor | Colored Inflate: shared membrane normals and smooth RGB vector ramps replace flat polygon bands; channel groups are batched per solid-fill surface. White-light Inflate retains its original polygons, paint composition and seam joins; the 0.05 seam minimum is restored. Scores and hour estimates unchanged. |
| 2026-10-16 | minor | 3D lighting: one to ten directional lights, a selectable light list, Add Light and Delete Light, independent settings and artboard gestures; ambient applied once and smooth RGB vector export; scores and hour estimates unchanged |
+7 -6
View File
File diff suppressed because one or more lines are too long
+4 -3
View File
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long