Effects: effect points on shape layers start at the layer centre (#227)

Shape and text layers have comp-sized effect bounds centred on their
origin, but effects measured positions from the origin: a point at its
default "layer centre" sat at the comp's bottom-right corner, so Twirl
on a centred shape changed nothing and Bulge only its bottom-right
quarter. As in After Effects, effects now work in effect space,
measured from the top-left of the layer's effect bounds.

The renderer moves the layer buffer into effect space for the whole
stack (Buf::rebase, CPU and GPU alike) and hands effects everything
else in it: masks, other layers' pixels for layer parameters (each in
its own effect space, so Set Matte and displacement maps see a whole
shape layer), the layer's own frames (layer_input, also what Puppet,
Roto Brush and the Layer panel read) and the comp camera
(CompScene::rebased). Layer bounds are now right for every effect, so
pin_rect and EffectEnv::bounds_origin go and layer_rect serves edge
pinning too. Adjustment layers still run on the comp below as it is.

effect_bounds / EvalCtx::effect_to_comp replace the repeated "comp size
when there is no source" code; the viewer's effect point handles and
eyedroppers, Puppet pins, the Layer panel (paint, Roto Brush) and the
tracker's Corner Pin output use them.

Project schema 2: opening a schema 1 project moves positions held by
effects on shape and text layers by half the comp size, so set points
keep their place (point parameters with keyframes, Paint paths and
clone positions, Puppet seeds and pins, Liquify and Roto Brush
strokes). Points still at their default become the layer centre;
expression controls, adjustment layers and expressions are left as
they are (docs/architecture.md). The disk cache's render version is
bumped.

Tests: default_effect_points_sit_at_the_centre_of_a_shape_layer and
layer_parameters_see_a_whole_shape_layer (render, both fail before),
effect_points_on_a_shape_layer (GPU vs CPU),
opening_a_schema_1_project_moves_shape_layer_effect_points_into_effect_space
(engine), and the effects migration tests.

