diff --git a/crates/effects/src/lib.rs b/crates/effects/src/lib.rs index 73bc7149c..10f0dbffb 100644 --- a/crates/effects/src/lib.rs +++ b/crates/effects/src/lib.rs @@ -354,6 +354,13 @@ pub fn effect_catalog() -> Vec { "{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…", diff --git a/crates/effects/src/pixel/artistic.rs b/crates/effects/src/pixel/artistic.rs index 0d6764feb..adcbaccea 100644 --- a/crates/effects/src/pixel/artistic.rs +++ b/crates/effects/src/pixel/artistic.rs @@ -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: //! and //! . //! 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 = (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); +} diff --git a/crates/effects/src/pixel/mod.rs b/crates/effects/src/pixel/mod.rs index 0ca1901e9..67f6e6881 100644 --- a/crates/effects/src/pixel/mod.rs +++ b/crates/effects/src/pixel/mod.rs @@ -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 { 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), diff --git a/crates/effects/src/pixel/sketch.rs b/crates/effects/src/pixel/sketch.rs index d05674cdd..a1fc37463 100644 --- a/crates/effects/src/pixel/sketch.rs +++ b/crates/effects/src/pixel/sketch.rs @@ -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: //! (Sketch effects redraw in //! black and white; Water Paper flows colours on damp fibrous paper) and the public Photoshop Elements Artistic filters guide. Visual diff --git a/crates/effects/src/pixel/tests.rs b/crates/effects/src/pixel/tests.rs index 1e82a7d0a..255a55739 100644 --- a/crates/effects/src/pixel/tests.rs +++ b/crates/effects/src/pixel/tests.rs @@ -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); diff --git a/crates/engine/src/tests_rasterfilters.rs b/crates/engine/src/tests_rasterfilters.rs index c2b2cab7b..313d7fe95 100644 --- a/crates/engine/src/tests_rasterfilters.rs +++ b/crates/engine/src/tests_rasterfilters.rs @@ -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", "{}"), diff --git a/crates/engine/tests/command_sweep.rs b/crates/engine/tests/command_sweep.rs index 155e515b7..f5789301a 100644 --- a/crates/engine/tests/command_sweep.rs +++ b/crates/engine/tests/command_sweep.rs @@ -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"}})), diff --git a/crates/ui-egui/src/i18n/bg.tsv b/crates/ui-egui/src/i18n/bg.tsv index 240f6c0b8..10e9a3085 100644 --- a/crates/ui-egui/src/i18n/bg.tsv +++ b/crates/ui-egui/src/i18n/bg.tsv @@ -4547,6 +4547,8 @@ lighting Light Светлина Film Grain… Филмова зърнистост… Cutout… Апликация… Cutout Апликация + Dry Brush… Суха четка… + Dry Brush Суха четка Neon Glow… Неоново сияние… Neon Glow Неоново сияние Poster Edges… Плакатни ръбове… diff --git a/crates/ui-egui/src/i18n/cs.tsv b/crates/ui-egui/src/i18n/cs.tsv index e029c9ea1..5fc134364 100644 --- a/crates/ui-egui/src/i18n/cs.tsv +++ b/crates/ui-egui/src/i18n/cs.tsv @@ -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… diff --git a/crates/ui-egui/src/i18n/da.tsv b/crates/ui-egui/src/i18n/da.tsv index b895cfc0d..795b35141 100644 --- a/crates/ui-egui/src/i18n/da.tsv +++ b/crates/ui-egui/src/i18n/da.tsv @@ -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 diff --git a/crates/ui-egui/src/i18n/de.tsv b/crates/ui-egui/src/i18n/de.tsv index 76305b953..70f8595c2 100644 --- a/crates/ui-egui/src/i18n/de.tsv +++ b/crates/ui-egui/src/i18n/de.tsv @@ -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… diff --git a/crates/ui-egui/src/i18n/el.tsv b/crates/ui-egui/src/i18n/el.tsv index d4c71fa40..65e0b77ab 100644 --- a/crates/ui-egui/src/i18n/el.tsv +++ b/crates/ui-egui/src/i18n/el.tsv @@ -2259,6 +2259,8 @@ lighting Light Φως Film Grain… Κόκκος φιλμ… Cutout… Αποκοπή… Cutout Αποκοπή + Dry Brush… Ξηρό πινέλο… + Dry Brush Ξηρό πινέλο Neon Glow… Λάμψη νέον… Neon Glow Λάμψη νέον Poster Edges… Άκρες αφίσας… diff --git a/crates/ui-egui/src/i18n/es.tsv b/crates/ui-egui/src/i18n/es.tsv index e829fbe40..582d33b53 100644 --- a/crates/ui-egui/src/i18n/es.tsv +++ b/crates/ui-egui/src/i18n/es.tsv @@ -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… diff --git a/crates/ui-egui/src/i18n/fi.tsv b/crates/ui-egui/src/i18n/fi.tsv index 69fd92a70..68d780b65 100644 --- a/crates/ui-egui/src/i18n/fi.tsv +++ b/crates/ui-egui/src/i18n/fi.tsv @@ -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… diff --git a/crates/ui-egui/src/i18n/fr.tsv b/crates/ui-egui/src/i18n/fr.tsv index b4ce1fcd4..563b9fe44 100644 --- a/crates/ui-egui/src/i18n/fr.tsv +++ b/crates/ui-egui/src/i18n/fr.tsv @@ -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… diff --git a/crates/ui-egui/src/i18n/hu.tsv b/crates/ui-egui/src/i18n/hu.tsv index 7aca38b52..6db74f1bd 100644 --- a/crates/ui-egui/src/i18n/hu.tsv +++ b/crates/ui-egui/src/i18n/hu.tsv @@ -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… diff --git a/crates/ui-egui/src/i18n/id.tsv b/crates/ui-egui/src/i18n/id.tsv index 35134c91b..e53341bb4 100644 --- a/crates/ui-egui/src/i18n/id.tsv +++ b/crates/ui-egui/src/i18n/id.tsv @@ -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 diff --git a/crates/ui-egui/src/i18n/it.tsv b/crates/ui-egui/src/i18n/it.tsv index c9fc47919..d312ce862 100644 --- a/crates/ui-egui/src/i18n/it.tsv +++ b/crates/ui-egui/src/i18n/it.tsv @@ -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… diff --git a/crates/ui-egui/src/i18n/ja.tsv b/crates/ui-egui/src/i18n/ja.tsv index 8a08ed1c1..da1d679a5 100644 --- a/crates/ui-egui/src/i18n/ja.tsv +++ b/crates/ui-egui/src/i18n/ja.tsv @@ -4715,6 +4715,8 @@ lighting Light ライト Film Grain… フィルム粒子… Cutout… カットアウト… Cutout カットアウト + Dry Brush… ドライブラシ… + Dry Brush ドライブラシ Neon Glow… ネオングロー… Neon Glow ネオングロー Poster Edges… ポスターエッジ… diff --git a/crates/ui-egui/src/i18n/ka.tsv b/crates/ui-egui/src/i18n/ka.tsv index 143e28e38..0ba856864 100644 --- a/crates/ui-egui/src/i18n/ka.tsv +++ b/crates/ui-egui/src/i18n/ka.tsv @@ -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 გრაფიკული სტილების წაშლა diff --git a/crates/ui-egui/src/i18n/ko.tsv b/crates/ui-egui/src/i18n/ko.tsv index 5ba2476a7..3e9de3daa 100644 --- a/crates/ui-egui/src/i18n/ko.tsv +++ b/crates/ui-egui/src/i18n/ko.tsv @@ -4639,6 +4639,8 @@ lighting Light 광원 Film Grain… 필름 그레인… Cutout… 오려내기… Cutout 오려내기 + Dry Brush… 드라이 브러시… + Dry Brush 드라이 브러시 Neon Glow… 네온 글로… Neon Glow 네온 글로 Poster Edges… 포스터 가장자리… diff --git a/crates/ui-egui/src/i18n/nl.tsv b/crates/ui-egui/src/i18n/nl.tsv index 6dcbde690..966001792 100644 --- a/crates/ui-egui/src/i18n/nl.tsv +++ b/crates/ui-egui/src/i18n/nl.tsv @@ -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 diff --git a/crates/ui-egui/src/i18n/pl.tsv b/crates/ui-egui/src/i18n/pl.tsv index e65639275..8840c2659 100644 --- a/crates/ui-egui/src/i18n/pl.tsv +++ b/crates/ui-egui/src/i18n/pl.tsv @@ -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… diff --git a/crates/ui-egui/src/i18n/pt-br.tsv b/crates/ui-egui/src/i18n/pt-br.tsv index ca0c12422..f7b086f73 100644 --- a/crates/ui-egui/src/i18n/pt-br.tsv +++ b/crates/ui-egui/src/i18n/pt-br.tsv @@ -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… diff --git a/crates/ui-egui/src/i18n/ru.tsv b/crates/ui-egui/src/i18n/ru.tsv index 5154d31ed..006b59de7 100644 --- a/crates/ui-egui/src/i18n/ru.tsv +++ b/crates/ui-egui/src/i18n/ru.tsv @@ -4672,6 +4672,8 @@ lighting Light Свет Film Grain… Зернистость плёнки… Cutout… Аппликация… Cutout Аппликация + Dry Brush… Сухая кисть… + Dry Brush Сухая кисть Neon Glow… Неоновое свечение… Neon Glow Неоновое свечение Poster Edges… Края плаката… diff --git a/crates/ui-egui/src/i18n/sv.tsv b/crates/ui-egui/src/i18n/sv.tsv index 7c41aa013..680f91ddb 100644 --- a/crates/ui-egui/src/i18n/sv.tsv +++ b/crates/ui-egui/src/i18n/sv.tsv @@ -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… diff --git a/crates/ui-egui/src/i18n/uk.tsv b/crates/ui-egui/src/i18n/uk.tsv index bb5cd7f60..e539fca5a 100644 --- a/crates/ui-egui/src/i18n/uk.tsv +++ b/crates/ui-egui/src/i18n/uk.tsv @@ -4656,6 +4656,8 @@ lighting Light Світло Film Grain… Зернистість плівки… Cutout… Аплікація… Cutout Аплікація + Dry Brush… Суха кисть… + Dry Brush Суха кисть Neon Glow… Неонове сяйво… Neon Glow Неонове сяйво Poster Edges… Плакатні краї… diff --git a/crates/ui-egui/src/i18n/vi.tsv b/crates/ui-egui/src/i18n/vi.tsv index 1d134f84f..53e3b1f60 100644 --- a/crates/ui-egui/src/i18n/vi.tsv +++ b/crates/ui-egui/src/i18n/vi.tsv @@ -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… diff --git a/crates/ui-egui/src/i18n/zh-hans.tsv b/crates/ui-egui/src/i18n/zh-hans.tsv index 8a35a574c..d40a9b918 100644 --- a/crates/ui-egui/src/i18n/zh-hans.tsv +++ b/crates/ui-egui/src/i18n/zh-hans.tsv @@ -4937,7 +4937,9 @@ lighting Light 光源 Artistic 艺术效果 Film Grain… 胶片颗粒… Cutout… 剪纸… + Dry Brush… 干画笔… Cutout 剪纸 + Dry Brush 干画笔 Neon Glow… 霓虹灯光… Neon Glow 霓虹灯光 Poster Edges… 海报边缘… diff --git a/crates/ui-egui/src/i18n/zh-hant.tsv b/crates/ui-egui/src/i18n/zh-hant.tsv index 355d560fb..d1d9340b7 100644 --- a/crates/ui-egui/src/i18n/zh-hant.tsv +++ b/crates/ui-egui/src/i18n/zh-hant.tsv @@ -4743,7 +4743,9 @@ lighting Light 光源 Artistic 藝術效果 Film Grain… 底片顆粒… Cutout… 剪紙… + Dry Brush… 乾畫筆… Cutout 剪紙 + Dry Brush 乾畫筆 Neon Glow… 霓虹光… Neon Glow 霓虹光 Poster Edges… 海報邊緣… diff --git a/crates/ui-egui/src/tests_labels.rs b/crates/ui-egui/src/tests_labels.rs index e497b3fcd..02f4c2587 100644 --- a/crates/ui-egui/src/tests_labels.rs +++ b/crates/ui-egui/src/tests_labels.rs @@ -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…"));