CC Simple Wire Removal: Frame Offset takes the strip from the previous or next frame (#533)

This commit is contained in:
MarekSpodBiedry 2026-10-10 19:10:57 +02:00
parent 1446e27d85
commit cbe740dce8
3 changed files with 69 additions and 1 deletions

View File

@ -340,6 +340,24 @@ fn wire_removal(ctx: &EffectCtx, mut b: Buf) -> Buf {
let (nx, ny) = (-ty, tx);
let src = b.img.clone();
let soft_w = half * slope as f64;
// Frame Offset: the same pixels from the previous or next frame, resampled into this grid.
let offset = ctx.params.f("frameOffset").round();
let neighbour = if style == 1 && offset != 0.0 {
ctx.env.host.and_then(|h| h.self_at(ctx.time + offset / ctx.fps(), ctx.env.effect_index)).map(|nb| {
if nb.img.width == b.img.width && nb.img.height == b.img.height && nb.offset == b.offset && nb.scale == b.scale {
nb.img
} else {
let k = nb.scale / b.scale.max(1e-9);
crate::util::gen_image(b.img.width, b.img.height, |x, y| {
let lx = (x as f64 + 0.5 - b.offset[0]) * k + nb.offset[0];
let ly = (y as f64 + 0.5 - b.offset[1]) * k + nb.offset[1];
nb.img.sample_bilinear(lx, ly)
})
}
})
} else {
None
};
b.img.rows_mut().for_each(|(y, row)| {
for (x, px) in row.iter_mut().enumerate() {
let (qx, qy) = (x as f64 + 0.5 - ax, y as f64 + 0.5 - ay);
@ -357,7 +375,9 @@ fn wire_removal(ctx: &EffectCtx, mut b: Buf) -> Buf {
let t = ((d + edge) / (2.0 * edge)) as f32;
lerp4(pos(-edge), pos(edge), t.clamp(0.0, 1.0))
}
// Displace: shift the image across the wire from the nearer side.
1 if neighbour.is_some() => neighbour.as_ref().map_or(*px, |n| n.get(x as i64, y as i64)),
// Displace: shift the image across the wire from the nearer side (also Frame Offset
// when no other frame is available).
2 | 1 => {
let s = if d >= 0.0 { d + 2.0 * (half - ad) + 1.0 } else { d - 2.0 * (half - ad) - 1.0 };
let sh = pos(s);
@ -560,6 +580,49 @@ mod tests {
}
}
#[test]
fn wire_removal_frame_offset_uses_neighbour_frame() {
use crate::{EffectHost, LayerPixels};
struct Frames;
impl EffectHost for Frames {
fn layer(&self, _: u64, _: bool) -> Option<LayerPixels> {
None
}
fn audio(&self, _: u64, _: f64, _: usize, _: u32) -> Option<Vec<f32>> {
None
}
fn self_at(&self, t: f64, _: usize) -> Option<Buf> {
// Frame n (at 10 fps) is a flat grey of n / 10.
let v = (t * 10.0).round() as f32 / 10.0;
Some(Buf { img: Image::filled(20, 20, [v, v, v, 1.0]), offset: [0.0; 2], scale: 1.0 })
}
}
let mut img = Image::filled(20, 20, [0.5, 0.5, 0.5, 1.0]);
for y in 0..20 {
img.set(10, y, [0.0, 0.0, 0.0, 1.0]);
}
let host = Frames;
let run = |style: u32, off: f64| {
let env = EffectEnv { host: Some(&host), frame_rate: 10.0, ..Default::default() };
let vals = [
("pointA", Value::Vec2([10.5, 0.0])),
("pointB", Value::Vec2([10.5, 20.0])),
("removalStyle", Value::Enum(style)),
("thickness", num(3.0)),
("frameOffset", num(off)),
];
run_fx("ec.key.ccsimplewireremoval", &vals, img.clone(), 0.5, env).img
};
// At 0.5 s (frame 5) offset -1 shows frame 4 and +1 shows frame 6 inside the strip.
let (prev, next) = (run(1, -1.0), run(1, 1.0));
assert!((prev.get(10, 10)[0] - 0.4).abs() < 1e-4, "{:?}", prev.get(10, 10));
assert!((next.get(10, 10)[0] - 0.6).abs() < 1e-4, "{:?}", next.get(10, 10));
// Outside the strip the current frame stays.
assert_eq!(prev.get(2, 10), img.get(2, 10));
// Displace ignores Frame Offset.
assert_eq!(run(2, -1.0).data, run(2, 1.0).data);
}
#[test]
fn inner_outer_key_more_masks_and_cleanup_strokes() {
let sq = |x0: f64, x1: f64| MaskShape { name: String::new(), points: vec![[x0, 2.0], [x1, 2.0], [x1, 8.0], [x0, 8.0]], closed: true, inverted: false };

View File

@ -476,6 +476,10 @@ fn wire_removal(e: &mut Enc, ctx: &EffectCtx, b: GBuf) -> Option<GBuf> {
if half <= 0.0 {
return Some(b);
}
// Frame Offset reads another frame from the effect host: that renders on the CPU.
if ctx.params.e("removalStyle") == 1 && ctx.params.f("frameOffset").round() != 0.0 && ctx.env.host.is_some() {
return None;
}
let slope = (ctx.params.f("slope") / 100.0).clamp(0.0, 1.0) as f32;
let mirror = (ctx.params.f("mirrorBlend") / 100.0).clamp(0.0, 1.0) as f32;
let a = b.to_px(ctx.params.v2("pointA"));

View File

@ -243,6 +243,7 @@ fn keys_and_3d() {
effect_case("ec.key.ccsimplewireremoval", &[]);
effect_case("ec.key.ccsimplewireremoval", &[("removalStyle", e(0)), ("thickness", n(8.0)), ("slope", n(50.0))]);
effect_case("ec.key.ccsimplewireremoval", &[("removalStyle", e(1)), ("mirrorBlend", n(60.0)), ("pointB", pt(60.0, 10.0))]);
effect_case("ec.key.ccsimplewireremoval", &[("removalStyle", e(1)), ("frameOffset", n(-2.0)), ("thickness", n(6.0))]);
effect_case("ec.key.ccsimplewireremoval", &[("removalStyle", e(3)), ("mirrorBlend", n(30.0)), ("slope", n(20.0))]);
effect_case("ec.obsolete.basic3d", &[("swivel", n(25.0))]);
effect_case("ec.obsolete.basic3d", &[("tilt", n(-30.0)), ("swivel", n(10.0)), ("specularHighlight", on())]);