diff --git a/App.tsx b/App.tsx index 1d4f3b3..b7ce447 100644 --- a/App.tsx +++ b/App.tsx @@ -764,6 +764,29 @@ export default function App() { lastStepRef.current = 0; setUndoDepth(0); }; + // FX's MONOCHROME switch: not a look of its own — it swaps the base filter for + // the mono stock and swaps right back. Adjustments are never touched, so a knob + // turned while it is on survives the round trip. + const monoOn = selectedRecipe?.baseFilter === 'monochrome'; + const monoPrevRef = useRef(null); + + const handleToggleMono = () => { + commit(true, () => { + if (selectedRecipe?.baseFilter === 'monochrome') { + const back = monoPrevRef.current ?? filmSimToRecipe(FILM_SIMS.find((f) => f.id === 'sim-provia')!); + monoPrevRef.current = null; + setSelectedRecipe(back); + } else { + monoPrevRef.current = selectedRecipe; + setSelectedRecipe({ + ...(selectedRecipe ?? filmSimToRecipe(FILM_SIMS[0])), + id: 'mono', + name: 'MONOCHROME', + baseFilter: 'monochrome', + }); + } + }); + }; const applyLook = (l: Look) => { setSelectedRecipe(l.recipe); setAdjustments(l.adjustments); @@ -1963,6 +1986,8 @@ export default function App() { openParam={openParamKey} onOpenParamChange={setOpenParamKey} currentBaseFilter={selectedRecipe?.baseFilter} + monoOn={monoOn} + onToggleMono={handleToggleMono} onCreateRecipe={handleCreateRecipe} editingRecipe={editingRecipe} onCloseEdit={() => setEditingRecipe(null)} diff --git a/src/components/AdjustmentPanel.tsx b/src/components/AdjustmentPanel.tsx index f526143..a759f15 100644 --- a/src/components/AdjustmentPanel.tsx +++ b/src/components/AdjustmentPanel.tsx @@ -80,6 +80,10 @@ interface AdjustmentPanelProps { // Base filter of the currently applied recipe — seeds the CREATE modal's // SIMULATION row so quick-input starts from the active look. currentBaseFilter?: BaseFilter; + // FX tab MONOCHROME: a switch, not a look — on it swaps the base filter for + // the mono stock, off it hands back the one that was there. + monoOn: boolean; + onToggleMono: () => void; // CREATE quick-input saves a brand-new recipe (name + adjustments from the // modal) and applies it, unlike onSaveRecipe which snapshots the live state. onCreateRecipe: (recipe: Omit) => void; @@ -243,6 +247,8 @@ export default function AdjustmentPanel({ openParam, onOpenParamChange, currentBaseFilter, + monoOn, + onToggleMono, onCreateRecipe, editingRecipe, onCloseEdit, @@ -896,7 +902,12 @@ export default function AdjustmentPanel({ ]); case 'filters': - return chipRow(paramChips(paramDefs.filters)); + // MONOCHROME leads the strip: a switch that swaps the base filter for + // the mono stock and swaps right back, so it never touches a knob. + return chipRow([ + { key: 'mono', label: 'MONOCHROME', active: monoOn, amberValue: monoOn, onPress: onToggleMono }, + ...paramChips(paramDefs.filters), + ]); case 'frame': // FRAME -> WATERMARK: the sub-panel row (breadcrumb back to the frames). diff --git a/src/types/index.ts b/src/types/index.ts index 0a0fff6..8280081 100644 --- a/src/types/index.ts +++ b/src/types/index.ts @@ -62,7 +62,7 @@ export const MIN_CROP_FRAC = 0.08; // cool tint in shade, plus a seasonal atmospheric haze/fog veil. export type CinemaSeason = 'spring' | 'summer' | 'autumn' | 'winter'; -export type BaseFilter = 'classic-neg' | 'classic-chrome' | 'provia' | 'velvia' | 'astia' | 'eterna' | 'monochrome' | 'leica' | 'leica-vivid' | 'none'; +export type BaseFilter = 'classic-neg' | 'classic-chrome' | 'classic-vivid' | 'provia' | 'velvia' | 'astia' | 'eterna' | 'monochrome' | 'leica' | 'leica-vivid' | 'none'; export interface ColorAdjustments { exposure: number; // -10 to +10 (mapped to matrix multiplier or offset) diff --git a/src/utils/colorUtils.ts b/src/utils/colorUtils.ts index 4a69a24..1de5316 100644 --- a/src/utils/colorUtils.ts +++ b/src/utils/colorUtils.ts @@ -120,6 +120,16 @@ export function getSkiaColorMatrix(baseFilter: BaseFilter, adj: ColorAdjustments 0.0652, 0.3973, 0.5575, 0, -0.0100, 0, 0, 0, 1, 0, ]; + } else if (baseFilter === 'classic-vivid') { + // Classic Vivid: the Classic Chrome blue row kept verbatim — skies stay the + // same muted teal/cyan — with the red and green rows taken from Velvia, so + // everything that is not blue reads loud. + matrix = [ + 1.3895, -0.2540, -0.0256, 0, -0.0550, // R — velvia + -0.0772, 1.2355, -0.0261, 0, -0.0661, // G — velvia + 0.0652, 0.3973, 0.5575, 0, -0.0100, // B — classic-chrome (nguyên bản) + 0, 0, 0, 1, 0, + ]; } else if (baseFilter === 'astia') { // Astia (Soft): gentler than standard — contrast below neutral (shadows // lift, highlights ease off) and colour held back, with a faint warm/pink diff --git a/src/utils/defaultRecipes.ts b/src/utils/defaultRecipes.ts index 22cb950..700978f 100644 --- a/src/utils/defaultRecipes.ts +++ b/src/utils/defaultRecipes.ts @@ -41,6 +41,7 @@ export const FILM_SIMS: FilmSim[] = [ { id: 'sim-provia', name: 'PROVIPES', baseFilter: 'provia', adjustments: { ...DEFAULT_ADJUSTMENTS } }, { id: 'sim-velvia', name: 'VELVIPES', baseFilter: 'velvia', adjustments: { ...DEFAULT_ADJUSTMENTS } }, { id: 'sim-classic-chrome', name: 'CLASSIC CHRIPES', baseFilter: 'classic-chrome', adjustments: { ...DEFAULT_ADJUSTMENTS } }, + { id: 'sim-classic-vivid', name: 'CLASSIC VIVIDIPES', baseFilter: 'classic-vivid', adjustments: { ...DEFAULT_ADJUSTMENTS } }, { id: 'sim-classic-neg', name: 'CLASSIC NEGIPES', baseFilter: 'classic-neg', adjustments: { ...DEFAULT_ADJUSTMENTS } }, { id: 'sim-astia', name: 'ASTIPES', baseFilter: 'astia', adjustments: { ...DEFAULT_ADJUSTMENTS } }, { id: 'sim-eterna', name: 'ETERNIPES', baseFilter: 'eterna', adjustments: { ...DEFAULT_ADJUSTMENTS } }, diff --git a/src/utils/toneShader.ts b/src/utils/toneShader.ts index 2325f36..2008728 100644 --- a/src/utils/toneShader.ts +++ b/src/utils/toneShader.ts @@ -188,6 +188,9 @@ export interface ToneUniforms { // without also moving the other. const FILM_TONE: Partial>> = { 'classic-chrome': { sh: -0.28 }, + // Classic Vivid keeps Chrome's dived shadow: the shadow pull belongs to the + // stock, not to the matrix rows it borrows from Velvia. + 'classic-vivid': { sh: -0.28 }, 'classic-neg': { shT: [-0.018, 0.009, 0.013], hlT: [0.024, 0.008, -0.012] }, // Acros. A black-and-white stock IS its grey ramp, so this entry only shapes // the two ENDS and leaves the middle an identity: a smooth shadow toe that