1082 lines
46 KiB
Rust
1082 lines
46 KiB
Rust
//! Distort effects, batch 2: Turbulent Displace, Displacement Map (self as map), Optics
|
||
//! Compensation, Magnify, Mesh Warp, Bezier Warp, CC Bend It, CC Lens, CC Page Turn, CC Tiler,
|
||
//! CC Griddler.
|
||
//!
|
||
//! Mesh Warp and Bezier Warp share a grid rasteriser: a (nx+1)×(ny+1) grid of destination points
|
||
//! and matching source points is split into triangles, bucketed per output row and filled
|
||
//! row-parallel with inverse barycentric sampling.
|
||
//!
|
||
//! Mesh Warp's distortion mesh is stored in the hidden string parameter `mesh`: whitespace (or
|
||
//! `;`) separated `dx,dy` pairs, row-major over the (rows+1)×(columns+1) vertices, giving each
|
||
//! vertex's offset in layer pixels. An empty string (or one with the wrong count) is the
|
||
//! undistorted mesh.
|
||
|
||
use std::f64::consts::{FRAC_PI_2, PI};
|
||
|
||
use effectcraft_color::{BlendMode, blend_pixel};
|
||
use effectcraft_keyframe::Value;
|
||
use effectcraft_project::ParamUi;
|
||
use effectcraft_raster::{Image, Px};
|
||
use rayon::prelude::*;
|
||
|
||
use crate::generate::value_noise;
|
||
use crate::util::{SRC_NAMES, Src, fit_layer, layer_rect, lerp4, map_xy, pick, premul, remap, smoothstep, src_at, unpremul};
|
||
use crate::{Buf, EffectCtx, EffectSpec, col, num, p, popup, slider};
|
||
|
||
fn spec(id: &'static str, name: &'static str, params: Vec<crate::ParamSpec>, render: crate::RenderFn) -> EffectSpec {
|
||
EffectSpec { id, name, category: "Distort", params, render, gpu: false, float: true }
|
||
}
|
||
|
||
fn pt(x: f64, y: f64) -> Value {
|
||
Value::Vec2([x, y])
|
||
}
|
||
|
||
fn pct(max: f64) -> ParamUi {
|
||
slider(0.0, max, 0.0, max.min(100.0), 1)
|
||
}
|
||
|
||
// ---------------------------------------------------------------- Turbulent Displace
|
||
|
||
fn fbm(u: f64, v: f64, z: f64, seed: u32, octaves: f64, falloff: f32) -> f32 {
|
||
let n = octaves.ceil().max(1.0) as usize;
|
||
let frac = (octaves - octaves.floor()) as f32;
|
||
let (mut sum, mut norm, mut amp, mut f) = (0.0f32, 0.0f32, 1.0f32, 1.0f32);
|
||
for o in 0..n {
|
||
let w = if o + 1 == n && frac > 0.0 { frac } else { 1.0 };
|
||
sum += value_noise(u as f32 * f, v as f32 * f, z as f32 + o as f32 * 5.17, seed.wrapping_add(o as u32 * 101)) * amp * w;
|
||
norm += amp * w;
|
||
amp *= falloff;
|
||
f *= 2.0;
|
||
}
|
||
sum / norm.max(1e-6)
|
||
}
|
||
|
||
const TURBULENT_KINDS: [&str; 9] = [
|
||
"Turbulent",
|
||
"Bulge",
|
||
"Twist",
|
||
"Turbulent Smoother",
|
||
"Bulge Smoother",
|
||
"Twist Smoother",
|
||
"Vertical Displacement",
|
||
"Horizontal Displacement",
|
||
"Cross Displacement",
|
||
];
|
||
const TURBULENT_PINNING: [&str; 8] = ["None", "Pin All", "Pin Horizontal", "Pin Vertical", "Pin Left", "Pin Top", "Pin Right", "Pin Bottom"];
|
||
|
||
fn turbulent_displace(ctx: &EffectCtx, mut b: Buf) -> Buf {
|
||
let amount = ctx.params.f("amount") * b.scale;
|
||
if amount.abs() < 1e-9 {
|
||
return b;
|
||
}
|
||
// TURBULENT_KINDS order: the "Smoother" variants (3, 4, 5) use a gentler octave falloff.
|
||
let kind = ctx.params.e("displacement");
|
||
let size = (ctx.params.f("size") * b.scale).max(1.0);
|
||
if ctx.params.b("resizeLayer") && !ctx.adjustment {
|
||
b.pad(amount.abs().ceil() as u32 + 1);
|
||
}
|
||
let off = b.to_px(ctx.params.v2("offset"));
|
||
let oct = ctx.params.f("complexity").clamp(1.0, 10.0);
|
||
let evo = ctx.params.f("evolution") / 360.0;
|
||
// Evolution Options: Cycle Evolution loops the noise every `cycle` revolutions.
|
||
let cycle = (ctx.params.b("evolutionOptions/cycleEvolution")).then(|| ctx.params.f("evolutionOptions/cycle").round().max(1.0));
|
||
let pin = ctx.params.e("pinning");
|
||
let falloff = if (3..=5).contains(&kind) { 0.3 } else { 0.5 };
|
||
let (lx, ly, lw, lh) = layer_rect(ctx, &b);
|
||
let edge = (lw.min(lh) * 0.1).max(1.0);
|
||
let seed = (ctx.seed ^ 0x7d15).wrapping_add((ctx.params.f("evolutionOptions/randomSeed") as i64 as u32).wrapping_mul(0x9e37_79b9));
|
||
let n = |u: f64, v: f64, s: u32| {
|
||
let s = seed.wrapping_add(s);
|
||
match cycle {
|
||
Some(c) => {
|
||
// Cross-fade the noise at z and z - c so z = 0 and z = c match.
|
||
let z = evo.rem_euclid(c);
|
||
let t = z / c;
|
||
(fbm(u, v, z, s, oct, falloff) as f64 * (1.0 - t) + fbm(u, v, z - c, s, oct, falloff) as f64 * t) - 0.5
|
||
}
|
||
None => fbm(u, v, evo, s, oct, falloff) as f64 - 0.5,
|
||
}
|
||
};
|
||
b.img = remap(&b.img, false, |x, y| {
|
||
let (u, v) = ((x - off.0) / size, (y - off.1) / size);
|
||
let (dx, dy) = match kind {
|
||
1 | 2 | 4 | 5 => {
|
||
let e = 0.05;
|
||
let gx = (n(u + e, v, 0) - n(u - e, v, 0)) / (2.0 * e);
|
||
let gy = (n(u, v + e, 0) - n(u, v - e, 0)) / (2.0 * e);
|
||
let k = amount * 0.25;
|
||
if kind == 1 || kind == 4 { (gx * k, gy * k) } else { (-gy * k, gx * k) }
|
||
}
|
||
6 => (0.0, n(u, v, 17) * amount),
|
||
7 => (n(u, v, 0) * amount, 0.0),
|
||
8 => {
|
||
let a = n(u, v, 0) * amount;
|
||
(a, a)
|
||
}
|
||
_ => (n(u, v, 0) * amount, n(u, v, 17) * amount),
|
||
};
|
||
// Measured from the outermost pixel centres, so pinned edges stay exactly in place.
|
||
let (l, r) = (((x - lx - 0.5) / edge).clamp(0.0, 1.0), ((lx + lw - 0.5 - x) / edge).clamp(0.0, 1.0));
|
||
let (t, bt) = (((y - ly - 0.5) / edge).clamp(0.0, 1.0), ((ly + lh - 0.5 - y) / edge).clamp(0.0, 1.0));
|
||
let k = match pin {
|
||
1 => l.min(r).min(t).min(bt),
|
||
2 => t.min(bt),
|
||
3 => l.min(r),
|
||
4 => l,
|
||
5 => t,
|
||
6 => r,
|
||
7 => bt,
|
||
_ => 1.0,
|
||
};
|
||
Some((x + dx * k, y + dy * k))
|
||
});
|
||
b
|
||
}
|
||
|
||
// ---------------------------------------------------------------- Displacement Map
|
||
|
||
fn displacement_map(ctx: &EffectCtx, mut b: Buf) -> Buf {
|
||
let hs = src_at(ctx.params.e("useForHorizontal"));
|
||
let vs = src_at(ctx.params.e("useForVertical"));
|
||
let mh = ctx.params.f("maxHorizontal") * b.scale;
|
||
let mv = ctx.params.f("maxVertical") * b.scale;
|
||
let wrap = ctx.params.b("wrapPixelsAround");
|
||
if ctx.params.b("expandOutput") && !wrap && !ctx.adjustment {
|
||
b.pad(mh.abs().max(mv.abs()).ceil() as u32);
|
||
}
|
||
// The map: the chosen layer (Center / Stretch / Tile), else the layer itself.
|
||
let layer = ctx.layer_param("displacementMapLayer", false);
|
||
let behavior = ctx.params.e("displacementMapBehavior");
|
||
let map_size = layer.as_ref().map(|o| o.size);
|
||
let map = match layer {
|
||
Some(o) => match behavior {
|
||
1 => fit_layer(ctx, &b, &o, true),
|
||
2 => {
|
||
let (os, ls) = (o.size, ctx.layer_size);
|
||
let (dx, dy) = ((os[0] - ls[0]) * 0.5, (os[1] - ls[1]) * 0.5);
|
||
let inv = 1.0 / b.scale.max(1e-9);
|
||
crate::util::gen_image(b.img.width, b.img.height, |x, y| {
|
||
let qx = ((x as f64 + 0.5 - b.offset[0]) * inv + dx).rem_euclid(os[0].max(1.0));
|
||
let qy = ((y as f64 + 0.5 - b.offset[1]) * inv + dy).rem_euclid(os[1].max(1.0));
|
||
o.buf.img.sample_bilinear_clamped(qx * o.buf.scale + o.buf.offset[0], qy * o.buf.scale + o.buf.offset[1])
|
||
})
|
||
}
|
||
_ => fit_layer(ctx, &b, &o, false),
|
||
},
|
||
None => b.img.clone(),
|
||
};
|
||
// Center Map: pixels outside the (smaller) map are not displaced.
|
||
let (scale, offset) = (b.scale, b.offset);
|
||
let outside = |x: usize, y: usize| -> bool {
|
||
match map_size {
|
||
Some(os) if behavior == 0 => {
|
||
let inv = 1.0 / scale.max(1e-9);
|
||
let ls = ctx.layer_size;
|
||
let qx = (x as f64 + 0.5 - offset[0]) * inv + (os[0] - ls[0]) * 0.5;
|
||
let qy = (y as f64 + 0.5 - offset[1]) * inv + (os[1] - ls[1]) * 0.5;
|
||
!(0.0..os[0]).contains(&qx) || !(0.0..os[1]).contains(&qy)
|
||
}
|
||
_ => false,
|
||
}
|
||
};
|
||
let src = b.img.clone();
|
||
let (w, h) = (src.width as f64, src.height as f64);
|
||
map_xy(&mut b.img, |x, y, px| {
|
||
if outside(x, y) {
|
||
return px;
|
||
}
|
||
let (c, a) = unpremul(map.get(x as i64, y as i64));
|
||
// "Off" turns displacement in that direction off.
|
||
let d = |s: Src, m: f64| if s == Src::Off { 0.0 } else { (pick(s, c, a) as f64 - 0.5) * 2.0 * m };
|
||
let (dx, dy) = (d(hs, mh), d(vs, mv));
|
||
if dx == 0.0 && dy == 0.0 {
|
||
return px;
|
||
}
|
||
let (mut sx, mut sy) = (x as f64 + 0.5 + dx, y as f64 + 0.5 + dy);
|
||
if wrap {
|
||
sx = sx.rem_euclid(w);
|
||
sy = sy.rem_euclid(h);
|
||
src.sample_bilinear_clamped(sx, sy)
|
||
} else {
|
||
src.sample_bilinear(sx, sy)
|
||
}
|
||
});
|
||
b
|
||
}
|
||
|
||
// ---------------------------------------------------------------- Optics Compensation
|
||
|
||
fn optics_compensation(ctx: &EffectCtx, mut b: Buf) -> Buf {
|
||
let fov = ctx.params.f("fieldOfView").clamp(0.0, 179.0).to_radians();
|
||
if fov < 1e-6 {
|
||
return b;
|
||
}
|
||
let reverse = ctx.params.b("reverseLensDistortion");
|
||
let (lx, ly, lw, lh) = layer_rect(ctx, &b);
|
||
let r_ref = match ctx.params.e("fovOrientation") {
|
||
1 => lh * 0.5,
|
||
2 => (lw * lw + lh * lh).sqrt() * 0.5,
|
||
_ => lw * 0.5,
|
||
}
|
||
.max(1.0);
|
||
let half = fov * 0.5;
|
||
let f = r_ref / half.tan();
|
||
// Resize (with Reverse Lens Distortion): grow the layer so the stretched corners stay
|
||
// visible, up to 2× / 4× the layer size (Unlimited is capped at 8×).
|
||
let resize = ctx.params.e("resize");
|
||
if reverse && resize > 0 && !ctx.adjustment {
|
||
let c = b.to_px(ctx.params.v2("viewCenter"));
|
||
let rc =
|
||
[(lx, ly), (lx + lw, ly), (lx, ly + lh), (lx + lw, ly + lh)].iter().map(|p| ((p.0 - c.0).powi(2) + (p.1 - c.1).powi(2)).sqrt()).fold(0.0, f64::max);
|
||
let th = rc / r_ref * half;
|
||
let cap = lw.max(lh) * [0.0, 0.5, 1.5, 3.5][resize.min(3) as usize];
|
||
let grow = if th < FRAC_PI_2 - 1e-3 { (f * th.tan() - rc).max(0.0) } else { cap };
|
||
b.pad(grow.min(cap).min(4096.0).ceil() as u32);
|
||
}
|
||
let c = b.to_px(ctx.params.v2("viewCenter"));
|
||
b.img = remap(&b.img, false, |x, y| {
|
||
let (dx, dy) = (x - c.0, y - c.1);
|
||
let r = (dx * dx + dy * dy).sqrt();
|
||
if r < 1e-9 {
|
||
return Some((x, y));
|
||
}
|
||
let rs = if reverse {
|
||
(r / f).atan() / half * r_ref
|
||
} else {
|
||
let th = r / r_ref * half;
|
||
if th >= FRAC_PI_2 - 1e-4 {
|
||
return None;
|
||
}
|
||
f * th.tan()
|
||
};
|
||
let k = rs / r;
|
||
Some((c.0 + dx * k, c.1 + dy * k))
|
||
});
|
||
b
|
||
}
|
||
|
||
// ---------------------------------------------------------------- Magnify
|
||
|
||
fn magnify(ctx: &EffectCtx, mut b: Buf) -> Buf {
|
||
let square = ctx.params.e("shape") == 1;
|
||
let c = b.to_px(ctx.params.v2("center"));
|
||
let mag = (ctx.params.f("magnification") / 100.0).max(0.01);
|
||
let link = ctx.params.e("link");
|
||
let mut size = ctx.params.f("size") * b.scale;
|
||
let mut feather = ctx.params.f("feather") * b.scale;
|
||
if link >= 1 {
|
||
size *= mag;
|
||
}
|
||
if link == 2 {
|
||
feather *= mag;
|
||
}
|
||
let op = (ctx.params.f("opacity") / 100.0) as f32;
|
||
let scaling = ctx.params.e("scaling");
|
||
// Blending Mode: index 0 is "None" (only the magnified area, transparent around it).
|
||
let mode = ctx.params.e("blendingMode") as usize;
|
||
let blend = MAGNIFY_MODES.get(mode.wrapping_sub(1)).copied();
|
||
if ctx.params.b("resizeLayer") && link == 0 && !ctx.adjustment {
|
||
// Let the magnified area extend past the layer edges.
|
||
let (lx, ly, lw, lh) = layer_rect(ctx, &b);
|
||
let reach = size.max(0.0);
|
||
let over = [lx - (c.0 - reach), (c.0 + reach) - (lx + lw), ly - (c.1 - reach), (c.1 + reach) - (ly + lh)].into_iter().fold(0.0, f64::max);
|
||
b.pad(over.min(4096.0).ceil() as u32);
|
||
}
|
||
let c = b.to_px(ctx.params.v2("center"));
|
||
let src = b.img.clone();
|
||
let seed = ctx.seed ^ 0x3a61;
|
||
map_xy(&mut b.img, |x, y, px| {
|
||
let (dx, dy) = (x as f64 + 0.5 - c.0, y as f64 + 0.5 - c.1);
|
||
let d = if square { dx.abs().max(dy.abs()) } else { (dx * dx + dy * dy).sqrt() };
|
||
let w = if feather > 0.01 { 1.0 - smoothstep((size - feather) as f32, size as f32, d as f32) } else { (size - d + 0.5).clamp(0.0, 1.0) as f32 } * op;
|
||
if w <= 0.0 {
|
||
return if blend.is_none() { [0.0; 4] } else { px };
|
||
}
|
||
let (sx, sy) = (c.0 + dx / mag, c.1 + dy / mag);
|
||
let m = match scaling {
|
||
// Soft: smooth (bicubic spline) interpolation.
|
||
1 => src.sample_bicubic(sx, sy),
|
||
// Scatter: jitter each sample within the enlarged source pixel.
|
||
2 => {
|
||
let j = |k: u32| (crate::util::hash1(x as u32, y as u32 ^ (k << 20), seed) as f64 - 0.5) * (1.0 - 1.0 / mag).max(0.0);
|
||
src.sample_bilinear(sx.floor() + 0.5 + j(1), sy.floor() + 0.5 + j(2))
|
||
}
|
||
// Standard: nearest source pixel (sharp, blocky at high magnification).
|
||
_ => src.sample_bilinear(sx.floor() + 0.5, sy.floor() + 0.5),
|
||
};
|
||
match blend {
|
||
None => m.map(|v| v * w),
|
||
Some(BlendMode::Normal) => lerp4(px, m, w),
|
||
Some(mode) => blend_pixel(mode, px, m.map(|v| v * w), 0.5),
|
||
}
|
||
});
|
||
b
|
||
}
|
||
|
||
/// Magnify's Blending Mode choices after "None".
|
||
const MAGNIFY_MODES: [BlendMode; 17] = [
|
||
BlendMode::Normal,
|
||
BlendMode::Add,
|
||
BlendMode::Multiply,
|
||
BlendMode::Screen,
|
||
BlendMode::Overlay,
|
||
BlendMode::SoftLight,
|
||
BlendMode::HardLight,
|
||
BlendMode::ColorDodge,
|
||
BlendMode::ColorBurn,
|
||
BlendMode::Darken,
|
||
BlendMode::Lighten,
|
||
BlendMode::Difference,
|
||
BlendMode::Exclusion,
|
||
BlendMode::Hue,
|
||
BlendMode::Saturation,
|
||
BlendMode::Color,
|
||
BlendMode::Luminosity,
|
||
];
|
||
const MAGNIFY_MODE_NAMES: [&str; 18] = [
|
||
"None",
|
||
"Normal",
|
||
"Add",
|
||
"Multiply",
|
||
"Screen",
|
||
"Overlay",
|
||
"Soft Light",
|
||
"Hard Light",
|
||
"Color Dodge",
|
||
"Color Burn",
|
||
"Darken",
|
||
"Lighten",
|
||
"Difference",
|
||
"Exclusion",
|
||
"Hue",
|
||
"Saturation",
|
||
"Color",
|
||
"Luminosity",
|
||
];
|
||
|
||
// ---------------------------------------------------------------- grid rasteriser
|
||
|
||
type Pt = (f64, f64);
|
||
|
||
/// Warp `src` into a `w`×`h` image: grid point `i` (row-major, (nx+1)×(ny+1)) at `dest[i]` shows
|
||
/// the source at `srcp[i]`. Uncovered pixels are transparent.
|
||
fn grid_warp(src: &Image, w: u32, h: u32, nx: usize, ny: usize, dest: &[Pt], srcp: &[Pt]) -> Image {
|
||
let idx = |i: usize, j: usize| j * (nx + 1) + i;
|
||
let mut tris: Vec<[usize; 3]> = Vec::with_capacity(nx * ny * 2);
|
||
for j in 0..ny {
|
||
for i in 0..nx {
|
||
let (a, b2, c, d) = (idx(i, j), idx(i + 1, j), idx(i + 1, j + 1), idx(i, j + 1));
|
||
tris.push([a, b2, c]);
|
||
tris.push([a, c, d]);
|
||
}
|
||
}
|
||
let hh = h as usize;
|
||
let mut buckets: Vec<Vec<usize>> = vec![Vec::new(); hh];
|
||
for (t, tri) in tris.iter().enumerate() {
|
||
let ys = tri.map(|k| dest[k].1);
|
||
let y0 = (ys[0].min(ys[1]).min(ys[2]) - 0.5).floor().max(0.0) as usize;
|
||
let y1 = (ys[0].max(ys[1]).max(ys[2]) + 0.5).ceil().max(0.0) as usize;
|
||
for y in y0..y1.min(hh) {
|
||
buckets[y].push(t);
|
||
}
|
||
}
|
||
let mut out = Image::new(w, h);
|
||
out.rows_mut().for_each(|(y, row)| {
|
||
let py = y as f64 + 0.5;
|
||
for &t in &buckets[y] {
|
||
let [ia, ib, ic] = tris[t];
|
||
let (a, bb, c) = (dest[ia], dest[ib], dest[ic]);
|
||
let den = (bb.1 - c.1) * (a.0 - c.0) + (c.0 - bb.0) * (a.1 - c.1);
|
||
if den.abs() < 1e-12 {
|
||
continue;
|
||
}
|
||
let x0 = (a.0.min(bb.0).min(c.0) - 0.5).floor().max(0.0) as usize;
|
||
let x1 = ((a.0.max(bb.0).max(c.0) + 0.5).ceil().max(0.0) as usize).min(row.len());
|
||
for x in x0..x1 {
|
||
if row[x][3] > 0.0 {
|
||
continue;
|
||
}
|
||
let px = x as f64 + 0.5;
|
||
let l1 = ((bb.1 - c.1) * (px - c.0) + (c.0 - bb.0) * (py - c.1)) / den;
|
||
let l2 = ((c.1 - a.1) * (px - c.0) + (a.0 - c.0) * (py - c.1)) / den;
|
||
let l3 = 1.0 - l1 - l2;
|
||
let e = -1e-9;
|
||
if l1 < e || l2 < e || l3 < e {
|
||
continue;
|
||
}
|
||
let (sa, sb, sc) = (srcp[ia], srcp[ib], srcp[ic]);
|
||
let sx = sa.0 * l1 + sb.0 * l2 + sc.0 * l3;
|
||
let sy = sa.1 * l1 + sb.1 * l2 + sc.1 * l3;
|
||
row[x] = src.sample_bilinear(sx, sy);
|
||
}
|
||
}
|
||
});
|
||
out
|
||
}
|
||
|
||
/// Mesh Warp's `mesh` parameter: the vertex offsets (layer pixels), row-major.
|
||
pub fn parse_mesh(s: &str) -> Vec<(f64, f64)> {
|
||
s.split(|c: char| c.is_whitespace() || c == ';')
|
||
.filter(|t| !t.is_empty())
|
||
.filter_map(|t| {
|
||
let mut it = t.split(',');
|
||
let x = it.next()?.trim().parse::<f64>().ok()?;
|
||
let y = it.next()?.trim().parse::<f64>().ok()?;
|
||
Some((x, y))
|
||
})
|
||
.collect()
|
||
}
|
||
|
||
fn mesh_warp(ctx: &EffectCtx, mut b: Buf) -> Buf {
|
||
let rows = ctx.params.f("rows").round().clamp(1.0, 31.0) as usize;
|
||
let cols = ctx.params.f("columns").round().clamp(1.0, 31.0) as usize;
|
||
let q = ctx.params.f("quality").round().clamp(1.0, 10.0) as usize;
|
||
let offs = parse_mesh(ctx.params.s("mesh"));
|
||
if offs.len() != (rows + 1) * (cols + 1) || offs.iter().all(|o| o.0 == 0.0 && o.1 == 0.0) {
|
||
return b;
|
||
}
|
||
let (lw, lh) = (ctx.layer_size[0], ctx.layer_size[1]);
|
||
let vert = |i: usize, j: usize| {
|
||
let o = offs[j * (cols + 1) + i];
|
||
b.to_px([i as f64 / cols as f64 * lw + o.0, j as f64 / rows as f64 * lh + o.1])
|
||
};
|
||
let (nx, ny) = (cols * q, rows * q);
|
||
let mut dest = Vec::with_capacity((nx + 1) * (ny + 1));
|
||
let mut srcp = Vec::with_capacity((nx + 1) * (ny + 1));
|
||
for gj in 0..=ny {
|
||
for gi in 0..=nx {
|
||
let (ci, cj) = ((gi / q).min(cols - 1), (gj / q).min(rows - 1));
|
||
let (u, v) = ((gi - ci * q) as f64 / q as f64, (gj - cj * q) as f64 / q as f64);
|
||
let (p00, p10, p01, p11) = (vert(ci, cj), vert(ci + 1, cj), vert(ci, cj + 1), vert(ci + 1, cj + 1));
|
||
let x = (p00.0 * (1.0 - u) + p10.0 * u) * (1.0 - v) + (p01.0 * (1.0 - u) + p11.0 * u) * v;
|
||
let y = (p00.1 * (1.0 - u) + p10.1 * u) * (1.0 - v) + (p01.1 * (1.0 - u) + p11.1 * u) * v;
|
||
dest.push((x, y));
|
||
srcp.push(b.to_px([gi as f64 / nx as f64 * lw, gj as f64 / ny as f64 * lh]));
|
||
}
|
||
}
|
||
b.img = grid_warp(&b.img, b.img.width, b.img.height, nx, ny, &dest, &srcp);
|
||
b
|
||
}
|
||
|
||
// ---------------------------------------------------------------- Bezier Warp
|
||
|
||
fn bez(p0: [f64; 2], p1: [f64; 2], p2: [f64; 2], p3: [f64; 2], t: f64) -> [f64; 2] {
|
||
let u = 1.0 - t;
|
||
let (a, bb, c, d) = (u * u * u, 3.0 * u * u * t, 3.0 * u * t * t, t * t * t);
|
||
[p0[0] * a + p1[0] * bb + p2[0] * c + p3[0] * d, p0[1] * a + p1[1] * bb + p2[1] * c + p3[1] * d]
|
||
}
|
||
|
||
fn bezier_warp(ctx: &EffectCtx, mut b: Buf) -> Buf {
|
||
let g = |id: &str| ctx.params.v2(id);
|
||
let (tl, tlt, trt, tr) = (g("topLeftVertex"), g("topLeftTangent"), g("topRightTangent"), g("rightTopVertex"));
|
||
let (rtt, rbt, br) = (g("rightTopTangent"), g("rightBottomTangent"), g("bottomRightVertex"));
|
||
let (brt, blt, bl) = (g("bottomRightTangent"), g("bottomLeftTangent"), g("leftBottomVertex"));
|
||
let (lbt, ltt) = (g("leftBottomTangent"), g("leftTopTangent"));
|
||
let n = (ctx.params.f("quality").round().clamp(1.0, 10.0) as usize) * 4;
|
||
let (lw, lh) = (ctx.layer_size[0], ctx.layer_size[1]);
|
||
let mut dest = Vec::with_capacity((n + 1) * (n + 1));
|
||
let mut srcp = Vec::with_capacity((n + 1) * (n + 1));
|
||
for j in 0..=n {
|
||
let v = j as f64 / n as f64;
|
||
let d0 = bez(tl, ltt, lbt, bl, v);
|
||
let d1 = bez(tr, rtt, rbt, br, v);
|
||
for i in 0..=n {
|
||
let u = i as f64 / n as f64;
|
||
let c0 = bez(tl, tlt, trt, tr, u);
|
||
let c1 = bez(bl, blt, brt, br, u);
|
||
let mut s = [0.0; 2];
|
||
for k in 0..2 {
|
||
let bil = (1.0 - u) * (1.0 - v) * tl[k] + u * (1.0 - v) * tr[k] + (1.0 - u) * v * bl[k] + u * v * br[k];
|
||
s[k] = (1.0 - v) * c0[k] + v * c1[k] + (1.0 - u) * d0[k] + u * d1[k] - bil;
|
||
}
|
||
dest.push(b.to_px(s));
|
||
srcp.push(b.to_px([u * lw, v * lh]));
|
||
}
|
||
}
|
||
b.img = grid_warp(&b.img, b.img.width, b.img.height, n, n, &dest, &srcp);
|
||
b
|
||
}
|
||
|
||
// ---------------------------------------------------------------- CC Bend It
|
||
|
||
fn bend_it(ctx: &EffectCtx, mut b: Buf) -> Buf {
|
||
let mut beta = ctx.params.f("bend").to_radians();
|
||
if beta.abs() < 1e-6 {
|
||
return b;
|
||
}
|
||
let s = b.to_px(ctx.params.v2("start"));
|
||
let e = b.to_px(ctx.params.v2("end"));
|
||
let static_pre = ctx.params.e("renderPrestart") == 1;
|
||
let l = ((e.0 - s.0).powi(2) + (e.1 - s.1).powi(2)).sqrt();
|
||
if l < 1e-6 {
|
||
return b;
|
||
}
|
||
let a = ((e.0 - s.0) / l, (e.1 - s.1) / l);
|
||
let mut n = (-a.1, a.0);
|
||
if beta < 0.0 {
|
||
beta = -beta;
|
||
n = (-n.0, -n.1);
|
||
}
|
||
let rho = l / beta;
|
||
let pe = (rho * beta.sin(), rho - rho * beta.cos());
|
||
let (t, nn) = ((beta.cos(), beta.sin()), (-beta.sin(), beta.cos()));
|
||
let src = b.img.clone();
|
||
let to_src = |u: f64, w: f64| (s.0 + a.0 * u + n.0 * w, s.1 + a.1 * u + n.1 * w);
|
||
map_xy(&mut b.img, |x, y, _| {
|
||
let (vx, vy) = (x as f64 + 0.5 - s.0, y as f64 + 0.5 - s.1);
|
||
let (lx, ly) = (vx * a.0 + vy * a.1, vx * n.0 + vy * n.1);
|
||
let mut cands: [Option<(f64, f64)>; 3] = [None; 3];
|
||
// Arc.
|
||
let (dx, dy) = (lx, ly - rho);
|
||
let th = dx.atan2(-dy);
|
||
if (0.0..=beta).contains(&th) {
|
||
cands[0] = Some((rho * th, rho - (dx * dx + dy * dy).sqrt()));
|
||
}
|
||
// Rigid continuation beyond the end.
|
||
let (ex, ey) = (lx - pe.0, ly - pe.1);
|
||
let u2 = ex * t.0 + ey * t.1;
|
||
if u2 >= 0.0 {
|
||
cands[1] = Some((l + u2, ex * nn.0 + ey * nn.1));
|
||
}
|
||
if lx < 0.0 && static_pre {
|
||
cands[2] = Some((lx, ly));
|
||
}
|
||
for (u, w) in cands.into_iter().flatten() {
|
||
let (sx, sy) = to_src(u, w);
|
||
let v = src.sample_bilinear(sx, sy);
|
||
if v[3] > 1e-4 {
|
||
return v;
|
||
}
|
||
}
|
||
[0.0; 4]
|
||
});
|
||
b
|
||
}
|
||
|
||
// ---------------------------------------------------------------- CC Lens
|
||
|
||
fn cc_lens(ctx: &EffectCtx, mut b: Buf) -> Buf {
|
||
let c = b.to_px(ctx.params.v2("center"));
|
||
let (_, _, lw, lh) = layer_rect(ctx, &b);
|
||
let r = (ctx.params.f("size") / 100.0 * lw.max(lh) * 0.5).max(0.5);
|
||
let k = 2f64.powf(ctx.params.f("convergence") / 50.0);
|
||
b.img = remap(&b.img, false, |x, y| {
|
||
let (dx, dy) = (x - c.0, y - c.1);
|
||
let d = (dx * dx + dy * dy).sqrt();
|
||
if d >= r {
|
||
return None;
|
||
}
|
||
if d < 1e-9 {
|
||
return Some((x, y));
|
||
}
|
||
let g = ((d / r).asin() / FRAC_PI_2).powf(k);
|
||
let s = r * g / d;
|
||
Some((c.0 + dx * s, c.1 + dy * s))
|
||
});
|
||
b
|
||
}
|
||
|
||
// ---------------------------------------------------------------- CC Page Turn
|
||
|
||
fn page_turn(ctx: &EffectCtx, mut b: Buf) -> Buf {
|
||
let f = b.to_px(ctx.params.v2("foldPosition"));
|
||
let phi = ctx.params.f("foldDirection").to_radians();
|
||
let r = (ctx.params.f("foldRadius") * b.scale).max(0.5);
|
||
let lam = ctx.params.f("lightDirection").to_radians();
|
||
let render = ctx.params.e("render");
|
||
let bo = (ctx.params.f("backPageOpacity") / 100.0) as f32;
|
||
let paper = ctx.params.color("paperColor");
|
||
// The fold travels along `fd`; the lifted part lies on the other side (toward `n`).
|
||
let n = (-phi.sin(), phi.cos());
|
||
let ldot = (n.0 * lam.sin() - n.1 * lam.cos()) as f32;
|
||
let (show_front, show_back) = (render != 1, render != 2);
|
||
let src = b.img.clone();
|
||
map_xy(&mut b.img, |x, y, _| {
|
||
let (px, py) = (x as f64 + 0.5, y as f64 + 0.5);
|
||
let d = (px - f.0) * n.0 + (py - f.1) * n.1;
|
||
if d > r {
|
||
return [0.0; 4];
|
||
}
|
||
// (u, back side?, shade) from the top layer down.
|
||
let mut cands: [Option<(f64, bool, f32)>; 4] = [None; 4];
|
||
if d <= 0.0 {
|
||
cands[0] = Some((PI * r - d, true, 0.9));
|
||
cands[3] = Some((d, false, 1.0));
|
||
} else {
|
||
let a = (d / r).clamp(-1.0, 1.0).asin();
|
||
cands[1] = Some((PI * r - r * a, true, 0.6 + 0.4 * a.cos() as f32));
|
||
let psi = (a / FRAC_PI_2) as f32;
|
||
cands[2] = Some((r * a, false, 1.0 - 0.4 * psi * (0.5 + 0.5 * ldot)));
|
||
}
|
||
for (u, back, shade) in cands.into_iter().flatten() {
|
||
if (back && !show_back) || (!back && !show_front) {
|
||
continue;
|
||
}
|
||
let k = u - d;
|
||
let v = src.sample_bilinear(px + k * n.0, py + k * n.1);
|
||
if v[3] <= 1e-4 {
|
||
continue;
|
||
}
|
||
let (c, a) = unpremul(v);
|
||
let c = if back { [0, 1, 2].map(|i| paper[i] * (1.0 - bo) + c[i] * bo) } else { c };
|
||
return premul(c.map(|v| v * shade), a);
|
||
}
|
||
[0.0; 4]
|
||
});
|
||
b
|
||
}
|
||
|
||
// ---------------------------------------------------------------- CC Tiler / CC Griddler
|
||
|
||
fn cc_tiler(ctx: &EffectCtx, mut b: Buf) -> Buf {
|
||
let s = (ctx.params.f("scale") / 100.0).max(0.001);
|
||
let c = b.to_px(ctx.params.v2("center"));
|
||
let blend = (ctx.params.f("blendWithOriginal") / 100.0) as f32;
|
||
let (x0, y0, w, h) = layer_rect(ctx, &b);
|
||
let (w, h) = (w.max(1.0), h.max(1.0));
|
||
let src = b.img.clone();
|
||
map_xy(&mut b.img, |x, y, px| {
|
||
let qx = c.0 + (x as f64 + 0.5 - c.0) / s;
|
||
let qy = c.1 + (y as f64 + 0.5 - c.1) / s;
|
||
let sx = x0 + (qx - x0).rem_euclid(w);
|
||
let sy = y0 + (qy - y0).rem_euclid(h);
|
||
let t: Px = src.sample_bilinear_clamped(sx, sy);
|
||
lerp4(t, px, blend)
|
||
});
|
||
b
|
||
}
|
||
|
||
fn cc_griddler(ctx: &EffectCtx, mut b: Buf) -> Buf {
|
||
let hs = (ctx.params.f("horizontalScale") / 100.0).max(0.001);
|
||
let vs = (ctx.params.f("verticalScale") / 100.0).max(0.001);
|
||
let rot = ctx.params.f("rotation").to_radians();
|
||
let cut = ctx.params.b("cutTiles");
|
||
let (x0, y0, lw, _) = layer_rect(ctx, &b);
|
||
let t = (ctx.params.f("tileSize") / 100.0 * lw).max(1.0);
|
||
if (hs - 1.0).abs() < 1e-9 && (vs - 1.0).abs() < 1e-9 && rot.abs() < 1e-9 && !cut {
|
||
return b;
|
||
}
|
||
let (sr, cr) = rot.sin_cos();
|
||
let src = b.img.clone();
|
||
map_xy(&mut b.img, |x, y, _| {
|
||
let (px, py) = (x as f64 + 0.5, y as f64 + 0.5);
|
||
let tcx = x0 + (((px - x0) / t).floor() + 0.5) * t;
|
||
let tcy = y0 + (((py - y0) / t).floor() + 0.5) * t;
|
||
let (lx, ly) = (px - tcx, py - tcy);
|
||
if cut && lx.abs().max(ly.abs()) > 0.45 * t {
|
||
return [0.0; 4];
|
||
}
|
||
let (rx, ry) = (lx * cr + ly * sr, -lx * sr + ly * cr);
|
||
src.sample_bilinear(tcx + rx / hs, tcy + ry / vs)
|
||
});
|
||
b
|
||
}
|
||
|
||
pub fn specs() -> Vec<EffectSpec> {
|
||
let srcs = || popup(&SRC_NAMES);
|
||
vec![
|
||
spec(
|
||
"ec.distort.turbulentdisplace",
|
||
"Turbulent Displace",
|
||
vec![
|
||
p("displacement", "Displacement", Value::Enum(0), popup(&TURBULENT_KINDS)),
|
||
p("amount", "Amount", num(50.0), slider(-1000.0, 1000.0, -200.0, 200.0, 1)),
|
||
p("size", "Size", num(100.0), slider(2.0, 4000.0, 2.0, 400.0, 1)),
|
||
p("offset", "Offset (Turbulence)", pt(0.5, 0.5), ParamUi::Point),
|
||
p("complexity", "Complexity", num(1.0), slider(1.0, 10.0, 1.0, 10.0, 1)),
|
||
p("evolution", "Evolution", num(0.0), ParamUi::Angle),
|
||
p("evolutionOptions/cycleEvolution", "Cycle Evolution", Value::Bool(false), ParamUi::Checkbox),
|
||
p("evolutionOptions/cycle", "Cycle (in Revolutions)", num(1.0), slider(1.0, 1000.0, 1.0, 20.0, 0)),
|
||
p("evolutionOptions/randomSeed", "Random Seed", num(0.0), slider(0.0, 10000.0, 0.0, 1000.0, 0)),
|
||
p("pinning", "Pinning", Value::Enum(1), popup(&TURBULENT_PINNING)),
|
||
p("resizeLayer", "Resize Layer", Value::Bool(false), ParamUi::Checkbox),
|
||
],
|
||
turbulent_displace,
|
||
),
|
||
spec(
|
||
"ec.distort.displacementmap",
|
||
"Displacement Map",
|
||
vec![
|
||
p("displacementMapLayer", "Displacement Map Layer", Value::Layer(None), ParamUi::Layer),
|
||
// The map layer is read without its masks or effects.
|
||
p("displacementMapLayerSource", "Displacement Map Layer Source", Value::Enum(0), ParamUi::Hidden),
|
||
p("useForHorizontal", "Use For Horizontal Displacement", Value::Enum(0), srcs()),
|
||
p("maxHorizontal", "Max Horizontal Displacement", num(5.0), slider(-32000.0, 32000.0, -100.0, 100.0, 1)),
|
||
p("useForVertical", "Use For Vertical Displacement", Value::Enum(1), srcs()),
|
||
p("maxVertical", "Max Vertical Displacement", num(5.0), slider(-32000.0, 32000.0, -100.0, 100.0, 1)),
|
||
p("displacementMapBehavior", "Displacement Map Behavior", Value::Enum(0), popup(&["Center Map", "Stretch Map to Fit", "Tile Map"])),
|
||
p("wrapPixelsAround", "Wrap Pixels Around", Value::Bool(false), ParamUi::Checkbox),
|
||
p("expandOutput", "Expand Output", Value::Bool(true), ParamUi::Checkbox),
|
||
],
|
||
displacement_map,
|
||
),
|
||
spec(
|
||
"ec.distort.opticscompensation",
|
||
"Optics Compensation",
|
||
vec![
|
||
p("fieldOfView", "Field Of View (FOV)", num(0.0), slider(0.0, 180.0, 0.0, 180.0, 1)),
|
||
p("reverseLensDistortion", "Reverse Lens Distortion", Value::Bool(false), ParamUi::Checkbox),
|
||
p("fovOrientation", "FOV Orientation", Value::Enum(0), popup(&["Horizontal", "Vertical", "Diagonal"])),
|
||
p("viewCenter", "View Center", pt(0.5, 0.5), ParamUi::Point),
|
||
p("resize", "Resize", Value::Enum(0), popup(&["Off", "Max 2X", "Max 4X", "Unlimited"])),
|
||
],
|
||
optics_compensation,
|
||
),
|
||
spec(
|
||
"ec.distort.magnify",
|
||
"Magnify",
|
||
vec![
|
||
p("shape", "Shape", Value::Enum(0), popup(&["Circle", "Square"])),
|
||
p("center", "Center", pt(0.5, 0.5), ParamUi::Point),
|
||
p("magnification", "Magnification", num(200.0), slider(1.0, 10000.0, 100.0, 600.0, 1)),
|
||
p("link", "Link", Value::Enum(0), popup(&["None", "Size To Magnification", "Size & Feather To Magnification"])),
|
||
p("size", "Size", num(100.0), slider(0.0, 4000.0, 0.0, 1000.0, 1)),
|
||
p("feather", "Feather", num(0.0), slider(0.0, 4000.0, 0.0, 200.0, 1)),
|
||
p("opacity", "Opacity", num(100.0), pct(100.0)),
|
||
p("scaling", "Scaling", Value::Enum(0), popup(&["Standard", "Soft", "Scatter"])),
|
||
p("blendingMode", "Blending Mode", Value::Enum(1), popup(&MAGNIFY_MODE_NAMES)),
|
||
p("resizeLayer", "Resize Layer", Value::Bool(false), ParamUi::Checkbox),
|
||
],
|
||
magnify,
|
||
),
|
||
spec(
|
||
"ec.distort.meshwarp",
|
||
"Mesh Warp",
|
||
vec![
|
||
p("rows", "Rows", num(7.0), slider(1.0, 31.0, 1.0, 31.0, 0)),
|
||
p("columns", "Columns", num(7.0), slider(1.0, 31.0, 1.0, 31.0, 0)),
|
||
p("quality", "Quality", num(8.0), slider(1.0, 10.0, 1.0, 10.0, 0)),
|
||
p("mesh", "Distortion Mesh", Value::Str(String::new()), ParamUi::Hidden),
|
||
],
|
||
mesh_warp,
|
||
),
|
||
spec(
|
||
"ec.distort.bezierwarp",
|
||
"Bezier Warp",
|
||
vec![
|
||
p("topLeftVertex", "Top Left Vertex", pt(0.0, 0.0), ParamUi::Point),
|
||
p("topLeftTangent", "Top Left Tangent", pt(1.0 / 3.0, 0.0), ParamUi::Point),
|
||
p("topRightTangent", "Top Right Tangent", pt(2.0 / 3.0, 0.0), ParamUi::Point),
|
||
p("rightTopVertex", "Right Top Vertex", pt(1.0, 0.0), ParamUi::Point),
|
||
p("rightTopTangent", "Right Top Tangent", pt(1.0, 1.0 / 3.0), ParamUi::Point),
|
||
p("rightBottomTangent", "Right Bottom Tangent", pt(1.0, 2.0 / 3.0), ParamUi::Point),
|
||
p("bottomRightVertex", "Bottom Right Vertex", pt(1.0, 1.0), ParamUi::Point),
|
||
p("bottomRightTangent", "Bottom Right Tangent", pt(2.0 / 3.0, 1.0), ParamUi::Point),
|
||
p("bottomLeftTangent", "Bottom Left Tangent", pt(1.0 / 3.0, 1.0), ParamUi::Point),
|
||
p("leftBottomVertex", "Left Bottom Vertex", pt(0.0, 1.0), ParamUi::Point),
|
||
p("leftBottomTangent", "Left Bottom Tangent", pt(0.0, 2.0 / 3.0), ParamUi::Point),
|
||
p("leftTopTangent", "Left Top Tangent", pt(0.0, 1.0 / 3.0), ParamUi::Point),
|
||
p("quality", "Quality", num(8.0), slider(1.0, 10.0, 1.0, 10.0, 0)),
|
||
],
|
||
bezier_warp,
|
||
),
|
||
spec(
|
||
"ec.distort.ccbendit",
|
||
"CC Bend It",
|
||
vec![
|
||
p("bend", "Bend", num(0.0), ParamUi::Angle),
|
||
p("start", "Start", pt(0.5, 0.2), ParamUi::Point),
|
||
p("end", "End", pt(0.5, 0.8), ParamUi::Point),
|
||
p("renderPrestart", "Render Prestart", Value::Enum(1), popup(&["None", "Static"])),
|
||
],
|
||
bend_it,
|
||
),
|
||
spec(
|
||
"ec.distort.cclens",
|
||
"CC Lens",
|
||
vec![
|
||
p("center", "Center", pt(0.5, 0.5), ParamUi::Point),
|
||
p("size", "Size", num(50.0), slider(0.0, 400.0, 0.0, 200.0, 1)),
|
||
p("convergence", "Convergence", num(0.0), slider(-100.0, 100.0, -100.0, 100.0, 1)),
|
||
],
|
||
cc_lens,
|
||
),
|
||
spec(
|
||
"ec.distort.ccpageturn",
|
||
"CC Page Turn",
|
||
vec![
|
||
p("foldPosition", "Fold Position", pt(0.7, 0.7), ParamUi::Point),
|
||
p("foldDirection", "Fold Direction", num(315.0), ParamUi::Angle),
|
||
p("foldRadius", "Fold Radius", num(20.0), slider(0.0, 1000.0, 0.0, 200.0, 1)),
|
||
p("lightDirection", "Light Direction", num(-45.0), ParamUi::Angle),
|
||
p("render", "Render", Value::Enum(0), popup(&["Front & Back Page", "Back Page", "Front Page"])),
|
||
p("backPageOpacity", "Back Opacity", num(75.0), pct(100.0)),
|
||
p("paperColor", "Paper Color", col(1.0, 1.0, 1.0), ParamUi::Color),
|
||
],
|
||
page_turn,
|
||
),
|
||
spec(
|
||
"ec.distort.cctiler",
|
||
"CC Tiler",
|
||
vec![
|
||
p("scale", "Scale", num(50.0), slider(1.0, 10000.0, 1.0, 200.0, 1)),
|
||
p("center", "Center", pt(0.5, 0.5), ParamUi::Point),
|
||
p("blendWithOriginal", "Blend w. Original", num(0.0), pct(100.0)),
|
||
],
|
||
cc_tiler,
|
||
),
|
||
spec(
|
||
"ec.distort.ccgriddler",
|
||
"CC Griddler",
|
||
vec![
|
||
p("horizontalScale", "Horizontal Scale", num(100.0), slider(1.0, 1000.0, 1.0, 400.0, 1)),
|
||
p("verticalScale", "Vertical Scale", num(100.0), slider(1.0, 1000.0, 1.0, 400.0, 1)),
|
||
p("tileSize", "Tile Size", num(10.0), slider(0.5, 100.0, 0.5, 100.0, 1)),
|
||
p("rotation", "Rotation", num(0.0), ParamUi::Angle),
|
||
p("cutTiles", "Cut Tiles", Value::Bool(false), ParamUi::Checkbox),
|
||
],
|
||
cc_griddler,
|
||
),
|
||
]
|
||
}
|
||
|
||
#[cfg(test)]
|
||
mod tests {
|
||
use super::*;
|
||
use crate::Params;
|
||
|
||
fn test_img(w: u32, h: u32) -> Image {
|
||
let mut img = Image::new(w, h);
|
||
for y in 0..h {
|
||
for x in 0..w {
|
||
let a = 1.0;
|
||
img.set(x, y, [x as f32 / w as f32 * a, y as f32 / h as f32 * a, ((x * 7 + y * 3) % 11) as f32 / 11.0 * a, a]);
|
||
}
|
||
}
|
||
img
|
||
}
|
||
|
||
fn run(id: &str, set: &[(&str, Value)], img: Image) -> Buf {
|
||
let s = crate::find(id).unwrap();
|
||
let mut params = Params { values: s.params.iter().map(|p| (p.id.to_string(), p.default.clone())).collect() };
|
||
let ls = [img.width as f64, img.height as f64];
|
||
for p in &s.params {
|
||
if let (ParamUi::Point, Value::Vec2(v)) = (&p.ui, &p.default) {
|
||
params.values.insert(p.id.to_string(), Value::Vec2([v[0] * ls[0], v[1] * ls[1]]));
|
||
}
|
||
}
|
||
for (k, v) in set {
|
||
params.values.insert(k.to_string(), v.clone());
|
||
}
|
||
let ctx = EffectCtx { params: ¶ms, time: 0.5, layer_size: ls, seed: 3, adjustment: false, env: Default::default() };
|
||
crate::apply(s, &ctx, Buf { img, offset: [0.0, 0.0], scale: 1.0 })
|
||
}
|
||
|
||
fn max_diff(a: &Image, b: &Image) -> f32 {
|
||
a.data.iter().zip(&b.data).map(|(p, q)| (0..4).map(|i| (p[i] - q[i]).abs()).fold(0.0, f32::max)).fold(0.0, f32::max)
|
||
}
|
||
|
||
#[test]
|
||
fn displacement_map_half_is_identity() {
|
||
let img = test_img(32, 24);
|
||
let o = run(
|
||
"ec.distort.displacementmap",
|
||
&[("useForHorizontal", Value::Enum(9)), ("useForVertical", Value::Enum(9)), ("expandOutput", Value::Bool(false))],
|
||
img.clone(),
|
||
);
|
||
assert_eq!(o.img, img);
|
||
let o = run(
|
||
"ec.distort.displacementmap",
|
||
&[("useForHorizontal", Value::Enum(8)), ("useForVertical", Value::Enum(10)), ("maxHorizontal", num(2.0)), ("expandOutput", Value::Bool(false))],
|
||
img.clone(),
|
||
);
|
||
// Full → +2 px horizontally: pixel x shows source x + 2; Off → no vertical displacement.
|
||
let (a, b2) = (o.img.get(5, 5), img.get(7, 5));
|
||
assert!((0..4).all(|i| (a[i] - b2[i]).abs() < 1e-5), "{a:?} vs {b2:?}");
|
||
}
|
||
|
||
#[test]
|
||
fn mesh_warp_empty_and_zero_are_identity() {
|
||
let img = test_img(32, 24);
|
||
assert_eq!(run("ec.distort.meshwarp", &[], img.clone()).img, img);
|
||
let zeros = vec!["0,0"; 64].join(" ");
|
||
assert_eq!(run("ec.distort.meshwarp", &[("mesh", Value::Str(zeros))], img.clone()).img, img);
|
||
}
|
||
|
||
#[test]
|
||
fn mesh_warp_uniform_offset_translates() {
|
||
let img = test_img(32, 24);
|
||
let shift = vec!["4,0"; 64].join(" ");
|
||
let o = run("ec.distort.meshwarp", &[("mesh", Value::Str(shift))], img.clone());
|
||
for y in 2..22 {
|
||
for x in 6..30 {
|
||
let (a, b2) = (o.img.get(x, y), img.get(x - 4, y));
|
||
assert!((0..4).all(|i| (a[i] - b2[i]).abs() < 1e-4), "({x},{y}) {a:?} vs {b2:?}");
|
||
}
|
||
}
|
||
assert!(o.img.get(1, 10)[3] < 1e-6);
|
||
}
|
||
|
||
#[test]
|
||
fn bezier_warp_default_is_identity() {
|
||
let img = test_img(32, 24);
|
||
let o = run("ec.distort.bezierwarp", &[], img.clone());
|
||
assert!(max_diff(&o.img, &img) < 1e-3, "{}", max_diff(&o.img, &img));
|
||
}
|
||
|
||
#[test]
|
||
fn optics_fov_zero_is_identity_and_nonzero_warps() {
|
||
let img = test_img(32, 24);
|
||
assert_eq!(run("ec.distort.opticscompensation", &[], img.clone()).img, img);
|
||
assert!(max_diff(&run("ec.distort.opticscompensation", &[("fieldOfView", num(90.0))], img.clone()).img, &img) > 1e-3);
|
||
}
|
||
|
||
#[test]
|
||
fn bend_it_zero_is_identity() {
|
||
let img = test_img(32, 24);
|
||
assert_eq!(run("ec.distort.ccbendit", &[], img.clone()).img, img);
|
||
assert!(max_diff(&run("ec.distort.ccbendit", &[("bend", num(45.0))], img.clone()).img, &img) > 1e-3);
|
||
}
|
||
|
||
#[test]
|
||
fn magnify_100_is_identity() {
|
||
let img = test_img(32, 24);
|
||
assert!(max_diff(&run("ec.distort.magnify", &[("magnification", num(100.0))], img.clone()).img, &img) < 1e-6);
|
||
let o = run("ec.distort.magnify", &[("size", num(8.0))], img.clone());
|
||
// Centre pixel unchanged; slightly off-centre samples move toward the centre.
|
||
assert!(max_diff(&o.img, &img) > 1e-3);
|
||
assert_eq!(o.img.get(0, 0), img.get(0, 0));
|
||
}
|
||
|
||
#[test]
|
||
fn tiler_100_is_identity_and_50_tiles() {
|
||
let img = test_img(32, 24);
|
||
assert!(max_diff(&run("ec.distort.cctiler", &[("scale", num(100.0))], img.clone()).img, &img) < 1e-6);
|
||
let o = run("ec.distort.cctiler", &[], img.clone());
|
||
assert!(o.img.data.iter().all(|p| p[3] > 0.99));
|
||
}
|
||
|
||
#[test]
|
||
fn turbulent_displace_zero_amount_is_identity() {
|
||
let img = test_img(32, 24);
|
||
assert_eq!(run("ec.distort.turbulentdisplace", &[("amount", num(0.0))], img.clone()).img, img);
|
||
assert!(max_diff(&run("ec.distort.turbulentdisplace", &[], img.clone()).img, &img) > 1e-3);
|
||
}
|
||
|
||
#[test]
|
||
fn griddler_default_is_identity() {
|
||
let img = test_img(32, 24);
|
||
assert_eq!(run("ec.distort.ccgriddler", &[], img.clone()).img, img);
|
||
}
|
||
|
||
#[test]
|
||
fn page_turn_removes_corner_and_keeps_far_side() {
|
||
let img = test_img(64, 64);
|
||
let o = run("ec.distort.ccpageturn", &[("foldRadius", num(4.0))], img.clone());
|
||
// Bottom-right corner is lifted away; top-left untouched.
|
||
assert!(o.img.get(63, 63)[3] < 1e-6);
|
||
assert_eq!(o.img.get(2, 2), img.get(2, 2));
|
||
}
|
||
|
||
#[test]
|
||
fn cc_lens_clears_outside() {
|
||
let img = test_img(32, 32);
|
||
let o = run("ec.distort.cclens", &[], img);
|
||
assert!(o.img.get(0, 0)[3] < 1e-6);
|
||
assert!(o.img.get(16, 16)[3] > 0.99);
|
||
}
|
||
|
||
/// A host whose every layer is a flat 8×8 image of `px`.
|
||
struct FlatHost([f32; 4]);
|
||
impl crate::EffectHost for FlatHost {
|
||
fn layer(&self, _: u64, _: bool) -> Option<crate::LayerPixels> {
|
||
Some(crate::LayerPixels { buf: Buf { img: Image::filled(8, 8, self.0), offset: [0.0; 2], scale: 1.0 }, size: [8.0, 8.0] })
|
||
}
|
||
fn audio(&self, _: u64, _: f64, _: usize, _: u32) -> Option<Vec<f32>> {
|
||
None
|
||
}
|
||
}
|
||
|
||
#[test]
|
||
fn displacement_map_reads_the_map_layer_and_expands_output() {
|
||
let img = test_img(32, 24);
|
||
// A white map layer → Red = 1 → full positive horizontal displacement everywhere.
|
||
let host = FlatHost([1.0, 1.0, 1.0, 1.0]);
|
||
let s = crate::find("ec.distort.displacementmap").unwrap();
|
||
let mut params = Params { values: s.params.iter().map(|p| (p.id.to_string(), p.default.clone())).collect() };
|
||
for (k, v) in [
|
||
("displacementMapLayer", Value::Layer(Some(7))),
|
||
("useForVertical", Value::Enum(10)),
|
||
("maxHorizontal", num(3.0)),
|
||
("expandOutput", Value::Bool(false)),
|
||
] {
|
||
params.values.insert(k.into(), v);
|
||
}
|
||
for behavior in 0..3 {
|
||
params.values.insert("displacementMapBehavior".into(), Value::Enum(behavior));
|
||
let env = crate::EffectEnv { host: Some(&host), ..Default::default() };
|
||
let ctx = EffectCtx { params: ¶ms, time: 0.0, layer_size: [32.0, 24.0], seed: 1, adjustment: false, env };
|
||
let o = crate::apply(s, &ctx, Buf { img: img.clone(), offset: [0.0, 0.0], scale: 1.0 });
|
||
if behavior == 0 {
|
||
// Center Map: the 8×8 map covers only the middle; outside it nothing moves.
|
||
assert_eq!(o.img.get(2, 2), img.get(2, 2));
|
||
assert!((o.img.get(16, 12)[0] - img.get(19, 12)[0]).abs() < 1e-5);
|
||
} else {
|
||
// Stretched or tiled, the map covers the whole layer.
|
||
assert!(
|
||
(o.img.get(2, 2)[0] - img.get(5, 2)[0]).abs() < 1e-5,
|
||
"behavior {behavior}: {:?} {:?} {:?}",
|
||
o.img.get(2, 2),
|
||
img.get(5, 2),
|
||
img.get(2, 2)
|
||
);
|
||
}
|
||
}
|
||
// Expand Output grows the layer by the maximum displacement.
|
||
let o = run("ec.distort.displacementmap", &[("maxHorizontal", num(6.0))], img.clone());
|
||
assert_eq!((o.img.width, o.img.height), (32 + 12, 24 + 12));
|
||
}
|
||
|
||
#[test]
|
||
fn turbulent_displace_pinning_and_cycle_evolution() {
|
||
let img = test_img(40, 40);
|
||
let pinned = run("ec.distort.turbulentdisplace", &[("size", num(10.0))], img.clone());
|
||
let free = run("ec.distort.turbulentdisplace", &[("size", num(10.0)), ("pinning", Value::Enum(0))], img.clone());
|
||
// Pin All (the default) holds the border.
|
||
for i in 0..40 {
|
||
assert_eq!(pinned.img.get(i, 0), img.get(i, 0));
|
||
assert_eq!(pinned.img.get(0, i), img.get(0, i));
|
||
}
|
||
assert!(max_diff(&pinned.img, &free.img) > 1e-3);
|
||
// Cycle Evolution: evolution 0 and `cycle` revolutions render the same frame.
|
||
let cyc = |evo: f64| {
|
||
run(
|
||
"ec.distort.turbulentdisplace",
|
||
&[("evolution", num(evo)), ("evolutionOptions/cycleEvolution", Value::Bool(true)), ("evolutionOptions/cycle", num(2.0))],
|
||
img.clone(),
|
||
)
|
||
.img
|
||
};
|
||
assert!(max_diff(&cyc(0.0), &cyc(720.0)) < 1e-4);
|
||
assert!(max_diff(&cyc(0.0), &cyc(200.0)) > 1e-3);
|
||
// Random Seed changes the pattern.
|
||
let seeded = run("ec.distort.turbulentdisplace", &[("size", num(10.0)), ("evolutionOptions/randomSeed", num(5.0))], img);
|
||
assert!(max_diff(&seeded.img, &pinned.img) > 1e-3);
|
||
}
|
||
|
||
#[test]
|
||
fn magnify_blending_none_and_scaling() {
|
||
let img = test_img(32, 32);
|
||
let none = run("ec.distort.magnify", &[("size", num(6.0)), ("blendingMode", Value::Enum(0))], img.clone());
|
||
assert!(none.img.get(0, 0)[3] < 1e-6);
|
||
assert!(none.img.get(16, 16)[3] > 0.99);
|
||
let std = run("ec.distort.magnify", &[("magnification", num(400.0))], img.clone());
|
||
let soft = run("ec.distort.magnify", &[("magnification", num(400.0)), ("scaling", Value::Enum(1))], img.clone());
|
||
assert!(max_diff(&std.img, &soft.img) > 1e-3);
|
||
let add = run("ec.distort.magnify", &[("blendingMode", Value::Enum(2))], img);
|
||
assert!(max_diff(&add.img, &std.img) > 1e-3);
|
||
}
|
||
|
||
#[test]
|
||
fn optics_resize_grows_reversed_layers() {
|
||
let img = test_img(32, 24);
|
||
let set = |r: u32| vec![("fieldOfView", num(120.0)), ("reverseLensDistortion", Value::Bool(true)), ("resize", Value::Enum(r))];
|
||
let off = run("ec.distort.opticscompensation", &set(0), img.clone());
|
||
assert_eq!(off.img.width, 32);
|
||
let max2 = run("ec.distort.opticscompensation", &set(1), img.clone());
|
||
assert!(max2.img.width > 32 && max2.img.width <= 32 + 2 * 16 + 2, "{}", max2.img.width);
|
||
let max4 = run("ec.distort.opticscompensation", &set(2), img);
|
||
assert!(max4.img.width >= max2.img.width);
|
||
}
|
||
}
|