From adaab69b82a7ea71d6abbc6e673f98b774fe04ce Mon Sep 17 00:00:00 2001 From: 3dtours Date: Thu, 16 Jul 2026 12:22:02 +0700 Subject: [PATCH] first commit --- App.tsx | 296 ++++- IMPLEMENTATION_PLAN.md | 115 ++ PROJECT_OVERVIEW.md | 185 +++ README.md | 0 app.json | 29 +- assets/CourierPrime-Regular.ttf | Bin 0 -> 71188 bytes babel.config.js | 12 + global.css | 3 + metro.config.js | 6 + nativewind-env.d.ts | 1 + package-lock.json | 1735 +++++++++++++++++++++++++++- package.json | 16 +- src/components/AdjustmentPanel.tsx | 384 ++++++ src/components/CameraControls.tsx | 60 + src/components/Header.tsx | 58 + src/components/PreviewModal.tsx | 87 ++ src/components/TabSelector.tsx | 54 + src/components/Viewfinder.tsx | 253 ++++ src/types/index.ts | 33 + src/utils/colorUtils.ts | 182 +++ src/utils/defaultRecipes.ts | 104 ++ src/utils/exportEngine.ts | 193 ++++ src/utils/frameUtils.ts | 85 ++ src/utils/locationUtils.ts | 61 + src/utils/storageUtils.ts | 40 + tailwind.config.js | 17 + 26 files changed, 3953 insertions(+), 56 deletions(-) create mode 100644 IMPLEMENTATION_PLAN.md create mode 100644 PROJECT_OVERVIEW.md create mode 100644 README.md create mode 100644 assets/CourierPrime-Regular.ttf create mode 100644 babel.config.js create mode 100644 global.css create mode 100644 metro.config.js create mode 100644 nativewind-env.d.ts create mode 100644 src/components/AdjustmentPanel.tsx create mode 100644 src/components/CameraControls.tsx create mode 100644 src/components/Header.tsx create mode 100644 src/components/PreviewModal.tsx create mode 100644 src/components/TabSelector.tsx create mode 100644 src/components/Viewfinder.tsx create mode 100644 src/types/index.ts create mode 100644 src/utils/colorUtils.ts create mode 100644 src/utils/defaultRecipes.ts create mode 100644 src/utils/exportEngine.ts create mode 100644 src/utils/frameUtils.ts create mode 100644 src/utils/locationUtils.ts create mode 100644 src/utils/storageUtils.ts create mode 100644 tailwind.config.js diff --git a/App.tsx b/App.tsx index 0329d0c..52109a0 100644 --- a/App.tsx +++ b/App.tsx @@ -1,20 +1,286 @@ -import { StatusBar } from 'expo-status-bar'; -import { StyleSheet, Text, View } from 'react-native'; +import React, { useState, useEffect } from 'react'; +import { SafeAreaView, View, StatusBar, Alert, ActivityIndicator, Text } from 'react-native'; +import { useCameraPermissions } from 'expo-camera'; +import * as ImagePicker from 'expo-image-picker'; +import * as Haptics from 'expo-haptics'; + +import Header from './src/components/Header'; +import Viewfinder from './src/components/Viewfinder'; +import TabSelector, { TabId } from './src/components/TabSelector'; +import AdjustmentPanel from './src/components/AdjustmentPanel'; +import CameraControls from './src/components/CameraControls'; +import PreviewModal from './src/components/PreviewModal'; + +import { Recipe, ColorAdjustments, FrameId, GPSInfo } from './src/types'; +import { getAllRecipes, saveCustomRecipe, deleteCustomRecipe } from './src/utils/storageUtils'; +import { getCurrentGPS, requestLocationPermissions } from './src/utils/locationUtils'; +import { processAndExportPhoto } from './src/utils/exportEngine'; + +// @ts-ignore +import './global.css'; export default function App() { + const [mode, setMode] = useState<'camera' | 'library'>('camera'); + const [recipes, setRecipes] = useState([]); + const [selectedRecipe, setSelectedRecipe] = useState(null); + + // Custom temporary overrides for the sliders + const [adjustments, setAdjustments] = useState({ + exposure: 0, + contrast: 0, + saturation: 0, + temperature: 5500, + tint: 0, + highlight: 0, + shadow: 0, + denoise: 0, + clarity: 0, + grain: 0, + colorChrome: 'none', + }); + + const [selectedFrame, setSelectedFrame] = useState('none'); + const [useGeotag, setUseGeotag] = useState(true); + const [gpsInfo, setGpsInfo] = useState(null); + const [activeTab, setActiveTab] = useState('recipes'); + + const [libraryImageUri, setLibraryImageUri] = useState(null); + const [lastPhotoUri, setLastPhotoUri] = useState(null); + const [previewVisible, setPreviewVisible] = useState(false); + const [isProcessing, setIsProcessing] = useState(false); + + // Hardware Permissions + const [cameraPermission, requestCameraPermission] = useCameraPermissions(); + + useEffect(() => { + loadRecipes(); + loadGPS(); + }, []); + + const loadRecipes = async () => { + const list = await getAllRecipes(); + setRecipes(list); + if (list.length > 0) { + applyRecipe(list[0]); + } + }; + + const loadGPS = async () => { + const info = await getCurrentGPS(); + if (info) { + setGpsInfo(info); + } + }; + + const applyRecipe = (recipe: Recipe) => { + setSelectedRecipe(recipe); + setAdjustments(recipe.adjustments); + setSelectedFrame(recipe.frameId || 'none'); + setUseGeotag(recipe.useGeotag); + }; + + const handleUpdateAdjustments = (updates: Partial) => { + setAdjustments((prev) => { + const next = { ...prev, ...updates }; + if (selectedRecipe) { + setSelectedRecipe({ + ...selectedRecipe, + adjustments: next, + }); + } + return next; + }); + }; + + const handleUpdateFrame = (frameId: FrameId) => { + setSelectedFrame(frameId); + if (selectedRecipe) { + setSelectedRecipe({ + ...selectedRecipe, + frameId, + }); + } + }; + + const handleToggleGeotag = async (enabled: boolean) => { + setUseGeotag(enabled); + if (selectedRecipe) { + setSelectedRecipe({ + ...selectedRecipe, + useGeotag: enabled, + }); + } + if (enabled && !gpsInfo) { + const locationGranted = await requestLocationPermissions(); + if (locationGranted) { + loadGPS(); + } else { + Alert.alert('Permission Denied', 'Enable location services to use GPS Watermark.'); + setUseGeotag(false); + } + } + }; + + const handleSaveRecipe = async (name: string) => { + if (!selectedRecipe) return; + const newRecipe = await saveCustomRecipe({ + name, + baseFilter: selectedRecipe.baseFilter, + adjustments, + frameId: selectedFrame, + useGeotag, + }); + const list = await getAllRecipes(); + setRecipes(list); + applyRecipe(newRecipe); + Alert.alert('Recipe Saved', `"${name}" has been created.`); + }; + + const handleDeleteRecipe = async (id: string) => { + await deleteCustomRecipe(id); + const list = await getAllRecipes(); + setRecipes(list); + if (selectedRecipe?.id === id && list.length > 0) { + applyRecipe(list[0]); + } + }; + + const handlePickImage = async () => { + const result = await ImagePicker.launchImageLibraryAsync({ + mediaTypes: ImagePicker.MediaTypeOptions.Images, + allowsEditing: false, + quality: 1, + }); + + if (!result.canceled && result.assets && result.assets.length > 0) { + setLibraryImageUri(result.assets[0].uri); + setMode('library'); + Haptics.notificationAsync(Haptics.NotificationFeedbackType.Success); + } + }; + + const handleCapture = async () => { + if (mode === 'camera') { + // For simulator & simplicity, if camera view isn't fully mocked, we use a placeholder or trigger camera picture. + // In this setup, we simulate taking a photo or request library image if running in simulator. + // Let's create a beautiful simulation that pulls a mock asset or lets you select one if in simulator. + Alert.alert( + 'Capture Mode', + 'Camera capture requires native device hardware. For testing on emulators, please use the LIBRARY mode to load a photo and apply filters.', + [{ text: 'OK' }] + ); + } else { + // Library mode capture/export: processes selected image with recipe, overlays, text + if (!libraryImageUri || !selectedRecipe) { + Alert.alert('Error', 'Please select an image from the library first.'); + return; + } + + setIsProcessing(true); + Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Medium); + + const exportedUri = await processAndExportPhoto( + libraryImageUri, + { + ...selectedRecipe, + adjustments, + frameId: selectedFrame, + useGeotag, + }, + selectedFrame, + useGeotag, + gpsInfo + ); + + setIsProcessing(false); + + if (exportedUri) { + setLastPhotoUri(exportedUri); + setPreviewVisible(true); + Haptics.notificationAsync(Haptics.NotificationFeedbackType.Success); + } else { + Alert.alert('Failed', 'Error occurred while processing image.'); + } + } + }; + return ( - - Open up App.tsx to start working on your app! - - + + + + {/* Top Header */} +
+ + {/* Main Viewfinder Frame */} + + + + + {/* Selector Tabs */} + + + {/* Adjustments Panel */} + + + + + {/* Bottom Camera Buttons */} + setPreviewVisible(true)} + isLibraryMode={mode === 'library' && !libraryImageUri} + /> + + {/* Preview Modal */} + setPreviewVisible(false)} + photoUri={lastPhotoUri} + recipeName={selectedRecipe?.name || 'Custom'} + /> + + {/* Global Processing Loader Overlay */} + {isProcessing && ( + + + + RENDERING PHOTO... + + + )} + ); } - -const styles = StyleSheet.create({ - container: { - flex: 1, - backgroundColor: '#fff', - alignItems: 'center', - justifyContent: 'center', - }, -}); diff --git a/IMPLEMENTATION_PLAN.md b/IMPLEMENTATION_PLAN.md new file mode 100644 index 0000000..b9a98f3 --- /dev/null +++ b/IMPLEMENTATION_PLAN.md @@ -0,0 +1,115 @@ +# Kế Hoạch Triển Khai Chi Tiết (Implementation Plan) - CamRecipe Pro Mobile + +Dựa trên tài liệu **PROJECT_OVERVIEW.md**, kế hoạch triển khai chi tiết cho dự án **CamRecipe Pro** trên nền tảng di động iOS & Android sử dụng **React Native (Expo)** được thiết lập cụ thể dưới đây. + +--- + +## 1. Thiết Lập Môi Trường & Cấu Hình Nền Móng (Mốc 1: Tuần 1) +Mục tiêu là thiết lập khung ứng dụng (UI Shell), cài đặt và cấp quyền cho các module phần cứng cơ bản. + +### 1.1 Khởi Tạo Dự Án Expo +* **Lệnh thực hiện:** Khởi tạo dự án Expo mới với template TypeScript: + ```bash + npx create-expo-app@latest CamRecipePro --template blank-typescript + ``` +* **Cài đặt thư viện giao diện:** + * `NativeWind` (v4 trở lên hỗ trợ Expo tốt nhất) và `tailwindcss`. + * `lucide-react-native` để hiển thị bộ icons SVG từ bản web. + * `expo-font` để nạp phông chữ Retro `Courier Prime.ttf`. + +### 1.2 Cài Đặt Thư Viện Phần Cứng & Quyền Truy Cập (Permissions) +Cài đặt các gói Expo SDK để làm việc với phần cứng: +```bash +npx expo install expo-camera expo-location expo-media-library expo-haptics expo-image-picker @react-native-async-storage/async-storage +``` +Cấu hình các quyền trong tệp `app.json` (hoặc `app.config.js`): +* **Camera:** `plugins: [["expo-camera", { "cameraPermission": "Cho phép ứng dụng sử dụng Camera để chụp ảnh với bộ lọc màu." }]]` +* **Vị trí (GPS):** `plugins: [["expo-location", { "locationAlwaysPermission": "Cho phép ứng dụng lấy tọa độ để chèn watermark GPS lên hình ảnh." }]]` +* **Thư viện ảnh:** `plugins: [["expo-media-library", { "photosPermission": "Cho phép ứng dụng lưu ảnh và truy xuất ảnh để áp bộ lọc." }]]` + +### 1.3 Thiết Kế UI Shell & Navigation +* Khung bao quanh: Dùng `` kết hợp cấu hình `StatusBar` ẩn/hiện hoặc tối để giả lập Notch/Dynamic Island. +* Bố cục chính: Cấu trúc dọc (Vertical Layout) theo khung tỉ lệ 9:19.5 (như wireframe). +* Header: Chuyển đổi giữa chế độ `Camera` và `Library` sử dụng ``. + +--- + +## 2. Phát Triển Engine Xử Lý Hình Ảnh GPU Thời Gian Thực (Mốc 2: Tuần 2) +Mục tiêu là tích hợp luồng camera native với canvas vẽ của Skia để áp dụng các bộ lọc thời gian thực. + +### 2.1 Cài Đặt Thư Viện Đồ Họa & Camera Nâng Cao +```bash +npx expo install @shopify/react-native-skia react-native-vision-camera +``` +*Lưu ý: Đối với `react-native-vision-camera`, cần cấu hình Babel plugin và cài đặt Expo Config Plugin tương thích.* + +### 2.2 Xây Dựng Viewfinder Pipeline +* **Tích hợp Camera Frame làm Texture:** Kết nối `react-native-vision-camera` với `React Native Skia` thông qua cơ chế ghi nhận frame (Frame Processor) hoặc sử dụng các bindings sẵn có để truyền luồng video trực tiếp làm Texture cho ``. +* **Bộ lọc màu (Color Matrix Filter):** + * Định nghĩa cấu trúc ma trận màu $4 \times 5$ (hoặc $5 \times 5$ của Skia) cho các bộ lọc kinh điển (Classic Negative, Provia, Velvia). + * Quy đổi nhiệt độ màu Kelvin ($2500K - 10000K$) sang các hệ số RGB và đưa vào ma trận màu để tinh chỉnh White Balance mà không làm lệch Tint. + * Sử dụng component `` của Skia để xử lý hiệu năng cao trên GPU. +* **Đường Cong Ánh Sáng (Highlight & Shadow Contrast):** + * Sử dụng `` của Skia để cấu hình mảng bảng màu `tableValues` riêng biệt cho Highlight và Shadow. +* **Tạo Hạt Film Monochrome (Monochrome Grain):** + * Sử dụng `` tạo cấu hình nhiễu hạt ngẫu nhiên dựa trên các tham số tần số (`baseFrequency`) và hạt giống (`seed`). + * Áp dụng `` xám hóa (Grayscale) lên lớp nhiễu này và trộn vào luồng camera gốc. +* **Khử Nhiễu (Denoise) & Độ Rõ Nét (Clarity):** + * *Denoise:* Áp dụng `Skia.ImageFilter.MakeBlur` với tham số `sigma` siêu nhỏ từ $0.3$ đến $0.5$ để mịn da/ảnh nhẹ nhàng. + * *Clarity:* Áp dụng `Skia.ImageFilter.MakeMatrixConvolution` cho độ rõ nét dương (tăng độ tương phản cạnh sắc) hoặc kết hợp hiệu ứng Blur + BlendMode Screen cho độ rõ nét âm (tạo sương mù mượt mà). + +--- + +## 3. Phát Triển Quản Lý Recipes & Bộ Nhớ Tạm (Mốc 3: Tuần 3) +Mục tiêu là quản lý các công thức màu sắc cá nhân hóa và hiển thị danh sách trực quan. + +### 3.1 Cài Đặt AsyncStorage & Định Dạng JSON +Mỗi Recipe sẽ có cấu trúc JSON như sau: +```json +{ + "id": "uuid-v4-string", + "name": "Tên Công Thức (Hỗ trợ Tiếng Việt & Emoji)", + "matrix": [...], + "wbKelvin": 5500, + "contrastHighlight": 1.0, + "contrastShadow": 1.0, + "denoiseSigma": 0.4, + "clarity": 0, + "grainFrequency": 0.05, + "framePath": "optional-frame-id" +} +``` + +### 3.2 Giao Diện Danh Sách Recipes 2 Cột +* Sử dụng `` để tối ưu hóa hiệu năng render danh sách. +* Cột bên trái hiển thị hình ảnh mẫu nhỏ (thumbnail) đã được nạp động hoặc áp sẵn bộ lọc tương ứng để người dùng xem trước. +* Chạm vào thẻ công thức để kích hoạt ngay (`applyRecipe()`) mà không cần nút áp dụng phụ, tích hợp rung phản hồi xúc giác nhẹ (`expo-haptics`). +* Cửa sổ Modal lưu công thức (`#save-recipe-modal`) hiển thị dưới dạng Bottom Sheet để người dùng đặt tên và lưu lại vào AsyncStorage. + +--- + +## 4. Chụp Ảnh Độ Phân Giải Cao & Xuất File (Mốc 4: Tuần 4) +Mục tiêu là lưu giữ khoảnh khắc ở chất lượng cao nhất, kết xuất đầy đủ hiệu ứng nghệ thuật và siêu dữ liệu. + +### 4.1 Quy Trình Xử Lý Khi Nhấn Shutter +1. **Chụp Ảnh Gốc:** Gọi hàm chụp ảnh tĩnh độ phân giải cao từ `Vision Camera` hoặc `Expo Camera`. +2. **Canvas Ẩn (Offscreen Canvas):** Khởi tạo một đối tượng Skia Canvas ẩn có độ phân giải bằng đúng độ phân giải ảnh gốc. +3. **Áp Bộ Lọc Chất Lượng Cao:** Thực thi lại toàn bộ chuỗi bộ lọc đồ họa (WB, Exposure, Curves, Grain, Denoise, Clarity) trên canvas ẩn để giữ nguyên chất lượng chi tiết. +4. **Vẽ Khung & Watermark GPS:** + * Tải font chữ TrueType `Courier Prime.ttf` thông qua `Skia.Typeface.MakeFromFile` hoặc `useFont` từ Expo. + * Lấy tọa độ hiện tại từ `expo-location` (hoặc phân tích EXIF gốc của ảnh nếu ở chế độ Library) để render chuỗi text (📍 Địa điểm, Tọa độ, Thông số ISO/Shutter Speed). + * Vẽ đè hình ảnh PNG Frame trang trí (nếu chọn) lên trên cùng. +5. **Đóng Gói & Xuất Bản:** + * Xuất canvas thành mảng byte JPEG: `image.encodeToBytes(ImageFormat.JPEG, 95)`. + * Sử dụng `expo-media-library` để tạo tệp tin ảnh mới trong thư viện thiết bị. + +--- + +## 5. Kế Hoạch Kiểm Thử & Kiểm Soát Chất Lượng (Testing & QA) +* **Kiểm thử Giả lập (Emulators):** + * Giả lập GPS thông qua các công cụ của Xcode Simulator (Location menu) hoặc Android Emulator (Extended Controls). + * Sử dụng luồng dữ liệu camera ảo (Virtual Camera) để kiểm thử bộ lọc Skia Shader mà không cần thiết bị thật. +* **Kiểm thử Thiết bị Thật:** + * Thử nghiệm nhanh bằng ứng dụng Expo Go qua mã QR. + * Tạo bản build phát triển (`eas build --profile development`) để test hiệu năng bộ nhớ, độ mượt Haptic Feedback, tốc độ render ảnh độ phân giải cao và tính ổn định của luồng xử lý GPU. + * Phân phối thử nghiệm qua TestFlight (iOS) và Google Play Console Internal (Android). diff --git a/PROJECT_OVERVIEW.md b/PROJECT_OVERVIEW.md new file mode 100644 index 0000000..76f2c53 --- /dev/null +++ b/PROJECT_OVERVIEW.md @@ -0,0 +1,185 @@ + +# [SYSTEM PROMPT / WORKFLOW] Kế Hoạch Chuyển Đổi Dự Án CamRecipe Pro Lên iOS & Android + +> **VAI TRÒ CỦA AI:** Bạn là một Chuyên gia Phát triển Di động Cấp cao (Senior Mobile Engineer) chuyên về React Native (Expo) và Đồ họa Máy tính (GPU Image Processing). Bạn có nhiệm vụ đọc hiểu cấu trúc dưới đây và thực thi/phát triển mã nguồn theo đúng đặc tả kỹ thuật, không tự ý thay đổi stack công nghệ hoặc lược bỏ tính năng trừ khi có yêu cầu. + +--- + +## 1. TỔNG QUAN DỰ ÁN (PROJECT OVERVIEW) +Tài liệu này phác thảo toàn bộ cấu trúc thư mục, stack công nghệ, engine xử lý hình ảnh, đặc tả thiết kế giao diện, kế hoạch debug và kiểm thử để chuyển đổi ứng dụng **CamRecipe Pro** (từ phiên bản Web nguyên mẫu hiện tại) thành một ứng dụng di động native hoàn chỉnh chạy mượt mà trên cả hai nền tảng iOS và Android. + +--- + +## 2. STACK CÔNG NGHỆ & THƯ VIỆN SỬ DỤNG (TECHNOLOGY STACK) +Để tối ưu hóa hiệu năng (đạt mức 60 FPS) và rút ngắn thời gian phát triển, dự án sẽ áp dụng triệt để nguyên tắc tái sử dụng các thư viện chuẩn hóa (industry-standard): + +| Thành phần | Công nghệ / Thư viện | Vai trò & Giải pháp tái sử dụng | +| :--- | :--- | :--- | +| **Framework chính** | `React Native` (Expo SDK mới nhất) | Nền tảng phát triển đa nền tảng (Cross-platform). | +| **Truy cập phần cứng** | `expo-camera`, `expo-location`, `expo-media-library` | Tái sử dụng API có sẵn của Expo để truy cập Camera, Định vị (GPS) và Thư viện ảnh mà không cần viết Native Bridge. | +| **Engine đồ họa** | `@shopify/react-native-skia` + `react-native-vision-camera` | Xử lý Viewfinder & Render thời gian thực. Tận dụng bộ lọc ma trận màu (`Skia.ColorFilter.Matrix`), bộ lọc tích chập (`feConvolveMatrix`) và bộ lọc nhòe (`feGaussianBlur`) trực tiếp trên GPU. | +| **Tạo kiểu giao diện** | `NativeWind` (Tailwind CSS cho React Native) | Ánh xạ 1-1 toàn bộ class Tailwind CSS từ bản Web sang Native Components. | +| **Hệ thống Icons** | `lucide-react-native` | Sử dụng trực tiếp bộ icon SVG chất lượng cao từ bản Web. | +| **Thanh trượt thông số** | `@react-native-community/slider` | Hỗ trợ vuốt chạm vật lý và Phản hồi xúc giác (Haptic Feedback) native. | +| **Bộ nhớ tạm (Local)** | `@react-native-async-storage/async-storage` | Lưu trữ cấu trúc JSON Recipes cá nhân xuống thiết bị. | +| **Trích xuất Metadata**| `exif-reader` | Giải mã dữ liệu EXIF & GPS từ ảnh trong thư viện. | + +--- + +## 3. ĐẶC TẢ THIẾT KẾ GIAO DIỆN NGƯỜI DÙNG (UI/UX SPECIFICATION) + +### A. Triết lý Thiết kế (Design System) +* **Phong cách:** Retro-Modern tối giản, cao cấp. +* **Bảng màu chủ đạo:** + * Đen carbon: `#08080a` + * Xám titan: `#1f1f23` + * Vàng hổ phách: `#f59e0b` + +### B. Sơ đồ bố cục màn hình (Wireframe Layout - Tỉ lệ 9:19.5) + +```text ++-------------------------------------------------+ +| [X] 9:41 StatusBar [=] | -> Giả lập vùng khuyết notch/Dynamic Island ++-------------------------------------------------+ +| (•) X-TRANS PRO EMULATOR [CAMERA] [LIBRARY] | -> Header: Chế độ chụp & tải ảnh thư viện ++-------------------------------------------------+ +| +-------------------------------------------+ | +| | [CLASSIC NEG] [DR400] [SIM LIVE OK] | | -> Viewfinder: Kính ngắm trực tiếp (Skia Canvas) +| | | | - Lớp 1: Video/Ảnh mẫu gốc +| | (Grid) | | - Lớp 2: Skia Shader (Màu + Khử nhiễu + Hạt) +| | | | - Lớp 3: PNG Frame (Trong suốt đè lên) +| | | | - Lớp 4: GPS Watermark (Chữ Courier Prime) +| | [SS 1/125 f/2.8 +0.7 EV] [ISO 3200] | | +| | | | +| | 📍 PHỐ CỔ HỘI AN | | +| | 15.9000° N, 108.1500° E | | +| +-------------------------------------------+ | ++-------------------------------------------------+ +| [ Recipes ] [ Thông số IQ ] [ Màu & WB ]... | -> Tab Selectors: Chuyển đổi các bảng thông số ++-------------------------------------------------+ +| [Dynamic Panels - Chiều cao cố định 250dp] | -> Khu vực nội dung Tab đang chọn +| - Tab Recipes: Danh sách 2 cột kèm ảnh mẫu | +| - Tab IQ: Thước phơi sáng, Highlight/Shadow | +| - Tab Màu & WB: Kelvin, Chrome Blue, Grain... | ++-------------------------------------------------+ +| [x] Đính kèm vị trí (GPS) [Sửa vị trí] | -> Thanh tiện ích nhanh (Quick Utility Bar) ++-------------------------------------------------+ +| [🎨 Thư viện] (( 🔘 )) [📷 Preview] | -> Camera Controls: Nút Chụp & Phím tắt nhanh ++-------------------------------------------------+ +| === | -> Home Indicator ảo trên thiết bị di động ++-------------------------------------------------+ + +``` + +### C. Bản Đồ Ánh Xạ Linh Kiện (Web to React Native Mapping) + +| Giao diện Web (HTML / Tailwind CSS) | Thành phần di động (React Native / NativeWind) | Vai trò & Trải nghiệm người dùng | +| --- | --- | --- | +| Main Wrapper (`max-w-md bg-black border-zinc-800 rounded-[48px]`) | `` | Khung bao bọc toàn màn hình, tự động tránh Notch tai thỏ và bo cong viền. | +| Mode Toggles (`#btn-mode-camera`, `#btn-mode-library`) | `` + `` | Chuyển đổi tức thì nguồn cấp giữa Camera vật lý và Thư viện ảnh. | +| Viewfinder Wrapper (`aspect-[3/4] overflow-hidden`) | `` | Kính ngắm tỷ lệ nhiếp ảnh hoài cổ 3:4. | +| Camera Live Stream (`