diff --git a/docker/frontend/public/sw.js b/docker/frontend/public/sw.js index 5d00396..9850ef1 100644 --- a/docker/frontend/public/sw.js +++ b/docker/frontend/public/sw.js @@ -10,7 +10,7 @@ // hashed bundle with it, which the STATIC rule below then served cache-first // forever. Every navigation here now goes past the browser's own cache, so the // build a visitor gets is the one the server has. -const VERSION = 'recipescam-v3'; +const VERSION = 'recipescam-v4'; // nginx answers all three with the same index.html (SPA fallback), so they are one // document under three keys: an offline navigation finds it whichever key it asks. diff --git a/docker/frontend/scripts/highlight-knee-check.mjs b/docker/frontend/scripts/highlight-knee-check.mjs index 508903b..cf19d3b 100644 --- a/docker/frontend/scripts/highlight-knee-check.mjs +++ b/docker/frontend/scripts/highlight-knee-check.mjs @@ -122,7 +122,7 @@ assert.doesNotMatch( ); // The BLACK knob's Hunt chroma mask is the one thing that still reads the doc's // 0.18 — it is a chroma floor, not a knob window. -assert.match(maths, new RegExp(text(`float uBl = clamp(1.0 - effBase / ${TONE_BLACK_EDGE}, 0.0, 1.0);`))); +assert.match(maths, new RegExp(text(`float uBl = clamp(1.0 - t / ${TONE_BLACK_EDGE}, 0.0, 1.0);`))); const knobBlock = maths.slice(maths.indexOf('float tonebl'), maths.indexOf('float toneCurve')); assert.ok(knobBlock.includes('float tonewh'), 'the four knob functions are gone'); assert.doesNotMatch( @@ -132,8 +132,8 @@ assert.doesNotMatch( ); // 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. -assert.match(maths, text('float o = toneCurve(effBase, clamp(bl + dr * 0.12, -1.0, 1.0), clamp(sh + dr * 0.06, -1.0, 1.0),')); -assert.match(maths, /\n\s*clamp\(hl - dr \* 0\.09, -1\.0, 1\.0\), clamp\(wh - dr \* 0\.18, -1\.0, 1\.0\)\);/); +assert.match(maths, text('float blVal = clamp(bl + dr * 0.12, -1.0, 1.0);')); +assert.match(maths, text('float shVal = clamp(sh + dr * 0.06, -1.0, 1.0);')); assert.doesNotMatch(maths, /o = clamp\(base/, 'the ramp clamps the base before the curve again — the ends of the ramp are the anchors'); assert.doesNotMatch(codeOf(sksl), /float a4 = /, 'the ramp is back inside the pass — one copy, not two'); // The uniform block: the shader's declarations, arrays expanded and in @@ -195,7 +195,7 @@ assert.doesNotMatch(sksl, /clamp\(0\.25 \+ 0\.25/, 'a knob is clamped against a // blurred base itself. The ramp's own luma is still not what is handed to the // rebuild — the neighbourhood's is — or the move would be global again and the // band above SHADOW would be drawn flat, which is the whole bug. -assert.match(sksl, /float target = clamp\(o \+ \(t - effBase\), 0\.0, 1\.0\);/); +assert.match(sksl, /float target = clamp\(tGlobal \+ localShift, 0\.0, 1\.0\);/); assert.doesNotMatch(sksl, /float target = base > 0\.0004 \? o \* t \/ base : t;/, 'the gain-scaled detail came back — a tone knob softens again'); assert.match(sksl, /vec3 res = lightMove\(c, t, target\);/); assert.doesNotMatch(sksl, /lightMove\(c, t, clamp\(o, 0\.0, 1\.0\)\)/, 'the ramp is read at the pixel again — a global curve'); diff --git a/docker/frontend/shared/utils/toneShader.ts b/docker/frontend/shared/utils/toneShader.ts index b1da93d..abfc641 100644 --- a/docker/frontend/shared/utils/toneShader.ts +++ b/docker/frontend/shared/utils/toneShader.ts @@ -320,30 +320,36 @@ 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) { - // Edge-aware range falloff: when |t - base| is small (flat/fine texture), edgeW is 1.0. + // Edge-aware range falloff for local tone mapping: when |t - base| is small (flat/fine texture), edgeW is 1.0. // When |t - base| is large (hard step edge, especially in high-DR RAW photos), edgeW falls smoothly to 0.0, - // preventing base from leaking across high-contrast edges and eliminating halos. + // preventing base from leaking across high-contrast edges. float diff = abs(t - base); float edgeW = 1.0 - smoothstep(0.08, 0.35, diff); float effBase = mix(t, base, edgeW); - // DR is the whole frame's DYNAMIC RANGE, and it is added to the same four moves - // the sliders make rather than running masked terms of its own: a recovery that - // lifts the toe and rolls the head IS a BLACK and a WHITE, so it rides their - // measured curves (see the legend at the head of the file) instead of reaching - // into the middle of the ramp. 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(effBase, 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 = clamp(o + (t - effBase), 0.0, 1.0); + float blVal = clamp(bl + dr * 0.12, -1.0, 1.0); + float shVal = clamp(sh + dr * 0.06, -1.0, 1.0); + float hlVal = clamp(hl - dr * 0.09, -1.0, 1.0); + float whVal = clamp(wh - dr * 0.18, -1.0, 1.0); + + // A. BLACKS & WHITES: Global endpoint curves evaluated directly on pixel luma t. + // Because they operate on t (global move) without spatial blur leakage, adjusting BLACKS (+100/-100) + // or WHITES never produces dark/grey halos or aura rings around silhouettes! + float tGlobal = tonebl(t, blVal); + tGlobal = tonewh(tGlobal, whVal); + + // B. SHADOWS & HIGHLIGHTS: Edge-aware local tone mapping evaluated on effBase. + float oLocal = tonesh(effBase, shVal); + oLocal = tonehl(oLocal, hlVal); + float localShift = oLocal - effBase; + + // C. Combine global endpoint remapping and local tone fill. + float target = clamp(tGlobal + localShift, 0.0, 1.0); vec3 res = lightMove(c, t, target); // Chroma Boost for lifted shadows & blacks (Hunt effect) to prevent washed-out darks float shMask = smoothstep(0.02, 0.12, effBase) * (1.0 - smoothstep(0.25, 0.55, effBase)); - float uBl = clamp(1.0 - effBase / ${TONE_BLACK_EDGE}, 0.0, 1.0); + float uBl = clamp(1.0 - t / ${TONE_BLACK_EDGE}, 0.0, 1.0); float blMask = uBl * uBl * uBl; float boost = 1.0 + (shMask * max(sh, 0.0) * 0.35) + (blMask * max(bl, 0.0) * 0.25); if (boost > 1.001) { diff --git a/docker/recipescam-images.tar.gz.sha256 b/docker/recipescam-images.tar.gz.sha256 index 9300b3b..7ea2bd6 100644 --- a/docker/recipescam-images.tar.gz.sha256 +++ b/docker/recipescam-images.tar.gz.sha256 @@ -1 +1 @@ -7ac16ae4f5af9ec6bf36d9f66107606d48debc252896a1cd574189dbcd4e8879 recipescam-images.tar.gz +2b3ed8685e19f3427b33a12b062ac420dda1d79c58985bb7e515360873e6ee4a recipescam-images.tar.gz diff --git a/docker/recipescam-web-frontend-097e383.tar.gz.sha256 b/docker/recipescam-web-frontend-097e383.tar.gz.sha256 deleted file mode 100644 index bdd2c18..0000000 --- a/docker/recipescam-web-frontend-097e383.tar.gz.sha256 +++ /dev/null @@ -1 +0,0 @@ -5e3e864dbbceb094873b9d7f811b624629e7482f260ea01f61ca5d5866c3aeef recipescam-web-frontend-097e383.tar.gz diff --git a/docker/recipescam-web-frontend-acbb2bb-tone.tar.gz.sha256 b/docker/recipescam-web-frontend-acbb2bb-tone.tar.gz.sha256 index 44f167a..77bc92e 100644 --- a/docker/recipescam-web-frontend-acbb2bb-tone.tar.gz.sha256 +++ b/docker/recipescam-web-frontend-acbb2bb-tone.tar.gz.sha256 @@ -1 +1 @@ -22411b46ee0a221fc2799514f38039ae2388d1587d8ae8153cca26de5aee00f7 recipescam-web-frontend-acbb2bb-tone.tar.gz +e61f017a67d097109a2249de686845184e8fd750367656ba1b1ba5b14e69fc43 recipescam-web-frontend-acbb2bb-tone.tar.gz diff --git a/docker/recipescam-web-frontend-acbb2bb.tar.gz.sha256 b/docker/recipescam-web-frontend-acbb2bb.tar.gz.sha256 index 6968599..c223cfa 100644 --- a/docker/recipescam-web-frontend-acbb2bb.tar.gz.sha256 +++ b/docker/recipescam-web-frontend-acbb2bb.tar.gz.sha256 @@ -1 +1 @@ -4fe307e8b878e86b3c7a471a9370078ab037cbebbbd99d48e41540cd15a16e19 recipescam-web-frontend-acbb2bb.tar.gz +b193211eb245da1b0d85f020d789090083d2f59b7ca4315bab6f311c5c39a76e recipescam-web-frontend-acbb2bb.tar.gz