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:
@@ -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.
|
||||
@@ -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);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user