fix(effects): cover Particle World's Texture Layer buffer before sampling it

Text and shape layers hand over a buffer that spans only their content;
the clamped sampling then stretched its edge pixels across the squares.
This commit is contained in:
Marco Arnulfo committed 2026-10-30 00:52:10 +01:00
1 parent a76c65bd00
commit efd6ab97dc
2 files changed
+41 -2

No files matched your search

+6 -2
View File
@@ -49,8 +49,12 @@ pub(crate) fn particle_world_textured(ctx: &EffectCtx) -> bool {
pub(super) fn particle_world(ctx: &EffectCtx, b: Buf) -> Buf {
// The texture at the current comp time, shared by every shutter sample. A layer the host
// can't give (the effect's own layer with its effects, a reference cycle) leaves the
// squares in the particle colour.
let tex = particle_world_textured(ctx).then(|| ctx.layer_param(PW_TEXTURE_LAYER, true)).flatten();
// squares in the particle colour. Authored buffers (text, shapes) span only their content:
// covering the whole layer keeps the clamped sampling from stretching their edge pixels.
let tex = particle_world_textured(ctx).then(|| ctx.layer_param(PW_TEXTURE_LAYER, true)).flatten().map(|mut t| {
t.buf.cover(t.size);
t
});
let (w, h) = (b.img.width, b.img.height);
let draw = |plan: &SpritePlan| match &tex {
Some(t) => splat_textured(w, h, &plan.sprites, plan.acc, t),
+35
View File
@@ -398,6 +398,41 @@ fn particle_world_textured_square_motion_trails_draw_the_texture_layer() {
assert!(seen.iter().any(|p| p[1] > 2.0 * p[2]) && seen.iter().any(|p| p[2] > 2.0 * p[1]), "both texture halves");
}
/// A host whose every layer is `0` as given (a buffer that may span only its content).
struct PixelsHost(crate::LayerPixels);
impl crate::EffectHost for PixelsHost {
fn layer(&self, _: u64, _: bool) -> Option<crate::LayerPixels> {
Some(self.0.clone())
}
fn audio(&self, _: u64, _: f64, _: usize, _: u32) -> Option<Vec<f32>> {
None
}
}
/// #1465: a Texture Layer whose buffer spans only its content (text, shapes) is mapped as
/// the whole layer, transparent around the content, not with its edge pixels stretched.
#[test]
fn particle_world_texture_layer_with_a_content_sized_buffer() {
// An 8×8 layer with a green 4×4 square in the middle: once as the full layer, once as
// just the square's pixels.
let mut full = Image::new(8, 8);
for y in 2..6 {
for x in 2..6 {
full.set(x, y, [0.0, 1.0, 0.0, 1.0]);
}
}
let content = Image::filled(4, 4, [0.0, 1.0, 0.0, 1.0]);
let render = |buf: Buf| {
let host = PixelsHost(crate::LayerPixels { buf, size: [8.0, 8.0] });
let env = EffectEnv { host: Some(&host), ..Default::default() };
run_fx("ec.sim.ccparticleworld", &pw_textured_vals(Some(1)), Image::new(48, 48), 1.0, env).img
};
let want = render(Buf { img: full, offset: [0.0; 2], scale: 1.0 });
let got = render(Buf { img: content, offset: [-2.0, -2.0], scale: 1.0 });
assert!(want.data.iter().any(|p| p[3] > 0.05), "particles drawn");
assert_eq!(want.data, got.data, "the content-sized buffer draws as the full layer");
}
#[test]
fn particle_systems2_seek_and_direction() {
let v = vec![