mirror of
https://github.com/storytold/effectcraft.git
synced 2026-10-31 05:27:08 -06:00
Effects: text and shape layers run their stack over the whole layer region (#659).
An authored layer's effect buffer spanned only its painted content — the glyph bounding box for text, the drawn bounds for shapes — while the size the stack measures from (effect_bounds) is the whole comp-sized layer rectangle. Tiling and displacement effects computed correct output for pixels outside the painted rectangle and then wrote them into the buffer that was too small, so CC Tiler repeated only within the text box and CC Smear lost its liquid trail at the glyph edge (#658). Buf::cover now grows the stack's input to the layer's rectangle with transparent margins — what a sized source's buffer already is — applied by run_effect_stack whenever an enabled video effect will run, and natively on the GPU by reallocating and copying the texture. The placement and the sized-source paths are unchanged; two regression tests (CC Tiler spans the region, CC Smear trails past the text bounds) fail without the growth.
This commit is contained in:
@@ -377,6 +377,16 @@ pub trait FxTarget {
|
||||
fn gpu(&mut self, steps: &[FxStep]) -> bool;
|
||||
/// Run one CPU effect: `f` maps the buffer.
|
||||
fn cpu(&mut self, f: &mut dyn FnMut(Buf) -> Buf);
|
||||
/// Grow the buffer to cover its whole layer rectangle (`size` layer pixels from its own
|
||||
/// corner, the `layer_size` handed to effects), as [`effectcraft_effects::Buf::cover`].
|
||||
/// Authored buffers (text, shapes) only span their painted content; this gives the stack
|
||||
/// the room a sized source's buffer already has (#659).
|
||||
fn cover(&mut self, size: [f64; 2]) {
|
||||
self.cpu(&mut |mut buf| {
|
||||
buf.cover(size);
|
||||
buf
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/// The CPU effect target (GPU chains go through [`Accelerator::effects`]: upload, readback).
|
||||
@@ -991,6 +1001,13 @@ impl<'a> Renderer<'a> {
|
||||
let Some(fx) = layer.effects() else { return };
|
||||
let lt = layer.layer_time(ctx.time);
|
||||
let layer_size = ctx.effect_bounds(layer).0;
|
||||
// #659: an authored layer's buffer spans only its painted content while the stack works
|
||||
// on the layer's whole region (`layer_size`): give the buffer the room a sized source
|
||||
// already has, so tiling or displacement can place pixels outside the content instead
|
||||
// of losing them at its edge. Only when something will actually run.
|
||||
if fx.groups().any(|g| g.enabled && matches!(&g.kind, GroupKind::Effect { effect } if effectcraft_effects::find(effect).is_some_and(|s| !effectcraft_effects::audio_fx::is_audio_effect(s.id)))) {
|
||||
target.cover(layer_size);
|
||||
}
|
||||
let mut mask_shapes = masks::shapes(ctx, layer);
|
||||
if origin != [0.0; 2] {
|
||||
for q in mask_shapes.iter_mut().flat_map(|m| m.points.iter_mut()) {
|
||||
|
||||
@@ -597,6 +597,59 @@ fn default_effect_points_sit_at_the_centre_of_a_shape_layer() {
|
||||
}
|
||||
}
|
||||
|
||||
/// #659: a tiling effect on a text layer tiles across the layer's whole region. The glyphs
|
||||
/// were cut off at their own bounding box: the effect computed over the comp-sized layer rect
|
||||
/// but wrote into the painted (glyph-bounds) buffer alone. Tiling shrinks each copy, so the
|
||||
/// probe is the x-span of the painted pixels, not their count.
|
||||
#[test]
|
||||
fn effects_on_text_reach_past_the_text_bounds() {
|
||||
let (mut p, cid, comp) = setup();
|
||||
let mut l = build::layer(&mut p, &comp, "Text", LayerSource::Text, (200, 100), None);
|
||||
l.props.prop_mut("text/sourceText").unwrap().value = Value::Text(Box::new(TextDoc { text: "HI".into(), size: 24.0, ..Default::default() }));
|
||||
l.props.prop_mut("transform/position").unwrap().value = Value::Vec3([50.0, 60.0, 0.0]);
|
||||
p.comp_mut(cid).unwrap().layers.push(l.clone());
|
||||
let plain = render_frame(&p, cid, Tick::ZERO, 1.0);
|
||||
let painted = plain.data.iter().filter(|px| px[3] > 0.5).count();
|
||||
add_effect_200(&mut p, &mut l, "ec.distort.cctiler", &[("scale", Value::Scalar(25.0))]);
|
||||
p.comp_mut(cid).unwrap().layers = vec![l];
|
||||
let tiled = render_frame(&p, cid, Tick::ZERO, 1.0);
|
||||
let (mut lo, mut hi) = (u32::MAX, 0u32);
|
||||
for y in 0..100 {
|
||||
for x in 0..200 {
|
||||
if tiled.get(x as i64, y as i64)[3] > 0.5 {
|
||||
lo = lo.min(x);
|
||||
hi = hi.max(x);
|
||||
}
|
||||
}
|
||||
}
|
||||
assert!(
|
||||
painted > 50 && hi.saturating_sub(lo) > 100,
|
||||
"CC Tiler tiles the glyphs across the layer region, not just its text bounds: {painted} painted, x-span {lo}..{hi}"
|
||||
);
|
||||
}
|
||||
|
||||
/// #658: CC Smear drags text glyphs beyond the text layer's own bounds. Pixels smeared past
|
||||
/// the painted rectangle used to fall off its edge, so the liquid trail never appeared.
|
||||
#[test]
|
||||
fn cc_smear_trails_text_past_its_bounds() {
|
||||
let (mut p, cid, comp) = setup();
|
||||
let mut l = build::layer(&mut p, &comp, "Text", LayerSource::Text, (200, 100), None);
|
||||
l.props.prop_mut("text/sourceText").unwrap().value = Value::Text(Box::new(TextDoc { text: "HI".into(), size: 24.0, ..Default::default() }));
|
||||
l.props.prop_mut("transform/position").unwrap().value = Value::Vec3([60.0, 60.0, 0.0]);
|
||||
p.comp_mut(cid).unwrap().layers.push(l.clone());
|
||||
let right = |img: &effectcraft_raster::Image| (0..100).map(|y| (0..200).filter(|x| img.get(*x, y)[3] > 0.5).max().unwrap_or(0)).max().unwrap_or(0);
|
||||
let plain = right(&render_frame(&p, cid, Tick::ZERO, 1.0));
|
||||
add_effect_200(
|
||||
&mut p,
|
||||
&mut l,
|
||||
"ec.distort.ccsmear",
|
||||
&[("from", Value::Vec2([110.0, 45.0])), ("to", Value::Vec2([185.0, 45.0])), ("reach", Value::Scalar(100.0)), ("radius", Value::Scalar(40.0))],
|
||||
);
|
||||
p.comp_mut(cid).unwrap().layers = vec![l];
|
||||
let smeared = right(&render_frame(&p, cid, Tick::ZERO, 1.0));
|
||||
assert!(plain > 40 && smeared > plain + 25, "CC Smear trails the glyphs past the text bounds: right edge {plain} → {smeared}");
|
||||
}
|
||||
|
||||
/// A layer parameter pointing at a shape layer sees all of it (in its effect space), and the
|
||||
/// effect's own masks are in effect space too: Set Matte with a centred shape layer as the
|
||||
/// matte keeps the centre of a full-comp solid (#227).
|
||||
|
||||
Reference in New Issue
Block a user