diff --git a/docker/frontend/src/App.tsx b/docker/frontend/src/App.tsx index c8ee643..e4cfbf4 100644 --- a/docker/frontend/src/App.tsx +++ b/docker/frontend/src/App.tsx @@ -1949,6 +1949,25 @@ export function Workspace() { return () => window.removeEventListener('pointerdown', dismiss, true); }, [openParam]); + // The mask's column belongs to the armed shape the same way: reaching for another + // chip or tab puts the tool down and takes its column off the stage in one gesture. + // The shape's own chips are left out — toggling one is its own onClick's business — + // and anything inside the column (or on the photo, where the shape is drawn) stays. + useEffect(() => { + if (!maskTool) return; + const dismiss = (e: PointerEvent) => { + const el = e.target as HTMLElement | null; + if ( + !el?.closest('.rail, .chip') || + el.closest('[data-col="mask"], .chip[data-key="linear"], .chip[data-key="radial"]') + ) + return; + setMaskTool(null); + }; + window.addEventListener('pointerdown', dismiss, true); + return () => window.removeEventListener('pointerdown', dismiss, true); + }, [maskTool]); + // Continuous parameter chips: label + live value; amber text when edited, // amber border while its ruler is open. const paramChips = (defs: ContinuousParamDef[]): ChipDef[] => diff --git a/docker/frontend/src/engine/rawDevelop.ts b/docker/frontend/src/engine/rawDevelop.ts index 38f2745..d7a1288 100644 --- a/docker/frontend/src/engine/rawDevelop.ts +++ b/docker/frontend/src/engine/rawDevelop.ts @@ -56,8 +56,22 @@ float3 encode(float3 x) { half4 main(float2 pos) { float4 p = raw.eval(float2(pos.x, pos.y - crop.x)); - float3 lin = clamp((p.rgb - black.rgb) * black.a, 0.0, 1.0) * mul.rgb; + // Only the floor here. A photo the sensor could not hold goes over the white + // level in all three channels, and clipping them one by one before the WB gains + // is what tints what is left of the highlight: green — the channel the gains are + // normalised to — stops at 1.0 while red and blue, which need their 2.6x and + // 1.6x, are already past it, so the blown area comes out magenta. Keep the + // channel ratios through the matrix instead and let the overflow fade to white. + float3 lin = max((p.rgb - black.rgb) * black.a, 0.0) * mul.rgb; float3 rgb = float3(dot(m0.xyz, lin), dot(m1.xyz, lin), dot(m2.xyz, lin)); + float mx = max(max(rgb.r, rgb.g), rgb.b); + // The overflow fades towards white instead of being cut, so the trace of a + // sensor that ran past its white level survives as a compressed ramp — which is + // what LIGHT's HIGHLIGHT row then has to pull on. ponytail: the ramp lives in + // [0.5, 1] and the band leaves as 8-bit JPEG, so the two stops above white are + // compressed, not kept; give develop a 16-bit output (or a knee of its own) when + // RAW highlights have to be recovered rather than merely look right. + rgb = mx > 1.0 ? mix(rgb / mx, float3(1.0), 1.0 - 1.0 / mx) : rgb; return half4(half3(encode(rgb)), 1.0); } `;