diff --git a/crates/effects/src/blur2.rs b/crates/effects/src/blur2.rs index def9d207..8ce9a840 100644 --- a/crates/effects/src/blur2.rs +++ b/crates/effects/src/blur2.rs @@ -596,7 +596,15 @@ pub fn specs() -> Vec { ], vector_blur, ), - 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), + 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", diff --git a/crates/effects/src/keying.rs b/crates/effects/src/keying.rs index c451c751..6be96506 100644 --- a/crates/effects/src/keying.rs +++ b/crates/effects/src/keying.rs @@ -485,7 +485,7 @@ pub fn specs() -> Vec { spec( "ec.key.luma", "Luma Key", - "Keying", + "Obsolete", vec![ p("keyType", "Key Type", Value::Enum(1), popup(&["Key Out Brighter", "Key Out Darker", "Key Out Similar", "Key Out Dissimilar"])), p("threshold", "Threshold", num(0.0), b255()), @@ -560,7 +560,7 @@ pub fn specs() -> Vec { spec( "ec.key.spill", "Spill Suppressor", - "Keying", + "Obsolete", vec![ p("colorToSuppress", "Color To Suppress", col(0.0, 0.0, 1.0), ParamUi::Color), p("suppression", "Suppression", num(100.0), slider(0.0, 200.0, 0.0, 100.0, 0)), diff --git a/crates/effects/src/obsolete.rs b/crates/effects/src/obsolete.rs index 331686c1..122ec32d 100644 --- a/crates/effects/src/obsolete.rs +++ b/crates/effects/src/obsolete.rs @@ -1,7 +1,184 @@ -//! Obsolete-category effects. +//! Obsolete-category effects: Basic 3D (tilt/swivel the layer in a simple perspective space +//! with an optional specular highlight) and Gaussian Blur (Legacy). -use crate::EffectSpec; +use effectcraft_keyframe::Value; +use effectcraft_project::ParamUi; +use effectcraft_raster::gaussian_blur; +use rayon::prelude::*; + +use crate::util::layer_rect; +use crate::{Buf, EffectCtx, EffectSpec, num, p, popup, slider}; + +fn spec(id: &'static str, name: &'static str, params: Vec, render: crate::RenderFn) -> EffectSpec { + EffectSpec { id, name, category: "Obsolete", params, render, gpu: false, float: true } +} + +// ---------------------------------------------------------------- Basic 3D + +fn basic_3d(ctx: &EffectCtx, mut b: Buf) -> Buf { + let swivel = ctx.params.f("swivel").to_radians(); + let tilt = ctx.params.f("tilt").to_radians(); + let dist = ctx.params.f("distanceToImage") * b.scale; + let spec_on = ctx.params.b("specularHighlight"); + let wire = ctx.params.b("drawPreviewWireframe"); + if swivel == 0.0 && tilt == 0.0 && dist == 0.0 && !spec_on && !wire { + return b; + } + let (lx, ly, lw, lh) = layer_rect(ctx, &b); + let (cx, cy) = (lx + lw * 0.5, ly + lh * 0.5); + // Eye on the -z axis at a focal distance comparable to a 50 mm lens. + let f = lw.max(lh).max(1.0) * 1.4; + // Plane basis after Y (swivel) then X (tilt) rotations. + let (cs, ss) = (swivel.cos(), swivel.sin()); + let (ct, st) = (tilt.cos(), tilt.sin()); + let ux = [cs, 0.0, -ss]; + let uy = [ss * st, ct, cs * st]; + let n = [ss * ct, -st, cs * ct]; + let dot = |a: [f64; 3], b: [f64; 3]| a[0] * b[0] + a[1] * b[1] + a[2] * b[2]; + let src = b.img.clone(); + let light = { + let l = [-0.5f64, -0.6, -1.0]; + let k = 1.0 / dot(l, l).sqrt(); + l.map(|v| v * k) + }; + b.img.rows_mut().for_each(|(y, row)| { + for (x, px) in row.iter_mut().enumerate() { + // Ray from the eye (0, 0, -f) through the pixel on the z = 0 plane (centre-relative). + let d = [x as f64 + 0.5 - cx, y as f64 + 0.5 - cy, f]; + let o = [0.0, 0.0, -f]; + let denom = dot(d, n); + if denom.abs() < 1e-9 { + *px = [0.0; 4]; + continue; + } + let pc = [0.0, 0.0, dist]; + let t = dot([pc[0] - o[0], pc[1] - o[1], pc[2] - o[2]], n) / denom; + if t <= 0.0 { + *px = [0.0; 4]; + continue; + } + let hit = [o[0] + d[0] * t - pc[0], o[1] + d[1] * t - pc[1], o[2] + d[2] * t - pc[2]]; + let (u, v) = (dot(hit, ux), dot(hit, uy)); + let mut c = src.sample_bilinear(cx + u, cy + v); + if spec_on && c[3] > 0.0 { + // Reflect the view direction about the normal and compare with the light. + let dl = dot(d, d).sqrt(); + let dv = d.map(|v| v / dl); + let k = 2.0 * dot(dv, n); + let r = [dv[0] - k * n[0], dv[1] - k * n[1], dv[2] - k * n[2]]; + let s = (-dot(r, light)).max(0.0).powi(40) as f32; + let a = c[3]; + for ch in c.iter_mut().take(3) { + *ch += s * a * (1.0 - *ch / a.max(1e-6)).max(0.0); + } + } + if wire { + let e = (u.abs() - lw * 0.5).abs().min((v.abs() - lh * 0.5).abs()); + if e < 0.75 && u.abs() <= lw * 0.5 + 0.75 && v.abs() <= lh * 0.5 + 0.75 { + c = [1.0, 1.0, 1.0, 1.0]; + } + } + *px = c; + } + }); + b +} + +// ---------------------------------------------------------------- Gaussian Blur (Legacy) + +fn gaussian_legacy(ctx: &EffectCtx, mut b: Buf) -> Buf { + let s = ctx.params.f("blurriness").max(0.0) * 0.5 * b.scale; + if s <= 0.0 { + return b; + } + let (kx, ky) = match ctx.params.e("blurDimensions") { + 1 => (1.0, 0.0), + 2 => (0.0, 1.0), + _ => (1.0, 1.0), + }; + if !ctx.adjustment { + b.pad((s * 3.0).ceil() as u32); + } + b.img = gaussian_blur(&b.img, s * kx, s * ky, ctx.adjustment); + b +} pub fn specs() -> Vec { - Vec::new() + vec![ + spec( + "ec.obsolete.basic3d", + "Basic 3D", + vec![ + p("swivel", "Swivel", num(0.0), ParamUi::Angle), + p("tilt", "Tilt", num(0.0), ParamUi::Angle), + p("distanceToImage", "Distance to Image", num(0.0), slider(-1000.0, 30000.0, -100.0, 100.0, 1)), + p("specularHighlight", "Specular Highlight", Value::Bool(false), ParamUi::Checkbox), + p("drawPreviewWireframe", "Draw Preview Wireframe", Value::Bool(false), ParamUi::Checkbox), + ], + basic_3d, + ), + spec( + "ec.obsolete.gaussianlegacy", + "Gaussian Blur (Legacy)", + vec![ + p("blurriness", "Blurriness", num(0.0), slider(0.0, 1000.0, 0.0, 50.0, 1)), + p("blurDimensions", "Blur Dimensions", Value::Enum(0), popup(&["Horizontal and Vertical", "Horizontal", "Vertical"])), + ], + gaussian_legacy, + ), + ] +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::{EffectEnv, run_fx}; + use effectcraft_raster::Image; + + fn ramp() -> Image { + let mut img = Image::new(20, 12); + for y in 0..12 { + for x in 0..20 { + img.set(x, y, [x as f32 / 20.0, y as f32 / 12.0, 0.5, 1.0]); + } + } + img + } + + #[test] + fn basic_3d_zero_identity_swivel_narrows_and_is_deterministic() { + let img = ramp(); + let id = run_fx("ec.obsolete.basic3d", &[], img.clone(), 0.0, EffectEnv::default()); + assert_eq!(id.img.data, img.data); + let flat = + run_fx("ec.obsolete.basic3d", &[("distanceToImage", num(0.0)), ("specularHighlight", Value::Bool(false))], img.clone(), 0.0, EffectEnv::default()); + assert_eq!(flat.img.data, img.data); + let sw = run_fx("ec.obsolete.basic3d", &[("swivel", num(60.0))], img.clone(), 0.0, EffectEnv::default()); + assert_eq!(sw.img.get(0, 6)[3], 0.0, "edges uncovered after swivel"); + assert!(sw.img.get(10, 6)[3] > 0.99); + let again = run_fx("ec.obsolete.basic3d", &[("swivel", num(60.0))], img.clone(), 0.0, EffectEnv::default()); + assert_eq!(sw.img.data, again.img.data); + let far = run_fx("ec.obsolete.basic3d", &[("distanceToImage", num(20.0))], img, 0.0, EffectEnv::default()); + assert_eq!(far.img.get(0, 0)[3], 0.0, "farther image is smaller"); + } + + #[test] + fn gaussian_legacy_zero_identity_and_blurs() { + let mut img = Image::new(15, 15); + img.set(7, 7, [1.0, 1.0, 1.0, 1.0]); + let id = run_fx("ec.obsolete.gaussianlegacy", &[], img.clone(), 0.0, EffectEnv::default()); + assert_eq!(id.img.data, img.data); + let out = run_fx("ec.obsolete.gaussianlegacy", &[("blurriness", num(4.0))], img.clone(), 0.0, EffectEnv::default()); + let total: f32 = out.img.data.iter().map(|p| p[3]).sum(); + assert!((total - 1.0).abs() < 1e-3, "{total}"); + let c = out.img.width / 2; + assert!(out.img.get(c as i64, c as i64)[3] < 0.5); + } + + #[test] + fn moved_effects_are_obsolete() { + for id in ["ec.key.luma", "ec.key.spill", "ec.blur.reduceflicker"] { + assert_eq!(crate::find(id).unwrap().category, "Obsolete", "{id}"); + } + } }