diff --git a/WEB_PLAN.md b/WEB_PLAN.md new file mode 100644 index 0000000..2949881 --- /dev/null +++ b/WEB_PLAN.md @@ -0,0 +1,134 @@ +# 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__.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