diff --git a/crates/effects/src/sim/particle_fx.rs b/crates/effects/src/sim/particle_fx.rs index 382913964..c2f493ab5 100644 --- a/crates/effects/src/sim/particle_fx.rs +++ b/crates/effects/src/sim/particle_fx.rs @@ -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), diff --git a/crates/effects/src/sim/tests.rs b/crates/effects/src/sim/tests.rs index 59780ca74..7166e3f0a 100644 --- a/crates/effects/src/sim/tests.rs +++ b/crates/effects/src/sim/tests.rs @@ -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 { + Some(self.0.clone()) + } + fn audio(&self, _: u64, _: f64, _: usize, _: u32) -> Option> { + 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![