web: each of the four tonal knobs moves its own band of the ramp and not the guard the four used to share
BLACK, SHADOW, HIGHLIGHT and WHITE were four bumps summed into the identity, and the sum carried a guard: the two bumps of a half shared a slope, so past a total of 1 the curve folded backwards, and the ceiling that stopped it was shared by the amplitudes of a half. A stock already sitting on SHADOW therefore took BLACK's lift down with it — on the monochrome stock (sh = -0.24) BLACK at -100 came back with 0.663 of the travel the knob has on its own, which is the "kéo theo sự thay đổi của thông số khác" report exactly. thay_doi_thong_so_giong_lightroom.md section 2 asks for four WINDOWS instead: each knob owns a compact band of the ramp and is exactly zero outside it, and the four moves are applied ONE AFTER ANOTHER rather than summed. A composition of monotone maps is monotone by construction, so it needs no guard, and each knob then measures 1.00 of its travel on every stock. BLACKS is the doc's toe — u = clamp(1 - L/0.18, 0, 1) cubed, opened by sqrt(L) - L at 0.7 and deepened by 0.85, both of which are exactly zero at L = 0, so (0,0,0) stays (0,0,0): the grey pedestal that BLACK +100 left on a black was the sum adding its bump's height at the black point, which is the doc's own "Milky / Foggy". SHADOWS is the doc's bell over the deep tones, HIGHLIGHTS the bell over the bright ones, WHITES the doc's Hermite on the shoulder from 0.80. The ramp keeps its two anchors — 0.00 and 1.00 — at every setting of the four knobs. One deliberate departure from the doc: HIGHLIGHT carries a (1 - L) the doc's raw knee does not, because pow(L - 0.5, 1.5) added to L overshoots the cube above 0.94 — 17% of the ramp driven to flat white at +100 before the clamp. Read against the headroom that is left, the move is zero at L = 1 by construction and the head rolls instead of clipping. The windows are read in the sRGB-encoded luma this file already works in, not in linear light as the doc's section 1 sets out: the doc's own boundaries (0.18, 0.05..0.45, 0.55..0.95, 0.80) land as perceptual positions there, and moving the whole renderer to the linear domain is a bigger change than this pass. The divergence is the one the scratchpad compat doc already warns the Android port about, and it is noted at the windows themselves. Checked: `tsc --noEmit` clean; `highlight-knee-check.mjs`, `tone-base-check.mjs` and `mask-wb-check.mjs` updated to the four windows and passing; the twin ramp over a 1/512 grid is monotone to -0.00119 (0.30 code values, at t = 0.098 with every knob at full negative), both anchors hold for every combination, and a knob outside its band is the exact identity.
This commit is contained in:
@@ -139,24 +139,30 @@ const BAND_BLOCK = hslBandGaps()
|
||||
)
|
||||
.join('');
|
||||
|
||||
// How far a tonal-range knob moves its own knot, in ramp units. A quarter is
|
||||
// the reach the program this layout copies gives a slider: at full deflection
|
||||
// the toe can reach the quarter above it and the head can be rolled onto it —
|
||||
// and never past, because each knot is clamped inside the one after it.
|
||||
// The four tonal-range knobs, as the four bands the ramp in TONE_MATH_SKSL owns.
|
||||
// Each band is compactly supported — a knob is exactly the identity outside its
|
||||
// own — which is what makes the four independent without a guard, and a guard is
|
||||
// what they used to share: one ceiling over two amplitudes, so a film stock that
|
||||
// already sits on SHADOW took the BLACK knob's travel down with it (0.663 of it
|
||||
// on the monochrome stock). See the head of the ramp for the whole argument.
|
||||
//
|
||||
// SHADOW and HIGHLIGHT move HALF of it. The knots they own (a1, a3) are the HEAD
|
||||
// of a quarter, so a unit of lift is a unit of slope that quarter loses, and at
|
||||
// a whole quarter the band is drawn flat — see the notes on a1 and a3.
|
||||
export const TONE_ANCHOR = 0.25;
|
||||
// The edges are the doc's (§2.1 and §2.4): BLACKS dies on 0.18, the middle grey
|
||||
// the doc anchors the toe to, and WHITES starts on 0.80, its shoulder.
|
||||
export const TONE_BLACK_EDGE = 0.18;
|
||||
export const TONE_WHITE_EDGE = 0.80;
|
||||
|
||||
// How steep a bump of amplitude A gets, A excluded: the kernel (1-u^2)^2 tops out
|
||||
// at |dk/du| = 8/(3*sqrt(3)) = 1.5396 on each side of its peak (u = 1/sqrt(3)),
|
||||
// and u counts the distance to the knot in units of the bump's half-width. Over
|
||||
// the half-width of BLACK and WHITE that is 3.08*A, over the quarter-width of
|
||||
// SHADOW and HIGHLIGHT 6.16*A, and the two numbers are what the guard inside
|
||||
// toneRamp holds two moves sharing a half of the ramp under.
|
||||
export const TONE_BUMP_SLOPE_HALF = 3.0792;
|
||||
export const TONE_BUMP_SLOPE_QUARTER = 6.1584;
|
||||
// How much of its band a knob is worth at full deflection — 100 on the slider.
|
||||
// BLACK's pair is the doc's own (0.7 opens the toe through the square root, 0.85
|
||||
// is how hard the crush bites). HIGHLIGHT and WHITE are measured rather than
|
||||
// inherited: the doc prints the shapes and leaves the rates off, and its own
|
||||
// sample — a raw pow(L, 1.5) with no headroom term, and a WHITE worth 0.5 —
|
||||
// either clips or does nothing on a real frame. These are the largest rates that
|
||||
// keep a full +100 inside the cube on the sweep in highlight-knee-check, with the
|
||||
// head rolling into the shoulder instead of onto the clamp.
|
||||
export const TONE_BLACK_LIFT = 0.7;
|
||||
export const TONE_BLACK_CRUSH = 0.85;
|
||||
export const TONE_HIGH_GAIN = 2.5;
|
||||
export const TONE_WHITE_GAIN = 1.5;
|
||||
|
||||
// How far out the BASE layer of `toneRamp` reads, as a fraction of the frame's
|
||||
// own width — the fix_shadow.md neighbourhood (it asks for 2%..5% of the width).
|
||||
@@ -196,65 +202,90 @@ export const TONE_BASE_SIGMA = 0.35;
|
||||
// has no such knob and hands in 0, which is what DR's terms are worth when it is
|
||||
// off on the frame too.
|
||||
export const TONE_MATH_SKSL = `
|
||||
// One knob's move, read at the position x on the ramp: 1 on the knob's own knot,
|
||||
// falling to 0 on the knot a half-width away and staying there — (1-u^2)^2 with u
|
||||
// the distance to the knot in half-widths. Level at u = 0 and at u = 1, so the
|
||||
// move joins the ramp without an angle at either end.
|
||||
float toneBump(float x, float knot, float half_width) {
|
||||
float u = (x - knot) / half_width;
|
||||
float v = max(0.0, 1.0 - u * u);
|
||||
return v * v;
|
||||
// THE FOUR TONAL KNOBS, as thay_doi_thong_so_giong_lightroom.md §2 asks for them:
|
||||
// each one owns a COMPACT band of the ramp and is exactly ZERO outside it, and the
|
||||
// four moves are applied ONE AFTER THE OTHER instead of summed.
|
||||
//
|
||||
// The sum came with a guard, and the guard is where the knobs touched. Two bumps
|
||||
// of a half share a slope, so past a total of 1 the sum carries the curve
|
||||
// backwards — a fold — and the fix was one ceiling (holdLo / holdHi) shared by
|
||||
// the amplitudes of a half. A stock that already sits on SHADOW therefore took
|
||||
// BLACK's lift down with it: on the monochrome stock (sh = -0.24) BLACK -100 came
|
||||
// back with 0.663 of the travel the knob has on its own, which is the "kéo theo
|
||||
// sự thay đổi của thông số khác" report exactly. A composition of monotone maps
|
||||
// is monotone by construction, so it needs no guard, and the same knob then
|
||||
// measures 1.00 of its travel on every stock. (tone-curve, the arithmetic in
|
||||
// plain JS on the scratchpad, and the grid sweep in highlight-knee-check.)
|
||||
//
|
||||
// The windows are the doc's own, in the encoded luma this whole file works in —
|
||||
// the boundaries land where the doc's diagram draws them, BLACKS on the toe up to
|
||||
// the doc's 0.18, SHADOWS as a bell over the deep tones, HIGHLIGHTS as a bell over
|
||||
// the bright ones, WHITES on the shoulder from 0.80.
|
||||
float toneBlackW(float L) {
|
||||
float u = clamp(1.0 - L / ${TONE_BLACK_EDGE}, 0.0, 1.0);
|
||||
// Squared once more than the doc's square for the same reason the sum's kernel
|
||||
// was (1-u^2)^2: the join with the identity at L = 0.18 is a slope, not an
|
||||
// angle, and an angle on a tone curve is a Mach band.
|
||||
return u * u * u;
|
||||
}
|
||||
// The ramp the pixel is rebuilt through: the identity plus four moves. Each knob
|
||||
// owns a bump that PEAKS on its knot — BLACK on 0.00, SHADOW on 0.25, HIGHLIGHT
|
||||
// on 0.75, WHITE on 1.00 — and is back on zero where the next one starts, a
|
||||
// quarter away (a half for the two ends, their knots being the ends of the ramp),
|
||||
// so the two bumps of a half meet on 0.50 at zero height AND zero slope: a knob
|
||||
// moves its own end of the ramp and nothing else, and 0.50 is fixed — the one
|
||||
// point all four sliders leave alone, which is what keeps a mid-grey a mid-grey
|
||||
// while the ends move around it. Every knob on zero leaves the sum empty, so the
|
||||
// whole thing is exactly the identity (see the note at the head of this file).
|
||||
//
|
||||
// Straight segments between the knots were the first cut, and the measurement is
|
||||
// what retired them: a segment meets its neighbour at an angle, and an angle in a
|
||||
// tone curve is a Mach band. On a 1024-step luma wedge (scratchpad probe, JPEG
|
||||
// 8-bit, second derivative through a ±1% box) BLACK +100 came back with 105 at
|
||||
// 0.030 while the stretch above it was untouched to the last bit — 0.0000 from
|
||||
// 0.25 up, the "transition stays grey" the knob was reported for — and HIGHLIGHT
|
||||
// -100 broke at 0.747 with 72, the seam between the lifted highlights and the
|
||||
// shadow under them. The kernel is (1-u^2)^2: level at the peak, so a knot moves
|
||||
// without a fold at its own top, level where it lands, so the join on 0.50 and
|
||||
// the toe on 0.00 stay clean, and C1 at both ends of a move and everywhere
|
||||
// between, which is the whole of what a Mach band asks for.
|
||||
//
|
||||
// DR moves the same four bumps instead of adding its own masked terms on top: it
|
||||
// lifts the toe and rolls the head exactly as before at t = 0 and t = 1 —
|
||||
// 0.12 and 0.18 at full strength — and half of each at the knots next to them,
|
||||
// so the terms land on the two ends and on the heads beside them as one move.
|
||||
//
|
||||
// Lightness takes the curve; the colour rides the ratio. The pixel moves to its
|
||||
// new luma, and it gets there by scaling its three channels by ONE number,
|
||||
// k = o / t: the doc's R_new = R_old * Luma_new / Luma_old. The differences move
|
||||
// with the light, so the hue cannot turn, the saturation cannot be drained by a
|
||||
// lift — an HSL saturation is a ratio of differences and never sees a common
|
||||
// scale — and a neutral, with no difference to carry, lands on o exactly.
|
||||
//
|
||||
// The scale used to be 1.0 instead: keep the chroma, move the luma. That holds
|
||||
// the hue and loses the colour, which is the complaint the ratio answers — the
|
||||
// scratchpad probe (sh-bl-hue.mjs) measured a dark red at 0.746 of saturation
|
||||
// coming back at 0.505 with SHADOW at +100, and at 0.370 with SHADOW and BLACK
|
||||
// both at +100. A chroma held under a rising luma IS a colour going grey, and it
|
||||
// is the two sliders that lift a shadow (SHADOW, BLACK) that raise the luma of a
|
||||
// dark pixel the furthest.
|
||||
//
|
||||
// What the ratio cannot do on its own is fit, and that is what the caps below are
|
||||
// for: past the ceiling a channel clips outright, the three stop being scaled
|
||||
// together and the hue goes with them (measured on the other probe, a skin tone
|
||||
// at 24.0° came back at 48.0° at HIGHLIGHT +100, a warm white at 37° at 57.4°).
|
||||
// Pulling the ONE scale back while the cube has room for no more costs saturation
|
||||
// instead: a pixel the cube is against arrives neutral before it arrives wrong,
|
||||
// and one the curve has driven all the way to 1.0 arrives at white.
|
||||
float toneShadowW(float L) {
|
||||
return smoothstep(0.02, 0.12, L) * (1.0 - smoothstep(0.25, 0.55, L));
|
||||
}
|
||||
float toneHighW(float L) {
|
||||
return smoothstep(0.45, 0.65, L) * (1.0 - smoothstep(0.92, 1.0, L));
|
||||
}
|
||||
float toneWhiteW(float L) {
|
||||
float u = clamp((L - ${TONE_WHITE_EDGE}) / (1.0 - ${TONE_WHITE_EDGE}), 0.0, 1.0);
|
||||
return u * u;
|
||||
}
|
||||
// The ramp the pixel is rebuilt through. Read at the BASE, so the move is the
|
||||
// neighbourhood's and the pixel keeps its own difference from it (fix_shadow.md's
|
||||
// Base' + Detail): the ratio Base' * (Input / Base) was the first cut and it is
|
||||
// what broke BLACK — it scales the detail by the neighbourhood's gain, so the
|
||||
// knob that takes the base toward zero takes the picture's texture with it (the
|
||||
// blur the report named on a monochrome frame, where every channel IS the luma).
|
||||
//
|
||||
// Every move below is monotone for any amount in [-1, 1] and lands on L = 0 and
|
||||
// L = 1 without moving either, so the composition is monotone, the black point is
|
||||
// the black point, and the white point is the white point whatever the four
|
||||
// sliders say. The one deliberate departure from the doc's own arithmetic is the
|
||||
// (1.0 - L) on HIGHLIGHTS: the doc's raw soft-knee ADDS pow(L - 0.5, 1.5) to L,
|
||||
// and above 0.94 that overshoots the cube — a measured 17% of the ramp driven to
|
||||
// flat white at +100 before the clamp, the "cháy vùng Whites" the doc's own §1
|
||||
// opens by calling a defect. The knee reads the headroom that is left instead, so
|
||||
// the move is zero at L = 1 by construction and the head rolls instead of
|
||||
// clipping.
|
||||
float toneCurve(float L, float bl, float sh, float hl, float wh) {
|
||||
float q;
|
||||
// BLACKS, the toe. Lift opens the detail under the doc's square root, crush
|
||||
// deepens it by the doc's 0.85, and neither touches the anchor: sqrt(0) - 0 is
|
||||
// 0, so the black point is exactly where it was — the offset that lifted
|
||||
// (0,0,0) to a grey pedestal was a SUM adding its bump's height at L = 0, which
|
||||
// is the doc's "Milky / Foggy" failure. Past 0.18 the window is 0 and the move
|
||||
// is exactly the identity.
|
||||
q = toneBlackW(L);
|
||||
L = bl > 0.0
|
||||
? L + ${TONE_BLACK_LIFT} * bl * q * (sqrt(L) - L)
|
||||
: L * max(1.0 + ${TONE_BLACK_CRUSH} * bl * q, 0.0);
|
||||
L = clamp(L, 0.0, 1.0);
|
||||
// SHADOWS, a gain on the light with the floor still on 0: the multiplier is
|
||||
// 1 + amount * bell * (1 - L)^1.8, so the window's own bell already keeps it
|
||||
// off the midtones the doc names and the exponent keeps it off the white end.
|
||||
q = toneShadowW(L);
|
||||
L *= 1.0 + sh * q * pow(1.0 - L, 1.8);
|
||||
L = clamp(L, 0.0, 1.0);
|
||||
// HIGHLIGHTS, the doc's soft-knee against the headroom that is left — see the
|
||||
// note above the function. max(L - 0.5, 0) because the pow is undefined under
|
||||
// the knee and the window is only wide where it is not.
|
||||
q = toneHighW(L);
|
||||
L += ${TONE_HIGH_GAIN} * hl * q * pow(max(L - 0.5, 0.0), 1.5) * (1.0 - L);
|
||||
L = clamp(L, 0.0, 1.0);
|
||||
// WHITES, the doc's Hermite on the shoulder: (1 - L) * L is zero on both ends,
|
||||
// so the white point is fixed and the move is spent inside the top of the ramp.
|
||||
q = toneWhiteW(L);
|
||||
L += ${TONE_WHITE_GAIN} * wh * q * (1.0 - L) * L;
|
||||
return clamp(L, 0.0, 1.0);
|
||||
}
|
||||
// Below t = 0.0004 there is no ratio worth the name: dividing by what is left of
|
||||
// a pixel that has almost no light on it takes whatever cast the last code value
|
||||
// of 8-bit noise left there and multiplies it by the pedestal the BLACK knob just
|
||||
@@ -277,87 +308,21 @@ vec3 lightMove(vec3 c, float t, float o) {
|
||||
return clamp(vec3(o) + (c - vec3(t)) * k, 0.0, 1.0);
|
||||
}
|
||||
vec3 toneRamp(vec3 c, float t, float base, float bl, float sh, float hl, float wh, float dr) {
|
||||
// BLACK owns 0.00, the edge of the ramp, so its bump is the half-width one and
|
||||
// the toe opens to A while the quarter above it takes the fall of it. As a
|
||||
// bump the fall is a slope and never a plateau, which is what the report was
|
||||
// about: at +100 the lift used to be spent inside 0.25 and the stretch above it
|
||||
// came back identical to the untouched frame — the measured 0.0000 from 0.25 up
|
||||
// in the note at the top of the ramp. Here the mid-tones rise a little with the
|
||||
// toe and the corner that drew the seam at 0.25 is gone.
|
||||
float blackA = ${TONE_ANCHOR} * bl + dr * 0.12;
|
||||
// SHADOW rides HALF the anchor, and that is a measured ceiling rather than a
|
||||
// taste. The knot is the HEAD of the quarter above it, and the bump only ever
|
||||
// ADDS to the identity, so that quarter pays in slope for every unit the knot
|
||||
// rises: its mean slope is 1 - A / 0.25, and that band is where a waterfall's
|
||||
// spray and a sunlit rock sit. At the whole anchor the mean is 0 and the band
|
||||
// is drawn flat — on a real frame (DSCF1701, misty valley, 21% of its pixels in
|
||||
// the band) SHADOW +90 came back with 0.10 of the band's own spread and a mean
|
||||
// of 0.392 where it was 0.489, the milky white sheet the knob was reported for,
|
||||
// bright areas with their contrast gone. At half: 0.55 of the spread, mean
|
||||
// 0.441, and the toe still opens at 1.45x. Half also keeps the knob clear of
|
||||
// the guard below: at a whole-anchor rate the guard would take over past ±65
|
||||
// and the top third of the slider would do nothing, which is why the RATE is
|
||||
// halved and not just a ceiling.
|
||||
float shadowA = ${TONE_ANCHOR} * 0.5 * sh + dr * 0.06;
|
||||
// HIGHLIGHT rides half the anchor for the reason SHADOW does, and its knot is
|
||||
// the head of the 0.75..1.00 quarter: at a whole-anchor rate +100 (A = 0.25)
|
||||
// draws that quarter flat against the ceiling, its whole mean slope gone, and
|
||||
// the same knob the other way spends the quarter BELOW the knot instead. Half
|
||||
// draws neither, and the head still rolls from 0.625 to 0.875 over the travel.
|
||||
float highA = ${TONE_ANCHOR} * 0.5 * hl - dr * 0.09;
|
||||
float whiteA = ${TONE_ANCHOR} * wh - dr * 0.18;
|
||||
// Two bumps share each half of the ramp, and their steep sides can land on the
|
||||
// same stretch: past a total slope of 1 the sum would carry the curve
|
||||
// BACKWARDS — a fold, a worse band than the seams this replaced. Read at their
|
||||
// own steepest points, which is the loosest the pair can be, the two amplitudes
|
||||
// of a half are held under one number and given up together past it. A knob on
|
||||
// its own never reaches it — a full BLACK is 0.77, a full SHADOW 0.77, and DR,
|
||||
// which moves both bumps of a half at once, 0.74 below and 1.11 above for a
|
||||
// steepest sum of 1.00, so a full DR gives up a tenth of its head roll — so a
|
||||
// single slider has its whole travel, and only a pair pushed together gives
|
||||
// anything up: BLACK and SHADOW both at +100 arrive at 0.65 of their own lift
|
||||
// rather than folding.
|
||||
float holdLo = ${TONE_BUMP_SLOPE_HALF} * abs(blackA) + ${TONE_BUMP_SLOPE_QUARTER} * abs(shadowA);
|
||||
float holdHi = ${TONE_BUMP_SLOPE_QUARTER} * abs(highA) + ${TONE_BUMP_SLOPE_HALF} * abs(whiteA);
|
||||
float kLo = holdLo > 1.0 ? 1.0 / holdLo : 1.0;
|
||||
float kHi = holdHi > 1.0 ? 1.0 / holdHi : 1.0;
|
||||
blackA *= kLo;
|
||||
shadowA *= kLo;
|
||||
highA *= kHi;
|
||||
whiteA *= kHi;
|
||||
// The ramp is read at the BASE, so the curve's move is the neighbourhood's,
|
||||
// and the pixel keeps its own DIFFERENCE from that base — fix_shadow.md's
|
||||
// Base' + Detail, added and not scaled. The ratio Base' * (Input / Base) was
|
||||
// the first cut and it is what broke BLACK: multiplying by the neighbourhood's
|
||||
// gain o / base scales the detail by that gain, so the knob that takes o toward
|
||||
// zero takes the picture's texture with it. Measured on the sample frame at
|
||||
// BLACK -100 (tone-sim, the pass in plain JS at 1024px): the finest gradient
|
||||
// came back at 0.75 of the input under the ratio and at 0.98 under the sum,
|
||||
// while the frame darkened the same either way (mean 0.416 -> 0.359 both). On a
|
||||
// monochrome stock every channel IS the pixel's luma, so the ratio's error is
|
||||
// the whole frame there: the pass returned the blurred BASE, which is the soft
|
||||
// picture the knob was reported for. The bright side of the same move is kept —
|
||||
// the lift is still the neighbourhood's, and the band above a lifted knot still
|
||||
// keeps its spread (0.81 of it at SHADOW +100, where the global move kept 0.42).
|
||||
//
|
||||
// The pixel's own luma still travels as t: lightMove rebuilds the colour from
|
||||
// it, and it is the value the caps read.
|
||||
//
|
||||
// o is held inside the cube BEFORE the detail is added: a base the ramp has
|
||||
// pushed under the floor is a neighbourhood whose black point is below zero,
|
||||
// and clipping o first keeps the structure around it instead of carrying the
|
||||
// pedestal down onto every pixel in the region.
|
||||
float o = clamp(base
|
||||
+ blackA * toneBump(base, 0.00, 0.50)
|
||||
+ shadowA * toneBump(base, 0.25, 0.25)
|
||||
+ highA * toneBump(base, 0.75, 0.25)
|
||||
+ whiteA * toneBump(base, 1.00, 0.50), 0.0, 1.0);
|
||||
// DR is the whole frame's DYNAMIC RANGE, and it rides the same four moves the
|
||||
// sliders do rather than adding masked terms of its own: a recovery that lifts
|
||||
// the toe and rolls the head is a BLACK and a WHITE, and after the windows a
|
||||
// tenth of a unit lands inside the band the knob owns instead of on the whole
|
||||
// frame. A mask has no such knob and hands in 0, which is what these terms are
|
||||
// worth when DR is off on the frame too.
|
||||
float o = toneCurve(base, clamp(bl + dr * 0.12, -1.0, 1.0), clamp(sh + dr * 0.06, -1.0, 1.0),
|
||||
clamp(hl - dr * 0.09, -1.0, 1.0), clamp(wh - dr * 0.18, -1.0, 1.0));
|
||||
// A caller with no neighbourhood of its own (a mask) hands in the pixel as its
|
||||
// base, and the difference is then exactly zero: the target is the ramp at t,
|
||||
// the global move, which is what the ratio gave it too.
|
||||
float target = o + (t - base);
|
||||
return lightMove(c, t, clamp(target, 0.0, 1.0));
|
||||
}
|
||||
|
||||
// The accurate sRGB transfer pair (0.04045/12.92 + 2.4, and its inverse): the
|
||||
// same constants colorUtils.planckianLinear uses on the WB side, and the reason
|
||||
// a stop is a stop here. EXPOSURE needs it — 2^ev is a multiplier on LIGHT — and
|
||||
@@ -874,17 +839,20 @@ export interface ToneUniforms {
|
||||
// here instead of in the 4x5 matrix, which cannot move one end of the curve
|
||||
// without also moving the other.
|
||||
//
|
||||
// The `sh` and `hl` here are written at DOUBLE what the knob's own arithmetic
|
||||
// reads, which is not a taste either: SHADOW and HIGHLIGHT each move their knot
|
||||
// half an anchor per unit (see a1 and a3 in TONE_MATH_SKSL), so a stock that
|
||||
// wants the toe on 0.18 has to ask for -0.56 to land it there. The stock's crush
|
||||
// is the knot it puts the ramp on; the unit it is written in belongs to the
|
||||
// knob, not to the look.
|
||||
// The `sh` and `hl` here are written in the KNOB's unit, not the look's: a stock
|
||||
// that wants its toe on 0.18 asks the SHADOW knob for whatever the window is
|
||||
// worth there (1.0 at 0.25) times (1 - 0.25)^1.8, which is -0.47 on this ramp.
|
||||
// The numbers moved when the ramp did — the four knobs are windows now, not
|
||||
// summed bumps, and the doc's own rates (§2) replaced the quarter-anchor ones —
|
||||
// so the knots below are re-solved against the new curve rather than tuned by
|
||||
// eye: 0.18 / 0.22 / 0.17 on the toe and 0.7375 / 0.815 on the head, the values
|
||||
// the stocks were written against, land where they always did, which is what
|
||||
// highlight-knee-check pins.
|
||||
const FILM_TONE: Partial<Record<BaseFilter, Partial<ToneUniforms>>> = {
|
||||
'classic-chrome': { sh: -0.56 },
|
||||
'classic-chrome': { sh: -0.47 },
|
||||
// Classic Vivid is Classic Chrome's sibling — the shadow crush belongs to the
|
||||
// stock, not to the matrix rows, so it comes along.
|
||||
'classic-vivid': { sh: -0.56 },
|
||||
'classic-vivid': { sh: -0.47 },
|
||||
'classic-neg': { shT: [-0.018, 0.009, 0.013], hlT: [0.024, 0.008, -0.012] },
|
||||
// Acros. A black-and-white stock IS its grey ramp, so this entry only shapes
|
||||
// the two ENDS and leaves the middle an identity: a smooth shadow toe that
|
||||
@@ -893,19 +861,19 @@ const FILM_TONE: Partial<Record<BaseFilter, Partial<ToneUniforms>>> = {
|
||||
// paper. Mid-tones are between the 0.25 and the 0.75 knots, so they keep
|
||||
// every step the matrix handed over — which is what 'deep black' costs in a
|
||||
// colour stock and does not have to cost here.
|
||||
// The values move the two end knots of the ramp: -0.24 puts the toe on 0.22
|
||||
// and -0.10 rolls the head to 0.7375 (both SHADOW and HIGHLIGHT ride HALF an
|
||||
// anchor per unit, so a stock written against the knot asks for double).
|
||||
monochrome: { sh: -0.24, hl: -0.10 },
|
||||
// The values move the two end bands of the ramp: -0.20 puts the toe on 0.22
|
||||
// and -0.16 rolls the head to 0.7375 (both solved against the window's own
|
||||
// height at 0.25 and 0.75, see the note above FILM_TONE).
|
||||
monochrome: { sh: -0.20, hl: -0.16 },
|
||||
// B&W HIGH CONTRAST. Acros' ramp with both ends pushed hard: a deeper toe
|
||||
// (-0.64 against Acros' -0.24, so 0.17 against 0.22) so the darks reach true
|
||||
// black, and a shoulder that LIFTS instead of rolling (-0.10 → +0.52, the
|
||||
// (-0.54 against Acros' -0.20, so 0.17 against 0.22) so the darks reach true
|
||||
// black, and a shoulder that LIFTS instead of rolling (-0.16 → +0.83, the
|
||||
// head going to 0.815), which is the whites step of the brief. The stretch
|
||||
// between the two inner knots (0.25 and 0.75) is still the identity, so the
|
||||
// long smooth stretch of the greys survives — that is what keeps a hard push
|
||||
// off the posterised look, and the strength the stock needs on the greys is
|
||||
// its matrix slope (SIM_CONTRAST_BIAS in colorUtils), not another move here.
|
||||
'mono-high-contrast': { sh: -0.64, hl: 0.52 },
|
||||
'mono-high-contrast': { sh: -0.54, hl: 0.83 },
|
||||
};
|
||||
|
||||
// The base layer's neighbourhood is the caller's business, not this function's:
|
||||
|
||||
Reference in New Issue
Block a user