mirror of
https://github.com/storytold/effectcraft.git
synced 2026-10-24 10:09:32 -06:00
Tint: Rec. 601 luminance weights, as After Effects
After Effects 2026 (sRGB working space, not linearised) maps red, green, blue and 50% grey solids at Tint's defaults to 76, 150, 29 and 128/255: Rec. 601 weights on the working-space values, not Rec. 709. CPU and GPU kernel changed; tests pin the four greys within 1/255. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
ef74d82902
commit
fb16d5573e
@@ -14,12 +14,19 @@ fn mix(a: [f32; 3], b: [f32; 3], t: f32) -> [f32; 3] {
|
||||
[a[0] + (b[0] - a[0]) * t, a[1] + (b[1] - a[1]) * t, a[2] + (b[2] - a[2]) * t]
|
||||
}
|
||||
|
||||
/// Tint's luminance: Rec. 601 weights on the working-space values, as After Effects 2026
|
||||
/// renders it (sRGB working space, not linearised: red 76/255, green 150/255, blue 29/255).
|
||||
/// The GPU kernel (`pointwise` op 1) mirrors it.
|
||||
fn tint_luma(c: [f32; 3]) -> f32 {
|
||||
0.299 * c[0] + 0.587 * c[1] + 0.114 * c[2]
|
||||
}
|
||||
|
||||
fn tint(ctx: &EffectCtx, mut b: Buf) -> Buf {
|
||||
let black = ctx.params.color("black");
|
||||
let white = ctx.params.color("white");
|
||||
let amt = ctx.params.f("amount") as f32 / 100.0;
|
||||
b.img.map_straight(|c| {
|
||||
let l = luminance(c[0], c[1], c[2]).clamp(0.0, 1.0);
|
||||
let l = tint_luma(c).clamp(0.0, 1.0);
|
||||
let t = [black[0] + (white[0] - black[0]) * l, black[1] + (white[1] - black[1]) * l, black[2] + (white[2] - black[2]) * l];
|
||||
mix(c, t, amt)
|
||||
});
|
||||
@@ -1173,6 +1180,17 @@ mod tests {
|
||||
}
|
||||
}
|
||||
|
||||
/// After Effects 2026 renders of full-frame solids with Tint at its defaults (sRGB working
|
||||
/// space, not linearised, 32 bpc, 8-bit capture): Rec. 601 weights on the encoded values.
|
||||
#[test]
|
||||
fn tint_defaults_match_after_effects_greys() {
|
||||
for (c, want) in [([1.0, 0.0, 0.0], 76.0), ([0.0, 1.0, 0.0], 150.0), ([0.0, 0.0, 1.0], 29.0), ([0.5, 0.5, 0.5], 128.0)] {
|
||||
let o = px("ec.color.tint", &[], [c[0], c[1], c[2], 1.0]);
|
||||
let want = want / 255.0;
|
||||
assert!(close(o, [want, want, want, 1.0], 1.0 / 255.0), "{c:?}: {o:?} vs {want}");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn colorama_reads_get_phase_from() {
|
||||
// Same intensity, different red: Intensity gives one colour, Red gives two.
|
||||
|
||||
@@ -605,7 +605,8 @@ fn pointwise(@builtin(global_invocation_id) gid: vec3<u32>) {
|
||||
case 1u: {
|
||||
if (a > 0.0) {
|
||||
let c = straight(px);
|
||||
let l = clamp(luminance(c), 0.0, 1.0);
|
||||
// Rec. 601 weights, as After Effects (effects crate tint_luma).
|
||||
let l = clamp(0.299 * c.x + 0.587 * c.y + 0.114 * c.z, 0.0, 1.0);
|
||||
let blk = P.f[0].xyz;
|
||||
let wht = P.f[1].xyz;
|
||||
let t = blk + (wht - blk) * l;
|
||||
|
||||
@@ -851,6 +851,25 @@ fn effects_whose_kernels_failed_render_on_the_cpu() {
|
||||
}
|
||||
}
|
||||
|
||||
/// The GPU Tint gives After Effects 2026's greys for full-frame solids at its defaults (sRGB
|
||||
/// working space, not linearised): Rec. 601 weights, as the CPU (`color_fx::tint_luma`).
|
||||
#[test]
|
||||
fn gpu_tint_defaults_match_after_effects_greys() {
|
||||
let Some(g) = gpu() else { return };
|
||||
for (col, want) in [([1.0, 0.0, 0.0], 76.0), ([0.0, 1.0, 0.0], 150.0), ([0.0, 0.0, 1.0], 29.0), ([0.5, 0.5, 0.5], 128.0)] {
|
||||
let mut s = Scene::new(BitDepth::Bpc32);
|
||||
s.p.settings.working_space = Some(ColorSpace::Srgb);
|
||||
let mut l = s.solid(col, 97, 61);
|
||||
s.effect(&mut l, "ec.color.tint", &[]);
|
||||
s.push(l);
|
||||
let mut r = Renderer::new(&s.p, &Pattern, RenderOpts { backend: Backend::Gpu, ..opts() });
|
||||
r.accel = Some(g);
|
||||
let o = g.render(&r, s.cid, Tick::ZERO).expect("the GPU renders the frame").get(48, 30);
|
||||
let want = want / 255.0;
|
||||
assert!(o[..3].iter().all(|v| (v - want).abs() <= 1.0 / 255.0), "{col:?}: {o:?} vs {want}");
|
||||
}
|
||||
}
|
||||
|
||||
/// A layer whose bounds change every frame reads back at a new size each time: the staging
|
||||
/// buffers kept for reuse stay within budget instead of growing for the whole render (#2081).
|
||||
#[test]
|
||||
|
||||
@@ -254,6 +254,22 @@ fn effects_run_in_pipeline() {
|
||||
assert!(c[1] > 0.99 && c[0] < 0.01, "{c:?}");
|
||||
}
|
||||
|
||||
/// After Effects 2026, sRGB working space (not linearised), Tint at its defaults on full-frame
|
||||
/// solids: the effect sees the encoded solid colour and weights it Rec. 601.
|
||||
#[test]
|
||||
fn tint_in_an_srgb_working_space_matches_after_effects_greys() {
|
||||
for (col, want) in [([1.0, 0.0, 0.0], 76.0), ([0.0, 1.0, 0.0], 150.0), ([0.0, 0.0, 1.0], 29.0), ([0.5, 0.5, 0.5], 128.0)] {
|
||||
let (mut p, cid, comp) = setup();
|
||||
p.settings.working_space = Some(effectcraft_color::ColorSpace::Srgb);
|
||||
let mut l = solid(&mut p, &comp, col, 200, 100);
|
||||
add_effect(&mut p, &mut l, "ec.color.tint", [200.0, 100.0], &[]);
|
||||
p.comp_mut(cid).unwrap().layers.push(l);
|
||||
let o = render_frame(&p, cid, Tick::ZERO, 1.0).get(100, 50);
|
||||
let want = want / 255.0;
|
||||
assert!(o[..3].iter().all(|v| (v - want).abs() <= 1.0 / 255.0), "{col:?}: {o:?} vs {want}");
|
||||
}
|
||||
}
|
||||
|
||||
/// Card Dance's Comp Camera through the default comp camera reproduces a 2D layer in place.
|
||||
#[test]
|
||||
fn card_dance_comp_camera_matches_the_default_view() {
|
||||
|
||||
@@ -15,7 +15,7 @@ After Effects', and does the preset library exist. The per-effect catalogue is t
|
||||
| Parameter names, order, popups, units, defaults | aligned in M9.12 | Agent-checked against After Effects' documentation, not against a live dump |
|
||||
| On the GPU | **280** (92%) | Measured: registry flags; GPU vs CPU agreement tested per effect |
|
||||
| 32-bit float, no clamping | 306 | Measured: registry flags |
|
||||
| **Rendered output matches After Effects** | **unmeasured** | No effect is compared with an After Effects render yet. `effectcraft_effects::fidelity::compare_frames` exists for the corpus ([fidelity/README.md](fidelity/README.md)); simulation effects are pinned by golden hashes of our own output (M13.27) |
|
||||
| **Rendered output matches After Effects** | **unmeasured** | Only Tint is checked against After Effects renders (four solids, within 1/255). `effectcraft_effects::fidelity::compare_frames` exists for the corpus ([fidelity/README.md](fidelity/README.md)); simulation effects are pinned by golden hashes of our own output (M13.27) |
|
||||
| Animation presets | **54** original presets (8 text-animator, 23 Transitions, 23 Backgrounds) against After Effects' **621** `.ffx` presets | Measured: `crates/engine/presets/text_animators.json`, `crates/engine/presets/transitions.json`, `crates/engine/presets/backgrounds.json`; the After Effects `Presets` folder |
|
||||
| 26.x additions | Curl Noise (26.3) present (#463 open); effect colour labels (26.5) present in Effect Controls only, not in the Timeline; the rebuilt Effect Controls (26.5) missing | Release notes |
|
||||
|
||||
@@ -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 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 has Swap Colors (#963) and matches After Effects 2026 renders of primary and grey solids (Rec. 601 luminance); 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 |
|
||||
@@ -64,6 +64,7 @@ the GPU is performance, not parity.
|
||||
|
||||
| Date | Change | Summary |
|
||||
|---|---|---|
|
||||
| 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-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 |
|
||||
| 2026-10-12 | minor | Tint's Swap Colors button and agent-drivable command (#963); undo/redo, instance targeting, animated layer-time edits and invalid-input tests |
|
||||
|
||||
Reference in New Issue
Block a user