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:
@@ -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);
|
||||
|
||||
@@ -95,6 +95,12 @@ export function filmSimToRecipe(sim: FilmSim): Recipe {
|
||||
};
|
||||
}
|
||||
|
||||
// The shipped recipes. HIGHLIGHT and SHADOW here are written in the KNOB's unit,
|
||||
// and both knobs ride HALF an anchor per unit (see a1/a3 in toneShader.ts), so a
|
||||
// recipe that wants a given roll-off asks for double what the knot move reads —
|
||||
// the same note FILM_TONE carries. The look is the knot, not the number, which is
|
||||
// why these were doubled when the two knobs' reach was halved: a recipe that had
|
||||
// its highlights rolled to 0.70 still rolls them there.
|
||||
export const DEFAULT_RECIPES: Recipe[] = [
|
||||
{
|
||||
id: 'classic-neg-default',
|
||||
@@ -106,8 +112,8 @@ export const DEFAULT_RECIPES: Recipe[] = [
|
||||
saturation: -1,
|
||||
temperature: 6300,
|
||||
tint: 2,
|
||||
highlight: -2,
|
||||
shadow: 1,
|
||||
highlight: -4,
|
||||
shadow: 2,
|
||||
denoise: 1,
|
||||
clarity: 2,
|
||||
grain: 4,
|
||||
@@ -130,8 +136,8 @@ export const DEFAULT_RECIPES: Recipe[] = [
|
||||
saturation: 3,
|
||||
temperature: 5500,
|
||||
tint: 0,
|
||||
highlight: 1,
|
||||
shadow: -1,
|
||||
highlight: 2,
|
||||
shadow: -2,
|
||||
denoise: 2,
|
||||
clarity: 4,
|
||||
grain: 1,
|
||||
@@ -208,8 +214,8 @@ export const DEFAULT_RECIPES: Recipe[] = [
|
||||
saturation: 1,
|
||||
temperature: 7500,
|
||||
tint: -2,
|
||||
highlight: -3,
|
||||
shadow: 2,
|
||||
highlight: -6,
|
||||
shadow: 4,
|
||||
denoise: 3,
|
||||
clarity: -2,
|
||||
grain: 5,
|
||||
@@ -233,8 +239,8 @@ export const DEFAULT_RECIPES: Recipe[] = [
|
||||
saturation: 1,
|
||||
temperature: 5800,
|
||||
tint: 0,
|
||||
highlight: -1,
|
||||
shadow: 1,
|
||||
highlight: -2,
|
||||
shadow: 2,
|
||||
denoise: 2,
|
||||
clarity: 1,
|
||||
grain: 3,
|
||||
@@ -258,8 +264,8 @@ export const DEFAULT_RECIPES: Recipe[] = [
|
||||
saturation: 2,
|
||||
temperature: 6000,
|
||||
tint: -1,
|
||||
highlight: 1,
|
||||
shadow: -1,
|
||||
highlight: 2,
|
||||
shadow: -2,
|
||||
denoise: 1,
|
||||
clarity: 2,
|
||||
grain: 2,
|
||||
@@ -283,8 +289,8 @@ export const DEFAULT_RECIPES: Recipe[] = [
|
||||
saturation: 2,
|
||||
temperature: 7200,
|
||||
tint: -2,
|
||||
highlight: -2,
|
||||
shadow: 1,
|
||||
highlight: -4,
|
||||
shadow: 2,
|
||||
denoise: 2,
|
||||
clarity: 0,
|
||||
grain: 4,
|
||||
@@ -308,8 +314,8 @@ export const DEFAULT_RECIPES: Recipe[] = [
|
||||
saturation: -2,
|
||||
temperature: 4500,
|
||||
tint: 2,
|
||||
highlight: -2,
|
||||
shadow: 2,
|
||||
highlight: -4,
|
||||
shadow: 4,
|
||||
denoise: 3,
|
||||
clarity: -2,
|
||||
grain: 3,
|
||||
|
||||
@@ -81,10 +81,16 @@ export function autoExposureStops(lum: number[]): number {
|
||||
return Math.max(-AUTO_EV_MAX, Math.min(AUTO_EV_MAX, stops));
|
||||
}
|
||||
|
||||
// How far AUTO may push HIGHLIGHT and SHADOW, in the sliders' own units: half
|
||||
// of the ±10 ruler, so the frame is corrected but a hand can still finish the
|
||||
// move. The knob then reports the number AUTO chose, the way the EV knob does.
|
||||
const AUTO_TONE_MAX = 5;
|
||||
// How far AUTO may push HIGHLIGHT and SHADOW, in the sliders' own units: the
|
||||
// whole ±10 ruler, so a frame that really is clipped (or really is crushed)
|
||||
// really does come back. It was 5 when a unit of HIGHLIGHT or SHADOW moved its
|
||||
// knot a whole anchor; both now ride HALF an anchor (see a1/a3 in toneShader.ts),
|
||||
// so 5 would have halved AUTO's own correction with the knob's reach. At 10 it
|
||||
// draws the knot as far as the old number did — HIGHLIGHT to 0.625, SHADOW to
|
||||
// 0.375 — where the quarter beside it is still 0.5 of its own slope and never
|
||||
// flat — and the knob then reports the number AUTO chose, the way the EV knob
|
||||
// does.
|
||||
const AUTO_TONE_MAX = 10;
|
||||
|
||||
// The value `p` of the way up the binned ramp (0.99 for the top 1% of pixels):
|
||||
// walk the cumulative count to the first bin that passes `p * total`, and report
|
||||
|
||||
Reference in New Issue
Block a user