mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-10-24 09:46:59 -06:00
Merge pull request #2525 from vangel76/feat/2435-artistic-filters
Effects: Artistic: Smudge Stick, Sponge and Watercolor (#2435)
This commit is contained in:
@@ -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
@@ -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
|
||||
|
||||
@@ -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…",
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
}
|
||||
@@ -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"})],
|
||||
);
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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}})),
|
||||
|
||||
@@ -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"),
|
||||
|
||||
@@ -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.
|
@@ -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.
|
@@ -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.
|
@@ -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.
|
@@ -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.
|
@@ -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.
|
@@ -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.
|
@@ -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.
|
@@ -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.
|
@@ -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.
|
@@ -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.
|
@@ -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.
|
@@ -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.
|
@@ -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.
|
@@ -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.
|
@@ -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.
|
@@ -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.
|
@@ -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.
|
@@ -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.
|
@@ -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.
|
@@ -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.
|
@@ -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.
|
@@ -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.
|
@@ -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
@@ -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
File diff suppressed because one or more lines are too long
+4
-3
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user