collective

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

diff --git a/crates/effects/src/matte.rs b/crates/effects/src/matte.rs--- a/crates/effects/src/matte.rs+++ b/crates/effects/src/matte.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();diff --git a/crates/effects/src/util.rs b/crates/effects/src/util.rs--- a/crates/effects/src/util.rs+++ b/crates/effects/src/util.rs@@ -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 {         Nonediff --git a/crates/gpu/src/fx_key.rs b/crates/gpu/src/fx_key.rs--- a/crates/gpu/src/fx_key.rs+++ b/crates/gpu/src/fx_key.rs@@ -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)?;