G2: Effect › Artistic › Fresco raster filter

Squashed from #3096 onto main.
This commit is contained in:
bflatastic
2026-10-22 01:15:09 +07:00
parent 5925e6339a
commit eb9e064254
29 changed files with 154 additions and 6 deletions
+7
View File
@@ -379,6 +379,13 @@ pub fn effect_catalog() -> Vec<EffectInfo> {
"{grain: 0..20 (4; monochrome noise in shadows and midtones), highlightArea: 0..20 (0; widens the bright highlight area), intensity: 0..10 (10; highlight strength and quieter highlight grain)}. Smooth document-space grain follows translations; minification uses approximate footprint filtering. Coverage and pure white are preserved; black takes light speckles, and saturated colour channels may clip. Grain 0 with either highlightArea 0 or intensity 0 leaves the art unchanged. Original visual approximation; chosen VectorCraft defaults.",
json!({"grain": 4.0, "highlightArea": 0.0, "intensity": 10.0}),
),
r(
"artistic.fresco",
"Fresco…",
ARTISTIC,
"{brushSize: 1..10 (2; wider brush dabs), brushDetail: 0..10 (10; sharper edges and less smoothing), texture: 1..3 (1; deeper plaster relief)} repaints the object as coarse pigment on a textured wall, keeping its coverage. Original visual approximation; chosen VectorCraft defaults.",
json!({"brushSize": 2.0, "brushDetail": 10.0, "texture": 1.0}),
),
r(
"artistic.neonGlow",
"Neon Glow…",
+56 -2
View File
@@ -1,4 +1,4 @@
//! Effect › Artistic (Colored Pencil, Cutout, Dry Brush, Film Grain, Neon Glow, Paint Daubs, Palette Knife, Plastic Wrap, Poster
//! Effect › 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 and Watercolor). Original filters built from
//! public behaviour documentation:
//! <https://helpx.adobe.com/illustrator/using/summary-effects.html> and
@@ -10,7 +10,7 @@ use std::f64::consts::TAU;
use super::brushstrokes::{LEFT, RIGHT, Smear, TONE_SOFTNESS, mix, streak, stroke_reach, unit, unpremultiply};
use super::edges::sobel;
use super::pixelate::{Around, Place, Soft, cell, point_in, rand, value_noise};
use super::texture::{self, Light, Texture, Tones, luma, premultiply, straight, texturizer};
use super::texture::{self, Light, Texture, Tones, luma, premultiply, smooth_noise, straight, texturizer};
use super::{PixelSpace, blur_plane, pick};
use vectorcraft_geom::{Point, Vec2};
@@ -72,6 +72,7 @@ pub(super) fn paint_daub_radius(size: f64) -> f64 {
size.clamp(1.0, 50.0) * 0.48
}
const SALT_PENCIL: u64 = 304;
const SALT_FRESCO: u64 = 320;
/// Keep the last cell's four corner values: magnified adjacent pixels often share a cell.
/// The cache changes only cost, never the field, and needs constant storage per raster.
@@ -426,6 +427,59 @@ fn rough_bristle(p: Point, k: u64) -> f32 {
rand(SALT_DAUB + 3, i, j, k) as f32
}
/// Fresco: coarse brush dabs on a plaster wall. `brush_size` (1..10) sets dab spacing,
/// `brush_detail` (0..10) keeps sharper edges when high, and `texture` (1..3) deepens the
/// relief. Coverage is kept.
pub(super) fn fresco(px: &mut [[u8; 4]], w: usize, h: usize, space: &PixelSpace, brush_size: f64, brush_detail: f64, texture: f64) {
if !([brush_size, brush_detail, texture].iter().all(|v| v.is_finite()) && space.px.is_finite() && space.px > 0.0) {
return;
}
let Some(place) = Place::new(space, w, h) else { return };
let brush_size = brush_size.clamp(1.0, 10.0);
let brush_detail = brush_detail.clamp(0.0, 10.0);
let texture = texture.round().clamp(1.0, 3.0);
let side = 2.0 + 1.8 * brush_size;
let sigma = (0.15 * (10.0 - brush_detail) * brush_size / place.px).max(0.0);
let soft = Soft::new(px, w, sigma);
let tex_strength = 0.08 + 0.07 * (texture - 1.0);
let tex_scale = 3.0 / texture;
let streak_mix = 0.25 * (1.0 - brush_detail / 10.0);
place.each(px, |x, y, p| {
if p[3] == 0 {
return;
}
let q = place.at(x, y);
let (Some(i), Some(j)) = (cell(q.x / side), cell(q.y / side)) else { return };
let jx = (rand(SALT_FRESCO, i, j, 0) - 0.5) * 0.35 * side;
let jy = (rand(SALT_FRESCO, i, j, 1) - 0.5) * 0.35 * side;
let sample = Point::new(q.x + jx, q.y + jy);
let mut sampled = soft.at(place.index(sample));
if sampled[3] <= 0.0 {
sampled = soft.at(place.index(q));
}
if sampled[3] <= 0.0 {
return;
}
let [r, g, b, alpha] = sampled;
let c = [r, g, b].map(|v| v / alpha);
let angle = TAU * rand(SALT_FRESCO, i, j, 2);
let (fx, fy) = (q.x - i as f64 * side, q.y - j as f64 * side);
let along = fx * angle.cos() + fy * angle.sin();
let streak = (1.0 - streak_mix * (0.5 + 0.5 * (along / side).sin().powi(2))) as f32;
let bump = (1.0 + tex_strength * smooth_noise(SALT_FRESCO + 3, q, tex_scale * side)) as f32;
let lit = c.map(|v| (v * streak * bump).clamp(0.0, 1.0));
*p = premultiply(lit, f32::from(p[3]) / 255.0);
});
}
/// How far (document units) a Fresco dab can reach from a pixel.
pub(super) fn fresco_reach(brush_size: f64, brush_detail: f64) -> f64 {
let brush_size = brush_size.clamp(1.0, 10.0);
let brush_detail = brush_detail.clamp(0.0, 10.0);
let side = 2.0 + 1.8 * brush_size;
side + 0.15 * (10.0 - brush_detail) * brush_size
}
/// How far (document units) Plastic Wrap's softened relief is sampled at `smoothness` (1..15).
pub(super) fn plastic_reach(smoothness: f64) -> f64 {
3.0 * (0.15 + 0.35 * smoothness.clamp(1.0, 15.0))
+14 -2
View File
@@ -1,4 +1,4 @@
//! Photoshop-style raster effects (Effect › Artistic › Colored Pencil, Cutout, Dry Brush, Film Grain, Neon Glow, Paint Daubs, Plastic Wrap and
//! Photoshop-style raster effects (Effect › Artistic › Colored Pencil, Cutout, Dry Brush, Film Grain, Fresco, Neon Glow, Paint Daubs, Plastic Wrap and
//! Poster Edges, Rough Pastels, 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,
@@ -44,11 +44,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; 54] = [
pub const PIXEL_EFFECTS: [&str; 55] = [
"artistic.coloredPencil",
"artistic.cutout",
"artistic.dryBrush",
"artistic.filmGrain",
"artistic.fresco",
"artistic.neonGlow",
"artistic.paintDaubs",
"artistic.paletteKnife",
@@ -118,6 +119,9 @@ 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 › Fresco: coarse brush dabs on plaster; `brush_size` (1..10), `brush_detail`
/// (0..10) and `texture` (1..3).
Fresco { brush_size: f64, brush_detail: f64, texture: 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).
@@ -374,6 +378,11 @@ pub(crate) fn parse(id: &str, p: &Value) -> Option<PixelFx> {
light: Light::parse(text(p, "lightDirection", "top")),
invert: flag(p, "invert", false),
},
"artistic.fresco" => PixelFx::Fresco {
brush_size: num(p, "brushSize", 2.0).clamp(1.0, 10.0),
brush_detail: num(p, "brushDetail", 10.0).clamp(0.0, 10.0),
texture: num(p, "texture", 1.0).round().clamp(1.0, 3.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),
@@ -621,6 +630,7 @@ impl PixelFx {
PixelFx::SmudgeStick { .. } | PixelFx::Sponge { .. } | PixelFx::Watercolor { .. } => 0.0,
PixelFx::FilmGrain { .. }
| PixelFx::ColoredPencil { .. }
| PixelFx::Fresco { .. }
| PixelFx::Cutout { .. }
| PixelFx::DryBrush { .. }
| PixelFx::NeonGlow { .. }
@@ -687,6 +697,7 @@ impl PixelFx {
| PixelFx::HalftonePattern { .. }
| PixelFx::NotePaper { .. }
| PixelFx::Reticulation { .. } => Some(0.0),
PixelFx::Fresco { brush_size, brush_detail, .. } => Some(artistic::fresco_reach(brush_size, brush_detail)),
PixelFx::DryBrush { size, detail, .. } => Some(artistic::dry_brush_reach(size, detail)),
PixelFx::Plaster { smoothness, .. } => Some(sketch::plaster_reach(smoothness)),
PixelFx::PaintDaubs { size, .. } => Some(artistic::paint_daub_radius(size) + size * 0.15),
@@ -725,6 +736,7 @@ impl PixelFx {
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::Fresco { brush_size, brush_detail, texture } => artistic::fresco(px, w, h, space, brush_size, brush_detail, texture),
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),
+26
View File
@@ -2549,6 +2549,11 @@ fn artistic_params_take_defaults_and_stay_in_range() {
),
PixelFx::RoughPastels { length: 0.0, detail: 20.0, texture: Texture::Canvas, scaling: 0.5, relief: 50.0, light: Light::Top, invert: true }
);
assert_eq!(fx("artistic.fresco", json!({})), PixelFx::Fresco { brush_size: 2.0, brush_detail: 10.0, texture: 1.0 });
assert_eq!(
fx("artistic.fresco", json!({"brushSize": 0, "brushDetail": 99, "texture": 4.7})),
PixelFx::Fresco { brush_size: 1.0, brush_detail: 10.0, texture: 3.0 }
);
assert_eq!(fx("artistic.plasticWrap", json!({})), PixelFx::PlasticWrap { highlight: 20.0, detail: 12.0, smoothness: 15.0 });
assert_eq!(
fx("artistic.plasticWrap", json!({"highlightStrength": -1, "detail": 99, "smoothness": 0})),
@@ -2558,6 +2563,7 @@ fn artistic_params_take_defaults_and_stay_in_range() {
"artistic.coloredPencil",
"artistic.cutout",
"artistic.dryBrush",
"artistic.fresco",
"artistic.neonGlow",
"artistic.plasticWrap",
"artistic.posterEdges",
@@ -2612,6 +2618,7 @@ fn artistic_filters_leave_invalid_coordinate_spaces_alone() {
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::Fresco { brush_size: 2.0, brush_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 },
@@ -2664,6 +2671,9 @@ fn artistic_filters_leave_invalid_coordinate_spaces_alone() {
light: Light::Top,
invert: false,
},
PixelFx::Fresco { brush_size: f64::NAN, brush_detail: 10.0, texture: 1.0 },
PixelFx::Fresco { brush_size: 2.0, brush_detail: f64::INFINITY, texture: 1.0 },
PixelFx::Fresco { brush_size: 2.0, brush_detail: 10.0, texture: f64::NEG_INFINITY },
] {
if run(&f, &space(w, h)) != src {
changed.push(format!("{f:?}"));
@@ -2693,6 +2703,22 @@ fn underpainting_softens_more_with_a_larger_brush_and_keeps_coverage() {
assert!(out.as_chunks::<4>().0.iter().zip(src.as_chunks::<4>().0).all(|(o, s)| o[3] == s[3]));
}
#[test]
fn fresco_keeps_coverage_and_responds_to_texture() {
let (w, h) = (48, 32);
let src = ramp(w, h);
let mut low = src.clone();
let mut high = src.clone();
fx("artistic.fresco", json!({"texture": 1})).apply(&mut low, w, h, &space(w, h));
fx("artistic.fresco", json!({"texture": 3})).apply(&mut high, w, h, &space(w, h));
assert_ne!(low, src);
assert_ne!(high, src);
assert_ne!(low, high, "deeper texture changes the relief");
for (a, b) in low.as_chunks::<4>().0.iter().zip(src.as_chunks::<4>().0) {
assert_eq!(a[3], b[3], "alpha is preserved");
}
}
#[test]
fn plastic_wrap_adds_specular_highlights_and_keeps_coverage() {
let (w, h) = (48, 32);
+3 -1
View File
@@ -31,11 +31,12 @@ const BRUSH_STROKES: [(&str, &str); 8] = [
];
/// Effect › Artistic, at their defaults.
const ARTISTIC: [(&str, &str); 14] = [
const ARTISTIC: [(&str, &str); 15] = [
("artistic.coloredPencil", "{}"),
("artistic.cutout", "{}"),
("artistic.dryBrush", "{}"),
("artistic.filmGrain", "{}"),
("artistic.fresco", "{}"),
("artistic.neonGlow", "{}"),
// Wide Blurry softens dabs; Simple leaves uniform fills unchanged on the striped art.
("artistic.paintDaubs", r#"{"brushType": "wideBlurry"}"#),
@@ -214,6 +215,7 @@ fn artistic_settings_survive_a_save_and_open() {
json!({"levels": 3.0, "edgeSimplicity": 2.0, "edgeFidelity": 3.0}),
[json!({"levels": 2.0}), json!({"edgeSimplicity": 10.0})],
),
("artistic.fresco", json!({"brushSize": 4.0, "brushDetail": 6.0, "texture": 2.0}), [json!({"brushSize": 8.0}), json!({"texture": 3.0})]),
(
"artistic.neonGlow",
json!({"glowSize": -6.0, "glowBrightness": 30.0, "color": "#ff3300"}),
+2
View File
@@ -5326,3 +5326,5 @@ theme Appearance Външен вид
Water Paper Мокра хартия
Ruby… Руби…
Ruby Руби
Fresco… Фреска…
Fresco Фреска
Can't render this file because it contains an unexpected character in line 2 and column 73.
+2
View File
@@ -5369,3 +5369,5 @@ mark-style Japanese Japonské
Water Paper Mokrý papír
Ruby… Ruby…
Ruby Ruby
Fresco… Freska…
Fresco Freska
Can't render this file because it contains an unexpected character in line 2 and column 73.
+2
View File
@@ -5342,3 +5342,5 @@ swatch Bright Violet Kraftig violet
Water Paper Vådt papir
Ruby… Ruby…
Ruby Ruby
Fresco… Fresko…
Fresco Fresko
Can't render this file because it contains an unexpected character in line 2 and column 73.
+2
View File
@@ -4950,3 +4950,5 @@ effect Soft Weich
Water Paper Feuchtes Papier
Ruby… Ruby…
Ruby Ruby
Fresco… Fresko…
Fresco Fresko
Can't render this file because it contains an unexpected character in line 2 and column 73.
+2
View File
@@ -5321,3 +5321,5 @@ effect Points Σημεία
Water Paper Βρεγμένο χαρτί
Ruby… Ruby…
Ruby Ruby
Fresco… Νωπογραφία…
Fresco Νωπογραφία
Can't render this file because it contains an unexpected character in line 2039 and column 19.
+2
View File
@@ -5311,3 +5311,5 @@ mark-style Japanese Japonesas
Water Paper Papel mojado
Ruby… Ruby…
Ruby Ruby
Fresco… Fresco…
Fresco Fresco
Can't render this file because it contains an unexpected character in line 2 and column 73.
+2
View File
@@ -5400,3 +5400,5 @@ swatch Bright Violet Kirkas violetti
Water Paper Märkä paperi
Ruby… Ruby…
Ruby Ruby
Fresco… Fresko…
Fresco Fresko
Can't render this file because it contains an unexpected character in line 2 and column 73.
+2
View File
@@ -5368,3 +5368,5 @@ theme Appearance Apparence
Water Paper Papier humide
Ruby… Ruby…
Ruby Ruby
Fresco… Fresque…
Fresco Fresque
Can't render this file because it contains an unexpected character in line 2 and column 73.
+2
View File
@@ -5355,3 +5355,5 @@ imageTrace Outline Körvonal
Water Paper Nedves papír
Ruby… Ruby…
Ruby Ruby
Fresco… Freskó…
Fresco Freskó
Can't render this file because it contains an unexpected character in line 2 and column 73.
+2
View File
@@ -5342,3 +5342,5 @@ swatch Bright Violet Violet cerah
Water Paper Kertas Basah
Ruby… Ruby…
Ruby Ruby
Fresco… Fresko…
Fresco Fresko
Can't render this file because it contains an unexpected character in line 3809 and column 19.
+2
View File
@@ -4984,3 +4984,5 @@ lighting Light Luce
Water Paper Carta bagnata
Ruby… Ruby…
Ruby Ruby
Fresco… Affresco…
Fresco Affresco
Can't render this file because it contains an unexpected character in line 2 and column 73.
+2
View File
@@ -5031,3 +5031,5 @@ lighting Light ライト
Yonroku-ban 四六判
Ruby… ルビ…
Ruby ルビ
Fresco… フレスコ…
Fresco フレスコ
Can't render this file because it contains an unexpected character in line 2 and column 73.
+2
View File
@@ -5279,3 +5279,5 @@ imageTrace Outline კონტური
selects several, or draws one inside an artboard) რამდენიმეს მონიშნავს ან ახალს სამუშაო დაფის შიგნით ხატავს)
Ruby… რუბი…
Ruby რუბი
Fresco… ფრესკა…
Fresco ფრესკა
Can't render this file because it contains an unexpected character in line 2 and column 73.
+2
View File
@@ -4947,3 +4947,5 @@ lighting Light 광원
Water Paper 물 종이
Ruby… 루비…
Ruby 루비
Fresco… 프레스코…
Fresco 프레스코
Can't render this file because it contains an unexpected character in line 2 and column 73.
+2
View File
@@ -5384,3 +5384,5 @@ swatch Bright Violet Felviolet
Water Paper Nat papier
Ruby… Ruby…
Ruby Ruby
Fresco… Fresco…
Fresco Fresco
Can't render this file because it contains an unexpected character in line 2 and column 73.
+2
View File
@@ -5221,3 +5221,5 @@ mark-style Japanese Japońskie
Water Paper Mokry papier
Ruby… Ruby…
Ruby Ruby
Fresco… Fresk…
Fresco Fresk
Can't render this file because it contains an unexpected character in line 2 and column 73.
+2
View File
@@ -5286,3 +5286,5 @@ mark-style Japanese Japonesas
Water Paper Papel molhado
Ruby… Ruby…
Ruby Ruby
Fresco… Afresco…
Fresco Afresco
Can't render this file because it contains an unexpected character in line 6 and column 47.
+2
View File
@@ -4980,3 +4980,5 @@ lighting Light Свет
Water Paper Мокрая бумага
Ruby… Руби…
Ruby Руби
Fresco… Фреска…
Fresco Фреска
Can't render this file because it contains an unexpected character in line 2 and column 73.
+2
View File
@@ -5325,3 +5325,5 @@ theme Appearance Utseende
Water Paper Vått papper
Ruby… Ruby…
Ruby Ruby
Fresco… Fresk…
Fresco Fresk
Can't render this file because it contains an unexpected character in line 5 and column 25.
+2
View File
@@ -4964,3 +4964,5 @@ lighting Light Світло
Water Paper Мокрий папір
Ruby… Рубі…
Ruby Рубі
Fresco… Фреска…
Fresco Фреска
Can't render this file because it contains an unexpected character in line 733 and column 40.
+2
View File
@@ -4964,3 +4964,5 @@ theme Appearance Giao diện
Water Paper Giấy ướt
Ruby… Ruby…
Ruby Ruby
Fresco… Bích họa…
Fresco Bích họa
Can't render this file because it contains an unexpected character in line 4021 and column 10.
+2
View File
@@ -5208,3 +5208,5 @@ lighting Light 光源
Water Paper 水彩画纸
Ruby… 注音…
Ruby 注音
Fresco… 壁画…
Fresco 壁画
Can't render this file because it contains an unexpected character in line 2 and column 73.
+2
View File
@@ -5003,3 +5003,5 @@ lighting Light 光源
Water Paper 水彩畫紙
Ruby… 注音…
Ruby 注音
Fresco… 壁畫…
Fresco 壁畫
Can't render this file because it contains an unexpected character in line 6 and column 47.
+2 -1
View File
@@ -93,6 +93,7 @@ fn effect_menu_lists_the_artistic_filters_before_blur() {
"Cutout…",
"Dry Brush…",
"Film Grain…",
"Fresco…",
"Neon Glow…",
"Paint Daubs…",
"Palette Knife…",
@@ -107,7 +108,7 @@ fn effect_menu_lists_the_artistic_filters_before_blur() {
);
assert!(artistic.iter().all(|e| e.command.as_deref() == Some("effect.dialog")));
assert_eq!(artistic[2].params, json!({"effect": "artistic.dryBrush"}));
assert_eq!(artistic[4].params, json!({"effect": "artistic.neonGlow"}));
assert_eq!(artistic[5].params, json!({"effect": "artistic.neonGlow"}));
let at = |name: &str| entries.iter().position(|e| e.path == ["Effect", name]).unwrap();
assert!(at("Artistic") < at("Blur"));
}