fix: lỗi áp dụng recipe lên live view của camera
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
import React from 'react';
|
||||
import { View, Text, TouchableOpacity, Image } from 'react-native';
|
||||
import { Image as ImageIcon, Eye } from 'lucide-react-native';
|
||||
import { Image as ImageIcon, Eye, Download, Camera } from 'lucide-react-native';
|
||||
|
||||
interface CameraControlsProps {
|
||||
lastPhotoUri: string | null;
|
||||
@@ -8,6 +8,7 @@ interface CameraControlsProps {
|
||||
onPickImage: () => void;
|
||||
onOpenPreview: () => void;
|
||||
isLibraryMode: boolean;
|
||||
onSwitchToCamera?: () => void;
|
||||
}
|
||||
|
||||
export default function CameraControls({
|
||||
@@ -16,6 +17,7 @@ export default function CameraControls({
|
||||
onPickImage,
|
||||
onOpenPreview,
|
||||
isLibraryMode,
|
||||
onSwitchToCamera,
|
||||
}: CameraControlsProps) {
|
||||
return (
|
||||
<View className="flex-row items-center justify-between px-8 py-6 bg-carbon border-t border-zinc-800/50">
|
||||
@@ -28,28 +30,36 @@ export default function CameraControls({
|
||||
<ImageIcon size={20} color="#f59e0b" />
|
||||
</TouchableOpacity>
|
||||
|
||||
{/* Shutter Button */}
|
||||
{/* Dynamic Center Button: Shutter or Save */}
|
||||
<TouchableOpacity
|
||||
onPress={onCapture}
|
||||
disabled={isLibraryMode}
|
||||
className={`w-20 h-20 rounded-full border-4 border-zinc-800 flex items-center justify-center ${
|
||||
isLibraryMode ? 'opacity-40' : 'opacity-100'
|
||||
}`}
|
||||
className="relative items-center justify-center"
|
||||
activeOpacity={0.8}
|
||||
>
|
||||
<View className="w-16 h-16 rounded-full bg-white border border-black flex items-center justify-center active:scale-95">
|
||||
<View className="w-14 h-14 rounded-full bg-zinc-100/90 border border-zinc-200" />
|
||||
</View>
|
||||
{isLibraryMode ? (
|
||||
<View className="w-16 h-16 rounded-full bg-amber-500 items-center justify-center border-4 border-zinc-800">
|
||||
<Download size={28} color="#000000" />
|
||||
</View>
|
||||
) : (
|
||||
<View className="w-20 h-20 rounded-full border-4 border-zinc-800 flex items-center justify-center">
|
||||
<View className="w-16 h-16 rounded-full bg-white border border-black flex items-center justify-center">
|
||||
<View className="w-14 h-14 rounded-full bg-zinc-100/90 border border-zinc-200" />
|
||||
</View>
|
||||
</View>
|
||||
)}
|
||||
</TouchableOpacity>
|
||||
|
||||
{/* Last Captured Image Preview Button */}
|
||||
{/* Right Action Button: Eye (preview) or Camera (switch back) */}
|
||||
<TouchableOpacity
|
||||
onPress={onOpenPreview}
|
||||
disabled={!lastPhotoUri}
|
||||
className="w-12 h-12 rounded-full bg-titan border border-zinc-800 flex items-center justify-center overflow-hidden"
|
||||
onPress={isLibraryMode && onSwitchToCamera ? onSwitchToCamera : onOpenPreview}
|
||||
className={`w-12 h-12 rounded-full bg-titan border border-zinc-800 flex items-center justify-center overflow-hidden ${
|
||||
isLibraryMode ? '' : ''
|
||||
}`}
|
||||
activeOpacity={0.7}
|
||||
>
|
||||
{lastPhotoUri ? (
|
||||
{isLibraryMode ? (
|
||||
<Camera size={20} color="#f59e0b" />
|
||||
) : lastPhotoUri ? (
|
||||
<Image source={{ uri: lastPhotoUri }} className="w-full h-full object-cover" />
|
||||
) : (
|
||||
<Eye size={20} color="#a1a1aa" />
|
||||
|
||||
@@ -1,10 +1,8 @@
|
||||
import React from 'react';
|
||||
import { View, Text, StyleSheet, Dimensions, ActivityIndicator, TouchableOpacity } from 'react-native';
|
||||
import React, { useRef, useEffect, useState, useCallback } from 'react';
|
||||
import { View, Text, StyleSheet, Dimensions, TouchableOpacity } from 'react-native';
|
||||
import { CameraView } from 'expo-camera';
|
||||
import {
|
||||
Canvas,
|
||||
Image as SkiaImage,
|
||||
ColorMatrix,
|
||||
useImage,
|
||||
useFont,
|
||||
Text as SkiaText,
|
||||
@@ -13,6 +11,8 @@ import {
|
||||
Skia,
|
||||
Group,
|
||||
Rect,
|
||||
ColorMatrix,
|
||||
Image as SkiaImage,
|
||||
} from '@shopify/react-native-skia';
|
||||
import { Recipe, GPSInfo, FrameId } from '../types';
|
||||
import { getSkiaColorMatrix } from '../utils/colorUtils';
|
||||
@@ -30,8 +30,8 @@ interface ViewfinderProps {
|
||||
}
|
||||
|
||||
const { width: screenWidth } = Dimensions.get('window');
|
||||
const viewfinderWidth = screenWidth - 32; // padding sides
|
||||
const viewfinderHeight = (viewfinderWidth * 4) / 3; // 3:4 aspect ratio
|
||||
const viewfinderWidth = screenWidth - 32;
|
||||
const viewfinderHeight = (viewfinderWidth * 4) / 3;
|
||||
|
||||
const noiseEffect = Skia.RuntimeEffect.Make(`
|
||||
vec4 main(vec2 pos) {
|
||||
@@ -40,6 +40,8 @@ const noiseEffect = Skia.RuntimeEffect.Make(`
|
||||
}
|
||||
`);
|
||||
|
||||
const VIEWFINDER_OVERLAY_OPACITY = 0.35;
|
||||
|
||||
export default function Viewfinder({
|
||||
mode,
|
||||
recipe,
|
||||
@@ -50,13 +52,13 @@ export default function Viewfinder({
|
||||
cameraPermissionGranted,
|
||||
onRequestCameraPermission,
|
||||
}: ViewfinderProps) {
|
||||
// Load library image if available
|
||||
const cameraRef = useRef(null);
|
||||
|
||||
const skiaImage = useImage(libraryImageUri || '');
|
||||
|
||||
// Load Courier Prime font for the GPS watermark
|
||||
const customFont = useFont(
|
||||
require('../../assets/CourierPrime-Regular.ttf'),
|
||||
Math.round(viewfinderWidth * 0.032) // ~12-14px scaled dynamically
|
||||
Math.round(viewfinderWidth * 0.032)
|
||||
);
|
||||
|
||||
const adjustments = recipe.adjustments;
|
||||
@@ -96,7 +98,6 @@ export default function Viewfinder({
|
||||
<Rect x={0} y={0} width={sideBorder} height={viewfinderHeight} color="#faf9f6" />
|
||||
<Rect x={viewfinderWidth - sideBorder} y={0} width={sideBorder} height={viewfinderHeight} color="#faf9f6" />
|
||||
<Rect x={0} y={viewfinderHeight - bottomBorder} width={viewfinderWidth} height={bottomBorder} color="#faf9f6" />
|
||||
|
||||
<Rect
|
||||
x={sideBorder - 1}
|
||||
y={sideBorder - 1}
|
||||
@@ -160,15 +161,36 @@ export default function Viewfinder({
|
||||
);
|
||||
};
|
||||
|
||||
const renderLiveOverlay = () => {
|
||||
// Shared overlay components for both modes
|
||||
const renderColorMatrixOverlay = (opacity: number = VIEWFINDER_OVERLAY_OPACITY) => (
|
||||
<Canvas style={StyleSheet.absoluteFill} pointerEvents="none">
|
||||
<Group opacity={opacity}>
|
||||
<ColorMatrix matrix={colorMatrix} />
|
||||
<Rect x={0} y={0} width={viewfinderWidth} height={viewfinderHeight} color="white" />
|
||||
</Group>
|
||||
</Canvas>
|
||||
);
|
||||
|
||||
const renderGrainOverlay = () => {
|
||||
if (grainOpacity <= 0 || !noiseEffect) return null;
|
||||
return (
|
||||
<Canvas style={StyleSheet.absoluteFill}>
|
||||
{renderFrameOverlay()}
|
||||
{renderGPSWatermark()}
|
||||
<Canvas style={StyleSheet.absoluteFill} pointerEvents="none">
|
||||
<Group blendMode="overlay">
|
||||
<Rect x={0} y={0} width={viewfinderWidth} height={viewfinderHeight} opacity={grainOpacity}>
|
||||
<Shader source={noiseEffect} />
|
||||
</Rect>
|
||||
</Group>
|
||||
</Canvas>
|
||||
);
|
||||
};
|
||||
|
||||
const renderBorderOverlay = () => (
|
||||
<Canvas style={StyleSheet.absoluteFill} pointerEvents="none">
|
||||
{renderFrameOverlay()}
|
||||
{renderGPSWatermark()}
|
||||
</Canvas>
|
||||
);
|
||||
|
||||
if (mode === 'camera') {
|
||||
if (!cameraPermissionGranted) {
|
||||
return (
|
||||
@@ -195,8 +217,9 @@ export default function Viewfinder({
|
||||
style={{ width: viewfinderWidth, height: viewfinderHeight }}
|
||||
className="relative overflow-hidden rounded-2xl border border-zinc-800 bg-black"
|
||||
>
|
||||
<CameraView style={StyleSheet.absoluteFill} facing="back" />
|
||||
|
||||
<CameraView ref={cameraRef} style={StyleSheet.absoluteFill} facing="back" />
|
||||
|
||||
{/* Grid */}
|
||||
<View className="absolute inset-0 flex-row justify-between pointer-events-none opacity-20">
|
||||
<View className="w-[1px] h-full bg-white ml-[33%]" />
|
||||
<View className="w-[1px] h-full bg-white mr-[33%]" />
|
||||
@@ -206,38 +229,36 @@ export default function Viewfinder({
|
||||
<View className="h-[1px] w-full bg-white mb-[33%]" />
|
||||
</View>
|
||||
|
||||
{renderLiveOverlay()}
|
||||
{/* ColorMatrix overlay shows the recipe effect on camera */}
|
||||
{renderColorMatrixOverlay()}
|
||||
{renderGrainOverlay()}
|
||||
{renderBorderOverlay()}
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
// Library mode
|
||||
return (
|
||||
<View
|
||||
style={{ width: viewfinderWidth, height: viewfinderHeight }}
|
||||
className="relative overflow-hidden rounded-2xl border border-zinc-800 bg-zinc-950 justify-center items-center"
|
||||
>
|
||||
{libraryImageUri && skiaImage ? (
|
||||
<Canvas style={StyleSheet.absoluteFill}>
|
||||
<Group>
|
||||
<ColorMatrix matrix={colorMatrix} />
|
||||
<SkiaImage
|
||||
image={skiaImage}
|
||||
fit="cover"
|
||||
rect={rect(0, 0, viewfinderWidth, viewfinderHeight)}
|
||||
/>
|
||||
</Group>
|
||||
|
||||
{grainOpacity > 0 && noiseEffect && (
|
||||
<Group blendMode="overlay">
|
||||
<Rect x={0} y={0} width={viewfinderWidth} height={viewfinderHeight} opacity={grainOpacity}>
|
||||
<Shader source={noiseEffect} />
|
||||
</Rect>
|
||||
<>
|
||||
<Canvas style={StyleSheet.absoluteFill}>
|
||||
<Group>
|
||||
<ColorMatrix matrix={colorMatrix} />
|
||||
<SkiaImage
|
||||
image={skiaImage}
|
||||
fit="cover"
|
||||
rect={rect(0, 0, viewfinderWidth, viewfinderHeight)}
|
||||
/>
|
||||
</Group>
|
||||
)}
|
||||
|
||||
{renderFrameOverlay()}
|
||||
{renderGPSWatermark()}
|
||||
</Canvas>
|
||||
{renderGrainOverlay()}
|
||||
</Canvas>
|
||||
{/* Border + GPS as separate canvas on top */}
|
||||
{renderBorderOverlay()}
|
||||
</>
|
||||
) : (
|
||||
<View className="items-center justify-center p-6">
|
||||
<Text className="text-zinc-500 font-mono text-center text-xs">
|
||||
|
||||
Reference in New Issue
Block a user