Files
RecipesCam/WEB_PLAN.md
T

135 lines
11 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.
# WEB_PLAN — RecipesCam WebUI trên Docker (branch `recipes-web`)
Trạng thái: kế hoạch, chưa code. Ngày: 2026-09-06.
Nguồn đối chiếu: app RN hiện tại (`App.tsx`, `src/components/*`, `src/utils/*`).
## Mục tiêu
WebUI chạy bằng Docker trên server, mô phỏng giao diện app điện thoại:
- **Cột trái**: rail 6 tab (PRESETS / FAVORITED / LIGHT / WB / FX / FRAME) — chip công cụ của tab đang chọn nằm ngay dưới/bên trong cột đó.
- **Giữa**: khung ảnh lớn, **kéo-thả ảnh từ máy tính** vào để nạp (kèm nút chọn file + paste clipboard).
- Đầy đủ tool như app; sau khi chỉnh → **EXPORT / RENDER** ra JPEG (DPI 300) tải về máy.
- Toàn bộ xử lý ảnh chạy **client-side** (không upload ảnh lên server) — server chỉ phục vụ file tĩnh.
## Quyết định kiến trúc (chốt để khỏi lưỡng lự khi code)
1. **Tách khỏi app RN**: không dùng `react-native-web`/Expo web — app phụ thuộc `react-native-vision-camera` + Skia JSI, web sẽ vỡ. Thêm app mới `web/` (Vite + React + TS) trong cùng repo.
2. **Engine = CanvasKit (canvaskit-wasm) tái dùng gần như nguyên vẹn `exportEngine.ts`**: CanvasKit hỗ trợ `RuntimeEffect` (SkSL y nguyên: `TONE_SKSL`, `CINEMA_SKSL`, noise grain), `ColorFilter.MakeMatrix`, `ImageFilter.MakeMatrixConvolution`/`MakeBlur`, `BlendMode.Overlay`, `drawImageRect`, `makeFreeTypeFaceFromData`, `encodeToBytes(JPEG, 95)`. → parity cao nhất với app, code ít nhất.
3. **Engine chạy trong Web Worker** (+ `OffscreenCanvas`) để UI không giật; preview chỉnh ở bản thu nhỏ (~2048px), render full-res khi export.
4. **Docker**: multi-stage `node:20-alpine` (build) → `nginx:alpine` (serve `dist/`), 1 container, `docker compose up -d`. Cấu hình `application/wasm` + cache header cho `.wasm`.
5. **Trạng thái**: `localStorage` thay `AsyncStorage` (recipes, favorites, phiên gần nhất).
## Tái sử dụng (không viết lại)
| Nguồn trong repo | Dùng cho web |
|---|---|
| `src/utils/colorUtils.ts` | `getSkiaColorMatrix`, `applyExposureGain`, `kelvinToRGB` — nguyên văn |
| `src/utils/toneShader.ts` | `TONE_SKSL` + `getToneUniforms` — SKSL nguyên văn cho CanvasKit |
| `src/utils/cinemaShader.ts` | `CINEMA_SKSL` + `getCinemaUniforms` |
| `src/utils/frameUtils.ts` | `FRAMES`, `polaroidLayout`, `wallframeLayout`, `drawFrameOnCanvas`, hằng số POLAROID/WALLFRAME |
| `src/utils/paramDefs.ts` | `PARAM_DEFS` (slider bounds/label/format) → render chip + slider |
| `src/utils/defaultRecipes.ts` | `FILM_SIMS`, `DEFAULT_ADJUSTMENTS`, recipe mặc định |
| `src/utils/jpegDpi.ts` | `patchJpegDpi(bytes, 300)` — chạy trong worker |
| `src/utils/recipeShare.ts` | IMPORT/EXPORT recipe chia sẻ giữa app ↔ web (cùng định dạng) |
| `src/types/index.ts` | `ColorAdjustments`, `Recipe`, `FrameId`, `AspectRatio`, `CropRatio`, `CropRect`, `ASPECT_RATIO_W_H`, `CROP_W_H` |
| `assets/Cousine-Regular.ttf`, `assets/NotoEmoji-GPS.ttf`, `wallframe.png` | font + artwork frame |
Cần viết mới: `web/src/engine/skiaShim.ts` (adapter API Skia trên CanvasKit) + `web/src/engine/exportEngine.ts` (port từ `src/utils/exportEngine.ts`, bỏ phần `expo-file-system`/MediaLibrary — thay bằng Blob/download).
## Bản đồ UI app → web
| Khu vực app | Web |
|---|---|
| `ToolRail` (rail ngang dưới màn hình) | Cột dọc bên trái, 6 tab |
| `AdjustmentPanel` chip row + strip (một row mở tại một thời điểm) | Cột thứ 2 cạnh rail: chip của tab + strip/slider mở tại chỗ; panel rộng ~280–320px |
| `Viewfinder` (khung ảnh giữa) | Khung canvas giữa, nhận drag&drop file ảnh |
| Drag-on-image để đổi giá trị slider | Giữ được: kéo dọc trên ảnh = chỉnh param đang mở |
| PEEK (giữ để so sánh) | Giữ chuột phải / phím `\` để xem ảnh gốc |
| `TopBar` (tên recipe, flash, UNDO, SAVE/SAVE AS, share) | Header trên: tên recipe, UNDO/RESET, SAVE AS, EXPORT, IMPORT/EXPORT recipe, ẩn các mục camera-only (flash) |
| `PhotoViewerModal` (swipe lịch sử) | Strip thumbnail kết quả export ở đáy, click mở lớn |
| `SettingsModal` | Popover settings; bỏ mục camera-only (shutter sound, RAW DNG, metering, touch-to-shutter, startup mode); giữ PHOTO RATIO |
### Danh sách tool đầy đủ theo tab (lấy từ code hiện tại)
- **PRESETS**: PHOTO STYLE (8 sim: `provia`, `velvia`, `classic-chrome`, `classic-neg`, `astia`, `eterna`, `monochrome`, `leica`, `leica-vivid`) · RECIPES (strip recipe bundled + user, có star) · CREATE · IMPORT. Recipe bundled gồm 4 recipe `cinema` (SPRING/SUMMER/AUTUMN/WINTER) + retro-amber v.v.
- **FAVORITED**: recipe đã star.
- **LIGHT (iq)**: EXPOSURE · EV · CONTRAST · COLOR · VIBRANCE · HIGHLIGHT · SHADOW · D.RANGE (AUTO/DR100/200/400).
- **WB**: TEMP (chip mở strip preset: AUTO / DAYLIGHT / DAYLIGHT -3R / CLOUDY / SHADE / TUNGSTEN / FLUOR + slider COLOR TEMP) · TINT · COLOR CHROME (none/weak/strong) · CHROME BLUE (none/weak/strong).
- **FX (filters)**: NOISE REDUCTION · CLARITY · SHARPENING · MONOCHROME GRAIN · HDF EFFECT · VIGNETTING.
- **FRAME**: NO FRAME / CLASSIC BORDER / RETRO INSTANT (polaroid) / WALL FRAME (+ WALL LANDSCAPE) · CROP (none/free/1:1/2:3/3:2/3:4/4:3/16:9 + APPLY, free crop kéo rect trên ảnh) · ROTATE (0/90/180/270 + STRAIGHTEN + AUTO STRAIGHTEN) · WATERMARK: GPS WATERMARK ON/OFF, PLACE NAME, TIME, TEXT COLOR, TEXT SIZE, TEXT FONT, TEXT ROTATE, CUSTOM WATERMARK (nội dung/color/size/font/rotate).
- **Settings**: PHOTO RATIO (FULL/4:3/3:2) — ảnh hưởng crop khi export.
### Khác biệt web (bắt buộc xử lý)
- **GPS watermark**: web không có GPS thiết bị → cho phép nhập PLACE NAME + timestamp thủ công (mặc định lấy tên file / thời điểm nạp ảnh). Không đọc EXIF GPS ở v1 (có thể thêm sau bằng `exifr`).
- **Crop/rotate/straighten**: thao tác chuột trên canvas (kéo rect, kéo góc xoay).
- **Export**: `Bitmap.compress` → `canvas.toBlob('image/jpeg', 0.95)` hoặc CanvasKit `encodeToBytes` → `patchJpegDpi(300)` → tải file `recipescam_<tên>_<timestamp>.jpg`.
## Cấu trúc thư mục đề xuất
```
web/
Dockerfile # multi-stage: build (node:20-alpine) → nginx:alpine
nginx.conf # wasm mime, cache, fallback index.html
docker-compose.yml # service webui, ports 8090:80, restart unless-stopped
package.json # vite, react, typescript, canvaskit-wasm
vite.config.ts # server.fs.allow ['..'], alias @shared -> ../src/utils, worker format es
index.html
src/
main.tsx App.tsx state/recipeStore.ts
ui/ (ToolRail, ChipColumn, ImageStage, SliderRow, StripRow, ThumbStrip, SettingsPopover)
engine/ (skiaShim.ts, exportEngine.ts, worker.ts, previewEngine.ts)
assets/ (fonts, wallframe.png — copy hoặc import từ ../assets)
```
Docker build context = repo root (cần `src/utils`, `assets/`, `wallframe.png`), `-f web/Dockerfile`.
## Phases
### W0 — Spike CanvasKit (parity gate)
Port thử 1 ảnh qua chuỗi: decode → crop → matrix → tone → cinema → denoise/clarity → HDF → grain → vignette → frame → watermark → JPEG 95 + DPI 300.
Exit: chạy được trong Node (không cần browser) trên 3 ảnh mẫu; so sánh với ảnh export từ app (meanAbs/gt12 như probe P6); đo thời gian 12MP; xác nhận font FreeType (`Cousine`, `NotoEmoji-GPS`) render trong CanvasKit — nếu emoji không ra thì fallback: vẽ watermark bằng lớp Canvas2D riêng.
Rủi ro chính của cả kế hoạch nằm ở đây → làm trước, tốn ~0.5–1 ngày.
### W1 — Scaffold + khung UI + drag&drop
Vite/React/TS; layout 3 vùng; rail 6 tab; chip column rỗng; canvas giữa; nạp ảnh bằng drop/chọn file/paste; preview downscale ~2048px (worker); zoom/pan cơ bản.
Exit: thả ảnh từ desktop vào → hiển thị; build `npm run build` sạch; typecheck.
### W2 — Toàn bộ tool phi cấu trúc ảnh
PRESETS (sim + recipes + create + import), FAVORITED, LIGHT, WB, FX; slider/chip/strip giống app (một row mở tại thời điểm); undo/reset; PEEK; trạng thái lưu localStorage.
Exit: đổi mọi tham số thấy thay đổi tức thì trên preview; reload giữ nguyên trạng thái.
### W3 — FRAME / CROP / ROTATE / WATERMARK / RATIO
Frame 4 loại + wall landscape; free crop kéo rect + crop cố định; rotate/straighten/auto; watermark GPS (nhập tay) + custom text; photo ratio.
Exit: preview đúng layout từng frame (đối chiếu tỉ lệ với app).
### W4 — EXPORT / RENDER full-res
Worker render full-res + thanh tiến trình + hủy; encode JPEG 95 + DPI 300; tải file; thumbnail strip lịch sử; tuỳ chọn batch nhiều ảnh (cùng recipe).
Exit: file tải về mở đúng 300 DPI, kích thước = app cho cùng input.
### W5 — Parity & perf
Bộ ảnh mẫu chạy cả app (native/Skia) và web; so sánh diff + mắt; tối ưu (worker pool, preview cache, tránh decode lại khi export — dùng lại bitmap đã decode).
Exit: diff trong ngưỡng đã chấp nhận ở P6; render 12MP web < ~3s trên server/khách.
### W6 — Docker + deploy + docs
`docker compose up -d --build`; kiểm tra qua cổng server; viết `web/README.md` (RUNBOOK: port, domain, cách đổi); cập nhật `README.md` gốc; commit + push.
Exit: mở webUI từ máy khác trong LAN, chụp màn hình xác nhận; container restart tự lên lại.
## Rủi ro
- **CanvasKit không có emoji màu** → watermark 📍/📷 có thể ra ô vuông; fallback Canvas2D overlay (đã tính ở W0).
- **Kích thước wasm** ~7MB (gzip ~3MB): lazy-load, nginx cache dài hạn.
- **RAM 12MP** trong browser (bitmap ~48MB + surface) — giới hạn pool worker = 1–2, giải phóng bitmap sau export.
- **Skia API shim**: `makeShaderWithChildren`, `MakeMatrixConvolution` signature khác nhau giữa JSI và CanvasKit → W0 chốt.
- **HDF/vignette** dùng gradient + blend (Skia mới thêm) — kiểm tra CanvasKit tương đương.
- Không xoá/đổi code app RN: web chỉ **đọc** `src/utils/*` (nếu cần sửa util để dùng chung, giữ tương thích app và typecheck lại).
- Import TS ngoài root `web/`: cấu hình `server.fs.allow` + alias; Docker context phải là repo root.
## Câu hỏi cần chốt trước khi code
1. Cổng/domain chạy trên server (đề xuất `8090`, đổi qua `docker-compose.yml`)?
2. Có cần đăng nhập / nhiều người dùng không, hay mở nội bộ LAN là đủ?
3. Có cần **batch export** nhiều ảnh cùng recipe ở v1 không?
4. WebUI cần hỗ trợ màn hình điện thoại (responsive) hay chỉ desktop?
5. Ảnh nạp bằng kéo-thả: có cần đọc EXIF GPS của ảnh để tự điền watermark không (v1 đang để nhập tay)?
## Checklist
- [x] Tạo branch `recipes-web`
- [ ] W0 spike CanvasKit (parity + font + perf)
- [ ] W1 scaffold + drag&drop
- [ ] W2 tool phi cấu trúc ảnh
- [ ] W3 frame/crop/rotate/watermark/ratio
- [ ] W4 export/render full-res
- [ ] W5 parity & perf
- [ ] W6 Docker + deploy + docs