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

8.1 KiB
Raw Blame History

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):

.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:

{ 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):

{ 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:

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

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):

uniform float wh;
uniform float bl;

Trong main, chèn sau hai dòng Dynamic Range và trước o = clamp(o, …):

// 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

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; …
}
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:

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.