Merge pull request #951 from HEOJUNFO/fix-939-noise-mask

Effects: Fractal and Turbulent Noise stay inside the layer's alpha (#939)
This commit is contained in:
Brandon Thomas 2026-10-12 04:21:08 -04:00
commit 7864ea41ae
3 changed files with 31 additions and 8 deletions

View File

@ -429,7 +429,8 @@ pub fn overflow(v: f32, mode: u32) -> f32 {
}
/// Composite grey noise `val` over the original pixel by Blending Mode, Opacity and (ours) Blend
/// With Original.
/// With Original. The noise stays inside the layer's alpha, so a masked solid shows it only within
/// its masks (#939).
pub(crate) fn composite(orig: [f32; 4], val: f32, opacity: f32, mode: u32, blend_orig: f32) -> [f32; 4] {
let src = [val * opacity, val * opacity, val * opacity, opacity];
let out = match mode {
@ -440,7 +441,7 @@ pub(crate) fn composite(orig: [f32; 4], val: f32, opacity: f32, mode: u32, blend
}
};
let k = 1.0 - blend_orig;
[0, 1, 2, 3].map(|c| orig[c] * blend_orig + out[c] * k)
[0, 1, 2, 3].map(|c| orig[c] * blend_orig + out[c] * orig[3] * k)
}
fn render(ctx: &EffectCtx, mut b: Buf, turbulent: bool) -> Buf {
@ -574,6 +575,28 @@ mod tests {
assert!(mul.data.iter().any(|p| p[2] > p[0] + 0.01));
}
#[test]
fn noise_stays_inside_the_layer_alpha() {
// #939: a masked solid (transparent outside the mask, a soft edge) filled the whole layer.
let mut img = Image::filled(40, 24, [0.2, 0.4, 0.6, 1.0]);
for (i, p) in img.data.iter_mut().enumerate() {
let a = match i % 40 {
0..10 => 0.0,
10..20 => 0.5,
_ => 1.0,
};
*p = p.map(|c| c * a);
}
for mode in [BLEND_NONE, BLEND_NORMAL, 3] {
let out = run_fx("ec.noise.fractal", &[("blendingMode", Value::Enum(mode))], img.clone(), 0.0, EffectEnv::default()).img;
for (o, p) in img.data.iter().zip(&out.data) {
assert!((o[3] - p[3]).abs() < 1e-6, "mode {mode}: alpha {} became {}", o[3], p[3]);
assert!(p[..3].iter().all(|&c| c <= p[3] + 1e-6), "mode {mode}: premultiplied colour above alpha");
}
assert!(out.data.iter().any(|p| p[3] == 1.0 && (p[0] - 0.2).abs() > 0.01), "mode {mode}: noise drawn inside the mask");
}
}
#[test]
fn sub_settings_and_fractal_types_change_the_result() {
let base = run("ec.noise.fractal", &[]);

View File

@ -665,7 +665,7 @@ fn fxn_fractal(@builtin(global_invocation_id) gid: vec3<u32>) {
r = blend_pixel(P.u[2].x, orig, s, 0.5);
}
let bo = P.f[1].w;
textureStore(out, p, orig * bo + r * (1.0 - bo));
textureStore(out, p, orig * bo + r * orig.a * (1.0 - bo));
}
// ---------------------------------------------------------------- Noise Alpha / Noise HLS (noise.rs)

View File

@ -865,13 +865,13 @@ fn pointwise(@builtin(global_invocation_id) gid: vec3<u32>) {
val = 1.0 - val;
}
let op = P.f[2].z;
var fx = vec4<f32>(vec3<f32>(val) * op, op);
if (P.u[1].z == 1u) {
// Normal: the noise over the layer at Opacity, then Blend With Original.
let over = vec4<f32>(vec3<f32>(val) * op, op) + px * (1.0 - op);
px = px * P.f[2].y + over * (1.0 - P.f[2].y);
} else {
px = put(px, vec3<f32>(val), op, P.f[2].y);
// Normal: the noise over the layer at Opacity.
fx = fx + px * (1.0 - op);
}
// Inside the layer's alpha (its masks), then Blend With Original.
px = px * P.f[2].y + fx * px.w * (1.0 - P.f[2].y);
}
default: {}
}