Part of gap 2: Effect › Distort (Diffuse Glow, Glass, Ocean Ripple)
Three raster effects on the pixel-filter pipeline (vectorcraft_effects::pixel::distort), under a new Effect › Distort submenu between Blur and Pixelate in the Raster Effects section (the vector Distort & Transform stays its own submenu): - Diffuse Glow (graininess 0..10, default 6; glowAmount 0..20, 10; clearAmount 0..20, 15): the highlights, softened over 2 points, glow white from the brightness Clear Amount leaves clear; white grain as dense as Graininess says is sprinkled over the object, thicker in the glow. Transparency stays as it was. - Glass (distortion 0..20, 5; smoothness 1..15, 3; texture Blocks, Canvas, Frosted or Tiny Lens, default Frosted; scaling % 50..200, 100; invert): the object seen through a glass surface; each pixel takes the colour the surface's slope bends it to, sampled between pixels. Slopes are taken over a span Smoothness widens. The surfaces are made in code (Canvas is Texturizer's weave); Load Texture is left out. - Ocean Ripple (rippleSize 1..15, 9; rippleMagnitude 0..20, 9): smooth noise ripples about Ripple Size + 2 points across shift the object by up to 0.03 x magnitude x their size. The shifts are capped, so the effects' outsets and reaches are exact, and every surface lies in document space around the object's centre: the same at any zoom or resolution, moving with the object, identical on every redraw. Catalogue rows with parameter docs, menu entries, the Texture dropdown, translations (and German's missing Canvas), tests (parameters and clamping, outsets, each texture and Invert, the ripples' reach, the glow and its grain, resolution and move invariance, canvas, multithreaded, PDF and SVG, the command sweep with junk parameters, the menu) and ROADMAP. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
f147eb58c7
commit
8a0cd9dbf6
15
ROADMAP.md
15
ROADMAP.md
File diff suppressed because one or more lines are too long
|
|
@ -146,6 +146,7 @@ const SHAPE: &[&str] = &["Effect", "Convert to Shape"];
|
|||
const STYLIZE: &[&str] = &["Effect", "Stylize"];
|
||||
const WARP: &[&str] = &["Effect", "Warp"];
|
||||
const BLUR: &[&str] = &["Effect", "Blur"];
|
||||
const DISTORT: &[&str] = &["Effect", "Distort"];
|
||||
const SHARPEN: &[&str] = &["Effect", "Sharpen"];
|
||||
const PIXELATE: &[&str] = &["Effect", "Pixelate"];
|
||||
const TEXTURE: &[&str] = &["Effect", "Texture"];
|
||||
|
|
@ -295,6 +296,27 @@ pub fn effect_catalog() -> Vec<EffectInfo> {
|
|||
"{radius: pt 0.1..100 (3), threshold: levels 0.1..100 (25; only colours closer than this blur together, so edges stay sharp), quality: \"low\"|\"medium\"|\"high\" (\"medium\"; 5, 7 or 9 samples across)} (Normal mode)",
|
||||
json!({"radius": 3.0, "threshold": 25.0, "quality": "medium"}),
|
||||
),
|
||||
r(
|
||||
"distort.diffuseGlow",
|
||||
"Diffuse Glow…",
|
||||
DISTORT,
|
||||
"{graininess: 0..10 (6; white grain, thicker in the glow), glowAmount: 0..20 (10), clearAmount: 0..20 (15; the higher, the more of the image stays clear of glow)} renders the object as if seen through a soft diffusion filter: its highlights glow white under see-through white grain",
|
||||
json!({"graininess": 6.0, "glowAmount": 10.0, "clearAmount": 15.0}),
|
||||
),
|
||||
r(
|
||||
"distort.glass",
|
||||
"Glass…",
|
||||
DISTORT,
|
||||
"{distortion: 0..20 (5), smoothness: 1..15 (3), texture: \"blocks\"|\"canvas\"|\"frosted\"|\"tinyLens\" (\"frosted\"; surfaces made in code), scaling: % 50..200 (100), invert: bool (false; turns the surface's heights over)} makes the object look as if seen through glass",
|
||||
json!({"distortion": 5.0, "smoothness": 3.0, "texture": "frosted", "scaling": 100.0, "invert": false}),
|
||||
),
|
||||
r(
|
||||
"distort.oceanRipple",
|
||||
"Ocean Ripple…",
|
||||
DISTORT,
|
||||
"{rippleSize: 1..15 (9), rippleMagnitude: 0..20 (9)} adds randomly spaced ripples, as if the object were under water",
|
||||
json!({"rippleSize": 9.0, "rippleMagnitude": 9.0}),
|
||||
),
|
||||
r(
|
||||
"sharpen.unsharpMask",
|
||||
"Unsharp Mask…",
|
||||
|
|
|
|||
|
|
@ -0,0 +1,200 @@
|
|||
//! Effect › Distort (Diffuse Glow, Glass, Ocean Ripple).
|
||||
//!
|
||||
//! Glass and Ocean Ripple move each pixel's content by a shift that a surface decides (the slope
|
||||
//! of Glass's texture, smooth noise for the ripples), sampling the object bilinearly. The shift is
|
||||
//! capped, so the object reaches no farther than [`glass_shift`] or [`ripple_shift`] past its
|
||||
//! bounds. Diffuse Glow brightens the object's highlights towards white and sprinkles it with
|
||||
//! white grain. Every surface lies in document space around the object's centre, as the Texture
|
||||
//! filters' do.
|
||||
|
||||
use vectorcraft_geom::{Point, Vec2};
|
||||
|
||||
use super::pixelate::{Place, value_noise};
|
||||
use super::texture::{Texture, cell_noise, luma, premultiply, smooth_noise, straight};
|
||||
use super::{PixelSpace, blur_plane, pick};
|
||||
|
||||
const SALT_GLOW: u64 = 120;
|
||||
const SALT_FROST: u64 = 128;
|
||||
const SALT_RIPPLE: u64 = 136;
|
||||
|
||||
/// Glass's surfaces, in its Texture menu's order: (label, parameter value).
|
||||
pub const GLASS_TEXTURES: [(&str, &str); 4] = [("Blocks", "blocks"), ("Canvas", "canvas"), ("Frosted", "frosted"), ("Tiny Lens", "tinyLens")];
|
||||
|
||||
/// A Glass surface.
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
pub enum GlassTexture {
|
||||
Blocks,
|
||||
Canvas,
|
||||
Frosted,
|
||||
TinyLens,
|
||||
}
|
||||
|
||||
impl GlassTexture {
|
||||
const ALL: [GlassTexture; 4] = [GlassTexture::Blocks, GlassTexture::Canvas, GlassTexture::Frosted, GlassTexture::TinyLens];
|
||||
|
||||
/// The surface of `value` (a [`GLASS_TEXTURES`] value, any case); Frosted otherwise.
|
||||
pub fn parse(value: &str) -> Self {
|
||||
pick(&GLASS_TEXTURES, &Self::ALL, value, GlassTexture::Frosted)
|
||||
}
|
||||
|
||||
/// The surface's height at `p` (points at 100 %), in points: slopes of about 1 at most.
|
||||
fn height(self, p: Point) -> f64 {
|
||||
match self {
|
||||
GlassTexture::Blocks => blocks(p),
|
||||
GlassTexture::Canvas => Texture::Canvas.height(p),
|
||||
GlassTexture::Frosted => frosted(p),
|
||||
GlassTexture::TinyLens => tiny_lens(p),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Square glass blocks 10 points across, flat on top with 2.5-point bevels.
|
||||
fn blocks(p: Point) -> f64 {
|
||||
const SIZE: f64 = 10.0;
|
||||
const BEVEL: f64 = 2.5;
|
||||
let to_edge = |v: f64| {
|
||||
let t = v.rem_euclid(SIZE);
|
||||
t.min(SIZE - t)
|
||||
};
|
||||
to_edge(p.x).min(to_edge(p.y)).min(BEVEL)
|
||||
}
|
||||
|
||||
/// Frosted glass: two octaves of fine smooth noise (1.2 and 0.5 points).
|
||||
fn frosted(p: Point) -> f64 {
|
||||
0.8 * value_noise(SALT_FROST, p.x / 1.2, p.y / 1.2) + 0.2 * value_noise(SALT_FROST + 1, p.x / 0.5, p.y / 0.5)
|
||||
}
|
||||
|
||||
/// Tiny lenses 4 points across in rows and columns, each a shallow dome.
|
||||
fn tiny_lens(p: Point) -> f64 {
|
||||
const SIZE: f64 = 4.0;
|
||||
let r = 0.5 * SIZE;
|
||||
let off = |v: f64| v.rem_euclid(SIZE) - r;
|
||||
let d2 = off(p.x).powi(2) + off(p.y).powi(2);
|
||||
0.5 * r * (1.0 - d2 / (r * r)).max(0.0)
|
||||
}
|
||||
|
||||
/// The longest shift (points) Glass gives at `distortion` (0..20).
|
||||
pub(super) fn glass_shift(distortion: f64) -> f64 {
|
||||
0.5 * distortion.clamp(0.0, 20.0)
|
||||
}
|
||||
|
||||
/// The ripples' size (points) for Ripple Size `size` (1..15).
|
||||
fn ripple_wave(size: f64) -> f64 {
|
||||
2.0 + size.clamp(1.0, 15.0)
|
||||
}
|
||||
|
||||
/// The longest shift (points) Ocean Ripple gives for Ripple Size `size` (1..15) and Ripple
|
||||
/// Magnitude `magnitude` (0..20).
|
||||
pub(super) fn ripple_shift(size: f64, magnitude: f64) -> f64 {
|
||||
0.03 * magnitude.clamp(0.0, 20.0) * ripple_wave(size)
|
||||
}
|
||||
|
||||
/// How far (points) Diffuse Glow spreads the highlights (the σ of their blur).
|
||||
pub(super) const GLOW_SPREAD: f64 = 2.0;
|
||||
|
||||
/// Glass: the object seen through a `texture` surface scaled by `scaling` (0.5..2), bent by
|
||||
/// `distortion` (0..20) and smoothed by `smoothness` (1..15); `invert` turns the surface's heights
|
||||
/// over.
|
||||
#[allow(clippy::too_many_arguments)] // the effect's five options
|
||||
pub(super) fn glass(
|
||||
px: &mut [[u8; 4]],
|
||||
w: usize,
|
||||
h: usize,
|
||||
space: &PixelSpace,
|
||||
distortion: f64,
|
||||
smoothness: f64,
|
||||
texture: GlassTexture,
|
||||
scaling: f64,
|
||||
invert: bool,
|
||||
) {
|
||||
let Some(place) = Place::new(space, w, h) else { return };
|
||||
if !(scaling.is_finite() && scaling > 0.0 && smoothness.is_finite()) {
|
||||
return;
|
||||
}
|
||||
// Slopes over a quarter point of the surface per step of Smoothness, or half a pixel where
|
||||
// pixels are larger: a smoother glass bends the image more gently.
|
||||
let step = (0.25 * smoothness.clamp(1.0, 15.0)).max(0.5 * place.px / scaling);
|
||||
let gain = 0.25 * distortion.clamp(0.0, 20.0) * if invert { -1.0 } else { 1.0 };
|
||||
displace(px, w, h, &place, glass_shift(distortion), |q| {
|
||||
let t = Point::new(q.x / scaling, q.y / scaling);
|
||||
let at = |dx: f64, dy: f64| texture.height(Point::new(t.x + dx, t.y + dy));
|
||||
Vec2::new(at(step, 0.0) - at(-step, 0.0), at(0.0, step) - at(0.0, -step)) * (gain / (2.0 * step))
|
||||
});
|
||||
}
|
||||
|
||||
/// Ocean Ripple: the object as if under rippling water, ripples about `size` (1..15) points plus
|
||||
/// two across, shifting it by up to [`ripple_shift`].
|
||||
pub(super) fn ocean_ripple(px: &mut [[u8; 4]], w: usize, h: usize, space: &PixelSpace, size: f64, magnitude: f64) {
|
||||
let Some(place) = Place::new(space, w, h) else { return };
|
||||
let (wave, most) = (ripple_wave(size), ripple_shift(size, magnitude));
|
||||
displace(px, w, h, &place, most, |q| {
|
||||
let n = |salt: u64| 0.7 * smooth_noise(salt, q, wave) + 0.3 * smooth_noise(salt + 2, q, 0.5 * wave);
|
||||
Vec2::new(n(SALT_RIPPLE), n(SALT_RIPPLE + 1)) * most
|
||||
});
|
||||
}
|
||||
|
||||
/// Diffuse Glow: the object as seen through a soft diffusion filter. Its highlights, softened over
|
||||
/// [`GLOW_SPREAD`], glow white as strongly as `glow` (0..20) says, from the brightness that
|
||||
/// `clear` (0..20) leaves clear of glow; white grain as dense as `graininess` (0..10) says is
|
||||
/// sprinkled over it, thicker in the glow.
|
||||
pub(super) fn diffuse_glow(px: &mut [[u8; 4]], w: usize, h: usize, space: &PixelSpace, graininess: f64, glow: f64, clear: f64) {
|
||||
let Some(place) = Place::new(space, w, h) else { return };
|
||||
let mut light: Vec<f32> = px.iter().map(|p| luma(straight(*p)) * f32::from(p[3]) / 255.0).collect();
|
||||
blur_plane(&mut light, w, h, GLOW_SPREAD / place.px);
|
||||
let amount = (glow / 20.0).clamp(0.0, 1.0) as f32;
|
||||
let from = (0.9 * clear / 20.0).clamp(0.0, 0.9) as f32;
|
||||
let grain = (graininess / 10.0).clamp(0.0, 1.0);
|
||||
place.each(px, |x, y, p| {
|
||||
if p[3] == 0 {
|
||||
return;
|
||||
}
|
||||
let g = ((light.get(y * w + x).copied().unwrap_or(0.0) - from) / (1.0 - from)).clamp(0.0, 1.0);
|
||||
let chance = 0.5 * (cell_noise(SALT_GLOW, place.at(x, y), 1.0, 0) + 1.0);
|
||||
let dot = if chance < grain * (0.08 + 0.32 * f64::from(g)) { 0.55 } else { 0.0 };
|
||||
let white = (amount * g + dot).min(1.0);
|
||||
*p = premultiply(straight(*p).map(|v| v + (1.0 - v) * white), f32::from(p[3]) / 255.0);
|
||||
});
|
||||
}
|
||||
|
||||
/// Move the object's content by `shift(q)` (document units; `q` a pixel's centre relative to the
|
||||
/// object's centre, its length capped at `most`): each pixel takes the colour `shift` away from it,
|
||||
/// interpolated between pixels, transparent beyond the raster.
|
||||
fn displace(px: &mut [[u8; 4]], w: usize, h: usize, place: &Place, most: f64, shift: impl Fn(Point) -> Vec2) {
|
||||
if !(most.is_finite() && most > 0.0) {
|
||||
return;
|
||||
}
|
||||
let src = px.to_vec();
|
||||
place.each(px, |x, y, p| {
|
||||
let q = place.at(x, y);
|
||||
let d = shift(q);
|
||||
if !(d.x.is_finite() && d.y.is_finite()) {
|
||||
return;
|
||||
}
|
||||
let len = d.hypot();
|
||||
let d = if len > most { d * (most / len) } else { d };
|
||||
let s = place.to_px * (q + d);
|
||||
*p = sample(&src, w, h, s.x - 0.5, s.y - 0.5);
|
||||
});
|
||||
}
|
||||
|
||||
/// The premultiplied colour at (`x`, `y`) of `w` × `h` pixels (pixel centres at whole
|
||||
/// coordinates), interpolated between the four around it, transparent outside.
|
||||
fn sample(src: &[[u8; 4]], w: usize, h: usize, x: f64, y: f64) -> [u8; 4] {
|
||||
if !(x.is_finite() && y.is_finite()) {
|
||||
return [0; 4];
|
||||
}
|
||||
let (x0, y0) = (x.floor(), y.floor());
|
||||
let (fx, fy) = (x - x0, y - y0);
|
||||
let at = |i: f64, j: f64| -> [f64; 4] {
|
||||
if i < 0.0 || j < 0.0 || i >= w as f64 || j >= h as f64 {
|
||||
return [0.0; 4];
|
||||
}
|
||||
src.get(j as usize * w + i as usize).map_or([0.0; 4], |p| p.map(f64::from))
|
||||
};
|
||||
let (a, b, c, d) = (at(x0, y0), at(x0 + 1.0, y0), at(x0, y0 + 1.0), at(x0 + 1.0, y0 + 1.0));
|
||||
std::array::from_fn(|k| {
|
||||
let top = a[k] + (b[k] - a[k]) * fx;
|
||||
let bottom = c[k] + (d[k] - c[k]) * fx;
|
||||
(top + (bottom - top) * fy).round().clamp(0.0, 255.0) as u8
|
||||
})
|
||||
}
|
||||
|
|
@ -1,5 +1,6 @@
|
|||
//! Photoshop-style raster effects (Effect › Blur › Radial Blur and Smart Blur, Pixelate › Color
|
||||
//! Halftone, Crystallize, Mezzotint and Pointillize, Sharpen › Unsharp Mask, Stylize › Glowing
|
||||
//! Photoshop-style raster effects (Effect › Blur › Radial Blur and Smart Blur, Distort › Diffuse
|
||||
//! Glow, Glass and Ocean Ripple, Pixelate › Color Halftone, Crystallize, Mezzotint and Pointillize,
|
||||
//! Sharpen › Unsharp Mask, 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.
|
||||
//!
|
||||
|
|
@ -11,6 +12,7 @@
|
|||
//! - Beyond the raster's edges is transparency.
|
||||
|
||||
mod blur;
|
||||
mod distort;
|
||||
mod edges;
|
||||
mod pixelate;
|
||||
mod sharpen;
|
||||
|
|
@ -22,13 +24,17 @@ use vectorcraft_geom::{Affine, Point, Rect};
|
|||
|
||||
use crate::util::{flag, num, text};
|
||||
|
||||
pub use distort::{GLASS_TEXTURES, GlassTexture};
|
||||
pub use pixelate::{MEZZOTINT_TYPES, Mezzotint};
|
||||
pub use texture::{GRAIN_TYPES, Grain, LIGHT_DIRECTIONS, Light, TEXTURES, Texture};
|
||||
|
||||
/// The Photoshop-style effect ids (all raster effects, see [`crate::is_raster`]).
|
||||
pub const PIXEL_EFFECTS: [&str; 16] = [
|
||||
pub const PIXEL_EFFECTS: [&str; 19] = [
|
||||
"blur.radial",
|
||||
"blur.smart",
|
||||
"distort.diffuseGlow",
|
||||
"distort.glass",
|
||||
"distort.oceanRipple",
|
||||
"pixelate.colorHalftone",
|
||||
"pixelate.crystallize",
|
||||
"pixelate.mezzotint",
|
||||
|
|
@ -56,6 +62,16 @@ pub enum PixelFx {
|
|||
/// whose colour differs from its own by no more than `threshold` levels, over a grid of
|
||||
/// `samples` × `samples` neighbours at most.
|
||||
SmartBlur { radius: f64, threshold: f64, samples: u32 },
|
||||
/// 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 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 },
|
||||
|
|
@ -149,6 +165,21 @@ pub(crate) fn parse(id: &str, p: &Value) -> Option<PixelFx> {
|
|||
threshold: num(p, "threshold", 25.0).clamp(0.1, 100.0),
|
||||
samples: quality([("low", 5), ("medium", 7), ("high", 9)], 7),
|
||||
},
|
||||
"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(text(p, "texture", "frosted")),
|
||||
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),
|
||||
|
|
@ -217,6 +248,10 @@ impl PixelFx {
|
|||
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::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 { .. } => 0.0,
|
||||
PixelFx::GlowingEdges { width, smoothness, .. } => width.max(smoothness * 3.0),
|
||||
PixelFx::ColorHalftone { .. }
|
||||
|
|
@ -240,6 +275,9 @@ impl PixelFx {
|
|||
match *self {
|
||||
PixelFx::RadialBlur { .. } => None,
|
||||
PixelFx::SmartBlur { radius, .. } => Some(radius),
|
||||
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::GlowingEdges { width, smoothness, .. } => Some(width.max(smoothness * 3.0)),
|
||||
// The cells around the pixel's, and the cells around theirs.
|
||||
|
|
@ -268,6 +306,11 @@ impl PixelFx {
|
|||
match *self {
|
||||
PixelFx::RadialBlur { amount, zoom, passes } => blur::radial(px, w, h, space, amount, zoom, passes),
|
||||
PixelFx::SmartBlur { radius, threshold, samples } => blur::smart(px, w, h, to_px(radius), threshold, samples),
|
||||
PixelFx::DiffuseGlow { graininess, glow, clear } => distort::diffuse_glow(px, w, h, space, graininess, glow, clear),
|
||||
PixelFx::Glass { distortion, smoothness, 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::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),
|
||||
|
|
|
|||
|
|
@ -826,3 +826,159 @@ fn texture_patterns_follow_the_object_at_any_resolution() {
|
|||
assert_eq!(shifted, one, "{id}");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn distort_params_take_defaults_and_stay_in_range() {
|
||||
assert_eq!(fx("distort.diffuseGlow", json!({})), PixelFx::DiffuseGlow { graininess: 6.0, glow: 10.0, clear: 15.0 });
|
||||
assert_eq!(
|
||||
fx("distort.diffuseGlow", json!({"graininess": 1e9, "glowAmount": -3, "clearAmount": "x"})),
|
||||
PixelFx::DiffuseGlow { graininess: 10.0, glow: 0.0, clear: 15.0 }
|
||||
);
|
||||
assert_eq!(
|
||||
fx("distort.glass", json!({})),
|
||||
PixelFx::Glass { distortion: 5.0, smoothness: 3.0, texture: GlassTexture::Frosted, scaling: 1.0, invert: false }
|
||||
);
|
||||
assert_eq!(
|
||||
fx("distort.glass", json!({"distortion": 1e308, "smoothness": 0, "texture": "TINYLENS", "scaling": 1, "invert": true})),
|
||||
PixelFx::Glass { distortion: 20.0, smoothness: 1.0, texture: GlassTexture::TinyLens, scaling: 0.5, invert: true }
|
||||
);
|
||||
assert_eq!(fx("distort.glass", json!({"texture": "bubbles"})), fx("distort.glass", json!({})));
|
||||
assert_eq!(fx("distort.oceanRipple", json!({})), PixelFx::OceanRipple { size: 9.0, magnitude: 9.0 });
|
||||
assert_eq!(fx("distort.oceanRipple", json!({"rippleSize": 99, "rippleMagnitude": -1})), PixelFx::OceanRipple { size: 15.0, magnitude: 0.0 });
|
||||
// Shifted content reaches past the bounds by the longest shift, and comes from as far.
|
||||
let b = Rect::new(0.0, 0.0, 100.0, 50.0);
|
||||
let glass = fx("distort.glass", json!({"distortion": 20}));
|
||||
assert_eq!((glass.outset(b), glass.reach()), (10.0, Some(10.0)));
|
||||
let ripple = fx("distort.oceanRipple", json!({}));
|
||||
assert!((ripple.outset(b) - 0.03 * 9.0 * 11.0).abs() < 1e-9 && ripple.reach() == Some(ripple.outset(b)));
|
||||
assert_eq!(fx("distort.diffuseGlow", json!({})).outset(b), 0.0);
|
||||
}
|
||||
|
||||
/// Vertical stripes 5 points wide (out of step with every glass surface), alternately light and
|
||||
/// dark, over the whole raster.
|
||||
fn stripes(w: usize, h: usize) -> Vec<u8> {
|
||||
image(w, h, |x, _| if (x / 5) % 2 == 0 { [230, 200, 60, 255] } else { [20, 40, 120, 255] })
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn glass_bends_the_image_by_its_texture() {
|
||||
let (w, h) = (48, 40);
|
||||
let src = stripes(w, h);
|
||||
let run = |p: serde_json::Value| {
|
||||
let mut d = src.clone();
|
||||
fx("distort.glass", p).apply(&mut d, w, h, &space(w, h));
|
||||
d
|
||||
};
|
||||
// No distortion, no change.
|
||||
assert_eq!(run(json!({"distortion": 0})), src);
|
||||
let mut seen = vec![];
|
||||
for (_, texture) in GLASS_TEXTURES {
|
||||
let bent = run(json!({"texture": texture, "distortion": 12}));
|
||||
assert_ne!(bent, src, "{texture}");
|
||||
assert!(!seen.contains(&bent), "{texture} differs from the other textures");
|
||||
assert_ne!(run(json!({"texture": texture, "distortion": 12, "invert": true})), bent, "{texture} inverted");
|
||||
// A colour moves; none is invented: every pixel blends two neighbouring stripes at most
|
||||
// (those within a shift of the raster's edges also take in the transparency beyond it).
|
||||
for (x, y) in (6..w - 6).flat_map(|x| (6..h - 6).map(move |y| (x, y))) {
|
||||
let p = at(&bent, w, x, y);
|
||||
assert!(p[3] == 255 && p[0] >= 20 && p[0] <= 230, "{texture} at ({x}, {y}): {p:?}");
|
||||
}
|
||||
seen.push(bent);
|
||||
}
|
||||
// Smoother glass bends the stripes differently (more gently).
|
||||
assert_ne!(run(json!({"distortion": 12, "smoothness": 15})), run(json!({"distortion": 12, "smoothness": 1})));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ocean_ripple_moves_content_no_farther_than_its_magnitude() {
|
||||
let (w, h) = (64, 32);
|
||||
// The left half white, the right half transparent.
|
||||
let src = image(w, h, |x, _| if x < 32 { grey(255) } else { [0; 4] });
|
||||
let mut d = src.clone();
|
||||
fx("distort.oceanRipple", json!({"rippleMagnitude": 0})).apply(&mut d, w, h, &space(w, h));
|
||||
assert_eq!(d, src, "no magnitude, no ripples");
|
||||
let f = fx("distort.oceanRipple", json!({"rippleSize": 6, "rippleMagnitude": 20}));
|
||||
let most = f.reach().unwrap();
|
||||
let mut d = src.clone();
|
||||
f.apply(&mut d, w, h, &space(w, h));
|
||||
assert_ne!(d, src);
|
||||
// Away from the white's edge, and from the raster's (beyond which is transparency), nothing
|
||||
// changes.
|
||||
let inside = |v: usize, n: usize| (v as f64 + 0.5).min(n as f64 - v as f64 - 0.5) > most + 1.0;
|
||||
for y in (0..h).filter(|y| inside(*y, h)) {
|
||||
for x in (0..w).filter(|x| inside(*x, w)) {
|
||||
let to_edge = (x as f64 + 0.5 - 32.0).abs();
|
||||
if to_edge > most + 1.0 {
|
||||
assert_eq!(at(&d, w, x, y), at(&src, w, x, y), "({x}, {y}) is {to_edge} from the edge, past {most}");
|
||||
}
|
||||
}
|
||||
}
|
||||
// The edge wanders: some rows reach farther right than others.
|
||||
let ends: Vec<usize> = (0..h).map(|y| (0..w).filter(|x| at(&d, w, *x, y)[3] > 127).count()).collect();
|
||||
assert!(ends.iter().min() != ends.iter().max(), "{ends:?}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn diffuse_glow_whitens_the_highlights_and_sprinkles_grain() {
|
||||
let (w, h) = (48, 48);
|
||||
// Dark on the left, light on the right, a transparent margin round them.
|
||||
let src = image(w, h, |x, y| {
|
||||
if !(4..44).contains(&x) || !(4..44).contains(&y) {
|
||||
[0; 4]
|
||||
} else if x < 24 {
|
||||
[30, 30, 60, 255]
|
||||
} else {
|
||||
[200, 190, 170, 255]
|
||||
}
|
||||
});
|
||||
let run = |p: serde_json::Value| {
|
||||
let mut d = src.clone();
|
||||
fx("distort.diffuseGlow", p).apply(&mut d, w, h, &space(w, h));
|
||||
d
|
||||
};
|
||||
let clean = run(json!({"graininess": 0}));
|
||||
// Transparency stays; the shadows stay clear; the highlights glow.
|
||||
for (a, b) in clean.as_chunks::<4>().0.iter().zip(src.as_chunks::<4>().0) {
|
||||
assert_eq!(a[3], b[3]);
|
||||
}
|
||||
assert_eq!(at(&clean, w, 10, 24), at(&src, w, 10, 24));
|
||||
let (lit, before) = (at(&clean, w, 36, 24), at(&src, w, 36, 24));
|
||||
assert!((0..3).all(|c| lit[c] > before[c]), "{lit:?} brighter than {before:?}");
|
||||
// More Clear Amount leaves more of the image clear of glow.
|
||||
assert!(at(&run(json!({"graininess": 0, "clearAmount": 20})), w, 36, 24)[0] < lit[0]);
|
||||
assert_eq!(run(json!({"graininess": 0, "glowAmount": 0})), src);
|
||||
// Grain: white specks, in the shadows too.
|
||||
let grainy = run(json!({"graininess": 10}));
|
||||
let specks = (4..24).flat_map(|x| (4..44).map(move |y| (x, y))).filter(|(x, y)| at(&grainy, w, *x, *y)[0] > 100).count();
|
||||
assert!(specks > 10, "{specks} white specks in the shadows");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn distort_patterns_follow_the_object_at_any_resolution() {
|
||||
let (w, h) = (48, 48);
|
||||
let art = |x: f64, y: f64| -> [u8; 4] {
|
||||
if (8.0..40.0).contains(&x) && (8.0..40.0).contains(&y) { [(x * 5.0) as u8, (y * 5.0) as u8, 120, 255] } else { [0; 4] }
|
||||
};
|
||||
for (id, p) in [
|
||||
("distort.diffuseGlow", json!({"clearAmount": 5})),
|
||||
("distort.glass", json!({"texture": "blocks", "distortion": 8})),
|
||||
("distort.oceanRipple", json!({"rippleMagnitude": 12})),
|
||||
] {
|
||||
let f = fx(id, p);
|
||||
let mut one = image(w, h, |x, y| art(x as f64 + 0.5, y as f64 + 0.5));
|
||||
f.apply(&mut one, w, h, &space(w, h));
|
||||
let mut two = image(2 * w, 2 * h, |x, y| art((x as f64 + 0.5) / 2.0, (y as f64 + 0.5) / 2.0));
|
||||
let fine = PixelSpace { to_doc: Affine::scale(0.5), px: 0.5, ..space(w, h) };
|
||||
f.apply(&mut two, 2 * w, 2 * h, &fine);
|
||||
let down = image(w, h, |x, y| {
|
||||
let q = [(0, 0), (1, 0), (0, 1), (1, 1)].map(|(dx, dy)| at(&two, 2 * w, 2 * x + dx, 2 * y + dy));
|
||||
std::array::from_fn(|c| (q.iter().map(|p| u32::from(p[c])).sum::<u32>() / 4) as u8)
|
||||
});
|
||||
let diff = one.iter().zip(&down).map(|(a, b)| f64::from(a.abs_diff(*b))).sum::<f64>() / one.len() as f64;
|
||||
assert!(diff < 12.0, "{id}: {diff}");
|
||||
let moved = PixelSpace { to_doc: Affine::translate((7.0, 3.0)), center: Point::new(31.0, 27.0), ..space(w, h) };
|
||||
let mut shifted = image(w, h, |x, y| art(x as f64 + 0.5, y as f64 + 0.5));
|
||||
f.apply(&mut shifted, w, h, &moved);
|
||||
assert_eq!(shifted, one, "{id}");
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -83,7 +83,7 @@ impl Texture {
|
|||
}
|
||||
|
||||
/// The surface's height at `p` (points at 100 %), in points: slopes of about 1 at most.
|
||||
fn height(self, p: Point) -> f64 {
|
||||
pub(super) fn height(self, p: Point) -> f64 {
|
||||
match self {
|
||||
Texture::Brick => brick(p),
|
||||
Texture::Burlap => weave(p, 7.0, true),
|
||||
|
|
@ -155,13 +155,13 @@ const SALT_SURFACE: u64 = 112;
|
|||
pub(super) const CONTOUR_SOFTNESS: f64 = 1.0 / 6.0;
|
||||
|
||||
/// A pixel's straight colour (0..1).
|
||||
fn straight(p: [u8; 4]) -> [f32; 3] {
|
||||
pub(super) fn straight(p: [u8; 4]) -> [f32; 3] {
|
||||
let a = f32::from(p[3]);
|
||||
[p[0], p[1], p[2]].map(|v| if a > 0.0 { (f32::from(v) / a).min(1.0) } else { 0.0 })
|
||||
}
|
||||
|
||||
/// The pixel of straight colour `c` at coverage `alpha` (0..1), premultiplied.
|
||||
fn premultiply(c: [f32; 3], alpha: f32) -> [u8; 4] {
|
||||
pub(super) fn premultiply(c: [f32; 3], alpha: f32) -> [u8; 4] {
|
||||
let a = (alpha.clamp(0.0, 1.0) * 255.0).round();
|
||||
let [r, g, b] = c.map(|v| (v.clamp(0.0, 1.0) * a).round().min(a) as u8);
|
||||
[r, g, b, a as u8]
|
||||
|
|
@ -185,7 +185,7 @@ fn cover(d: f64, px: f64) -> f64 {
|
|||
}
|
||||
|
||||
/// The luma of colour `c` (Rec. 601 weights).
|
||||
fn luma(c: [f32; 3]) -> f32 {
|
||||
pub(super) fn luma(c: [f32; 3]) -> f32 {
|
||||
0.299 * c[0] + 0.587 * c[1] + 0.114 * c[2]
|
||||
}
|
||||
|
||||
|
|
@ -308,7 +308,7 @@ pub(super) fn craquelure(px: &mut [[u8; 4]], w: usize, h: usize, space: &PixelSp
|
|||
}
|
||||
|
||||
/// A random value in [-1, 1) for the `size`-point cell holding `q`; `k` picks one of several.
|
||||
fn cell_noise(salt: u64, q: Point, size: f64, k: u64) -> f64 {
|
||||
pub(super) fn cell_noise(salt: u64, q: Point, size: f64, k: u64) -> f64 {
|
||||
match (cell(q.x / size), cell(q.y / size)) {
|
||||
(Some(i), Some(j)) => 2.0 * rand(salt, i, j, k) - 1.0,
|
||||
_ => 0.0,
|
||||
|
|
@ -316,7 +316,7 @@ fn cell_noise(salt: u64, q: Point, size: f64, k: u64) -> f64 {
|
|||
}
|
||||
|
||||
/// [`value_noise`] in [-1, 1) over cells `size` points wide.
|
||||
fn smooth_noise(salt: u64, q: Point, size: f64) -> f64 {
|
||||
pub(super) fn smooth_noise(salt: u64, q: Point, size: f64) -> f64 {
|
||||
2.0 * value_noise(salt, q.x / size, q.y / size) - 1.0
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
//! The Photoshop-style raster effects (Effect › Blur › Radial Blur and Smart Blur, Pixelate ›
|
||||
//! Color Halftone, Crystallize, Mezzotint and Pointillize, Sharpen › Unsharp Mask, Texture ›
|
||||
//! The Photoshop-style raster effects (Effect › Blur › Radial Blur and Smart Blur, Distort ›
|
||||
//! Diffuse Glow, Glass and Ocean Ripple, Pixelate › Color Halftone, Crystallize, Mezzotint and Pointillize, Sharpen › Unsharp Mask, Texture ›
|
||||
//! Craquelure, Grain, Mosaic Tiles, Patchwork, Stained Glass and Texturizer, Video): applied and
|
||||
//! edited as commands, listed in the catalogue, drawn on the canvas, and written to PDF and SVG
|
||||
//! as images of the effected object.
|
||||
|
|
@ -18,6 +18,11 @@ const EFFECTS: [(&str, &str); 3] = [
|
|||
("sharpen.unsharpMask", r#"{"amount": 300, "radius": 3}"#),
|
||||
];
|
||||
|
||||
/// Effect › Distort (Diffuse Glow with its highlights glowing from a low brightness up, as the
|
||||
/// striped art's reds are dark).
|
||||
const DISTORT: [(&str, &str); 3] =
|
||||
[("distort.diffuseGlow", r#"{"clearAmount": 0, "glowAmount": 20}"#), ("distort.glass", "{}"), ("distort.oceanRipple", "{}")];
|
||||
|
||||
/// Effect › Pixelate, at their defaults.
|
||||
const PIXELATE: [(&str, &str); 4] =
|
||||
[("pixelate.colorHalftone", "{}"), ("pixelate.crystallize", "{}"), ("pixelate.mezzotint", "{}"), ("pixelate.pointillize", "{}")];
|
||||
|
|
@ -73,6 +78,7 @@ fn listed_applied_edited_and_undone() {
|
|||
EFFECTS.iter().map(|(id, _)| list["catalog"].as_array().unwrap().iter().find(|e| e["id"] == *id).unwrap()["menu"].clone()).collect();
|
||||
assert_eq!(menus, [json!(["Effect", "Blur"]), json!(["Effect", "Blur"]), json!(["Effect", "Sharpen"])]);
|
||||
for (group, menu) in [
|
||||
(DISTORT.as_slice(), "Distort"),
|
||||
(PIXELATE.as_slice(), "Pixelate"),
|
||||
(TEXTURE.as_slice(), "Texture"),
|
||||
(VIDEO.as_slice(), "Video"),
|
||||
|
|
@ -102,7 +108,7 @@ fn listed_applied_edited_and_undone() {
|
|||
|
||||
#[test]
|
||||
fn each_effect_changes_the_canvas_and_draws_the_same_twice() {
|
||||
for (id, params) in EFFECTS.into_iter().chain(PIXELATE).chain(TEXTURE).chain(VIDEO) {
|
||||
for (id, params) in EFFECTS.into_iter().chain(DISTORT).chain(PIXELATE).chain(TEXTURE).chain(VIDEO) {
|
||||
let (mut s, g) = striped();
|
||||
let plain = render(&s);
|
||||
let params: Value = serde_json::from_str(params).unwrap();
|
||||
|
|
@ -131,7 +137,7 @@ fn spin_blur_reaches_past_the_corners() {
|
|||
|
||||
#[test]
|
||||
fn pdf_and_svg_write_the_effected_object_as_an_image() {
|
||||
for (id, params) in EFFECTS.into_iter().chain(PIXELATE).chain(TEXTURE).chain(VIDEO) {
|
||||
for (id, params) in EFFECTS.into_iter().chain(DISTORT).chain(PIXELATE).chain(TEXTURE).chain(VIDEO) {
|
||||
let (mut s, g) = striped();
|
||||
let params: Value = serde_json::from_str(params).unwrap();
|
||||
s.execute("effect.apply", &json!({"effect": id, "ids": [g], "params": params})).unwrap();
|
||||
|
|
|
|||
|
|
@ -307,6 +307,9 @@ fn structured_junk() {
|
|||
),
|
||||
("effect.apply", json!({"effect": "video.deinterlace", "params": {"eliminate": 7, "create": ["x"]}})),
|
||||
("effect.apply", json!({"effect": "video.ntscColors", "params": {"junk": 1e308}})),
|
||||
("effect.apply", json!({"effect": "distort.diffuseGlow", "params": {"graininess": "x", "glowAmount": 1e308, "clearAmount": -1e308}})),
|
||||
("effect.apply", json!({"effect": "distort.glass", "params": {"distortion": 1e308, "smoothness": "NaN", "texture": 3, "scaling": -1}})),
|
||||
("effect.apply", json!({"effect": "distort.oceanRipple", "params": {"rippleSize": [1], "rippleMagnitude": 1e308}})),
|
||||
("effect.apply", json!({"effect": "no.such.effect"})),
|
||||
("effect.remove", json!({"index": 99})),
|
||||
("effect.setParams", json!({"index": 0, "params": null})),
|
||||
|
|
|
|||
|
|
@ -159,6 +159,7 @@ fn choices(effect: &str, key: &str) -> Option<form::Choices> {
|
|||
("blur.radial", "method") => Some(&[("Spin", "spin"), ("Zoom", "zoom")]),
|
||||
("blur.radial", "quality") => Some(&[("Draft", "draft"), ("Good", "good"), ("Best", "best")]),
|
||||
("blur.smart", "quality") => Some(&[("Low", "low"), ("Medium", "medium"), ("High", "high")]),
|
||||
("distort.glass", "texture") => Some(&vectorcraft_effects::pixel::GLASS_TEXTURES),
|
||||
("pixelate.mezzotint", "type") => Some(&vectorcraft_effects::pixel::MEZZOTINT_TYPES),
|
||||
("texture.grain", "grainType") => Some(&vectorcraft_effects::pixel::GRAIN_TYPES),
|
||||
("texture.texturizer", "texture") => Some(&vectorcraft_effects::pixel::TEXTURES),
|
||||
|
|
|
|||
|
|
@ -674,3 +674,7 @@
|
|||
Texturizer… Texturovač…
|
||||
System Title Bar Systémové záhlaví okna
|
||||
Exit Ukončit
|
||||
Distort Zkreslení
|
||||
Diffuse Glow… Rozptýlená záře…
|
||||
Glass… Sklo…
|
||||
Ocean Ripple… Vlnky na hladině…
|
||||
|
|
|
|||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
|
|
@ -3938,3 +3938,19 @@ theme Appearance Erscheinungsbild
|
|||
On Both Sides Auf beiden Seiten
|
||||
Short Kurz
|
||||
Full Width Volle Breite
|
||||
Distort Verzerren
|
||||
Diffuse Glow… Weiches Licht…
|
||||
Glass… Glas…
|
||||
Ocean Ripple… Ozeanwellen…
|
||||
Diffuse Glow Weiches Licht
|
||||
Glass Glas
|
||||
Ocean Ripple Ozeanwellen
|
||||
Graininess Körnung
|
||||
Glow Amount Lichtmenge
|
||||
Clear Amount Klarer Bereich
|
||||
Ripple Size Wellengröße
|
||||
Ripple Magnitude Wellenstärke
|
||||
Blocks Blöcke
|
||||
Frosted Mattiert
|
||||
Tiny Lens Kleine Linsen
|
||||
Canvas Leinwand
|
||||
|
|
|
|||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
|
|
@ -3982,3 +3982,18 @@ axis Both Ambos
|
|||
On Both Sides En ambos lados
|
||||
Short Corta
|
||||
Full Width Ancho completo
|
||||
Distort Distorsionar
|
||||
Diffuse Glow… Resplandor difuso…
|
||||
Glass… Vidrio…
|
||||
Ocean Ripple… Rizo oceánico…
|
||||
Diffuse Glow Resplandor difuso
|
||||
Glass Vidrio
|
||||
Ocean Ripple Rizo oceánico
|
||||
Graininess Granulosidad
|
||||
Glow Amount Cantidad de resplandor
|
||||
Clear Amount Cantidad de claridad
|
||||
Ripple Size Tamaño de rizo
|
||||
Ripple Magnitude Magnitud de rizo
|
||||
Blocks Bloques
|
||||
Frosted Esmerilado
|
||||
Tiny Lens Lente pequeña
|
||||
|
|
|
|||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
|
|
@ -3994,3 +3994,18 @@ effect Soft Doux
|
|||
On Both Sides Des deux côtés
|
||||
Short Courte
|
||||
Full Width Toute la largeur
|
||||
Distort Déformation
|
||||
Diffuse Glow… Diffusion lumineuse…
|
||||
Glass… Verre…
|
||||
Ocean Ripple… Ondulation océanique…
|
||||
Diffuse Glow Diffusion lumineuse
|
||||
Glass Verre
|
||||
Ocean Ripple Ondulation océanique
|
||||
Graininess Granularité
|
||||
Glow Amount Intensité de la lueur
|
||||
Clear Amount Zone claire
|
||||
Ripple Size Taille de l'ondulation
|
||||
Ripple Magnitude Amplitude de l'ondulation
|
||||
Blocks Blocs
|
||||
Frosted Dépoli
|
||||
Tiny Lens Petites lentilles
|
||||
|
|
|
|||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
|
|
@ -3984,3 +3984,18 @@ effect Regular Normale
|
|||
On Both Sides Su entrambi i lati
|
||||
Short Breve
|
||||
Full Width Larghezza intera
|
||||
Distort Distorsione
|
||||
Diffuse Glow… Bagliore diffuso…
|
||||
Glass… Vetro…
|
||||
Ocean Ripple… Increspatura oceanica…
|
||||
Diffuse Glow Bagliore diffuso
|
||||
Glass Vetro
|
||||
Ocean Ripple Increspatura oceanica
|
||||
Graininess Granulosità
|
||||
Glow Amount Quantità di bagliore
|
||||
Clear Amount Quantità di chiarezza
|
||||
Ripple Size Dimensione increspatura
|
||||
Ripple Magnitude Ampiezza increspatura
|
||||
Blocks Blocchi
|
||||
Frosted Smerigliato
|
||||
Tiny Lens Lente piccola
|
||||
|
|
|
|||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
|
|
@ -3977,3 +3977,18 @@ effect Vertical 垂直
|
|||
On Both Sides 両側
|
||||
Short 短い
|
||||
Full Width 全幅
|
||||
Distort 変形
|
||||
Diffuse Glow… 光彩拡散…
|
||||
Glass… ガラス…
|
||||
Ocean Ripple… 海の波紋…
|
||||
Diffuse Glow 光彩拡散
|
||||
Glass ガラス
|
||||
Ocean Ripple 海の波紋
|
||||
Graininess 粒子
|
||||
Glow Amount 光彩の量
|
||||
Clear Amount クリア量
|
||||
Ripple Size 波紋のサイズ
|
||||
Ripple Magnitude 波紋の強さ
|
||||
Blocks ブロック
|
||||
Frosted すりガラス
|
||||
Tiny Lens 小さなレンズ
|
||||
|
|
|
|||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
|
|
@ -2734,3 +2734,7 @@
|
|||
Texturizer… Texturizador…
|
||||
System Title Bar Barra de título do sistema
|
||||
Exit Sair
|
||||
Distort Distorcer
|
||||
Diffuse Glow… Brilho difuso…
|
||||
Glass… Vidro…
|
||||
Ocean Ripple… Ondulação oceânica…
|
||||
|
|
|
|||
|
Can't render this file because it contains an unexpected character in line 6 and column 47.
|
|
|
@ -3980,3 +3980,18 @@ axis Both Оба
|
|||
On Both Sides С обеих сторон
|
||||
Short Короткие
|
||||
Full Width Во всю ширину
|
||||
Distort Искажение
|
||||
Diffuse Glow… Рассеянное свечение…
|
||||
Glass… Стекло…
|
||||
Ocean Ripple… Океанские волны…
|
||||
Diffuse Glow Рассеянное свечение
|
||||
Glass Стекло
|
||||
Ocean Ripple Океанские волны
|
||||
Graininess Зернистость
|
||||
Glow Amount Сила свечения
|
||||
Clear Amount Чистые области
|
||||
Ripple Size Размер волн
|
||||
Ripple Magnitude Сила волн
|
||||
Blocks Блоки
|
||||
Frosted Матовое
|
||||
Tiny Lens Мелкие линзы
|
||||
|
|
|
|||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
|
|
@ -3965,3 +3965,18 @@ axis Both Обидві
|
|||
On Both Sides З обох боків
|
||||
Short Короткі
|
||||
Full Width На всю ширину
|
||||
Distort Спотворення
|
||||
Diffuse Glow… Розсіяне світіння…
|
||||
Glass… Скло…
|
||||
Ocean Ripple… Океанські хвилі…
|
||||
Diffuse Glow Розсіяне світіння
|
||||
Glass Скло
|
||||
Ocean Ripple Океанські хвилі
|
||||
Graininess Зернистість
|
||||
Glow Amount Сила світіння
|
||||
Clear Amount Чисті ділянки
|
||||
Ripple Size Розмір хвиль
|
||||
Ripple Magnitude Сила хвиль
|
||||
Blocks Блоки
|
||||
Frosted Матове
|
||||
Tiny Lens Дрібні лінзи
|
||||
|
|
|
|||
|
Can't render this file because it has a wrong number of fields in line 13.
|
|
|
@ -3613,3 +3613,18 @@ effect Vertical 垂直
|
|||
On Both Sides 两侧
|
||||
Short 短
|
||||
Full Width 全宽
|
||||
Distort 扭曲
|
||||
Diffuse Glow… 扩散亮光…
|
||||
Glass… 玻璃…
|
||||
Ocean Ripple… 海洋波纹…
|
||||
Diffuse Glow 扩散亮光
|
||||
Glass 玻璃
|
||||
Ocean Ripple 海洋波纹
|
||||
Graininess 颗粒
|
||||
Glow Amount 发光量
|
||||
Clear Amount 清除数量
|
||||
Ripple Size 波纹大小
|
||||
Ripple Magnitude 波纹幅度
|
||||
Blocks 块状
|
||||
Frosted 磨砂
|
||||
Tiny Lens 小镜头
|
||||
|
|
|
|||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
|
|
@ -3394,3 +3394,18 @@ effect Soft 柔和
|
|||
On Both Sides 兩側
|
||||
Short 短
|
||||
Full Width 全寬
|
||||
Distort 扭曲
|
||||
Diffuse Glow… 擴散光暈…
|
||||
Glass… 玻璃…
|
||||
Ocean Ripple… 海洋波紋…
|
||||
Diffuse Glow 擴散光暈
|
||||
Glass 玻璃
|
||||
Ocean Ripple 海洋波紋
|
||||
Graininess 顆粒
|
||||
Glow Amount 光暈量
|
||||
Clear Amount 清除量
|
||||
Ripple Size 波紋大小
|
||||
Ripple Magnitude 波紋強度
|
||||
Blocks 塊狀
|
||||
Frosted 磨砂
|
||||
Tiny Lens 小鏡頭
|
||||
|
|
|
|||
|
Can't render this file because it contains an unexpected character in line 6 and column 47.
|
|
|
@ -3297,8 +3297,9 @@ fn font_items(english_names: bool) -> Vec<Item> {
|
|||
|
||||
/// The raster effects' submenus of the Effect menu (the Photoshop-style effects, below the
|
||||
/// vector effects): (submenu, the catalogue's menu path of its effects).
|
||||
const RASTER_MENUS: [(&str, &[&str]); 5] = [
|
||||
const RASTER_MENUS: [(&str, &[&str]); 6] = [
|
||||
("Blur", &["Effect", "Blur"]),
|
||||
("Distort", &["Effect", "Distort"]),
|
||||
("Pixelate", &["Effect", "Pixelate"]),
|
||||
("Sharpen", &["Effect", "Sharpen"]),
|
||||
("Texture", &["Effect", "Texture"]),
|
||||
|
|
|
|||
|
|
@ -78,6 +78,23 @@ fn effect_menu_lists_the_video_filters_last() {
|
|||
assert!(at("Sharpen") < at("Video"));
|
||||
}
|
||||
|
||||
/// Effect › Distort, between Blur and Pixelate: each filter opens its dialog.
|
||||
#[test]
|
||||
fn effect_menu_lists_the_distort_filters_between_blur_and_pixelate() {
|
||||
let app = app();
|
||||
let entries = menus::menu_entries(&app);
|
||||
let distort: Vec<_> = entries.iter().filter(|e| e.path == ["Effect", "Distort"]).collect();
|
||||
let labels: Vec<&str> = distort.iter().map(|e| e.label.as_str()).collect();
|
||||
assert_eq!(labels, ["Diffuse Glow…", "Glass…", "Ocean Ripple…"]);
|
||||
assert!(distort.iter().all(|e| e.command.as_deref() == Some("effect.dialog")));
|
||||
let raster: Vec<&str> =
|
||||
entries.iter().filter_map(|e| e.path.get(1).filter(|_| e.path.len() == 2 && e.path[0] == "Effect")).map(String::as_str).collect();
|
||||
let at = |name: &str| raster.iter().position(|s| *s == name).unwrap();
|
||||
assert!(at("Blur") < at("Distort") && at("Distort") < at("Pixelate"));
|
||||
// The vector Distort & Transform stays its own submenu.
|
||||
assert!(raster.contains(&"Distort & Transform"));
|
||||
}
|
||||
|
||||
/// Effect › Texture, between Sharpen and Video: each filter opens its dialog.
|
||||
#[test]
|
||||
fn effect_menu_lists_the_texture_filters_between_sharpen_and_video() {
|
||||
|
|
|
|||
Loading…
Reference in New Issue