Merge #2003: Motion Tile's Output size works on adjustment layers (#1727) (@ded-achtung)

This commit is contained in:
aiongg
2026-10-23 23:22:30 +00:00
8 changed files with 280 additions and 15 deletions
+18
View File
@@ -1246,6 +1246,24 @@ impl<'g> Enc<'g> {
);
}
/// A `w` × `h` image showing `src` with its pixel (`x`, `y`) at the top-left, transparent
/// where `src` doesn't reach (Image::crop).
pub fn crop(&mut self, src: &GpuImage, x: i64, y: i64, w: u32, h: u32) -> GpuImage {
let dst = self.image(w, h);
let span = |o: i64, n: u32, len: u32| {
let (a, b) = (o.max(0), (o + n as i64).min(len as i64));
(b > a).then(|| (a as u32, (a - o) as u32, (b - a) as u32))
};
let (Some((sx, dx, cw)), Some((sy, dy, ch))) = (span(x, w, src.width), span(y, h, src.height)) else { return dst };
let enc = self.encoder();
enc.copy_texture_to_texture(
wgpu::TexelCopyTextureInfo { texture: &src.texture, mip_level: 0, origin: wgpu::Origin3d { x: sx, y: sy, z: 0 }, aspect: wgpu::TextureAspect::All },
wgpu::TexelCopyTextureInfo { texture: &dst.texture, mip_level: 0, origin: wgpu::Origin3d { x: dx, y: dy, z: 0 }, aspect: wgpu::TextureAspect::All },
wgpu::Extent3d { width: cw, height: ch, depth_or_array_layers: 1 },
);
dst
}
/// Submit recorded work.
pub fn submit(&mut self) {
if let Some((enc, id)) = self.enc.take() {
+1 -7
View File
@@ -637,13 +637,7 @@ fn motion_tile(e: &mut Enc, ctx: &EffectCtx, b: GBuf) -> Option<GBuf> {
let mirror = ctx.params.b("mirrorEdges");
let phase = ctx.params.f("phase") / 360.0;
let hshift = ctx.params.b("horizontalPhaseShift");
let (nw, nh, noff) = if ctx.adjustment {
(b.img.width, b.img.height, b.offset)
} else {
let ow = (lw * ctx.params.f("outputWidth") / 100.0).max(1.0 / s);
let oh = (lh * ctx.params.f("outputHeight") / 100.0).max(1.0 / s);
((ow * s).round().max(1.0) as u32, (oh * s).round().max(1.0) as u32, [(ow - lw) * 0.5 * s, (oh - lh) * 0.5 * s])
};
let (nw, nh, noff) = effectcraft_effects::motion_tile_frame(ctx, b.offset, (b.img.width, b.img.height), s);
if !e.g.fits(nw, nh) {
return None;
}
+41
View File
@@ -89,6 +89,8 @@ fn tiles_kaleida_repetile() {
"ec.stylize.motiontile",
&[("horizontalPhaseShift", on()), ("phase", n(33.0)), ("tileCenter", pt(30.0, 17.0)), ("tileHeight", n(55.0)), ("outputWidth", n(80.0))],
);
// Output under 100 % on an adjustment crops the comp below (#1727).
effect_case("ec.stylize.motiontile", &[("outputWidth", n(60.0)), ("outputHeight", n(300.0)), ("mirrorEdges", on())]);
effect_case("ec.stylize.cckaleida", &[]);
effect_case("ec.stylize.cckaleida", &[("mirroring", e(2)), ("rotation", n(20.0)), ("size", n(150.0))]);
effect_case("ec.stylize.ccrepetile", &[("expandRight", n(10.0)), ("expandUp", n(4.0))]);
@@ -225,3 +227,42 @@ fn transform_motion_blur() {
check(&format!("{label} half"), compare_at(&s, RenderOpts { scale: 0.5, ..opts() }, Tick::from_seconds_f64(0.3)), 0.0);
}
}
/// Motion Tile's Output size on an adjustment layer with a Transform after it that scales the
/// tiles into view, and cropping (Output under 100 %): the stack's grown or cropped frame goes
/// back onto the canvas as on the CPU (#1727).
#[test]
fn adjustment_motion_tile_output_then_transform() {
for (out_w, out_h, scale) in [(300.0, 300.0, 40.0), (250.0, 120.0, 70.0), (60.0, 80.0, 100.0)] {
for depth in [BitDepth::Bpc8, BitDepth::Bpc32] {
let mut s = Scene::new(depth);
let bg = s.footage(97, 61);
s.push(bg);
let mut adj = s.solid([1.0, 1.0, 1.0], 97, 61);
adj.switches.adjustment = true;
s.effect(&mut adj, "ec.stylize.motiontile", &[("outputWidth", n(out_w)), ("outputHeight", n(out_h)), ("mirrorEdges", on())]);
s.effect(&mut adj, "ec.distort.transform", &[("scaleHeight", n(scale)), ("scaleWidth", n(scale))]);
s.push(adj);
let label = format!("adjustment Motion Tile {out_w}×{out_h} % then Scale {scale} % {depth:?}");
check(&label, compare_at(&s, opts(), Tick::ZERO), 0.0);
check(&format!("{label} half"), compare_at(&s, RenderOpts { scale: 0.5, ..opts() }, Tick::ZERO), 0.0);
}
}
}
/// The same on a full-frame footage layer (#1991).
#[test]
fn layer_motion_tile_output_then_transform() {
for (out_w, out_h, scale) in [(300.0, 300.0, 40.0), (250.0, 120.0, 70.0)] {
for depth in [BitDepth::Bpc8, BitDepth::Bpc32] {
let mut s = Scene::new(depth);
let mut l = s.footage(97, 61);
s.effect(&mut l, "ec.stylize.motiontile", &[("outputWidth", n(out_w)), ("outputHeight", n(out_h)), ("mirrorEdges", on())]);
s.effect(&mut l, "ec.distort.transform", &[("scaleHeight", n(scale)), ("scaleWidth", n(scale))]);
s.push(l);
let label = format!("layer Motion Tile {out_w}×{out_h} % then Scale {scale} % {depth:?}");
check(&label, compare_at(&s, opts(), Tick::ZERO), 0.0);
check(&format!("{label} half"), compare_at(&s, RenderOpts { scale: 0.5, ..opts() }, Tick::ZERO), 0.0);
}
}
}
+5 -1
View File
@@ -357,7 +357,11 @@ fn draw_adjustment(e: &mut Enc, r: &Renderer, ctx: &EvalCtx, layer: &Layer, canv
};
let mut fx = crate::effects::GpuFx::new(e, below, [0.0; 2], r.opts.scale, pipe.levels);
r.run_effects_on(ctx, layer, true, &mut fx);
let (mut adjusted, _, _) = fx.finish()?;
let (mut adjusted, offset, _) = fx.finish()?;
// An effect that changed the frame (Motion Tile's Output size): back onto the canvas.
if let Some((dx, dy)) = effectcraft_effects::adjustment_crop(offset, [0.0; 2], (adjusted.width, adjusted.height), (w, h)) {
adjusted = e.crop(&adjusted, dx, dy, w, h);
}
if adjusted.width != w || adjusted.height != h {
return Some(());
}