Add wall frame overlay with transparent PNG window
This commit is contained in:
@@ -26,7 +26,7 @@ import { Recipe, GPSInfo, FrameId } from '../types';
|
||||
import { getSkiaColorMatrix, applyExposureGain } from '../utils/colorUtils';
|
||||
import { TONE_SKSL, getToneUniforms } from '../utils/toneShader';
|
||||
import { CINEMA_SKSL, getCinemaUniforms, cinemaOffFlat } from '../utils/cinemaShader';
|
||||
import { polaroidLayout } from '../utils/frameUtils';
|
||||
import { polaroidLayout, wallframeLayout } from '../utils/frameUtils';
|
||||
|
||||
interface ViewfinderProps {
|
||||
mode: 'camera' | 'library';
|
||||
@@ -102,6 +102,9 @@ export default function Viewfinder({
|
||||
const device = useCameraDevice('back');
|
||||
|
||||
const skiaImage = useImage(libraryImageUri || '');
|
||||
// Wall-frame PNG artwork (project root). Loaded lazily so other frames pay
|
||||
// no asset cost; source switches to '' when unselected (useImage → null).
|
||||
const wallImage = useImage(selectedFrame === 'wallframe' ? require('../../wallframe.png') : '');
|
||||
const customFont = useFont(
|
||||
require('../../assets/Cousine-Regular.ttf'),
|
||||
Math.round(vw * 0.032)
|
||||
@@ -409,6 +412,13 @@ export default function Viewfinder({
|
||||
: 3 / 4;
|
||||
return polaroidLayout(vw, vh, aspect, 'contain');
|
||||
}, [selectedFrame, mode, skiaImage, vw, vh]);
|
||||
// Wall-frame PNG geometry (JS/overlay space). Fixed landscape 3:2 window —
|
||||
// the PNG is contain-fitted like a physical frame; photo content behind it
|
||||
// is cover-cropped by the mat opening (never letterboxed inside the window).
|
||||
const wallGeo = useMemo(() => {
|
||||
if (selectedFrame !== 'wallframe') return null;
|
||||
return wallframeLayout(vw, vh, 'contain');
|
||||
}, [selectedFrame, vw, vh]);
|
||||
const onLibTouchStart = (e: any) => {
|
||||
const ts = e.nativeEvent.touches;
|
||||
if (ts.length >= 2) {
|
||||
@@ -543,6 +553,25 @@ export default function Viewfinder({
|
||||
);
|
||||
}
|
||||
|
||||
if (selectedFrame === 'wallframe') {
|
||||
const geo = wallGeo;
|
||||
if (!geo || !wallImage) return null; // PNG still loading → bare feed
|
||||
const { frame } = geo;
|
||||
// Letterbox the live feed black outside the PNG, then draw the artwork:
|
||||
// its transparent window lets the fullscreen feed show through (the mat
|
||||
// opening = center crop of the feed, WYSIWYG with the export crop).
|
||||
const black = '#000000';
|
||||
return (
|
||||
<Group>
|
||||
<Rect x={0} y={0} width={vw} height={frame.y} color={black} />
|
||||
<Rect x={0} y={frame.y + frame.h} width={vw} height={vh - frame.y - frame.h} color={black} />
|
||||
<Rect x={0} y={frame.y} width={frame.x} height={frame.h} color={black} />
|
||||
<Rect x={frame.x + frame.w} y={frame.y} width={vw - frame.x - frame.w} height={frame.h} color={black} />
|
||||
<SkiaImage image={wallImage} x={frame.x} y={frame.y} width={frame.w} height={frame.h} fit="fill" />
|
||||
</Group>
|
||||
);
|
||||
}
|
||||
|
||||
if (selectedFrame === 'classic-white') {
|
||||
return (
|
||||
<Group color="white">
|
||||
@@ -582,7 +611,7 @@ export default function Viewfinder({
|
||||
// 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 win = polaroidGeo?.window ?? wallGeo?.window ?? null;
|
||||
const area = win
|
||||
? { dx: win.x, dy: win.y, dw: win.w, dh: win.h }
|
||||
: mode === 'library' && imageFitRect
|
||||
@@ -612,18 +641,20 @@ export default function Viewfinder({
|
||||
);
|
||||
};
|
||||
|
||||
const renderCameraOverlays = () => (
|
||||
const renderCameraOverlays = () => {
|
||||
const framedWin = polaroidGeo?.window ?? wallGeo?.window ?? null;
|
||||
return (
|
||||
<>
|
||||
<Canvas style={StyleSheet.absoluteFill} pointerEvents="none">
|
||||
{grainOpacity > 0 && noiseEffect && (
|
||||
<Group blendMode="overlay">
|
||||
{/* Grain belongs to the photo; on a polaroid card it must not spill
|
||||
onto the white deck, so it is clipped to the photo window. */}
|
||||
{/* Grain belongs to the photo; on a framed photo it must not spill
|
||||
onto the mat, so it is clipped to the photo window. */}
|
||||
<Rect
|
||||
x={polaroidGeo ? polaroidGeo.window.x : 0}
|
||||
y={polaroidGeo ? polaroidGeo.window.y : 0}
|
||||
width={polaroidGeo ? polaroidGeo.window.w : vw}
|
||||
height={polaroidGeo ? polaroidGeo.window.h : vh}
|
||||
x={framedWin ? framedWin.x : 0}
|
||||
y={framedWin ? framedWin.y : 0}
|
||||
width={framedWin ? framedWin.w : vw}
|
||||
height={framedWin ? framedWin.h : vh}
|
||||
opacity={grainOpacity}
|
||||
>
|
||||
<Shader source={noiseEffect} />
|
||||
@@ -644,6 +675,7 @@ export default function Viewfinder({
|
||||
</View>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
if (mode === 'camera') {
|
||||
if (!cameraPermissionGranted) {
|
||||
@@ -824,6 +856,65 @@ export default function Viewfinder({
|
||||
);
|
||||
}
|
||||
|
||||
// WALL FRAME (library): the photo is cover-cropped (center) into the PNG's
|
||||
// fixed 3:2 window — the mat opening of a real frame hides the print edges.
|
||||
// The window never letterboxes a mismatched aspect; the artwork PNG is drawn
|
||||
// over the crop afterwards (its transparent window reveals the photo).
|
||||
if (mode === 'library' && wallGeo && wallImage && skiaImage && libraryImageUri) {
|
||||
const win = wallGeo.window;
|
||||
const frame = wallGeo.frame;
|
||||
const winRect = rect(win.x, win.y, win.w, win.h);
|
||||
return (
|
||||
<View className="flex-1 relative overflow-hidden bg-black items-center justify-center">
|
||||
<Canvas style={StyleSheet.absoluteFill}>
|
||||
<Rect x={0} y={0} width={vw} height={vh} color="#000000" />
|
||||
<Group>
|
||||
<ColorMatrix matrix={colorMatrix} />
|
||||
{useShaderPass ? (
|
||||
<Rect x={win.x} y={win.y} width={win.w} height={win.h}>
|
||||
{cinemaOn ? (
|
||||
<Shader source={cinemaEffect!} uniforms={cinemaParams!.uniforms}>
|
||||
{toneEffect && toneOn ? (
|
||||
<Shader
|
||||
source={toneEffect}
|
||||
uniforms={{ dr: toneParams[0], hl: toneParams[1], sh: toneParams[2] }}
|
||||
>
|
||||
<ImageShader image={skiaImage} fit="cover" tx="clamp" ty="clamp" rect={winRect} />
|
||||
</Shader>
|
||||
) : (
|
||||
<ImageShader image={skiaImage} fit="cover" tx="clamp" ty="clamp" rect={winRect} />
|
||||
)}
|
||||
</Shader>
|
||||
) : toneEffect && toneOn ? (
|
||||
<Shader
|
||||
source={toneEffect}
|
||||
uniforms={{ dr: toneParams[0], hl: toneParams[1], sh: toneParams[2] }}
|
||||
>
|
||||
<ImageShader image={skiaImage} fit="cover" tx="clamp" ty="clamp" rect={winRect} />
|
||||
</Shader>
|
||||
) : null}
|
||||
</Rect>
|
||||
) : (
|
||||
<SkiaImage image={skiaImage} fit="cover" rect={winRect} />
|
||||
)}
|
||||
</Group>
|
||||
{grainOpacity > 0 && noiseEffect && (
|
||||
<Group blendMode="overlay">
|
||||
<Rect x={win.x} y={win.y} width={win.w} height={win.h} opacity={grainOpacity}>
|
||||
<Shader source={noiseEffect} />
|
||||
</Rect>
|
||||
</Group>
|
||||
)}
|
||||
{/* Artwork over the crop: transparent window reveals it, mat covers the
|
||||
rest. Drawn last so semi-transparent mat edges blend over the photo
|
||||
instead of the reverse. */}
|
||||
<SkiaImage image={wallImage} x={frame.x} y={frame.y} width={frame.w} height={frame.h} fit="fill" />
|
||||
{renderGPSWatermark()}
|
||||
</Canvas>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<View className="flex-1 relative overflow-hidden bg-black items-center justify-center">
|
||||
{libraryImageUri && skiaImage ? (
|
||||
|
||||
Reference in New Issue
Block a user