Adjustment layers blend in their blend mode (#2618)

An adjustment layer replaced the comp below with its effects' result whatever its blend
mode. As in After Effects, a mode other than Normal now blends the adjusted comp onto the
comp below in that mode, inside the footprint, track matte and opacity (an adjustment
layer with no effects in Screen screens the comp with itself). CPU and GPU paths.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
bflatastic
2026-10-30 11:43:19 +07:00
co-authored by Claude Opus 5.5
parent e56c916085
commit 94c8bde240
4 changed files with 54 additions and 4 deletions
+13 -1
View File
@@ -522,7 +522,7 @@ impl<'a> Renderer<'a> {
}
/// Adjustment layer: effects applied to the comp below, limited to the layer's footprint
/// and its track matte.
/// and its track matte, and blended back in the layer's blend mode.
fn draw_adjustment(&self, ctx: &EvalCtx, layer: &Layer, canvas: &mut Image, opacity: f32) {
let Some(mut foot) = self.source(ctx, layer) else { return };
let _ = masks::apply(ctx, layer, &mut foot, self.mask_blur(ctx, layer));
@@ -548,6 +548,18 @@ impl<'a> Renderer<'a> {
if adjusted.img.width != canvas.width || adjusted.img.height != canvas.height {
return;
}
if layer.blend_mode != BlendMode::Normal {
// The adjusted comp is a layer in that mode over the comp below, as in After Effects:
// an adjustment layer with no effects in Screen screens the comp with itself (#2618).
let mut iso = adjusted.img;
iso.data.par_iter_mut().zip(matte.data.par_iter()).for_each(|(p, m)| {
for c in p.iter_mut() {
*c *= m[3];
}
});
canvas.blend_from(&iso, layer.blend_mode, opacity, layer.id.0 as u32);
return;
}
canvas.data.par_iter_mut().zip(adjusted.img.data.par_iter()).zip(matte.data.par_iter()).for_each(|((c, a), m)| {
let k = m[3] * opacity;
for i in 0..4 {
+26
View File
@@ -206,6 +206,32 @@ fn track_matte_limits_an_adjustment_layer() {
}
}
/// An adjustment layer's blend mode blends the adjusted comp onto the comp below, inside its
/// footprint: with no effects, Screen screens a 0.25 grey with itself, Multiply darkens it,
/// Normal leaves it, and Opacity mixes toward the result (#2618).
#[test]
fn adjustment_layer_blends_in_its_mode() {
for (mode, opacity, want) in
[(BlendMode::Normal, 100.0, 0.25), (BlendMode::Screen, 100.0, 0.4375), (BlendMode::Multiply, 100.0, 0.0625), (BlendMode::Screen, 50.0, 0.34375)]
{
let (mut p, cid, comp) = setup();
let mut adj = solid(&mut p, &comp, [1.0, 1.0, 1.0], 100, 100);
adj.props.prop_mut("transform/position").unwrap().value = Value::Vec3([50.0, 50.0, 0.0]);
adj.props.prop_mut("transform/opacity").unwrap().value = Value::Scalar(opacity);
adj.switches.adjustment = true;
adj.blend_mode = mode;
let bg = solid(&mut p, &comp, [0.25, 0.25, 0.25], 200, 100);
let c = p.comp_mut(cid).unwrap();
c.layers.push(adj);
c.layers.push(bg);
let img = render_frame(&p, cid, Tick::ZERO, 1.0);
let (inside, outside) = (img.get(50, 50), img.get(150, 50));
assert!((inside[0] - want).abs() < 1e-4, "{mode:?} {opacity}: {inside:?}");
assert!((outside[0] - 0.25).abs() < 1e-4, "{mode:?} {opacity}: outside the footprint {outside:?}");
assert_eq!(inside[3], 1.0, "{mode:?} keeps the comp opaque");
}
}
#[test]
fn shape_layer_draws_fill_and_stroke() {
let (mut p, cid, comp) = setup();