mirror of
https://github.com/storytold/effectcraft.git
synced 2026-10-26 02:57:34 -06:00
968 lines
39 KiB
Rust
968 lines
39 KiB
Rust
//! Blur & Sharpen, batch 2: edge-preserving blurs (Bilateral, Smart), per-channel and map-driven
|
||
//! blurs (Channel, Compound, CC Vector), bokeh (Camera Lens Blur), radial streaks and small
|
||
//! utilities (Reduce Interlace Flicker, CC Cross Blur).
|
||
|
||
use effectcraft_color::luminance;
|
||
use effectcraft_keyframe::Value;
|
||
use effectcraft_project::ParamUi;
|
||
use effectcraft_raster::{Image, Px, gaussian_blur};
|
||
use rayon::prelude::*;
|
||
|
||
use crate::util::{Plane, gauss_plane, premul, unpremul};
|
||
use crate::{Buf, EffectCtx, EffectSpec, num, p, popup, slider};
|
||
|
||
fn spec(id: &'static str, name: &'static str, params: Vec<crate::ParamSpec>, render: crate::RenderFn) -> EffectSpec {
|
||
EffectSpec { id, name, category: "Blur & Sharpen", params, render, gpu: false, float: true }
|
||
}
|
||
|
||
#[inline]
|
||
fn plum(p: Px) -> f32 {
|
||
luminance(p[0], p[1], p[2])
|
||
}
|
||
|
||
// ---------------------------------------------------------------------------------------------
|
||
// Bilateral / Smart Blur
|
||
|
||
/// Edge-preserving neighbourhood average. `hard` = box spatial kernel + hard luminance range test
|
||
/// (Smart Blur); otherwise Gaussian spatial × Gaussian range weights (bilateral filter, Tomasi &
|
||
/// Manduchi 1998). Large radii are sampled on a stride to stay fast.
|
||
fn edge_preserving(src: &Image, r: f64, range: f32, hard: bool) -> Image {
|
||
let ri = r.ceil() as i64;
|
||
let stride = ((r / 6.0).ceil() as i64).max(1);
|
||
let sig_s2 = 2.0 * (r / 2.0).max(0.5).powi(2);
|
||
let sig_r2 = 2.0 * range.max(1e-3).powi(2);
|
||
let mut out = Image::new(src.width, src.height);
|
||
out.rows_mut().for_each(|(y, row)| {
|
||
for (x, o) in row.iter_mut().enumerate() {
|
||
let c = src.data[y * src.width as usize + x];
|
||
let cl = plum(c);
|
||
let mut acc = [0.0f32; 4];
|
||
let mut wsum = 0.0f32;
|
||
let mut dy = -ri;
|
||
while dy <= ri {
|
||
let mut dx = -ri;
|
||
while dx <= ri {
|
||
let d2 = (dx * dx + dy * dy) as f64;
|
||
if d2 <= r * r + 0.5 {
|
||
let q = src.get_clamped(x as i64 + dx, y as i64 + dy);
|
||
let w = if hard {
|
||
if (plum(q) - cl).abs() <= range { 1.0 } else { 0.0 }
|
||
} else {
|
||
let dc = (q[0] - c[0]).powi(2) + (q[1] - c[1]).powi(2) + (q[2] - c[2]).powi(2) + (q[3] - c[3]).powi(2);
|
||
((-d2 / sig_s2) as f32 - dc / sig_r2).exp()
|
||
};
|
||
if w > 0.0 {
|
||
for k in 0..4 {
|
||
acc[k] += q[k] * w;
|
||
}
|
||
wsum += w;
|
||
}
|
||
}
|
||
dx += stride;
|
||
}
|
||
dy += stride;
|
||
}
|
||
*o = if wsum > 0.0 { acc.map(|v| v / wsum) } else { c };
|
||
}
|
||
});
|
||
out
|
||
}
|
||
|
||
fn bilateral(ctx: &EffectCtx, mut b: Buf) -> Buf {
|
||
let r = ctx.params.f("radius") * b.scale;
|
||
if r < 0.5 {
|
||
return b;
|
||
}
|
||
let range = ctx.params.f("threshold") as f32 / 100.0;
|
||
b.img = edge_preserving(&b.img, r, range, false);
|
||
if !ctx.params.b("colorize") {
|
||
b.img.map_straight(|c| {
|
||
let l = luminance(c[0], c[1], c[2]);
|
||
[l, l, l]
|
||
});
|
||
}
|
||
b
|
||
}
|
||
|
||
/// Sobel gradient magnitude of premultiplied luminance.
|
||
fn sobel(img: &Image) -> Plane {
|
||
let l = Plane::from_image(img, plum);
|
||
let mut out = Plane::new(l.w, l.h);
|
||
if l.w == 0 {
|
||
return out;
|
||
}
|
||
out.data.par_chunks_mut(l.w).enumerate().for_each(|(y, row)| {
|
||
let y = y as i64;
|
||
for (x, o) in row.iter_mut().enumerate() {
|
||
let x = x as i64;
|
||
let g = |dx: i64, dy: i64| l.get_clamped(x + dx, y + dy);
|
||
let gx = g(1, -1) + 2.0 * g(1, 0) + g(1, 1) - g(-1, -1) - 2.0 * g(-1, 0) - g(-1, 1);
|
||
let gy = g(-1, 1) + 2.0 * g(0, 1) + g(1, 1) - g(-1, -1) - 2.0 * g(0, -1) - g(1, -1);
|
||
*o = (gx * gx + gy * gy).sqrt() * 0.25;
|
||
}
|
||
});
|
||
out
|
||
}
|
||
|
||
fn smart_blur(ctx: &EffectCtx, mut b: Buf) -> Buf {
|
||
let r = ctx.params.f("radius") * b.scale;
|
||
let thr = ctx.params.f("threshold") as f32 / 100.0;
|
||
let mode = ctx.params.e("mode");
|
||
let blurred = if r >= 0.5 { edge_preserving(&b.img, r, thr, true) } else { b.img.clone() };
|
||
if mode == 0 {
|
||
b.img = blurred;
|
||
return b;
|
||
}
|
||
let edges = sobel(&blurred);
|
||
let et = thr.max(0.01) * 0.5;
|
||
let mut out = blurred;
|
||
out.data.par_iter_mut().zip(edges.data.par_iter()).for_each(|(px, &e)| {
|
||
let a = px[3];
|
||
let on = e > et;
|
||
if mode == 1 {
|
||
let v = if on { 1.0 } else { 0.0 };
|
||
*px = premul([v; 3], a);
|
||
} else if on {
|
||
*px = premul([1.0; 3], a);
|
||
}
|
||
});
|
||
b.img = out;
|
||
b
|
||
}
|
||
|
||
// ---------------------------------------------------------------------------------------------
|
||
// Channel Blur
|
||
|
||
fn dims_xy(dim: u32) -> (f64, f64) {
|
||
match dim {
|
||
1 => (1.0, 0.0),
|
||
2 => (0.0, 1.0),
|
||
_ => (1.0, 1.0),
|
||
}
|
||
}
|
||
|
||
fn channel_blur(ctx: &EffectCtx, mut b: Buf) -> Buf {
|
||
let sig = ["redBlurriness", "greenBlurriness", "blueBlurriness", "alphaBlurriness"].map(|id| ctx.params.f(id).max(0.0) * 0.5 * b.scale);
|
||
if sig.iter().all(|&s| s < 0.05) {
|
||
return b;
|
||
}
|
||
let (kx, ky) = dims_xy(ctx.params.e("dimensions"));
|
||
let repeat = ctx.params.b("repeatEdge") || ctx.adjustment;
|
||
let maxs = sig.iter().cloned().fold(0.0, f64::max);
|
||
if !repeat {
|
||
b.pad((maxs * 3.0).ceil() as u32 + 1);
|
||
}
|
||
// The buffer is padded with transparency (or edges repeat), so an edge-clamped plane blur fits both.
|
||
let blur = |pl: &Plane, s: f64| -> Plane { gauss_plane(pl, s * kx, s * ky) };
|
||
let chans: [Plane; 4] = crate::util::split(&b.img);
|
||
let alpha = &chans[3];
|
||
let mut alpha_cache: Vec<(u64, Plane)> = Vec::new();
|
||
let mut get_alpha = |s: f64| -> Plane {
|
||
let key = s.to_bits();
|
||
if let Some((_, pl)) = alpha_cache.iter().find(|(k, _)| *k == key) {
|
||
return pl.clone();
|
||
}
|
||
let pl = if s < 0.05 { alpha.clone() } else { blur(alpha, s) };
|
||
alpha_cache.push((key, pl.clone()));
|
||
pl
|
||
};
|
||
let new_a = get_alpha(sig[3]);
|
||
let mut straight: Vec<Plane> = Vec::with_capacity(3);
|
||
for c in 0..3 {
|
||
if sig[c] < 0.05 {
|
||
straight.push(chans[c].zip_map(alpha, |v, a| if a > 1e-6 { v / a } else { 0.0 }));
|
||
} else {
|
||
let bc = blur(&chans[c], sig[c]);
|
||
let ba = get_alpha(sig[c]);
|
||
straight.push(bc.zip_map(&ba, |v, a| if a > 1e-6 { v / a } else { 0.0 }));
|
||
}
|
||
}
|
||
b.img.data.par_iter_mut().enumerate().for_each(|(i, px)| {
|
||
let a = new_a.data[i].clamp(0.0, 1.0);
|
||
*px = premul([straight[0].data[i], straight[1].data[i], straight[2].data[i]], a);
|
||
});
|
||
b
|
||
}
|
||
|
||
// ---------------------------------------------------------------------------------------------
|
||
// Compound Blur
|
||
|
||
fn compound_blur(ctx: &EffectCtx, mut b: Buf) -> Buf {
|
||
let max_sigma = ctx.params.f("maximumBlur").max(0.0) * 0.5 * b.scale;
|
||
if max_sigma < 0.05 {
|
||
return b;
|
||
}
|
||
let invert = ctx.params.b("invertBlur");
|
||
if !ctx.adjustment {
|
||
b.pad((max_sigma * 3.0).ceil() as u32 + 1);
|
||
}
|
||
// The blur map: the Blur Layer's luminance (stretched to fit or centred), else the layer's own.
|
||
let map_img = crate::util::layer_or_self(ctx, &b, "blurLayer", true, ctx.params.b("stretchMapToFit"));
|
||
let map = Plane::from_image(&map_img, |p| {
|
||
let (c, a) = unpremul(p);
|
||
let l = luminance(c[0], c[1], c[2]) * a;
|
||
if invert { 1.0 - l } else { l }
|
||
});
|
||
const N: usize = 5;
|
||
let levels: Vec<Image> = (0..N)
|
||
.into_par_iter()
|
||
.map(|k| {
|
||
if k == 0 {
|
||
b.img.clone()
|
||
} else {
|
||
gaussian_blur(&b.img, max_sigma * k as f64 / (N - 1) as f64, max_sigma * k as f64 / (N - 1) as f64, ctx.adjustment)
|
||
}
|
||
})
|
||
.collect();
|
||
b.img.data.par_iter_mut().enumerate().for_each(|(i, px)| {
|
||
let t = map.data[i].clamp(0.0, 1.0) * (N - 1) as f32;
|
||
let k = (t.floor() as usize).min(N - 2);
|
||
let f = t - k as f32;
|
||
let a = levels[k].data[i];
|
||
let c = levels[k + 1].data[i];
|
||
*px = crate::util::lerp4(a, c, f);
|
||
});
|
||
b
|
||
}
|
||
|
||
// ---------------------------------------------------------------------------------------------
|
||
// Camera Lens Blur
|
||
|
||
/// Half-open horizontal spans (dy, x_left, x_right) of a polygonal / round iris of radius `r`.
|
||
fn iris_spans(r: f64, sides: u32, rotation_deg: f64, roundness: f64) -> Vec<(i64, i64, i64)> {
|
||
let ri = r.ceil() as i64;
|
||
let rot = rotation_deg.to_radians();
|
||
let verts: Vec<(f64, f64)> = (0..sides)
|
||
.map(|k| {
|
||
let a = rot - std::f64::consts::FRAC_PI_2 + k as f64 * std::f64::consts::TAU / sides.max(3) as f64;
|
||
(r * a.cos(), r * a.sin())
|
||
})
|
||
.collect();
|
||
let mut spans = Vec::new();
|
||
for dy in -ri..=ri {
|
||
let y = dy as f64;
|
||
let circ = if y.abs() <= r { Some((r * r - y * y).max(0.0).sqrt()) } else { None };
|
||
let poly = if sides >= 3 {
|
||
let mut lo = f64::INFINITY;
|
||
let mut hi = f64::NEG_INFINITY;
|
||
for k in 0..verts.len() {
|
||
let (x0, y0) = verts[k];
|
||
let (x1, y1) = verts[(k + 1) % verts.len()];
|
||
if (y0 - y) * (y1 - y) <= 0.0 && (y1 - y0).abs() > 1e-12 {
|
||
let x = x0 + (y - y0) / (y1 - y0) * (x1 - x0);
|
||
lo = lo.min(x);
|
||
hi = hi.max(x);
|
||
}
|
||
}
|
||
if lo <= hi { Some((lo, hi)) } else { None }
|
||
} else {
|
||
circ.map(|c| (-c, c))
|
||
};
|
||
let span = match (poly, circ) {
|
||
(Some((lo, hi)), Some(c)) => Some((lo + (-c - lo) * roundness, hi + (c - hi) * roundness)),
|
||
(Some(s), None) => Some(s),
|
||
(None, Some(c)) if roundness > 0.5 => Some((-c, c)),
|
||
_ => None,
|
||
};
|
||
if let Some((lo, hi)) = span {
|
||
let (l, h) = (lo.round() as i64, hi.round() as i64);
|
||
if l <= h {
|
||
spans.push((dy, l, h));
|
||
}
|
||
}
|
||
}
|
||
if spans.is_empty() {
|
||
spans.push((0, 0, 0));
|
||
}
|
||
spans
|
||
}
|
||
|
||
/// Camera Lens Blur's iris spans at blur radius `r` pixels (shape, rotation, roundness and
|
||
/// aspect ratio applied) and the iris' horizontal reach (shared with the GPU kernel).
|
||
pub fn camera_lens_spans(ctx: &EffectCtx, r: f64) -> (Vec<(i64, i64, i64)>, f64) {
|
||
let shape = ctx.params.e("irisProperties/irisShape");
|
||
let sides = if shape >= 8 { 0 } else { shape + 3 };
|
||
let roundness = ctx.params.f("irisProperties/irisRoundness").clamp(0.0, 100.0) / 100.0;
|
||
// Aspect Ratio stretches the iris horizontally (> 1) or squeezes it (< 1).
|
||
let aspect = ctx.params.f("irisProperties/irisAspectRatio");
|
||
let mut spans = iris_spans(r, sides, ctx.params.f("irisProperties/irisRotation"), roundness);
|
||
if aspect > 0.0 && (aspect - 1.0).abs() > 1e-6 {
|
||
for s in spans.iter_mut() {
|
||
s.1 = (s.1 as f64 * aspect).round() as i64;
|
||
s.2 = (s.2 as f64 * aspect).round() as i64;
|
||
}
|
||
}
|
||
(spans, r * aspect.max(1.0))
|
||
}
|
||
|
||
/// Whether Camera Lens Blur needs more than plain iris spans (Diffraction Fringe or a Blur
|
||
/// Map): the GPU kernel handles the plain blur only.
|
||
pub fn camera_lens_plain(ctx: &EffectCtx) -> bool {
|
||
ctx.params.f("irisProperties/diffractionFringe") <= 0.0 && ctx.params.get("blurMap/blurMapLayer").and_then(|v| v.as_layer()).is_none()
|
||
}
|
||
|
||
fn camera_lens_blur(ctx: &EffectCtx, mut b: Buf) -> Buf {
|
||
let r = ctx.params.f("blurRadius").max(0.0) * b.scale;
|
||
if r < 0.5 {
|
||
return b;
|
||
}
|
||
let shape = ctx.params.e("irisProperties/irisShape");
|
||
let sides = if shape >= 8 { 0 } else { shape + 3 };
|
||
let roundness = ctx.params.f("irisProperties/irisRoundness").clamp(0.0, 100.0) / 100.0;
|
||
// Aspect Ratio stretches the iris horizontally (> 1) or squeezes it (< 1).
|
||
let aspect = ctx.params.f("irisProperties/irisAspectRatio");
|
||
let reach = r * aspect.max(1.0);
|
||
let repeat = ctx.params.b("repeatEdge") || ctx.adjustment;
|
||
if !repeat {
|
||
b.pad(reach.ceil() as u32 + 1);
|
||
}
|
||
let linear = ctx.params.b("useLinear");
|
||
let to_lin = |px: &mut [f32; 4]| {
|
||
let (c, a) = unpremul(*px);
|
||
*px = premul(c.map(effectcraft_color::srgb_to_linear), a);
|
||
};
|
||
// Highlights: boost bright pixels before the blur so they bloom into bokeh shapes; Saturation
|
||
// keeps their colour (100) or pushes the added energy towards white (0).
|
||
let thr = ctx.params.f("highlight/specularThreshold") as f32 / 255.0;
|
||
let boost = 1.0 + ctx.params.f("highlight/specularBrightness").max(0.0) as f32 / 100.0 * 4.0;
|
||
let sat = (ctx.params.f("highlight/highlightSaturation") / 100.0).clamp(0.0, 1.0) as f32;
|
||
let mut src = b.img.clone();
|
||
if linear {
|
||
src.data.par_iter_mut().for_each(to_lin);
|
||
}
|
||
if boost > 1.0 {
|
||
src.data.par_iter_mut().for_each(|px| {
|
||
let (c, a) = unpremul(*px);
|
||
if a > 0.0 && luminance(c[0], c[1], c[2]) >= thr {
|
||
let m = c[0].max(c[1]).max(c[2]);
|
||
for k in 0..3 {
|
||
let tinted = c[k] + (m - c[k]) * (1.0 - sat);
|
||
px[k] = (c[k] + tinted * (boost - 1.0)) * a;
|
||
}
|
||
}
|
||
});
|
||
}
|
||
let (w, h) = (src.width as usize, src.height as usize);
|
||
// Per-row prefix sums (f64 for precision).
|
||
let prefix: Vec<Vec<[f64; 4]>> = src
|
||
.data
|
||
.par_chunks(w.max(1))
|
||
.map(|row| {
|
||
let mut v = Vec::with_capacity(w + 1);
|
||
let mut acc = [0.0f64; 4];
|
||
v.push(acc);
|
||
for px in row {
|
||
for c in 0..4 {
|
||
acc[c] += px[c] as f64;
|
||
}
|
||
v.push(acc);
|
||
}
|
||
v
|
||
})
|
||
.collect();
|
||
// Sum of the source over the iris spans centred on (x, y).
|
||
let span_sum = |spans: &[(i64, i64, i64)], x: usize, y: usize| -> [f64; 4] {
|
||
let mut acc = [0.0f64; 4];
|
||
for &(dy, l, rr) in spans {
|
||
let mut yy = y as i64 + dy;
|
||
if yy < 0 || yy >= h as i64 {
|
||
if !repeat {
|
||
continue;
|
||
}
|
||
yy = yy.clamp(0, h as i64 - 1);
|
||
}
|
||
let pre = &prefix[yy as usize];
|
||
let (mut a, mut bnd) = (x as i64 + l, x as i64 + rr);
|
||
if repeat {
|
||
if a < 0 {
|
||
let n = (-a).min(bnd - a + 1) as f64;
|
||
let first = src.data[yy as usize * w];
|
||
for c in 0..4 {
|
||
acc[c] += first[c] as f64 * n;
|
||
}
|
||
a = 0;
|
||
}
|
||
if bnd >= w as i64 {
|
||
let n = (bnd - (w as i64 - 1)).min(bnd - a + 1) as f64;
|
||
let last = src.data[yy as usize * w + w - 1];
|
||
for c in 0..4 {
|
||
acc[c] += last[c] as f64 * n;
|
||
}
|
||
bnd = w as i64 - 1;
|
||
}
|
||
} else {
|
||
a = a.max(0);
|
||
bnd = bnd.min(w as i64 - 1);
|
||
}
|
||
if a <= bnd {
|
||
for c in 0..4 {
|
||
acc[c] += pre[bnd as usize + 1][c] - pre[a as usize][c];
|
||
}
|
||
}
|
||
}
|
||
acc
|
||
};
|
||
// Diffraction Fringe: the iris edge (outer 20 % of the radius) gets more of the energy:
|
||
// 0 = an even disc, 100 = a natural halo, 500 = all of it in the ring.
|
||
let fringe = (ctx.params.f("irisProperties/diffractionFringe") / 500.0).clamp(0.0, 1.0);
|
||
let (w_in, w_ring) = (1.0 - fringe, 1.0 + 4.0 * fringe);
|
||
let iris = |radius: f64| -> (Vec<(i64, i64, i64)>, Vec<(i64, i64, i64)>) {
|
||
let stretch = |mut s: Vec<(i64, i64, i64)>| {
|
||
if aspect > 0.0 && (aspect - 1.0).abs() > 1e-6 {
|
||
for v in s.iter_mut() {
|
||
v.1 = (v.1 as f64 * aspect).round() as i64;
|
||
v.2 = (v.2 as f64 * aspect).round() as i64;
|
||
}
|
||
}
|
||
s
|
||
};
|
||
let outer = stretch(iris_spans(radius, sides, ctx.params.f("irisProperties/irisRotation"), roundness));
|
||
let inner = if fringe > 0.0 && radius * 0.8 >= 1.0 {
|
||
stretch(iris_spans(radius * 0.8, sides, ctx.params.f("irisProperties/irisRotation"), roundness))
|
||
} else {
|
||
vec![]
|
||
};
|
||
(outer, inner)
|
||
};
|
||
let count = |s: &[(i64, i64, i64)]| s.iter().map(|(_, l, r)| (r - l + 1) as f64).sum::<f64>();
|
||
// One blurred pixel with the given iris.
|
||
let blur_px = |sp: &(Vec<(i64, i64, i64)>, Vec<(i64, i64, i64)>), x: usize, y: usize| -> [f32; 4] {
|
||
let (outer, inner) = sp;
|
||
let acc = if inner.is_empty() {
|
||
let n = count(outer).max(1.0);
|
||
span_sum(outer, x, y).map(|v| v / n)
|
||
} else {
|
||
let (so, si) = (span_sum(outer, x, y), span_sum(inner, x, y));
|
||
let (no, ni) = (count(outer), count(inner));
|
||
let norm = (w_in * ni + w_ring * (no - ni)).max(1e-9);
|
||
std::array::from_fn(|c| (w_in * si[c] + w_ring * (so[c] - si[c])) / norm)
|
||
};
|
||
let mut px = acc.map(|v| v as f32);
|
||
px[3] = px[3].clamp(0.0, 1.0);
|
||
if linear {
|
||
let (c, a) = unpremul(px);
|
||
px = premul(c.map(|v| effectcraft_color::linear_to_srgb(v.max(0.0))), a);
|
||
}
|
||
px
|
||
};
|
||
// Blur Map: per-pixel radius from a map layer (no blur at Blur Focal Distance, full radius
|
||
// at the farthest value).
|
||
let map = ctx.layer_param("blurMap/blurMapLayer", true).map(|o| {
|
||
let stretch = ctx.params.e("blurMap/placement") == 1;
|
||
let img = crate::util::fit_layer(ctx, &b, &o, stretch);
|
||
let ch = ctx.params.e("blurMap/channel");
|
||
let invert = ctx.params.b("blurMap/invertBlurMap");
|
||
let focal = (ctx.params.f("blurMap/blurFocalDistance") / 255.0).clamp(0.0, 1.0) as f32;
|
||
let span = focal.max(1.0 - focal).max(1e-3);
|
||
img.data
|
||
.iter()
|
||
.map(|p| {
|
||
let (c, a) = unpremul(*p);
|
||
let mut v = match ch {
|
||
1 => a,
|
||
2 => c[0].max(c[1]).max(c[2]) * a,
|
||
_ => luminance(c[0], c[1], c[2]) * a,
|
||
};
|
||
if invert {
|
||
v = 1.0 - v;
|
||
}
|
||
((v - focal).abs() / span).clamp(0.0, 1.0)
|
||
})
|
||
.collect::<Vec<f32>>()
|
||
});
|
||
let mut out = Image::new(src.width, src.height);
|
||
match &map {
|
||
None => {
|
||
let sp = iris(r);
|
||
out.rows_mut().for_each(|(y, row)| {
|
||
for (x, o) in row.iter_mut().enumerate() {
|
||
*o = blur_px(&sp, x, y);
|
||
}
|
||
});
|
||
}
|
||
Some(m) => {
|
||
// Radii quantised to levels; each pixel blends the two nearest.
|
||
let levels = (r.ceil() as usize).clamp(1, 8);
|
||
let irises: Vec<_> = (0..=levels).map(|k| iris(r * k as f64 / levels as f64)).collect();
|
||
let srcd = &b.img.data;
|
||
out.rows_mut().for_each(|(y, row)| {
|
||
for (x, o) in row.iter_mut().enumerate() {
|
||
let f = m[y * w + x] * levels as f32;
|
||
let k0 = (f.floor() as usize).min(levels);
|
||
let t = f - k0 as f32;
|
||
let at = |k: usize| if k == 0 || r * k as f64 / (levels as f64) < 0.5 { srcd[y * w + x] } else { blur_px(&irises[k], x, y) };
|
||
let a = at(k0);
|
||
*o = if t > 1e-4 && k0 < levels {
|
||
let bq = at(k0 + 1);
|
||
std::array::from_fn(|c| a[c] + (bq[c] - a[c]) * t)
|
||
} else {
|
||
a
|
||
};
|
||
}
|
||
});
|
||
}
|
||
}
|
||
b.img = out;
|
||
b
|
||
}
|
||
|
||
// ---------------------------------------------------------------------------------------------
|
||
// CC Radial Fast Blur
|
||
|
||
fn radial_fast_blur(ctx: &EffectCtx, mut b: Buf) -> Buf {
|
||
let s = (ctx.params.f("amount") / 100.0).clamp(0.0, 1.0) * 0.5;
|
||
if s <= 0.0 {
|
||
return b;
|
||
}
|
||
let c = b.to_px(ctx.params.v2("center"));
|
||
let mode = ctx.params.e("zoom");
|
||
let src = b.img.clone();
|
||
b.img.rows_mut().for_each(|(y, row)| {
|
||
for (x, o) in row.iter_mut().enumerate() {
|
||
let (dx, dy) = (x as f64 + 0.5 - c.0, y as f64 + 0.5 - c.1);
|
||
let len = (dx * dx + dy * dy).sqrt() * s;
|
||
let n = (len.ceil() as usize).clamp(2, 64);
|
||
let mut acc = match mode {
|
||
1 => [f32::NEG_INFINITY; 4],
|
||
2 => [f32::INFINITY; 4],
|
||
_ => [0.0; 4],
|
||
};
|
||
for i in 0..n {
|
||
let k = 1.0 - s * i as f64 / (n - 1) as f64;
|
||
let q = src.sample_bilinear(c.0 + dx * k, c.1 + dy * k);
|
||
for ch in 0..4 {
|
||
acc[ch] = match mode {
|
||
1 => acc[ch].max(q[ch]),
|
||
2 => acc[ch].min(q[ch]),
|
||
_ => acc[ch] + q[ch] / n as f32,
|
||
};
|
||
}
|
||
}
|
||
for ch in 0..3 {
|
||
acc[ch] = acc[ch].min(acc[3]).max(0.0);
|
||
}
|
||
acc[3] = acc[3].clamp(0.0, 1.0);
|
||
*o = acc;
|
||
}
|
||
});
|
||
b
|
||
}
|
||
|
||
// ---------------------------------------------------------------------------------------------
|
||
// CC Vector Blur
|
||
|
||
fn vector_blur(ctx: &EffectCtx, mut b: Buf) -> Buf {
|
||
let amount = ctx.params.f("amount").max(0.0) * b.scale;
|
||
if amount < 0.5 {
|
||
return b;
|
||
}
|
||
let kind = ctx.params.e("type");
|
||
let off = ctx.params.f("angleOffset").to_radians();
|
||
let soft = ctx.params.f("mapSoftness").max(0.0) * 0.5 * b.scale;
|
||
let map = gauss_plane(&Plane::from_image(&b.img, plum), soft, soft);
|
||
let src = b.img.clone();
|
||
let (w, h) = (map.w as i64, map.h as i64);
|
||
b.img.rows_mut().for_each(|(y, row)| {
|
||
let y = y as i64;
|
||
for (x, o) in row.iter_mut().enumerate() {
|
||
let x = x as i64;
|
||
let gx = (map.get_clamped((x + 1).min(w - 1), y) - map.get_clamped((x - 1).max(0), y)) * 0.5;
|
||
let gy = (map.get_clamped(x, (y + 1).min(h - 1)) - map.get_clamped(x, (y - 1).max(0))) * 0.5;
|
||
let mag = (gx * gx + gy * gy).sqrt() as f64;
|
||
let m = map.get_clamped(x, y) as f64;
|
||
let (ang, len) = match kind {
|
||
0..=2 => {
|
||
if mag < 1e-6 {
|
||
continue;
|
||
}
|
||
let g = (gy as f64).atan2(gx as f64);
|
||
let base = if kind == 2 { g } else { g + std::f64::consts::FRAC_PI_2 };
|
||
let len = if kind == 1 { amount } else { amount * (mag * 10.0).min(1.0) };
|
||
(base + off, len)
|
||
}
|
||
3 => (m * std::f64::consts::TAU + off, amount),
|
||
_ => (m * std::f64::consts::TAU + off, amount * m),
|
||
};
|
||
if len < 0.5 {
|
||
continue;
|
||
}
|
||
let (dx, dy) = (ang.cos() * len, ang.sin() * len);
|
||
let n = ((len * 2.0).ceil() as usize).clamp(2, 48);
|
||
let mut acc = [0.0f32; 4];
|
||
for i in 0..n {
|
||
let t = i as f64 / (n - 1) as f64 - 0.5;
|
||
let q = src.sample_bilinear_clamped(x as f64 + 0.5 + dx * t, y as f64 + 0.5 + dy * t);
|
||
for c in 0..4 {
|
||
acc[c] += q[c] / n as f32;
|
||
}
|
||
}
|
||
*o = acc;
|
||
}
|
||
});
|
||
b
|
||
}
|
||
|
||
// ---------------------------------------------------------------------------------------------
|
||
// Reduce Interlace Flicker / CC Cross Blur
|
||
|
||
fn reduce_flicker(ctx: &EffectCtx, mut b: Buf) -> Buf {
|
||
let s = ctx.params.f("softness").max(0.0) * b.scale;
|
||
if s < 0.05 {
|
||
return b;
|
||
}
|
||
b.img = gaussian_blur(&b.img, 0.0, s, true);
|
||
b
|
||
}
|
||
|
||
fn cross_blur(ctx: &EffectCtx, mut b: Buf) -> Buf {
|
||
let rx = (ctx.params.f("radiusX").max(0.0) * b.scale).round() as usize;
|
||
let ry = (ctx.params.f("radiusY").max(0.0) * b.scale).round() as usize;
|
||
if rx == 0 && ry == 0 {
|
||
return b;
|
||
}
|
||
if !ctx.adjustment {
|
||
b.pad((rx.max(ry) * 3) as u32 + 1);
|
||
}
|
||
let repeat = ctx.adjustment;
|
||
let hz = effectcraft_raster::box_blur(&b.img, rx, 0, 3, repeat);
|
||
let vt = effectcraft_raster::box_blur(&b.img, 0, ry, 3, repeat);
|
||
let mode = ctx.params.e("transferMode");
|
||
b.img.data.par_iter_mut().enumerate().for_each(|(i, px)| {
|
||
let (a, v) = (hz.data[i], vt.data[i]);
|
||
let mut o = [0.0f32; 4];
|
||
for c in 0..4 {
|
||
o[c] = match mode {
|
||
1 => a[c] + v[c],
|
||
2 => a[c] + v[c] - a[c] * v[c],
|
||
3 => a[c].max(v[c]),
|
||
_ => (a[c] + v[c]) * 0.5,
|
||
};
|
||
}
|
||
o[3] = o[3].clamp(0.0, 1.0);
|
||
*px = o;
|
||
});
|
||
b
|
||
}
|
||
|
||
pub fn specs() -> Vec<EffectSpec> {
|
||
let dims = || popup(&["Horizontal and Vertical", "Horizontal", "Vertical"]);
|
||
let blur_s = || slider(0.0, 3000.0, 0.0, 50.0, 1);
|
||
vec![
|
||
spec(
|
||
"ec.blur.bilateral",
|
||
"Bilateral Blur",
|
||
vec![
|
||
p("radius", "Radius", num(5.0), slider(0.0, 200.0, 0.0, 50.0, 1)),
|
||
p("threshold", "Threshold", num(10.0), slider(0.0, 100.0, 0.0, 100.0, 1)),
|
||
p("colorize", "Colorize", Value::Bool(true), ParamUi::Checkbox),
|
||
],
|
||
bilateral,
|
||
),
|
||
spec(
|
||
"ec.blur.smart",
|
||
"Smart Blur",
|
||
vec![
|
||
p("radius", "Radius", num(3.0), slider(0.0, 100.0, 0.0, 20.0, 1)),
|
||
p("threshold", "Threshold", num(25.0), slider(0.0, 100.0, 0.0, 100.0, 1)),
|
||
p("mode", "Mode", Value::Enum(0), popup(&["Normal", "Edge Only", "Overlay Edge"])),
|
||
],
|
||
smart_blur,
|
||
),
|
||
spec(
|
||
"ec.blur.channel",
|
||
"Channel Blur",
|
||
vec![
|
||
p("redBlurriness", "Red Blurriness", num(0.0), blur_s()),
|
||
p("greenBlurriness", "Green Blurriness", num(0.0), blur_s()),
|
||
p("blueBlurriness", "Blue Blurriness", num(0.0), blur_s()),
|
||
p("alphaBlurriness", "Alpha Blurriness", num(0.0), blur_s()),
|
||
p("repeatEdge", "Repeat Edge Pixels", Value::Bool(false), ParamUi::Checkbox),
|
||
p("dimensions", "Blur Dimensions", Value::Enum(0), dims()),
|
||
],
|
||
channel_blur,
|
||
),
|
||
spec(
|
||
"ec.blur.compound",
|
||
"Compound Blur",
|
||
vec![
|
||
p("blurLayer", "Blur Layer", Value::Layer(None), ParamUi::Layer),
|
||
p("maximumBlur", "Maximum Blur", num(20.0), slider(0.0, 1000.0, 0.0, 100.0, 1)),
|
||
p("stretchMapToFit", "Stretch Map to Fit", Value::Bool(true), ParamUi::Checkbox),
|
||
p("invertBlur", "Invert Blur", Value::Bool(false), ParamUi::Checkbox),
|
||
],
|
||
compound_blur,
|
||
),
|
||
spec(
|
||
"ec.blur.cameralens",
|
||
"Camera Lens Blur",
|
||
vec![
|
||
p("blurRadius", "Blur Radius", num(10.0), slider(0.0, 500.0, 0.0, 100.0, 1)),
|
||
p(
|
||
"irisProperties/irisShape",
|
||
"Shape",
|
||
Value::Enum(3),
|
||
popup(&["Triangle", "Square", "Pentagon", "Hexagon", "Heptagon", "Octagon", "Nonagon", "Decagon", "Circle"]),
|
||
),
|
||
p("irisProperties/irisRoundness", "Roundness", num(0.0), slider(0.0, 100.0, 0.0, 100.0, 1)),
|
||
p("irisProperties/irisAspectRatio", "Aspect Ratio", num(1.0), slider(0.01, 100.0, 0.25, 4.0, 2)),
|
||
p("irisProperties/irisRotation", "Rotation", num(0.0), ParamUi::Angle),
|
||
p("irisProperties/diffractionFringe", "Diffraction Fringe", num(0.0), slider(0.0, 500.0, 0.0, 500.0, 1)),
|
||
p("blurMap/blurMapLayer", "Layer", Value::Layer(None), ParamUi::Layer),
|
||
p("blurMap/channel", "Channel", Value::Enum(0), popup(&["Luminance", "Alpha", "Color"])),
|
||
p("blurMap/placement", "Placement", Value::Enum(1), popup(&["Center", "Stretch"])),
|
||
p("blurMap/blurFocalDistance", "Blur Focal Distance", num(0.0), slider(0.0, 255.0, 0.0, 255.0, 0)),
|
||
p("blurMap/invertBlurMap", "Invert Blur Map", Value::Bool(false), ParamUi::Checkbox),
|
||
p("highlight/specularBrightness", "Gain", num(0.0), slider(0.0, 100.0, 0.0, 100.0, 1)),
|
||
p("highlight/specularThreshold", "Threshold", num(255.0), slider(0.0, 255.0, 0.0, 255.0, 0)),
|
||
p("highlight/highlightSaturation", "Saturation", num(100.0), slider(0.0, 100.0, 0.0, 100.0, 1)),
|
||
p("repeatEdge", "Repeat Edge Pixels", Value::Bool(false), ParamUi::Checkbox),
|
||
p("useLinear", "Use Linear Working Space", Value::Bool(false), ParamUi::Checkbox),
|
||
],
|
||
camera_lens_blur,
|
||
),
|
||
spec(
|
||
"ec.blur.ccradialfast",
|
||
"CC Radial Fast Blur",
|
||
vec![
|
||
p("center", "Center", Value::Vec2([0.5, 0.5]), ParamUi::Point),
|
||
p("amount", "Amount", num(50.0), slider(0.0, 100.0, 0.0, 100.0, 1)),
|
||
p("zoom", "Zoom", Value::Enum(0), popup(&["Standard", "Brightest", "Darkest"])),
|
||
],
|
||
radial_fast_blur,
|
||
),
|
||
spec(
|
||
"ec.blur.ccvector",
|
||
"CC Vector Blur",
|
||
vec![
|
||
p("type", "Type", Value::Enum(0), popup(&["Natural", "Constant Length", "Perpendicular", "Direction Center", "Direction Fading"])),
|
||
p("amount", "Amount", num(10.0), slider(0.0, 500.0, 0.0, 100.0, 1)),
|
||
p("angleOffset", "Angle Offset", num(0.0), ParamUi::Angle),
|
||
p("mapSoftness", "Map Softness", num(15.0), slider(0.0, 100.0, 0.0, 100.0, 1)),
|
||
],
|
||
vector_blur,
|
||
),
|
||
EffectSpec {
|
||
category: "Obsolete",
|
||
..spec(
|
||
"ec.blur.reduceflicker",
|
||
"Reduce Interlace Flicker",
|
||
vec![p("softness", "Softness", num(0.0), slider(0.0, 100.0, 0.0, 10.0, 2))],
|
||
reduce_flicker,
|
||
)
|
||
},
|
||
spec(
|
||
"ec.blur.cccross",
|
||
"CC Cross Blur",
|
||
vec![
|
||
p("radiusX", "Radius X", num(10.0), slider(0.0, 500.0, 0.0, 100.0, 1)),
|
||
p("radiusY", "Radius Y", num(10.0), slider(0.0, 500.0, 0.0, 100.0, 1)),
|
||
p("transferMode", "Transfer Mode", Value::Enum(0), popup(&["Blend", "Add", "Screen", "Lighten"])),
|
||
],
|
||
cross_blur,
|
||
),
|
||
]
|
||
}
|
||
|
||
#[cfg(test)]
|
||
mod tests {
|
||
use super::*;
|
||
use crate::{Params, find};
|
||
|
||
fn run(id: &str, img: &Image, over: &[(&str, Value)]) -> Buf {
|
||
let s = find(id).unwrap();
|
||
let mut params = Params { values: s.params.iter().map(|p| (p.id.to_string(), p.default.clone())).collect() };
|
||
for (k, v) in over {
|
||
params.values.insert(k.to_string(), v.clone());
|
||
}
|
||
let ctx =
|
||
EffectCtx { params: ¶ms, time: 0.0, layer_size: [img.width as f64, img.height as f64], seed: 1, adjustment: false, env: Default::default() };
|
||
(s.render)(&ctx, Buf { img: img.clone(), offset: [0.0, 0.0], scale: 1.0 })
|
||
}
|
||
|
||
fn edge_img() -> Image {
|
||
let mut img = Image::new(32, 16);
|
||
for y in 0..16 {
|
||
for x in 0..32 {
|
||
let v = if x < 16 { 0.1 } else { 0.9 };
|
||
img.set(x, y, [v, v, v, 1.0]);
|
||
}
|
||
}
|
||
img
|
||
}
|
||
|
||
#[test]
|
||
fn bilateral_keeps_edge_sharper_than_gaussian() {
|
||
let img = edge_img();
|
||
let bl = run("ec.blur.bilateral", &img, &[("radius", num(4.0))]).img;
|
||
let g = gaussian_blur(&img, 2.0, 2.0, true);
|
||
let jump_b = bl.get(16, 8)[0] - bl.get(15, 8)[0];
|
||
let jump_g = g.get(16, 8)[0] - g.get(15, 8)[0];
|
||
assert!(jump_b > 0.7, "{jump_b}");
|
||
assert!(jump_b > jump_g + 0.3);
|
||
}
|
||
|
||
#[test]
|
||
fn channel_blur_zero_is_identity() {
|
||
let img = edge_img();
|
||
let out = run("ec.blur.channel", &img, &[]);
|
||
assert_eq!(out.img, img);
|
||
}
|
||
|
||
#[test]
|
||
fn channel_blur_only_touches_selected_channel() {
|
||
let img = edge_img();
|
||
let out = run("ec.blur.channel", &img, &[("redBlurriness", num(6.0)), ("repeatEdge", Value::Bool(true))]).img;
|
||
let (l, r) = (out.get(15, 8), out.get(16, 8));
|
||
assert!(r[0] - l[0] < 0.6);
|
||
assert!((l[1] - 0.1).abs() < 1e-5 && (r[1] - 0.9).abs() < 1e-5);
|
||
}
|
||
|
||
#[test]
|
||
fn lens_blur_preserves_constant_and_energy() {
|
||
let img = Image::filled(24, 20, [0.4, 0.3, 0.2, 1.0]);
|
||
let out = run("ec.blur.cameralens", &img, &[("repeatEdge", Value::Bool(true))]).img;
|
||
assert!(out.data.iter().all(|p| (p[0] - 0.4).abs() < 1e-4 && (p[3] - 1.0).abs() < 1e-4));
|
||
let mut dot = Image::new(9, 9);
|
||
dot.set(4, 4, [1.0, 1.0, 1.0, 1.0]);
|
||
let out = run("ec.blur.cameralens", &dot, &[("blurRadius", num(5.0))]);
|
||
let total: f32 = out.img.data.iter().map(|p| p[3]).sum();
|
||
assert!((total - 1.0).abs() < 1e-3, "{total}");
|
||
assert!(out.img.width > 9);
|
||
}
|
||
|
||
#[test]
|
||
fn lens_blur_aspect_ratio_widens_the_bokeh() {
|
||
let mut dot = Image::new(41, 41);
|
||
dot.set(20, 20, [1.0, 1.0, 1.0, 1.0]);
|
||
let wide = run("ec.blur.cameralens", &dot, &[("blurRadius", num(5.0)), ("irisProperties/irisAspectRatio", num(2.0))]);
|
||
let (o, d) = (wide.offset[0] as i64, wide.offset[1] as i64);
|
||
// Lit 8 px to the side but not 8 px above.
|
||
assert!(wide.img.get(20 + o + 8, 20 + d)[3] > 0.0);
|
||
assert_eq!(wide.img.get(20 + o, 20 + d - 8)[3], 0.0);
|
||
}
|
||
|
||
#[test]
|
||
fn lens_blur_highlight_saturation_whitens_boosted_colour() {
|
||
let mut img = Image::new(21, 21);
|
||
img.set(10, 10, [1.0, 0.2, 0.2, 1.0]);
|
||
let over = |s: f64| {
|
||
[
|
||
("blurRadius", num(3.0)),
|
||
("highlight/specularBrightness", num(100.0)),
|
||
("highlight/specularThreshold", num(0.0)),
|
||
("highlight/highlightSaturation", num(s)),
|
||
]
|
||
};
|
||
let keep = run("ec.blur.cameralens", &img, &over(100.0));
|
||
let white = run("ec.blur.cameralens", &img, &over(0.0));
|
||
let c = |b: &Buf| b.img.get(10 + b.offset[0] as i64, 10 + b.offset[1] as i64);
|
||
// Same red energy, more green with Saturation 0.
|
||
assert!((c(&keep)[0] - c(&white)[0]).abs() < 1e-5);
|
||
assert!(c(&white)[1] > c(&keep)[1] + 1e-4);
|
||
}
|
||
|
||
#[test]
|
||
fn compound_blur_reads_the_blur_layer() {
|
||
use crate::{EffectEnv, EffectHost, LayerPixels};
|
||
struct Map;
|
||
impl EffectHost for Map {
|
||
fn layer(&self, _: u64, _: bool) -> Option<LayerPixels> {
|
||
// Left half black (sharp), right half white (full blur).
|
||
let mut m = Image::new(20, 20);
|
||
for y in 0..20 {
|
||
for x in 10..20 {
|
||
m.set(x, y, [1.0, 1.0, 1.0, 1.0]);
|
||
}
|
||
}
|
||
Some(LayerPixels { buf: Buf { img: m, offset: [0.0; 2], scale: 1.0 }, size: [20.0, 20.0] })
|
||
}
|
||
fn audio(&self, _: u64, _: f64, _: usize, _: u32) -> Option<Vec<f32>> {
|
||
None
|
||
}
|
||
}
|
||
let mut img = Image::filled(20, 20, [0.0, 0.0, 0.0, 1.0]);
|
||
for y in 0..20 {
|
||
img.set(4, y, [1.0, 1.0, 1.0, 1.0]);
|
||
img.set(15, y, [1.0, 1.0, 1.0, 1.0]);
|
||
}
|
||
let out = crate::run_fx(
|
||
"ec.blur.compound",
|
||
&[("blurLayer", Value::Layer(Some(1))), ("maximumBlur", num(6.0))],
|
||
img,
|
||
0.0,
|
||
EffectEnv { host: Some(&Map), ..Default::default() },
|
||
);
|
||
let (o, d) = (out.offset[0] as i64, out.offset[1] as i64);
|
||
assert!((out.img.get(4 + o, 10 + d)[0] - 1.0).abs() < 1e-4, "dark map keeps the line sharp");
|
||
assert!(out.img.get(15 + o, 10 + d)[0] < 0.9, "bright map blurs");
|
||
}
|
||
|
||
#[test]
|
||
fn iris_spans_hexagon_is_symmetric() {
|
||
let s = iris_spans(10.0, 6, 0.0, 0.0);
|
||
let s0 = s.iter().find(|t| t.0 == 0).unwrap();
|
||
assert_eq!(s0.1, -s0.2);
|
||
let circle = iris_spans(10.0, 0, 0.0, 0.0);
|
||
assert_eq!(circle.len(), 21);
|
||
}
|
||
|
||
#[test]
|
||
fn reduce_flicker_blurs_vertically_only() {
|
||
let mut img = Image::filled(8, 8, [0.0, 0.0, 0.0, 1.0]);
|
||
for x in 0..8 {
|
||
img.set(x, 4, [1.0, 1.0, 1.0, 1.0]);
|
||
}
|
||
let out = run("ec.blur.reduceflicker", &img, &[("softness", num(1.0))]).img;
|
||
assert!(out.get(3, 4)[0] < 0.9 && out.get(3, 3)[0] > 0.05);
|
||
assert!((out.get(0, 4)[0] - out.get(7, 4)[0]).abs() < 1e-6);
|
||
}
|
||
|
||
#[test]
|
||
fn compound_blur_dark_pixels_stay_sharp() {
|
||
let mut img = Image::new(16, 16);
|
||
for y in 0..16 {
|
||
for x in 0..16 {
|
||
img.set(x, y, if (x + y) % 2 == 0 { [0.0, 0.0, 0.0, 1.0] } else { [0.02, 0.02, 0.02, 1.0] });
|
||
}
|
||
}
|
||
let out = run("ec.blur.compound", &img, &[("maximumBlur", num(10.0))]);
|
||
// Luminance ≈ 0 → (almost) no blur.
|
||
let pad = (out.img.width - 16) / 2;
|
||
assert!(out.img.get(pad as i64, pad as i64)[0] < 0.005);
|
||
}
|
||
|
||
#[test]
|
||
fn camera_lens_blur_diffraction_fringe_and_blur_map() {
|
||
use crate::{EffectEnv, EffectHost, LayerPixels};
|
||
// A single bright point spreads into a disc; with Diffraction Fringe its rim is brighter.
|
||
let mut dot = Image::filled(41, 41, [0.0, 0.0, 0.0, 1.0]);
|
||
dot.set(20, 20, [1.0, 1.0, 1.0, 1.0]);
|
||
let v = [("blurRadius", num(10.0)), ("irisProperties/irisShape", Value::Enum(8)), ("repeatEdge", Value::Bool(true))];
|
||
let plain = run("ec.blur.cameralens", &dot, &v).img;
|
||
let ring = run("ec.blur.cameralens", &dot, &[v.as_slice(), &[("irisProperties/diffractionFringe", num(300.0))]].concat()).img;
|
||
assert!((plain.get(20, 20)[0] - plain.get(29, 20)[0]).abs() < 1e-5, "even disc");
|
||
assert!(ring.get(29, 20)[0] > ring.get(20, 20)[0] * 2.0, "{:?} {:?}", ring.get(29, 20), ring.get(20, 20));
|
||
// Blur Map: black (focal distance 0) stays sharp, white gets the full radius.
|
||
struct Half;
|
||
impl EffectHost for Half {
|
||
fn layer(&self, _: u64, _: bool) -> Option<LayerPixels> {
|
||
let mut m = Image::filled(40, 20, [0.0, 0.0, 0.0, 1.0]);
|
||
for y in 0..20 {
|
||
for x in 20..40 {
|
||
m.set(x, y, [1.0, 1.0, 1.0, 1.0]);
|
||
}
|
||
}
|
||
Some(LayerPixels { buf: Buf { img: m, offset: [0.0; 2], scale: 1.0 }, size: [40.0, 20.0] })
|
||
}
|
||
fn audio(&self, _: u64, _: f64, _: usize, _: u32) -> Option<Vec<f32>> {
|
||
None
|
||
}
|
||
}
|
||
let stripes = crate::util::gen_image(40, 20, |x, _| if x % 4 < 2 { [1.0, 1.0, 1.0, 1.0] } else { [0.0, 0.0, 0.0, 1.0] });
|
||
let vals = [("blurRadius", num(6.0)), ("repeatEdge", Value::Bool(true)), ("blurMap/blurMapLayer", Value::Layer(Some(1)))];
|
||
let out = crate::run_fx("ec.blur.cameralens", &vals, stripes.clone(), 0.0, EffectEnv { host: Some(&Half), ..Default::default() }).img;
|
||
assert_eq!(out.get(4, 10), stripes.get(4, 10), "sharp where the map is at the focal distance");
|
||
assert!((out.get(30, 10)[0] - 0.5).abs() < 0.2, "{:?}", out.get(30, 10));
|
||
}
|
||
}
|