G2: Effect › Artistic › Palette Knife raster filter

Squashed from #3038 onto main.
This commit is contained in:
bflatastic
2026-10-22 00:23:48 +07:00
parent bbe026607f
commit f329f6271d
29 changed files with 164 additions and 5 deletions
+8
View File
@@ -148,6 +148,7 @@ fn lengths_of(id: &str) -> Lengths {
"stylize.innerGlow" | "stylize.outerGlow" => always(&["blur"]),
"stylize.glowingEdges" => always(&["edgeWidth", "smoothness"]),
"brushStrokes.accentedEdges" => always(&["edgeWidth"]),
"artistic.paletteKnife" => always(&["strokeSize"]),
"brushStrokes.angledStrokes" | "brushStrokes.crosshatch" | "brushStrokes.inkOutlines" => always(&["strokeLength"]),
"brushStrokes.spatter" => always(&["sprayRadius"]),
"brushStrokes.sprayedStrokes" => always(&["strokeLength", "sprayRadius"]),
@@ -377,6 +378,13 @@ pub fn effect_catalog() -> Vec<EffectInfo> {
"{glowSize: -24..24 (5; above 0 the light areas glow, below 0 the dark ones; farther from 0 spreads the glow wider), glowBrightness: 0..50 (15; a stronger glow over dimmer greys), color: \"#rrggbb\" (\"#4da6ff\"; the glow's colour)} redraws the object in greys lit by a coloured glow, keeping its coverage. Original visual approximation; chosen VectorCraft defaults.",
json!({"glowSize": 5.0, "glowBrightness": 15.0, "color": "#4da6ff"}),
),
r(
"artistic.paletteKnife",
"Palette Knife…",
ARTISTIC,
"{strokeSize: 1..50 (10; length of the knife strokes in document units), strokeDetail: 1..3 (2; more detail keeps more of the original), softness: 0..10 (3; softens the strokes)} smears the object's colours with short angled knife strokes, keeping its coverage. Original visual approximation; chosen VectorCraft defaults.",
json!({"strokeSize": 10.0, "strokeDetail": 2.0, "softness": 3.0}),
),
r(
"artistic.plasticWrap",
"Plastic Wrap…",
+53 -2
View File
@@ -1,11 +1,13 @@
//! Effect › Artistic (Cutout, Dry Brush, Film Grain, Neon Glow, Plastic Wrap, Poster Edges, Smudge Stick, Sponge and
//! Effect › Artistic (Cutout, Dry Brush, Film Grain, Neon Glow, Palette Knife, 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 std::f64::consts::TAU;
use super::brushstrokes::{Smear, mix, stroke_reach, unit, unpremultiply};
use super::edges::sobel;
use super::pixelate::{Place, cell, rand, value_noise};
use super::texture::{luma, premultiply, straight};
@@ -472,3 +474,52 @@ pub(super) fn dry_brush(px: &mut [[u8; 4]], w: usize, h: usize, space: &PixelSpa
}
px.copy_from_slice(&out);
}
const SALT_KNIFE: u64 = 208;
/// How far Palette Knife reaches with strokes `stroke_size` long and Softness `softness` (0..10).
pub(super) fn palette_knife_reach(stroke_size: f64, softness: f64) -> f64 {
stroke_reach(stroke_size) + 3.0 * 0.35 * softness.clamp(0.0, 10.0) + 1.0
}
/// Artistic › Palette Knife: the object repainted with short, angled knife strokes that smear its
/// colours; `stroke_size` (1..50) sets their length, `stroke_detail` (1..3) keeps more of the
/// original where it's higher, and `softness` (0..10) softens the strokes. Coverage is kept.
pub(super) fn palette_knife(px: &mut [[u8; 4]], w: usize, h: usize, space: &PixelSpace, stroke_size: f64, stroke_detail: f64, softness: f64) {
let Some(place) = Place::new(space, w, h) else { return };
if !(stroke_size.is_finite() && stroke_detail.is_finite() && softness.is_finite()) {
return;
}
let len = stroke_size.clamp(1.0, 50.0);
let keep = ((stroke_detail.clamp(1.0, 3.0) - 1.0) / 2.0) as f32;
let smear = Smear::new(px, w, h, &place, len);
let row = (0.25 * smear.len).max(1.0);
let cell = len * 0.35;
let src = px.to_vec();
place.each(px, |x, y, p| {
if p[3] == 0 {
return;
}
let q = place.at(x, y);
let angle = TAU * value_noise(SALT_KNIFE, q.x / cell, q.y / cell);
let dir = Vec2::new(angle.cos(), angle.sin());
let pm = smear.stroke(SALT_KNIFE + 1, q, dir, row);
let orig = unit(src.get(y * w + x).copied().unwrap_or([0; 4]));
let coverage = orig[3];
let (c, _) = unpremultiply(mix(orig, pm, 1.0 - keep));
*p = premultiply(c, coverage);
});
let sigma = 0.35 * softness.clamp(0.0, 10.0) / place.px;
if sigma > 0.0 {
let (mut colour, alpha) = planes(px);
let cover = alpha.clone();
soften(&mut colour, &alpha, w, h, sigma);
for (k, p) in px.iter_mut().enumerate() {
if cover[k] == 0.0 {
continue;
}
let c = [colour[0][k], colour[1][k], colour[2][k]];
*p = premultiply(c, cover[k]);
}
}
}
+16 -1
View File
@@ -43,11 +43,12 @@ pub use sketch::{HALFTONE_TYPES, HalftonePattern};
pub use texture::{GRAIN_TYPES, Grain, LIGHT_DIRECTIONS, Light, TEXTURES, TEXTURES_LOADED, Texture};
/// The Photoshop-style effect ids (all raster effects, see [`crate::is_raster`]).
pub const PIXEL_EFFECTS: [&str; 48] = [
pub const PIXEL_EFFECTS: [&str; 49] = [
"artistic.cutout",
"artistic.dryBrush",
"artistic.filmGrain",
"artistic.neonGlow",
"artistic.paletteKnife",
"artistic.plasticWrap",
"artistic.posterEdges",
"artistic.smudgeStick",
@@ -122,6 +123,10 @@ pub enum PixelFx {
/// light areas (`size` above 0) or the dark ones (below 0), spread by |`size`| (-24..24), as
/// strong as `brightness` (0..50).
NeonGlow { size: f64, brightness: f64, colour: [f32; 3] },
/// Artistic › Palette Knife: knife strokes `stroke_size` (1..50) long smear the colours;
/// `stroke_detail` (1..3) keeps more original detail when higher; `softness` (0..10) softens
/// the strokes.
PaletteKnife { stroke_size: f64, stroke_detail: f64, softness: f64 },
/// Artistic › Plastic Wrap: specular highlights as strong as `highlight` (0..20) on a relief
/// wrinkled by `detail` (0..15) and softened by `smoothness` (1..15).
PlasticWrap { highlight: f64, detail: f64, smoothness: f64 },
@@ -307,6 +312,11 @@ pub(crate) fn parse(id: &str, p: &Value) -> Option<PixelFx> {
brightness: num(p, "glowBrightness", 15.0).clamp(0.0, 50.0),
colour: crate::raster::color_param(p, vectorcraft_doc::color::Color::rgb(0.3, 0.65, 1.0)).to_rgb(),
},
"artistic.paletteKnife" => PixelFx::PaletteKnife {
stroke_size: num(p, "strokeSize", 10.0).clamp(1.0, 50.0),
stroke_detail: num(p, "strokeDetail", 2.0).clamp(1.0, 3.0),
softness: num(p, "softness", 3.0).clamp(0.0, 10.0),
},
"artistic.posterEdges" => PixelFx::PosterEdges {
thickness: num(p, "edgeThickness", 2.0).clamp(0.0, 10.0),
intensity: num(p, "edgeIntensity", 1.0).clamp(0.0, 10.0),
@@ -523,6 +533,7 @@ impl PixelFx {
PixelFx::SmartBlur { radius, .. } => radius,
PixelFx::AccentedEdges { .. } => 0.0,
// Strokes reach out of the object as far as they reach into it.
PixelFx::PaletteKnife { stroke_size, softness, .. } => artistic::palette_knife_reach(stroke_size, softness),
PixelFx::AngledStrokes { length, .. } | PixelFx::GraphicPen { length, .. } | PixelFx::InkOutlines { length, .. } => {
brushstrokes::stroke_reach(length)
}
@@ -581,6 +592,7 @@ impl PixelFx {
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::PaletteKnife { stroke_size, softness, .. } => Some(artistic::palette_knife_reach(stroke_size, softness)),
PixelFx::AngledStrokes { length, .. } | PixelFx::GraphicPen { length, .. } => {
Some(brushstrokes::stroke_reach(length).max(brushstrokes::TONE_REACH))
}
@@ -651,6 +663,9 @@ impl PixelFx {
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::PaletteKnife { stroke_size, stroke_detail, softness } => {
artistic::palette_knife(px, w, h, space, stroke_size, stroke_detail, softness)
}
PixelFx::PosterEdges { thickness, intensity, posterization } => {
artistic::poster_edges(px, w, h, space, thickness, intensity, posterization)
}
+27
View File
@@ -2647,3 +2647,30 @@ fn artistic_filters_look_the_same_at_any_resolution() {
assert!(diff < 12.0, "{id}: mean difference {diff}");
}
}
#[test]
fn palette_knife_params_take_defaults_and_stay_in_range() {
assert_eq!(fx("artistic.paletteKnife", json!({})), PixelFx::PaletteKnife { stroke_size: 10.0, stroke_detail: 2.0, softness: 3.0 });
assert_eq!(
fx("artistic.paletteKnife", json!({"strokeSize": 0, "strokeDetail": 0, "softness": 99})),
PixelFx::PaletteKnife { stroke_size: 1.0, stroke_detail: 1.0, softness: 10.0 }
);
assert!(fx("artistic.paletteKnife", json!({})).outset(Rect::new(0.0, 0.0, 40.0, 30.0)) > 0.0);
}
#[test]
fn palette_knife_smears_a_ramp_and_keeps_coverage() {
let (w, h) = (48, 24);
let src = ramp(w, h);
let mut out = src.clone();
fx("artistic.paletteKnife", json!({"strokeSize": 20, "strokeDetail": 1, "softness": 0})).apply(&mut out, w, h, &space(w, h));
assert_ne!(out, src);
assert!(out.as_chunks::<4>().0.iter().zip(src.as_chunks::<4>().0).all(|(o, s)| o[3] == s[3]));
let mut gentle = src.clone();
fx("artistic.paletteKnife", json!({"strokeSize": 20, "strokeDetail": 3, "softness": 0})).apply(&mut gentle, w, h, &space(w, h));
let mean = |buf: &Vec<u8>| {
let n = buf.len() / 4;
buf.as_chunks::<4>().0.iter().map(|p| f64::from(p[0])).sum::<f64>() / n as f64
};
assert!((mean(&gentle) - mean(&src)).abs() < (mean(&out) - mean(&src)).abs());
}
+2 -1
View File
@@ -31,11 +31,12 @@ const BRUSH_STROKES: [(&str, &str); 8] = [
];
/// Effect › Artistic, at their defaults.
const ARTISTIC: [(&str, &str); 9] = [
const ARTISTIC: [(&str, &str); 10] = [
("artistic.cutout", "{}"),
("artistic.dryBrush", "{}"),
("artistic.filmGrain", "{}"),
("artistic.neonGlow", "{}"),
("artistic.paletteKnife", "{}"),
("artistic.plasticWrap", "{}"),
("artistic.posterEdges", "{}"),
("artistic.smudgeStick", "{}"),
+2
View File
@@ -4555,7 +4555,9 @@ lighting Light Светлина
Dry Brush… Суха четка…
Dry Brush Суха четка
Neon Glow… Неоново сияние…
Palette Knife… Нож за палитра…
Neon Glow Неоново сияние
Palette Knife Нож за палитра
Poster Edges… Плакатни ръбове…
Poster Edges Плакатни ръбове
Levels Нива
Can't render this file because it contains an unexpected character in line 2 and column 73.
+2
View File
@@ -5054,7 +5054,9 @@ imageTrace Outline Obrys
Dry Brush… Suchý štětec…
Dry Brush Suchý štětec
Neon Glow… Neonová záře…
Palette Knife… Špachtle…
Neon Glow Neonová záře
Palette Knife Špachtle
Poster Edges… Plakátové okraje…
Poster Edges Plakátové okraje
Levels Úrovně
Can't render this file because it contains an unexpected character in line 2 and column 73.
+2
View File
@@ -5178,10 +5178,12 @@ swatch Bright Violet Kraftig violet
Cutout… Udklip…
Dry Brush… Tør pensel…
Neon Glow… Neonglød…
Palette Knife… Paletkniv…
Poster Edges… Plakatkanter…
Cutout Udklip
Dry Brush Tør pensel
Neon Glow Neonglød
Palette Knife Paletkniv
Poster Edges Plakatkanter
Apply Data Set Anvend datasæt
Cancel Pattern Editing Annullér redigering af mønster
Can't render this file because it contains an unexpected character in line 2 and column 73.
+2
View File
@@ -4642,7 +4642,9 @@ lighting Light Licht
Dry Brush… Trockenpinsel…
Dry Brush Trockenpinsel
Neon Glow… Neonglühen…
Palette Knife… Palettenmesser…
Neon Glow Neonglühen
Palette Knife Palettenmesser
Poster Edges… Posterkanten…
Poster Edges Posterkanten
Levels Stufen
Can't render this file because it contains an unexpected character in line 2 and column 73.
+2
View File
@@ -2264,7 +2264,9 @@ lighting Light Φως
Dry Brush… Ξηρό πινέλο…
Dry Brush Ξηρό πινέλο
Neon Glow… Λάμψη νέον…
Palette Knife… Μαχαίρι παλέτας…
Neon Glow Λάμψη νέον
Palette Knife Μαχαίρι παλέτας
Poster Edges… Άκρες αφίσας…
Poster Edges Άκρες αφίσας
Levels Επίπεδα
Can't render this file because it contains an unexpected character in line 2031 and column 19.
+2
View File
@@ -4642,7 +4642,9 @@ lighting Light Luz
Dry Brush… Pincel seco…
Dry Brush Pincel seco
Neon Glow… Resplandor de neón…
Palette Knife… Espátula…
Neon Glow Resplandor de neón
Palette Knife Espátula
Poster Edges… Bordes de póster…
Poster Edges Bordes de póster
Levels Niveles
Can't render this file because it contains an unexpected character in line 2 and column 73.
+2
View File
@@ -4949,7 +4949,9 @@ imageTrace Outline Ääriviiva
Dry Brush… Kuiva sivellin…
Dry Brush Kuiva sivellin
Neon Glow… Neonhehku…
Palette Knife… Palettiveitsi…
Neon Glow Neonhehku
Palette Knife Palettiveitsi
Poster Edges… Julisteen reunat…
Poster Edges Julisteen reunat
Levels Tasot
Can't render this file because it contains an unexpected character in line 2 and column 73.
+2
View File
@@ -4652,7 +4652,9 @@ lighting Light Lumière
Dry Brush… Pinceau sec…
Dry Brush Pinceau sec
Neon Glow… Néon…
Palette Knife… Couteau à palette…
Neon Glow Néon
Palette Knife Couteau à palette
Poster Edges… Bords d’affiche…
Poster Edges Bords d’affiche
Levels Niveaux
Can't render this file because it contains an unexpected character in line 2 and column 73.
+2
View File
@@ -1585,7 +1585,9 @@ file-filter Templates Sablonok
Dry Brush… Száraz ecset…
Dry Brush Száraz ecset
Neon Glow… Neonfény…
Palette Knife… Palettakés…
Neon Glow Neonfény
Palette Knife Palettakés
Poster Edges… Poszterszélek…
Poster Edges Poszterszélek
Levels Szintek
Can't render this file because it contains an unexpected character in line 2 and column 73.
+2
View File
@@ -5178,10 +5178,12 @@ swatch Bright Violet Violet cerah
Cutout… Potongan…
Dry Brush… Kuas kering…
Neon Glow… Pendar neon…
Palette Knife… Pisau palet…
Poster Edges… Tepi poster…
Cutout Potongan
Dry Brush Kuas kering
Neon Glow Pendar neon
Palette Knife Pisau palet
Poster Edges Tepi poster
Apply Data Set Terapkan set data
Cancel Pattern Editing Batalkan pengeditan pola
Can't render this file because it contains an unexpected character in line 3801 and column 19.
+2
View File
@@ -4684,7 +4684,9 @@ lighting Light Luce
Dry Brush… Pennello secco…
Dry Brush Pennello secco
Neon Glow… Bagliore neon…
Palette Knife… Spatola…
Neon Glow Bagliore neon
Palette Knife Spatola
Poster Edges… Bordi poster…
Poster Edges Bordi poster
Levels Livelli
Can't render this file because it contains an unexpected character in line 2 and column 73.
+2
View File
@@ -4723,7 +4723,9 @@ lighting Light ライト
Dry Brush… ドライブラシ…
Dry Brush ドライブラシ
Neon Glow… ネオングロー…
Palette Knife… パレットナイフ…
Neon Glow ネオングロー
Palette Knife パレットナイフ
Poster Edges… ポスターエッジ…
Poster Edges ポスターエッジ
Levels レベル数
Can't render this file because it contains an unexpected character in line 2 and column 73.
+2
View File
@@ -5077,7 +5077,9 @@ imageTrace Outline კონტური
Move Slice ნაჭრის გადაადგილება
Move Type on a Path ტრაექტორიაზე ტექსტის გადაადგილება
Neon Glow ნეონის ნათება
Palette Knife პალეტის დანა
Neon Glow… ნეონის ნათება…
Palette Knife… პალეტის დანა…
New Artboard Options… ახალი სამუშაო დაფის პარამეტრები…
New Guide ახალი დამხმარე ხაზი
Next Artboard შემდეგი სამუშაო დაფა
Can't render this file because it contains an unexpected character in line 2 and column 73.
+2
View File
@@ -4647,7 +4647,9 @@ lighting Light 광원
Dry Brush… 드라이 브러시…
Dry Brush 드라이 브러시
Neon Glow… 네온 글로…
Palette Knife… 팔레트 나이프…
Neon Glow 네온 글로
Palette Knife 팔레트 나이프
Poster Edges… 포스터 가장자리…
Poster Edges 포스터 가장자리
Levels 레벨 수
Can't render this file because it contains an unexpected character in line 2 and column 73.
+2
View File
@@ -5189,10 +5189,12 @@ swatch Bright Violet Felviolet
Cutout… Knipsel…
Dry Brush… Droog penseel…
Neon Glow… Neongloed…
Palette Knife… Paletmes…
Poster Edges… Posterranden…
Cutout Knipsel
Dry Brush Droog penseel
Neon Glow Neongloed
Palette Knife Paletmes
Poster Edges Posterranden
Apply Data Set Gegevensset toepassen
Cancel Pattern Editing Patroon bewerken annuleren
Can't render this file because it contains an unexpected character in line 2 and column 73.
+2
View File
@@ -4887,7 +4887,9 @@ imageTrace Outline Kontur
Dry Brush… Suchy pędzel…
Dry Brush Suchy pędzel
Neon Glow… Neonowa poświata…
Palette Knife… Szpachel…
Neon Glow Neonowa poświata
Palette Knife Szpachel
Poster Edges… Krawędzie plakatu…
Poster Edges Krawędzie plakatu
Levels Poziomy
Can't render this file because it contains an unexpected character in line 2 and column 73.
+2
View File
@@ -4044,7 +4044,9 @@ axis Both Ambos
Dry Brush… Pincel seco…
Dry Brush Pincel seco
Neon Glow… Brilho neon…
Palette Knife… Espátula…
Neon Glow Brilho neon
Palette Knife Espátula
Poster Edges… Bordas de pôster…
Poster Edges Bordas de pôster
Levels Níveis
Can't render this file because it contains an unexpected character in line 6 and column 47.
+2
View File
@@ -4680,7 +4680,9 @@ lighting Light Свет
Dry Brush… Сухая кисть…
Dry Brush Сухая кисть
Neon Glow… Неоновое свечение…
Palette Knife… Нож для палитры…
Neon Glow Неоновое свечение
Palette Knife Нож для палитры
Poster Edges… Края плаката…
Poster Edges Края плаката
Levels Уровни
Can't render this file because it contains an unexpected character in line 2 and column 73.
+2
View File
@@ -4561,7 +4561,9 @@ lighting Light Ljus
Dry Brush… Torr pensel…
Dry Brush Torr pensel
Neon Glow… Neonsken…
Palette Knife… Palettkniv…
Neon Glow Neonsken
Palette Knife Palettkniv
Poster Edges… Affischkanter…
Poster Edges Affischkanter
Levels Nivåer
Can't render this file because it contains an unexpected character in line 5 and column 25.
+2
View File
@@ -4664,7 +4664,9 @@ lighting Light Світло
Dry Brush… Суха кисть…
Dry Brush Суха кисть
Neon Glow… Неонове сяйво…
Palette Knife… Ніж палітри…
Neon Glow Неонове сяйво
Palette Knife Ніж палітри
Poster Edges… Плакатні краї…
Poster Edges Плакатні краї
Levels Рівні
Can't render this file because it contains an unexpected character in line 726 and column 40.
+2
View File
@@ -1273,7 +1273,9 @@
Dry Brush… Cọ khô…
Dry Brush Cọ khô
Neon Glow… Ánh neon…
Palette Knife… Dao palette…
Neon Glow Ánh neon
Palette Knife Dao palette
Poster Edges… Viền áp phích…
Poster Edges Viền áp phích
Levels Số mức
1 # Vietnamese (vi) catalog. Same format as zh-hant.tsv: context <TAB> source <TAB> translation
1273
1274
1275
1276
1277
1278
1279
1280
1281
+2
View File
@@ -4948,7 +4948,9 @@ lighting Light 光源
Cutout 剪纸
Dry Brush 干画笔
Neon Glow… 霓虹灯光…
Palette Knife… 调色刀…
Neon Glow 霓虹灯光
Palette Knife 调色刀
Poster Edges… 海报边缘…
Poster Edges 海报边缘
Levels 色阶数
Can't render this file because it contains an unexpected character in line 2 and column 73.
+2
View File
@@ -4754,7 +4754,9 @@ lighting Light 光源
Cutout 剪紙
Dry Brush 乾畫筆
Neon Glow… 霓虹光…
Palette Knife… 調色刀…
Neon Glow 霓虹光
Palette Knife 調色刀
Poster Edges… 海報邊緣…
Poster Edges 海報邊緣
Levels 色階數
Can't render this file because it contains an unexpected character in line 6 and column 47.
+12 -1
View File
@@ -88,7 +88,18 @@ fn effect_menu_lists_the_artistic_filters_before_blur() {
let labels: Vec<&str> = artistic.iter().map(|e| e.label.as_str()).collect();
assert_eq!(
labels,
["Cutout…", "Dry Brush…", "Film Grain…", "Neon Glow…", "Plastic Wrap…", "Poster Edges…", "Smudge Stick…", "Sponge…", "Watercolor…"]
[
"Cutout…",
"Dry Brush…",
"Film Grain…",
"Neon Glow…",
"Palette Knife…",
"Plastic Wrap…",
"Poster Edges…",
"Smudge Stick…",
"Sponge…",
"Watercolor…"
]
);
assert!(artistic.iter().all(|e| e.command.as_deref() == Some("effect.dialog")));
assert_eq!(artistic[1].params, json!({"effect": "artistic.dryBrush"}));