Add wall frame overlay with transparent PNG window

This commit is contained in:
2026-09-08 17:56:50 +07:00
parent 669224fa39
commit a1769e8c84
5 changed files with 218 additions and 22 deletions
+100 -9
View File
@@ -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 ? (