From 7df16a5e20f7c24a3c702d6c963651fb7b09b85c Mon Sep 17 00:00:00 2001 From: Marco Arnulfo Date: Wed, 14 Oct 2026 02:56:17 +0200 Subject: [PATCH] Effects: 4-Color Gradient keeps the layer's alpha with Blending Mode None (refs #1381) --- crates/effects/src/generate.rs | 24 +++++++++++++++++++++++- crates/gpu/src/shaders/fx_generate.wgsl | 6 ++++-- 2 files changed, 27 insertions(+), 3 deletions(-) diff --git a/crates/effects/src/generate.rs b/crates/effects/src/generate.rs index b29ff915..fd5a5412 100644 --- a/crates/effects/src/generate.rs +++ b/crates/effects/src/generate.rs @@ -138,7 +138,10 @@ fn four_color(ctx: &EffectCtx, mut b: Buf) -> Buf { let n = (effectcraft_raster::hash_noise(x as u32, y as u32, seed) - 0.5) * jitter * (8.0 / 255.0); c = c.map(|v| v + n); } - *px = gen_blend(*px, [c[0] * op, c[1] * op, c[2] * op, op], mode); + // None keeps the layer's alpha (#1381), as the Gradient Ramp does: the gradient fills + // a shape or text, not its bounding box. + let a = if gen_mode(mode).is_none() { op * px[3] } else { op }; + *px = gen_blend(*px, [c[0] * a, c[1] * a, c[2] * a, a], mode); } }); b @@ -420,6 +423,25 @@ mod generator_tests { } } + #[test] + fn four_color_none_stays_inside_the_layer_alpha() { + // #1381: like the Gradient Ramp, the gradient fills a shape or text, not its bounding box. + let mut img = Image::filled(40, 24, [1.0, 0.0, 0.0, 1.0]); + for (i, p) in img.data.iter_mut().enumerate() { + let a = [0.0, 0.5, 1.0][i % 40 / 14]; + *p = p.map(|c| c * a); + } + for op in [100.0, 50.0] { + let o = run_fx("ec.generate.fourcolor", &[("opacity", num(op))], img.clone(), 0.0, EffectEnv::default()); + let k = op as f32 / 100.0; + for (src, out) in img.data.iter().zip(&o.img.data) { + assert!((src[3] * k - out[3]).abs() < 1e-6, "opacity {op}: alpha {} became {}", src[3], out[3]); + assert!(out[..3].iter().all(|&c| c <= out[3] + 1e-6), "opacity {op}: premultiplied colour above alpha"); + } + assert!(o.img.data.iter().any(|p| p[1] > 0.1), "opacity {op}: the gradient replaces the layer's colour"); + } + } + #[test] fn checkerboard_blending_none_shows_pattern_only_and_size_from_width() { let set = [("width", num(8.0)), ("height", num(2.0))]; diff --git a/crates/gpu/src/shaders/fx_generate.wgsl b/crates/gpu/src/shaders/fx_generate.wgsl index 60fc2f9d..f46b2f0b 100644 --- a/crates/gpu/src/shaders/fx_generate.wgsl +++ b/crates/gpu/src/shaders/fx_generate.wgsl @@ -510,8 +510,10 @@ fn gen_fourcolor(@builtin(global_invocation_id) gid: vec3) { let n = (hash_noise(u32(p.x), u32(p.y), P.u[0].y) - 0.5) * jitter * (8.0 / 255.0); c += vec3(n); } - let op = P.f[8].z; - textureStore(out, p, gen_blend(textureLoad(src, p, 0), vec4(c * op, op), P.u[0].x)); + let o = textureLoad(src, p, 0); + // None keeps the layer's alpha, as the CPU path does (#1381). + let op = select(P.f[8].z, P.f[8].z * o.w, P.u[0].x == 255u); + textureStore(out, p, gen_blend(o, vec4(c * op, op), P.u[0].x)); } // ---------------------------------------------------------------- Noise / Add Grain