diff --git a/src/components/AdjustmentPanel.tsx b/src/components/AdjustmentPanel.tsx index 7322def..5012dc1 100644 --- a/src/components/AdjustmentPanel.tsx +++ b/src/components/AdjustmentPanel.tsx @@ -13,6 +13,9 @@ import { FRAMES } from '../utils/frameUtils'; // Darkroom-flavoured: amber = active/edited, red = destructive, mono labels. type TabId = 'recipes' | 'iq' | 'wb' | 'filters' | 'frame'; +// Choice-group keys that open a strip above the chip row: color chrome groups +// (wb tab) and the D.RANGE selector (iq tab). +type GroupKey = 'cx' | 'cxb' | 'dr'; interface AdjustmentPanelProps { activeTab: TabId; @@ -72,9 +75,9 @@ export default function AdjustmentPanel({ // The single parameter whose slider row is open — a new selection replaces // the previous row (one row at a time, above the chips). const [openParam, setOpenParam] = useState(null); - // The single color-chrome group whose OFF/WEAK/STRONG strip is open above the - // chips (same one-row-at-a-time pattern as openParam, but for choice chips). - const [openGroup, setOpenGroup] = useState<'cx' | 'cxb' | null>(null); + // The single choice-group whose strip (OFF/WEAK/STRONG, or the DR ladder) is + // open above the chips (same one-row-at-a-time pattern as openParam). + const [openGroup, setOpenGroup] = useState(null); useEffect(() => { setOpenParam(null); setOpenGroup(null); @@ -85,7 +88,7 @@ export default function AdjustmentPanel({ setOpenParam((prev) => (prev === key ? null : key)); }; - const toggleGroup = (g: 'cx' | 'cxb') => { + const toggleGroup = (g: GroupKey) => { haptic(); setOpenGroup((prev) => (prev === g ? null : g)); }; @@ -210,22 +213,46 @@ export default function AdjustmentPanel({ } }; - // Color-chrome groups: one toggle chip among the WB chips (COLOR CHROME / - // CHROME BLUE); tapping it reveals the OFF/WEAK/STRONG strip above the chip - // row. Amber text while an effect is active, amber border while its strip is - // open — same affordances as the parameter chips. - const chromeGroup: Record< - 'cx' | 'cxb', - { label: string; value: ColorAdjustments['colorChrome']; onChange: (v: ColorAdjustments['colorChrome']) => void } - > = { - cx: { label: 'COLOR CHROME', value: a.colorChrome || 'none', onChange: (v) => onUpdateAdjustments({ colorChrome: v }) }, - cxb: { label: 'CHROME BLUE', value: a.colorChromeBlue || 'none', onChange: (v) => onUpdateAdjustments({ colorChromeBlue: v }) }, - }; + // Choice-groups: one toggle chip sits in the chip row (COLOR CHROME / + // CHROME BLUE among the WB chips, D.RANGE after the LIGHT params); tapping it + // reveals a strip above the chip row with that group's options. Amber text + // while a value differs from the group's off value, amber border while its + // strip is open — same affordances as the parameter chips. const chromeOptions = [ { v: 'none', d: 'OFF' }, { v: 'weak', d: 'WEAK' }, { v: 'strong', d: 'STRONG' }, ]; + const drOptions = [ + { v: 'auto', d: 'AUTO' }, + { v: '100', d: 'DR100' }, + { v: '200', d: 'DR200' }, + { v: '400', d: 'DR400' }, + ]; + const groupDefs: Record void }> = { + cx: { + label: 'COLOR CHROME', off: 'none', value: a.colorChrome || 'none', options: chromeOptions, + onPick: (v) => onUpdateAdjustments({ colorChrome: v as ColorAdjustments['colorChrome'] }), + }, + cxb: { + label: 'CHROME BLUE', off: 'none', value: a.colorChromeBlue || 'none', options: chromeOptions, + onPick: (v) => onUpdateAdjustments({ colorChromeBlue: v as ColorAdjustments['colorChromeBlue'] }), + }, + dr: { + label: 'D.RANGE', off: 'auto', value: String(a.dynamicRange ?? 'auto'), options: drOptions, + onPick: (v) => + onUpdateAdjustments({ + dynamicRange: (v === 'auto' ? 'auto' : parseInt(v, 10)) as ColorAdjustments['dynamicRange'], + }), + }, + }; + const groupChip = (g: GroupKey): ChipDef => ({ + key: `grp-${g}`, + label: groupDefs[g].label, + active: openGroup === g, + amberValue: openGroup !== g && groupDefs[g].value !== groupDefs[g].off, + onPress: () => toggleGroup(g), + }); // ---- per-tab chip rows (bottom, above the tool rail) ----------------------- const chipsRow = (() => { @@ -258,36 +285,18 @@ export default function AdjustmentPanel({ } case 'iq': + // LIGHT: continuous params on one row; D.RANGE is a toggle chip that + // opens its AUTO/DR100/DR200/DR400 strip above (same as the chrome + // groups on WB). return ( - - - {choiceChips( - 'dr', - [ - { v: 'auto', d: 'AUTO' }, - { v: '100', d: 'DR100' }, - { v: '200', d: 'DR200' }, - { v: '400', d: 'DR400' }, - ], - String(a.dynamicRange ?? 'auto'), - (val) => - onUpdateAdjustments({ - dynamicRange: (val === 'auto' ? 'auto' : parseInt(val, 10)) as ColorAdjustments['dynamicRange'], - }) - )} - - - {paramChips(paramDefs.iq)} - - + + {paramChips(paramDefs.iq)} + {renderChip(groupChip('dr'))} + ); case 'wb': @@ -313,15 +322,7 @@ export default function AdjustmentPanel({ wbApply )} {paramChips(paramDefs.wb)} - {(['cx', 'cxb'] as const).map((g) => - renderChip({ - key: `grp-${g}`, - label: chromeGroup[g].label, - active: openGroup === g, - amberValue: openGroup !== g && chromeGroup[g].value !== 'none', - onPress: () => toggleGroup(g), - }) - )} + {(['cx', 'cxb'] as const).map((g) => renderChip(groupChip(g)))} ); @@ -417,14 +418,12 @@ export default function AdjustmentPanel({ > {renderChip({ key: `grp-hint-${openGroup}`, - label: chromeGroup[openGroup].label, + label: groupDefs[openGroup].label, disabled: true, active: false, onPress: () => {}, })} - {choiceChips(openGroup, chromeOptions, chromeGroup[openGroup].value, (v) => - chromeGroup[openGroup].onChange(v as ColorAdjustments['colorChrome']) - )} + {choiceChips(openGroup, groupDefs[openGroup].options, groupDefs[openGroup].value, groupDefs[openGroup].onPick)} )} diff --git a/src/components/Viewfinder.tsx b/src/components/Viewfinder.tsx index ca9a180..9d5c944 100644 --- a/src/components/Viewfinder.tsx +++ b/src/components/Viewfinder.tsx @@ -395,14 +395,18 @@ export default function Viewfinder({ return { dx: (vw - dw) / 2, dy: (vh - dh) / 2, dw, dh }; }, [skiaImage, vw, vh]); // Polaroid card geometry (JS/overlay space) for the CURRENT mode. Library - // stills keep the photo aspect; camera previews use the screen aspect (the - // feed fills the screen, so the card overlays it in view coordinates). + // stills keep the photo aspect; the camera assumes the upright capture + // aspect of the photo output (4:3 sensor → 3:4 portrait), NOT the screen + // aspect: a screen-aspect window made the card nearly fullscreen and + // pole-like, whereas the real print is a shorter 3:4 card letterboxed on + // screen. ponytail: verified on Xiaomi quality-mode captures (4:3); if the + // session negotiates 16:9 photos, change to 9/16. const polaroidGeo = useMemo(() => { if (selectedFrame !== 'polaroid') return null; const aspect = mode === 'library' && skiaImage && skiaImage.width() > 0 && skiaImage.height() > 0 ? skiaImage.width() / skiaImage.height() - : vw / vh; + : 3 / 4; return polaroidLayout(vw, vh, aspect, 'contain'); }, [selectedFrame, mode, skiaImage, vw, vh]); const onLibTouchStart = (e: any) => {