Files
RecipesCam/6_CREATE_COLLAPSE.md
T
3dtours 99069aa837 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.
2026-09-18 12:32:13 +07:00

192 lines
8.1 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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.