diff --git a/6_CREATE_COLLAPSE.md b/6_CREATE_COLLAPSE.md new file mode 100644 index 0000000..d16be30 --- /dev/null +++ b/6_CREATE_COLLAPSE.md @@ -0,0 +1,191 @@ +# 6 — CREATE RECIPES: gấp nhóm + thêm EXPOSURE / EV / WHITE / BLACK + +Nhánh: `feat/vision-camera-v5`. Ngày: 2026-09-06. +Nguồn đối chiếu: nhánh `recipes-web`, commit **`f17cb08`** ("web: fold the create +form's groups and add the white/black point rows") — bản web đã làm, đã đo và +đang chạy ở `http://localhost:8090/app`. Commit `cf4b01d` là việc đổi màu tên +recipe đang chọn trong FAVORITED; bản app **đã có sẵn** (`AdjustmentPanel.tsx`, +tab `favorites`: `active: r.id === currentRecipeId`) nên không cần làm lại. + +Đây là ghi chú yêu cầu, **chưa implement trên app**. Web đã xong và là bản mẫu +để đối chiếu từng dòng. + +--- + +## 1. Mục tiêu + +Form CREATE RECIPES đã dài thành một cuộn dọc. Hai việc: + +1. **Gấp được** sáu nhóm phân loại: SIMULATION, DYNAMIC RANGE, GRAIN EFFECT, + COLOR CHROME EFFECT, COLOR CHROME EFFECT BLUE, WHITE BALANCE. +2. **Thêm bốn ô số**: EXPOSURE, EV, WHITE, BLACK. + +Thứ tự hàng giữ nguyên như điện thoại đang có, chỉ chèn thêm — trừ +`EXPOSURE COMP.` đổi tên thành `EV` (cùng một tham số `exposureCompensation`, +chỉ khác nhãn; web đã đổi để hai chữ EXPOSURE/EV không lẫn nhau). + +--- + +## 2. Việc 1 — gấp nhóm (thuần UI, chỉ `RecipeCreateModal.tsx`) + +Web dùng `
`/`` gốc của trình duyệt: không state, không +thư viện, trình duyệt tự giữ cờ mở/đóng. App dùng React Native nên phải tự làm: +một `useState>` cho sáu nhóm, nhãn nhóm bấm được, mũi +tên xoay 90° khi mở, và phần thân chỉ render khi nhóm đang mở. + +Ranh giới từng nhóm trong `RecipeCreateModal.tsx` (số dòng theo bản hiện tại): + +| Nhóm | Hàng nằm trong | +| --- | --- | +| SIMULATION | dải chip `FILM_SIMS` | +| DYNAMIC RANGE | dải chip `DR_OPTS` | +| GRAIN EFFECT | dải chip `GRAIN_OPTS` | +| COLOR CHROME EFFECT | dải chip `CC_OPTS` | +| COLOR CHROME EFFECT BLUE | dải chip `CC_OPTS` | +| WHITE BALANCE | dải chip `WB_PRESETS` **+ hai ô WB SHIFT RED/BLUE** | + +Ngoài nhóm (luôn hiện): NAME, lưới số (`fieldSets`), hàng ISO, SAVE/EXPORT. +Khối WB SHIFT đi cùng WHITE BALANCE — về mặt nhiếp ảnh nó là một phần của WB. + +Mặc định **mở cả sáu** để form không đổi hành vi cho tới khi người dùng tự gấp. + +CSS web đã dùng (đối chiếu nếu muốn giống hệt): + +```css +.create-sec { display: flex; flex-direction: column; gap: 6px; } +.create-sec > summary { display: flex; align-items: center; gap: 6px; cursor: pointer; list-style: none; user-select: none; } +.create-sec > summary::-webkit-details-marker { display: none; } +.create-sec > summary::before { content: '▸'; font-size: 9px; transition: transform 0.15s ease; } +.create-sec[open] > summary::before { transform: rotate(90deg); } +``` + +--- + +## 3. Việc 2 — bốn ô số + +`fieldSets` hiện tại: + +```ts +{ label: 'HIGHLIGHT', value: fHigh, set: setFHigh }, +{ label: 'SHADOW', value: fShadow, set: setFShadow }, +{ label: 'COLOR', value: fColor, set: setFColor }, +{ label: 'NOISE RED.', value: fNr, set: setFNr, ph: '-10..10' }, +{ label: 'SHARPENING', value: fSharp, set: setFSharp, ph: '-10..10' }, +{ label: 'CLARITY', value: fClarity, set: setFClarity }, +{ label: 'EXPOSURE COMP.', value: fEv, set: setFEv, ph: 'EV' }, +``` + +Web sau khi sửa (đúng thứ tự này): + +```ts +{ label: 'HIGHLIGHT', value: fHigh, set: setFHigh }, +{ label: 'SHADOW', value: fShadow, set: setFShadow }, +{ label: 'WHITE', value: fWhites, set: setFWhites }, +{ label: 'BLACK', value: fBlacks, set: setFBlacks }, +{ label: 'COLOR', value: fColor, set: setFColor }, +{ label: 'NOISE RED.', value: fNr, set: setFNr, ph: '-10..10' }, +{ label: 'SHARPENING', value: fSharp, set: setFSharp, ph: '-10..10' }, +{ label: 'CLARITY', value: fClarity, set: setFClarity }, +{ label: 'EXPOSURE', value: fExposure, set: setFExposure }, +{ label: 'EV', value: fEv, set: setFEv, ph: '±3 EV' }, +``` + +Thêm ba state (`fWhites`, `fBlacks`, `fExposure`) song song với các `f*` đang +có, seed từ `seedAdj`/`valuesOf`, và **bắt buộc** thêm vào `handleSave` — thiếu +một dòng là ô vừa nhập bị `...seedAdj` ghi đè và recipe lưu ra khác màn hình: + +```ts +whites: int(fWhites, simAdj.whites ?? 0, -10, 10), +blacks: int(fBlacks, simAdj.blacks ?? 0, -10, 10), +exposure: int(fExposure, simAdj.exposure, -10, 10), +``` + +`EXPOSURE` là `adjustments.exposure` (-10..10, độ lợi ma trận) — đã có sẵn trong +model và trong tab LIGHT, chỉ thiếu hàng trong CREATE. `EV` là +`exposureCompensation` (-3..+3 EV) như hàng `EXPOSURE COMP.` cũ. + +--- + +## 4. `WHITE` / `BLACK` là tham số MỚI — phải thêm cả model lẫn shader + +Không có `whites`/`blacks` ở đâu trong app (đã grep cả hai nhánh). Nếu chỉ vẽ ô +nhập mà không đụng engine thì đó là **ô chết**: nhập xong không ảnh nào đổi. + +### 4.1 `src/types/index.ts` — `ColorAdjustments` + +```ts +highlight: number; // -10 to +10 (- pulls highlights down / roll, + brightens) +shadow: number; // -10 to +10 (+ lifts shadows, - deepens) +whites?: number; // -10 to +10 (white point: the very top of the tone curve) +blacks?: number; // -10 to +10 (black point: the very toe of the tone curve) +``` + +Để **optional** (`?`) — recipe cũ trong máy không có hai khóa này vẫn đọc được. + +### 4.2 `src/utils/toneShader.ts` — `TONE_SKSL` + +Khai báo uniform ngay sau `sh` (thứ tự này phải khớp `toneUniformArray`): + +```glsl +uniform float wh; +uniform float bl; +``` + +Trong `main`, chèn **sau** hai dòng Dynamic Range và **trước** `o = clamp(o, …)`: + +```glsl +// White/black point. The HL/SH knees above already spend the slope budget +// (each just under 1.0), so these two use cubic end-weights instead of +// another smoothstep: only the last/first stretch of the ramp moves, and the +// derivative stays >= 1 - 3*0.18 = 0.46 for every combination, so the curve +// can never invert. + lifts that end (faded blacks / brighter whites), +// - digs it in (true black / rolled white). +o += bl * 0.18 * (1.0 - t) * (1.0 - t) * (1.0 - t); +o += wh * 0.18 * t * t * t; +``` + +Vì sao **không** dùng thêm một cặp `smoothstep` như HL/SH: hai knee 0.65..1.00 +và 0.00..0.55 đã tiêu gần hết "ngân sách độ dốc" (mỗi cái ~0.94/1.0); thêm một +knee nữa là đẩy tổng độ dốc qua 1.0 và đường cong **đảo** (điểm sáng hơn ra tối +hơn). Cubic end-weight chỉ chạm ~đoạn cuối của dải, đạo hàm luôn ≥ 0.46. + +### 4.3 Bốn chỗ còn lại trong cùng file + +```ts +export interface ToneUniforms { + dr: number; hl: number; sh: number; + wh: number; // -1..1 (adjustments.whites / 10 — white point) + bl: number; // -1..1 (adjustments.blacks / 10 — black point) + vib: number; … +} +``` + +```ts +const wh = Math.max(-1, Math.min(1, (adj.whites ?? 0) / 10)); +const bl = Math.max(-1, Math.min(1, (adj.blacks ?? 0) / 10)); +``` + +`getToneUniforms` trả thêm `wh, bl`; `toneIsActive` thêm `u.wh !== 0 || u.bl !== 0` +(không thêm thì pass tone bị bỏ qua và hai ô không có tác dụng); và +`toneUniformArray` — **thứ tự phần tử phải trùng khai báo GLSL**: + +```ts +return [u.dr, u.hl, u.sh, u.wh, u.bl, u.vib, + u.shT[0], u.shT[1], u.shT[2], u.hlT[0], u.hlT[1], u.hlT[2], u.cc, u.ccb]; +``` + +`dr`/`hl`/`sh` hai nhánh giống nhau; chỉ 4 khối ở mục 4.2–4.3 là mới. + +--- + +## 5. Kiểm thử (đo được, không đoán) + +Web đã đo bằng ảnh thật (`/tmp/w0/p2.jpg`): `WHITE +10` nâng độ chói decile trên +cùng, `BLACK +10` nâng decile dưới cùng và **không** đụng đỉnh, `BLACK -10` hạ +decile dưới. Vòng round-trip `.recipe` (XML xor16-v1) giữ nguyên +`whites = 4`, `blacks = -3`, `exposureCompensation = 2` qua chính importer của +điện thoại. + +Bản app cần đo lại trên **cả ba đường vẽ** (engine Skia xuất file, native export, +preview viewfinder) như ghi chú 5 đã nêu: cùng một cặp WHITE/BLACK phải ra cùng +một ảnh. diff --git a/src/components/RecipeCreateModal.tsx b/src/components/RecipeCreateModal.tsx index 7c83e37..4edf352 100644 --- a/src/components/RecipeCreateModal.tsx +++ b/src/components/RecipeCreateModal.tsx @@ -18,6 +18,15 @@ import { DEFAULT_ADJUSTMENTS, FILM_SIMS } from '../utils/defaultRecipes'; // plain textboxes (typed fast, one row each), categorical knobs are chip // dropdowns. Used to enter Fuji-style recipes (DR/Highlight/Shadow/Color/NR/ // Sharpness/Clarity/Grain/CC/CCB/WB+shift/ISO/EV) without touching sliders. +// +// TODO (chưa làm — xem `6_CREATE_COLLAPSE.md`, bản web đối chiếu commit +// f17cb08 của nhánh recipes-web): +// 1. Gấp sáu nhóm: SIMULATION, DYNAMIC RANGE, GRAIN EFFECT, COLOR CHROME +// EFFECT, COLOR CHROME EFFECT BLUE, WHITE BALANCE (WB SHIFT đi kèm WB). +// 2. Thêm bốn ô số: EXPOSURE, EV (đổi tên từ EXPOSURE COMP.), WHITE, BLACK. +// WHITE/BLACK chưa có trong `ColorAdjustments` — phải thêm cả hai field +// (src/types/index.ts) và cặp uniform `wh`/`bl` trong `src/utils/toneShader.ts` +// (cubic end-weights), nếu không hai ô đó là ô chết. const haptic = () => Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Light);