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
+70 -12
View File
@@ -7,7 +7,7 @@ import { Recipe, GPSInfo, FrameId } from '../types';
import { getSkiaColorMatrix, applyExposureGain } from './colorUtils';
import { TONE_SKSL, getToneUniforms, toneIsActive } from './toneShader';
import { CINEMA_SKSL, getCinemaUniforms, cinemaIsActive } from './cinemaShader';
import { drawFrameOnCanvas } from './frameUtils';
import { drawFrameOnCanvas, polaroidLayout, POLAROID_CARD, POLAROID_WIN_W } from './frameUtils';
import { patchJpegDpi } from './jpegDpi';
export interface ExportResult {
@@ -256,8 +256,56 @@ export async function processAndExportPhoto(
}
}
// 7. Render Frame Border (Polaroid / Classic White / Cinematic)
drawFrameOnCanvas(canvas, width, height, frameId);
// 7. Frame. RETRO POLAROID is not a border drawn on the photo: the whole
// processed picture is composited, uncropped, into the window of a bigger
// off-white card (real print proportions, see frameUtils) — the export IS
// the card, with the photo scaled to the window so it keeps ~WIN_W of the
// card width. Other frames (classic white border, legacy cinematic bars)
// draw over the photo-sized canvas as before.
let outputSurface: SkSurface = surface;
let stampCanvas = canvas;
let stampW = width;
let stampH = height;
let polaroidWindow: { x: number; y: number; w: number; h: number } | null = null;
if (frameId === 'polaroid') {
const photoImg = surface.makeImageSnapshot();
const outW = width; // card width matches the photo width
const outH = Math.round(
width * (POLAROID_CARD.sideMargin + POLAROID_CARD.bottomDeck) + height * POLAROID_WIN_W
);
const cardSurface = createSurface(outW, outH);
if (!cardSurface) {
console.error('Failed to create polaroid card surface');
return null;
}
const cardCanvas = cardSurface.getCanvas();
const bgPaint = Skia.Paint();
bgPaint.setColor(Skia.Color('#faf9f6')); // off-white card
cardCanvas.drawRect(Skia.XYWHRect(0, 0, outW, outH), bgPaint);
const pl = polaroidLayout(outW, outH, width / height, 'contain');
const win = pl.window;
// Window aspect == photo aspect → fills exactly, no letterbox inside.
const plainPaint = Skia.Paint();
cardCanvas.drawImageRect(
photoImg,
Skia.XYWHRect(0, 0, width, height),
Skia.XYWHRect(win.x, win.y, win.w, win.h),
plainPaint
);
// Thin seam separating image window from the card.
const seamPaint = Skia.Paint();
seamPaint.setColor(Skia.Color('#e5e5e5'));
seamPaint.setStyle(1);
seamPaint.setStrokeWidth(1);
cardCanvas.drawRect(Skia.XYWHRect(win.x + 0.5, win.y + 0.5, win.w - 1, win.h - 1), seamPaint);
outputSurface = cardSurface;
stampCanvas = cardCanvas;
stampW = outW;
stampH = outH;
polaroidWindow = win;
} else {
drawFrameOnCanvas(canvas, width, height, frameId);
}
// 8. Render GPS Geotag Watermark
if (useGeotag && gpsInfo) {
@@ -272,7 +320,7 @@ export async function processAndExportPhoto(
const iconTypeface = Skia.Typeface.MakeFreeTypeFaceFromData(Skia.Data.fromBase64(iconFontData));
if (typeface && iconTypeface) {
const fontSize = Math.round(width * 0.032); // 3.2% of image width
const fontSize = Math.round(stampW * 0.032); // 3.2% of output width
const font = Skia.Font(typeface, fontSize);
const iconFont = Skia.Font(iconTypeface, fontSize);
const textPaint = Skia.Paint();
@@ -284,21 +332,31 @@ export async function processAndExportPhoto(
const d = new Date(gpsInfo.timestamp);
const pad = (n: number) => String(n).padStart(2, '0');
const timestampStr = `${pad(d.getDate())}/${pad(d.getMonth() + 1)}/${d.getFullYear()} ${pad(d.getHours())}:${pad(d.getMinutes())}`;
const yOffset = height - (frameId === 'polaroid' ? height * 0.15 : fontSize * 1.5);
const xOffset = frameId === 'polaroid' ? width * 0.08 : fontSize * 1.2;
const gap = Math.round(fontSize * 1.6); // icon box (~1em) + one clear mono space (~0.6em)
canvas.drawText('📍', xOffset, yOffset - fontSize * 1.4, iconPaint, iconFont);
canvas.drawText(locationName, xOffset + gap, yOffset - fontSize * 1.4, textPaint, font);
canvas.drawText('📷', xOffset, yOffset, iconPaint, iconFont);
canvas.drawText(timestampStr, xOffset + gap, yOffset, textPaint, font);
// Polaroid: caption at the bottom-left inside the photo window (the
// spot the deck edge meets the print). Other frames: bottom-left of
// the image, clear of the edge.
let baseX: number;
let baseY: number;
if (polaroidWindow) {
baseX = polaroidWindow.x + polaroidWindow.w * 0.05;
baseY = polaroidWindow.y + polaroidWindow.h - Math.round(fontSize * 1.4);
} else {
baseX = fontSize * 1.2;
baseY = stampH - fontSize * 1.5;
}
stampCanvas.drawText('📍', baseX, baseY - fontSize * 1.4, iconPaint, iconFont);
stampCanvas.drawText(locationName, baseX + gap, baseY - fontSize * 1.4, textPaint, font);
stampCanvas.drawText('📷', baseX, baseY, iconPaint, iconFont);
stampCanvas.drawText(timestampStr, baseX + gap, baseY, textPaint, font);
}
}
// 9. Snapshot & Encode to JPEG (base64 string, no SkData round-trip)
console.error(`[export] composite ${Date.now() - t0}ms`);
let resultImage = surface.makeImageSnapshot();
let resultImage = outputSurface.makeImageSnapshot();
// Sharpening knob (0..10) overrides the legacy boolean sharpen flag:
// legacy camera captures keep sharpen:true → 0.5 fallback when knob is 0.
const sharpKnob = adjustments.sharpening ?? 0;