mirror of
https://github.com/storytold/effectcraft.git
synced 2026-11-02 22:42:24 -06:00
Brightness & Contrast: negative settings invert the positive curves, as After Effects
In After Effects 2026 the non-legacy curve of Brightness -40 is the inverse of Brightness +40 (within 2.6/255 on a full black-to-white ramp) and Contrast -50 the inverse of Contrast +50. EffectCraft fitted the negative brightness with its own power (2^(1.32 |b|) on v), 20/255 off AE at -40; the negatives now use the reciprocal exponents of the positive curves: Brightness -40 max 6.2/255 (was 19.8), Contrast -50 4.1 (was 7.0); positives unchanged (40: 5.5, 80: 7.8, Contrast 50: 2.7, 90: 5.0; B20 C30: 6.6, which confirms brightness before contrast). The #681 oracle samples still pass. CPU and GPU. The positive curves' remaining 5-8/255 needs a better curve family (a two-point spline fits each capture within 1.6/255), left open. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
515cc3d5f7
commit
dc9f15ada1
@@ -59,18 +59,22 @@ fn tritone(ctx: &EffectCtx, mut b: Buf) -> Buf {
|
||||
/// power toward white (`1-(1-v)^e`) or black (`v^e`); contrast uses `g = 1 + 0.53 c` instead of
|
||||
/// the old `2^(2c)` (that crushed 0.25 to ~8/255 at Contrast 100; AE is ~40/255). Values above 1
|
||||
/// pass through. The GPU kernel mirrors it.
|
||||
///
|
||||
/// Negative Brightness and Contrast are the inverse curves of the positive ones (After Effects
|
||||
/// 2026 ramps: Brightness −40 is Brightness +40 inverted within 2.6/255, Contrast −50 likewise),
|
||||
/// so they use the reciprocal exponents: Brightness −40 was 20/255 off AE, now 6.2.
|
||||
pub fn bc_modern(v: f32, b: f32, c: f32) -> f32 {
|
||||
if v >= 1.0 {
|
||||
return v;
|
||||
}
|
||||
let mut v = v.max(0.0);
|
||||
if b > 0.0 {
|
||||
v = 1.0 - (1.0 - v).powf(2f32.powf(1.24 * b));
|
||||
} else if b < 0.0 {
|
||||
v = v.powf(2f32.powf(-1.32 * b));
|
||||
if b != 0.0 {
|
||||
let e = 2f32.powf(1.24 * b.abs());
|
||||
v = 1.0 - (1.0 - v).powf(if b > 0.0 { e } else { 1.0 / e });
|
||||
}
|
||||
if c != 0.0 {
|
||||
let g = (1.0 + 0.53 * c).max(0.05);
|
||||
let g = (1.0 + 0.53 * c.abs()).max(0.05);
|
||||
let g = if c > 0.0 { g } else { 1.0 / g };
|
||||
v = if v < 0.5 { 0.5 * (2.0 * v).powf(g) } else { 1.0 - 0.5 * (2.0 - 2.0 * v).max(0.0).powf(g) };
|
||||
}
|
||||
v
|
||||
@@ -1340,6 +1344,32 @@ mod tests {
|
||||
}
|
||||
}
|
||||
|
||||
/// After Effects 2026 ramps (RENDERER round 2, session R8): negative Brightness and Contrast
|
||||
/// are the inverses of the positive curves. (brightness, contrast, input, AE 8-bit)
|
||||
#[test]
|
||||
fn negative_brightness_and_contrast_invert_the_positive_curves() {
|
||||
let go = |b: f64, c: f64, x: u8| {
|
||||
let v = x as f32 / 255.0;
|
||||
(px("ec.color.brightnesscontrast", &[("brightness", num(b)), ("contrast", num(c))], [v, v, v, 1.0])[0] * 255.0).round() as i32
|
||||
};
|
||||
let samples: &[(f64, f64, u8, u8)] = &[
|
||||
(-40.0, 0.0, 64, 50),
|
||||
(-40.0, 0.0, 128, 99),
|
||||
(-40.0, 0.0, 191, 155),
|
||||
(-40.0, 0.0, 234, 207),
|
||||
(0.0, -50.0, 21, 28),
|
||||
(0.0, -50.0, 64, 76),
|
||||
(0.0, -50.0, 191, 179),
|
||||
(0.0, -50.0, 234, 227),
|
||||
(40.0, 0.0, 128, 162),
|
||||
(0.0, 50.0, 64, 52),
|
||||
];
|
||||
for &(b, c, x, ae) in samples {
|
||||
let got = go(b, c, x);
|
||||
assert!((got - ae as i32).abs() <= 7, "B{b} C{c} in {x}: got {got} AE {ae}");
|
||||
}
|
||||
}
|
||||
|
||||
/// #1463: Black & White's sliders set the gray of each pure hue and blend between
|
||||
/// neighbouring hues; neutral pixels keep their value, overbright ones too, and an overbright
|
||||
/// colour stays finite (it used to divide by zero).
|
||||
|
||||
Reference in New Issue
Block a user