#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.
This commit is contained in:
arnav-hiwarkar committed 2026-11-12 15:14:34 +05:30
1 parent 39a3b565d8
commit 363a800d3f
8 files changed
+53 -9

No files matched your search

-2
View File
@@ -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),
+25 -3
View File
@@ -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<EffectSpec> {
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).
+1 -1
View File
@@ -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::{
@@ -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"),
+5 -2
View File
@@ -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 {
+18
View File
@@ -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");
}
+1 -1
View File
@@ -385,7 +385,7 @@ fn four_color(e: &mut Enc, ctx: &EffectCtx, b: GBuf) -> Option<GBuf> {
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)
+2
View File
@@ -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]