e0526283ca
The web studio grew a CLASSIC VIVIDIPES sim (Classic Chrome's blue row spliced to Velvia's red and green) and an FX MONOCHROME switch; both are written up here with the exact matrices, anchors and the checks they have to pass before they count as ported.
124 lines
5.7 KiB
Markdown
124 lines
5.7 KiB
Markdown
# Ghi chú port web → Android: sim `CLASSIC VIVIDIPES` + chip FX `MONOCHROME`
|
|
|
|
Nguồn: web studio `docker/frontend` (nhánh `recipes-web`), commit `d7d7be2` (sim mới) và
|
|
`4d0d441` (chip FX). Việc dưới đây là **port sang app Android** (`src/`, nhánh này).
|
|
|
|
Đối chiếu: `src/` giữa `recipes-web` và `feat/vision-camera-v5` **chỉ khác**
|
|
`src/components/RecipeCreateModal.tsx` (+9 dòng) — nên port gần như là chép y nguyên.
|
|
Số dòng dưới đây tính theo nhánh này.
|
|
|
|
---
|
|
|
|
## 1. Sim mới `CLASSIC VIVIDIPES`
|
|
|
|
Ghép tại dòng blue: **giữ nguyên dòng blue của `classic-chrome`**, lấy **dòng R/G của `velvia`**.
|
|
|
|
Sửa 4 file:
|
|
|
|
1. `src/types/index.ts:65` — `BaseFilter`: thêm `'classic-vivid'`.
|
|
2. `src/utils/colorUtils.ts` — thêm nhánh **ngay sau** nhánh `classic-chrome` (dòng 111):
|
|
```ts
|
|
} 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,
|
|
];
|
|
```
|
|
3. `src/utils/defaultRecipes.ts:43` — `FILM_SIMS`, thêm ngay sau `sim-classic-chrome`:
|
|
```ts
|
|
{ id: 'sim-classic-vivid', name: 'CLASSIC VIVIDIPES', baseFilter: 'classic-vivid', adjustments: { ...DEFAULT_ADJUSTMENTS } },
|
|
```
|
|
4. `src/utils/toneShader.ts:190` — `FILM_TONE`, cạnh `'classic-chrome'`:
|
|
```ts
|
|
'classic-vivid': { sh: -0.28 },
|
|
```
|
|
(Dìm shadow thuộc về stock chứ không thuộc dòng ma trận → đi kèm. Bỏ dòng này nếu muốn
|
|
VIVIDIPES không dìm shadow.)
|
|
|
|
**Không phải sửa gì thêm**: chip `PHOTO STYLE` (`AdjustmentPanel.tsx` `groupDefs.style`), TopBar
|
|
(`activeRecipe.name`) và picker trong `RecipeCreateModal.tsx` đều map từ `FILM_SIMS`.
|
|
|
|
**Kiểm chứng**: strip PHOTO STYLE có **10** sim; chọn `CLASSIC VIVIDIPES` → look khác **cả**
|
|
`CLASSIC CHRIPES` lẫn `VELVIPES`; `CLASSIC CHRIPES` vẫn nguyên vẹn cạnh nó.
|
|
|
|
---
|
|
|
|
## 2. Chip `MONOCHROME` ở tab FX (`filters`)
|
|
|
|
Đây là **công tắc**, không phải một look: bật thì đổi `baseFilter` sang `monochrome`, tắt thì
|
|
trả lại đúng cái ảnh đang mặc (kể cả tên sim). **Không được đụng `adjustments`** — knob chỉnh
|
|
trong lúc đang bật phải sống sót.
|
|
|
|
Web: `docker/frontend/src/App.tsx` — `monoPrev` ref + `toggleMono`, chip `mono` đứng đầu
|
|
`case 'fx'`, `active: monoOn` với `monoOn = recipe.baseFilter === 'monochrome'`.
|
|
|
|
Android cần 2 chỗ:
|
|
|
|
**a. `src/components/AdjustmentPanel.tsx`** — `case 'filters'` (dòng 888):
|
|
```ts
|
|
case 'filters':
|
|
return chipRow([
|
|
{ key: 'mono', label: 'MONOCHROME', active: monoOn, amberValue: monoOn, onPress: onToggleMono },
|
|
...paramChips(paramDefs.filters),
|
|
]);
|
|
```
|
|
+ thêm 2 prop vào `interface AdjustmentPanelProps`: `monoOn: boolean;` và `onToggleMono: () => void;`
|
|
+ truyền từ `App.tsx` chỗ render `<AdjustmentPanel … />` (`App.tsx:1929` có `currentBaseFilter`,
|
|
`onSelectRecipe` — đặt cạnh đó).
|
|
|
|
**b. `App.tsx`** — Android giữ `adjustments` là state **riêng** (không nằm trong `selectedRecipe`),
|
|
nên công tắc chỉ chạm `selectedRecipe`:
|
|
|
|
```ts
|
|
// 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.
|
|
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')!);
|
|
setSelectedRecipe(back);
|
|
} else {
|
|
monoPrevRef.current = selectedRecipe;
|
|
setSelectedRecipe({
|
|
...(selectedRecipe ?? filmSimToRecipe(FILM_SIMS[0])),
|
|
id: 'mono',
|
|
name: 'MONOCHROME',
|
|
baseFilter: 'monochrome',
|
|
});
|
|
}
|
|
});
|
|
};
|
|
```
|
|
|
|
Lưu ý:
|
|
- `commit(true, …)` (dòng 739) để công tắc vào **undo stack** như mọi edit khác — `handleResetAll`
|
|
dùng đúng cách này.
|
|
- `Look` đã có `recipe` → UNDO tự trả lại đúng `baseFilter` cũ, không cần state phụ nào khác.
|
|
- `id: 'mono'` không trùng sim nào nên khi bật, chip PHOTO STYLE **không** còn sáng tên sim cũ
|
|
(`groupDefs.style.value` tính từ `currentRecipeId` so với `FILM_SIMS`).
|
|
- Tắt khi `monoPrevRef` rỗng (ví dụ vào monochrome bằng cách chọn sim ACRIPES, không qua công tắc)
|
|
→ rơi về PROVIA, tức là "tắt là có màu", không phải về đen trắng.
|
|
|
|
**Kiểm chứng**: bật → ảnh xám (mọi pixel `R=G=B`; chỉ lệch khi WB khác 5500K hoặc tint khác 0);
|
|
tắt → ảnh trở lại **y hệt** trước khi bật (so hash pixel); đổi `CONTRAST` trong lúc bật/tắt →
|
|
giá trị vẫn giữ; UNDO một nhịp → về màu, chip trở lại trạng thái tắt.
|
|
|
|
---
|
|
|
|
## 3. Test web đã chạy (tham chiếu — nằm trong scratchpad của agent, không nằm trong repo)
|
|
|
|
- `sim-vivid-test.cjs` — **9/0**: chip có mặt, strip đủ 10 sim, chọn được, render khác cả
|
|
CHRIPES lẫn VELVIPES.
|
|
- `fx-mono-test.cjs` — **15/0**: đo trực tiếp kênh màu trên pixel preview (spread `>25` khi màu,
|
|
`<=6` khi mono), hash quay lại y hệt khi tắt, CONTRAST sống sót, UNDO gỡ mono.
|
|
- Regression web quanh 2 thay đổi: `panel`, `layout`, `fonts 27/0`, `create 52/0`, `share 22/0`,
|
|
`photo-save 19/0`, `redo 30/0`, `accent-brand` — tất cả xanh.
|