diff --git a/docker/frontend/src/App.tsx b/docker/frontend/src/App.tsx index 89928f3..d416c6c 100644 --- a/docker/frontend/src/App.tsx +++ b/docker/frontend/src/App.tsx @@ -33,7 +33,7 @@ import { type HslBandId, type Recipe, } from '../shared/types'; -import { HSL_BANDS, isMonochromeBase, nearestHslBand, rgbToHsl } from '../shared/utils/colorUtils'; +import { HSL_BANDS, isMonochromeBase, kelvinToRGB, nearestHslBand, rgbToHsl } from '../shared/utils/colorUtils'; import type { MsgKey } from './i18n/vi'; // Mirrors the API's MAX_PHOTOS_PER_USER: shown on SAVE PHOTO, enforced there. @@ -117,15 +117,17 @@ const TEMP_PRESETS: { label: string; kelvin: number }[] = [ { label: 'FLUOR', kelvin: 4000 }, ]; -// Kelvin -> sRGB (Tanner Helland's approximation, what every WB widget uses): -// 2500K reads warm orange, 5500K a near-neutral white, 10000K blue. -function kelvinToHex(k: number): string { - const t = Math.min(40000, Math.max(1000, k)) / 100; - const chan = (v: number) => Math.round(Math.min(255, Math.max(0, v))).toString(16).padStart(2, '0'); - const r = t <= 66 ? 255 : 329.698727446 * (t - 60) ** -0.1332047592; - const g = t <= 66 ? 99.4708025861 * Math.log(t) - 161.1195681661 : 288.1221695283 * (t - 60) ** -0.0755148492; - const b = t >= 66 ? 255 : t <= 19 ? 0 : 138.5177312231 * Math.log(t - 10) - 305.0447927307; - return `#${chan(r)}${chan(g)}${chan(b)}`; +// Kelvin -> the CAST the ruler prints. Not the colour of a blackbody at that K: +// the number names the light the scene was shot under, so the engine compensates +// for it and WARMS the picture as K rises (kelvinToRGB). A swatch painted with +// the light's own colour would therefore run the other way from the picture — +// orange under a knob that blue-d the frame. Measuring the ruler means painting +// what the render does: the engine's own gains on a mid grey, which is the same +// matrix step the preview puts on every pixel, so this can never drift from it. +function temperatureSwatch(k: number): string { + const g = kelvinToRGB(k); + const chan = (v: number) => Math.round(Math.min(255, Math.max(0, v * 255))).toString(16).padStart(2, '0'); + return `#${chan(0.5 * g.r)}${chan(0.5 * g.g)}${chan(0.5 * g.b)}`; } // HSL -> #rrggbb (CSS keeps hsl() opaque to a test, and the swatch prints its @@ -144,7 +146,7 @@ function hslToHex(h: number, s: number, l: number): string { // visible next to the number. TINT runs green (-10) through a neutral grey (0) // to magenta (+10) — the axis every WB tool labels G..M. const SLIDER_SWATCH: Partial string>> = { - temperature: kelvinToHex, + temperature: temperatureSwatch, tint: (v) => hslToHex(140 + ((v + 10) / 20) * 160, Math.abs(v) * 7, 58 + (10 - Math.abs(v)) * 3), };