G2: Effect › Artistic › Dry Brush raster filter

Squashed from #3026 onto main.
This commit is contained in:
bflatastic
2026-10-21 23:38:02 +07:00
parent 571ac32362
commit 0a3628f62f
31 changed files with 256 additions and 15 deletions
+7
View File
@@ -354,6 +354,13 @@ pub fn effect_catalog() -> Vec<EffectInfo> {
"{levels: 2..8 (4; steps per colour channel: more levels, more flat colours), edgeSimplicity: 0..10 (4; simpler, smoother edges), edgeFidelity: 1..3 (2; keeps more of the edges' detail)} redraws the object as flat pieces of coloured paper, each piece painted in the average colour that falls into it, keeping its coverage. Original visual approximation; chosen VectorCraft defaults.",
json!({"levels": 4.0, "edgeSimplicity": 4.0, "edgeFidelity": 2.0}),
),
r(
"artistic.dryBrush",
"Dry Brush…",
ARTISTIC,
"{brushSize: 1..10 (2; length of the brush strokes), brushDetail: 0..10 (10; more detail keeps finer structure), texture: 1..3 (1; stronger edges and bristle grain at 3)} repaints the object with short strokes along its form and darkens its edges, keeping its coverage. Original visual approximation; chosen VectorCraft defaults.",
json!({"brushSize": 2.0, "brushDetail": 10.0, "texture": 1.0}),
),
r(
"artistic.filmGrain",
"Film Grain…",
+138 -5
View File
@@ -1,16 +1,20 @@
//! Effect › Artistic (Cutout, Film Grain, Neon Glow, Plastic Wrap, Poster Edges). Original filters built from
//! Effect › Artistic (Cutout, Dry Brush, Film Grain, Neon Glow, Plastic Wrap, Poster Edges, 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>.
//! These are visual approximations, not reproductions of a proprietary algorithm.
use super::brushstrokes::stroke_reach;
use super::edges::sobel;
use super::pixelate::{Place, cell, rand};
const SALT_PLASTIC: u64 = 512;
use super::pixelate::{Place, cell, rand, value_noise};
use super::texture::{luma, premultiply, straight};
use super::{PixelSpace, blur_plane};
use vectorcraft_geom::Point;
use vectorcraft_geom::{Point, Vec2};
const SALT_DRY: u64 = 264;
const MARGIN: f64 = 1.0;
const SALT_PLASTIC: u64 = 512;
const SALT_FILM: u64 = 256;
@@ -314,6 +318,21 @@ pub(super) fn plastic_wrap(px: &mut [[u8; 4]], w: usize, h: usize, space: &Pixel
});
}
/// Stroke length (document units) for Dry Brush at Brush Size `size` (1..10).
fn dry_brush_length(size: f64) -> f64 {
2.0 + 2.8 * (size.clamp(1.0, 10.0) - 1.0)
}
/// How much Dry Brush softens before smearing at Brush Detail `detail` (0..10; 10 keeps the most detail).
fn dry_brush_sigma(detail: f64) -> f64 {
0.22 * (10.0 - detail.clamp(0.0, 10.0))
}
/// How far (document units) Dry Brush reads from a pixel.
pub(super) fn dry_brush_reach(size: f64, detail: f64) -> f64 {
stroke_reach(dry_brush_length(size)) + 3.0 * dry_brush_sigma(detail) + MARGIN
}
/// How far (document units) Neon Glow's glow spreads at `size` (-24..24).
pub(super) fn glow_spread(size: f64) -> f64 {
0.25 * size.abs().min(24.0)
@@ -343,3 +362,117 @@ pub(super) fn neon_glow(px: &mut [[u8; 4]], w: usize, h: usize, space: &PixelSpa
*p = premultiply(c, f32::from(p[3]) / 255.0);
}
}
/// The tone at `(x, y)` in `tone`, `w` × `h`, or 0 outside.
fn tone_at(tone: &[f32], w: usize, h: usize, x: isize, y: isize) -> f32 {
if x < 0 || y < 0 {
return 0.0;
}
let (x, y) = (x as usize, y as usize);
if x >= w || y >= h {
return 0.0;
}
tone[y * w + x]
}
/// A unit vector along the image gradient's tangent at `(x, y)`, or horizontal when flat.
fn dry_brush_dir(tone: &[f32], w: usize, h: usize, x: usize, y: usize) -> Vec2 {
let gx = 0.5 * (tone_at(tone, w, h, x as isize + 1, y as isize) - tone_at(tone, w, h, x as isize - 1, y as isize));
let gy = 0.5 * (tone_at(tone, w, h, x as isize, y as isize + 1) - tone_at(tone, w, h, x as isize, y as isize - 1));
let v = Vec2::new(-f64::from(gy), f64::from(gx));
let len = v.hypot();
if len > 1e-6 { v / len } else { Vec2::new(1.0, 0.0) }
}
/// Premultiplied RGBA (0..1) at document `q`, bilinearly from `src`.
fn sample_pm(src: &[[f32; 4]], w: usize, h: usize, place: &Place, q: Point) -> [f32; 4] {
let p = place.to_px * q;
if !(p.x.is_finite() && p.y.is_finite()) {
return [0.0; 4];
}
let (x, y) = (p.x - 0.5, p.y - 0.5);
let (i, j) = (x.floor() as isize, y.floor() as isize);
let (fx, fy) = (x - i as f64, y - j as f64);
let mut acc = [0.0f32; 4];
for (row, wy) in [(j, 1.0 - fy), (j + 1, fy)] {
let Some(row) = usize::try_from(row).ok().filter(|r| *r < h) else { continue };
for (col, wx) in [(i, 1.0 - fx), (i + 1, fx)] {
let Some(col) = usize::try_from(col).ok().filter(|c| *c < w) else { continue };
let k = (wx * wy) as f32;
if let Some(p) = src.get(row * w + col) {
for (a, v) in acc.iter_mut().zip(p) {
*a += *v * k;
}
}
}
}
acc
}
/// Dry Brush: the object repainted with short strokes along its form, edges darkened. `size`
/// (1..10) sets the stroke length, `detail` (0..10) how much is softened first (10 keeps the most
/// detail), and `texture` (1..3) how strongly edges and bristle grain contrast. Coverage is kept.
pub(super) fn dry_brush(px: &mut [[u8; 4]], w: usize, h: usize, space: &PixelSpace, size: f64, detail: f64, texture: f64) {
if !([size, detail, texture].iter().all(|v| v.is_finite()) && space.px.is_finite() && space.px > 0.0) {
return;
}
let len = dry_brush_length(size);
let sigma = dry_brush_sigma(detail);
let texture = texture.clamp(1.0, 3.0);
let edge_gain = (0.25 + 0.2 * (texture - 1.0)) as f32;
let bristle = (0.04 + 0.05 * (texture - 1.0)) as f32;
let (mut colour, alpha) = planes(px);
if sigma > 0.0 {
soften(&mut colour, &alpha, w, h, sigma);
}
let at = |k: usize| [0, 1, 2].map(|i| colour[i].get(k).copied().unwrap_or(0.0));
let tone: Vec<f32> = (0..w * h)
.map(|k| {
let c = at(k);
let a = alpha.get(k).copied().unwrap_or(0.0);
(0.299 * c[0] + 0.587 * c[1] + 0.114 * c[2]) * a
})
.collect();
let edges = sobel(&tone, w, h);
let src: Vec<[f32; 4]> = (0..w * h)
.map(|k| {
let c = at(k);
let a = alpha.get(k).copied().unwrap_or(0.0);
[c[0] * a, c[1] * a, c[2] * a, a]
})
.collect();
let Some(place) = Place::new(space, w, h) else { return };
const SAMPLES: usize = 5;
let mut out = px.to_vec();
for (k, p) in out.iter_mut().enumerate() {
if p[3] == 0 {
continue;
}
let (x, y) = (k % w, k / w);
let q = place.at(x, y);
let dir = dry_brush_dir(&tone, w, h, x, y);
let mut acc = [0.0f32; 4];
for i in 0..SAMPLES {
let t = (i as f64 / (SAMPLES - 1) as f64 - 0.5) * len;
let s = sample_pm(&src, w, h, &place, q + dir * t);
for (a, v) in acc.iter_mut().zip(s) {
*a += v;
}
}
for a in &mut acc {
*a /= SAMPLES as f32;
}
let a = acc[3].clamp(0.0, 1.0);
let c = if a > 1e-6 {
[0, 1, 2].map(|i| (acc[i] / a).clamp(0.0, 1.0))
} else {
straight(*p)
};
let edge = edges[k].min(1.0);
let dark = (edge_gain * edge).clamp(0.0, 0.75);
let grain = bristle * (2.0 * value_noise(SALT_DRY, q.x / (5.0 * space.px), q.y / (5.0 * space.px)) - 1.0) as f32;
let c = c.map(|v| (v * (1.0 - dark) + grain).clamp(0.0, 1.0));
*p = premultiply(c, f32::from(p[3]) / 255.0);
}
px.copy_from_slice(&out);
}
+17 -4
View File
@@ -1,9 +1,9 @@
//! Photoshop-style raster effects (Effect › Artistic › Cutout, Film Grain, Neon Glow, Plastic Wrap and
//! Photoshop-style raster effects (Effect › Artistic › Cutout, Dry Brush, Film Grain, Neon Glow, Plastic Wrap and
//! 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, Note Paper, Plaster and
//! Reticulation, Stylize ›
//! Sharpen › Unsharp Mask, Sketch › Bas Relief, Chalk & Charcoal, Charcoal, Graphic Pen, Halftone Pattern, Note Paper, Photocopy, Plaster,
//! Reticulation and Stamp, 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.
@@ -43,8 +43,9 @@ 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; 47] = [
pub const PIXEL_EFFECTS: [&str; 48] = [
"artistic.cutout",
"artistic.dryBrush",
"artistic.filmGrain",
"artistic.neonGlow",
"artistic.plasticWrap",
@@ -100,6 +101,10 @@ pub enum PixelFx {
/// Artistic › Cutout: flat pieces of colour in `levels` (2..8) steps per channel, their
/// edges as simple as `simplicity` (0..10) and as faithful as `fidelity` (1..3) say.
Cutout { levels: f64, simplicity: f64, fidelity: f64 },
/// Artistic › Dry Brush: short strokes along the form with darkened edges; `size` (1..10) is the
/// brush length, `detail` (0..10) how much detail is kept, `texture` (1..3) the edge and bristle
/// contrast.
DryBrush { size: f64, detail: f64, texture: f64 },
/// Artistic › Film Grain: monochrome grain in shadows and midtones, a wider bright
/// highlight area (0..20), and tonal intensity (0..10), preserving coverage.
FilmGrain { grain: f64, highlight: f64, intensity: f64 },
@@ -292,6 +297,11 @@ pub(crate) fn parse(id: &str, p: &Value) -> Option<PixelFx> {
simplicity: num(p, "edgeSimplicity", 4.0).clamp(0.0, 10.0),
fidelity: num(p, "edgeFidelity", 2.0).round().clamp(1.0, 3.0),
},
"artistic.dryBrush" => PixelFx::DryBrush {
size: num(p, "brushSize", 2.0).clamp(1.0, 10.0),
detail: num(p, "brushDetail", 10.0).clamp(0.0, 10.0),
texture: num(p, "texture", 1.0).round().clamp(1.0, 3.0),
},
"artistic.neonGlow" => PixelFx::NeonGlow {
size: num(p, "glowSize", 5.0).clamp(-24.0, 24.0),
brightness: num(p, "glowBrightness", 15.0).clamp(0.0, 50.0),
@@ -540,6 +550,7 @@ impl PixelFx {
PixelFx::SmudgeStick { .. } | PixelFx::Sponge { .. } | PixelFx::Watercolor { .. } => 0.0,
PixelFx::FilmGrain { .. }
| PixelFx::Cutout { .. }
| PixelFx::DryBrush { .. }
| PixelFx::NeonGlow { .. }
| PixelFx::PlasticWrap { .. }
| PixelFx::PosterEdges { .. }
@@ -600,6 +611,7 @@ impl PixelFx {
| PixelFx::HalftonePattern { .. }
| PixelFx::NotePaper { .. }
| PixelFx::Reticulation { .. } => Some(0.0),
PixelFx::DryBrush { size, detail, .. } => Some(artistic::dry_brush_reach(size, detail)),
PixelFx::Plaster { smoothness, .. } => Some(sketch::plaster_reach(smoothness)),
PixelFx::WaterPaper { fiber_length, .. } => {
Some(sketch::water_paper_shift(fiber_length) + 3.0 * (0.06 * fiber_length.clamp(3.0, 50.0)).max(0.35))
@@ -632,6 +644,7 @@ impl PixelFx {
let to_px = |len: f64| len / space.px.max(1e-9);
match *self {
PixelFx::Cutout { levels, simplicity, fidelity } => artistic::cutout(px, w, h, space, levels, simplicity, fidelity),
PixelFx::DryBrush { size, detail, texture } => artistic::dry_brush(px, w, h, space, size, detail, texture),
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),
+2 -1
View File
@@ -1,4 +1,5 @@
//! Effect › Sketch (Chalk & Charcoal, Chrome, Halftone Pattern, Note Paper, Plaster, Reticulation and Water Paper; Graphic Pen lives in
//! Effect › Sketch (Bas Relief, Chalk & Charcoal, Charcoal, Chrome, Halftone Pattern, Note Paper, Photocopy, Plaster,
//! Reticulation, Stamp and Water Paper; Graphic Pen lives in
//! [`super::brushstrokes`]). Original filters built from public behaviour documentation:
//! <https://helpx.adobe.com/illustrator/using/summary-effects.html> (Sketch effects redraw in
//! black and white; Water Paper flows colours on damp fibrous paper) and the public Photoshop Elements Artistic filters guide. Visual
+25 -1
View File
@@ -2401,6 +2401,11 @@ fn artistic_params_take_defaults_and_stay_in_range() {
fx("artistic.cutout", json!({"levels": 1e308, "edgeSimplicity": -5, "edgeFidelity": "NaN"})),
PixelFx::Cutout { levels: 8.0, simplicity: 0.0, fidelity: 2.0 }
);
assert_eq!(fx("artistic.dryBrush", json!({})), PixelFx::DryBrush { size: 2.0, detail: 10.0, texture: 1.0 });
assert_eq!(
fx("artistic.dryBrush", json!({"brushSize": 1e308, "brushDetail": 0, "texture": -2})),
PixelFx::DryBrush { size: 10.0, detail: 0.0, texture: 1.0 }
);
assert_eq!(fx("artistic.posterEdges", json!({})), PixelFx::PosterEdges { thickness: 2.0, intensity: 1.0, posterization: 2.0 });
assert_eq!(
fx("artistic.posterEdges", json!({"edgeThickness": 99, "edgeIntensity": -1, "posterization": 6.4})),
@@ -2418,7 +2423,7 @@ fn artistic_params_take_defaults_and_stay_in_range() {
fx("artistic.plasticWrap", json!({"highlightStrength": -1, "detail": 99, "smoothness": 0})),
PixelFx::PlasticWrap { highlight: 0.0, detail: 15.0, smoothness: 1.0 }
);
for id in ["artistic.cutout", "artistic.neonGlow", "artistic.plasticWrap", "artistic.posterEdges"] {
for id in ["artistic.cutout", "artistic.dryBrush", "artistic.neonGlow", "artistic.plasticWrap", "artistic.posterEdges"] {
assert_eq!(fx(id, json!({})).outset(Rect::new(0.0, 0.0, 40.0, 30.0)), 0.0, "{id} keeps the object's shape");
}
}
@@ -2435,6 +2440,7 @@ fn artistic_filters_leave_invalid_coordinate_spaces_alone() {
let colour = [0.3, 0.65, 1.0];
let filters = [
PixelFx::Cutout { levels: 4.0, simplicity: 4.0, fidelity: 2.0 },
PixelFx::DryBrush { size: 2.0, detail: 10.0, texture: 1.0 },
PixelFx::NeonGlow { size: 5.0, brightness: 15.0, colour },
PixelFx::PlasticWrap { highlight: 20.0, detail: 12.0, smoothness: 15.0 },
PixelFx::PosterEdges { thickness: 2.0, intensity: 1.0, posterization: 2.0 },
@@ -2548,6 +2554,24 @@ fn poster_edges_thickness_widens_the_lines() {
assert!(dark(10.0) > dark(0.0), "{} vs {}", dark(10.0), dark(0.0));
}
#[test]
fn dry_brush_repaints_and_preserves_coverage() {
let (w, h) = (48, 24);
let src = ramp(w, h);
let mut out = src.clone();
fx("artistic.dryBrush", json!({})).apply(&mut out, w, h, &space(w, h));
assert_ne!(out, src);
for (a, b) in src.as_chunks::<4>().0.iter().zip(out.as_chunks::<4>().0) {
assert_eq!(a[3], b[3], "coverage preserved");
}
let mut clear = vec![0u8; w * h * 4];
let before = clear.clone();
fx("artistic.dryBrush", json!({})).apply(&mut clear, w, h, &space(w, h));
assert_eq!(clear, before);
let reach = fx("artistic.dryBrush", json!({"brushSize": 10, "brushDetail": 1})).reach();
assert!(reach.is_some_and(|r| r > 0.0));
}
#[test]
fn neon_glow_lights_the_bright_or_the_dark_areas_in_its_colour() {
let (w, h) = (40, 20);
+2 -1
View File
@@ -31,8 +31,9 @@ const BRUSH_STROKES: [(&str, &str); 8] = [
];
/// Effect › Artistic, at their defaults.
const ARTISTIC: [(&str, &str); 8] = [
const ARTISTIC: [(&str, &str); 9] = [
("artistic.cutout", "{}"),
("artistic.dryBrush", "{}"),
("artistic.filmGrain", "{}"),
("artistic.neonGlow", "{}"),
("artistic.plasticWrap", "{}"),
+1
View File
@@ -370,6 +370,7 @@ fn structured_junk() {
("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.dryBrush", "params": {"brushSize": -1e308, "brushDetail": "NaN", "texture": 1e308}})),
("effect.apply", json!({"effect": "artistic.neonGlow", "params": {"glowSize": -1e308, "glowBrightness": "1e999", "color": [1]}})),
("effect.apply", json!({"effect": "artistic.posterEdges", "params": {"edgeThickness": 1e308, "edgeIntensity": [], "posterization": -7}})),
("effect.apply", json!({"effect": "sketch.chrome", "params": {"detail": -1e308, "smoothness": "NaN"}})),
+2
View File
@@ -4547,6 +4547,8 @@ lighting Light Светлина
Film Grain… Филмова зърнистост…
Cutout… Апликация…
Cutout Апликация
Dry Brush… Суха четка…
Dry Brush Суха четка
Neon Glow… Неоново сияние…
Neon Glow Неоново сияние
Poster Edges… Плакатни ръбове…
Can't render this file because it contains an unexpected character in line 2 and column 73.
+2
View File
@@ -5046,6 +5046,8 @@ imageTrace Outline Obrys
Film Grain… Filmové zrno…
Cutout… Výřez…
Cutout Výřez
Dry Brush… Suchý štětec…
Dry Brush Suchý štětec
Neon Glow… Neonová záře…
Neon Glow Neonová záře
Poster Edges… Plakátové okraje…
Can't render this file because it contains an unexpected character in line 2 and column 73.
+2
View File
@@ -5165,9 +5165,11 @@ swatch Bright Violet Kraftig violet
Unlock Variables Lås variabler op
Change to Artboard Rulers Skift til kunstpladelinealer
Cutout… Udklip…
Dry Brush… Tør pensel…
Neon Glow… Neonglød…
Poster Edges… Plakatkanter…
Cutout Udklip
Dry Brush Tør pensel
Neon Glow Neonglød
Poster Edges Plakatkanter
Apply Data Set Anvend datasæt
Can't render this file because it contains an unexpected character in line 2 and column 73.
+2
View File
@@ -4634,6 +4634,8 @@ lighting Light Licht
Film Grain… Filmkörnung…
Cutout… Ausschnitt…
Cutout Ausschnitt
Dry Brush… Trockenpinsel…
Dry Brush Trockenpinsel
Neon Glow… Neonglühen…
Neon Glow Neonglühen
Poster Edges… Posterkanten…
Can't render this file because it contains an unexpected character in line 2 and column 73.
+2
View File
@@ -2259,6 +2259,8 @@ lighting Light Φως
Film Grain… Κόκκος φιλμ…
Cutout… Αποκοπή…
Cutout Αποκοπή
Dry Brush… Ξηρό πινέλο…
Dry Brush Ξηρό πινέλο
Neon Glow… Λάμψη νέον…
Neon Glow Λάμψη νέον
Poster Edges… Άκρες αφίσας…
Can't render this file because it contains an unexpected character in line 2029 and column 19.
+2
View File
@@ -4634,6 +4634,8 @@ lighting Light Luz
Film Grain… Granulado de película…
Cutout… Recorte…
Cutout Recorte
Dry Brush… Pincel seco…
Dry Brush Pincel seco
Neon Glow… Resplandor de neón…
Neon Glow Resplandor de neón
Poster Edges… Bordes de póster…
Can't render this file because it contains an unexpected character in line 2 and column 73.
+2
View File
@@ -4941,6 +4941,8 @@ imageTrace Outline Ääriviiva
Film Grain… Filmin rakeisuus…
Cutout… Leikkaus…
Cutout Leikkaus
Dry Brush… Kuiva sivellin…
Dry Brush Kuiva sivellin
Neon Glow… Neonhehku…
Neon Glow Neonhehku
Poster Edges… Julisteen reunat…
Can't render this file because it contains an unexpected character in line 2 and column 73.
+2
View File
@@ -4644,6 +4644,8 @@ lighting Light Lumière
Film Grain… Grain photographique…
Cutout… Découpage…
Cutout Découpage
Dry Brush… Pinceau sec…
Dry Brush Pinceau sec
Neon Glow… Néon…
Neon Glow Néon
Poster Edges… Bords d’affiche…
Can't render this file because it contains an unexpected character in line 2 and column 73.
+2
View File
@@ -1581,6 +1581,8 @@ file-filter Templates Sablonok
Film Grain… Filmszemcse…
Cutout… Kivágás…
Cutout Kivágás
Dry Brush… Száraz ecset…
Dry Brush Száraz ecset
Neon Glow… Neonfény…
Neon Glow Neonfény
Poster Edges… Poszterszélek…
Can't render this file because it contains an unexpected character in line 2 and column 73.
+2
View File
@@ -5165,9 +5165,11 @@ swatch Bright Violet Violet cerah
Unlock Variables Buka kunci variabel
Change to Artboard Rulers Ubah ke penggaris artboard
Cutout… Potongan…
Dry Brush… Kuas kering…
Neon Glow… Pendar neon…
Poster Edges… Tepi poster…
Cutout Potongan
Dry Brush Kuas kering
Neon Glow Pendar neon
Poster Edges Tepi poster
Apply Data Set Terapkan set data
Can't render this file because it contains an unexpected character in line 3797 and column 19.
+2
View File
@@ -4676,6 +4676,8 @@ lighting Light Luce
Film Grain… Grana della pellicola…
Cutout… Ritaglio…
Cutout Ritaglio
Dry Brush… Pennello secco…
Dry Brush Pennello secco
Neon Glow… Bagliore neon…
Neon Glow Bagliore neon
Poster Edges… Bordi poster…
Can't render this file because it contains an unexpected character in line 2 and column 73.
+2
View File
@@ -4715,6 +4715,8 @@ lighting Light ライト
Film Grain… フィルム粒子…
Cutout… カットアウト…
Cutout カットアウト
Dry Brush… ドライブラシ…
Dry Brush ドライブラシ
Neon Glow… ネオングロー…
Neon Glow ネオングロー
Poster Edges… ポスターエッジ…
Can't render this file because it contains an unexpected character in line 2 and column 73.
+2
View File
@@ -5002,7 +5002,9 @@ imageTrace Outline კონტური
Create Slice from Selection ნაჭრის შექმნა მონიშნულიდან
Create Slices from Guides ნაჭრების შექმნა დამხმარე ხაზებიდან
Cutout აპლიკაცია
Dry Brush მშრალი ფერწანი
Cutout… აპლიკაცია…
Dry Brush… მშრალი ფერწანი…
Delete All Slices ყველა ნაჭრის წაშლა
Delete Data Sets მონაცემთა ნაკრებების წაშლა
Delete Graphic Styles გრაფიკული სტილების წაშლა
Can't render this file because it contains an unexpected character in line 2 and column 73.
+2
View File
@@ -4639,6 +4639,8 @@ lighting Light 광원
Film Grain… 필름 그레인…
Cutout… 오려내기…
Cutout 오려내기
Dry Brush… 드라이 브러시…
Dry Brush 드라이 브러시
Neon Glow… 네온 글로…
Neon Glow 네온 글로
Poster Edges… 포스터 가장자리…
Can't render this file because it contains an unexpected character in line 2 and column 73.
+2
View File
@@ -5177,9 +5177,11 @@ swatch Bright Violet Felviolet
Unlock Variables Variabelen ontgrendelen
Change to Artboard Rulers Wijzigen in tekenbordlinialen
Cutout… Knipsel…
Dry Brush… Droog penseel…
Neon Glow… Neongloed…
Poster Edges… Posterranden…
Cutout Knipsel
Dry Brush Droog penseel
Neon Glow Neongloed
Poster Edges Posterranden
Apply Data Set Gegevensset toepassen
Can't render this file because it contains an unexpected character in line 2 and column 73.
+2
View File
@@ -4879,6 +4879,8 @@ imageTrace Outline Kontur
Film Grain… Ziarno filmu…
Cutout… Wycinanka…
Cutout Wycinanka
Dry Brush… Suchy pędzel…
Dry Brush Suchy pędzel
Neon Glow… Neonowa poświata…
Neon Glow Neonowa poświata
Poster Edges… Krawędzie plakatu…
Can't render this file because it contains an unexpected character in line 2 and column 73.
+2
View File
@@ -4035,6 +4035,8 @@ axis Both Ambos
Film Grain… Granulação de filme…
Cutout… Recorte…
Cutout Recorte
Dry Brush… Pincel seco…
Dry Brush Pincel seco
Neon Glow… Brilho neon…
Neon Glow Brilho neon
Poster Edges… Bordas de pôster…
Can't render this file because it contains an unexpected character in line 6 and column 47.
+2
View File
@@ -4672,6 +4672,8 @@ lighting Light Свет
Film Grain… Зернистость плёнки…
Cutout… Аппликация…
Cutout Аппликация
Dry Brush… Сухая кисть…
Dry Brush Сухая кисть
Neon Glow… Неоновое свечение…
Neon Glow Неоновое свечение
Poster Edges… Края плаката…
Can't render this file because it contains an unexpected character in line 2 and column 73.
+2
View File
@@ -4553,6 +4553,8 @@ lighting Light Ljus
Film Grain… Filmkorn…
Cutout… Urklipp…
Cutout Urklipp
Dry Brush… Torr pensel…
Dry Brush Torr pensel
Neon Glow… Neonsken…
Neon Glow Neonsken
Poster Edges… Affischkanter…
Can't render this file because it contains an unexpected character in line 5 and column 25.
+2
View File
@@ -4656,6 +4656,8 @@ lighting Light Світло
Film Grain… Зернистість плівки…
Cutout… Аплікація…
Cutout Аплікація
Dry Brush… Суха кисть…
Dry Brush Суха кисть
Neon Glow… Неонове сяйво…
Neon Glow Неонове сяйво
Poster Edges… Плакатні краї…
Can't render this file because it contains an unexpected character in line 726 and column 40.
+2
View File
@@ -1269,6 +1269,8 @@
Film Grain… Hạt phim…
Cutout… Cắt dán…
Cutout Cắt dán
Dry Brush… Cọ khô…
Dry Brush Cọ khô
Neon Glow… Ánh neon…
Neon Glow Ánh neon
Poster Edges… Viền áp phích…
1 # Vietnamese (vi) catalog. Same format as zh-hant.tsv: context <TAB> source <TAB> translation
1269
1270
1271
1272
1273
1274
1275
1276
+2
View File
@@ -4937,7 +4937,9 @@ lighting Light 光源
Artistic 艺术效果
Film Grain… 胶片颗粒…
Cutout… 剪纸…
Dry Brush… 干画笔…
Cutout 剪纸
Dry Brush 干画笔
Neon Glow… 霓虹灯光…
Neon Glow 霓虹灯光
Poster Edges… 海报边缘…
Can't render this file because it contains an unexpected character in line 2 and column 73.
+2
View File
@@ -4743,7 +4743,9 @@ lighting Light 光源
Artistic 藝術效果
Film Grain… 底片顆粒…
Cutout… 剪紙…
Dry Brush… 乾畫筆…
Cutout 剪紙
Dry Brush 乾畫筆
Neon Glow… 霓虹光…
Neon Glow 霓虹光
Poster Edges… 海報邊緣…
Can't render this file because it contains an unexpected character in line 6 and column 47.
+18 -3
View File
@@ -86,9 +86,23 @@ 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…", "Smudge Stick…", "Sponge…", "Watercolor…"]);
assert_eq!(
labels,
[
"Cutout…",
"Dry Brush…",
"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"}));
assert_eq!(artistic[1].params, json!({"effect": "artistic.dryBrush"}));
assert_eq!(artistic[3].params, json!({"effect": "artistic.neonGlow"}));
let at = |name: &str| entries.iter().position(|e| e.path == ["Effect", name]).unwrap();
assert!(at("Artistic") < at("Blur"));
}
@@ -209,7 +223,8 @@ fn effect_menu_lists_the_sketch_filters_between_sharpen_and_stylize() {
assert_eq!(sketch[10].params, json!({"effect": "sketch.stamp"}));
assert_eq!(sketch[11].params, json!({"effect": "sketch.waterPaper"}));
let at = |label: &str| entries.iter().position(|e| e.path[0] == "Effect" && e.label == label).unwrap();
assert!(at("Unsharp Mask…") < at("Chalk & Charcoal…") && at("Chalk & Charcoal…") < at("Chrome…"));
assert!(at("Unsharp Mask…") < at("Bas Relief…") && at("Bas Relief…") < at("Chalk & Charcoal…"));
assert!(at("Chalk & Charcoal…") < at("Chrome…"));
assert!(at("Chrome…") < at("Graphic Pen…") && at("Graphic Pen…") < at("Halftone Pattern…"));
assert!(at("Halftone Pattern…") < at("Note Paper…") && at("Note Paper…") < at("Plaster…"));
assert!(at("Plaster…") < at("Reticulation…"));