Files
effectcraft/crates/gpu/src/fx_stylize.rs
T
dedandClaude Opus 5.5 915ba28a9d Motion Tile's Output size works on adjustment layers (#1727)
On an adjustment layer Motion Tile kept the comp-sized buffer whatever the
Output Width / Height, and the adjustment draw dropped any stack result whose
size differed from the canvas. After Effects grows (or crops) the comp
rectangle there as on any layer, so a Transform after it brings the extra
tiles into view, as in the frames attached to #1727 and #1409.

- motion_tile_frame: one output frame for the CPU and GPU effect; on an
  adjustment it grows about the comp rectangle on whole pixels, keeps the
  buffer at 100 % and past 16 384 pixels a side.
- adjustment_crop: the CPU and GPU adjustment draws put a stack result whose
  frame changed back onto the canvas instead of dropping it (Enc::crop on
  the GPU).

Tests: the #1727 After Effects frames (910 x 340 % and 90 x 340 % then
Scale 12 %), tiles past the frame and cropping on CPU, GPU vs CPU for
adjustment and full-frame layers.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-21 10:35:47 +03:00

1009 lines
38 KiB
Rust

//! GPU effects, stylize and distort family (kernels in `shaders/fx_stylize.wgsl`, every entry point prefixed
//! `fxs_`): the CPU effects' exact steps with the pixel loops as compute kernels.
//!
//! Parameter conversions, padding, output geometry and anything the CPU computes in f64 with a
//! floor or a wrap (tile indices, wrapped coordinates, brush cells) are computed here exactly as
//! the CPU does and uploaded as per-column / per-row / per-cell tables; the kernels do the
//! per-pixel work. Glow's other Glow Operations / Arbitrary Map and Transform's shutter-angle
//! motion blur extend the base kernels in `effects.rs` (see [`glow_extra`], [`transform_blur`]).
use std::f64::consts::{FRAC_PI_2, TAU};
use effectcraft_color::BlendMode;
use effectcraft_effects::EffectCtx;
use effectcraft_geom::{Mat3, vec2};
use effectcraft_raster::Sampling;
use crate::context::{Enc, Params};
use crate::effects::{GBuf, gaussian_blur};
use crate::ops;
/// Compute entry points in `fx_stylize.wgsl`.
pub(crate) const KERNELS: &[&str] = &[
"fxs_point",
"fxs_warp",
"fxs_table",
"fxs_griddler",
"fxs_motiontile",
"fxs_repetile",
"fxs_magnify",
"fxs_scatter",
"fxs_brush",
"fxs_roughen",
"fxs_tex_luma",
"fxs_texturize",
"fxs_glow_map",
"fxs_glow_op",
"fxs_add",
"fxs_div",
"fxs_flomotion",
];
/// Effect ids implemented here.
pub(crate) const IDS: &[&str] = &[
"ec.stylize.posterize",
"ec.stylize.threshold",
"ec.stylize.ccthreshold",
"ec.stylize.ccthresholdrgb",
"ec.stylize.strobe",
"ec.stylize.ccvignette",
"ec.stylize.scatter",
"ec.stylize.brushstrokes",
"ec.stylize.roughenedges",
"ec.stylize.texturize",
"ec.stylize.motiontile",
"ec.stylize.cckaleida",
"ec.stylize.ccrepetile",
"ec.distort.mirror",
"ec.distort.offset",
"ec.distort.polar",
"ec.distort.spherize",
"ec.distort.cornerpin",
"ec.distort.opticscompensation",
"ec.distort.magnify",
"ec.distort.ccslant",
"ec.distort.ccsmear",
"ec.distort.ccsplit",
"ec.distort.ccsplit2",
"ec.distort.cctiler",
"ec.distort.ccgriddler",
"ec.distort.liquify",
"ec.distort.twirllegacy",
"ec.distort.ccripplepulse",
"ec.distort.ccpowerpin",
"ec.distort.ccflomotion",
];
/// Run effect `id` (one of [`IDS`]); `None` = this parameter combination runs on the CPU.
pub(crate) fn apply(e: &mut Enc, id: &str, ctx: &EffectCtx, b: GBuf) -> Option<GBuf> {
match id {
"ec.stylize.posterize" => posterize(e, ctx, b),
"ec.stylize.threshold" => threshold(e, ctx, b),
"ec.stylize.ccthreshold" => cc_threshold(e, ctx, b),
"ec.stylize.ccthresholdrgb" => cc_threshold_rgb(e, ctx, b),
"ec.stylize.strobe" => strobe(e, ctx, b),
"ec.stylize.ccvignette" => vignette(e, ctx, b),
"ec.stylize.scatter" => scatter(e, ctx, b),
"ec.stylize.brushstrokes" => brush_strokes(e, ctx, b),
"ec.stylize.roughenedges" => roughen_edges(e, ctx, b),
"ec.stylize.texturize" => texturize(e, ctx, b),
"ec.stylize.motiontile" => motion_tile(e, ctx, b),
"ec.stylize.cckaleida" => kaleida(e, ctx, b),
"ec.stylize.ccrepetile" => repetile(e, ctx, b),
"ec.distort.mirror" => mirror(e, ctx, b),
"ec.distort.offset" => offset(e, ctx, b),
"ec.distort.polar" => polar(e, ctx, b),
"ec.distort.spherize" => spherize(e, ctx, b),
"ec.distort.cornerpin" => corner_pin(e, ctx, b),
"ec.distort.opticscompensation" => optics_compensation(e, ctx, b),
"ec.distort.magnify" => magnify(e, ctx, b),
"ec.distort.ccslant" => cc_slant(e, ctx, b),
"ec.distort.ccsmear" => cc_smear(e, ctx, b),
"ec.distort.ccsplit" => {
let s = ctx.params.f("split") / 100.0;
cc_split(e, ctx, b, s, s)
}
"ec.distort.ccsplit2" => cc_split(e, ctx, b, ctx.params.f("split1") / 100.0, ctx.params.f("split2") / 100.0),
"ec.distort.cctiler" => cc_tiler(e, ctx, b),
"ec.distort.ccgriddler" => cc_griddler(e, ctx, b),
"ec.distort.liquify" => liquify(e, ctx, b),
"ec.distort.twirllegacy" => twirl_legacy(e, ctx, b),
"ec.distort.ccripplepulse" => ripple_pulse(e, ctx, b),
"ec.distort.ccpowerpin" => power_pin(e, ctx, b),
"ec.distort.ccflomotion" => flo_motion(e, ctx, b),
_ => None,
}
}
/// Run a same-size per-pixel kernel over the buffer.
fn run(e: &mut Enc, entry: &str, p: &Params, mut b: GBuf, data: Option<&wgpu::Buffer>) -> Option<GBuf> {
let out = e.scratch(b.img.width, b.img.height);
e.pixels(entry, p, &b.img, None, &out, data);
b.img = out;
Some(b)
}
/// util::layer_rect.
fn layer_rect(ctx: &EffectCtx, b: &GBuf) -> (f64, f64, f64, f64) {
effectcraft_effects::util::layer_rect_at(ctx, b.offset, b.scale)
}
// ---------------------------------------------------------------- per-pixel (fxs_point)
fn point(e: &mut Enc, mode: u32, f: impl FnOnce(&mut Params), b: GBuf) -> Option<GBuf> {
let mut p = Params::default();
p.u[0][0] = mode;
f(&mut p);
run(e, "fxs_point", &p, b, None)
}
fn posterize(e: &mut Enc, ctx: &EffectCtx, b: GBuf) -> Option<GBuf> {
let lv = ctx.params.f("level").round().clamp(2.0, 255.0) as f32 - 1.0;
point(e, 1, |p| p.f[0][0] = lv, b)
}
fn threshold(e: &mut Enc, ctx: &EffectCtx, b: GBuf) -> Option<GBuf> {
let t = ctx.params.f("level") as f32 / 255.0;
point(e, 2, |p| p.f[0][0] = t, b)
}
fn cc_threshold(e: &mut Enc, ctx: &EffectCtx, b: GBuf) -> Option<GBuf> {
let t = ctx.params.f("threshold") as f32 / 255.0;
let blend = ctx.params.f("blend") as f32 / 100.0;
let (ch, inv) = (ctx.params.e("channel"), ctx.params.b("invert") as u32);
point(
e,
3,
|p| {
p.u[0][1] = ch;
p.u[0][2] = inv;
p.f[0] = [t, blend, 0.0, 0.0];
},
b,
)
}
fn cc_threshold_rgb(e: &mut Enc, ctx: &EffectCtx, b: GBuf) -> Option<GBuf> {
let t = [ctx.params.f("redThreshold"), ctx.params.f("greenThreshold"), ctx.params.f("blueThreshold")].map(|v| v as f32 / 255.0);
let inv = [ctx.params.b("invertRed"), ctx.params.b("invertGreen"), ctx.params.b("invertBlue")].map(u32::from);
let blend = ctx.params.f("blend") as f32 / 100.0;
point(
e,
4,
|p| {
p.u[0] = [4, inv[0], inv[1], inv[2]];
p.f[0] = [t[0], t[1], t[2], blend];
},
b,
)
}
fn strobe(e: &mut Enc, ctx: &EffectCtx, b: GBuf) -> Option<GBuf> {
let on = effectcraft_effects::strobe_on(
ctx.time,
ctx.params.f("strobeDuration"),
ctx.params.f("strobePeriod"),
ctx.params.f("randomStrobeProbability") / 100.0,
ctx.seed,
);
if !on {
return Some(b);
}
let blend = (ctx.params.f("blendWithOriginal") as f32 / 100.0).clamp(0.0, 1.0);
if ctx.params.e("strobe") == 1 {
// Image::scale_alpha.
if (blend - 1.0).abs() < 1e-7 {
return Some(b);
}
return point(e, 6, |p| p.f[0][0] = blend, b);
}
let s = ctx.params.color("strobeColor");
let op = ctx.params.e("strobeOperator");
point(
e,
5,
|p| {
p.u[0][1] = op;
p.f[0] = [s[0], s[1], s[2], blend];
},
b,
)
}
fn vignette(e: &mut Enc, ctx: &EffectCtx, b: GBuf) -> Option<GBuf> {
let amt = ctx.params.f("amount") as f32 / 100.0;
let fov = ctx.params.f("angleOfView").clamp(0.0, 179.0).to_radians();
if amt == 0.0 || fov <= 0.0 {
return Some(b);
}
let c = b.to_px(ctx.params.v2("center"));
let pin = ctx.params.f("pinHighlights") as f32 / 100.0;
let (lw, lh) = (ctx.layer_size[0] * b.scale, ctx.layer_size[1] * b.scale);
let half_diag = (lw * lw + lh * lh).sqrt() * 0.5;
let dist = half_diag.max(1.0) / (fov * 0.5).tan();
point(
e,
7,
|p| {
p.f[0] = [c.0 as f32, c.1 as f32, dist as f32, amt];
p.f[1][0] = pin;
},
b,
)
}
// ---------------------------------------------------------------- warps (fxs_warp)
fn warp(e: &mut Enc, mode: u32, f: impl FnOnce(&mut Params), b: GBuf, data: Option<&wgpu::Buffer>) -> Option<GBuf> {
let mut p = Params::default();
p.u[0][0] = mode;
f(&mut p);
run(e, "fxs_warp", &p, b, data)
}
fn mirror(e: &mut Enc, ctx: &EffectCtx, b: GBuf) -> Option<GBuf> {
let c = b.to_px(ctx.params.v2("center"));
let a = ctx.params.f("angle").to_radians();
warp(e, 1, |p| p.f[0] = [c.0 as f32, c.1 as f32, a.cos() as f32, a.sin() as f32], b, None)
}
fn spherize(e: &mut Enc, ctx: &EffectCtx, b: GBuf) -> Option<GBuf> {
let r = (ctx.params.f("radius") * b.scale).max(1.0);
let c = b.to_px(ctx.params.v2("center"));
warp(e, 2, |p| p.f[0] = [c.0 as f32, c.1 as f32, r as f32, 0.0], b, None)
}
fn polar(e: &mut Enc, ctx: &EffectCtx, b: GBuf) -> Option<GBuf> {
let amt = ctx.params.f("interpolation") / 100.0;
let to_polar = ctx.params.e("conversion") == 1;
let (w, h) = (b.img.width as f64, b.img.height as f64);
let (cx, cy) = (w / 2.0, h / 2.0);
let rmax = cx.min(cy);
warp(
e,
3,
|p| {
p.u[0][1] = to_polar as u32;
p.f[0] = [w as f32, h as f32, cx as f32, cy as f32];
p.f[1] = [rmax as f32, amt as f32, 0.0, 0.0];
},
b,
None,
)
}
fn cc_slant(e: &mut Enc, ctx: &EffectCtx, mut b: GBuf) -> Option<GBuf> {
let k = ctx.params.f("slant") / 100.0;
let height = ctx.params.f("height") / 100.0;
if k == 0.0 && (height - 1.0).abs() < 1e-12 {
return Some(b);
}
let stretch = ctx.params.b("stretching");
let xs = if stretch { 1.0 / (1.0 + k * k).sqrt() } else { 1.0 };
let hinv = if height.abs() < 1e-6 { 1e6 } else { 1.0 / height };
if !ctx.adjustment {
let lean = (k * ctx.layer_size[1] * b.scale * height.max(1.0)).abs();
let grow = ((height - 1.0).max(0.0) * ctx.layer_size[1] * b.scale).max(lean);
b.pad(e, grow.ceil().min(4096.0) as u32)?;
}
let fl = b.to_px(ctx.params.v2("floor"));
warp(
e,
4,
|p| {
p.f[0] = [fl.0 as f32, fl.1 as f32, hinv as f32, k as f32];
p.f[1][0] = xs as f32;
},
b,
None,
)
}
fn cc_smear(e: &mut Enc, ctx: &EffectCtx, mut b: GBuf) -> Option<GBuf> {
let reach = ctx.params.f("reach") / 100.0;
if !ctx.adjustment {
b.pad(e, effectcraft_effects::cc_smear_pad(ctx, [b.img.width, b.img.height], b.offset, b.scale))?;
}
let from = b.to_px(ctx.params.v2("from"));
let to = b.to_px(ctx.params.v2("to"));
let radius = (ctx.params.f("radius") * b.scale).max(0.5);
let (dx, dy) = (to.0 - from.0, to.1 - from.1);
let l2 = dx * dx + dy * dy;
if l2 < 1e-12 || reach == 0.0 {
return Some(b);
}
warp(
e,
5,
|p| {
p.f[0] = [from.0 as f32, from.1 as f32, dx as f32, dy as f32];
p.f[1] = [l2 as f32, reach as f32, radius as f32, 0.0];
},
b,
None,
)
}
/// distort3::split_impl (CC Split / CC Split 2).
fn cc_split(e: &mut Enc, ctx: &EffectCtx, b: GBuf, s1: f64, s2: f64) -> Option<GBuf> {
if s1 == 0.0 && s2 == 0.0 {
return Some(b);
}
let a = b.to_px(ctx.params.v2("pointA"));
let c = b.to_px(ctx.params.v2("pointB"));
let (dx, dy) = (c.0 - a.0, c.1 - a.1);
let len = (dx * dx + dy * dy).sqrt();
if len < 1e-6 {
return Some(b);
}
let (ux, uy) = (dx / len, dy / len);
warp(
e,
6,
|p| {
p.f[0] = [a.0 as f32, a.1 as f32, ux as f32, uy as f32];
p.f[1] = [len as f32, s1 as f32, s2 as f32, (len * 0.5) as f32];
},
b,
None,
)
}
fn optics_compensation(e: &mut Enc, ctx: &EffectCtx, mut b: GBuf) -> Option<GBuf> {
let fov = ctx.params.f("fieldOfView").clamp(0.0, 179.0).to_radians();
if fov < 1e-6 {
return Some(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();
let optimal = ctx.params.b("optimalPixels");
let resize = if optimal { 0 } else { ctx.params.e("resize") };
let corners = [(lx, ly), (lx + lw, ly), (lx, ly + lh), (lx + lw, ly + lh)];
if reverse && resize > 0 && !ctx.adjustment {
let c = b.to_px(ctx.params.v2("viewCenter"));
let rc = corners.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(e, grow.min(cap).min(4096.0).ceil() as u32)?;
}
let c = b.to_px(ctx.params.v2("viewCenter"));
let fit = if optimal {
let rc = corners.iter().map(|p| ((p.0 - c.0).powi(2) + (p.1 - c.1).powi(2)).sqrt()).fold(1.0, f64::max);
if reverse {
let th = (rc / r_ref * half).min(FRAC_PI_2 - 1e-3);
(f * th.tan() / rc).max(1.0)
} else {
((rc / f).atan() / half * r_ref / rc).min(1.0)
}
} else {
1.0
};
warp(
e,
7,
|p| {
p.u[0][1] = reverse as u32;
p.f[0] = [c.0 as f32, c.1 as f32, fit as f32, f as f32];
p.f[1] = [r_ref as f32, half as f32, 0.0, 0.0];
},
b,
None,
)
}
fn kaleida(e: &mut Enc, ctx: &EffectCtx, b: GBuf) -> Option<GBuf> {
let c = b.to_px(ctx.params.v2("center"));
let size = ctx.params.f("size").max(1.0) / 100.0;
let mode = ctx.params.e("mirroring");
let rot = ctx.params.f("rotation").to_radians();
let n = [6.0, 8.0, 4.0, 12.0, 3.0][mode.min(4) as usize];
let seg = TAU / n;
warp(
e,
8,
|p| {
p.u[0][1] = (mode != 3) as u32;
p.f[0] = [c.0 as f32, c.1 as f32, size as f32, rot as f32];
p.f[1][0] = seg as f32;
},
b,
None,
)
}
fn liquify(e: &mut Enc, ctx: &EffectCtx, b: GBuf) -> Option<GBuf> {
// View Freeze Area Mask / View Mesh overlays render on the CPU (catalog CPU_ONLY_CONTROLS).
if ctx.params.b("viewFreezeAreaMask") || ctx.params.b("viewMesh") {
return None;
}
if ctx.params.s("distortionMesh").trim().is_empty() {
return Some(b);
}
let pct = ctx.params.f("distortionPercentage") / 100.0;
let mesh = effectcraft_effects::liquify_mesh(ctx);
if mesh.nx < 2 || mesh.ny < 2 {
return None;
}
let off = ctx.params.v2("distortionMeshOffset");
let data: Vec<f32> = mesh.d.iter().flat_map(|d| [d[0] as f32, d[1] as f32]).collect();
let buf = e.data(&data);
let (scale, o) = (b.scale, b.offset);
warp(
e,
9,
|p| {
p.u[0] = [9, mesh.nx as u32, mesh.ny as u32, 0];
p.f[0] = [mesh.cell as f32, off[0] as f32, off[1] as f32, pct as f32];
p.f[1] = [scale as f32, o[0] as f32, o[1] as f32, 0.0];
},
b,
Some(&buf),
)
}
fn twirl_legacy(e: &mut Enc, ctx: &EffectCtx, b: GBuf) -> Option<GBuf> {
let ang = ctx.params.f("angle").to_radians();
if ang == 0.0 {
return Some(b);
}
let (lw, lh) = (ctx.layer_size[0] * b.scale, ctx.layer_size[1] * b.scale);
let r = (ctx.params.f("radius") / 100.0 * lw.min(lh) * 0.5).max(1.0);
let c = b.to_px(ctx.params.v2("center"));
warp(e, 10, |p| p.f[0] = [c.0 as f32, c.1 as f32, r as f32, ang as f32], b, None)
}
fn ripple_pulse(e: &mut Enc, ctx: &EffectCtx, b: GBuf) -> Option<GBuf> {
let level = ctx.params.f("pulseLevel") / 100.0;
let amp = ctx.params.f("amplitude") / 100.0;
if level == 0.0 || amp == 0.0 {
return Some(b);
}
let c = b.to_px(ctx.params.v2("center"));
let span = ctx.params.f("timeSpan").max(0.01);
let size = ctx.layer_size[0].max(ctx.layer_size[1]) * b.scale;
let speed = size * 0.5 / span;
let lambda = (size * 0.06).max(2.0);
let height = level * amp * lambda * 0.5;
let front = speed * ctx.time.max(0.0);
let bump = ctx.params.b("renderBump");
warp(
e,
11,
|p| {
p.u[0][1] = bump as u32;
p.f[0] = [c.0 as f32, c.1 as f32, front as f32, speed as f32];
p.f[1] = [span as f32, lambda as f32, height as f32, 0.0];
},
b,
None,
)
}
fn power_pin(e: &mut Enc, ctx: &EffectCtx, mut b: GBuf) -> Option<GBuf> {
if ctx.params.b("unstretch") {
return None; // Unstretch (reverse pinning) renders on the CPU (catalog CPU_ONLY_CONTROLS).
}
let persp = (ctx.params.f("perspective") / 100.0).clamp(0.0, 1.0);
let (lw, lh) = (ctx.layer_size[0].max(1.0), ctx.layer_size[1].max(1.0));
let ex = [ctx.params.f("expandTop"), ctx.params.f("expandLeft"), ctx.params.f("expandRight"), ctx.params.f("expandBottom")];
let (x0, y0) = (-ex[1] / 100.0 * lw, -ex[0] / 100.0 * lh);
let (x1, y1) = (lw + ex[2] / 100.0 * lw, lh + ex[3] / 100.0 * lh);
let corners = [ctx.params.v2("topLeft"), ctx.params.v2("topRight"), ctx.params.v2("bottomRight"), ctx.params.v2("bottomLeft")];
if !ctx.adjustment {
let mut need = 0.0f64;
for c in &corners {
let (px, py) = b.to_px(*c);
need = need.max(-px).max(-py).max(px - b.img.width as f64).max(py - b.img.height as f64);
}
if need > 0.0 {
b.pad(e, need.ceil().min(4096.0) as u32 + 1)?;
}
}
let q = corners.map(|c| {
let p = b.to_px(c);
vec2(p.0, p.1)
});
let Some(inv) = Mat3::square_to_quad(q).inverse() else { return Some(b) };
let r = inv.0.map(|r| [r[0] as f32, r[1] as f32, r[2] as f32, 0.0]);
let (sc, off) = (b.scale, b.offset);
warp(
e,
12,
|p| {
p.f[2] = r[0];
p.f[3] = r[1];
p.f[4] = r[2];
p.f[5] = [x0 as f32, y0 as f32, (x1 - x0) as f32, (y1 - y0) as f32];
p.f[6] = [sc as f32, off[0] as f32, off[1] as f32, 0.0];
if persp < 1.0 {
let rel = |i: usize| [(q[i].x - q[0].x) as f32, (q[i].y - q[0].y) as f32];
let (q1, q2, q3) = (rel(1), rel(2), rel(3));
p.f[7] = [q[0].x as f32, q[0].y as f32, persp as f32, 1.0];
p.f[8] = [q1[0], q1[1], q2[0], q2[1]];
p.f[9] = [q3[0], q3[1], 0.0, 0.0];
}
},
b,
None,
)
}
fn flo_motion(e: &mut Enc, ctx: &EffectCtx, b: GBuf) -> Option<GBuf> {
let knots = [(ctx.params.v2("knot1"), ctx.params.f("amount1") / 100.0), (ctx.params.v2("knot2"), ctx.params.f("amount2") / 100.0)];
if knots.iter().all(|k| k.1 == 0.0) {
return Some(b);
}
let ss = match ctx.params.e("antialiasing") {
0 => 1,
1 => 2,
_ => 3,
};
let falloff = ctx.params.f("falloff").max(0.01);
let (lw, lh) = (ctx.layer_size[0].max(1.0), ctx.layer_size[1].max(1.0));
let radius = (lw * lw + lh * lh).sqrt() * 0.5;
let mut p = Params::default();
p.u[0] = [ss, ctx.params.b("tileEdges") as u32, 0, 0];
for (i, (k, a)) in knots.iter().enumerate() {
p.f[i] = [k[0] as f32, k[1] as f32, *a as f32, 0.0];
}
p.f[2] = [falloff as f32, radius as f32, lw as f32, lh as f32];
p.f[3] = [b.scale as f32, b.offset[0] as f32, b.offset[1] as f32, 0.0];
run(e, "fxs_flomotion", &p, b, None)
}
// ---------------------------------------------------------------- separable tables
/// Per-column `fx(x + 0.5)` then per-row `fy(y + 0.5)`, `k` values each.
fn axis_table<const K: usize>(w: u32, h: u32, fx: impl Fn(f64) -> [f64; K], fy: impl Fn(f64) -> [f64; K]) -> Vec<f32> {
let mut v = Vec::with_capacity((w + h) as usize * K);
v.extend((0..w).flat_map(|x| fx(x as f64 + 0.5).map(|t| t as f32)));
v.extend((0..h).flat_map(|y| fy(y as f64 + 0.5).map(|t| t as f32)));
v
}
fn table(e: &mut Enc, b: GBuf, data: &[f32], repeat: bool, blend: f32) -> Option<GBuf> {
let mut p = Params::default();
p.u[0] = [b.img.width, repeat as u32, 0, 0];
p.f[0][0] = blend;
let buf = e.data(data);
run(e, "fxs_table", &p, b, Some(&buf))
}
fn offset(e: &mut Enc, ctx: &EffectCtx, b: GBuf) -> Option<GBuf> {
let shift = ctx.params.v2("shift");
let (w, h) = (b.img.width as f64, b.img.height as f64);
let blend = ctx.params.f("blend") as f32 / 100.0;
let (cx, cy) = (w / 2.0, h / 2.0);
let (sx, sy) = b.to_px(shift);
let data = axis_table(b.img.width, b.img.height, |x| [(x - (sx - cx)).rem_euclid(w)], |y| [(y - (sy - cy)).rem_euclid(h)]);
table(e, b, &data, false, blend.max(0.0))
}
fn cc_tiler(e: &mut Enc, ctx: &EffectCtx, mut b: GBuf) -> Option<GBuf> {
let s = (ctx.params.f("scale") / 100.0).max(0.001);
if !ctx.adjustment {
b.pad(e, effectcraft_effects::util::layer_cover_pad(ctx, [b.img.width, b.img.height], b.offset, b.scale))?;
}
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 data = axis_table(b.img.width, b.img.height, |x| [x0 + (c.0 + (x - c.0) / s - x0).rem_euclid(w)], |y| [y0 + (c.1 + (y - c.1) / s - y0).rem_euclid(h)]);
table(e, b, &data, true, blend)
}
fn cc_griddler(e: &mut Enc, ctx: &EffectCtx, b: GBuf) -> Option<GBuf> {
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 Some(b);
}
let (sr, cr) = rot.sin_cos();
let centre = |p: f64, o: f64| {
let tc = o + (((p - o) / t).floor() + 0.5) * t;
[tc, p - tc]
};
let data = axis_table(b.img.width, b.img.height, |x| centre(x, x0), |y| centre(y, y0));
let mut p = Params::default();
p.u[0] = [b.img.width, cut as u32, 0, 0];
p.f[0] = [cr as f32, sr as f32, hs as f32, vs as f32];
p.f[1][0] = (0.45 * t) as f32;
let buf = e.data(&data);
run(e, "fxs_griddler", &p, b, Some(&buf))
}
fn motion_tile(e: &mut Enc, ctx: &EffectCtx, b: GBuf) -> Option<GBuf> {
let s = b.scale;
let (lw, lh) = (ctx.layer_size[0], ctx.layer_size[1]);
if lw <= 0.0 || lh <= 0.0 {
return Some(b);
}
let tc = ctx.params.v2("tileCenter");
let tw = (lw * ctx.params.f("tileWidth") / 100.0).max(0.01);
let th = (lh * ctx.params.f("tileHeight") / 100.0).max(0.01);
let mirror = ctx.params.b("mirrorEdges");
let phase = ctx.params.f("phase") / 360.0;
let hshift = ctx.params.b("horizontalPhaseShift");
let (nw, nh, noff) = effectcraft_effects::motion_tile_frame(ctx, b.offset, (b.img.width, b.img.height), s);
if !e.g.fits(nw, nh) {
return None;
}
let split = |t: f64| [t.floor(), t - t.floor()];
let data = axis_table(nw, nh, |x| split(((x - noff[0]) / s - tc[0]) / tw + 0.5), |y| split(((y - noff[1]) / s - tc[1]) / th + 0.5));
let mut p = Params::default();
p.u[0] = [nw, hshift as u32, mirror as u32, 0];
p.f[0] = [phase as f32, (lw * s) as f32, (lh * s) as f32, 0.0];
p.f[1] = [b.offset[0] as f32, b.offset[1] as f32, 0.0, 0.0];
let buf = e.data(&data);
let out = e.scratch(nw, nh);
e.pixels("fxs_motiontile", &p, &b.img, None, &out, Some(&buf));
Some(GBuf { img: out, offset: noff, scale: s })
}
fn repetile(e: &mut Enc, ctx: &EffectCtx, b: GBuf) -> Option<GBuf> {
let s = b.scale;
let (r, l, d, u) = (
ctx.params.f("expandRight").max(0.0) * s,
ctx.params.f("expandLeft").max(0.0) * s,
ctx.params.f("expandDown").max(0.0) * s,
ctx.params.f("expandUp").max(0.0) * s,
);
if ctx.adjustment || r + l + d + u < 0.5 {
return Some(b);
}
let (x0, y0, w, h) = layer_rect(ctx, &b);
if w < 1.0 || h < 1.0 {
return Some(b);
}
let (nw, nh) = ((w + l + r).round().max(1.0) as u32, (h + u + d).round().max(1.0) as u32);
if !e.g.fits(nw, nh) {
return None;
}
// stylize2::tile_coord.
let tile = |v: f64, size: f64| {
let i = (v / size).floor();
[i, v - i * size]
};
let data = axis_table(nw, nh, |x| tile(x - l, w), |y| tile(y - u, h));
let mut p = Params::default();
p.u[0] = [nw, ctx.params.e("tiling"), 0, 0];
p.f[0] = [x0 as f32, y0 as f32, w as f32, h as f32];
let buf = e.data(&data);
let out = e.scratch(nw, nh);
e.pixels("fxs_repetile", &p, &b.img, None, &out, Some(&buf));
Some(GBuf { img: out, offset: [l, u], scale: s })
}
fn magnify(e: &mut Enc, ctx: &EffectCtx, mut b: GBuf) -> Option<GBuf> {
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 mode = ctx.params.e("blendingMode") as usize;
let blend = effectcraft_effects::MAGNIFY_MODES.get(mode.wrapping_sub(1)).copied();
if ctx.params.b("resizeLayer") && link == 0 && !ctx.adjustment {
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(e, over.min(4096.0).ceil() as u32)?;
}
let c = b.to_px(ctx.params.v2("center"));
let axis = |p: f64, c: f64| {
let d = p - c;
let sp = c + d / mag;
[d, sp, sp.floor() + 0.5]
};
let data = axis_table(b.img.width, b.img.height, |x| axis(x, c.0), |y| axis(y, c.1));
let (kind, mode_id) = match blend {
None => (0, 0),
Some(BlendMode::Normal) => (1, 0),
Some(m) => (2, ops::mode_id(m)),
};
let mut p = Params::default();
p.u[0] = [b.img.width, square as u32, ctx.params.e("scaling"), kind];
p.u[1] = [mode_id, ctx.seed ^ 0x3a61, 0, 0];
p.f[0] = [size as f32, feather as f32, op, mag as f32];
p.f[1][0] = (size - feather) as f32;
let buf = e.data(&data);
run(e, "fxs_magnify", &p, b, Some(&buf))
}
// ---------------------------------------------------------------- stylize2.rs
fn scatter(e: &mut Enc, ctx: &EffectCtx, b: GBuf) -> Option<GBuf> {
let amt = ctx.params.f("amount") * b.scale;
if amt <= 0.0 {
return Some(b);
}
let seed = if ctx.params.b("randomizeEveryFrame") { ctx.seed ^ ((ctx.time * 1000.0) as u32).wrapping_mul(0x9e37_79b9) } else { ctx.seed };
let mut p = Params::default();
p.u[0] = [ctx.params.e("grain"), seed, 0, 0];
p.f[0][0] = amt as f32;
run(e, "fxs_scatter", &p, b, None)
}
fn brush_strokes(e: &mut Enc, ctx: &EffectCtx, b: GBuf) -> Option<GBuf> {
let ang = ctx.params.f("strokeAngle");
let size = (ctx.params.f("brushSize") * b.scale).max(0.5);
let len = ctx.params.f("strokeLength") * b.scale;
let density = ctx.params.f("strokeDensity") as f32;
let rnd = ctx.params.f("strokeRandomness");
let blend = ctx.params.f("blendWithOriginal") as f32 / 100.0;
let seed = ctx.seed ^ 0xb705;
let cell = (size * 2.0).max(1.0);
let (w, h) = (b.img.width, b.img.height);
let cell_of = |i: u32| (i as f64 / cell).floor() as i64 as u32;
let (ncx, ncy) = (cell_of(w.saturating_sub(1)) + 1, cell_of(h.saturating_sub(1)) + 1);
if (ncx as u64 * ncy as u64 * 6 + (w + h) as u64) >= 1 << 24 {
return None;
}
let mut data: Vec<f32> = (0..w).map(|x| cell_of(x) as f32).chain((0..h).map(|y| cell_of(y) as f32)).collect();
data.reserve((ncx * ncy * 6) as usize);
for cy in 0..ncy {
for cx in 0..ncx {
let hh = |k: u32| effectcraft_raster::hash_noise(cx.wrapping_add(k.wrapping_mul(7919)), cy, seed) as f64;
let a = (ang + (hh(0) - 0.5) * rnd * 60.0).to_radians();
let l = len * (0.5 + hh(1));
let (dx, dy) = (a.sin(), -a.cos());
let jit = (hh(2) - 0.5) * size;
let n = l.ceil().clamp(1.0, 32.0);
let m = if (hh(3) as f32) < density.min(1.0) { 1.0 } else { 0.0 };
data.extend([dx as f32, dy as f32, l as f32, jit as f32, n as f32, m]);
}
}
let mut p = Params::default();
p.u[0] = [w, ncx, ctx.params.e("paintSurface"), h];
p.f[0][0] = blend;
let buf = e.data(&data);
run(e, "fxs_brush", &p, b, Some(&buf))
}
fn roughen_edges(e: &mut Enc, ctx: &EffectCtx, mut b: GBuf) -> Option<GBuf> {
let kind = ctx.params.e("edgeType");
let ec = ctx.params.color("edgeColor");
let border = ctx.params.f("border") * b.scale;
let sharp = ctx.params.f("edgeSharpness").max(0.01) as f32;
let infl = ctx.params.f("fractalInfluence") as f32;
let scale = (ctx.params.f("scale") * b.scale * 0.1).max(0.5) as f32;
let (ox, oy) = b.to_px(ctx.params.v2("offset"));
let oct = ctx.params.f("complexity") as f32;
let evo = ctx.params.f("evolution") as f32 / 360.0;
let st = ctx.params.f("stretch") as f32;
let (sx, sy) = (1.0 + st.max(0.0), 1.0 + (-st).max(0.0));
let cycle = ctx.params.b("evolutionOptions/cycleEvolution").then(|| ctx.params.f("evolutionOptions/cycle").round().max(1.0) as f32);
let (evo, evo_w, c) = match cycle {
Some(c) => {
let ev = evo.rem_euclid(c);
(ev, ev / c, c)
}
None => (evo, 0.0, 0.0),
};
let (fm, im, sm, colored) = match kind {
1 => (1.0, 1.0, 1.0, true),
2 => (1.0, 1.0, 3.0, false),
3 => (3.0, 1.0, 1.5, false),
4 => (1.5, 1.5, 1.0, false),
5 => (1.5, 1.5, 1.0, true),
6 => (0.6, 1.2, 2.5, false),
7 => (0.6, 1.2, 2.5, true),
_ => (1.0, 1.0, 1.0, false),
};
let extra = if kind == 4 || kind == 5 { 1.0 } else { 0.0 };
let seed = ctx.seed ^ 0x40f1 ^ (ctx.params.f("evolutionOptions/randomSeed").round() as i64 as u32).wrapping_mul(0x9e37_79b9);
// util::gauss_plane of the alpha (edges repeated); the kernel reads .w.
let d = if border > 0.05 { gaussian_blur(e, &b.img, border * 0.5, border * 0.5, true) } else { b.img.clone() };
let mut p = Params::default();
p.u[0] = [seed, cycle.is_some() as u32, colored as u32, 0];
p.f[0] = [ox as f32, oy as f32, scale * sx, scale * sy];
p.f[1] = [fm, evo, c, evo_w];
p.f[2] = [oct + extra, infl, im, sharp];
p.f[3] = [ec[0], ec[1], ec[2], sm];
let out = e.scratch(b.img.width, b.img.height);
e.pixels("fxs_roughen", &p, &b.img, Some(&d), &out, None);
b.img = out;
Some(b)
}
fn texturize(e: &mut Enc, ctx: &EffectCtx, mut b: GBuf) -> Option<GBuf> {
let ang = ctx.params.f("lightDirection").to_radians();
let k = ctx.params.f("textureContrast") as f32;
if k == 0.0 {
return Some(b);
}
let Some(tex) = ctx.layer_param("textureLayer", true) else { return Some(b) };
if tex.buf.img.is_empty() {
return None;
}
// transition::place_layer, per column / row in the texture buffer's pixels.
let (ls, os) = (ctx.layer_size, tex.size);
let inv = 1.0 / b.scale.max(1e-9);
let mode = ctx.params.e("texturePlacement");
let place = |v: f64, k: usize| {
let p = (v - b.offset[k]) * inv;
let q = match mode {
0 if os[0] >= 1.0 && os[1] >= 1.0 => p.rem_euclid(os[k]),
1 => p + (os[k] - ls[k]) * 0.5,
_ if ls[0] > 0.0 && ls[1] > 0.0 => p * os[k] / ls[k],
_ => p,
};
[q * tex.buf.scale + tex.buf.offset[k]]
};
let data = axis_table(b.img.width, b.img.height, |x| place(x, 0), |y| place(y, 1));
let timg = e.g.upload_image(&tex.buf.img)?;
let mut p = Params::default();
p.u[0][0] = b.img.width;
let buf = e.data(&data);
let luma = e.image(b.img.width, b.img.height);
e.pixels("fxs_tex_luma", &p, &b.img, Some(&timg), &luma, Some(&buf));
let s = 0.7 * b.scale;
let hgt = gaussian_blur(e, &luma, s, s, true);
let mut p = Params::default();
p.f[0] = [ang.cos() as f32, -ang.sin() as f32, k, 0.0];
let out = e.scratch(b.img.width, b.img.height);
e.pixels("fxs_texturize", &p, &b.img, Some(&hgt), &out, None);
b.img = out;
Some(b)
}
// ---------------------------------------------------------------- distort.rs
fn corner_pin(e: &mut Enc, ctx: &EffectCtx, mut b: GBuf) -> Option<GBuf> {
let (w, h) = (ctx.layer_size[0].max(1.0), ctx.layer_size[1].max(1.0));
let q = ["ul", "ur", "lr", "ll"].map(|k| {
let p = b.to_px(ctx.params.v2(k));
vec2(p.0, p.1)
});
let quad = Mat3::square_to_quad(q);
let o = b.to_px([0.0, 0.0]);
let m = quad * Mat3::scale(vec2(1.0 / (w * b.scale), 1.0 / (h * b.scale))) * Mat3::translate(vec2(-o.0, -o.1));
let empty = e.image(b.img.width, b.img.height);
b.img = ops::warp(e, &empty, &b.img, &m, Sampling::Bilinear, BlendMode::Normal, 1.0, 0, None);
Some(b)
}
// ---------------------------------------------------------------- Glow / Transform extras
/// Glow settings the base kernel in `effects.rs` leaves to the CPU (another Glow Operation or
/// the Arbitrary Map) and [`glow`] implements.
pub(crate) fn glow_extra(ctx: &EffectCtx) -> bool {
ctx.params.e("colors") == 2 || effectcraft_effects::glow_operation(ctx) != BlendMode::Add
}
fn dims_xy(dim: u32) -> (f64, f64) {
match dim {
1 => (1.0, 0.0),
2 => (0.0, 1.0),
_ => (1.0, 1.0),
}
}
/// misc::glow with any Glow Operation and the Arbitrary Map.
pub(crate) fn glow(e: &mut Enc, ctx: &EffectCtx, mut b: GBuf) -> Option<GBuf> {
let thr = ctx.params.f("threshold") as f32 / 100.0;
let radius = ctx.params.f("radius") * b.scale;
let intensity = ctx.params.f("intensity") as f32;
let colors = ctx.params.e("colors");
let (kx, ky) = dims_xy(ctx.params.e("glowDimensions"));
if !ctx.adjustment {
b.pad(e, (radius * 1.5).ceil() as u32 + 2)?;
}
let mut p = Params::default();
p.u[0] = [(colors == 1) as u32, (ctx.params.e("based") == 0) as u32, ctx.params.e("colorLooping"), 0];
p.f[0] = [thr, ctx.params.f("colorLoops") as f32, (ctx.params.f("colorPhase") / 360.0) as f32, ctx.params.f("abMidpoint") as f32 / 100.0];
let bright = e.image(b.img.width, b.img.height);
if colors == 2 {
// Arbitrary Map: Curves tables per channel (see effects::curves).
let mut parts = ctx.params.s("arbitraryMap").split('|');
let curves: Vec<Option<effectcraft_effects::Curve>> = (0..3).map(|_| parts.next().and_then(effectcraft_effects::Curve::parse)).collect();
let mut data = vec![0.0f32];
let mut offs = [-1.0f32; 3];
for (i, c) in curves.iter().enumerate() {
let Some(c) = c else { continue };
let (xs, ys, ms, lut) = c.tables();
offs[i] = data.len() as f32;
data.push(xs.len() as f32);
data.extend_from_slice(xs);
data.extend_from_slice(ys);
data.extend_from_slice(ms);
data.extend_from_slice(lut);
}
p.f[1] = [offs[0], offs[1], offs[2], 0.0];
let buf = e.data(&data);
e.pixels("fxs_glow_map", &p, &b.img, None, &bright, Some(&buf));
} else {
p.f[1] = ctx.params.color("colorA");
p.f[2] = ctx.params.color("colorB");
e.pixels("glow_bright", &p, &b.img, None, &bright, None);
}
let s = (radius / 2.0).max(0.5);
let blurred = gaussian_blur(e, &bright, s * kx, s * ky, false);
let operation = ctx.params.e("operation");
let glow_op = effectcraft_effects::glow_operation(ctx);
let mut p = Params::default();
p.f[0][0] = intensity;
let out = e.scratch(b.img.width, b.img.height);
if operation == 0 && glow_op != BlendMode::Add {
p.u[0][0] = ops::mode_id(glow_op);
e.pixels("fxs_glow_op", &p, &b.img, Some(&blurred), &out, None);
} else {
p.u[0][0] = operation.min(2);
e.pixels("glow_combine", &p, &b.img, Some(&blurred), &out, None);
}
b.img = out;
Some(b)
}
/// Transform renders with motion blur (a shutter and a host to read the parameters at other
/// times): [`transform`] implements it, the base kernel in `effects.rs` the sharp case.
pub(crate) fn transform_blur(ctx: &EffectCtx) -> bool {
ctx.env.host.is_some() && effectcraft_effects::transform_shutter(ctx).is_some()
}
/// distort::transform_matrix.
fn transform_matrix(pr: &effectcraft_effects::Params, b: &GBuf) -> Mat3 {
let anchor = b.to_px(pr.v2("anchor"));
let pos = b.to_px(pr.v2("position"));
let sh = pr.f("scaleHeight");
let sw = if pr.b("uniform") { sh } else { pr.f("scaleWidth") };
Mat3::translate(vec2(pos.0, pos.1))
* Mat3::rotate_deg(pr.f("rotation"))
* Mat3::skew_deg(pr.f("skew"), pr.f("skewAxis"))
* Mat3::scale(vec2(sw / 100.0, sh / 100.0))
* Mat3::translate(vec2(-anchor.0, -anchor.1))
}
/// distort::transform's motion blur: the transform at Samples instants across the shutter,
/// averaged.
pub(crate) fn transform(e: &mut Enc, ctx: &EffectCtx, mut b: GBuf) -> Option<GBuf> {
let (angle, phase, n) = effectcraft_effects::transform_shutter(ctx)?;
let host = ctx.env.host?;
let sampling = if ctx.params.e("sampling") == 1 { Sampling::Bicubic } else { Sampling::Bilinear };
let fd = 1.0 / ctx.fps();
let (w, h) = (b.img.width, b.img.height);
let mut acc = e.image(w, h);
let mut count = 0u32;
for k in 0..n {
let t = ctx.time + (phase + angle * k as f64 / (n - 1) as f64) / 360.0 * fd;
let Some(pr) = host.params_at(t) else { continue };
let op = pr.f("opacity") as f32 / 100.0;
let empty = e.image(w, h);
let one = ops::warp(e, &empty, &b.img, &transform_matrix(&pr, &b), sampling, BlendMode::Normal, op, 0, None);
let sum = e.image(w, h);
e.pixels("fxs_add", &Params::default(), &acc, Some(&one), &sum, None);
acc = sum;
count += 1;
}
if count == 0 {
// The CPU renders the frame sharp.
return None;
}
let mut p = Params::default();
p.f[0][0] = count as f32;
let out = e.scratch(w, h);
e.pixels("fxs_div", &p, &acc, None, &out, None);
b.img = out;
Some(b)
}