From 8eece41c7efed630ef0e66cc9153d4a22592ef67 Mon Sep 17 00:00:00 2001 From: Kristian Myram Date: Mon, 12 Oct 2026 19:41:35 +0200 Subject: [PATCH] #1063: keep the Gradient Ramp inside the layer's alpha The Gradient Ramp wrote alpha 1.0 to every pixel, so a shape layer's transparent area filled up with the ramp and the layer's bounds showed instead of the shape. The ramp now uses the pixel's own alpha on the CPU and in the GPU kernel, as the #939 fix does for the noise effects. A regression test checks that transparent pixels stay transparent at two Blend With Original values. Co-Authored-By: Claude Haiku 5.5 --- crates/effects/src/generate.rs | 22 +++++++++++++++++++++- crates/gpu/src/shaders/kernels.wgsl | 3 ++- 2 files changed, 23 insertions(+), 2 deletions(-) diff --git a/crates/effects/src/generate.rs b/crates/effects/src/generate.rs index 2b543352..b29ff915 100644 --- a/crates/effects/src/generate.rs +++ b/crates/effects/src/generate.rs @@ -44,7 +44,9 @@ fn gradient_ramp(ctx: &EffectCtx, mut b: Buf) -> Buf { } let t = t.clamp(0.0, 1.0); let c = [c0[0] + (c1[0] - c0[0]) * t, c0[1] + (c1[1] - c0[1]) * t, c0[2] + (c1[2] - c0[2]) * t]; - put(px, c, 1.0, blend); + // The ramp fills only where the layer has alpha (#1063), so it keeps the source alpha. + let a = px[3]; + put(px, c, a, blend); } }); b @@ -400,6 +402,24 @@ mod generator_tests { Image::filled(32, 32, [1.0, 0.0, 0.0, 1.0]) } + #[test] + fn gradient_ramp_stays_inside_the_layer_alpha() { + // #1063: a shape's transparent pixels stay transparent; the ramp fills only the shape. + 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 = if i % 40 < 10 { 0.0 } else { 1.0 }; + *p = p.map(|c| c * a); + } + for blend in [0.0, 50.0] { + let o = run_fx("ec.generate.gradientramp", &[("blend", num(blend))], img.clone(), 0.0, EffectEnv::default()); + for (src, out) in img.data.iter().zip(&o.img.data) { + assert!((src[3] - out[3]).abs() < 1e-6, "blend {blend}: alpha {} became {}", src[3], out[3]); + assert!(out[..3].iter().all(|&c| c <= out[3] + 1e-6), "blend {blend}: premultiplied colour above alpha"); + } + assert!(o.img.data.iter().any(|p| p[3] > 0.99), "blend {blend}: the ramp is drawn inside the shape"); + } + } + #[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/kernels.wgsl b/crates/gpu/src/shaders/kernels.wgsl index 406472e7..defbb5bf 100644 --- a/crates/gpu/src/shaders/kernels.wgsl +++ b/crates/gpu/src/shaders/kernels.wgsl @@ -806,7 +806,8 @@ fn pointwise(@builtin(global_invocation_id) gid: vec3) { t = clamp(t, 0.0, 1.0); let c0 = P.f[1].xyz; let c1 = P.f[2].xyz; - px = put(px, c0 + (c1 - c0) * t, 1.0, P.f[3].z); + // Keep the source alpha, as the CPU path does (#1063): the ramp fills only the shape. + px = put(px, c0 + (c1 - c0) * t, px.w, P.f[3].z); } case 10u: { let contrast = P.f[0].x;