web: the four tonal knobs are sized by what the eye can see, and the highlight head is squared so a bigger rate fits inside the cube
The report was "giá trị thay đổi của các thông số quá nhỏ, không thể hiện được
trên thị giác của ảnh" — at the doc's own rates a full +100 was worth 0.060 of
luma on BLACKS, 0.082 on HIGHLIGHTS and 0.042 on WHITES, and the probe that ran
the frame through the pass read BLACKS +100 moving its mean by 0.0001. Every
rate below is now the largest its own move allows, measured rather than
inherited: 0.058 -> 0.080 on BLACKS, 0.082 -> 0.113 on HIGHLIGHTS and 0.042 ->
0.080 on WHITES, with SHADOWS' 0.151 left where it was because it already bit.
- `TONE_BLACK_LIFT` 0.7 -> 0.93. The ceiling is a FOLD, not a slope: past
0.9387 the doc's own square root carries luma backwards inside its window
(0.94 folds 3.4e-6, 0.95 folds 8.9e-5) and a gradient wears it as a band.
0.93 is the last round rate under it — monotone on the check's 1/32768
grid, and the 1e-4 of travel between it and 0.94 is not a code value.
- `TONE_BLACK_CRUSH` 0.85 -> 8.0. The doc's own form — `L * (1 + amount * W
* 0.85)` — is bounded by its own window, which is 1 only AT the floor, so
its whole visible travel at full -100 is 0.019 of luma: five code values on
a black patch, and a rate past 1 drives the product negative and clips the
toe to a flat black instead of deepening it. The toe's own EXPONENT,
`L -> W * (L/W)^(1 + rate * W)`, is monotone for ANY rate and worth 0.054,
while x = 0 stays on 0 and the 0.18 edge stays on 1 — both anchors and the
compact support kept.
- `TONE_HIGH_GAIN` 2.5 -> 14.0, with the head term moved from `(1 - L)` to
`(1 - L)^2`. The linear headroom dies too slowly to keep the rate's own
ceiling off the clamp: above a gain worth 2.6 the move overshoots 1.0, the
clamp draws a plateau and the ramp falls back over it by 0.018 — the fold
the knee check now measures as a drawdown from the running maximum. Squared,
the move has died out by the time the ramp reaches the clamp: 14.0 is
fold-free at both signs and still lands 1.44x of the 1.5x the quarter it
owns is allowed.
- `TONE_WHITE_GAIN` 1.5 -> 3.0, which takes the top of the ramp TO the
ceiling from 0.92 up and leaves the clamp to flatten what is left. That is
the doc's own §2.4, where a WHITE is the frame's clipping point ("giới hạn
cháy sáng") and not a Hermite that cannot move the head; it is felt only
above the 0.80 shoulder and the head is still exactly 1.0 on 1.0.
`FILM_TONE` is re-solved for the squared head, which is worth less at the 0.75
knot for the same rate: monochrome -0.16 -> -0.1143 and mono-high-contrast
+0.83 -> +0.5929. The four knots the stocks are tuned to do not move — 0.22 and
0.7375 on Acros, 0.17 and 0.815 on Acros HC — and the check pins each of them.
The knee check's guard is REPLACED. The old one compared the first cell of the
sweep against the second (a slope at 1/512), which is blind to a fold that
starts later: it passed a ramp whose own drawdown was 0.018. The new one walks
1/32768 of the ramp and measures `running max - value` for each knob at both
signs, over the four moves alone and then over a 243-combination sweep, so what
is pinned is the fold itself and where it is.
Two folds are pinned rather than removed, both named in the check:
- SHADOWS -100 dips 0.018 (4.6 code values) around 0.06..0.11 of its own
accord. It is the doc's §2.2 formula — `L *= 1 + amount * W * (1 - L)^1.8`
— where the window rises faster than the light, and it PREDATES this change.
The monotone rewrite (`L' = 1 - (1 - L)^(1 - SH * |a| * W(L))`) is written
out beside it and was NOT taken: it is exact but it costs the knob 30-50% of
its crush.
- WHITE +100 rests a plateau on the clamp from 0.92 up. That is what §2.4 asks
of the knob and the ramp is non-decreasing through it, so it is not a fold.
`scripts/tone-base-check.mjs`'s mirror of the pass takes the same three moves
(its own `toneBlack` and the squared head) so the pixel it predicts is still the
pixel the pass draws.
Checked: node scripts/highlight-knee-check.mjs; node scripts/tone-base-check.mjs;
node scripts/auto-tone-check.mjs; node scripts/half-check.mjs; node
scripts/mask-wb-check.mjs; node scripts/preview-match-check.mjs; node
scripts/raw-develop-check.mjs; node scripts/white-level-check.mjs; node
scripts/wb-table-check.mjs; node scripts/sharpen-check.mjs; node
scripts/denoise-check.mjs; npx tsc --noEmit.
This commit is contained in:
@@ -78,7 +78,7 @@ assert.ok(tmpl, 'TONE_SKSL is gone');
|
||||
// compat doc §3.3 warns the Android port about).
|
||||
const mathTmpl = tone.match(/export const TONE_MATH_SKSL = `([\s\S]*?)`;/)?.[1];
|
||||
assert.ok(mathTmpl, 'TONE_MATH_SKSL is gone — the frame and a mask no longer share the maths');
|
||||
assert.equal((mathTmpl.match(/\$\{TONE_[A-Z_]+\}/g) ?? []).length, 7, 'a window edge or a rate is pinned to a literal, not to its TONE_ constant');
|
||||
assert.equal((mathTmpl.match(/\$\{TONE_[A-Z_]+\}/g) ?? []).length, 8, 'a window edge or a rate is pinned to a literal, not to its TONE_ constant');
|
||||
assert.ok(tmpl.includes('${TONE_MATH_SKSL}'), 'the frame pass carries its own copy of the ramp again');
|
||||
assert.match(tmpl, /rgb = toneRamp\(rgb, t, baseLuma\(xy\), bl, sh, hl, wh, dr\);/);
|
||||
// The BASE layer the ramp is drawn through. It is ONE tap of a blurred child,
|
||||
@@ -167,15 +167,19 @@ assert.doesNotMatch(codeOf(sksl), /toneBump/, 'the summed kernel is back — the
|
||||
assert.doesNotMatch(codeOf(sksl), /holdLo|holdHi/, 'the shared ceiling is back — a stock drags another knob with it');
|
||||
assert.doesNotMatch(tone, /export const TONE_ANCHOR|export const TONE_BUMP_SLOPE/, 'the old anchor and guard constants are still declared');
|
||||
// The composition, move for move: the black pair (lift through the doc's square
|
||||
// root, crush by the doc's rate, one or the other — both zero at L = 0, so the
|
||||
// black point stays the black point and no knob leaves a pedestal), the shadow
|
||||
// gain, the highlight knee against the headroom that is LEFT (the (1 - L) is the
|
||||
// root, crush by the toe's own exponent, one or the other — both zero at L = 0, so
|
||||
// the black point stays the black point and no knob leaves a pedestal), the shadow
|
||||
// gain, the highlight knee against the headroom that is LEFT (the (1 - L)^2 is the
|
||||
// one deliberate departure from the doc's raw pow, which overshoots the cube), and
|
||||
// the white Hermite (1 - L) * L — zero on BOTH ends, so the head cannot move.
|
||||
assert.match(maths, text('float toneCurve(float L, float bl, float sh, float hl, float wh) {'));
|
||||
assert.match(maths, text(` q = toneBlackW(L);\n L = bl > 0.0\n ? L + ${TONE_BLACK_LIFT} * bl * q * (sqrt(L) - L)\n : L * max(1.0 + ${TONE_BLACK_CRUSH} * bl * q, 0.0);\n L = clamp(L, 0.0, 1.0);`));
|
||||
assert.match(maths, text(' L = toneBlack(L, bl);\n L = clamp(L, 0.0, 1.0);'));
|
||||
// ...and the black pair itself, pinned on the source as the function the curve now
|
||||
// calls: the doc's lift at its rate, and the exponent that replaces the doc's
|
||||
// crush (the rate is on the exponent, so the closed form cannot run backwards).
|
||||
assert.match(maths, text(`float toneBlack(float L, float bl) {\n const float W = ${TONE_BLACK_EDGE};\n float q = toneBlackW(L);\n if (bl > 0.0) return L + ${TONE_BLACK_LIFT} * bl * q * (sqrt(L) - L);\n if (L >= W) return L;\n return W * pow(L / W, 1.0 + ${TONE_BLACK_CRUSH} * (-bl) * q);\n}`));
|
||||
assert.match(maths, text(' q = toneShadowW(L);\n L *= 1.0 + sh * q * pow(1.0 - L, 1.8);\n L = clamp(L, 0.0, 1.0);'));
|
||||
assert.match(maths, text(` q = toneHighW(L);\n L += ${TONE_HIGH_GAIN} * hl * q * pow(max(L - 0.5, 0.0), 1.5) * (1.0 - L);\n L = clamp(L, 0.0, 1.0);`));
|
||||
assert.match(maths, text(` q = toneHighW(L);\n L += ${TONE_HIGH_GAIN} * hl * q * pow(max(L - 0.5, 0.0), 1.5) * (1.0 - L) * (1.0 - L);\n L = clamp(L, 0.0, 1.0);`));
|
||||
assert.match(maths, text(` q = toneWhiteW(L);\n L += ${TONE_WHITE_GAIN} * wh * q * (1.0 - L) * L;`));
|
||||
// DR rides the same four moves with a share of each instead of masked terms of
|
||||
// its own, so it cannot fight a knob over a band or invert the ramp.
|
||||
@@ -314,26 +318,35 @@ const toneWhiteW = (L) => {
|
||||
const u = clamp01((L - TONE_WHITE_EDGE) / (1 - TONE_WHITE_EDGE));
|
||||
return u * u;
|
||||
};
|
||||
// toneBlack, move for move — the doc's lift through the square root, and the
|
||||
// toe's own exponent for the crush, which is what the shader carries (see
|
||||
// toneShader.ts): the doc's multiplicative crush is bounded by its own window and
|
||||
// cannot be seen. It is a function of its own so the arithmetic below and the
|
||||
// source's own text can be pinned to one shape.
|
||||
const toneBlack = (L, bl) => {
|
||||
const q = toneBlackW(L);
|
||||
if (bl > 0) return L + TONE_BLACK_LIFT * bl * q * (Math.sqrt(L) - L);
|
||||
if (L >= TONE_BLACK_EDGE) return L;
|
||||
return TONE_BLACK_EDGE * (L / TONE_BLACK_EDGE) ** (1 + TONE_BLACK_CRUSH * -bl * q);
|
||||
};
|
||||
// toneCurve, move for move, with the clamp the shader puts after each one: BLACK
|
||||
// (the doc's lift through the square root, or the doc's crush), SHADOW (a gain on
|
||||
// the light), HIGHLIGHT (the doc's knee against the headroom that is left) and
|
||||
// WHITE (the Hermite (1 - L) * L). `afterBlack` is the L the shadow move reads —
|
||||
// the value a stock's own SHADOW cannot drag, because BLACK runs before it.
|
||||
// (toneBlack), SHADOW (a gain on the light), HIGHLIGHT (the doc's knee against the
|
||||
// headroom that is left, squared) and WHITE (the Hermite (1 - L) * L).
|
||||
// `afterBlack` is the L the shadow move reads — the value a stock's own SHADOW
|
||||
// cannot drag, because BLACK runs before it.
|
||||
function tones(t, k) {
|
||||
const { dr = 0, hl = 0, sh = 0, wh = 0, bl = 0 } = k;
|
||||
let L = t;
|
||||
let q = toneBlackW(L);
|
||||
let q;
|
||||
const blackA = clamp(bl + dr * 0.12, -1, 1);
|
||||
L = blackA > 0
|
||||
? L + TONE_BLACK_LIFT * blackA * q * (Math.sqrt(L) - L)
|
||||
: L * Math.max(1 + TONE_BLACK_CRUSH * blackA * q, 0);
|
||||
L = toneBlack(L, blackA);
|
||||
const afterBlack = clamp01(L);
|
||||
L = afterBlack;
|
||||
q = toneShadowW(L);
|
||||
L *= 1 + clamp(sh + dr * 0.06, -1, 1) * q * (1 - L) ** 1.8;
|
||||
L = clamp01(L);
|
||||
q = toneHighW(L);
|
||||
L += TONE_HIGH_GAIN * clamp(hl - dr * 0.09, -1, 1) * q * Math.max(L - 0.5, 0) ** 1.5 * (1 - L);
|
||||
L += TONE_HIGH_GAIN * clamp(hl - dr * 0.09, -1, 1) * q * Math.max(L - 0.5, 0) ** 1.5 * (1 - L) ** 2;
|
||||
L = clamp01(L);
|
||||
q = toneWhiteW(L);
|
||||
L += TONE_WHITE_GAIN * clamp(wh - dr * 0.18, -1, 1) * q * (1 - L) * L;
|
||||
@@ -391,34 +404,65 @@ near4(ramp(0.5, { sh: -1 }).o, 0.48936, 'SHADOW no longer spans the middle');
|
||||
|
||||
// Monotone under EVERY combination of the four at full deflection, DR included.
|
||||
// The composition of monotone moves is monotone by construction, but each move is
|
||||
// only piecewise — a window edge is a slope change — so this is the measurement
|
||||
// of that. The grid is 1/512 thick, so a step can straddle an edge and backtrack
|
||||
// inside the width of one cell: the worst measured is -0.00119 at t = 0.0977
|
||||
// (BLACK -1 with SHADOW -1, the two crush moves meeting under the toe window's
|
||||
// edge — 0.30 of a code value), and the same sweep at 1/8192 reads -0.0000745,
|
||||
// so the size of the cell is what the number is. A wider backtrack is a fold.
|
||||
// only piecewise — a window edge is a slope change — so this is the measurement of
|
||||
// that, and the measurement is the DROP FROM THE RUNNING MAXIMUM rather than the
|
||||
// step between two cells. A fold of a tenth of a code value spread over a dozen
|
||||
// cells hides from a cell-sized step: the step this file used to read here was
|
||||
// -0.00119 while the drop under SHADOW -100 alone was 0.018, three times the guard
|
||||
// and the thing a gradient wears as a band. The drop is what the eye reads.
|
||||
//
|
||||
// Every rate this pass sets is fold-free: the worst any of them can do is 3.4e-6
|
||||
// of luma (BLACK's lift, at the square root's own corner). The one fold left in
|
||||
// the ramp is SHADOWS' and it is the doc's own arithmetic, not a rate: §2.2's
|
||||
// crush multiplies what a window leaves, that window RISES as the luma does over
|
||||
// 0.02..0.12, so the move outruns the light and the ramp comes back down by 0.018
|
||||
// of luma (4.6 code values) around 0.06..0.11 at full -100 — at any rate, since
|
||||
// scaling it only scales the same shape. It predates this pass and is pinned here
|
||||
// so a rate change cannot quietly deepen it: the assertion below is that no rate
|
||||
// in this file folds the ramp at all, and that no composition of the four is worse
|
||||
// than the doc's own crush.
|
||||
const combos = [];
|
||||
for (const bl of [-1, 0, 1])
|
||||
for (const sh of [-1, 0, 1])
|
||||
for (const hl of [-1, 0, 1])
|
||||
for (const wh of [-1, 0, 1])
|
||||
for (const dr of [0, 1]) combos.push({ bl, sh, hl, wh, dr });
|
||||
let worst = Infinity;
|
||||
let worstAt = 0;
|
||||
let worstCombo = null;
|
||||
for (const k of combos) {
|
||||
let prev = null;
|
||||
for (let t = 0; t <= 1; t += 1 / 512) {
|
||||
const fold = (k, n) => {
|
||||
let top = -Infinity;
|
||||
let worst = 0;
|
||||
let at = 0;
|
||||
for (let i = 0; i <= n; i++) {
|
||||
const t = i / n;
|
||||
const o = ramp(t, k).o;
|
||||
if (prev !== null && o - prev < worst) {
|
||||
worst = o - prev;
|
||||
worstAt = t;
|
||||
worstCombo = k;
|
||||
if (o > top) top = o;
|
||||
else if (top - o > worst) {
|
||||
worst = top - o;
|
||||
at = t;
|
||||
}
|
||||
prev = o;
|
||||
}
|
||||
return { worst, at };
|
||||
};
|
||||
// A knob's own rate, at full deflection either way: the rates are set so this is
|
||||
// zero to the rounding — 2e-3 is half a code value, under what 8-bit can carry.
|
||||
for (const [knob, amount] of [['bl', 1], ['bl', -1], ['hl', 1], ['hl', -1], ['wh', 1], ['wh', -1]]) {
|
||||
const { worst, at } = fold({ [knob]: amount }, 32768);
|
||||
assert.ok(worst <= 2e-3, `${knob} at ${amount} folds the ramp by ${worst} at ${at} — a rate past what its own shape allows`);
|
||||
}
|
||||
// SHADOW's crush, the doc's own — see the note above. Pinned: this number is a
|
||||
// property of §2.2's form, so a change here means the form changed.
|
||||
near4(fold({ sh: -1 }, 32768).worst, 0.01795, 'SHADOW no longer folds where the doc\'s own crush folds — re-pin, and see toneShader.ts');
|
||||
let worstFold = 0;
|
||||
let foldAt = 0;
|
||||
let foldCombo = null;
|
||||
for (const k of combos) {
|
||||
const { worst, at } = fold(k, 8192);
|
||||
if (worst > worstFold) {
|
||||
worstFold = worst;
|
||||
foldAt = at;
|
||||
foldCombo = k;
|
||||
}
|
||||
}
|
||||
assert.ok(worst >= -2e-3, `worst step ${worst} at ${worstAt} for ${JSON.stringify(worstCombo)} — the ramp is folded`);
|
||||
assert.ok(worstFold <= 0.01795 + 2e-3, `worst fold ${worstFold} at ${foldAt} for ${JSON.stringify(foldCombo)} — a composition deepens the doc's fold`);
|
||||
// ...and every one of those combinations still lands on the two anchors, exactly.
|
||||
for (const k of combos) {
|
||||
exact(ramp(0, k).o, 0, `the toe moved at ${JSON.stringify(k)}`);
|
||||
@@ -434,35 +478,41 @@ exact(ramp(0.90, { hl: -1 }).o, ramp(0.90, { hl: -1, sh: 0.47 }).o, 'SHADOW reac
|
||||
exact(ramp(0.05, { sh: -1 }).o, ramp(0.05, { sh: -1, wh: 1 }).o, 'WHITE reached into SHADOW\'s band');
|
||||
// The reach of each knob, read off the formula. These are OUTPUT values of the
|
||||
// ramp (`.o`) to five decimals, so near4 is the rounding of what is written down
|
||||
// and nothing wider. BLACK - is the doc's crush and + the doc's lift; SHADOW is a
|
||||
// gain on the light, so it reaches past 0.5 (0.51064 at +100 against 0.48936 at
|
||||
// -100 — the bell under the window is not symmetric); HIGHLIGHT and WHITE move
|
||||
// the head and neither clips it.
|
||||
// and nothing wider. BLACK - is the toe's exponent and + the doc's lift; SHADOW is
|
||||
// a gain on the light, so it reaches past 0.5 (0.51064 at +100 against 0.48936 at
|
||||
// -100 — the bell under the window is not symmetric); HIGHLIGHT moves the head
|
||||
// without reaching it (0.95722 at 0.95 against 0.97579 before this pass — the
|
||||
// squared headroom spends the move lower down), and WHITE is the one that does
|
||||
// reach it, from 0.92 up (see the check below).
|
||||
for (const [knob, sides] of [
|
||||
['bl', [
|
||||
[-1, [[0.05, 0.03399], [0.10, 0.09254], [0.15, 0.14941], [0.20, 0.20], [0.50, 0.50]]],
|
||||
[1, [[0.05, 0.09578], [0.10, 0.11329], [0.18, 0.18]]],
|
||||
[-1, [[0.05, 0.00105], [0.10, 0.06618], [0.15, 0.14899], [0.20, 0.20], [0.50, 0.50]]],
|
||||
[1, [[0.05, 0.11082], [0.10, 0.11765], [0.18, 0.18]]],
|
||||
]],
|
||||
['sh', [
|
||||
[1, [[0.10, 0.17412], [0.20, 0.33384], [0.50, 0.51064]]],
|
||||
[-1, [[0.10, 0.02588], [0.20, 0.06616], [0.50, 0.48936]]],
|
||||
]],
|
||||
['hl', [
|
||||
[1, [[0.70, 0.76708], [0.80, 0.88216], [0.90, 0.96325], [0.95, 0.97579]]],
|
||||
[-1, [[0.70, 0.63292], [0.80, 0.71784], [0.95, 0.92421]]],
|
||||
[1, [[0.70, 0.81270], [0.80, 0.89202], [0.90, 0.93542], [0.95, 0.95722]]],
|
||||
[-1, [[0.70, 0.58730], [0.80, 0.70798], [0.95, 0.94278]]],
|
||||
]],
|
||||
['wh', [
|
||||
[1, [[0.90, 0.93375], [0.95, 0.99008]]],
|
||||
[-1, [[0.90, 0.86625], [0.95, 0.90992]]],
|
||||
[1, [[0.90, 0.96750], [0.95, 1.0]]],
|
||||
[-1, [[0.90, 0.83250], [0.95, 0.86984]]],
|
||||
]],
|
||||
])
|
||||
for (const [amount, cases] of sides)
|
||||
for (const [t, want] of cases)
|
||||
near4(ramp(t, { [knob]: amount }).o, want, `${knob} at ${amount} on ${t}`);
|
||||
// Nothing clips inside the ramp: HIGHLIGHT +100 at 0.95 is still under the
|
||||
// ceiling of the cube, and the only interior value that reaches 1.0 is all four
|
||||
// at +100 on a pixel already at the head — the anchors are what the clamp holds.
|
||||
// HIGHLIGHT does not clip: +100 at 0.95 is still under the ceiling of the cube
|
||||
// (0.95722, and 0.93542 at 0.90), because the headroom it reads is squared and the
|
||||
// move has died out by the top — while WHITE, the frame's clipping point, takes the
|
||||
// ramp TO 1.0 from 0.92 up, which is what §2.4 asks of it. Either way the only
|
||||
// interior value a full set of knobs reaches 1.0 on is a pixel already at the head.
|
||||
assert.ok(ramp(0.95, { hl: 1 }).o < 1, 'HIGHLIGHT +100 is clipping the head');
|
||||
assert.ok(ramp(0.9, { hl: 1 }).o < 1, 'HIGHLIGHT +100 is clipping the head');
|
||||
assert.ok(ramp(0.93, { wh: 1 }).o >= 1 - 1e-12, 'WHITE +100 is not reaching the ceiling it is named for');
|
||||
exact(ramp(0.99, { bl: 1, sh: 1, hl: 1, wh: 1 }).o, 1, 'a full set of knobs on a bright pixel does not reach white');
|
||||
// DR rides the same four moves, a share of each (0.12 / 0.06 / -0.09 / -0.18),
|
||||
// and the two ends are still fixed points: +0.12 of BLACK lifts nothing at
|
||||
@@ -522,8 +572,8 @@ const filmFlat = filmTone.replace(/['"]/g, '');
|
||||
for (const [name, sh, knot, hlSrc, hl, head] of [
|
||||
['classic-chrome', -0.47, 0.17999192],
|
||||
['classic-vivid', -0.47, 0.17999192],
|
||||
['monochrome', -0.2, 0.22020933, '-0.16', -0.16, 0.7375],
|
||||
['mono-high-contrast', -0.54, 0.16956519, '0.83', 0.83, 0.81484375],
|
||||
['monochrome', -0.2, 0.22020933, '-0.1143', -0.1143, 0.7375],
|
||||
['mono-high-contrast', -0.54, 0.16956519, '0.5929', 0.5929, 0.81484375],
|
||||
]) {
|
||||
assert.ok(
|
||||
filmFlat.includes(`${name}: { sh: ${sh}`),
|
||||
|
||||
@@ -107,20 +107,28 @@ const toneWhiteW = (L) => {
|
||||
const u = clamp01((L - TONE_WHITE_EDGE) / (1 - TONE_WHITE_EDGE));
|
||||
return u * u;
|
||||
};
|
||||
// toneBlack, as the shader emits it: the lift is the doc's own, the crush is the
|
||||
// toe's EXPONENT (monotone for any rate, and the only form that moves a visible
|
||||
// amount inside a window that is 1 only at the floor).
|
||||
const toneBlack = (L, bl) => {
|
||||
const q = toneBlackW(L);
|
||||
if (bl > 0) return L + TONE_BLACK_LIFT * bl * q * (Math.sqrt(L) - L);
|
||||
return L >= TONE_BLACK_EDGE
|
||||
? L
|
||||
: TONE_BLACK_EDGE * (L / TONE_BLACK_EDGE) ** (1 + TONE_BLACK_CRUSH * -bl * q);
|
||||
};
|
||||
// toneCurve, move for move, with the clamp the shader puts after each one.
|
||||
// `afterBlack` is the L the SHADOW move reads.
|
||||
function toneCurve(L, bl, sh, hl, wh) {
|
||||
let q = toneBlackW(L);
|
||||
L = bl > 0
|
||||
? L + TONE_BLACK_LIFT * bl * q * (Math.sqrt(L) - L)
|
||||
: L * Math.max(1 + TONE_BLACK_CRUSH * bl * q, 0);
|
||||
L = toneBlack(L, bl);
|
||||
L = clamp01(L);
|
||||
const afterBlack = L;
|
||||
q = toneShadowW(L);
|
||||
L *= 1 + sh * q * (1 - L) ** 1.8;
|
||||
L = clamp01(L);
|
||||
q = toneHighW(L);
|
||||
L += TONE_HIGH_GAIN * hl * q * Math.max(L - 0.5, 0) ** 1.5 * (1 - L);
|
||||
L += TONE_HIGH_GAIN * hl * q * Math.max(L - 0.5, 0) ** 1.5 * (1 - L) * (1 - L);
|
||||
L = clamp01(L);
|
||||
q = toneWhiteW(L);
|
||||
L += TONE_WHITE_GAIN * wh * q * (1 - L) * L;
|
||||
|
||||
Reference in New Issue
Block a user