docs: note the CREATE form's folds and the white/black point rows

The web form (recipes-web f17cb08) now folds its six categorical groups and
carries EXPOSURE, EV, WHITE and BLACK. WHITE/BLACK do not exist in
ColorAdjustments on either side, so the note spells out the two model fields
and the wh/bl uniforms the tone pass needs — an input row without them would be
dead. A TODO in RecipeCreateModal's header points here.
This commit is contained in:
2026-09-18 12:32:13 +07:00
parent 82efc1f675
commit 99069aa837
2 changed files with 200 additions and 0 deletions
+191
View File
@@ -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 `<details>`/`<summary>` 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<Record<string, boolean>>` 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.
+9
View File
@@ -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);