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:
bflatasticandClaude Opus 5.5 committed 2026-10-30 11:43:19 +07:00
1 parent e56c916085
commit 94c8bde240
4 files changed
+54 -4

No files matched your search

+6
View File
@@ -510,6 +510,12 @@ fn layer_styles_collapsed_precomps_and_cpu_fallbacks() {
adj.switches.adjustment = true;
s.effect(&mut adj, "ec.channel.invert", &[]);
s.push(adj);
// An adjustment layer in another blend mode blends the adjusted comp in that mode (#2618).
let mut adj = s.solid([1.0, 1.0, 1.0], 70, 40);
adj.switches.adjustment = true;
adj.blend_mode = BlendMode::Screen;
set(&mut adj, "transform/opacity", Value::Scalar(60.0));
s.push(adj);
check(&format!("collapse + adjustment {depth:?}"), compare_at(&s, opts(), Tick::ZERO), 0.0);
}
}
+9 -3
View File
@@ -352,8 +352,8 @@ fn draw_precomp(e: &mut Enc, r: &Renderer, ctx: &EvalCtx, layer: &Layer, canvas:
/// Adjustment layer (`Renderer::draw_adjustment`): the effect stack runs on the comp below,
/// resident on the GPU (non-GPU effects read back, run on the CPU and upload), and the result
/// replaces the canvas inside the layer's footprint × track matte × opacity. `None` = draw it
/// on the CPU.
/// replaces the canvas inside the layer's footprint × track matte × opacity, or blends onto it in
/// the layer's blend mode when that isn't Normal. `None` = draw it on the CPU.
fn draw_adjustment(e: &mut Enc, r: &Renderer, ctx: &EvalCtx, layer: &Layer, canvas: &mut GpuImage) -> Option<()> {
let opacity = r.layer_opacity(ctx, layer);
let Some(foot) = r.adjustment_footprint(ctx, layer) else { return Some(()) };
@@ -379,7 +379,13 @@ fn draw_adjustment(e: &mut Enc, r: &Renderer, ctx: &EvalCtx, layer: &Layer, canv
if let Some(c) = pipe.to_blend() {
adjusted = ops::convert(e, &adjusted, &c);
}
*canvas = ops::adjust_mix(e, canvas, &adjusted, &matte, opacity);
*canvas = if layer.blend_mode == BlendMode::Normal {
ops::adjust_mix(e, canvas, &adjusted, &matte, opacity)
} else {
// The adjusted comp is a layer in that mode over the comp below (#2618).
let iso = ops::matte(e, &adjusted, &matte, effectcraft_project::MatteKind::Alpha);
ops::blend_full(e, canvas, &iso, layer.blend_mode, opacity, layer.id.0 as u32, None)
};
Some(())
}
+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();