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 <[email protected]>
This commit is contained in:
Brandon ThomasandClaude Opus 5.5 committed 2026-10-03 04:57:05 -04:00
1 parent 413e3ea97d
commit 6d5e86601f
7 files changed
+236 -34

No files matched your search

-3
View File
@@ -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"),
+41 -9
View File
@@ -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<f64> = 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: &params, 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<u32> = 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));
}
}
+50 -3
View File
@@ -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<EffectSpec> {
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:?}");
}
}
+96 -14
View File
@@ -102,8 +102,11 @@ fn mask_sdf(ctx: &EffectCtx, b: &Buf, idx: &[u32]) -> Option<Plane> {
}
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<u32> =
std::iter::once(ctx.params.e("foreground")).chain((1..=10).map(|k| ctx.params.e(&format!("additionalForeground/foreground{k}")))).collect();
let bg_idx: Vec<u32> =
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<EffectSpec> {
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);
}
}
+24
View File
@@ -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"),
+20
View File
@@ -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<Value> {
("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));
}
}
+5 -5
View File
@@ -3,11 +3,11 @@
<!-- Generated from the effect registry (crates/effects/src/catalog.rs); do not edit by hand.
Regenerate: UPDATE_DOCS=1 cargo test -p effectcraft-effects --lib effects_doc_is_current -->
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.<category>.<name>`) 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.<category>.<name>`) 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.<category>.<name>`) 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 |