From 363a800d3f65a113aae7d442b47583b39b60cf47 Mon Sep 17 00:00:00 2001 From: arnav-hiwarkar Date: Thu, 12 Nov 2026 15:14:34 +0530 Subject: [PATCH] #3478, #3479: 4-Color Gradient: Blend and Jitter take After Effects' ranges Blend goes up to 10000 (it keeps its minimum of 1, below After Effects' 5) and Jitter up to 500 %, After Effects' recorded valid ranges in the same units. Defaults and sliders are unchanged. The CPU and GPU renderers capped Jitter's dither at 100 %, so higher values would be stored but render as 100. Both now take it from one function, four_color_jitter, which keeps the dither linear up to 500 %. Blend 10000 is a finite softening (Blend squared) at the same cost. Jitter was written as "percent" in keyframe text because its range was 0..100; a UNITS row keeps that unit. --- crates/aep/src/export/fx_defs.rs | 2 -- crates/effects/src/generate.rs | 28 +++++++++++++++++-- crates/effects/src/lib.rs | 2 +- .../src/interchange/keyframe_text_effects.rs | 1 + crates/engine/src/tests/effect_ranges.rs | 7 +++-- crates/engine/src/tests/keyframe_text.rs | 18 ++++++++++++ crates/gpu/src/fx_generate.rs | 2 +- crates/gpu/src/tests/fx_generate.rs | 2 ++ 8 files changed, 53 insertions(+), 9 deletions(-) diff --git a/crates/aep/src/export/fx_defs.rs b/crates/aep/src/export/fx_defs.rs index 92ff7c20c..cc80eaaaa 100644 --- a/crates/aep/src/export/fx_defs.rs +++ b/crates/aep/src/export/fx_defs.rs @@ -743,8 +743,6 @@ mod tests { ("ec.generate.beam", "endThickness", 3475), ("ec.generate.grid", "border", 3476), ("ec.stylize.scatter", "amount", 3477), - ("ec.generate.fourcolor", "blend", 3478), - ("ec.generate.fourcolor", "jitter", 3479), ("ec.color.changecolor", "hueTransform", 3480), ("ec.time.pixelmotionblur", "shutterAngle", 3481), ("ec.time.pixelmotionblur", "shutterSamples", 3482), diff --git a/crates/effects/src/generate.rs b/crates/effects/src/generate.rs index ae0d61861..47a6d6314 100644 --- a/crates/effects/src/generate.rs +++ b/crates/effects/src/generate.rs @@ -121,11 +121,17 @@ pub fn four_color_softening(blend: f64, scale: f64) -> f64 { (b * b).max(1e-6) } +/// 4-Color Gradient's Jitter (percent, up to After Effects' 500 %) as the dither's strength: 1 at +/// 100 %, a few code values of noise per unit. +pub fn four_color_jitter(jitter: f64) -> f32 { + (jitter / 100.0).clamp(0.0, 5.0) as f32 +} + fn four_color(ctx: &EffectCtx, mut b: Buf) -> Buf { let pts: Vec<((f64, f64), [f32; 4])> = (1..=4).map(|i| (b.to_px(ctx.params.v2(&format!("positionsColors/point{i}"))), ctx.params.color(&format!("positionsColors/color{i}")))).collect(); let k = four_color_softening(ctx.params.f("blend"), b.scale); - let jitter = (ctx.params.f("jitter") / 100.0).clamp(0.0, 1.0) as f32; + let jitter = four_color_jitter(ctx.params.f("jitter")); let op = ctx.params.f("opacity") as f32 / 100.0; let mode = ctx.params.e("blendingMode"); let seed = ctx.seed; @@ -349,8 +355,9 @@ pub fn specs() -> Vec { p("positionsColors/color3", "Color 3", col(1.0, 0.0, 1.0), ParamUi::Color), p("positionsColors/point4", "Point 4", pt(0.9, 0.9), ParamUi::Point), p("positionsColors/color4", "Color 4", col(0.0, 0.0, 1.0), ParamUi::Color), - p("blend", "Blend", num(100.0), slider(1.0, 1000.0, 1.0, 1000.0, 1)), - p("jitter", "Jitter", num(0.0), slider(0.0, 100.0, 0.0, 100.0, 1)), + // After Effects' valid ranges (#3478, #3479); Blend keeps its lower minimum (1, not 5). + p("blend", "Blend", num(100.0), slider(1.0, 10000.0, 1.0, 1000.0, 1)), + p("jitter", "Jitter", num(0.0), slider(0.0, 500.0, 0.0, 100.0, 1)), p("opacity", "Opacity", num(100.0), slider(0.0, 100.0, 0.0, 100.0, 1)), p("blendingMode", "Blending Mode", Value::Enum(0), popup(&GEN_BLEND_MODES[..18])), ], @@ -465,6 +472,21 @@ mod generator_tests { } } + /// #3479: Jitter goes up to After Effects' 500 %, and more jitter dithers more (the dither + /// stopped growing at 100 %). + #[test] + fn four_color_jitter_grows_up_to_five_hundred_percent() { + let img = Image::filled(32, 32, [0.0, 0.0, 0.0, 1.0]); + let spread = |jitter: f64| { + let o = run_fx("ec.generate.fourcolor", &[("jitter", num(jitter))], img.clone(), 0.0, EffectEnv::default()).img; + let flat = run_fx("ec.generate.fourcolor", &[], img.clone(), 0.0, EffectEnv::default()).img; + o.data.iter().zip(&flat.data).map(|(a, b)| (a[0] - b[0]).abs()).fold(0.0f32, f32::max) + }; + let (at100, at500) = (spread(100.0), spread(500.0)); + assert!(at100 > 0.0 && at500 > 4.0 * at100, "{at100} {at500}"); + assert!(at500 < 0.2, "a few code values: {at500}"); + } + /// After Effects 2026, 4-Color Gradient defaults (yellow, green, magenta, blue at 10 % / 90 % /// of a 1920 × 1080 layer) at Blend 100 and 300, here on a tenth-size layer with Blend 10 /// and 30 (the weights scale with the layer): colours at AE pixels (10x + 5, 10y + 5). diff --git a/crates/effects/src/lib.rs b/crates/effects/src/lib.rs index 9105f4587..e030c6748 100644 --- a/crates/effects/src/lib.rs +++ b/crates/effects/src/lib.rs @@ -99,7 +99,7 @@ pub use sim3::{PgBlit, PgPlan, playground_plan, playground_state}; pub use stylize2::strobe_on; // CPU helpers the GPU kernels share (effectcraft-gpu). pub use blur2::{CAMERA_LENS_MAX_RADIUS, camera_lens_plain, camera_lens_radius, camera_lens_spans, radial_fast_pad}; -pub use generate::{four_color_softening, gen_mode}; +pub use generate::{four_color_jitter, four_color_softening, gen_mode}; pub use generate2::pattern_kind as cell_pattern_kind; pub use generate2::{BoltSeg, Contour, Ghost, RadioPlan, Wave, flare_ghosts, lightning_segments, radio_plan}; pub use generate3::{ diff --git a/crates/engine/src/interchange/keyframe_text_effects.rs b/crates/engine/src/interchange/keyframe_text_effects.rs index 83f5b6c20..94d81afcc 100644 --- a/crates/engine/src/interchange/keyframe_text_effects.rs +++ b/crates/engine/src/interchange/keyframe_text_effects.rs @@ -273,6 +273,7 @@ pub(crate) const UNITS: &[(&str, &str, &str)] = &[ ("ec.distort.wavewarp", "width", "percent"), ("ec.distort.wavewarp", "speed", "pixels"), ("ec.generate.fourcolor", "blend", "percent"), + ("ec.generate.fourcolor", "jitter", "percent"), ("ec.generate.advancedlightning", "coreSettings/coreRadius", ""), ("ec.generate.advancedlightning", "glowSettings/glowRadius", "percent"), ("ec.generate.advancedlightning", "alphaObstacle", "pixels"), diff --git a/crates/engine/src/tests/effect_ranges.rs b/crates/engine/src/tests/effect_ranges.rs index 3581f4411..cb4d4ea24 100644 --- a/crates/engine/src/tests/effect_ranges.rs +++ b/crates/engine/src/tests/effect_ranges.rs @@ -9,8 +9,9 @@ use serde_json::{Value, json}; use crate::Session; -/// (issue, effect, parameter path, lowest, highest): the ends of After Effects' valid range, -/// in EffectCraft's units, each outside the range the effect accepted before the issue. +/// (issue, effect, parameter path, lowest, highest): the ends of the range the effect now accepts, +/// in EffectCraft's units: After Effects' valid range, or wider where EffectCraft already was. +/// At least one end of each is outside the range accepted before the issue. #[rustfmt::skip] const ROWS: &[(u32, &str, &str, f64, f64)] = &[ (3487, "ec.color.levelsic", "rgb/rgbInBlack", -10000.0, 10000.0), @@ -38,6 +39,8 @@ const ROWS: &[(u32, &str, &str, f64, f64)] = &[ (3489, "ec.color.levelsic", "alpha/alphaOutBlack", -10000.0, 10000.0), (3490, "ec.color.levelsic", "alpha/alphaOutWhite", -10000.0, 10000.0), (3491, "ec.color.levelsic", "alpha/alphaGamma", 0.0, 10.0), + (3478, "ec.generate.fourcolor", "blend", 1.0, 10000.0), + (3479, "ec.generate.fourcolor", "jitter", 0.0, 500.0), ]; fn path(effect: &str, param: &str) -> String { diff --git a/crates/engine/src/tests/keyframe_text.rs b/crates/engine/src/tests/keyframe_text.rs index 30d3b436e..81d9487d9 100644 --- a/crates/engine/src/tests/keyframe_text.rs +++ b/crates/engine/src/tests/keyframe_text.rs @@ -480,3 +480,21 @@ fn camera_light_and_material_options_are_written_as_after_effects_does() { assert!(text.contains(block), "{block}in\n{text}"); } } + +/// The unit column of `name`'s block in copied keyframe text. +fn unit_of<'a>(text: &'a str, name: &str) -> &'a str { + let mut lines = text.lines().skip_while(|l| !l.contains(&format!("\t{name} #"))); + lines.next().unwrap_or_else(|| panic!("no {name} in\n{text}")); + lines.next().and_then(|l| l.strip_prefix("\tFrame\t")).and_then(|l| l.strip_suffix('\t')).unwrap_or_else(|| panic!("{name} in\n{text}")) +} + +/// Widening a 0..100 slider to After Effects' range (#3479, #3483, #3484) keeps the unit the +/// keyframe text writes for it. +#[test] +fn widened_effect_ranges_keep_their_keyframe_text_units() { + let (mut s, l) = session(); + s.execute("effect.apply", json!({"layer": l.0, "effect": "ec.generate.fourcolor"})).unwrap(); + let text = copy_props(&mut s, l, &["effects/#1/blend", "effects/#1/jitter"]); + assert_eq!(unit_of(&text, "Blend"), "percent"); + assert_eq!(unit_of(&text, "Jitter"), "percent"); +} diff --git a/crates/gpu/src/fx_generate.rs b/crates/gpu/src/fx_generate.rs index 893208a15..2dda38f45 100644 --- a/crates/gpu/src/fx_generate.rs +++ b/crates/gpu/src/fx_generate.rs @@ -385,7 +385,7 @@ fn four_color(e: &mut Enc, ctx: &EffectCtx, b: GBuf) -> Option { p.f[4 + i] = ctx.params.color(&format!("positionsColors/color{}", i + 1)); } let k = effectcraft_effects::four_color_softening(ctx.params.f("blend"), b.scale); - let jitter = (ctx.params.f("jitter") / 100.0).clamp(0.0, 1.0) as f32; + let jitter = effectcraft_effects::four_color_jitter(ctx.params.f("jitter")); p.u[0] = [gen_mode(ctx.params.e("blendingMode")), ctx.seed, 0, 0]; p.f[8] = [k as f32, jitter, ctx.params.f("opacity") as f32 / 100.0, 0.0]; run(e, "gen_fourcolor", &p, b, None, None) diff --git a/crates/gpu/src/tests/fx_generate.rs b/crates/gpu/src/tests/fx_generate.rs index d12920797..d7e05e047 100644 --- a/crates/gpu/src/tests/fx_generate.rs +++ b/crates/gpu/src/tests/fx_generate.rs @@ -167,6 +167,8 @@ fn generators() { effect_case("ec.generate.fourcolor", &[]); effect_case("ec.generate.fourcolor", &[("blend", n(40.0)), ("jitter", n(50.0)), ("opacity", n(80.0)), ("blendingMode", e(3))]); effect_case("ec.generate.fourcolor", &[("blend", n(600.0)), ("blendingMode", e(16)), ("positionsColors/point2", pt(30.0, 20.0))]); + // #3478, #3479: the top of After Effects' ranges. + effect_case("ec.generate.fourcolor", &[("blend", n(10000.0)), ("jitter", n(500.0))]); } #[test]