Browse / github.com/storytold/effectcraft / patch
Keep layer colour in Simple Choker growth beyond -8
Simple choker's negative numbers don't work beyond -10 Simple Choker's negative numbers don't work beyond -20, the positive numbers work up to +100, for compositing we need more. The increasing and decreasing of mattes size is essential for compositing. <img width="1214" height="936" alt="Image" src="https://github.com/user-attachments/assets/49f9c22c-4273-4dc5-af0b-3caafbcaa5f0" /> <img width="1058" height="589" alt="Image" src="https://github.com/user-attachments/assets/9cc5db30-fbdf-4d81-b760-64851a309931" />
- Status
- open
- Score
- 0.00
- Adopters
- 0 other operators
- Author
- Silvertree2010 (imported from pull request #2313, open)
- Base
eb5108786b90bf994a6abe4e6c0b2614c6aeaa29- Size
- +80 −3 in 3 files
- Review
- pending
- Shared
- 2026-10-28
- Id
b6eeceacb85482ac28793425
How it went
No other operator has reported using it yet.
Diff
raw · crates/effects/src/matte.rs crates/effects/src/util.rs crates/gpu/src/fx_key.rs
@@ -6,7 +6,7 @@ use effectcraft_project::ParamUi; use effectcraft_raster::gaussian_blur; use rayon::prelude::*; -use crate::util::{Plane, gauss_plane, guided_filter, morph_frac, morph_plane, premul, set_alpha, unpremul};+use crate::util::{FILL_REACH, Plane, gauss_plane, guided_filter, morph_frac, morph_plane, premul, set_alpha, set_alpha_fill, unpremul}; use crate::{Buf, EffectCtx, EffectSpec, num, p, popup, slider}; fn spec(id: &'static str, name: &'static str, params: Vec<crate::ParamSpec>, render: crate::RenderFn) -> EffectSpec {@@ -28,7 +28,7 @@ fn simple_choker(ctx: &EffectCtx, mut b: Buf) -> Buf { } let a = Plane::alpha(&b.img); let na = if c > 0.0 { morph_frac(&a, c, false) } else { morph_frac(&a, -c, true) };- set_alpha(&mut b.img, &na);+ set_alpha_fill(&mut b.img, &na, c < -FILL_REACH); } if ctx.params.e("view") == 1 { matte_view(&mut b);@@ -349,6 +349,20 @@ mod tests { img.data.iter().map(|p| p[3]).sum() } + #[test]+ fn simple_choker_far_growth_keeps_layer_colour() {+ let mut img = Image::new(100, 100);+ for y in 46..54 {+ for x in 46..54 {+ img.set(x, y, [0.8, 0.4, 0.2, 1.0]);+ }+ }+ let grown = run("ec.matte.simplechoker", &[("chokeMatte", num(-30.0))], img);+ // 25 px from the original edge, well beyond the colour-fill blur's reach.+ let (c, a) = unpremul(grown.get(21, 50));+ assert!(a > 0.99 && c[0] > 0.5, "{c:?} {a}");+ }+ #[test] fn simple_choker_shrinks_and_grows() { let img = square();@@ -377,12 +377,71 @@ pub fn gen_image(w: u32, h: u32, f: impl Fn(usize, usize) -> Px + Sync) -> Image img } +/// Distance (px) up to which the small colour-fill blur in [`set_alpha`] reaches.+pub const FILL_REACH: f64 = 8.0;++/// Where `fill` has (almost) no coverage, copy in the colour of the nearest pixel that+/// has some (jump flooding), so distant growth does not turn black.+fn nearest_fill(fill: &mut Image) {+ let (w, h) = (fill.width as usize, fill.height as usize);+ let mut seed: Vec<Option<u32>> = (0..w * h).map(|i| (fill.data[i][3] > 1e-3).then_some(i as u32)).collect();+ let mut step = w.max(h).next_power_of_two() / 2;+ while step >= 1 {+ let prev = seed.clone();+ for y in 0..h {+ for x in 0..w {+ let i = y * w + x;+ let mut best = prev[i];+ let mut bd = best.map_or(i64::MAX, |s| d2(s as usize, x, y, w));+ for dy in [-(step as isize), 0, step as isize] {+ for dx in [-(step as isize), 0, step as isize] {+ let (nx, ny) = (x as isize + dx, y as isize + dy);+ if nx < 0 || ny < 0 || nx >= w as isize || ny >= h as isize {+ continue;+ }+ if let Some(s) = prev[ny as usize * w + nx as usize] {+ let d = d2(s as usize, x, y, w);+ if d < bd {+ bd = d;+ best = Some(s);+ }+ }+ }+ }+ seed[i] = best;+ }+ }+ step /= 2;+ }+ for i in 0..w * h {+ if fill.data[i][3] <= 1e-3+ && let Some(s) = seed[i]+ {+ fill.data[i] = fill.data[s as usize];+ }+ }+}++fn d2(s: usize, x: usize, y: usize, w: usize) -> i64 {+ let (sx, sy) = ((s % w) as i64, (s / w) as i64);+ (sx - x as i64).pow(2) + (sy - y as i64).pow(2)+}+ /// Replace the alpha of `img` with `new_a`, keeping straight colour. Pixels that were (nearly) /// transparent take their colour from a small blur of the surroundings. pub fn set_alpha(img: &mut Image, new_a: &Plane) {+ set_alpha_fill(img, new_a, false);+}++/// [`set_alpha`]; with `far` the colour also reaches pixels beyond the small blur, from the nearest+/// pixel that has some (for mattes grown by more than [`FILL_REACH`]).+pub fn set_alpha_fill(img: &mut Image, new_a: &Plane, far: bool) { let needs_fill = img.data.par_iter().zip(new_a.data.par_iter()).any(|(p, &a)| p[3] <= 1e-4 && a > 1e-4); let fill = if needs_fill {- let blurred = effectcraft_raster::gaussian_blur(img, 3.0, 3.0, true);+ let mut blurred = effectcraft_raster::gaussian_blur(img, 3.0, 3.0, true);+ if far {+ nearest_fill(&mut blurred);+ } Some(blurred) } else { None@@ -313,6 +313,10 @@ fn key_cleaner(e: &mut Enc, ctx: &EffectCtx, b: GBuf) -> Option<GBuf> { fn simple_choker(e: &mut Enc, ctx: &EffectCtx, mut b: GBuf) -> Option<GBuf> { let c = ctx.params.f("chokeMatte") * b.scale;+ if c < -effectcraft_effects::util::FILL_REACH {+ // The colour fill beyond the small blur (nearest pixel) runs on the CPU.+ return None;+ } if c.abs() > 1e-6 { if c < 0.0 && !ctx.adjustment { b.pad(e, (-c).ceil() as u32 + 1)?;