import { SkCanvas, SkPaint, Skia, BlendMode } from '@shopify/react-native-skia'; import { FrameId } from '../types'; export interface FrameDefinition { id: FrameId; name: string; } export const FRAMES: FrameDefinition[] = [ { id: 'none', name: 'NO FRAME' }, { id: 'classic-white', name: 'CLASSIC BORDER' }, { id: 'polaroid', name: 'RETRO INSTANT' }, { id: 'wallframe', name: 'WALL FRAME' }, ]; // Real Polaroid print proportions (88 x 107 mm card, 79 x 79 mm image print). // All fractions are relative to the CARD WIDTH so the layout scales to any // pixel size while keeping the physical look. export const POLAROID_CARD = { // Horizontal + top margin: (88 - 79)/2 / 88 mm each side. sideMargin: (88 - 79) / 2 / 88, // ≈ 0.05114 // Bottom deck: remaining height of the 107 mm card below the 79 mm print, // minus the top margin: (107 - 79 - 4.5) / 88 mm. bottomDeck: (107 - 79 - (88 - 79) / 2) / 88, // ≈ 0.26705 } as const; // Image window width as a fraction of card width (79 / 88 mm). export const POLAROID_WIN_W = 1 - 2 * POLAROID_CARD.sideMargin; // ≈ 0.89773 export interface PolaroidLayout { card: { x: number; y: number; w: number; h: number }; window: { x: number; y: number; w: number; h: number }; } // Wall frame: the PNG artwork lives at the project root (wallframe.png, // 4000x3117 RGBA, transparent window cols 675..3322 x rows 675..2441). Only the // geometry lives here; the bitmap itself is drawn by the caller. The phone is // portrait, so the app shows the artwork ROTATED 90° CW (original top edge → // right edge): a portrait frame whose transparent window is a fixed 2:3 // vertical opening. The photo is cover-cropped (center) into that opening — the // mat of a real frame hides the print edges. WALLFRAME_W/H and WALLFRAME_WIN // are the rotated display orientation; the affine in wallframeLayout maps the // source PNG pixels onto the frame rect so the drawn opening and the photo // window always line up. export const WALLFRAME_PNG_W = 4000; // source PNG width export const WALLFRAME_PNG_H = 3117; // source PNG height export const WALLFRAME_W = 3117; // rotated display width (= PNG height) export const WALLFRAME_H = 4000; // rotated display height (= PNG width) // Transparent window as fractions of the rotated image, derived from the same // affine used to draw the bitmap: X = w - (y/3117)w, Y = (x/4000)h. export const WALLFRAME_WIN = { left: 676 / 3117, // ≈ 0.21688 top: 675 / 4000, // 0.16875 w: 1766 / 3117, // ≈ 0.56657 h: 2647 / 4000, // 0.66175 } as const; // Landscape hang: the PNG's OWN orientation, so nothing is rotated — the // canvas is the artwork (4000x3117) and the opening is the source window as // fractions of it. Same 3:2 mat opening, hung the other way round. export const WALLFRAME_LAND_W = WALLFRAME_PNG_W; // 4000 export const WALLFRAME_LAND_H = WALLFRAME_PNG_H; // 3117 export const WALLFRAME_LAND_WIN = { left: 675 / WALLFRAME_PNG_W, // 0.16875 top: 675 / WALLFRAME_PNG_H, // ≈ 0.21688 w: 2647 / WALLFRAME_PNG_W, // 0.66175 h: 1766 / WALLFRAME_PNG_H, // ≈ 0.56657 } as const; export interface WallframeLayout { frame: { x: number; y: number; w: number; h: number }; window: { x: number; y: number; w: number; h: number }; // Row-major Skia matrix that rotates the source PNG (WALLFRAME_PNG_W/H) 90° CW // into the frame rect. Feed it to (viewfinder) or concat it on // the canvas (export) before drawing the bitmap. drawMatrix: number[]; } // Geometry of the wall-frame inside an arbitrary container. The frame is // contain-fitted ('contain', centered, letterboxed) for the live viewfinder, // or fills the container exactly ('fill', export canvas already at frame size). // `landscape` hangs the artwork the other way round: same PNG, no rotation. export function wallframeLayout( contW: number, contH: number, mode: 'contain' | 'fill' = 'contain', landscape = false ): WallframeLayout { const FW = landscape ? WALLFRAME_LAND_W : WALLFRAME_W; const FH = landscape ? WALLFRAME_LAND_H : WALLFRAME_H; const WIN = landscape ? WALLFRAME_LAND_WIN : WALLFRAME_WIN; let frameW: number; let frameH: number; if (mode === 'fill') { frameW = contW; frameH = contH; } else { const s = Math.min(contW / FW, contH / FH); frameW = FW * s; frameH = FH * s; } const fx = (contW - frameW) / 2; const fy = (contH - frameH) / 2; const scale = frameW / FW; // == frameH / FH return { frame: { x: fx, y: fy, w: frameW, h: frameH }, window: { x: fx + WIN.left * frameW, y: fy + WIN.top * frameH, w: WIN.w * frameW, h: WIN.h * frameH, }, // Portrait: the source PNG must be turned 90° CW onto the canvas. // Landscape: it already sits the right way up — scale and centre it. drawMatrix: landscape ? [scale, 0, fx, 0, scale, fy, 0, 0, 1] : [0, -scale, fx + frameW, scale, 0, fy, 0, 0, 1], }; } // Geometry of a retro-polaroid card inside an arbitrary container. The photo is // NEVER cropped: the image window keeps the photo aspect ratio (photoW/photoH) // and the card grows/shrinks around it, so the whole picture always fits the // window. 'contain' fits the full card inside the container (centered, with // letterbox bars); 'fill' sizes the container exactly to the natural card // proportions (used at export where the canvas IS the card). export function polaroidLayout( contW: number, contH: number, photoAspect: number, // photo width / photo height (>1 = landscape) mode: 'contain' | 'fill' = 'contain' ): PolaroidLayout { const S = POLAROID_CARD; // Height of the card once the photo window (width WIN_W of the card width, // height = that / photoAspect) is stacked below the top margin and above the // bottom deck: cardH = cardW*(sideMargin + bottomDeck) + cardW*WIN_W/photoAspect const naturalAspect = S.sideMargin + S.bottomDeck + POLAROID_WIN_W / photoAspect; // cardH/cardW let cardW: number; let cardH: number; if (mode === 'fill') { cardW = contW; cardH = contH; } else { cardW = Math.min(contW, contH / naturalAspect); cardH = cardW * naturalAspect; } const cx = (contW - cardW) / 2; const cy = (contH - cardH) / 2; const top = S.sideMargin * cardW; const side = S.sideMargin * cardW; let winW = POLAROID_WIN_W * cardW; let winH = winW / photoAspect; // Container aspect may leave less vertical room than the natural card (e.g. // phone camera feed) — shrink the window to the available space instead of // overflowing the card. const maxWinH = cardH - top - S.bottomDeck * cardW; if (winH > maxWinH) { winH = maxWinH; winW = winH * photoAspect; } return { card: { x: cx, y: cy, w: cardW, h: cardH }, window: { x: cx + (cardW - winW) / 2, y: cy + top, w: winW, h: winH }, }; } export function drawFrameOnCanvas( canvas: SkCanvas, width: number, height: number, frameId: FrameId ) { if (frameId === 'none') return; const paint = Skia.Paint(); if (frameId === 'classic-white') { // A clean white border around the picture paint.setColor(Skia.Color('#ffffff')); paint.setStyle(0); // Fill const borderWidth = Math.min(width, height) * 0.05; // 5% border // Top border canvas.drawRect(Skia.XYWHRect(0, 0, width, borderWidth), paint); // Bottom border canvas.drawRect(Skia.XYWHRect(0, height - borderWidth, width, borderWidth), paint); // Left border canvas.drawRect(Skia.XYWHRect(0, 0, borderWidth, height), paint); // Right border canvas.drawRect(Skia.XYWHRect(width - borderWidth, 0, borderWidth, height), paint); } else if (frameId === 'cinematic') { // Cinematic black bars on top and bottom paint.setColor(Skia.Color('#000000')); paint.setStyle(0); // Fill const barHeight = height * 0.12; // 12% cinematic letterbox height // Top letterbox canvas.drawRect(Skia.XYWHRect(0, 0, width, barHeight), paint); // Bottom letterbox canvas.drawRect(Skia.XYWHRect(0, height - barHeight, width, barHeight), paint); } }