Effects: Effect › Sketch › Halftone Pattern redraws an object through a halftone screen of dots, rings or lines

This commit is contained in:
Marco Arnulfo
2026-10-16 15:27:53 +07:00
committed by bflatastic
parent ddfd08f321
commit 403b335039
35 changed files with 380 additions and 46 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 (28 of 57 still missing; Film Grain is available under Artistic) and the Effect Gallery;
- 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;
- 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.
+1 -1
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 29/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 30/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 |
+10 -1
View File
@@ -6,7 +6,7 @@
//! - **Geometry effects** (Distort & Transform, Path, Convert to Shape, Round Corners, Scribble,
//! Warp) rewrite a path: [`apply_geometry`] evaluates them in stack order.
//! - **Raster effects** (Drop Shadow, Inner/Outer Glow, Feather, Gaussian Blur, and the
//! Photoshop-style filters of [`pixel`]: Radial Blur, Smart Blur, Color Halftone, Crystallize, Mezzotint, Pointillize,
//! Photoshop-style filters of [`pixel`]: Radial Blur, Smart Blur, Color Halftone, Crystallize, Mezzotint, Pointillize, Halftone Pattern,
//! Unsharp Mask and Glowing Edges) are described by
//! [`raster_effects`] and painted by the renderer; [`outset`] says how far they reach beyond
//! the geometry.
@@ -149,6 +149,7 @@ fn lengths_of(id: &str) -> Lengths {
"texture.mosaicTiles" => always(&["tileSize"]),
"texture.patchwork" => always(&["squareSize"]),
"texture.stainedGlass" => always(&["cellSize"]),
"sketch.halftonePattern" => always(&["size"]),
_ => Lengths::default(),
}
}
@@ -165,6 +166,7 @@ const ARTISTIC: &[&str] = &["Effect", "Artistic"];
const BRUSH_STROKES: &[&str] = &["Effect", "Brush Strokes"];
const DISTORT: &[&str] = &["Effect", "Distort"];
const SHARPEN: &[&str] = &["Effect", "Sharpen"];
const SKETCH: &[&str] = &["Effect", "Sketch"];
const PIXELATE: &[&str] = &["Effect", "Pixelate"];
const TEXTURE: &[&str] = &["Effect", "Texture"];
const VIDEO: &[&str] = &["Effect", "Video"];
@@ -432,6 +434,13 @@ pub fn effect_catalog() -> Vec<EffectInfo> {
"{amount: % 1..500 (50), radius: pt 0.1..250 (1; σ of the blur edges are found against), threshold: levels 0..255 (0; smaller differences are left alone)}",
json!({"amount": 50.0, "radius": 1.0, "threshold": 0.0}),
),
r(
"sketch.halftonePattern",
"Halftone Pattern…",
SKETCH,
"{size: pt 1..12 (1; how far apart the screen's dots, rings or lines are), contrast: 0..50 (5; 50 draws pure black and white), patternType: \"circle\"|\"dot\"|\"line\" (\"dot\"; rings round the object's centre, dots on a square grid, horizontal lines)} redraws the object in black ink on white paper through a halftone screen whose ink covers as much as the colour is dark, keeping its coverage. Original visual approximation; chosen VectorCraft defaults.",
json!({"size": 1.0, "contrast": 5.0, "patternType": "dot"}),
),
r(
"stylize.glowingEdges",
"Glowing Edges…",
+17 -4
View File
@@ -1,7 +1,8 @@
//! Photoshop-style raster effects (Effect › Artistic › Film Grain, 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, Stylize › Glowing Edges, Texture
//! Crystallize, Mezzotint and Pointillize, Sharpen › Unsharp Mask, Sketch › Halftone Pattern, Stylize ›
//! Glowing Edges, Texture
//! › Craquelure, Grain, Mosaic Tiles, Patchwork, Stained Glass and Texturizer, and Video ›
//! De-Interlace and NTSC Colors): filters over premultiplied RGBA8 pixels.
//!
@@ -20,6 +21,7 @@ mod edges;
mod loaded;
mod pixelate;
mod sharpen;
mod sketch;
mod texture;
mod video;
@@ -33,10 +35,11 @@ pub use brushstrokes::{STROKE_DIRECTIONS, StrokeDirection};
pub use distort::{GLASS_TEXTURES, GLASS_TEXTURES_LOADED, GlassTexture};
pub use loaded::{LOADED, LoadedTexture, TEXTURE_IMAGE, texture};
pub use pixelate::{MEZZOTINT_TYPES, Mezzotint};
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; 28] = [
pub const PIXEL_EFFECTS: [&str; 29] = [
"artistic.filmGrain",
"blur.radial",
"blur.smart",
@@ -56,6 +59,7 @@ pub const PIXEL_EFFECTS: [&str; 28] = [
"pixelate.mezzotint",
"pixelate.pointillize",
"sharpen.unsharpMask",
"sketch.halftonePattern",
"stylize.glowingEdges",
"texture.craquelure",
"texture.grain",
@@ -123,6 +127,9 @@ pub enum PixelFx {
/// Sharpen › Unsharp Mask: colours pushed away from a Gaussian blur of σ = `radius` by `amount`
/// (1 = 100 %), where they differ from it by at least `threshold` levels.
UnsharpMask { amount: f64, radius: f64, threshold: f64 },
/// Sketch › Halftone Pattern: black ink on white paper through a `kind` screen of cells `size`
/// (1..12) apart, its edges as hard as `contrast` (0..50) says.
HalftonePattern { size: f64, contrast: f64, kind: HalftonePattern },
/// Stylize › Glowing Edges: bright coloured Sobel outlines on black.
GlowingEdges { width: f64, brightness: f64, smoothness: f64 },
/// Pixelate › Color Halftone: each colour channel screened at its angle (`angles`° of channels
@@ -277,6 +284,11 @@ pub(crate) fn parse(id: &str, p: &Value) -> Option<PixelFx> {
radius: num(p, "radius", 1.0).clamp(0.1, 250.0),
threshold: num(p, "threshold", 0.0).clamp(0.0, 255.0),
},
"sketch.halftonePattern" => PixelFx::HalftonePattern {
size: num(p, "size", 1.0).clamp(1.0, 12.0),
contrast: num(p, "contrast", 5.0).clamp(0.0, 50.0),
kind: HalftonePattern::parse(text(p, "patternType", "dot")),
},
"stylize.glowingEdges" => PixelFx::GlowingEdges {
width: num(p, "edgeWidth", 2.0).clamp(1.0, 14.0),
brightness: num(p, "edgeBrightness", 6.0).clamp(0.0, 20.0),
@@ -353,7 +365,7 @@ impl PixelFx {
// Content shifted outwards reaches past the bounds by as much as the shift.
PixelFx::Glass { distortion, .. } => distort::glass_shift(distortion),
PixelFx::OceanRipple { size, magnitude } => distort::ripple_shift(size, magnitude),
PixelFx::UnsharpMask { .. } => 0.0,
PixelFx::UnsharpMask { .. } | PixelFx::HalftonePattern { .. } => 0.0,
PixelFx::GlowingEdges { width, smoothness, .. } => width.max(smoothness * 3.0),
PixelFx::FilmGrain { .. }
| PixelFx::ColorHalftone { .. }
@@ -396,7 +408,7 @@ impl PixelFx {
// The cells around the pixel's, and the cells around theirs.
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 { .. } => Some(0.0),
PixelFx::FilmGrain { .. } | PixelFx::Mezzotint { .. } | PixelFx::HalftonePattern { .. } => Some(0.0),
// The dots' points, and the softened colour around them.
PixelFx::Pointillize { cell } => Some(2.5 * cell),
// The softened tones around the pixel.
@@ -439,6 +451,7 @@ impl PixelFx {
}
PixelFx::OceanRipple { size, magnitude } => distort::ocean_ripple(px, w, h, space, size, magnitude),
PixelFx::UnsharpMask { amount, radius, threshold } => sharpen::unsharp(px, w, h, amount, to_px(radius), threshold),
PixelFx::HalftonePattern { size, contrast, kind } => sketch::halftone_pattern(px, w, h, space, size, contrast, kind),
PixelFx::GlowingEdges { width, brightness, smoothness } => edges::glow(px, w, h, to_px(width), brightness, to_px(smoothness)),
PixelFx::ColorHalftone { max_radius, angles } => pixelate::color_halftone(px, w, h, space, max_radius, angles),
PixelFx::Crystallize { cell } => pixelate::crystallize(px, w, h, space, cell),
+71
View File
@@ -0,0 +1,71 @@
//! Effect › Sketch › Halftone Pattern. Original filter built from public behaviour documentation:
//! <https://helpx.adobe.com/illustrator/using/summary-effects.html> (Sketch effects redraw in
//! black and white; Halftone Pattern simulates a halftone screen while keeping the range of tones)
//! and <https://helpx.adobe.com/illustrator/using/effects.html> (only Size depends on the raster
//! resolution). A visual approximation, not a reproduction of a proprietary algorithm.
use super::pixelate::Place;
use super::texture::{luma, premultiply, straight};
use super::{PixelSpace, pick};
/// Halftone Pattern's screens, in its Pattern Type menu's order: (label, parameter value).
pub const HALFTONE_TYPES: [(&str, &str); 3] = [("Circle", "circle"), ("Dot", "dot"), ("Line", "line")];
/// A Halftone Pattern screen ([`HALFTONE_TYPES`]).
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum HalftonePattern {
/// Rings round the object's centre.
Circle,
/// Dots on a square grid.
Dot,
/// Horizontal lines.
Line,
}
impl HalftonePattern {
const ALL: [HalftonePattern; 3] = [HalftonePattern::Circle, HalftonePattern::Dot, HalftonePattern::Line];
/// The screen of Pattern Type `value` (a [`HALFTONE_TYPES`] value, any case); Dot otherwise.
pub fn parse(value: &str) -> Self {
pick(&HALFTONE_TYPES, &Self::ALL, value, HalftonePattern::Dot)
}
}
/// Halftone Pattern: the object redrawn in black ink on white paper through a `kind` screen whose
/// cells are `size` points apart, anchored at the object's centre. The ink covers as much of a
/// cell as the colour is dark; `contrast` (0..50) hardens the screen's soft edges, to pure black
/// and white at 50. Coverage is kept.
pub(super) fn halftone_pattern(px: &mut [[u8; 4]], w: usize, h: usize, space: &PixelSpace, size: f64, contrast: f64, kind: HalftonePattern) {
if !(size.is_finite() && size > 0.0 && contrast.is_finite()) {
return;
}
let Some(place) = Place::new(space, w, h) else { return };
// The width of the screen's soft edge, in tones: 0 is a hard threshold.
let soft = (0.5 * (1.0 - contrast / 50.0)).clamp(0.0, 0.5);
place.each(px, |x, y, p| {
if p[3] == 0 {
return;
}
let q = place.at(x, y);
let band = |v: f64| (2.0 * ((v / size).rem_euclid(1.0) - 0.5)).abs();
// Where in its cell the pixel lies, 0 where the ink starts to 1 where it ends.
let screen = match kind {
HalftonePattern::Line => band(q.y),
HalftonePattern::Circle => band(q.x.hypot(q.y)),
HalftonePattern::Dot => 0.5 * (band(q.x).powi(2) + band(q.y).powi(2)),
};
let dark = f64::from(1.0 - luma(straight(*p))).clamp(0.0, 1.0);
let ink = if dark >= 1.0 {
1.0
} else if soft > 0.0 {
// The threshold kept inside [soft / 2, 1 - soft / 2], so white stays white and black black.
let threshold = 0.5 * soft + screen * (1.0 - soft);
((dark - threshold) / soft + 0.5).clamp(0.0, 1.0)
} else if dark > screen {
1.0
} else {
0.0
};
*p = premultiply([(1.0 - ink) as f32; 3], f32::from(p[3]) / 255.0);
});
}
+116
View File
@@ -1893,3 +1893,119 @@ fn brush_strokes_follow_the_object_at_any_resolution() {
assert_eq!(shifted, one, "{id}");
}
}
/// Halftone Pattern of `p` over `w` × `h` pixels of `src`, laid out by `s`.
fn halftone(p: serde_json::Value, src: &[u8], w: usize, h: usize, s: &PixelSpace) -> Vec<u8> {
let mut out = src.to_vec();
fx("sketch.halftonePattern", p).apply(&mut out, w, h, s);
out
}
/// The smallest period of `values` (their length when they don't repeat).
fn period(values: &[u8]) -> usize {
(1..values.len()).find(|p| values.iter().zip(&values[*p..]).all(|(a, b)| a == b)).unwrap_or(values.len())
}
#[test]
fn halftone_pattern_params_take_defaults_and_stay_in_range() {
assert_eq!(fx("sketch.halftonePattern", json!({})), PixelFx::HalftonePattern { size: 1.0, contrast: 5.0, kind: HalftonePattern::Dot });
assert_eq!(
fx("sketch.halftonePattern", json!({"size": 1e9, "contrast": -3, "patternType": "LINE"})),
PixelFx::HalftonePattern { size: 12.0, contrast: 0.0, kind: HalftonePattern::Line }
);
assert_eq!(
fx("sketch.halftonePattern", json!({"size": 0, "contrast": 1e9, "patternType": "circle"})),
PixelFx::HalftonePattern { size: 1.0, contrast: 50.0, kind: HalftonePattern::Circle }
);
for junk in [json!({"patternType": 2}), json!({"patternType": "nonsense"})] {
assert_eq!(fx("sketch.halftonePattern", junk), fx("sketch.halftonePattern", json!({})));
}
let doc = crate::effect_info("sketch.halftonePattern").unwrap().params;
let kinds: Vec<HalftonePattern> = HALFTONE_TYPES.iter().map(|(_, v)| HalftonePattern::parse(v)).collect();
assert_eq!(kinds, [HalftonePattern::Circle, HalftonePattern::Dot, HalftonePattern::Line]);
assert!(HALFTONE_TYPES.iter().all(|(_, v)| doc.contains(&format!("\"{v}\""))), "{doc}");
let f = fx("sketch.halftonePattern", json!({"size": 12}));
assert_eq!(f.outset(Rect::new(0.0, 0.0, 100.0, 50.0)), 0.0);
assert_eq!(f.reach(), Some(0.0));
}
/// Line's rows repeat every Size points: Size pixels at 72 ppi, twice as many at 144 ppi.
#[test]
fn halftone_line_period_is_the_size_at_any_resolution() {
let p = json!({"size": 4, "contrast": 50, "patternType": "line"});
let (w, h) = (48, 48);
let out = halftone(p.clone(), &image(w, h, |_, _| grey(128)), w, h, &space(w, h));
let column: Vec<u8> = (0..h).map(|y| at(&out, w, 0, y)[0]).collect();
assert!(column.contains(&0) && column.contains(&255), "{column:?}");
assert_eq!(period(&column), 4, "{column:?}");
let fine = PixelSpace { to_doc: Affine::scale(0.5), px: 0.5, ..space(w, h) };
let out = halftone(p, &image(2 * w, 2 * h, |_, _| grey(128)), 2 * w, 2 * h, &fine);
let column: Vec<u8> = (0..2 * h).map(|y| at(&out, 2 * w, 0, y)[0]).collect();
assert_eq!(period(&column), 8, "{column:?}");
}
/// Circle draws rings round the object's centre, Line rows that don't change along them, and
/// Dot neither.
#[test]
fn halftone_pattern_type_changes_the_pattern() {
let (w, h) = (64, 64);
let src = image(w, h, |_, _| grey(100));
// The centre on a pixel's centre: pixel (32 + dx, 32 + dy) lies at (dx, dy) from it.
let s = PixelSpace { center: Point::new(32.5, 32.5), ..space(w, h) };
let draw = |kind: &str| halftone(json!({"size": 8, "contrast": 50, "patternType": kind}), &src, w, h, &s);
let (circle, dot, line) = (draw("circle"), draw("dot"), draw("line"));
assert!(circle != dot && dot != line && line != circle);
// Pixels at the same distance from the centre in different directions.
let same_radius: [&[(i64, i64)]; 6] = [
&[(5, 0), (3, 4), (-4, 3), (0, -5), (-3, -4)],
&[(10, 0), (6, 8), (-8, -6), (0, 10)],
&[(13, 0), (5, 12), (-12, 5), (0, -13)],
&[(15, 0), (9, 12), (-12, 9)],
&[(17, 0), (8, 15), (-15, -8)],
&[(25, 0), (7, 24), (-15, 20), (-20, -15), (24, -7)],
];
let value = |d: &[u8], (dx, dy): (i64, i64)| at(d, w, (32 + dx) as usize, (32 + dy) as usize)[0];
let radial = |d: &[u8]| same_radius.iter().all(|ring| ring.iter().all(|q| value(d, *q) == value(d, ring[0])));
assert!(radial(&circle), "Circle is the same all round");
assert!(!radial(&dot) && !radial(&line));
for y in 0..h {
assert!((0..w).all(|x| at(&line, w, x, y) == at(&line, w, 0, y)), "row {y} of Line is uniform");
}
}
#[test]
fn halftone_contrast_sharpens_the_screen() {
let (w, h) = (64, 32);
let src = image(w, h, |x, _| grey((x * 4) as u8));
let mid = |d: &[u8]| share(d, |p| p[0] != 0 && p[0] != 255);
let soft = halftone(json!({"size": 6, "contrast": 0}), &src, w, h, &space(w, h));
let hard = halftone(json!({"size": 6, "contrast": 50}), &src, w, h, &space(w, h));
assert!(mid(&soft) > 0.3, "{}", mid(&soft));
assert_eq!(mid(&hard), 0.0);
}
/// Halftone Pattern redraws in black ink on white paper, whatever the object's colours, and
/// keeps its coverage.
#[test]
fn halftone_draws_in_black_and_white_only() {
let (w, h) = (48, 48);
let src = image(w, h, |x, y| {
let c = [[230u8, 20, 20], [20, 200, 40], [30, 60, 220], [240, 220, 30]][(x / 12) % 4];
let a: u16 = if y < 24 { 255 } else { 128 };
[c[0], c[1], c[2]].map(|v| (u16::from(v) * a / 255) as u8).into_iter().chain([a as u8]).collect::<Vec<_>>().try_into().unwrap()
});
for kind in ["circle", "dot", "line"] {
let out = halftone(json!({"size": 6, "contrast": 50, "patternType": kind}), &src, w, h, &space(w, h));
for (p, q) in out.as_chunks::<4>().0.iter().zip(src.as_chunks::<4>().0) {
assert_eq!(p[3], q[3], "{kind}: coverage kept");
assert!(p[0] == p[1] && p[1] == p[2] && (p[0] == 0 || p[0] == p[3]), "{kind}: {p:?} from {q:?}");
}
assert!(share(&out, |p| p[0] == 0) > 0.1 && share(&out, |p| p[0] == p[3]) > 0.1, "{kind}: ink and paper");
let soft = halftone(json!({"size": 6, "contrast": 0, "patternType": kind}), &src, w, h, &space(w, h));
assert!(soft.as_chunks::<4>().0.iter().all(|p| p[0] == p[1] && p[1] == p[2]), "{kind}: greys only");
for (tone, expect) in [(0, 0), (255, 255)] {
let out = halftone(json!({"size": 6, "contrast": 0, "patternType": kind}), &image(w, h, |_, _| grey(tone)), w, h, &space(w, h));
assert!(out.as_chunks::<4>().0.iter().all(|p| p[0] == expect), "{kind}: {tone} stays {expect}");
}
}
}
+30 -3
View File
@@ -1,5 +1,5 @@
//! The Photoshop-style raster effects (Effect › 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, Texture ›
//! 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
//! as images of the effected object.
@@ -42,6 +42,9 @@ const DISTORT: [(&str, &str); 3] =
const PIXELATE: [(&str, &str); 4] =
[("pixelate.colorHalftone", "{}"), ("pixelate.crystallize", "{}"), ("pixelate.mezzotint", "{}"), ("pixelate.pointillize", "{}")];
/// Effect › Sketch, at their defaults.
const SKETCH: [(&str, &str); 1] = [("sketch.halftonePattern", "{}")];
/// Effect › Texture, at their defaults.
const TEXTURE: [(&str, &str); 6] = [
("texture.craquelure", "{}"),
@@ -97,6 +100,7 @@ fn listed_applied_edited_and_undone() {
(BRUSH_STROKES.as_slice(), "Brush Strokes"),
(DISTORT.as_slice(), "Distort"),
(PIXELATE.as_slice(), "Pixelate"),
(SKETCH.as_slice(), "Sketch"),
(TEXTURE.as_slice(), "Texture"),
(VIDEO.as_slice(), "Video"),
([("video.ntscColors", "{}")].as_slice(), "Video"),
@@ -134,7 +138,9 @@ fn listed_applied_edited_and_undone() {
#[test]
fn each_effect_changes_the_canvas_and_draws_the_same_twice() {
for (id, params) in EFFECTS.into_iter().chain(ARTISTIC).chain(BRUSH_STROKES).chain(DISTORT).chain(PIXELATE).chain(TEXTURE).chain(VIDEO) {
for (id, params) in
EFFECTS.into_iter().chain(ARTISTIC).chain(BRUSH_STROKES).chain(DISTORT).chain(PIXELATE).chain(SKETCH).chain(TEXTURE).chain(VIDEO)
{
let (mut s, g) = striped();
let plain = render(&s);
let params: Value = serde_json::from_str(params).unwrap();
@@ -150,6 +156,25 @@ fn each_effect_changes_the_canvas_and_draws_the_same_twice() {
}
}
/// Halftone Pattern's Size, Contrast and Pattern Type are saved with the document and draw the same
/// when it opens again.
#[test]
fn halftone_pattern_settings_survive_a_save_and_open() {
let (mut s, g) = striped();
let set = json!({"size": 7.0, "contrast": 20.0, "patternType": "line"});
s.execute("effect.apply", &json!({"effect": "sketch.halftonePattern", "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);
assert_eq!(Renderer::new().render_region(&doc, Rect::new(0.0, 0.0, 200.0, 200.0), 1.0, true).pixels, drawn.pixels);
// Each setting shows: changing any one draws differently.
for change in [json!({"size": 3.0}), json!({"contrast": 50.0}), json!({"patternType": "dot"})] {
s.execute("effect.setParams", &json!({"index": 0, "ids": [g], "params": change})).unwrap();
assert!(mean_diff(&drawn, &render(&s)) > 0.001, "{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| {
@@ -251,7 +276,9 @@ fn spin_blur_reaches_past_the_corners() {
#[test]
fn pdf_and_svg_write_the_effected_object_as_an_image() {
for (id, params) in EFFECTS.into_iter().chain(ARTISTIC).chain(BRUSH_STROKES).chain(DISTORT).chain(PIXELATE).chain(TEXTURE).chain(VIDEO) {
for (id, params) in
EFFECTS.into_iter().chain(ARTISTIC).chain(BRUSH_STROKES).chain(DISTORT).chain(PIXELATE).chain(SKETCH).chain(TEXTURE).chain(VIDEO)
{
let (mut s, g) = striped();
let params: Value = serde_json::from_str(params).unwrap();
s.execute("effect.apply", &json!({"effect": id, "ids": [g], "params": params})).unwrap();
+1
View File
@@ -322,6 +322,7 @@ 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": "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}})),
("effect.apply", json!({"effect": "texture.mosaicTiles", "params": {"tileSize": "1e999", "groutWidth": -1, "lightenGrout": null}})),
+1
View File
@@ -266,6 +266,7 @@ fn choices(effect: &str, key: &str) -> Option<form::Choices> {
("brushStrokes.sprayedStrokes", "strokeDirection") => Some(&vectorcraft_effects::pixel::STROKE_DIRECTIONS),
("distort.glass", "texture") => Some(&vectorcraft_effects::pixel::GLASS_TEXTURES),
("pixelate.mezzotint", "type") => Some(&vectorcraft_effects::pixel::MEZZOTINT_TYPES),
("sketch.halftonePattern", "patternType") => Some(&vectorcraft_effects::pixel::HALFTONE_TYPES),
("texture.grain", "grainType") => Some(&vectorcraft_effects::pixel::GRAIN_TYPES),
("texture.texturizer", "texture") => Some(&vectorcraft_effects::pixel::TEXTURES),
("texture.texturizer", "lightDirection") => Some(&vectorcraft_effects::pixel::LIGHT_DIRECTIONS),
+1
View File
@@ -185,6 +185,7 @@ fn raster_filter_dialogs_preview_and_apply() {
("pixelate.crystallize", "cellSize", "20"),
("pixelate.mezzotint", "type", "longLines"),
("pixelate.pointillize", "cellSize", "12"),
("sketch.halftonePattern", "patternType", "line"),
("texture.craquelure", "crackDepth", "9"),
("texture.grain", "grainType", "stippled"),
("texture.mosaicTiles", "tileSize", "20"),
+5 -1
View File
@@ -4925,7 +4925,11 @@ theme Appearance Външен вид
Amber Кехлибарен
Coral Коралов
Violet Виолетов
Sketch Скица
Halftone Pattern… Полутонов шаблон…
Halftone Pattern Полутонов шаблон
Pattern Type Тип шаблон
Dot Точка
# User Interface › Color Scheme
Color Scheme Цветова схема
Modern Модерна
Can't render this file because it contains an unexpected character in line 2 and column 73.
+5 -1
View File
@@ -4943,7 +4943,11 @@ mark-style Japanese Japonské
Amber Jantarová
Coral Korálová
Violet Fialová
Sketch Skica
Halftone Pattern… Polotónový vzorek…
Halftone Pattern Polotónový vzorek
Pattern Type Typ vzorku
Dot Tečka
# User Interface › Color Scheme
Color Scheme Barevné schéma
Modern Moderní
Can't render this file because it contains an unexpected character in line 2 and column 73.
+5 -1
View File
@@ -4480,7 +4480,11 @@ theme Appearance Erscheinungsbild
Coral Koralle
Violet Violett
Pink Pink
Sketch Zeichenfilter
Halftone Pattern… Rasterungseffekt…
Halftone Pattern Rasterungseffekt
Pattern Type Musterart
Dot Punkt
# User Interface › Color Scheme
Color Scheme Farbschema
Modern Modern
Can't render this file because it contains an unexpected character in line 2 and column 73.
+5 -1
View File
@@ -4895,7 +4895,11 @@ effect Points Σημεία
Amber Κεχριμπαρί
Coral Κοραλλί
Violet Βιολετί
Sketch Σκίτσο
Halftone Pattern… Μοτίβο ημιτόνου…
Halftone Pattern Μοτίβο ημιτόνου
Pattern Type Τύπος μοτίβου
Dot Κουκκίδα
# User Interface › Color Scheme
Color Scheme Χρωματικός συνδυασμός
Modern Σύγχρονο
Can't render this file because it contains an unexpected character in line 1859 and column 19.
+5 -1
View File
@@ -4885,7 +4885,11 @@ mark-style Japanese Japonesas
Amber Ámbar
Coral Coral
Violet Violeta
Sketch Bosquejar
Halftone Pattern… Trama de semitonos…
Halftone Pattern Trama de semitonos
Pattern Type Tipo de trama
Dot Punto
# User Interface › Color Scheme
Color Scheme Esquema de color
Modern Moderno
Can't render this file because it contains an unexpected character in line 2 and column 73.
+5 -1
View File
@@ -4907,7 +4907,11 @@ mark-style Japanese Japanilainen
Amber Meripihka
Coral Koralli
Violet Violetti
Sketch Luonnos
Halftone Pattern… Rasterikuvio…
Halftone Pattern Rasterikuvio
Pattern Type Kuviotyyppi
Dot Piste
# User Interface › Color Scheme
Color Scheme Värimalli
Modern Moderni
Can't render this file because it contains an unexpected character in line 2 and column 73.
+5 -1
View File
@@ -4942,7 +4942,11 @@ theme Appearance Apparence
Amber Ambre
Coral Corail
Violet Violet
Sketch Esquisse
Halftone Pattern… Trame de demi-teintes…
Halftone Pattern Trame de demi-teintes
Pattern Type Type de motif
Dot Point
# User Interface › Color Scheme
Color Scheme Jeu de couleurs
Modern Moderne
Can't render this file because it contains an unexpected character in line 2 and column 73.
+5 -1
View File
@@ -4910,7 +4910,11 @@ imageTrace Outline Körvonal
Amber Borostyán
Coral Korall
Violet Ibolya
Sketch Vázlat
Halftone Pattern… Féltónus minta…
Halftone Pattern Féltónus minta
Pattern Type Mintatípus
Dot Pont
# User Interface › Color Scheme
Color Scheme Színséma
Modern Modern
Can't render this file because it contains an unexpected character in line 2 and column 73.
+5 -1
View File
@@ -4520,7 +4520,11 @@ lighting Light Luce
Coral Corallo
Violet Viola
Pink Rosa
Sketch Schizzo
Halftone Pattern… Pattern mezzetinte…
Halftone Pattern Pattern mezzetinte
Pattern Type Tipo di pattern
Dot Punto
# User Interface › Color Scheme
Color Scheme Combinazione colori
Modern Moderno
Can't render this file because it contains an unexpected character in line 2 and column 73.
+5 -1
View File
@@ -4561,7 +4561,11 @@ lighting Light ライト
Amber アンバー
Violet バイオレット
Pink ピンク
Sketch スケッチ
Halftone Pattern… ハーフトーンパターン…
Halftone Pattern ハーフトーンパターン
Pattern Type パターンタイプ
Dot ドット
# User Interface › Color Scheme
Color Scheme 配色
Modern モダン
Can't render this file because it contains an unexpected character in line 2 and column 73.
+5 -1
View File
@@ -4484,7 +4484,11 @@ lighting Light 광원
Coral 코럴
Violet 보라
Pink 분홍
Sketch 스케치
Halftone Pattern… 하프톤 패턴…
Halftone Pattern 하프톤 패턴
Pattern Type 패턴 유형
Dot 점
# User Interface › Color Scheme
Color Scheme 색 구성표
Modern 모던
Can't render this file because it contains an unexpected character in line 2 and column 73.
+5 -1
View File
@@ -4788,7 +4788,11 @@ mark-style Japanese Japońskie
Coral Koralowy
Violet Fioletowy
Pink Różowy
Sketch Szkic
Halftone Pattern… Wzór rastra…
Halftone Pattern Wzór rastra
Pattern Type Typ wzoru
Dot Kropka
# User Interface › Color Scheme
Color Scheme Schemat kolorów
Modern Nowoczesny
Can't render this file because it contains an unexpected character in line 2 and column 73.
+5 -1
View File
@@ -4862,7 +4862,11 @@ mark-style Japanese Japonesas
Amber Âmbar
Coral Coral
Violet Violeta
Sketch Esboço
Halftone Pattern… Padrão de meio-tom…
Halftone Pattern Padrão de meio-tom
Pattern Type Tipo de padrão
Dot Ponto
# User Interface › Color Scheme
Color Scheme Esquema de cores
Modern Moderno
Can't render this file because it contains an unexpected character in line 6 and column 47.
+5 -1
View File
@@ -4516,7 +4516,11 @@ lighting Light Свет
Coral Коралловый
Violet Фиолетовый
Pink Розовый
Sketch Эскиз
Halftone Pattern… Полутоновый узор…
Halftone Pattern Полутоновый узор
Pattern Type Тип узора
Dot Точка
# User Interface › Color Scheme
Color Scheme Цветовая схема
Modern Современная
Can't render this file because it contains an unexpected character in line 2 and column 73.
+5 -1
View File
@@ -4899,7 +4899,11 @@ theme Appearance Utseende
Amber Bärnsten
Coral Korall
Violet Violett
Sketch Skiss
Halftone Pattern… Rastermönster…
Halftone Pattern Rastermönster
Pattern Type Mönstertyp
Dot Punkt
# User Interface › Color Scheme
Color Scheme Färgschema
Modern Modern
Can't render this file because it contains an unexpected character in line 5 and column 25.
+5 -1
View File
@@ -4500,7 +4500,11 @@ lighting Light Світло
Coral Кораловий
Violet Фіолетовий
Pink Рожевий
Sketch Ескіз
Halftone Pattern… Напівтоновий візерунок…
Halftone Pattern Напівтоновий візерунок
Pattern Type Тип візерунка
Dot Крапка
# User Interface › Color Scheme
Color Scheme Колірна схема
Modern Сучасна
Can't render this file because it contains an unexpected character in line 718 and column 40.
+5 -1
View File
@@ -4500,7 +4500,11 @@ theme Appearance Giao diện
Coral San hô
Violet Tím
Pink Hồng
Sketch Phác thảo
Halftone Pattern… Mẫu bán sắc…
Halftone Pattern Mẫu bán sắc
Pattern Type Loại mẫu
Dot Chấm
# User Interface › Color Scheme
Color Scheme Bảng phối màu
Modern Hiện đại
Can't render this file because it contains an unexpected character in line 3734 and column 10.
+5 -1
View File
@@ -4051,7 +4051,11 @@ lighting Light 光源
Amber 琥珀色
Coral 珊瑚色
Violet 紫罗兰色
Sketch 素描
Halftone Pattern… 半调图案…
Halftone Pattern 半调图案
Pattern Type 图案类型
Dot 网点
# User Interface › Color Scheme
Color Scheme 配色方案
Modern 现代
Can't render this file because it contains an unexpected character in line 2 and column 73.
+5 -1
View File
@@ -3839,7 +3839,11 @@ lighting Light 光源
Coral 珊瑚色
Violet 紫羅蘭色
Pink 粉紅色
Sketch 素描
Halftone Pattern… 半色調圖樣…
Halftone Pattern 半色調圖樣
Pattern Type 圖樣類型
Dot 網點
# User Interface › Color Scheme
Color Scheme 配色方案
Modern 現代
Can't render this file because it contains an unexpected character in line 6 and column 47.
+2 -1
View File
@@ -3679,13 +3679,14 @@ fn font_items(english_names: bool) -> Vec<Item> {
/// vector effects): (submenu, the catalogue's menu path of its effects).
/// The raster Stylize submenu shares its path with the vector one and lists only the effects at
/// that path that [`vectorcraft_effects::PIXEL_EFFECTS`] names.
const RASTER_MENUS: [(&str, &[&str]); 9] = [
const RASTER_MENUS: [(&str, &[&str]); 10] = [
("Artistic", &["Effect", "Artistic"]),
("Blur", &["Effect", "Blur"]),
("Brush Strokes", &["Effect", "Brush Strokes"]),
("Distort", &["Effect", "Distort"]),
("Pixelate", &["Effect", "Pixelate"]),
("Sharpen", &["Effect", "Sharpen"]),
("Sketch", &["Effect", "Sketch"]),
("Stylize", &["Effect", "Stylize"]),
("Texture", &["Effect", "Texture"]),
("Video", &["Effect", "Video"]),
+14
View File
@@ -140,6 +140,20 @@ fn effect_menu_lists_the_texture_filters_between_sharpen_and_video() {
assert!(at("Sharpen") < at("Texture") && at("Texture") < at("Video"));
}
/// Effect › Sketch, between Sharpen and the raster Stylize: Halftone Pattern opens its dialog.
#[test]
fn effect_menu_lists_the_sketch_filters_between_sharpen_and_stylize() {
let app = app();
let entries = menus::menu_entries(&app);
let sketch: Vec<_> = entries.iter().filter(|e| e.path == ["Effect", "Sketch"]).collect();
let labels: Vec<&str> = sketch.iter().map(|e| e.label.as_str()).collect();
assert_eq!(labels, ["Halftone Pattern…"]);
assert_eq!(sketch[0].command.as_deref(), Some("effect.dialog"));
assert_eq!(sketch[0].params, json!({"effect": "sketch.halftonePattern"}));
let at = |label: &str| entries.iter().position(|e| e.path[0] == "Effect" && e.label == label).unwrap();
assert!(at("Unsharp Mask…") < at("Halftone Pattern…") && at("Halftone Pattern…") < at("Glowing Edges…"));
}
/// Effect › Stylize under Raster Effects, between Sharpen and Texture: Glowing Edges opens its
/// dialog.
#[test]
+5 -4
View File
@@ -1,6 +1,6 @@
# Effects parity
> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-15 · **Change:** minor (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-16 · **Change:** minor (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)
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 | 44 | 88% | 6–10 |
| Photoshop effects (raster, Effect Gallery) | 57 | 29 | 51% | 23–39 |
| Photoshop effects (raster, Effect Gallery) | 57 | 30 | 53% | 22–38 |
| 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 | — | — |
@@ -44,7 +44,7 @@ its resolution, PDF export warns once per object with the requested and the lowe
| 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 | 0/14 | all |
| Sketch | Bas Relief, Chalk & Charcoal, Charcoal, Chrome, Conté Crayon, Graphic Pen, Halftone Pattern, Note Paper, Photocopy, Plaster, Reticulation, Stamp, Torn Edges, Water Paper | 1/14 | Bas Relief, Chalk & Charcoal, Charcoal, Chrome, Conté Crayon, Graphic Pen, Note Paper, Photocopy, Plaster, Reticulation, 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 28 missing filters are ~19–34 h and the Effect
(see [calibration](target-app-parity.md#calibration)), so the 27 missing filters are ~18–33 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
@@ -99,6 +99,7 @@ angular cut caps, exact visibility, 3D Classic, Turntable, 3D export) is 25–45
| Date | Change | Summary |
|---|---|---|
| 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-15 | minor | Round Corners rounds corners whose anchor carries a handle (where a curve meets a line, or two curves meet) with the round cut Live Corners uses (#1144), reaching `radius` along each side; before, they were left sharp. Corners between handle-less anchors are rounded as before; score unchanged (estimated) |
| 2026-10-14 | minor | Revolve: automatic smooth vector shading, default-on Cap with continuous curved rims, and bounded seam joins; angular cut sides remain open. Scores and hour estimates unchanged at this rounding |
| 2026-10-14 | minor | Wide drop shadows and glows at high zoom render within the raster budget (#1706); stacked Transform effects are bounded (#1705); 3D effects keep the area a nonzero compound path fills (#1724); pattern tiles redraw when a symbol, pattern or image their art shows changes (#1723) (all #1864). Scores unchanged (estimated) |
+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