light: AUTO and the shipped recipes ride the halved knobs too — AUTO's ceiling doubles with them so a blown frame still comes back the way it did, and each recipe's HIGHLIGHT/SHADOW doubles so its look stays on the knot it was tuned to

This commit is contained in:
2026-09-30 18:10:51 +07:00
parent a56581c757
commit 25b1312e0a
3 changed files with 40 additions and 28 deletions
+10 -10
View File
@@ -62,28 +62,28 @@ assert.equal(Math.round(stops(spike(87)) * 10) / 10, 0.5, 'a dark frame is asked
assert.ok(Math.abs(stops(flat()) + 0.0589) < 5e-4, 'a flat ramp sits just under mid-grey');
assert.equal(stops(spike(20, 0)), AUTO_EV_MAX, 'an empty histogram takes the ceiling, not infinity');
// --- Highlight / Shadow: the 1% tails, thresholds 0.9 / 0.02, ramp to ±5.
assert.deepEqual(autoTone(spike(255)), { highlight: -5, shadow: 0 }, 'blown frame only pulls highlights');
assert.deepEqual(autoTone(spike(0)), { highlight: 0, shadow: 5 }, 'black frame only opens shadows');
// --- Highlight / Shadow: the 1% tails, thresholds 0.9 / 0.02, ramp to ±10.
assert.deepEqual(autoTone(spike(255)), { highlight: -10, shadow: 0 }, 'blown frame only pulls highlights');
assert.deepEqual(autoTone(spike(0)), { highlight: 0, shadow: 10 }, 'black frame only opens shadows');
// The flat ramp is the interesting one: its own top 1% really is clipped and its
// own bottom 1% really is on the floor, so both ends move. p99 = 253/255 = 0.992
// (5 units), p01 = 2/255 = 0.008 (3 units).
assert.deepEqual(autoTone(flat()), { highlight: -5, shadow: 3 }, 'an even ramp has both ends in play');
// (9 units), p01 = 2/255 = 0.008 (6 units).
assert.deepEqual(autoTone(flat()), { highlight: -9, shadow: 6 }, 'an even ramp has both ends in play');
// 1% of the frame at white over an otherwise mid ramp: the spec's small blown
// window, and the shadow side stays put because the ramp's floor is not black.
const blown = flat();
blown[255] += 3;
assert.deepEqual(autoTone(blown), { highlight: -5, shadow: 3 }, 'a blown 1% is enough to pull the top');
assert.deepEqual(autoTone(blown), { highlight: -10, shadow: 6 }, 'a blown 1% is enough to pull the top');
// p99 landed just over the threshold: 989 pixels mid-ramp and 11 at bin 243 puts
// p99 at 0.953, 0.053 over 0.9 -> 2.65 units -> 3. Proof the pull is graded.
// p99 at 0.953, 0.053 over 0.9 -> 5.3 units -> 5. Proof the pull is graded.
const shoulder = spike(100, 989);
shoulder[243] = 11;
assert.deepEqual(autoTone(shoulder), { highlight: -3, shadow: 0 }, 'p99 just over 0.9 pulls a third of the way');
assert.deepEqual(autoTone(shoulder), { highlight: -5, shadow: 0 }, 'p99 just over 0.9 pulls half the way');
// 2% of pixels two bins off the floor: p01 = 0.008, so the shadow side moves —
// but only 3 units, because the tail is barely under the 0.02 threshold.
// but only 6 units, because the tail is barely under the 0.02 threshold.
const crushed = spike(128, 980);
crushed[2] = 20;
assert.deepEqual(autoTone(crushed), { highlight: 0, shadow: 3 }, 'a crushed 2% opens the shadows part way');
assert.deepEqual(autoTone(crushed), { highlight: 0, shadow: 6 }, 'a crushed 2% opens the shadows part way');
// 0.4% on the floor is NOT the bottom 1%: the tenth-darkest pixel is the mid
// ramp, so the threshold is not crossed and the knob stays put.
const speck = spike(128, 996);