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; diff --git a/crates/gpu/src/tests_fx_tone.rs b/crates/gpu/src/tests_fx_tone.rs index 61c977d1..08212a12 100644 --- a/crates/gpu/src/tests_fx_tone.rs +++ b/crates/gpu/src/tests_fx_tone.rs @@ -183,3 +183,8 @@ fn color_stabilizer() { } } } + +#[test] +fn gradient_ramp_matches_the_cpu() { + effect_case("ec.generate.gradientramp", &[]); +}