From 4ad860cb69c8fdca97fc97ce84e63ae81c340bb5 Mon Sep 17 00:00:00 2001 From: Marco Arnulfo Date: Tue, 13 Oct 2026 04:23:52 +0200 Subject: [PATCH] Adjustment layers: a Transform replaces the picture below instead of drawing over it (fixes #1524) --- crates/render/src/adjustment_tests.rs | 20 ++++++++++++++++++++ crates/render/src/lib.rs | 25 +++++++++++++++++-------- 2 files changed, 37 insertions(+), 8 deletions(-) diff --git a/crates/render/src/adjustment_tests.rs b/crates/render/src/adjustment_tests.rs index 3d6eaa68..4625eca1 100644 --- a/crates/render/src/adjustment_tests.rs +++ b/crates/render/src/adjustment_tests.rs @@ -156,6 +156,26 @@ fn motion_and_masks_restrict_the_region() { assert!(near(img.get(280, 90), [1.0, 0.0, 0.0], 1e-4)); } +#[test] +fn a_transform_replaces_the_picture_below() { + // Premiere: Transform on an adjustment layer moves the composite below; it is not a copy + // drawn over the untouched original (#1524). + let mut s = scene(2); + let red = matte(&mut s.p, &mut s.map, [1.0, 0.0, 0.0, 1.0]); + place(&mut s.p, s.seq, 0, red); + let adj = adjustment(&mut s.p, s.seq, 1); + fx(&mut s, adj, "transform"); + let tf = clip(&mut s, adj).effects.iter_mut().find(|e| e.effect == "transform").unwrap(); + tf.params.get_mut("scale_height").unwrap().value = ParamValue::Float(50.0); + let img = render(&s); + assert!(near(img.get(160, 90), [1.0, 0.0, 0.0], 1e-4), "{:?}", img.get(160, 90)); + assert_eq!(img.get(10, 10), [0.0; 4], "the original is gone outside the scaled picture"); + // at 50 % opacity: half the original, half the (empty) transformed picture + clip(&mut s, adj).effect_mut("opacity").unwrap().params.get_mut("opacity").unwrap().value = ParamValue::Float(50.0); + let c = render(&s).get(10, 10); + assert!((c[0] - 0.5).abs() < 1e-3 && (c[3] - 0.5).abs() < 1e-3, "{c:?}"); +} + #[test] fn nested_adjustment_layers_stay_in_their_sequence() { let mut s = scene(2); diff --git a/crates/render/src/lib.rs b/crates/render/src/lib.rs index 22d6629f..0854cf75 100644 --- a/crates/render/src/lib.rs +++ b/crates/render/src/lib.rs @@ -235,18 +235,27 @@ pub(crate) fn render_seq_tracks(project: &Project, seq: &Sequence, t: Tick, opts mask::apply_effect(&mut adjusted, e, &cx); } let (op, bl) = opacity_blend(item, mt); - // Adjustment layer opacity (and opacity masks, in sequence pixels) mix adjusted over original. - let mut out = canvas.snapshot(); - let mut adj = adjusted; - adj.scale_alpha(op); - mask::apply_opacity_masks(&mut adj, item, mt, opts.scale); + // Adjustment layer opacity (and opacity masks, in sequence pixels) weigh adjusted against original. + let mut weight = Image::filled(w, h, [op; 4]); + mask::apply_opacity_masks(&mut weight, item, mt, opts.scale); // Motion moves / scales the adjustment layer's frame: it only applies inside it. if let Some(region) = adjustment_region(seq, item, project, mt, opts.scale, w, h) { let cov: Vec = region.px.as_chunks::<4>().0.iter().map(|p| p[3]).collect(); - mask::scale_by(&mut adj, &cov); + mask::scale_by(&mut weight, &cov); + } + let cov: Vec = weight.px.as_chunks::<4>().0.iter().map(|p| p[3]).collect(); + let mut out = canvas.snapshot(); + let mut adj = adjusted; + if bl == Blend::Normal { + // The adjusted picture replaces the original instead of being drawn over it: what + // Transform or Basic 3D leave empty shows nothing of the untransformed picture (#1524). + mask::mix(&mut adj, &out, &cov); + canvas.set(adj); + } else { + mask::scale_by(&mut adj, &cov); + blend::composite(&mut out, &adj, 1.0, bl); + canvas.set(out); } - blend::composite(&mut out, &adj, 1.0, bl); - canvas.set(out); continue; } match item_layer_ex(project, seq, item, t, opts, sources, &tc, true) {