The five changes recipes-web landed on 2026-09-29 (34f8601, c0aaa67, bd57dd7,
3b92e4e) only put two new fields into the shareable payload, but four of them
change the look a given recipe produces — so both halves of the story belong in
one note, next to doc 8 which was written against 6814b05.
What the note says:
- The slider/store split. Web now reads EXPOSURE as stops (-5..+5, two decimals)
and every develop knob as -100..+100, but paramDefs translates both ways and
the stored units did not move: exposure is still +-10 units of 0.25 EV, every
other knob is still +-10. Android can keep its +-10 sliders and still be
byte-compatible, so the doc says that instead of asking for a UI port.
- The one place a unit really does differ: masks[].exposure is the EV itself
(MASK_EXPOSURE_MAX = 5) because the shader reads pow(2.0, a.x). Getting that
wrong is a factor of four, so it gets its own row and its own acceptance test.
- The recipe format did not change. The shareable object is still
{name, baseFilter, adjustments, frameId, useGeotag}, adjustments still travels
whole, and Android already keeps unknown fields through import -> Look ->
export because only hslBands is sanitized. So masks survive web -> Android ->
web today; that is worth an explicit round-trip test rather than being relied
on as an accident.
- The two new mask fields: temperature (2500..10000 K, NEUTRAL_K = 5500) and
tint (+-10), resolved in readMasks so a mask stored before them carries a gain
of exactly (1,1,1), plus the extra float4 wb[count] uniform and the one line
c = clamp(c * wb, 0, 1) right after EXPOSURE. Gain is hoisted to JS on purpose:
the Kelvin fit is a curve and a second copy of it in SkSL is a second answer.
- whiteBalanceGain is the same arithmetic Android already has inline in
getSkiaColorMatrix, so the port is a lift-and-shift with a bit-identical
matrix as its acceptance test.
One divergence is called out rather than papered over: gradientMask.ts still
runs the old cg = clamp(lifted / max(l, 0.0004), 0.55, 1.35) for the tone knobs
inside a mask, and its comment still claims that is TONE_SKSL's own gain — which
stopped being true when c0aaa67 gave the frame-wide shader the hue-preserving
knee and the five-knot ramp. A mask's HIGHLIGHT is therefore not the frame-wide
HIGHLIGHT on a smaller area, and the note says which side of that the Android
port should follow until the project owner decides.
Verified: every file:line, commit hash and unit in the note was read back out of
origin/recipes-web at 3b92e4e (toneShader.ts, colorUtils.ts, paramDefs.ts,
gradientMask.ts, types/index.ts, ToolRail.tsx, App.tsx, recipeShare.ts) and out
of this branch (src/utils/colorUtils.ts:384-394, src/types/index.ts:81-105,
src/utils/recipeShare.ts:59-110), including grep runs showing Android has no
masks field at all. The measured WB numbers quoted (1.250 -> 1.724 inside the
mask, 0.994 -> 0.994 outside) are the ones the studio's own mask-wb check
produced against the live build.
ponytail: no code touched, one file. The mask-tone alignment is left as an open
question with both candidate behaviours written down, because choosing for the
owner would bake a look decision into two codebases at once.
Co-authored-by: PenguinHarness <noreply@penguin.local>