mirror of
https://github.com/storytold/effectcraft.git
synced 2026-10-24 18:18:17 -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).
|
||||
|
||||
@@ -624,14 +624,20 @@ fn pointwise(@builtin(global_invocation_id) gid: vec3<u32>) {
|
||||
} else if (P.u[0].y == 2u) {
|
||||
// Brightness & Contrast tone curves (effects crate bc_modern, AE-fitted #681).
|
||||
var o = c;
|
||||
let g = max(1.0 + 0.53 * ct, 0.05);
|
||||
// Negative settings invert the positive curves (reciprocal exponents).
|
||||
var g = max(1.0 + 0.53 * abs(ct), 0.05);
|
||||
if (ct < 0.0) {
|
||||
g = 1.0 / g;
|
||||
}
|
||||
var e = exp2(1.24 * abs(br));
|
||||
if (br < 0.0) {
|
||||
e = 1.0 / e;
|
||||
}
|
||||
for (var i = 0; i < 3; i++) {
|
||||
if (c[i] < 1.0) {
|
||||
var v = max(c[i], 0.0);
|
||||
if (br > 0.0) {
|
||||
v = 1.0 - powz(1.0 - v, exp2(1.24 * br));
|
||||
} else if (br < 0.0) {
|
||||
v = powz(v, exp2(-1.32 * br));
|
||||
if (br != 0.0) {
|
||||
v = 1.0 - powz(1.0 - v, e);
|
||||
}
|
||||
if (ct != 0.0) {
|
||||
if (v < 0.5) {
|
||||
|
||||
@@ -30,7 +30,7 @@ corpus compares renders, each effect is assumed to differ somewhere, and users k
|
||||
| Audio | 13 | 0 | CPU by design (applied in the mixdown) |
|
||||
| Blur & Sharpen | 16 | 15 | Gaussian Blur interaction and output to be measured first ([fidelity/ae-2026-acceptance.md](fidelity/ae-2026-acceptance.md)) |
|
||||
| Channel | 13 | 13 | — |
|
||||
| Color Correction | 40 | 40 | Tint, Tritone and Black & White (sliders and Tint) match After Effects 2026 renders (Rec. 601 luminance); Levels (Individual Controls) matches After Effects 2026 in 32 bpc (channel controls before RGB, "Off for 32 bpc Color" unclipped; plain Levels' channel order not measured); Tint has Swap Colors (#963); ACES 2.0 / OCIO 2.5 (26.5); Apply Color LUT (#514) and 32-bpc blending (#494) are closed |
|
||||
| Color Correction | 40 | 40 | Tint, Tritone and Black & White (sliders and Tint) match After Effects 2026 renders (Rec. 601 luminance); Levels (Individual Controls) matches After Effects 2026 in 32 bpc (channel controls before RGB, "Off for 32 bpc Color" unclipped; plain Levels' channel order not measured); Brightness & Contrast (non-legacy) within about 6/255 of After Effects 2026 ramps (negative settings invert the positive curves); Tint has Swap Colors (#963); ACES 2.0 / OCIO 2.5 (26.5); Apply Color LUT (#514) and 32-bpc blending (#494) are closed |
|
||||
| Distort | 39 | 35 | Liquify has no viewer brush; #303 (Mesh Warp drag) and #534 (CC Power Pin Unstretch) are closed |
|
||||
| Expression Controls | 9 | 9 | — |
|
||||
| Generate | 26 | 25 | Fractal on the CPU (needs f64); #526 (Reset) and #536 (Write-on marks) are closed |
|
||||
@@ -67,8 +67,8 @@ the GPU is performance, not parity.
|
||||
| 2026-10-19 | minor | Tritone uses Rec. 601 luminance, measured against After Effects 2026 renders of a red solid and a grey ramp (CPU and GPU) |
|
||||
| 2026-10-19 | minor | Black & White's Tint keeps the gray's luminosity (Color blend), measured against After Effects 2026 renders (CPU and GPU) |
|
||||
| 2026-10-19 | minor | Levels: "Off for 32 bpc Color" no longer clips in 32 bpc projects, unclipped values extrapolate below Output Black; Levels (Individual Controls) applies the channel controls before the RGB ones, measured against After Effects 2026 (CPU and GPU) |
|
||||
| 2026-10-19 | minor | Brightness & Contrast: negative Brightness / Contrast are the inverse curves of the positive ones (Brightness −40: 20/255 → 6/255 off After Effects 2026), CPU and GPU |
|
||||
| 2026-10-18 | minor | Tint uses Rec. 601 luminance weights, measured against After Effects 2026 renders of red, green, blue and grey solids (CPU and GPU) |
|
||||
| 2026-10-18 | minor | Exposure converts to linear light with a pure 2.2 power curve, as measured in After Effects (Offset 0.0005 / 0.002 / 0.02 / 0.1 on black: 8 / 15 / 43 / 90), not the piecewise sRGB curve |
|
||||
| 2026-10-18 | minor | Exposure converts to linear light with a pure 2.2 power curve, as measured in After Effects (Offset 0.0005 / 0.002 / 0.02 / 0.1 on black: 8 / 15 / 43 / 90), not the piecewise sRGB curve; its sliders take After Effects' ranges (Exposure −100..100, slider −4..4; Offset −2..2, slider −0.5..0.5; Gamma Correction 0.1..10) |
|
||||
| 2026-10-12 | minor | Parameters 3,014 to 3,017 (effects.md); #303, #514, #526, #533–#536 marked closed instead of pending |
|
||||
| 2026-10-12 | minor | Effect colour labels (26.5) added in Effect Controls |
|
||||
|
||||
Reference in New Issue
Block a user