diff --git a/crates/effects/src/blur2.rs b/crates/effects/src/blur2.rs index e4f2b034..929f4155 100644 --- a/crates/effects/src/blur2.rs +++ b/crates/effects/src/blur2.rs @@ -509,11 +509,24 @@ fn camera_lens_blur(ctx: &EffectCtx, mut b: Buf) -> Buf { // --------------------------------------------------------------------------------------------- // CC Radial Fast Blur +/// Transparent pixels CC Radial Fast Blur adds on every side of a `w` × `h` buffer with Center +/// at `c` (buffer pixels) and smear `s`: content `r` from Center streaks out to `r / (1 − s)`, +/// so the farthest corner reaches `d · s / (1 − s)` past itself (at most 4096, as the other +/// growing effects). Shared with the GPU path so both grow the same way. +pub fn radial_fast_pad(c: (f64, f64), w: u32, h: u32, s: f64) -> u32 { + let (w, h) = (f64::from(w), f64::from(h)); + let far = [(0.0, 0.0), (w, 0.0), (0.0, h), (w, h)].iter().map(|&(x, y)| (x - c.0).hypot(y - c.1)).fold(0.0, f64::max); + ((far * s / (1.0 - s)).ceil() + 1.0).min(4096.0) as u32 +} + 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; } + if !ctx.adjustment { + b.pad(radial_fast_pad(b.to_px(ctx.params.v2("center")), b.img.width, b.img.height, s)); + } let c = b.to_px(ctx.params.v2("center")); let mode = ctx.params.e("zoom"); let src = b.img.clone(); @@ -906,6 +919,23 @@ mod tests { assert_eq!(circle.len(), 21); } + #[test] + fn radial_fast_blur_streaks_past_the_layer_edge() { + // Center left of an opaque square: the streaks run right, beyond the layer rectangle (#2095). + let img = Image::filled(20, 20, [1.0, 1.0, 1.0, 1.0]); + let out = run("ec.blur.ccradialfast", &img, &[("center", Value::Vec2([-20.0, 10.0])), ("amount", num(80.0))]); + assert!(out.img.width > 20 && out.img.height > 20, "{}x{}", out.img.width, out.img.height); + let (o, d) = (out.offset[0] as i64, out.offset[1] as i64); + assert!(out.img.get(30 + o, 10 + d)[3] > 0.1, "{:?}", out.img.get(30 + o, 10 + d)); + // Its streak samples only the square: still opaque, so the content keeps its place. + assert!((out.img.get(19 + o, 10 + d)[3] - 1.0).abs() < 1e-5, "{:?}", out.img.get(19 + o, 10 + d)); + // An adjustment layer is comp-sized already; its buffer is not grown. + let s = find("ec.blur.ccradialfast").unwrap(); + let params = Params { values: [("center".into(), Value::Vec2([-20.0, 10.0])), ("amount".into(), num(80.0))].into_iter().collect() }; + let ctx = EffectCtx { params: ¶ms, time: 0.0, layer_size: [20.0, 20.0], seed: 1, adjustment: true, env: Default::default() }; + assert_eq!((s.render)(&ctx, Buf { img, offset: [0.0, 0.0], scale: 1.0 }).img.width, 20); + } + #[test] fn reduce_flicker_blurs_vertically_only() { let mut img = Image::filled(8, 8, [0.0, 0.0, 0.0, 1.0]); diff --git a/crates/effects/src/lib.rs b/crates/effects/src/lib.rs index 88a46f1e..ea63244d 100644 --- a/crates/effects/src/lib.rs +++ b/crates/effects/src/lib.rs @@ -91,7 +91,7 @@ pub use sim::particle_state; pub use sim3::{PgBlit, PgPlan, playground_plan, playground_state}; pub use stylize2::strobe_on; // CPU helpers the GPU kernels share (effectcraft-gpu). -pub use blur2::{camera_lens_plain, camera_lens_spans}; +pub use blur2::{camera_lens_plain, camera_lens_spans, radial_fast_pad}; pub use generate::{four_color_softening, gen_mode}; pub use generate2::pattern_kind as cell_pattern_kind; pub use generate2::{BoltSeg, Contour, Ghost, RadioPlan, Wave, flare_ghosts, lightning_segments, radio_plan}; diff --git a/crates/gpu/src/fx_generate.rs b/crates/gpu/src/fx_generate.rs index 0216d390..0089dd4f 100644 --- a/crates/gpu/src/fx_generate.rs +++ b/crates/gpu/src/fx_generate.rs @@ -117,11 +117,14 @@ fn radial_blur(e: &mut Enc, ctx: &EffectCtx, b: GBuf) -> Option { run(e, "gen_radial_blur", &p, b, None, Some(&data)) } -fn radial_fast(e: &mut Enc, ctx: &EffectCtx, b: GBuf) -> Option { +fn radial_fast(e: &mut Enc, ctx: &EffectCtx, mut b: GBuf) -> Option { let s = (ctx.params.f("amount") / 100.0).clamp(0.0, 1.0) * 0.5; if s <= 0.0 { return Some(b); } + if !ctx.adjustment { + b.pad(e, effectcraft_effects::radial_fast_pad(b.to_px(ctx.params.v2("center")), b.img.width, b.img.height, s))?; + } let c = b.to_px(ctx.params.v2("center")); let mut p = Params::default(); p.u[0][0] = ctx.params.e("zoom"); diff --git a/crates/gpu/src/tests_fx_generate.rs b/crates/gpu/src/tests_fx_generate.rs index e15abb37..ebe2f2bf 100644 --- a/crates/gpu/src/tests_fx_generate.rs +++ b/crates/gpu/src/tests_fx_generate.rs @@ -31,6 +31,8 @@ fn cc_radial_fast_blur() { effect_case("ec.blur.ccradialfast", &[("amount", n(40.0))]); effect_case("ec.blur.ccradialfast", &[("amount", n(70.0)), ("zoom", e(1)), ("center", pt(22.4, 16.1))]); effect_case("ec.blur.ccradialfast", &[("amount", n(25.0)), ("zoom", e(2))]); + // Center outside the layer: the buffer grows so the streaks pass its edge (#2095). + effect_case("ec.blur.ccradialfast", &[("amount", n(80.0)), ("center", pt(-30.0, 20.0))]); } #[test]