Merge pull request #981 from storytold/feat/distort-effects
Part of gap 2: Effect › Distort (Diffuse Glow, Glass, Ocean Ripple)
This commit is contained in:
commit
e3874e89e7
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