Polaroid print layout in viewfinder and export; split chip rows; drop cinematic; debounce EV bias

This commit is contained in:
2026-09-08 16:30:29 +07:00
parent 7f7ceeddc6
commit bfdc964f65
4 changed files with 414 additions and 182 deletions
+66 -33
View File
@@ -10,9 +10,74 @@ export const FRAMES: FrameDefinition[] = [
{ id: 'none', name: 'NO FRAME' },
{ id: 'classic-white', name: 'CLASSIC BORDER' },
{ id: 'polaroid', name: 'RETRO POLAROID' },
{ id: 'cinematic', name: 'CINEMATIC BARS' },
];
// 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 };
}
// 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,
@@ -38,38 +103,6 @@ export function drawFrameOnCanvas(
canvas.drawRect(Skia.XYWHRect(0, 0, borderWidth, height), paint);
// Right border
canvas.drawRect(Skia.XYWHRect(width - borderWidth, 0, borderWidth, height), paint);
} else if (frameId === 'polaroid') {
// Polaroid style: thick white borders, thicker at the bottom
paint.setColor(Skia.Color('#faf9f6')); // Off-white
paint.setStyle(0); // Fill
const sideBorder = Math.min(width, height) * 0.06; // 6% borders on sides/top
const bottomBorder = height * 0.18; // 18% border at bottom
// Top border
canvas.drawRect(Skia.XYWHRect(0, 0, width, sideBorder), paint);
// Left border
canvas.drawRect(Skia.XYWHRect(0, 0, sideBorder, height), paint);
// Right border
canvas.drawRect(Skia.XYWHRect(width - sideBorder, 0, sideBorder, height), paint);
// Bottom border
canvas.drawRect(Skia.XYWHRect(0, height - bottomBorder, width, bottomBorder), paint);
// Inner thin border (grey line separating image and card)
const innerPaint = Skia.Paint();
innerPaint.setColor(Skia.Color('#e5e5e5'));
innerPaint.setStrokeWidth(1);
innerPaint.setStyle(1); // Stroke
canvas.drawRect(
Skia.XYWHRect(
sideBorder - 1,
sideBorder - 1,
width - 2 * sideBorder + 2,
height - sideBorder - bottomBorder + 2
),
innerPaint
);
} else if (frameId === 'cinematic') {
// Cinematic black bars on top and bottom
paint.setColor(Skia.Color('#000000'));