Pinch/drag library photo inside polaroid & wall frame windows with export parity

The framed windows (polaroid card / wall frame opening) now take the same pinch + double-tap gestures as plain library, clamped so the photo keeps covering the mat opening, and the visible sub-rectangle is handed to the export engine so the file matches the preview.

Wall frame export also stopped crashing: on Android release an image asset resolves to a drawable *name* ('wallframe') and is flagged downloaded, so Asset.downloadAsync short-circuited and readAsStringAsync threw 'Unsupported scheme for location wallframe'. Clearing that state lets the native module copy the drawable into the cache.
This commit is contained in:
2026-09-10 08:02:55 +07:00
parent c0c2f8532f
commit ff06045e02
3 changed files with 150 additions and 38 deletions
+42 -2
View File
@@ -38,6 +38,13 @@ export interface ExportOptions {
// (BASELINE anchor + 0.35em drop — same formula as the viewfinder overlay,
// so the export lands where the preview showed it). Amber mono, no icons.
watermark?: { text: string; x: number; y: number } | null;
// Library framed-window photo reposition (Viewfinder.getFrameWindowZoom):
// after pinch/drag inside a polaroid card window or a wall-frame opening,
// s = scale and u/v = the fraction of the photo's cover plane sitting at the
// WINDOW CENTER. The export draws the source sub-rectangle that stays
// visible through the opening, so the file matches the preview. Null /
// undefined = centered 1x (the historical full-image draw).
frameWindowZoom?: { s: number; u: number; v: number } | null;
}
// Lightroom-style "Screen" output sharpen: subtle unsharp mask. A 3x3
@@ -332,10 +339,22 @@ export async function processAndExportPhoto(
const pl = polaroidLayout(outW, outH, width / height, 'contain');
const win = pl.window;
// Window aspect == photo aspect → fills exactly, no letterbox inside.
// With a framed-window zoom the source rect shrinks to the visible part
// (window keeps showing the same geometry as the viewfinder).
const plainPaint = Skia.Paint();
const pz = options?.frameWindowZoom ?? null;
const pwz = pz && pz.s > 1.001 ? pz : null;
const pVisW = pwz ? width / pwz.s : width;
const pVisH = pwz ? height / pwz.s : height;
const pSx = pwz
? Math.min(Math.max(width * pwz.u - pVisW / 2, 0), width - pVisW)
: 0;
const pSy = pwz
? Math.min(Math.max(height * pwz.v - pVisH / 2, 0), height - pVisH)
: 0;
cardCanvas.drawImageRect(
photoImg,
Skia.XYWHRect(0, 0, width, height),
Skia.XYWHRect(pSx, pSy, pVisW, pVisH),
Skia.XYWHRect(win.x, win.y, win.w, win.h),
plainPaint
);
@@ -367,6 +386,13 @@ export async function processAndExportPhoto(
// Decode the frame PNG artwork (bundled asset → base64, same path as the
// GPS fonts below — embedded assets may not expose a readable file URI).
const wfAsset = Asset.fromModule(require('../../wallframe.png'));
// Android release builds resolve an image asset to a drawable *name*
// ('wallframe', no scheme) and mark it downloaded, so downloadAsync()
// short-circuits and readAsStringAsync throws 'Unsupported scheme for
// location wallframe'. Clear that state so the native module copies the
// drawable out of the APK into the cache as a real file path.
wfAsset.localUri = null;
wfAsset.downloaded = false;
await wfAsset.downloadAsync();
const wfDataStr = wfAsset.localUri
? await FileSystem.readAsStringAsync(wfAsset.localUri, { encoding: FileSystem.EncodingType.Base64 })
@@ -388,9 +414,23 @@ export async function processAndExportPhoto(
const scale = Math.max(win.w / pw, win.h / ph);
const srcW = win.w / scale;
const srcH = win.h / scale;
// Cover plane (center crop) that fills the window; a framed-window zoom
// shrinks the visible source sub-rect inside that plane.
const planeX = (pw - srcW) / 2;
const planeY = (ph - srcH) / 2;
const wz = options?.frameWindowZoom ?? null;
const wZoom = wz && wz.s > 1.001 ? wz : null;
const wVisW = wZoom ? srcW / wZoom.s : srcW;
const wVisH = wZoom ? srcH / wZoom.s : srcH;
const wSx = wZoom
? Math.min(Math.max(planeX + srcW * wZoom.u - wVisW / 2, planeX), planeX + srcW - wVisW)
: planeX;
const wSy = wZoom
? Math.min(Math.max(planeY + srcH * wZoom.v - wVisH / 2, planeY), planeY + srcH - wVisH)
: planeY;
cardCanvas.drawImageRect(
photoImg,
Skia.XYWHRect((pw - srcW) / 2, (ph - srcH) / 2, srcW, srcH),
Skia.XYWHRect(wSx, wSy, wVisW, wVisH),
Skia.XYWHRect(win.x, win.y, win.w, win.h),
plainPaint
);