fix: fresh export URI per save (stale thumbnail), lock spot-meter + quick EV slider, honor photo GPS over device fallback
This commit is contained in:
@@ -36,6 +36,8 @@ interface ViewfinderProps {
|
||||
cameraPermissionGranted: boolean;
|
||||
onRequestCameraPermission: () => void;
|
||||
photoOutput: CameraPhotoOutput;
|
||||
// Quick exposure slider (shown while AE/AF is locked) — live AE bias in EV.
|
||||
onExposureChange?: (ev: number) => void;
|
||||
}
|
||||
|
||||
const noiseEffect = Skia.RuntimeEffect.Make(`
|
||||
@@ -76,6 +78,7 @@ export default function Viewfinder({
|
||||
cameraPermissionGranted,
|
||||
onRequestCameraPermission,
|
||||
photoOutput,
|
||||
onExposureChange,
|
||||
}: ViewfinderProps) {
|
||||
// Fullscreen hero: the viewfinder fills the entire screen, every overlay
|
||||
// rect derives from window size.
|
||||
@@ -236,6 +239,31 @@ export default function Viewfinder({
|
||||
}
|
||||
};
|
||||
|
||||
// Quick exposure slider — appears while AE/AF is locked. Drag up/down maps
|
||||
// directly to the camera AE bias in EV stops (same path as the recipe EV
|
||||
// slider: Viewfinder already syncs adjustments.exposureCompensation to the
|
||||
// controller via setExposureBias, and captures apply the bias on-device).
|
||||
const EV_RANGE = 3; // matches the ±24 index range at 1 index = 1/8 EV
|
||||
const evTrackTop = vh * 0.24;
|
||||
const evTrackH = vh * 0.42;
|
||||
const evPxPerStop = evTrackH / (2 * EV_RANGE);
|
||||
const clampEv = (v: number) => Math.max(-EV_RANGE, Math.min(EV_RANGE, v));
|
||||
const evDragRef = useRef<{ y0: number; ev0: number } | null>(null);
|
||||
const evThumbY = evTrackTop + ((EV_RANGE - clampEv(evStops)) / (2 * EV_RANGE)) * evTrackH;
|
||||
const onEvSliderGrant = (e: any) => {
|
||||
evDragRef.current = { y0: e.nativeEvent.locationY, ev0: evStops };
|
||||
};
|
||||
const onEvSliderMove = (e: any) => {
|
||||
const g = evDragRef.current;
|
||||
if (!g || !onExposureChange) return;
|
||||
const dy = g.y0 - e.nativeEvent.locationY; // drag up = brighter
|
||||
const ev = clampEv(g.ev0 + dy / evPxPerStop);
|
||||
onExposureChange(Math.round(ev * 10) / 10);
|
||||
};
|
||||
const onEvSliderRelease = () => {
|
||||
evDragRef.current = null;
|
||||
};
|
||||
|
||||
// Instrumentation for device logs (user captures only logcat E level, so
|
||||
// console.log/warn never show up — these are error-level on purpose).
|
||||
const lastPreviewLog = useRef(0);
|
||||
@@ -559,8 +587,11 @@ export default function Viewfinder({
|
||||
className="absolute"
|
||||
style={{ left: focusPoint.x - 26, top: focusPoint.y - 26 }}
|
||||
>
|
||||
<View className="w-[52px] h-[52px] rounded-full border-[1.5px] border-amber-400" />
|
||||
<View className="absolute left-1/2 top-1/2 -ml-px -mt-px w-[2px] h-[2px] rounded-full bg-amber-400" />
|
||||
<View className="w-[52px] h-[52px] rounded-full border-[1.5px] border-amber-400 items-center justify-center">
|
||||
<View className="w-9 h-9 rounded-full bg-amber-400/15 items-center justify-center">
|
||||
<Lock size={14} color="#f59e0b" strokeWidth={2.5} />
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
)}
|
||||
{focusPoint && (
|
||||
@@ -573,6 +604,40 @@ export default function Viewfinder({
|
||||
<Text className="text-amber-400 font-mono text-[10px] font-bold">AE/AF LOCKED — TAP TO UNLOCK</Text>
|
||||
</TouchableOpacity>
|
||||
)}
|
||||
{focusPoint && onExposureChange && (
|
||||
<>
|
||||
{/* EV value readout (left of the slider) */}
|
||||
<View pointerEvents="none" className="absolute" style={{ right: 108, top: evThumbY - 12 }}>
|
||||
<Text className="text-amber-400 font-mono text-xs font-bold bg-black/50 px-1.5 py-0.5 rounded">
|
||||
EV {(evStops > 0 ? '+' : '') + evStops.toFixed(1)}
|
||||
</Text>
|
||||
</View>
|
||||
{/* Vertical track: drag up = brighter, drag down = darker */}
|
||||
<View
|
||||
className="absolute"
|
||||
style={{ left: vw - 56, top: evTrackTop, width: 40, height: evTrackH }}
|
||||
onStartShouldSetResponder={() => true}
|
||||
onMoveShouldSetResponder={() => true}
|
||||
onResponderGrant={onEvSliderGrant}
|
||||
onResponderMove={onEvSliderMove}
|
||||
onResponderRelease={onEvSliderRelease}
|
||||
onResponderTerminate={onEvSliderRelease}
|
||||
>
|
||||
<View pointerEvents="none" className="absolute left-1/2 top-0 bottom-0 w-[2px] -ml-px bg-white/20 rounded-full" />
|
||||
<View
|
||||
pointerEvents="none"
|
||||
className="absolute w-8 h-8 rounded-full border-2 border-amber-400 bg-black/60"
|
||||
style={{ top: evThumbY - evTrackTop - 16, left: 4 }}
|
||||
/>
|
||||
</View>
|
||||
<View pointerEvents="none" className="absolute" style={{ left: vw - 48, top: evTrackTop - 22 }}>
|
||||
<Text className="text-zinc-400 font-mono text-[10px] text-center w-9">+3</Text>
|
||||
</View>
|
||||
<View pointerEvents="none" className="absolute" style={{ left: vw - 48, top: evTrackTop + evTrackH + 6 }}>
|
||||
<Text className="text-zinc-400 font-mono text-[10px] text-center w-9">-3</Text>
|
||||
</View>
|
||||
</>
|
||||
)}
|
||||
{renderCameraOverlays()}
|
||||
</View>
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user