Files
vectorcraft/crates/effects/src/pixel/mod.rs
T

1102 lines
61 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
//! 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,
//! Sharpen › Unsharp Mask, Sketch › Bas Relief, Chalk & Charcoal, Charcoal, Chrome, Conté Crayon, Graphic Pen, Halftone Pattern,
//! Note Paper, Photocopy, Plaster, Reticulation, Stamp, Torn Edges and Water Paper, 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.
//!
//! - Every distance is in document units (points) and becomes pixels through the raster's
//! [`PixelSpace::px`], so an effect looks the same at any zoom and at any Document Raster Effects
//! Settings resolution (as Gaussian Blur's radius does).
//! - The work per pixel is bounded whatever the parameters: blurs are box approximations whose
//! cost doesn't grow with the radius, and sample counts come from the effect's quality.
//! - Beyond the raster's edges is transparency.
mod artistic;
mod blur;
mod brushstrokes;
mod distort;
mod edges;
mod loaded;
mod paint;
mod pixelate;
mod sharpen;
mod sketch;
mod sketch_ink;
mod texture;
mod video;
use serde_json::Value;
use vectorcraft_geom::{Affine, Point, Rect};
use crate::util::{flag, num, text};
pub use artistic::{PAINT_DAUB_BRUSH_TYPES, PaintDaubBrush};
pub use blur::{SMART_BLUR_MODES, SmartBlurMode};
pub use brushstrokes::{STROKE_DIRECTIONS, StrokeDirection};
pub use distort::{GLASS_TEXTURES, GLASS_TEXTURES_LOADED, GlassTexture};
pub use loaded::{LOADED, LoadedTexture, TEXTURE_IMAGE, texture};
pub use pixelate::{MEZZOTINT_TYPES, Mezzotint};
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; 56] = [
"artistic.coloredPencil",
"artistic.cutout",
"artistic.dryBrush",
"artistic.filmGrain",
"artistic.fresco",
"artistic.neonGlow",
"artistic.paintDaubs",
"artistic.paletteKnife",
"artistic.plasticWrap",
"artistic.posterEdges",
"artistic.roughPastels",
"artistic.smudgeStick",
"artistic.sponge",
"artistic.underpainting",
"artistic.watercolor",
"blur.radial",
"blur.smart",
"brushStrokes.accentedEdges",
"brushStrokes.angledStrokes",
"brushStrokes.crosshatch",
"brushStrokes.darkStrokes",
"brushStrokes.inkOutlines",
"brushStrokes.spatter",
"brushStrokes.sprayedStrokes",
"brushStrokes.sumiE",
"distort.diffuseGlow",
"distort.glass",
"distort.oceanRipple",
"pixelate.colorHalftone",
"pixelate.crystallize",
"pixelate.mezzotint",
"pixelate.pointillize",
"sharpen.unsharpMask",
"sketch.basRelief",
"sketch.chalkCharcoal",
"sketch.charcoal",
"sketch.chrome",
"sketch.conteCrayon",
"sketch.graphicPen",
"sketch.halftonePattern",
"sketch.notePaper",
"sketch.photocopy",
"sketch.plaster",
"sketch.reticulation",
"sketch.stamp",
"sketch.tornEdges",
"sketch.waterPaper",
"stylize.glowingEdges",
"texture.craquelure",
"texture.grain",
"texture.mosaicTiles",
"texture.patchwork",
"texture.stainedGlass",
"texture.texturizer",
"video.deinterlace",
"video.ntscColors",
];
/// A Photoshop-style raster effect with its parameters read and clamped to their ranges. Lengths
/// are in document units.
#[derive(Clone, Debug, PartialEq)]
pub enum PixelFx {
/// Artistic › Colored Pencil: coloured strokes `width` (1..24) apart, as heavy as `pressure`
/// (0..15), on paper lifted by `paper` (0..50).
ColoredPencil { width: f64, pressure: f64, paper: f64 },
/// 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 },
/// 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).
SmudgeStick { stroke: f64, highlight: f64, intensity: f64 },
/// Artistic › Sponge: soft blobs of lighter and darker colour about `brush` (0..10) large,
/// contrasting as `definition` (0..25) says, their edges as soft as `smoothness` (1..15).
Sponge { brush: f64, definition: f64, smoothness: f64 },
/// Artistic › Watercolor: detail simplified by a brush of `detail` (1..14), edges saturated,
/// shadows deepened by `shadow` (0..10), `texture` (1..3) grains of paper showing.
Watercolor { detail: f64, shadow: f64, texture: u32 },
/// Artistic › Neon Glow: dimmed greys lit by `colour` (straight RGB, 0..1) glowing from the
/// light areas (`size` above 0) or the dark ones (below 0), spread by |`size`| (-24..24), as
/// strong as `brightness` (0..50).
NeonGlow { size: f64, brightness: f64, colour: [f32; 3] },
/// Artistic › Paint Daubs: dabs `size` (1..50) points apart, as sharp as `sharpness` (0..40),
/// with brush `kind`.
PaintDaubs { size: f64, sharpness: f64, kind: PaintDaubBrush },
/// 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 },
/// Artistic › Poster Edges: colours posterized to `posterization` + 2 levels (0..6) with black
/// lines on the edges, as thick as `thickness` (0..10) and as dark as `intensity` (0..10).
PosterEdges { thickness: f64, intensity: f64, posterization: f64 },
/// Artistic › Underpainting: colours softened as widely as `brush_size` (0..40), then a
/// `texture` surface (`scaling`, `relief`, `light`, `invert`) blended in as strongly as
/// `texture_coverage` (0..40) says.
Underpainting { brush_size: f64, texture_coverage: f64, texture: Texture, scaling: f64, relief: f64, light: Light, invert: bool },
/// Artistic › Rough Pastels: diagonal pastel strokes `length` (0..40) long, bristle detail
/// (1..20), then a [`Texture`] surface scaled by `scaling` (0.5..2), in relief as strong as
/// `relief` (0..50) and lit from `light`; `invert` turns the surface's heights over.
RoughPastels { length: f64, detail: f64, texture: Texture, scaling: f64, relief: f64, light: Light, invert: bool },
/// Blur › Radial Blur around the object's centre: along arcs (spin, an arc of `amount`°) or
/// along rays (zoom, the content scaled by up to about ±`amount`/2 %), averaging 2^`passes` samples.
RadialBlur { amount: f64, zoom: bool, passes: u32 },
/// Blur › Smart Blur (Normal mode): each pixel averaged with the neighbours within `radius`
/// whose colour differs from its own by no more than `threshold` levels, over a grid of
/// `samples` × `samples` neighbours at most.
/// The Edge Only `mode` draws the edges that `threshold` and `radius` find (`blur::smart_edges`)
/// in white on black, and Overlay Edge draws them in white over the Normal result.
SmartBlur { radius: f64, threshold: f64, samples: u32, mode: SmartBlurMode },
/// Brush Strokes › Accented Edges: the edges, about `width` (1..14) wide, accented towards
/// white chalk or black ink as `brightness` (0..50) says; `smoothness` (1..15) softens them.
AccentedEdges { width: f64, brightness: f64, smoothness: f64 },
/// Brush Strokes › Angled Strokes: diagonal strokes `length` long, the light areas' and the
/// dark areas' going opposite ways, split as `balance` (0..100) says, as crisp as `sharpness`
/// (0..10).
AngledStrokes { balance: f64, length: f64, sharpness: f64 },
/// Brush Strokes › Crosshatch: pencil hatching `length` long along both diagonals, as crisp
/// as `sharpness` (0..20), in `strength` (1..3) passes.
Crosshatch { length: f64, sharpness: f64, strength: u32 },
/// Brush Strokes › Dark Strokes: short strokes towards black in the dark areas, long ones
/// towards white in the light areas, split by `balance` (0..10), as dark as `black` (0..10)
/// and as light as `white` (0..10).
DarkStrokes { balance: f64, black: f64, white: f64 },
/// Brush Strokes › Ink Outlines: pen-and-ink outlines and strokes `length` long, the shadows
/// darkened by `dark` (0..50) and the highlights lightened by `light` (0..50).
InkOutlines { length: f64, dark: f64, light: f64 },
/// Brush Strokes › Spatter: a spatter airbrush spraying within `radius` (0..25), its grains as
/// smooth as `smoothness` (1..15).
Spatter { radius: f64, smoothness: f64 },
/// Brush Strokes › Sprayed Strokes: sprayed strokes `length` long along `direction`, scattered
/// as `radius` (0..25) says.
SprayedStrokes { length: f64, radius: f64, direction: StrokeDirection },
/// Brush Strokes › Sumi-e: wet black brush strokes `width` (3..15) across on rice paper, as
/// loaded as `pressure` (0..15) says, with `contrast` (0..40).
SumiE { width: f64, pressure: f64, contrast: f64 },
/// Distort › Diffuse Glow: the highlights glow white as strongly as `glow` (0..20), from the
/// brightness `clear` (0..20) leaves clear, under white grain as dense as `graininess` (0..10).
DiffuseGlow { graininess: f64, glow: f64, clear: f64 },
/// Distort › Glass: the object seen through a `texture` surface (a built-in one, or an image
/// the user loaded) scaled by `scaling` (0.5..2), bent by `distortion` (0..20), smoothed by
/// `smoothness` (1..15), its heights turned over when `invert`.
Glass { distortion: f64, smoothness: f64, texture: GlassTexture, scaling: f64, invert: bool },
/// Distort › Ocean Ripple: the object under ripples of `size` (1..15) shifting it as much as
/// `magnitude` (0..20) says.
OceanRipple { size: f64, magnitude: f64 },
/// Sharpen › Unsharp Mask: colours pushed away from a Gaussian blur of σ = `radius` by `amount`
/// (1 = 100 %), where they differ from it by at least `threshold` levels.
UnsharpMask { amount: f64, radius: f64, threshold: f64 },
/// Sketch › Chalk & Charcoal: chalk highlights in light areas and charcoal in dark ones;
/// `chalk` and `charcoal` (0..20) set how much, `pressure` (0..5) the stroke weight.
ChalkCharcoal { chalk: f64, charcoal: f64, pressure: f64 },
/// Sketch › Chrome: polished chrome relief with `detail` (0..10) and `smoothness` (0..10).
Chrome { detail: f64, smoothness: f64 },
/// Sketch › Conté Crayon: dark areas in a black crayon, light areas in a white one, on mid-grey
/// paper; `foreground` and `background` (0..20) set how far each crayon reaches into the
/// midtones, and the paper is a `texture` surface scaled by `scaling` (0.5..2), in relief as
/// strong as `relief` (0..50), lit from `light`, its heights turned over when `invert`.
ConteCrayon { foreground: f64, background: f64, texture: Texture, scaling: f64, relief: f64, light: Light, invert: bool },
/// Sketch › Graphic Pen: fine linear ink strokes along `dir`, each `length` (1..15) points long
/// on white paper; `balance` (0..100) sets how much of the tone range becomes black ink.
GraphicPen { length: f64, balance: f64, direction: StrokeDirection },
/// Sketch › Halftone Pattern: black ink on white paper through a `kind` screen of cells `size`
/// (1..12) apart, its edges as hard as `contrast` (0..50) says.
HalftonePattern { size: f64, contrast: f64, kind: HalftonePattern },
/// Sketch › Note Paper: embossed tones on fibrous paper (`balance`, `graininess`, `relief`).
NotePaper { balance: f64, graininess: f64, relief: f64 },
/// Sketch › Bas Relief: low relief lit from `light`, keeping the finer features as `detail`
/// (1..15) says and rounded by `smoothness` (1..15).
BasRelief { detail: f64, smoothness: f64, light: Light },
/// Sketch › Charcoal: bold edges `thickness` (1..7) wide, as fine as `detail` (0..5) says, and
/// diagonal strokes over the midtones; `balance` (0..100) sets how far into the lights it reaches.
Charcoal { thickness: f64, detail: f64, balance: f64 },
/// Sketch › Photocopy: black only where a pixel is darker than its surroundings, as fine as
/// `detail` (1..24) says, and as readily as `darkness` (1..50) says.
Photocopy { detail: f64, darkness: f64 },
/// Sketch › Stamp: solid black and white, printing black as much as `balance` (0..50) says,
/// its shapes rounded by `smoothness` (1..50).
Stamp { balance: f64, smoothness: f64 },
/// Sketch › Plaster: relief plaster lit from `light`; dark areas raised, light recessed;
/// `imageBalance` (1..50) shifts more of the image lower at higher values; `smoothness` (1..15)
/// softens the relief.
Plaster { image_balance: f64, smoothness: f64, light: Light },
/// Sketch › Reticulation: black ink on white paper through a high-contrast film-grain screen;
/// `density` (0..50), `foreground` and `background` (0..50) pull tones before the grain.
Reticulation { density: f64, foreground: f64, background: f64 },
/// Sketch › Torn Edges: high-contrast black and white with ragged edges; `image_balance`
/// (0..50) shifts ink versus paper, `smoothness` (0..15) softens tones and fraying, `contrast`
/// (0..20) hardens the split.
TornEdges { image_balance: f64, smoothness: f64, contrast: f64 },
/// Sketch › Water Paper: colours flow on damp fibrous paper; `fiber_length` (3..50) sets the
/// fibers and daubs, `brightness` and `contrast` (0..100) adjust the wash. Ranges and defaults
/// are unverified VectorCraft choices (helpx documents the controls only).
WaterPaper { fiber_length: f64, brightness: f64, contrast: f64 },
/// Stylize › Glowing Edges: bright coloured Sobel outlines on black.
GlowingEdges { width: f64, brightness: f64, smoothness: f64 },
/// Pixelate › Color Halftone: each colour channel screened at its angle (`angles`° of channels
/// 1 to 4) into dots of up to `max_radius`, whose area follows the channel's strength.
ColorHalftone { max_radius: f64, angles: [f64; 4] },
/// Pixelate › Crystallize: polygons of solid colour around random points about `cell` apart.
Crystallize { cell: f64 },
/// Pixelate › Mezzotint: every colour channel fully on or off against a random pattern.
Mezzotint { kind: Mezzotint },
/// Pixelate › Pointillize: random dots about `cell` across of the object's colours on a white
/// canvas.
Pointillize { cell: f64 },
/// Texture › Craquelure: cracked relief plaster, plates about `spacing` apart, cracks as deep
/// as `depth` (0..10), lit as brightly as `brightness` (0..10).
Craquelure { spacing: f64, depth: f64, brightness: f64 },
/// Texture › Grain: noise of `kind` as strong as `intensity` (0..100), the image's contrast
/// set by `contrast` (0..100, 50 leaves it).
Grain { intensity: f64, contrast: f64, kind: Grain },
/// Texture › Mosaic Tiles: irregular tiles about `tile` across with sunken grout `grout` / 2
/// wide (1..15), lightened by `lighten` (0..10).
MosaicTiles { tile: f64, grout: f64, lighten: f64 },
/// Texture › Patchwork: squares `square` across of the colour around their centres, raised by
/// up to `relief` (0..25).
Patchwork { square: f64, relief: f64 },
/// Texture › Stained Glass: panes about `cell` across leaded `border` / 2 wide (1..20) in
/// black, lit at the centre by `light` (0..10).
StainedGlass { cell: f64, border: f64, light: f64 },
/// Texture › Texturizer: a `texture` surface scaled by `scaling` (0.5..2), in relief as strong
/// as `relief` (0..50), lit from `light`, its heights turned over when `invert`.
Texturizer { texture: Texture, scaling: f64, relief: f64, light: Light, invert: bool },
/// Video › De-Interlace: the odd (or `even`) field lines made again from the others, by
/// duplication or, with `interpolate`, by averaging.
DeInterlace { even: bool, interpolate: bool },
/// Video › NTSC Colors: colours a television signal can't carry made less saturated.
NtscColors,
}
/// What a raster's colour channels hold, for the filters that treat them apart (Color Halftone
/// screens each channel, Mezzotint thresholds each).
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub enum Channels {
/// Red, green and blue light.
#[default]
Rgb,
/// Screen colours of a CMYK document: treated as its cyan, magenta, yellow and black inks.
Cmyk,
/// The complemented C, M and Y inks (a CMYK document drawn ink plane by ink plane).
CmyPlane,
/// The complemented K ink, as a grey.
KPlane,
}
/// Where a raster's pixels lie in the document.
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct PixelSpace {
/// Pixel coordinates (x right, y down, (0, 0) the top-left corner of the first pixel) →
/// document coordinates.
pub to_doc: Affine,
/// The size of a pixel in document units.
pub px: f64,
/// The object's centre (document coordinates): where Radial Blur blurs around, and where the
/// Pixelate filters' patterns are anchored (so they move with the object).
pub center: Point,
/// What the colour channels hold.
pub channels: Channels,
/// The height of a line of the document's raster grid (document units: 72 / Document Raster
/// Effects Settings › Resolution), the fields De-Interlace works on.
pub line: f64,
/// How far the corners of the object's bounds lie from its centre (document units): Stained
/// Glass's light fades out over it.
pub extent: f64,
}
/// Effect `id`'s parameters `p` (defaults merged in) as a [`PixelFx`]; `None` for other effects.
pub(crate) fn parse(id: &str, p: &Value) -> Option<PixelFx> {
let quality = |levels: [(&str, u32); 3], default: u32| {
let q = text(p, "quality", "");
levels.iter().find(|(name, _)| name.eq_ignore_ascii_case(q)).map_or(default, |(_, v)| *v)
};
Some(match id {
"artistic.coloredPencil" => PixelFx::ColoredPencil {
width: num(p, "pencilWidth", 5.0).clamp(1.0, 24.0),
pressure: num(p, "strokePressure", 5.0).clamp(0.0, 15.0),
paper: num(p, "paperBrightness", 25.0).clamp(0.0, 50.0),
},
"artistic.cutout" => PixelFx::Cutout {
levels: num(p, "levels", 4.0).round().clamp(2.0, 8.0),
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),
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),
posterization: num(p, "posterization", 2.0).round().clamp(0.0, 6.0),
},
"artistic.filmGrain" => PixelFx::FilmGrain {
grain: num(p, "grain", 4.0).clamp(0.0, 20.0),
highlight: num(p, "highlightArea", 0.0).clamp(0.0, 20.0),
intensity: num(p, "intensity", 10.0).clamp(0.0, 10.0),
},
"artistic.underpainting" => PixelFx::Underpainting {
brush_size: num(p, "brushSize", 6.0).clamp(0.0, 40.0),
texture_coverage: num(p, "textureCoverage", 16.0).clamp(0.0, 40.0),
texture: Texture::parse(text(p, "texture", "canvas")),
scaling: num(p, "scaling", 100.0).clamp(50.0, 200.0) / 100.0,
relief: num(p, "relief", 4.0).clamp(0.0, 50.0),
light: Light::parse(text(p, "lightDirection", "top")),
invert: flag(p, "invert", false),
},
"artistic.roughPastels" => PixelFx::RoughPastels {
length: num(p, "strokeLength", 6.0).clamp(0.0, 40.0),
detail: num(p, "strokeDetail", 10.0).clamp(1.0, 20.0),
texture: Texture::parse(text(p, "texture", "canvas")),
scaling: num(p, "scaling", 100.0).clamp(50.0, 200.0) / 100.0,
relief: num(p, "relief", 4.0).clamp(0.0, 50.0),
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),
intensity: num(p, "intensity", 10.0).clamp(0.0, 10.0),
},
"artistic.sponge" => PixelFx::Sponge {
brush: num(p, "brushSize", 2.0).clamp(0.0, 10.0),
definition: num(p, "definition", 12.0).clamp(0.0, 25.0),
smoothness: num(p, "smoothness", 5.0).clamp(1.0, 15.0),
},
"artistic.watercolor" => PixelFx::Watercolor {
detail: num(p, "brushDetail", 9.0).clamp(1.0, 14.0),
shadow: num(p, "shadowIntensity", 1.0).clamp(0.0, 10.0),
texture: num(p, "texture", 1.0).clamp(1.0, 3.0).round() as u32,
},
"artistic.paintDaubs" => PixelFx::PaintDaubs {
size: num(p, "brushSize", 10.0).clamp(1.0, 50.0),
sharpness: num(p, "sharpness", 25.0).clamp(0.0, 40.0),
kind: PaintDaubBrush::parse(text(p, "brushType", "simple")),
},
"artistic.plasticWrap" => PixelFx::PlasticWrap {
highlight: num(p, "highlightStrength", 20.0).clamp(0.0, 20.0),
detail: num(p, "detail", 12.0).clamp(0.0, 15.0),
smoothness: num(p, "smoothness", 15.0).clamp(1.0, 15.0),
},
"blur.radial" => PixelFx::RadialBlur {
amount: num(p, "amount", 10.0).clamp(1.0, 100.0),
zoom: text(p, "method", "spin").eq_ignore_ascii_case("zoom"),
passes: quality([("draft", 4), ("good", 6), ("best", 8)], 6),
},
"blur.smart" => PixelFx::SmartBlur {
radius: num(p, "radius", 3.0).clamp(0.1, 100.0),
threshold: num(p, "threshold", 25.0).clamp(0.1, 100.0),
samples: quality([("low", 5), ("medium", 7), ("high", 9)], 7),
mode: SmartBlurMode::parse(text(p, "blurMode", "normal")),
},
"brushStrokes.accentedEdges" => PixelFx::AccentedEdges {
width: num(p, "edgeWidth", 2.0).clamp(1.0, 14.0),
brightness: num(p, "edgeBrightness", 38.0).clamp(0.0, 50.0),
smoothness: num(p, "smoothness", 5.0).clamp(1.0, 15.0),
},
"brushStrokes.angledStrokes" => PixelFx::AngledStrokes {
balance: num(p, "directionBalance", 50.0).clamp(0.0, 100.0),
length: num(p, "strokeLength", 15.0).clamp(3.0, 50.0),
sharpness: num(p, "sharpness", 3.0).clamp(0.0, 10.0),
},
"brushStrokes.crosshatch" => PixelFx::Crosshatch {
length: num(p, "strokeLength", 9.0).clamp(3.0, 50.0),
sharpness: num(p, "sharpness", 6.0).clamp(0.0, 20.0),
strength: num(p, "strength", 1.0).clamp(1.0, 3.0).round() as u32,
},
"brushStrokes.darkStrokes" => PixelFx::DarkStrokes {
balance: num(p, "balance", 5.0).clamp(0.0, 10.0),
black: num(p, "blackIntensity", 6.0).clamp(0.0, 10.0),
white: num(p, "whiteIntensity", 2.0).clamp(0.0, 10.0),
},
"brushStrokes.inkOutlines" => PixelFx::InkOutlines {
length: num(p, "strokeLength", 4.0).clamp(1.0, 50.0),
dark: num(p, "darkIntensity", 20.0).clamp(0.0, 50.0),
light: num(p, "lightIntensity", 10.0).clamp(0.0, 50.0),
},
"brushStrokes.spatter" => {
PixelFx::Spatter { radius: num(p, "sprayRadius", 10.0).clamp(0.0, 25.0), smoothness: num(p, "smoothness", 5.0).clamp(1.0, 15.0) }
}
"brushStrokes.sprayedStrokes" => PixelFx::SprayedStrokes {
length: num(p, "strokeLength", 12.0).clamp(0.0, 20.0),
radius: num(p, "sprayRadius", 7.0).clamp(0.0, 25.0),
direction: StrokeDirection::parse(text(p, "strokeDirection", "rightDiagonal")),
},
"brushStrokes.sumiE" => PixelFx::SumiE {
width: num(p, "strokeWidth", 10.0).clamp(3.0, 15.0),
pressure: num(p, "strokePressure", 2.0).clamp(0.0, 15.0),
contrast: num(p, "contrast", 16.0).clamp(0.0, 40.0),
},
"distort.diffuseGlow" => PixelFx::DiffuseGlow {
graininess: num(p, "graininess", 6.0).clamp(0.0, 10.0),
glow: num(p, "glowAmount", 10.0).clamp(0.0, 20.0),
clear: num(p, "clearAmount", 15.0).clamp(0.0, 20.0),
},
"distort.glass" => PixelFx::Glass {
distortion: num(p, "distortion", 5.0).clamp(0.0, 20.0),
smoothness: num(p, "smoothness", 3.0).clamp(1.0, 15.0),
texture: GlassTexture::parse_with(text(p, "texture", "frosted"), p.get(TEXTURE_IMAGE).and_then(Value::as_str)),
scaling: num(p, "scaling", 100.0).clamp(50.0, 200.0) / 100.0,
invert: flag(p, "invert", false),
},
"distort.oceanRipple" => {
PixelFx::OceanRipple { size: num(p, "rippleSize", 9.0).clamp(1.0, 15.0), magnitude: num(p, "rippleMagnitude", 9.0).clamp(0.0, 20.0) }
}
"sharpen.unsharpMask" => PixelFx::UnsharpMask {
amount: num(p, "amount", 50.0).clamp(1.0, 500.0) / 100.0,
radius: num(p, "radius", 1.0).clamp(0.1, 250.0),
threshold: num(p, "threshold", 0.0).clamp(0.0, 255.0),
},
"sketch.chalkCharcoal" => PixelFx::ChalkCharcoal {
chalk: num(p, "chalkArea", 6.0).clamp(0.0, 20.0),
charcoal: num(p, "charcoalArea", 6.0).clamp(0.0, 20.0),
pressure: num(p, "strokePressure", 2.0).clamp(0.0, 5.0),
},
"sketch.chrome" => PixelFx::Chrome { detail: num(p, "detail", 4.0).clamp(0.0, 10.0), smoothness: num(p, "smoothness", 4.0).clamp(0.0, 10.0) },
"sketch.conteCrayon" => PixelFx::ConteCrayon {
foreground: num(p, "foregroundLevel", 12.0).clamp(0.0, 20.0),
background: num(p, "backgroundLevel", 10.0).clamp(0.0, 20.0),
texture: Texture::parse(text(p, "texture", "canvas")),
scaling: num(p, "scaling", 100.0).clamp(50.0, 200.0) / 100.0,
relief: num(p, "relief", 4.0).clamp(0.0, 50.0),
light: Light::parse(text(p, "lightDirection", "top")),
invert: flag(p, "invert", false),
},
"sketch.graphicPen" => PixelFx::GraphicPen {
length: num(p, "strokeLength", 5.0).clamp(1.0, 15.0),
balance: num(p, "lightDarkBalance", 50.0).clamp(0.0, 100.0),
direction: StrokeDirection::parse(text(p, "strokeDirection", "rightDiagonal")),
},
"sketch.halftonePattern" => PixelFx::HalftonePattern {
size: num(p, "size", 1.0).clamp(1.0, 12.0),
contrast: num(p, "contrast", 5.0).clamp(0.0, 50.0),
kind: HalftonePattern::parse(text(p, "patternType", "dot")),
},
"sketch.notePaper" => PixelFx::NotePaper {
balance: num(p, "imageBalance", 25.0).clamp(0.0, 50.0),
graininess: num(p, "graininess", 0.0).clamp(0.0, 10.0),
relief: num(p, "relief", 4.0).clamp(0.0, 50.0),
},
"sketch.basRelief" => PixelFx::BasRelief {
detail: num(p, "detail", 13.0).clamp(1.0, 15.0),
smoothness: num(p, "smoothness", 3.0).clamp(1.0, 15.0),
light: Light::parse(text(p, "lightDirection", "bottom")),
},
"sketch.charcoal" => PixelFx::Charcoal {
thickness: num(p, "thickness", 1.0).clamp(1.0, 7.0),
detail: num(p, "detail", 1.0).clamp(0.0, 5.0),
balance: num(p, "lightDarkBalance", 50.0).clamp(0.0, 100.0),
},
"sketch.photocopy" => {
PixelFx::Photocopy { detail: num(p, "detail", 7.0).clamp(1.0, 24.0), darkness: num(p, "darkness", 25.0).clamp(1.0, 50.0) }
}
"sketch.stamp" => {
PixelFx::Stamp { balance: num(p, "lightDarkBalance", 25.0).clamp(0.0, 50.0), smoothness: num(p, "smoothness", 5.0).clamp(1.0, 50.0) }
}
"sketch.plaster" => PixelFx::Plaster {
image_balance: num(p, "imageBalance", 20.0).clamp(1.0, 50.0),
smoothness: num(p, "smoothness", 8.0).clamp(1.0, 15.0),
light: Light::parse(text(p, "lightDirection", "top")),
},
"sketch.reticulation" => PixelFx::Reticulation {
density: num(p, "density", 12.0).clamp(0.0, 50.0),
foreground: num(p, "foregroundLevel", 6.0).clamp(0.0, 50.0),
background: num(p, "backgroundLevel", 6.0).clamp(0.0, 50.0),
},
"sketch.tornEdges" => PixelFx::TornEdges {
image_balance: num(p, "imageBalance", 25.0).clamp(0.0, 50.0),
smoothness: num(p, "smoothness", 11.0).clamp(0.0, 15.0),
contrast: num(p, "contrast", 17.0).clamp(0.0, 20.0),
},
"sketch.waterPaper" => PixelFx::WaterPaper {
fiber_length: num(p, "fiberLength", 15.0).clamp(3.0, 50.0),
brightness: num(p, "brightness", 50.0).clamp(0.0, 100.0),
contrast: num(p, "contrast", 50.0).clamp(0.0, 100.0),
},
"stylize.glowingEdges" => PixelFx::GlowingEdges {
width: num(p, "edgeWidth", 2.0).clamp(1.0, 14.0),
brightness: num(p, "edgeBrightness", 6.0).clamp(0.0, 20.0),
smoothness: num(p, "smoothness", 5.0).clamp(1.0, 15.0),
},
"pixelate.colorHalftone" => PixelFx::ColorHalftone {
max_radius: num(p, "maxRadius", 8.0).clamp(4.0, 127.0),
angles: [("channel1", 108.0), ("channel2", 162.0), ("channel3", 90.0), ("channel4", 45.0)]
.map(|(k, d)| num(p, k, d).clamp(-360.0, 360.0)),
},
"pixelate.crystallize" => PixelFx::Crystallize { cell: num(p, "cellSize", 10.0).clamp(3.0, 300.0) },
"pixelate.mezzotint" => PixelFx::Mezzotint { kind: Mezzotint::parse(text(p, "type", "fineDots")) },
"pixelate.pointillize" => PixelFx::Pointillize { cell: num(p, "cellSize", 5.0).clamp(3.0, 300.0) },
"texture.craquelure" => PixelFx::Craquelure {
spacing: num(p, "crackSpacing", 15.0).clamp(2.0, 100.0),
depth: num(p, "crackDepth", 6.0).clamp(0.0, 10.0),
brightness: num(p, "crackBrightness", 9.0).clamp(0.0, 10.0),
},
"texture.grain" => PixelFx::Grain {
intensity: num(p, "intensity", 40.0).clamp(0.0, 100.0),
contrast: num(p, "contrast", 50.0).clamp(0.0, 100.0),
kind: Grain::parse(text(p, "grainType", "regular")),
},
"texture.mosaicTiles" => PixelFx::MosaicTiles {
tile: num(p, "tileSize", 12.0).clamp(2.0, 100.0),
grout: num(p, "groutWidth", 3.0).clamp(1.0, 15.0),
lighten: num(p, "lightenGrout", 9.0).clamp(0.0, 10.0),
},
"texture.patchwork" => {
PixelFx::Patchwork { square: num(p, "squareSize", 4.0).clamp(0.0, 10.0), relief: num(p, "relief", 8.0).clamp(0.0, 25.0) }
}
"texture.stainedGlass" => PixelFx::StainedGlass {
cell: num(p, "cellSize", 10.0).clamp(2.0, 50.0),
border: num(p, "borderThickness", 4.0).clamp(1.0, 20.0),
light: num(p, "lightIntensity", 3.0).clamp(0.0, 10.0),
},
"texture.texturizer" => PixelFx::Texturizer {
texture: Texture::parse_with(text(p, "texture", "canvas"), p.get(TEXTURE_IMAGE).and_then(Value::as_str)),
scaling: num(p, "scaling", 100.0).clamp(50.0, 200.0) / 100.0,
relief: num(p, "relief", 4.0).clamp(0.0, 50.0),
light: Light::parse(text(p, "lightDirection", "top")),
invert: flag(p, "invert", false),
},
"video.deinterlace" => PixelFx::DeInterlace {
even: text(p, "eliminate", "odd").eq_ignore_ascii_case("even"),
interpolate: text(p, "create", "duplication").eq_ignore_ascii_case("interpolation"),
},
"video.ntscColors" => PixelFx::NtscColors,
_ => return None,
})
}
impl PixelFx {
/// How far beyond `bounds` (the content's, document units) the effect can paint.
pub fn outset(&self, bounds: Rect) -> f64 {
// From the centre to the farthest corner, and to the nearest side.
let far = 0.5 * bounds.width().hypot(bounds.height());
let near = 0.5 * bounds.width().min(bounds.height());
match *self {
// A rotated corner reaches the circle through the corners.
PixelFx::RadialBlur { zoom: false, .. } => (far - near).max(0.0),
PixelFx::RadialBlur { amount, zoom: true, .. } => far * (blur::zoom_extent(amount).exp() - 1.0),
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, .. }
| PixelFx::RoughPastels { length, .. } => brushstrokes::stroke_reach(length),
PixelFx::Crosshatch { length, .. } => brushstrokes::stroke_reach(length) + brushstrokes::rough(length),
PixelFx::DarkStrokes { .. } => brushstrokes::stroke_reach(brushstrokes::LIGHT_LENGTH),
PixelFx::Spatter { radius, .. } => brushstrokes::spatter_shift(radius),
PixelFx::SprayedStrokes { length, radius, .. } => brushstrokes::stroke_reach(length) + brushstrokes::spray_shift(radius),
// Sumi-e keeps the object's shape.
PixelFx::SumiE { .. } => 0.0,
PixelFx::DiffuseGlow { .. } => 0.0,
// Content shifted outwards reaches past the bounds by as much as the shift.
PixelFx::Glass { distortion, .. } => distort::glass_shift(distortion),
PixelFx::OceanRipple { size, magnitude } => distort::ripple_shift(size, magnitude),
PixelFx::UnsharpMask { .. }
| PixelFx::ChalkCharcoal { .. }
| PixelFx::Chrome { .. }
| PixelFx::ConteCrayon { .. }
| PixelFx::HalftonePattern { .. }
| PixelFx::NotePaper { .. }
| PixelFx::Plaster { .. }
| PixelFx::Reticulation { .. }
| PixelFx::TornEdges { .. } => 0.0,
PixelFx::BasRelief { .. } | PixelFx::Charcoal { .. } | PixelFx::Photocopy { .. } | PixelFx::Stamp { .. } => 0.0,
PixelFx::WaterPaper { fiber_length, .. } => sketch::water_paper_shift(fiber_length),
PixelFx::GlowingEdges { width, smoothness, .. } => width.max(smoothness * 3.0),
PixelFx::PaintDaubs { size, .. } => artistic::paint_daub_radius(size),
// These three keep the object's coverage.
PixelFx::SmudgeStick { .. } | PixelFx::Sponge { .. } | PixelFx::Watercolor { .. } => 0.0,
PixelFx::FilmGrain { .. }
| PixelFx::ColoredPencil { .. }
| PixelFx::Fresco { .. }
| PixelFx::Cutout { .. }
| PixelFx::DryBrush { .. }
| PixelFx::NeonGlow { .. }
| PixelFx::PlasticWrap { .. }
| PixelFx::PosterEdges { .. }
| PixelFx::Underpainting { .. }
| PixelFx::ColorHalftone { .. }
| PixelFx::Mezzotint { .. }
| PixelFx::Craquelure { .. }
| PixelFx::Grain { .. }
| PixelFx::MosaicTiles { .. }
| PixelFx::Texturizer { .. }
| PixelFx::DeInterlace { .. }
| PixelFx::NtscColors => 0.0,
// A crystal's, dot's or pane's point inside the object reaches out by up to its cell.
PixelFx::Crystallize { cell } | PixelFx::Pointillize { cell } | PixelFx::StainedGlass { cell, .. } => 1.5 * cell,
// A square whose centre is on the object is drawn whole.
PixelFx::Patchwork { square, .. } => square.max(1.0),
}
}
/// How far (document units) from a pixel the content that decides it lies; `None` when that
/// is anywhere in the object (Radial Blur).
pub fn reach(&self) -> Option<f64> {
match *self {
PixelFx::RadialBlur { .. } => None,
PixelFx::SmartBlur { radius, mode: SmartBlurMode::Normal, .. } => Some(radius),
PixelFx::SmartBlur { radius, samples, .. } => Some(blur::edge_reach(radius, samples)),
PixelFx::SmudgeStick { stroke, .. } => Some(paint::smudge_reach(stroke)),
PixelFx::Sponge { brush, smoothness, .. } => Some(paint::sponge_reach(brush, smoothness)),
PixelFx::Watercolor { detail, .. } => Some(paint::watercolor_reach(detail)),
PixelFx::AccentedEdges { width, smoothness, .. } => Some(brushstrokes::accent_reach(width, smoothness)),
// The strokes, and the softened tones that choose them.
PixelFx::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))
}
PixelFx::RoughPastels { length, .. } => Some(artistic::rough_pastels_reach(length)),
PixelFx::Crosshatch { length, .. } => {
Some((brushstrokes::stroke_reach(length) + brushstrokes::rough(length)).max(brushstrokes::TONE_REACH))
}
PixelFx::DarkStrokes { .. } => Some(brushstrokes::stroke_reach(brushstrokes::LIGHT_LENGTH).max(brushstrokes::TONE_REACH)),
PixelFx::InkOutlines { length, .. } => Some(brushstrokes::ink_reach(length)),
PixelFx::Spatter { radius, .. } => Some(brushstrokes::spatter_shift(radius)),
PixelFx::SprayedStrokes { length, radius, .. } => Some(brushstrokes::stroke_reach(length) + brushstrokes::spray_shift(radius)),
PixelFx::SumiE { width, .. } => Some(brushstrokes::sumi_reach(width)),
PixelFx::DiffuseGlow { .. } => Some(3.0 * distort::GLOW_SPREAD),
PixelFx::Glass { distortion, .. } => Some(distort::glass_shift(distortion)),
PixelFx::OceanRipple { size, magnitude } => Some(distort::ripple_shift(size, magnitude)),
PixelFx::UnsharpMask { radius, .. } => Some(3.0 * radius),
PixelFx::BasRelief { detail, smoothness, .. } => Some(sketch_ink::relief_reach(detail, smoothness)),
PixelFx::Charcoal { thickness, detail, .. } => Some(sketch_ink::charcoal_reach(thickness, detail)),
PixelFx::Photocopy { detail, .. } => Some(sketch_ink::copy_reach(detail)),
PixelFx::Stamp { smoothness, .. } => Some(sketch_ink::stamp_reach(smoothness)),
PixelFx::GlowingEdges { width, smoothness, .. } => Some(width.max(smoothness * 3.0)),
// The cells around the pixel's, and the cells around theirs.
PixelFx::ColorHalftone { max_radius, .. } => Some(3.0 * std::f64::consts::SQRT_2 * max_radius),
PixelFx::Crystallize { cell } => Some(1.5 * cell),
PixelFx::ColoredPencil { width, .. } => Some(artistic::colored_pencil_reach(width)),
PixelFx::ChalkCharcoal { .. } => Some(sketch::CHALK_CHARCOAL_REACH),
PixelFx::Chrome { smoothness, .. } => Some(sketch::chrome_reach(smoothness)),
PixelFx::ConteCrayon { .. } => Some(sketch::CONTE_REACH),
PixelFx::FilmGrain { .. }
| PixelFx::Mezzotint { .. }
| 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),
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))
}
// Cutout's palette and Torn Edges' peak-normalised edges come from the whole object.
PixelFx::Cutout { .. } | PixelFx::TornEdges { .. } => None,
// The softened tones around the pixel.
PixelFx::NeonGlow { size, .. } => Some(3.0 * artistic::glow_spread(size)),
PixelFx::PlasticWrap { smoothness, .. } => Some(artistic::plastic_reach(smoothness)),
// The edges' lines, spread round them.
PixelFx::PosterEdges { thickness, .. } => Some(3.0 * (0.3 + 0.25 * thickness) + 1.0),
PixelFx::Underpainting { brush_size, .. } => Some(3.0 * (0.1 * brush_size.max(0.25)) + 1.0),
// The dots' points, and the softened colour around them.
PixelFx::Pointillize { cell } => Some(2.5 * cell),
// The softened tones around the pixel.
PixelFx::Craquelure { spacing, .. } => Some(3.0 * texture::CONTOUR_SOFTNESS * spacing),
PixelFx::Grain { .. } | PixelFx::MosaicTiles { .. } | PixelFx::Texturizer { .. } => Some(0.0),
// The square's or pane's point, and the softened colour around it.
PixelFx::Patchwork { square, .. } => Some(2.0 * square.max(1.0)),
PixelFx::StainedGlass { cell, .. } => Some(2.5 * cell),
// The lines above and below.
PixelFx::DeInterlace { .. } => Some(video::MAX_LINE),
PixelFx::NtscColors => Some(0.0),
}
}
/// Run the effect on `data`, `w` × `h` premultiplied RGBA8 pixels lying in the document as
/// `space` says. Does nothing when the sizes don't match.
pub fn apply(&self, data: &mut [u8], w: usize, h: usize, space: &PixelSpace) {
let Some(px) = pixels(data, w, h) else { return };
let to_px = |len: f64| len / space.px.max(1e-9);
match *self {
PixelFx::ColoredPencil { width, pressure, paper } => artistic::colored_pencil(px, w, h, space, width, pressure, paper),
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),
PixelFx::PaintDaubs { size, sharpness, kind } => artistic::paint_daubs(px, w, h, space, size, sharpness, kind),
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)
}
PixelFx::Underpainting { brush_size, texture_coverage, ref texture, scaling, relief, light, invert } => {
artistic::underpainting(px, w, h, space, brush_size, texture_coverage, texture, scaling, relief, light, invert)
}
PixelFx::RoughPastels { length, detail, ref texture, scaling, relief, light, invert } => {
artistic::rough_pastels(px, w, h, space, length, detail, texture, scaling, relief, light, invert)
}
PixelFx::RadialBlur { amount, zoom, passes } => blur::radial(px, w, h, space, amount, zoom, passes),
PixelFx::SmartBlur { radius, threshold, samples, mode: SmartBlurMode::Normal } => {
blur::smart(px, w, h, to_px(radius), threshold, samples)
}
PixelFx::SmartBlur { radius, threshold, samples, mode } => {
blur::smart_edges(px, w, h, to_px(radius), threshold, samples, mode == SmartBlurMode::OverlayEdge)
}
PixelFx::AccentedEdges { width, brightness, smoothness } => brushstrokes::accented_edges(px, w, h, space, width, brightness, smoothness),
PixelFx::AngledStrokes { balance, length, sharpness } => brushstrokes::angled_strokes(px, w, h, space, balance, length, sharpness),
PixelFx::Crosshatch { length, sharpness, strength } => brushstrokes::crosshatch(px, w, h, space, length, sharpness, strength),
PixelFx::DarkStrokes { balance, black, white } => brushstrokes::dark_strokes(px, w, h, space, balance, black, white),
PixelFx::InkOutlines { length, dark, light } => brushstrokes::ink_outlines(px, w, h, space, length, dark, light),
PixelFx::Spatter { radius, smoothness } => brushstrokes::spatter(px, w, h, space, radius, smoothness),
PixelFx::SprayedStrokes { length, radius, direction } => brushstrokes::sprayed_strokes(px, w, h, space, length, radius, direction),
PixelFx::SumiE { width, pressure, contrast } => brushstrokes::sumi_e(px, w, h, space, width, pressure, contrast),
PixelFx::DiffuseGlow { graininess, glow, clear } => distort::diffuse_glow(px, w, h, space, graininess, glow, clear),
PixelFx::Glass { distortion, smoothness, ref texture, scaling, invert } => {
distort::glass(px, w, h, space, distortion, smoothness, texture, scaling, invert)
}
PixelFx::OceanRipple { size, magnitude } => distort::ocean_ripple(px, w, h, space, size, magnitude),
PixelFx::UnsharpMask { amount, radius, threshold } => sharpen::unsharp(px, w, h, amount, to_px(radius), threshold),
PixelFx::ChalkCharcoal { chalk, charcoal, pressure } => sketch::chalk_charcoal(px, w, h, space, chalk, charcoal, pressure),
PixelFx::Chrome { detail, smoothness } => sketch::chrome(px, w, h, space, detail, smoothness),
PixelFx::ConteCrayon { foreground, background, ref texture, scaling, relief, light, invert } => {
sketch::conte_crayon(px, w, h, space, foreground, background, texture, scaling, relief, light, invert)
}
PixelFx::GraphicPen { length, balance, direction } => brushstrokes::graphic_pen(px, w, h, space, length, balance, direction),
PixelFx::HalftonePattern { size, contrast, kind } => sketch::halftone_pattern(px, w, h, space, size, contrast, kind),
PixelFx::NotePaper { balance, graininess, relief } => sketch::note_paper(px, w, h, space, balance, graininess, relief),
PixelFx::BasRelief { detail, smoothness, light } => sketch_ink::bas_relief(px, w, h, space, detail, smoothness, light),
PixelFx::Charcoal { thickness, detail, balance } => sketch_ink::charcoal(px, w, h, space, thickness, detail, balance),
PixelFx::Photocopy { detail, darkness } => sketch_ink::photocopy(px, w, h, space, detail, darkness),
PixelFx::Stamp { balance, smoothness } => sketch_ink::stamp(px, w, h, space, balance, smoothness),
PixelFx::Plaster { image_balance, smoothness, light } => sketch::plaster(px, w, h, space, image_balance, smoothness, light),
PixelFx::Reticulation { density, foreground, background } => sketch::reticulation(px, w, h, space, density, foreground, background),
PixelFx::TornEdges { image_balance, smoothness, contrast } => sketch::torn_edges(px, w, h, space, image_balance, smoothness, contrast),
PixelFx::WaterPaper { fiber_length, brightness, contrast } => sketch::water_paper(px, w, h, space, fiber_length, brightness, contrast),
PixelFx::GlowingEdges { width, brightness, smoothness } => edges::glow(px, w, h, to_px(width), brightness, to_px(smoothness)),
PixelFx::ColorHalftone { max_radius, angles } => pixelate::color_halftone(px, w, h, space, max_radius, angles),
PixelFx::Crystallize { cell } => pixelate::crystallize(px, w, h, space, cell),
PixelFx::Mezzotint { kind } => pixelate::mezzotint(px, w, h, space, kind),
PixelFx::Pointillize { cell } => pixelate::pointillize(px, w, h, space, cell),
PixelFx::Craquelure { spacing, depth, brightness } => texture::craquelure(px, w, h, space, spacing, depth, brightness),
PixelFx::Grain { intensity, contrast, kind } => texture::grain(px, w, h, space, intensity, contrast, kind),
PixelFx::MosaicTiles { tile, grout, lighten } => texture::mosaic_tiles(px, w, h, space, tile, grout, lighten),
PixelFx::Patchwork { square, relief } => texture::patchwork(px, w, h, space, square, relief),
PixelFx::StainedGlass { cell, border, light } => texture::stained_glass(px, w, h, space, cell, border, light),
PixelFx::Texturizer { ref texture, scaling, relief, light, invert } => {
texture::texturizer(px, w, h, space, texture, scaling, relief, light, invert)
}
PixelFx::DeInterlace { even, interpolate } => video::deinterlace(px, w, h, space, even, interpolate),
PixelFx::NtscColors => video::ntsc(px),
}
}
}
/// The entry of `all` whose value in `table` (the same order) is `value`, any case; `default`
/// otherwise.
fn pick<T: Clone>(table: &[(&str, &str)], all: &[T], value: &str, default: T) -> T {
table.iter().zip(all).find(|((_, v), _)| v.eq_ignore_ascii_case(value)).map_or(default, |(_, t)| t.clone())
}
/// `data` as `w` × `h` RGBA pixels, `None` when it isn't that size (or is empty).
fn pixels(data: &mut [u8], w: usize, h: usize) -> Option<&mut [[u8; 4]]> {
let (px, rest) = data.as_chunks_mut::<4>();
(w > 0 && h > 0 && rest.is_empty() && w.checked_mul(h) == Some(px.len())).then_some(px)
}
/// The largest box radius the blurs use: wider boxes look the same on any raster we make.
const MAX_BOX: usize = 1 << 17;
/// Radii of three box blurs approximating a Gaussian of `sigma` pixels.
pub fn gauss_boxes(sigma: f64) -> [usize; 3] {
let sigma = if sigma.is_finite() { sigma.clamp(0.0, MAX_BOX as f64 / 2.0) } else { 0.0 };
let n = 3.0;
let w_ideal = (12.0 * sigma * sigma / n + 1.0).sqrt();
let mut wl = w_ideal.floor() as i64;
if wl % 2 == 0 {
wl -= 1;
}
let wu = wl + 2;
let m_ideal = (12.0 * sigma * sigma - n * (wl * wl) as f64 - 4.0 * n * wl as f64 - 3.0 * n) / (-4.0 * wl as f64 - 4.0);
let m = m_ideal.round() as i64;
let r = |i: i64| ((((if i < m { wl } else { wu }) - 1) / 2).max(0) as usize).min(MAX_BOX);
[r(0), r(1), r(2)]
}
/// One horizontal box blur of radius `r` over the rows of `w` values of a plane (running sum,
/// zero past the ends).
fn box_plane(src: &[f32], dst: &mut [f32], w: usize, r: usize) {
if r == 0 {
dst.copy_from_slice(src);
return;
}
let norm = 1.0 / (2 * r + 1) as f32;
for (row, out) in src.chunks_exact(w).zip(dst.chunks_exact_mut(w)) {
let mut acc: f32 = row.iter().take(r + 1).sum();
for (x, o) in out.iter_mut().enumerate() {
*o = acc * norm;
if let Some(v) = row.get(x + r + 1) {
acc += v;
}
if let Some(v) = x.checked_sub(r).and_then(|i| row.get(i)) {
acc -= v;
}
}
}
}
fn transpose<T: Copy>(src: &[T], dst: &mut [T], w: usize, h: usize) {
for (y, row) in src.chunks_exact(w).enumerate().take(h) {
for (x, v) in row.iter().enumerate() {
if let Some(d) = dst.get_mut(x * h + y) {
*d = *v;
}
}
}
}
/// Gaussian-blur a `w` × `h` plane of values in place (three box passes per axis, zero past the
/// edges). Does nothing when the sizes don't match.
pub fn blur_plane(a: &mut Vec<f32>, w: usize, h: usize, sigma: f64) {
if sigma < 0.2 || w == 0 || h == 0 || w.checked_mul(h) != Some(a.len()) {
return;
}
let boxes = gauss_boxes(sigma);
let mut tmp = vec![0.0; a.len()];
for (len, other) in [(w, h), (h, w)] {
for r in boxes {
box_plane(a, &mut tmp, len, r);
std::mem::swap(a, &mut tmp);
}
transpose(a, &mut tmp, len, other);
std::mem::swap(a, &mut tmp);
}
}
/// Grow a `w` × `h` plane by `radius` pixels each way, the box of SVG's feMorphology: each value
/// becomes the largest within the radius. A fractional radius blends the two whole radii around
/// it, so a silhouette grows smoothly as the raster scales. Does nothing for a radius of zero or
/// less, or when the sizes don't match.
pub fn dilate_plane(a: &mut Vec<f32>, w: usize, h: usize, radius: f64) {
if radius.is_nan() || radius <= 0.0 || w == 0 || h == 0 || w.checked_mul(h) != Some(a.len()) {
return;
}
let radius = radius.min(MAX_BOX as f64);
let whole = radius.floor() as usize;
let frac = (radius - whole as f64) as f32;
let mut tmp = vec![0.0; a.len()];
for (len, other) in [(w, h), (h, w)] {
max_plane(a, &mut tmp, len, whole, frac);
std::mem::swap(a, &mut tmp);
transpose(a, &mut tmp, len, other);
std::mem::swap(a, &mut tmp);
}
}
/// One horizontal pass of [`dilate_plane`] over the rows of `w` values of a plane: the largest
/// value within `whole`, blended by `frac` towards the largest within `whole + 1`.
fn max_plane(src: &[f32], dst: &mut [f32], w: usize, whole: usize, frac: f32) {
let (mut inner, mut ring) = (vec![0.0f32; w], vec![0.0f32; w]);
for (row, out) in src.chunks_exact(w).zip(dst.chunks_exact_mut(w)) {
window_max(row, whole, &mut inner);
window_max(row, whole + 1, &mut ring);
for ((o, i), r) in out.iter_mut().zip(&inner).zip(&ring) {
*o = i + (r - i) * frac;
}
}
}
/// For each `x`, the largest of `row[x - radius..=x + radius]` (cut at the row's ends) into `out`,
/// in time linear in the row (a sliding window, not a scan of the window for each `x`).
fn window_max(row: &[f32], radius: usize, out: &mut [f32]) {
use std::collections::VecDeque;
// Indices of the window's values that can still be its largest, falling in value.
let mut keep: VecDeque<usize> = VecDeque::new();
let mut next = 0;
for (x, o) in out.iter_mut().enumerate().take(row.len()) {
let hi = x.saturating_add(radius).min(row.len().saturating_sub(1));
while next <= hi {
let v = row[next];
while keep.back().is_some_and(|&b| row[b] <= v) {
keep.pop_back();
}
keep.push_back(next);
next += 1;
}
let lo = x.saturating_sub(radius);
while keep.front().is_some_and(|&f| f < lo) {
keep.pop_front();
}
*o = keep.front().map_or(row[x], |&f| row[f]);
}
}
/// Premultiplied RGBA at 16 bits (×257): blurs and averages that keep their tones.
pub(super) type Px16 = [u16; 4];
pub(super) fn to16(px: &[[u8; 4]]) -> Vec<Px16> {
px.iter().map(|p| p.map(|v| v as u16 * 257)).collect()
}
pub(super) fn to8(v: u16) -> u8 {
((v as u32 + 128) / 257).min(255) as u8
}
fn add(acc: &mut [u64; 4], p: &Px16) {
for (a, v) in acc.iter_mut().zip(p) {
*a += *v as u64;
}
}
fn sub(acc: &mut [u64; 4], p: &Px16) {
for (a, v) in acc.iter_mut().zip(p) {
*a = a.saturating_sub(*v as u64);
}
}
fn mean(acc: &[u64; 4], n: u64) -> Px16 {
acc.map(|a| ((a + n / 2) / n.max(1)).min(65535) as u16)
}
/// One box blur of radius `r` along the rows of `w` pixels (transparent past the ends).
fn box_rows(src: &[Px16], dst: &mut [Px16], w: usize, r: usize) {
let n = 2 * r as u64 + 1;
for (row, out) in src.chunks_exact(w).zip(dst.chunks_exact_mut(w)) {
let mut acc = [0u64; 4];
row.iter().take(r + 1).for_each(|p| add(&mut acc, p));
for (x, o) in out.iter_mut().enumerate() {
*o = mean(&acc, n);
if let Some(p) = row.get(x + r + 1) {
add(&mut acc, p);
}
if let Some(p) = x.checked_sub(r).and_then(|i| row.get(i)) {
sub(&mut acc, p);
}
}
}
}
/// One box blur of radius `r` down the columns of `w`-wide rows (transparent past the ends), row
/// by row with a running sum per column.
fn box_cols(src: &[Px16], dst: &mut [Px16], w: usize, r: usize) {
let n = 2 * r as u64 + 1;
let rows: Vec<&[Px16]> = src.chunks_exact(w).collect();
let mut acc = vec![[0u64; 4]; w];
let add_row = |acc: &mut [[u64; 4]], row: &[Px16]| acc.iter_mut().zip(row).for_each(|(a, p)| add(a, p));
rows.iter().take(r + 1).for_each(|row| add_row(&mut acc, row));
for (y, out) in dst.chunks_exact_mut(w).enumerate() {
for (o, a) in out.iter_mut().zip(&acc) {
*o = mean(a, n);
}
if let Some(row) = rows.get(y + r + 1) {
add_row(&mut acc, row);
}
if let Some(row) = y.checked_sub(r).and_then(|i| rows.get(i)) {
acc.iter_mut().zip(*row).for_each(|(a, p)| sub(a, p));
}
}
}
/// Box-blur `w`-wide rows of pixels in place with radius `r` along both axes.
pub(super) fn box_blur(px: &mut [Px16], w: usize, r: usize) {
if r == 0 || w == 0 {
return;
}
let mut tmp = vec![[0u16; 4]; px.len()];
box_rows(px, &mut tmp, w, r);
box_cols(&tmp, px, w, r);
}
/// Gaussian-blur `w`-wide rows of premultiplied pixels in place (σ = `sigma` pixels, three box
/// passes per axis, transparent past the edges).
pub(super) fn gaussian(px: &mut [Px16], w: usize, sigma: f64) {
if sigma < 0.2 || w == 0 {
return;
}
let mut tmp = vec![[0u16; 4]; px.len()];
for r in gauss_boxes(sigma) {
box_rows(px, &mut tmp, w, r);
px.copy_from_slice(&tmp);
}
for r in gauss_boxes(sigma) {
box_cols(px, &mut tmp, w, r);
px.copy_from_slice(&tmp);
}
}
/// Gaussian-blur `data`, `w` × `h` premultiplied RGBA8 pixels, in place (σ = `sigma` pixels). Does
/// nothing when the sizes don't match.
pub fn gaussian_rgba(data: &mut [u8], w: usize, h: usize, sigma: f64) {
if let Some(px) = pixels(data, w, h)
&& sigma >= 0.2
{
let mut wide = to16(px);
gaussian(&mut wide, w, sigma);
for (p, q) in px.iter_mut().zip(&wide) {
*p = q.map(to8);
}
}
}
#[cfg(test)]
mod tests;
#[cfg(test)]
mod tests_paint;
#[cfg(test)]
mod tests_sketch;
#[cfg(test)]
mod window_max_tests {
use super::window_max;
#[test]
fn the_sliding_maximum_matches_a_scan_of_each_window() {
let row: Vec<f32> = (0..40u32).map(|i| ((i * 7919) % 31) as f32).collect();
for radius in [0, 1, 2, 5, 39, 100] {
let mut out = vec![0.0; row.len()];
window_max(&row, radius, &mut out);
for (x, got) in out.iter().enumerate() {
let (lo, hi) = (x.saturating_sub(radius), (x + radius).min(row.len() - 1));
let want = row[lo..=hi].iter().copied().fold(f32::MIN, f32::max);
assert_eq!(*got, want, "radius {radius} at {x}");
}
}
}
}