Fix camera polaroid overflow and chrome WB chips; move GPS watermark to top
This commit is contained in:
@@ -1,5 +1,6 @@
|
||||
import React, { useCallback, useEffect, useMemo, useRef, useState, type ComponentProps } from 'react';
|
||||
import { View, Text, StyleSheet, TouchableOpacity, useWindowDimensions, AppState } from 'react-native';
|
||||
import { useSafeAreaInsets } from 'react-native-safe-area-context';
|
||||
import { useCameraDevice, type CameraPhotoOutput } from 'react-native-vision-camera';
|
||||
import { SkiaCamera, type SkiaCameraRef } from 'react-native-vision-camera-skia';
|
||||
import { createSynchronizable } from 'react-native-worklets';
|
||||
@@ -93,6 +94,7 @@ export default function Viewfinder({
|
||||
// Fullscreen hero: the viewfinder fills the entire screen, every overlay
|
||||
// rect derives from window size.
|
||||
const { width: vw, height: vh } = useWindowDimensions();
|
||||
const insets = useSafeAreaInsets();
|
||||
|
||||
// useCameraDevice resolves the back camera fresh on every render, so once the
|
||||
// permission lands (first-run flow: GRANT button → OS dialog) a re-render is
|
||||
@@ -315,11 +317,12 @@ export default function Viewfinder({
|
||||
// the preview flicker. All live values flow through the synchronizables.
|
||||
type SkiaCameraOnFrame = NonNullable<ComponentProps<typeof SkiaCamera>['onFrame']>;
|
||||
const toneFailLogged = useMemo(() => createSynchronizable<boolean>(false), []);
|
||||
// RETRO POLAROID on the live camera: the card + photo window are drawn here,
|
||||
// in the frame pipeline, because only the worklet can scale the camera feed
|
||||
// into the polaroid window (a JS overlay could only mask the fullscreen feed,
|
||||
// which crops the scene instead of fitting it into the frame).
|
||||
const polaroidCam = mode === 'camera' && selectedFrame === 'polaroid';
|
||||
// RETRO POLAROID on the live camera is NOT drawn in the frame pipeline: that
|
||||
// canvas is an offscreen surface in frame-pixel space which RNSkia later
|
||||
// cover-fits onto the view, so frame-space geometry overflows the screen and
|
||||
// cannot be aligned with the dp-space overlays. The camera just renders the
|
||||
// plain fullscreen feed here; the polaroid card is composited by the JS
|
||||
// overlay (renderFrameOverlay) in view coordinates, like every other frame.
|
||||
const handleFrame = useCallback<SkiaCameraOnFrame>(
|
||||
(frame, render) => {
|
||||
'worklet';
|
||||
@@ -361,57 +364,11 @@ export default function Viewfinder({
|
||||
}
|
||||
canvas.drawImage(frameTexture, 0, 0, paint);
|
||||
};
|
||||
if (polaroidCam) {
|
||||
const W = frameTexture.width();
|
||||
const H = frameTexture.height();
|
||||
// Geometry mirror of utils/frameUtils.polaroidLayout('contain'),
|
||||
// inlined because worklet code cannot call across module imports.
|
||||
// Physical polaroid proportions (88x107 mm card, 79x79 mm print).
|
||||
const SIDE = (88 - 79) / 2 / 88; // 0.05114 per side of card width
|
||||
const DECK = (107 - 79 - (88 - 79) / 2) / 88; // 0.26705 bottom deck
|
||||
const WINW = 1 - 2 * SIDE; // 0.89773 window width
|
||||
const naturalAspect = SIDE + DECK + WINW / (W / H); // cardH / cardW
|
||||
const cardW = Math.min(W, H / naturalAspect);
|
||||
const cardH = cardW * naturalAspect;
|
||||
const cx = (W - cardW) / 2;
|
||||
const cy = (H - cardH) / 2;
|
||||
const top = SIDE * cardW;
|
||||
const maxWinH = cardH - top - DECK * cardW;
|
||||
let winW = WINW * cardW;
|
||||
let winH = winW / (W / H);
|
||||
if (winH > maxWinH) {
|
||||
winH = maxWinH;
|
||||
winW = winH * (W / H);
|
||||
}
|
||||
const wx = cx + (cardW - winW) / 2;
|
||||
const wy = cy + top;
|
||||
// Letterbox bars (black) around the print, then the white card.
|
||||
const bgPaint = Skia.Paint();
|
||||
bgPaint.setColor(Skia.Color('#000000'));
|
||||
canvas.drawRect(Skia.XYWHRect(0, 0, W, H), bgPaint);
|
||||
const cardPaint = Skia.Paint();
|
||||
cardPaint.setColor(Skia.Color('#faf9f6'));
|
||||
canvas.drawRect(Skia.XYWHRect(cx, cy, cardW, cardH), cardPaint);
|
||||
// Scale the whole live frame into the window: draw the pass over the
|
||||
// full frame rect while the canvas is scaled, so the complete camera
|
||||
// scene is squeezed into the polaroid window — never cropped.
|
||||
canvas.save();
|
||||
canvas.translate(wx, wy);
|
||||
canvas.scale(winW / W, winH / H);
|
||||
drawPass();
|
||||
canvas.restore();
|
||||
const seamPaint = Skia.Paint();
|
||||
seamPaint.setColor(Skia.Color('#e5e5e5'));
|
||||
seamPaint.setStyle(1);
|
||||
seamPaint.setStrokeWidth(1);
|
||||
canvas.drawRect(Skia.XYWHRect(wx + 0.5, wy + 0.5, winW - 1, winH - 1), seamPaint);
|
||||
return;
|
||||
}
|
||||
drawPass();
|
||||
});
|
||||
frame.dispose();
|
||||
},
|
||||
[colorMatrixSync, toneSync, cinemaSync, toneFailLogged, polaroidCam]
|
||||
[colorMatrixSync, toneSync, cinemaSync, toneFailLogged]
|
||||
);
|
||||
|
||||
const grainOpacity = adjustments.grain / 20;
|
||||
@@ -437,9 +394,9 @@ export default function Viewfinder({
|
||||
const dh = h * s;
|
||||
return { dx: (vw - dw) / 2, dy: (vh - dh) / 2, dw, dh };
|
||||
}, [skiaImage, vw, vh]);
|
||||
// Polaroid card geometry (JS/overlay space). The live camera worklet mirrors
|
||||
// the same math in frame-pixel space (see handleFrame) — both spaces share the
|
||||
// same aspect ratio, so the overlay rects line up with the drawn card.
|
||||
// Polaroid card geometry (JS/overlay space) for the CURRENT mode. Library
|
||||
// stills keep the photo aspect; camera previews use the screen aspect (the
|
||||
// feed fills the screen, so the card overlays it in view coordinates).
|
||||
const polaroidGeo = useMemo(() => {
|
||||
if (selectedFrame !== 'polaroid') return null;
|
||||
const aspect =
|
||||
@@ -540,10 +497,47 @@ export default function Viewfinder({
|
||||
|
||||
const renderFrameOverlay = () => {
|
||||
if (selectedFrame === 'none') return null;
|
||||
// Polaroid is drawn by the camera frame pipeline (camera) or its own
|
||||
// contain-fit canvas (library) — the overlay only handles the frames that
|
||||
// sit on top of an already fullscreen picture.
|
||||
if (selectedFrame === 'polaroid') return null;
|
||||
// Library polaroid is drawn by its own contain-fit canvas (see the early
|
||||
// return further down); the overlay here handles the frames that sit on top
|
||||
// of an already fullscreen picture.
|
||||
if (selectedFrame === 'polaroid') {
|
||||
const geo = polaroidGeo;
|
||||
if (!geo) return null;
|
||||
const { card, window: win } = geo;
|
||||
// Camera polaroid, composited in view (dp) coordinates: letterbox the
|
||||
// live feed black outside the card, paint the white card around the photo
|
||||
// window and a thin seam at the window edge. The window itself stays
|
||||
// transparent so the fullscreen feed shows through underneath — drawn
|
||||
// this way the card can never overflow the screen, and it lines up
|
||||
// exactly with the grain clip / GPS anchor (same polaroidGeo rects).
|
||||
const black = '#000000';
|
||||
const white = '#faf9f6';
|
||||
const seam = '#e5e5e5';
|
||||
return (
|
||||
<Group>
|
||||
{/* black letterbox bars around the card */}
|
||||
<Rect x={0} y={0} width={vw} height={card.y} color={black} />
|
||||
<Rect x={0} y={card.y + card.h} width={vw} height={vh - card.y - card.h} color={black} />
|
||||
<Rect x={0} y={card.y} width={card.x} height={card.h} color={black} />
|
||||
<Rect x={card.x + card.w} y={card.y} width={vw - card.x - card.w} height={card.h} color={black} />
|
||||
{/* white card area around the photo window */}
|
||||
<Rect x={card.x} y={card.y} width={card.w} height={win.y - card.y} color={white} />
|
||||
<Rect x={card.x} y={win.y + win.h} width={card.w} height={card.y + card.h - win.y - win.h} color={white} />
|
||||
<Rect x={card.x} y={win.y} width={win.x - card.x} height={win.h} color={white} />
|
||||
<Rect x={win.x + win.w} y={win.y} width={card.x + card.w - win.x - win.w} height={win.h} color={white} />
|
||||
{/* seam between photo window and card */}
|
||||
<Rect
|
||||
x={win.x + 0.5}
|
||||
y={win.y + 0.5}
|
||||
width={win.w - 1}
|
||||
height={win.h - 1}
|
||||
color={seam}
|
||||
style="stroke"
|
||||
strokeWidth={1}
|
||||
/>
|
||||
</Group>
|
||||
);
|
||||
}
|
||||
|
||||
if (selectedFrame === 'classic-white') {
|
||||
return (
|
||||
@@ -576,13 +570,14 @@ export default function Viewfinder({
|
||||
const pad = (n: number) => String(n).padStart(2, '0');
|
||||
const timestampStr = `${pad(d.getDate())}/${pad(d.getMonth() + 1)}/${d.getFullYear()} ${pad(d.getHours())}:${pad(d.getMinutes())}`;
|
||||
|
||||
// Anchor to the PHOTO, not the screen. In camera mode the photo fills the
|
||||
// screen (area = full canvas). In library mode the photo is contain-fitted
|
||||
// with black letterbox — a screen-bottom baseline used to land below the
|
||||
// photo on the letterbox, off the image, so users could not see it. Compute
|
||||
// inside the fitted rect (imageFitRect) so the mark sits at the photo's
|
||||
// bottom-left corner; keep it above the bottom chrome/shutter rail. With a
|
||||
// polaroid frame the photo lives inside the card window — anchor to that.
|
||||
// Anchor to the PHOTO's top-left, not the screen. Bottom anchoring put the
|
||||
// mark under the floating chip rows (open WB/FRAME panel covered it), so it
|
||||
// now lives at the top of the photo/window: the bottom sheet can never reach
|
||||
// it. In camera mode the photo fills the screen; in library mode it is
|
||||
// contain-fitted with letterbox — use imageFitRect so the mark sits on the
|
||||
// photo corner. With a polaroid frame the photo lives inside the card
|
||||
// window — anchor to that. Always clear the floating TopBar (recipe name +
|
||||
// SAVE), which ends around insets.top + 56 dp.
|
||||
const win = polaroidGeo?.window ?? null;
|
||||
const area = win
|
||||
? { dx: win.x, dy: win.y, dw: win.w, dh: win.h }
|
||||
@@ -590,17 +585,13 @@ export default function Viewfinder({
|
||||
? imageFitRect
|
||||
: { dx: 0, dy: 0, dw: vw, dh: vh };
|
||||
const iconSize = Math.round(vw * 0.032); // matches customFont/iconFont size
|
||||
const padBottom = win
|
||||
? Math.round(iconSize * 1.4) // polaroid: just above the window's bottom edge
|
||||
: mode === 'camera'
|
||||
? Math.min(150, area.dh * 0.2) // keep clear of the bottom shutter deck
|
||||
: Math.round(iconSize * 1.5); // library: sit at the photo corner
|
||||
const yOffset =
|
||||
mode === 'library'
|
||||
? Math.min(area.dy + area.dh - padBottom, vh - 190) // never under the deck
|
||||
: area.dy + area.dh - padBottom;
|
||||
const topBarBottom = insets.top + 56;
|
||||
const topBase = Math.max(area.dy + Math.round(iconSize * 0.6), topBarBottom);
|
||||
const xOffset =
|
||||
area.dx + (win ? area.dw * 0.05 : Math.min(16, area.dw * 0.04));
|
||||
// Two stacked lines (pin/location above camera/timestamp).
|
||||
const row1Y = topBase + Math.round(iconSize * 1.1);
|
||||
const row2Y = topBase + Math.round(iconSize * 2.3);
|
||||
// One clear space (0.6em ≈ a mono space) between each icon and its text.
|
||||
const gap = (w: number) => Math.round(Math.max(w, iconSize) + iconSize * 0.6);
|
||||
const pinW = iconFont.measureText('📍');
|
||||
@@ -609,10 +600,10 @@ export default function Viewfinder({
|
||||
return (
|
||||
<Group>
|
||||
{/* Icons white so they read as glyphs, not part of the amber text */}
|
||||
<SkiaText x={xOffset} y={yOffset - 16} text="📍" font={iconFont} color="#ffffff" />
|
||||
<SkiaText x={xOffset + gap(pinW.width)} y={yOffset - 16} text={locationName} font={customFont} color="#f59e0b" />
|
||||
<SkiaText x={xOffset} y={yOffset} text="📷" font={iconFont} color="#ffffff" />
|
||||
<SkiaText x={xOffset + gap(camW.width)} y={yOffset} text={timestampStr} font={customFont} color="#f59e0b" />
|
||||
<SkiaText x={xOffset} y={row1Y} text="📍" font={iconFont} color="#ffffff" />
|
||||
<SkiaText x={xOffset + gap(pinW.width)} y={row1Y} text={locationName} font={customFont} color="#f59e0b" />
|
||||
<SkiaText x={xOffset} y={row2Y} text="📷" font={iconFont} color="#ffffff" />
|
||||
<SkiaText x={xOffset + gap(camW.width)} y={row2Y} text={timestampStr} font={customFont} color="#f59e0b" />
|
||||
</Group>
|
||||
);
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user