From 6d5e86601f1d3f1eddb67b58a62b015479678729 Mon Sep 17 00:00:00 2001 From: Brandon Thomas Date: Sat, 3 Oct 2026 04:57:05 -0400 Subject: [PATCH] M9.13: Cell Pattern HQ variants, Advanced Spill Suppressor Ultra controls, Inner/Outer Key masks and cleanup - Cell Pattern: HQ variants search a wider cell neighbourhood and anti-alias. - Advanced Spill Suppressor: Tolerance, Desaturate, Spill Color Correction. - Inner/Outer Key: Additional Foreground / Background 1-10 and Cleanup Foreground / Background 1-10 brush strokes (older single masks migrate to slot 1). Co-Authored-By: Claude Opus 5.5 --- crates/effects/src/catalog.rs | 3 - crates/effects/src/generate2.rs | 50 ++++++++++++--- crates/effects/src/keying.rs | 53 ++++++++++++++- crates/effects/src/keying2.rs | 110 ++++++++++++++++++++++++++++---- crates/effects/src/lib.rs | 24 +++++++ crates/effects/src/migrate.rs | 20 ++++++ docs/effects.md | 10 +-- 7 files changed, 236 insertions(+), 34 deletions(-) diff --git a/crates/effects/src/catalog.rs b/crates/effects/src/catalog.rs index 9966bfa9d..8c501a3c6 100644 --- a/crates/effects/src/catalog.rs +++ b/crates/effects/src/catalog.rs @@ -12,9 +12,6 @@ pub const PARTIAL: &[(&str, &str)] = &[ ("ec.distort.rollingshutterrepair", "Pixel Motion Detail has no effect"), ("ec.generate.advancedlightning", "no Alpha Obstacle; most Expert Settings missing"), ("ec.generate.scribble", "no caps, joins or Start/End Apply To"), - ("ec.generate.cellpattern", "HQ variants render like the standard ones"), - ("ec.key.innerouter", "one additional mask per side; no cleanup strokes"), - ("ec.key.advancedspill", "no Tolerance, Desaturate or Spill Color Correction"), ("ec.keying.keylight", "no Source Crops X/Y Method or Edge Colour; no colour suppression / balancing"), ("ec.obsolete.pathtext", "no kerning, shear/scale, line spacing, fade time or jitter"), ("ec.noise.addgrain", "no preview region, presets, channel balance or temporal controls"), diff --git a/crates/effects/src/generate2.rs b/crates/effects/src/generate2.rs index 9dc26333b..2234fc6a3 100644 --- a/crates/effects/src/generate2.rs +++ b/crates/effects/src/generate2.rs @@ -43,8 +43,9 @@ fn add_light(px: Px, l: [f32; 3], la: f32) -> Px { // ---------------------------------------------------------------- Cell Pattern -/// Cell Pattern options. The HQ variants share the shaping of their standard counterparts -/// (we always render at full precision). +/// Cell Pattern options. The HQ variants share the shaping of their standard counterparts but +/// search a wider neighbourhood of cells (no artefacts at high Disperse) and anti-alias the +/// cell edges (4×4 samples per pixel). const CELL_PATTERNS: [&str; 12] = [ "Bubbles", "Crystals", @@ -75,13 +76,13 @@ fn pattern_kind(i: u32) -> u32 { /// Worley distances for point (u, v) in cell units: (F1, F2, hash of the nearest cell). /// `tile` repeats the cell layout every (columns, rows) cells. -fn worley(u: f64, v: f64, disperse: f64, evo: f64, seed: u32, tile: Option<(i64, i64)>) -> (f64, f64, f32) { +fn worley_r(u: f64, v: f64, disperse: f64, evo: f64, seed: u32, tile: Option<(i64, i64)>, reach: i64) -> (f64, f64, f32) { let (cx, cy) = (u.floor() as i64, v.floor() as i64); let mut f1 = f64::INFINITY; let mut f2 = f64::INFINITY; let mut id = 0.0f32; - for j in -1..=1 { - for i in -1..=1 { + for j in -reach..=reach { + for i in -reach..=reach { let (gx, gy) = (cx + i, cy + j); let (tx, ty) = match tile { Some((nx, ny)) => (gx.rem_euclid(nx), gy.rem_euclid(ny)), @@ -135,6 +136,7 @@ fn overflow(v: f32, mode: u32) -> f32 { fn cell_pattern(ctx: &EffectCtx, mut b: Buf) -> Buf { let pattern = pattern_kind(ctx.params.e("cellPattern")); + let hq = (6..=10).contains(&ctx.params.e("cellPattern")); let invert = ctx.params.b("invert"); let contrast = ctx.params.f("contrast") as f32 / 100.0; let ov = ctx.params.e("overflow"); @@ -152,11 +154,19 @@ fn cell_pattern(ctx: &EffectCtx, mut b: Buf) -> Buf { .params .b("tilingOptions/enableTiling") .then(|| (ctx.params.f("tilingOptions/cellsHorizontal").round().max(1.0) as i64, ctx.params.f("tilingOptions/cellsVertical").round().max(1.0) as i64)); + let subs: Vec = if hq { (0..4).map(|k| (k as f64 + 0.5) / 4.0).collect() } else { vec![0.5] }; + let reach = if hq { 2 } else { 1 }; map_xy(&mut b.img, |x, y, _| { - let u = (x as f64 + 0.5 - off.0) / size; - let v = (y as f64 + 0.5 - off.1) / size; - let (f1, f2, id) = worley(u, v, disperse, evo, seed, tile); - let mut val = cell_value(pattern, f1, f2, id); + let mut val = 0.0; + for sy in &subs { + for sx in &subs { + let u = (x as f64 + sx - off.0) / size; + let v = (y as f64 + sy - off.1) / size; + let (f1, f2, id) = worley_r(u, v, disperse, evo, seed, tile, reach); + val += cell_value(pattern, f1, f2, id); + } + } + let mut val = val / (subs.len() * subs.len()) as f32; val = overflow((val - 0.5) * contrast + 0.5, ov); if invert { val = 1.0 - val; @@ -1347,4 +1357,26 @@ mod tests { assert!(old[0] > 0.9 && old[1] < 0.1, "{old:?}"); assert!(new[1] > 0.9 && new[0] < 0.1, "{new:?}"); } + + #[test] + fn cell_pattern_hq_variants_antialias() { + let s = crate::find("ec.generate.cellpattern").unwrap(); + let r = |pat: u32| { + let mut params = Params { values: s.params.iter().map(|p| (p.id.to_string(), p.default.clone())).collect() }; + params.values.insert("cellPattern".into(), Value::Enum(pat)); + params.values.insert("size".into(), num(12.0)); + let ctx = EffectCtx { params: ¶ms, time: 0.0, layer_size: [48.0, 48.0], seed: 1, adjustment: false, env: Default::default() }; + crate::apply(s, &ctx, Buf { img: Image::new(48, 48), offset: [0.0; 2], scale: 1.0 }).img + }; + let distinct = |img: &Image| { + let mut v: Vec = img.data.iter().map(|p| (p[0] * 10000.0) as u32).collect(); + v.sort_unstable(); + v.dedup(); + v.len() + }; + let (std, hq) = (r(4), r(9)); + assert_eq!(CELL_PATTERNS[9], "Crystallize HQ"); + assert_ne!(std, hq); + assert!(distinct(&hq) > distinct(&std) * 2, "{} vs {}", distinct(&hq), distinct(&std)); + } } diff --git a/crates/effects/src/keying.rs b/crates/effects/src/keying.rs index 6ec886413..9d61ff8c8 100644 --- a/crates/effects/src/keying.rs +++ b/crates/effects/src/keying.rs @@ -496,14 +496,36 @@ fn advanced_spill(ctx: &EffectCtx, mut b: Buf) -> Buf { let amt = ctx.params.f("suppression") as f32 / 100.0; let range = ctx.params.f("ultraSettings/spillRange") as f32 / 100.0; let luma = ctx.params.f("ultraSettings/lumaCorrection") as f32 / 100.0; + // Ultra: Tolerance limits suppression to hues near the key colour; Desaturate takes the + // colour out of suppressed pixels; Spill Color Correction puts the removed spill back as + // neutral light (so suppressed areas keep their brightness without the cast). + let key = ctx.params.color("ultraSettings/keyColor"); + let key_hue = effectcraft_color::rgb_to_hsl(key[0], key[1], key[2]).0; + let tol = if ultra { ctx.params.get("ultraSettings/tolerance").map(Value::as_f64).unwrap_or(100.0) as f32 / 100.0 } else { 1.0 }; + let desat = if ultra { ctx.params.f("ultraSettings/desaturate") as f32 / 100.0 } else { 0.0 }; + let neutral = if ultra { ctx.params.f("ultraSettings/spillColorCorrection") as f32 / 100.0 } else { 0.0 }; b.img.map_straight(|mut c| { + let w = if tol < 1.0 { + let h = effectcraft_color::rgb_to_hsl(c[0], c[1], c[2]).0; + let d = (h - key_hue).abs(); + let d = d.min(1.0 - d); + (1.0 - d / (tol * 0.5).max(1e-4)).clamp(0.0, 1.0) + } else { + 1.0 + }; let mx = c[o1].max(c[o2]); let avg = 0.5 * (c[o1] + c[o2]); let limit = mx + (avg - mx) * range; - let spill = (c[pi] - limit).max(0.0) * amt; + let spill = (c[pi] - limit).max(0.0) * amt * w; c[pi] -= spill; - let back = spill * LUMA_W[pi] * luma; - c.map(|v| v + back) + let back = spill * LUMA_W[pi] * luma + spill * neutral / 3.0; + let mut c = c.map(|v| v + back); + if desat > 0.0 && spill > 0.0 { + let l = effectcraft_color::luminance(c[0], c[1], c[2]); + let k = (desat * (spill * 4.0).min(1.0)).min(1.0); + c = c.map(|v| v + (l - v) * k); + } + c }); b } @@ -675,7 +697,10 @@ pub fn specs() -> Vec { p("method", "Method", Value::Enum(0), popup(&["Standard", "Ultra"])), p("suppression", "Suppression", num(100.0), slider(0.0, 200.0, 0.0, 100.0, 0)), p("ultraSettings/keyColor", "Key Color", col(0.0, 1.0, 0.0), ParamUi::Color), + p("ultraSettings/tolerance", "Tolerance", num(50.0), pct()), + p("ultraSettings/desaturate", "Desaturate", num(0.0), pct()), p("ultraSettings/spillRange", "Spill Range", num(50.0), pct()), + p("ultraSettings/spillColorCorrection", "Spill Color Correction", num(0.0), pct()), p("ultraSettings/lumaCorrection", "Luma Correction", num(0.0), pct()), ], advanced_spill, @@ -846,4 +871,26 @@ mod tests { let out = run("ec.key.keycleaner", &[], img.clone()); assert_eq!(out.get(12, 2), img.get(12, 2)); } + + #[test] + fn advanced_spill_ultra_tolerance_desaturate_and_color_correction() { + let ultra = |extra: &[(&str, Value)], c: [f32; 4]| { + let mut v = vec![("method", Value::Enum(1)), ("ultraSettings/keyColor", Value::Color([0.0, 1.0, 0.0, 1.0]))]; + v.extend_from_slice(extra); + run("ec.key.advancedspill", &v, Image::filled(1, 1, c)).get(0, 0) + }; + // A yellow-green pixel (hue away from the key) is left alone with a narrow tolerance. + let yg = [0.6, 0.8, 0.1, 1.0]; + assert!((ultra(&[("ultraSettings/tolerance", num(5.0))], yg)[1] - 0.8).abs() < 1e-5); + assert!(ultra(&[("ultraSettings/tolerance", num(100.0))], yg)[1] < 0.79); + // Desaturate greys the suppressed pixel. + let g = [0.3, 0.8, 0.3, 1.0]; + let d = ultra(&[("ultraSettings/desaturate", num(100.0))], g); + assert!((d[0] - d[1]).abs() < 0.05, "{d:?}"); + // Spill Color Correction keeps the brightness the suppression removed. + let plain = ultra(&[], g); + let cc = ultra(&[("ultraSettings/spillColorCorrection", num(100.0))], g); + let sum = |p: [f32; 4]| p[0] + p[1] + p[2]; + assert!(sum(cc) > sum(plain) + 0.1 && (sum(cc) - sum(g)).abs() < 1e-4, "{plain:?} {cc:?}"); + } } diff --git a/crates/effects/src/keying2.rs b/crates/effects/src/keying2.rs index 41ed1fc0a..4467318b7 100644 --- a/crates/effects/src/keying2.rs +++ b/crates/effects/src/keying2.rs @@ -102,8 +102,11 @@ fn mask_sdf(ctx: &EffectCtx, b: &Buf, idx: &[u32]) -> Option { } fn inner_outer_key(ctx: &EffectCtx, mut b: Buf) -> Buf { - let fg_idx = [ctx.params.e("foreground"), ctx.params.e("additionalForeground")]; - let bg_idx = [ctx.params.e("background"), ctx.params.e("additionalBackground")]; + // Foreground (Inside) plus Additional Foreground 1–10; likewise for the background. + let fg_idx: Vec = + std::iter::once(ctx.params.e("foreground")).chain((1..=10).map(|k| ctx.params.e(&format!("additionalForeground/foreground{k}")))).collect(); + let bg_idx: Vec = + std::iter::once(ctx.params.e("background")).chain((1..=10).map(|k| ctx.params.e(&format!("additionalBackground/background{k}")))).collect(); let radius = (ctx.params.f("singleMaskHighlightRadius") * b.scale) as f32; let thin = ctx.params.f("edgeThin") * b.scale; let feather = ctx.params.f("edgeFeather") * b.scale; @@ -187,6 +190,26 @@ fn inner_outer_key(ctx: &EffectCtx, mut b: Buf) -> Buf { .collect(), ); } + // Cleanup Foreground / Background: brush strokes along mask paths that paint the matte + // towards opaque / transparent (Brush Radius, Brush Pressure). + for (side, target) in [("Foreground", 1.0f32), ("Background", 0.0)] { + for k in 1..=10 { + let g = format!("cleanup{side}/cleanup{side}{k}"); + let idx = ctx.params.e(&format!("{g}/path")); + let Some(m) = (idx > 0).then(|| ctx.env.masks.get(idx as usize - 1)).flatten() else { continue }; + let rad = (ctx.params.f(&format!("{g}/brushRadius")) * b.scale).max(0.5); + let pressure = (ctx.params.f(&format!("{g}/brushPressure")) / 100.0).clamp(0.0, 1.0) as f32; + let pts: Vec<[f64; 2]> = m.points.iter().map(|q| [q[0] * b.scale + b.offset[0], q[1] * b.scale + b.offset[1]]).collect(); + alpha.data.par_iter_mut().enumerate().for_each(|(i, a)| { + let (x, y) = ((i % w) as f64 + 0.5, (i / w) as f64 + 0.5); + let d = dist_to_poly(&pts, m.closed, x, y); + if d < rad + 1.0 { + let cov = ((rad - d) / rad.max(1.0) * 2.0).clamp(0.0, 1.0) as f32 * pressure; + *a += (target - *a) * cov; + } + }); + } + } if thin.abs() > 0.01 { alpha = morph_frac(&alpha, thin.abs(), thin < 0.0); } @@ -332,18 +355,34 @@ pub fn specs() -> Vec { spec( "ec.key.innerouter", "Inner/Outer Key", - vec![ - p("foreground", "Foreground (Inside)", Value::Enum(0), mask_options()), - p("additionalForeground", "Additional Foreground", Value::Enum(0), mask_options()), - p("background", "Background (Outside)", Value::Enum(0), mask_options()), - p("additionalBackground", "Additional Background", Value::Enum(0), mask_options()), - p("singleMaskHighlightRadius", "Single Mask Highlight Radius", num(0.0), slider(0.0, 100.0, 0.0, 100.0, 1)), - p("edgeThin", "Edge Thin", num(0.0), slider(-10.0, 10.0, -10.0, 10.0, 1)), - p("edgeFeather", "Edge Feather", num(0.0), slider(0.0, 100.0, 0.0, 50.0, 1)), - p("edgeThreshold", "Edge Threshold", num(0.0), pct()), - p("invertExtraction", "Invert Extraction", Value::Bool(false), ParamUi::Checkbox), - p("blendWithOriginal", "Blend with Original", num(0.0), pct()), - ], + { + let leak = |s: String| -> &'static str { Box::leak(s.into_boxed_str()) }; + let mut v = vec![p("foreground", "Foreground (Inside)", Value::Enum(0), mask_options())]; + for k in 1..=10 { + v.push(p(leak(format!("additionalForeground/foreground{k}")), leak(format!("Foreground {k}")), Value::Enum(0), mask_options())); + } + v.push(p("background", "Background (Outside)", Value::Enum(0), mask_options())); + for k in 1..=10 { + v.push(p(leak(format!("additionalBackground/background{k}")), leak(format!("Background {k}")), Value::Enum(0), mask_options())); + } + for side in ["Foreground", "Background"] { + for k in 1..=10 { + let g = format!("cleanup{side}/cleanup{side}{k}"); + v.push(p(leak(format!("{g}/path")), "Path", Value::Enum(0), mask_options())); + v.push(p(leak(format!("{g}/brushRadius")), "Brush Radius", num(10.0), slider(0.0, 1000.0, 0.0, 100.0, 1))); + v.push(p(leak(format!("{g}/brushPressure")), "Brush Pressure", num(80.0), pct())); + } + } + v.extend([ + p("singleMaskHighlightRadius", "Single Mask Highlight Radius", num(0.0), slider(0.0, 100.0, 0.0, 100.0, 1)), + p("edgeThin", "Edge Thin", num(0.0), slider(-10.0, 10.0, -10.0, 10.0, 1)), + p("edgeFeather", "Edge Feather", num(0.0), slider(0.0, 100.0, 0.0, 50.0, 1)), + p("edgeThreshold", "Edge Threshold", num(0.0), pct()), + p("invertExtraction", "Invert Extraction", Value::Bool(false), ParamUi::Checkbox), + p("blendWithOriginal", "Blend with Original", num(0.0), pct()), + ]); + v + }, inner_outer_key, ), spec( @@ -478,4 +517,47 @@ mod tests { assert_eq!(out.img.get(2, 10), img.get(2, 10)); } } + + #[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 }; + // Masks 1..4: foreground squares; mask 5: a horizontal cleanup stroke across the middle. + let masks = vec![ + sq(1.0, 5.0), + sq(7.0, 11.0), + sq(13.0, 17.0), + sq(19.0, 23.0), + MaskShape { name: String::new(), points: vec![[0.0, 5.0], [30.0, 5.0]], closed: false, inverted: false }, + ]; + let env = EffectEnv { masks: &masks, ..Default::default() }; + let img = Image::filled(30, 10, [0.5, 0.5, 0.5, 1.0]); + let vals = [ + ("foreground", Value::Enum(1)), + ("additionalForeground/foreground1", Value::Enum(2)), + ("additionalForeground/foreground2", Value::Enum(3)), + ("additionalForeground/foreground5", Value::Enum(4)), + ]; + let out = run_fx("ec.key.innerouter", &vals, img.clone(), 0.0, env).img; + for x in [3, 9, 15, 21] { + assert_eq!(out.get(x, 3)[3], 1.0, "foreground square at {x}"); + } + assert_eq!(out.get(27, 3)[3], 0.0); + // Cleanup Background along mask 5 erases a band; Cleanup Foreground restores outside. + let mut bg = vals.to_vec(); + bg.extend([ + ("cleanupBackground/cleanupBackground1/path", Value::Enum(5)), + ("cleanupBackground/cleanupBackground1/brushRadius", num(1.5)), + ("cleanupBackground/cleanupBackground1/brushPressure", num(100.0)), + ]); + let erased = run_fx("ec.key.innerouter", &bg, img.clone(), 0.0, env).img; + assert!(erased.get(3, 5)[3] < 0.1 && erased.get(3, 2)[3] > 0.9); + let mut fg = vals.to_vec(); + fg.extend([ + ("cleanupForeground/cleanupForeground3/path", Value::Enum(5)), + ("cleanupForeground/cleanupForeground3/brushRadius", num(1.5)), + ("cleanupForeground/cleanupForeground3/brushPressure", num(100.0)), + ]); + let painted = run_fx("ec.key.innerouter", &fg, img, 0.0, env).img; + assert!(painted.get(27, 5)[3] > 0.9 && painted.get(27, 2)[3] < 0.1); + } } diff --git a/crates/effects/src/lib.rs b/crates/effects/src/lib.rs index 90a85a6d0..ba0b90645 100644 --- a/crates/effects/src/lib.rs +++ b/crates/effects/src/lib.rs @@ -626,6 +626,30 @@ pub const PARAM_GROUPS: &[(&str, &str)] = &[ ("options", "Options"), // Keying, Matte, Obsolete (Key Light's groups are in `keylight::GROUPS`). ("ultraSettings", "Ultra Settings"), + ("additionalForeground", "Additional Foreground"), + ("additionalBackground", "Additional Background"), + ("cleanupForeground", "Cleanup Foreground"), + ("cleanupBackground", "Cleanup Background"), + ("cleanupForeground1", "Cleanup Foreground 1"), + ("cleanupForeground2", "Cleanup Foreground 2"), + ("cleanupForeground3", "Cleanup Foreground 3"), + ("cleanupForeground4", "Cleanup Foreground 4"), + ("cleanupForeground5", "Cleanup Foreground 5"), + ("cleanupForeground6", "Cleanup Foreground 6"), + ("cleanupForeground7", "Cleanup Foreground 7"), + ("cleanupForeground8", "Cleanup Foreground 8"), + ("cleanupForeground9", "Cleanup Foreground 9"), + ("cleanupForeground10", "Cleanup Foreground 10"), + ("cleanupBackground1", "Cleanup Background 1"), + ("cleanupBackground2", "Cleanup Background 2"), + ("cleanupBackground3", "Cleanup Background 3"), + ("cleanupBackground4", "Cleanup Background 4"), + ("cleanupBackground5", "Cleanup Background 5"), + ("cleanupBackground6", "Cleanup Background 6"), + ("cleanupBackground7", "Cleanup Background 7"), + ("cleanupBackground8", "Cleanup Background 8"), + ("cleanupBackground9", "Cleanup Background 9"), + ("cleanupBackground10", "Cleanup Background 10"), ("fillAndStroke", "Fill and Stroke"), ("pathOptions", "Path Options"), ("controlPoints", "Control Points"), diff --git a/crates/effects/src/migrate.rs b/crates/effects/src/migrate.rs index cba69cbe2..02b51dfb8 100644 --- a/crates/effects/src/migrate.rs +++ b/crates/effects/src/migrate.rs @@ -38,6 +38,10 @@ pub const PARAM_ID_ALIASES: &[(&str, &str, &str)] = &[ ("ec.matte.rotobrush", "refineEdgeMatte/extendWhereSmoothed", "refineEdgeMatte/decontamination/extendWhereSmoothed"), ("ec.matte.rotobrush", "refineEdgeMatte/increaseDecontaminationRadius", "refineEdgeMatte/decontamination/increaseDecontaminationRadius"), ("ec.matte.rotobrush", "refineEdgeMatte/viewDecontaminationMap", "refineEdgeMatte/decontamination/viewDecontaminationMap"), + // Keying: Inner/Outer Key's single additional masks became Additional Foreground / + // Background 1 (of 10). + ("ec.key.innerouter", "additionalForeground", "additionalForeground/foreground1"), + ("ec.key.innerouter", "additionalBackground", "additionalBackground/background1"), // Generate: Radio Waves' growth speed was called Velocity; Velocity now moves the wave. ("ec.generate.radiowaves", "velocity", "waveMotion/expansion"), ]; @@ -153,6 +157,8 @@ pub fn legacy_default(effect: &str, param: &str) -> Option { ("ec.noise.removegrain", "noiseReductionSettings/mode") => Value::Enum(1), // Audio: the Compressor's release was always manual. ("ec.audio.compressor", "autoRelease") => Value::Bool(false), + // Keying: Advanced Spill Suppressor's Ultra mode had no hue tolerance. + ("ec.key.advancedspill", "ultraSettings/tolerance") => Value::Scalar(100.0), // Transition: Block Dissolve's blocks had hard edges. ("ec.transition.blockdissolve", "softEdges") => Value::Bool(false), _ => return None, @@ -435,4 +441,18 @@ mod tests { assert_eq!(prop_at(&mut g, "details/reds/redsCyan").unwrap().value, Value::Scalar(40.0)); assert_eq!(g.sub("details").unwrap().sub("reds").unwrap().name, "Reds"); } + + #[test] + fn inner_outer_key_additional_masks_move_into_their_groups() { + let spec = find("ec.key.innerouter").unwrap(); + let mut next = 1; + let mut g = instantiate(spec, &mut Ids(&mut next), "Inner/Outer Key", [100.0, 50.0]); + let mut pr = take_at(&mut g, "additionalForeground/foreground1").unwrap(); + pr.match_id = "additionalForeground".into(); + pr.value = Value::Enum(3); + g.children.push(pr.into()); + assert!(upgrade_instance(spec, &mut g, &mut Ids(&mut next), [100.0, 50.0])); + assert!(g.get("additionalForeground").is_none()); + assert_eq!(prop_at(&mut g, "additionalForeground/foreground1").unwrap().value, Value::Enum(3)); + } } diff --git a/docs/effects.md b/docs/effects.md index 8bb8d9e42..8b57bc176 100644 --- a/docs/effects.md +++ b/docs/effects.md @@ -3,11 +3,11 @@ -EffectCraft ships 303 effects with 2773 parameters in total, grouped into the same categories as the Effects & Presets panel. Every effect is our own implementation, written from public behaviour descriptions and standard image-processing literature. Parameter names, order, popup options, units, defaults and ranges follow the reference application so that projects, expressions and muscle memory carry over. +EffectCraft ships 303 effects with 2854 parameters in total, grouped into the same categories as the Effects & Presets panel. Every effect is our own implementation, written from public behaviour descriptions and standard image-processing literature. Parameter names, order, popup options, units, defaults and ranges follow the reference application so that projects, expressions and muscle memory carry over. - **GPU**: 24 effects also run on the GPU compositor with identical results. - **32**: 303 effects process 32-bit float (HDR, overbright) pixels without clamping. -- **Status**: 287 implemented in full, 16 partial (what is missing is listed). +- **Status**: 290 implemented in full, 13 partial (what is missing is listed). Effects are addressed by id (`ec..`) or display name in commands, scripts and the control channel, and every parameter by its id (`effects/#1/blurriness`). @@ -197,7 +197,7 @@ Effects are addressed by id (`ec..`) or display name in commands | CC Light Rays | `ec.generate.cclightrays` | 8 | | 32 | Implemented | | CC Light Sweep | `ec.generate.cclightsweep` | 9 | | 32 | Implemented | | CC Threads | `ec.generate.ccthreads` | 8 | | 32 | Implemented | -| Cell Pattern | `ec.generate.cellpattern` | 14 | | 32 | Partial: HQ variants render like the standard ones | +| Cell Pattern | `ec.generate.cellpattern` | 14 | | 32 | Implemented | | Checkerboard | `ec.generate.checkerboard` | 10 | | 32 | Implemented | | Circle | `ec.generate.circle` | 11 | | 32 | Implemented | | Ellipse | `ec.generate.ellipse` | 8 | | 32 | Implemented | @@ -235,13 +235,13 @@ Effects are addressed by id (`ec..`) or display name in commands | Effect | Id | Params | GPU | 32 | Status | |---|---|---:|:-:|:-:|---| -| Advanced Spill Suppressor | `ec.key.advancedspill` | 5 | | 32 | Partial: no Tolerance, Desaturate or Spill Color Correction | +| Advanced Spill Suppressor | `ec.key.advancedspill` | 8 | | 32 | Implemented | | CC Simple Wire Removal | `ec.key.ccsimplewireremoval` | 7 | | 32 | Implemented | | Color Difference Key | `ec.key.colordifference` | 16 | | 32 | Implemented | | Color Range | `ec.key.colorrange` | 8 | | 32 | Implemented | | Difference Matte | `ec.key.differencematte` | 6 | | 32 | Implemented | | Extract | `ec.key.extract` | 6 | | 32 | Implemented | -| Inner/Outer Key | `ec.key.innerouter` | 10 | | 32 | Partial: one additional mask per side; no cleanup strokes | +| Inner/Outer Key | `ec.key.innerouter` | 88 | | 32 | Implemented | | Key Cleaner | `ec.key.keycleaner` | 4 | | 32 | Implemented | | Key Light | `ec.keying.keylight` | 42 | | 32 | Partial: no Source Crops X/Y Method or Edge Colour; no colour suppression / balancing | | Linear Color Key | `ec.key.linearcolor` | 6 | | 32 | Implemented |