Part of #227

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
bflatastic
2026-10-08 20:33:03 +07:00
co-authored by Claude Opus 5.5
parent 52ab38fcff
commit 940bea3ba7
36 changed files with 683 additions and 153 deletions
-2
View File
@@ -253,8 +253,6 @@ pub(crate) fn chain_key(chain: &[FxStep], buf: &Buf, levels: Option<f32>) -> u12
k.debug(&e.working_space);
k.u64(e.working_linear as u64);
k.debug(&e.shutter);
k.f64(e.bounds_origin[0]);
k.f64(e.bounds_origin[1]);
}
k.finish()
}
+3 -3
View File
@@ -96,7 +96,7 @@ fn bulge(e: &mut Enc, ctx: &EffectCtx, b: GBuf) -> Option<GBuf> {
let h = ctx.params.f("height");
let taper = 1.0 + ctx.params.f("taperRadius").max(0.0) / 100.0 * 3.0;
let pin = ctx.params.b("pinAllEdges");
let (x0, y0, lw, lh) = util::pin_rect(ctx, b.offset, b.scale);
let (x0, y0, lw, lh) = util::layer_rect_at(ctx, b.offset, b.scale);
let edge = (lw.min(lh) * 0.1).max(1.0);
let mut p = Params::default();
p.f[0] = [c.0 as f32, c.1 as f32, rx as f32, ry as f32];
@@ -115,7 +115,7 @@ fn wave_warp(e: &mut Enc, ctx: &EffectCtx, mut b: GBuf) -> Option<GBuf> {
if !ctx.adjustment {
b.pad(e, h.abs().ceil() as u32 + 1)?;
}
let (x0, y0, lw, lh) = util::pin_rect(ctx, b.offset, b.scale);
let (x0, y0, lw, lh) = util::layer_rect_at(ctx, b.offset, b.scale);
let rect = [x0 as f32, y0 as f32, lw.max(1.0) as f32, lh.max(1.0) as f32];
// The wave phase in cycles, (x·dx + y·dy) / width + phase / 2π, is separable: per column
// and per row (fraction, whole cycles) in f64, so the kernel's sum keeps the fraction exact
@@ -183,7 +183,7 @@ fn turbulent(e: &mut Enc, ctx: &EffectCtx, mut b: GBuf) -> Option<GBuf> {
let evo = ctx.params.f("evolution") / 360.0;
let cycle = ctx.params.b("evolutionOptions/cycleEvolution").then(|| ctx.params.f("evolutionOptions/cycle").round().max(1.0));
let falloff = if (3..=5).contains(&kind) { 0.3 } else { 0.5 };
let (x0, y0, lw, lh) = util::pin_rect(ctx, b.offset, b.scale);
let (x0, y0, lw, lh) = util::layer_rect_at(ctx, b.offset, b.scale);
let edge = (lw.min(lh) * 0.1).max(1.0);
let seed = (ctx.seed ^ 0x7d15).wrapping_add((ctx.params.f("evolutionOptions/randomSeed") as i64 as u32).wrapping_mul(0x9e37_79b9));
let (z0, z1, t) = match cycle {
+1 -1
View File
@@ -125,7 +125,7 @@ fn run(e: &mut Enc, entry: &str, p: &Params, mut b: GBuf, data: Option<&wgpu::Bu
/// util::layer_rect.
fn layer_rect(ctx: &EffectCtx, b: &GBuf) -> (f64, f64, f64, f64) {
(b.offset[0], b.offset[1], ctx.layer_size[0] * b.scale, ctx.layer_size[1] * b.scale)
effectcraft_effects::util::layer_rect_at(ctx, b.offset, b.scale)
}
// ---------------------------------------------------------------- per-pixel (fxs_point)
+2 -2
View File
@@ -160,8 +160,8 @@ impl Scene {
pub(crate) fn effect(&mut self, l: &mut Layer, id: &str, vals: &[(&str, Value)]) {
let spec = effectcraft_effects::find(id).unwrap();
let mut next = self.p.next_id;
let size = effectcraft_render::source_size(&self.p, l);
let mut g = effectcraft_effects::instantiate(spec, &mut Ids(&mut next), spec.name, [size.0 as f64, size.1 as f64]);
let size = effectcraft_render::effect_bounds(&self.p, &self.comp, l).0;
let mut g = effectcraft_effects::instantiate(spec, &mut Ids(&mut next), spec.name, size);
self.p.next_id = next;
for (k, v) in vals {
g.prop_mut(k).unwrap_or_else(|| panic!("{id}: no {k}")).value = v.clone();
+36 -20
View File
@@ -55,28 +55,44 @@ fn turbulent_displace_and_cc_lens() {
effect_case("ec.distort.cclens", &[("convergence", n(-70.0))]);
}
/// Edge pinning on a shape layer, whose comp-sized bounds are centred on its origin (#227).
/// Effect `id` on a shape layer, whose comp-sized bounds are centred on its origin and whose
/// effects run in effect space from the bounds' top-left (#227), at full and half resolution
/// (the 97 × 61 comp puts that corner between pixels).
fn shape_case(id: &str, vals: &[(&str, Value)]) {
let mut s = Scene::new(BitDepth::Bpc32);
let mut l = build::layer(&mut s.p, &s.comp, "Shape", LayerSource::Shape, (97, 61), None);
let mut next = s.p.next_id;
let mut ids = Ids(&mut next);
let rect = build::shape_rect(&mut ids, [90.0, 54.0], [0.0, 0.0], 0.0);
let fill = build::shape_fill(&mut ids, [0.9, 0.3, 0.1, 1.0]);
let g = build::shape_group(&mut ids, "Rectangle 1", vec![rect, fill]);
s.p.next_id = next;
l.props.sub_mut("contents").unwrap().children.push(g.into());
s.effect(&mut l, id, vals);
s.push(l);
check(&format!("{id} on a shape layer"), compare_at(&s, opts(), Tick::ZERO), 0.0);
check(&format!("{id} on a shape layer, half"), compare_at(&s, RenderOpts { scale: 0.5, ..opts() }, Tick::ZERO), 0.0);
}
/// Edge pinning on a shape layer (#227).
#[test]
fn edge_pinning_on_a_shape_layer() {
let cases: [(&str, &[(&str, Value)]); 3] = [
("ec.distort.turbulentdisplace", &[("size", n(30.0)), ("amount", n(20.0))]),
("ec.distort.wavewarp", &[("pinning", e(1)), ("height", n(6.0)), ("width", n(25.0))]),
("ec.distort.bulge", &[("pinAllEdges", on()), ("height", n(2.0)), ("hradius", n(40.0)), ("vradius", n(30.0)), ("center", Value::Vec2([-20.0, -10.0]))]),
];
for (id, vals) in cases {
let mut s = Scene::new(BitDepth::Bpc32);
let mut l = build::layer(&mut s.p, &s.comp, "Shape", LayerSource::Shape, (97, 61), None);
let mut next = s.p.next_id;
let mut ids = Ids(&mut next);
let rect = build::shape_rect(&mut ids, [90.0, 54.0], [0.0, 0.0], 0.0);
let fill = build::shape_fill(&mut ids, [0.9, 0.3, 0.1, 1.0]);
let g = build::shape_group(&mut ids, "Rectangle 1", vec![rect, fill]);
s.p.next_id = next;
l.props.sub_mut("contents").unwrap().children.push(g.into());
s.effect(&mut l, id, vals);
s.push(l);
check(&format!("{id} on a shape layer"), compare_at(&s, opts(), Tick::ZERO), 0.0);
}
shape_case("ec.distort.turbulentdisplace", &[("size", n(30.0)), ("amount", n(20.0))]);
shape_case("ec.distort.wavewarp", &[("pinning", e(1)), ("height", n(6.0)), ("width", n(25.0))]);
shape_case(
"ec.distort.bulge",
&[("pinAllEdges", on()), ("height", n(2.0)), ("hradius", n(40.0)), ("vradius", n(30.0)), ("center", Value::Vec2([28.5, 20.5]))],
);
}
/// Effect points on a shape layer at their defaults (the bounds' centre) and set (#227).
#[test]
fn effect_points_on_a_shape_layer() {
shape_case("ec.distort.twirl", &[("angle", n(120.0))]);
shape_case("ec.distort.bulge", &[("hradius", n(30.0)), ("vradius", n(20.0)), ("height", n(1.8))]);
shape_case("ec.distort.ripple", &[("radius", n(80.0)), ("waveWidth", n(9.0)), ("waveHeight", n(4.0))]);
shape_case("ec.distort.cclens", &[("size", n(45.0)), ("convergence", n(60.0)), ("center", Value::Vec2([30.0, 20.0]))]);
shape_case("ec.distort.turbulentdisplace", &[("size", n(25.0)), ("amount", n(30.0)), ("offset", Value::Vec2([10.0, 40.0]))]);
}
#[test]