android: add the CLASSIC VIVIDIPES sim and the FX MONOCHROME switch
Straight off PLAN-WEB-TO-ANDROID.md: the sim keeps Classic Chrome's blue row verbatim and takes Velvia's red and green rows, so skies stay muted while everything else reads loud, and it carries Chrome's shadow pull. MONOCHROME is a switch, not a look: it swaps the base filter for the mono stock and swaps right back, leaving adjustments untouched so a knob turned while it is on survives the round trip. It goes through commit(), so UNDO undoes it.
This commit is contained in:
@@ -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<Recipe | null>(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)}
|
||||
|
||||
@@ -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<Recipe, 'id' | 'isCustom'>) => 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).
|
||||
|
||||
+1
-1
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 } },
|
||||
|
||||
@@ -188,6 +188,9 @@ export interface ToneUniforms {
|
||||
// without also moving the other.
|
||||
const FILM_TONE: Partial<Record<BaseFilter, Partial<ToneUniforms>>> = {
|
||||
'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
|
||||
|
||||
Reference in New Issue
Block a user