#3483, #3484: Noise HLS: amounts and Grain Size take After Effects' ranges

Hue, Lightness and Saturation go up to 1000 % and Grain Size down to 0 (it
keeps its wider maximum, 100), After Effects' recorded valid ranges in the
same units. Noise HLS Auto shares the controls and widens with them.
Defaults and sliders are unchanged.

The renderers need no change: hue wraps, lightness and saturation clamp per
pixel after the noise is added, and Grain Size is floored at 0.1 px on the
CPU and the GPU, so the new values cost the same and stay finite.

The amounts were written as "percent" in keyframe text because their range
was 0..100, and Grain Size would become "percent" at 0..100; UNITS rows keep
the units they had.
This commit is contained in:
arnav-hiwarkar committed 2026-11-12 15:46:48 +05:30
1 parent 363a800d3f
commit 244f9bd846
6 files changed
+55 -8

No files matched your search

-4
View File
@@ -746,10 +746,6 @@ mod tests {
("ec.color.changecolor", "hueTransform", 3480),
("ec.time.pixelmotionblur", "shutterAngle", 3481),
("ec.time.pixelmotionblur", "shutterSamples", 3482),
("ec.noise.noisehls", "hue", 3483),
("ec.noise.noisehls", "lightness", 3483),
("ec.noise.noisehls", "saturation", 3483),
("ec.noise.noisehls", "grainSize", 3484),
("ec.noise.fractal", "subSettings/subInfluence", 3485),
("ec.noise.fractal", "subSettings/subScaling", 3486),
("ec.distort.ccpowerpin", "expandTop", 3492),
+28 -4
View File
@@ -766,10 +766,12 @@ pub fn specs() -> Vec<EffectSpec> {
let hls_common = || {
vec![
p("noise", "Noise", Value::Enum(0), popup(&["Uniform", "Squared", "Grain"])),
p("hue", "Hue", num(0.0), pct()),
p("lightness", "Lightness", num(0.0), pct()),
p("saturation", "Saturation", num(0.0), pct()),
p("grainSize", "Grain Size", num(1.0), slider(0.1, 100.0, 0.5, 10.0, 2)),
// After Effects' valid ranges (#3483, #3484): the amounts go to 1000 %, Grain Size
// down to 0 (the renderer floors it at 0.1 px) and keeps its wider maximum.
p("hue", "Hue", num(0.0), slider(0.0, 1000.0, 0.0, 100.0, 1)),
p("lightness", "Lightness", num(0.0), slider(0.0, 1000.0, 0.0, 100.0, 1)),
p("saturation", "Saturation", num(0.0), slider(0.0, 1000.0, 0.0, 100.0, 1)),
p("grainSize", "Grain Size", num(1.0), slider(0.0, 100.0, 0.5, 10.0, 2)),
]
};
let mut hls = hls_common();
@@ -967,6 +969,28 @@ mod tests {
assert_eq!(still.img, img);
}
/// #3483, #3484: amounts above 100 % keep adding noise, and Grain Size 0 renders (floored at
/// 0.1 px) with finite output, for both effects.
#[test]
fn noise_hls_takes_after_effects_ranges() {
let mut img = Image::new(16, 16);
for (i, p) in img.data.iter_mut().enumerate() {
*p = premul([0.6, 0.3, (i % 7) as f32 / 7.0], 1.0);
}
for id in ["ec.noise.noisehls", "ec.noise.noisehlsauto"] {
let at = |amount: f64, grain: f64| {
let vals =
[("noise", Value::Enum(2)), ("hue", num(amount)), ("lightness", num(amount)), ("saturation", num(amount)), ("grainSize", num(grain))];
run(id, &vals, img.clone(), 0.0).img
};
let (low, high) = (at(100.0, 1.0), at(1000.0, 1.0));
assert_ne!(low, high, "{id}");
for out in [high, at(1000.0, 0.0)] {
assert!(out.data.iter().flatten().all(|v| v.is_finite() && (0.0..=1.0).contains(v)), "{id}");
}
}
}
#[test]
fn add_grain_keeps_flat_mean() {
let out = run("ec.noise.addgrain", &[("tweaking/intensity", num(2.0)), ("application/blendingMode", Value::Enum(2))], gray(64, 64, 0.5), 0.0);
@@ -318,6 +318,15 @@ pub(crate) const UNITS: &[(&str, &str, &str)] = &[
("ec.matte.rotobrush", "refineEdgeMatte/shiftEdge", "percent"),
("ec.matte.rotobrush", "refineEdgeMatte/decontamination/increaseDecontaminationRadius", ""),
("ec.matte.simplechoker", "chokeMatte", "pixels"),
// Their ranges were 0..100 (#3483, #3484).
("ec.noise.noisehls", "hue", "percent"),
("ec.noise.noisehls", "lightness", "percent"),
("ec.noise.noisehls", "saturation", "percent"),
("ec.noise.noisehls", "grainSize", ""),
("ec.noise.noisehlsauto", "hue", "percent"),
("ec.noise.noisehlsauto", "lightness", "percent"),
("ec.noise.noisehlsauto", "saturation", "percent"),
("ec.noise.noisehlsauto", "grainSize", ""),
("ec.noise.fractal", "transform/scale", "percent"),
("ec.noise.fractal", "transform/scaleWidth", "percent"),
("ec.noise.fractal", "transform/scaleHeight", "percent"),
+8
View File
@@ -41,6 +41,14 @@ const ROWS: &[(u32, &str, &str, f64, f64)] = &[
(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),
(3483, "ec.noise.noisehls", "hue", 0.0, 1000.0),
(3483, "ec.noise.noisehls", "lightness", 0.0, 1000.0),
(3483, "ec.noise.noisehls", "saturation", 0.0, 1000.0),
(3484, "ec.noise.noisehls", "grainSize", 0.0, 100.0),
(3483, "ec.noise.noisehlsauto", "hue", 0.0, 1000.0),
(3483, "ec.noise.noisehlsauto", "lightness", 0.0, 1000.0),
(3483, "ec.noise.noisehlsauto", "saturation", 0.0, 1000.0),
(3484, "ec.noise.noisehlsauto", "grainSize", 0.0, 100.0),
];
fn path(effect: &str, param: &str) -> String {
+8
View File
@@ -497,4 +497,12 @@ fn widened_effect_ranges_keep_their_keyframe_text_units() {
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");
for fx in ["ec.noise.noisehls", "ec.noise.noisehlsauto"] {
let (mut s, l) = session();
s.execute("effect.apply", json!({"layer": l.0, "effect": fx})).unwrap();
let text = copy_props(&mut s, l, &["effects/#1/hue", "effects/#1/lightness", "effects/#1/saturation", "effects/#1/grainSize"]);
for (name, unit) in [("Hue", "percent"), ("Lightness", "percent"), ("Saturation", "percent"), ("Grain Size", "")] {
assert_eq!(unit_of(&text, name), unit, "{fx} {name}");
}
}
}
+2
View File
@@ -164,6 +164,8 @@ fn noise_alpha_and_hls() {
effect_case("ec.noise.noisehls", &[("hue", n(30.0)), ("lightness", n(20.0)), ("saturation", n(40.0))]);
effect_case("ec.noise.noisehls", &[("noise", e(1)), ("lightness", n(50.0)), ("noisePhase", n(200.0))]);
effect_case("ec.noise.noisehls", &[("noise", e(2)), ("hue", n(60.0)), ("saturation", n(25.0)), ("grainSize", n(3.0))]);
// #3483, #3484: the ends of After Effects' ranges.
effect_case("ec.noise.noisehls", &[("noise", e(2)), ("hue", n(1000.0)), ("lightness", n(1000.0)), ("saturation", n(1000.0)), ("grainSize", n(0.0))]);
effect_case("ec.noise.noisehlsauto", &[("noise", e(0)), ("hue", n(20.0)), ("lightness", n(30.0)), ("noiseAnimationSpeed", n(7.0))]);
}