Add WATERMARK tab with custom draggable text; hide FRAME in camera; fix double-dark after highlight capture

This commit is contained in:
2026-09-10 07:17:14 +07:00
parent 2512e05c92
commit 144b211d35
7 changed files with 315 additions and 42 deletions
+44 -21
View File
@@ -34,6 +34,10 @@ export interface ExportOptions {
// viewfinder displayed. WALL FRAME ignores this (frame keeps its fixed
// opening and export size). 'full'/undefined = no crop.
aspect?: AspectRatio;
// Custom text watermark: typed text + 0..1 fractions of the photo area
// (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;
}
// Lightroom-style "Screen" output sharpen: subtle unsharp mask. A 3x3
@@ -407,8 +411,11 @@ export async function processAndExportPhoto(
drawFrameOnCanvas(canvas, width, height, frameId);
}
// 8. Render GPS Geotag Watermark
if (useGeotag && gpsInfo) {
// 8. Render GPS Geotag Watermark + Custom Text Watermark. One asset load +
// one amber-mono paint path serves both: GPS keeps its two icon lines; the
// custom watermark is plain text drawn at the user-dragged position.
const customWm = options?.watermark ?? null;
if ((useGeotag && gpsInfo) || customWm) {
const fontAsset = Asset.fromModule(require('../../assets/Cousine-Regular.ttf'));
const iconFontAsset = Asset.fromModule(require('../../assets/NotoEmoji-GPS.ttf'));
await Promise.all([fontAsset.downloadAsync(), iconFontAsset.downloadAsync()]);
@@ -428,28 +435,44 @@ export async function processAndExportPhoto(
const iconPaint = Skia.Paint();
iconPaint.setColor(Skia.Color('#ffffff')); // icons white, distinct from text
const locationName = gpsInfo.locality || 'STREET VIEW';
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 gap = Math.round(fontSize * 1.6); // icon box (~1em) + one clear mono space (~0.6em)
// GPS lines keep the historical bottom-left anchor (framed -> inside
// the photo window; other frames -> bottom-left of the image).
if (useGeotag && gpsInfo) {
const locationName = gpsInfo.locality || 'STREET VIEW';
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 gap = Math.round(fontSize * 1.6); // icon box (~1em) + one clear mono space (~0.6em)
// Framed (polaroid/wallframe): caption at the bottom-left inside the
// photo window. Other frames: bottom-left of the image, clear of edge.
let baseX: number;
let baseY: number;
if (framedWindow) {
baseX = framedWindow.x + framedWindow.w * 0.05;
baseY = framedWindow.y + framedWindow.h - Math.round(fontSize * 1.4);
} else {
baseX = fontSize * 1.2;
baseY = stampH - fontSize * 1.5;
let baseX: number;
let baseY: number;
if (framedWindow) {
baseX = framedWindow.x + framedWindow.w * 0.05;
baseY = framedWindow.y + framedWindow.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);
}
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);
// Custom watermark: baseline at the dragged fraction of the photo/window
// area + ~0.35em drop (the grabbed anchor reads as the text's visual
// center, not its baseline) — the exact formula Viewfinder uses, so
// preview == export.
if (customWm) {
const area = framedWindow
? { dx: framedWindow.x, dy: framedWindow.y, dw: framedWindow.w, dh: framedWindow.h }
: { dx: 0, dy: 0, dw: stampW, dh: stampH };
const clamp01 = (v: number) => Math.max(0, Math.min(1, v));
const tx = area.dx + clamp01(customWm.x) * area.dw;
const ty = area.dy + clamp01(customWm.y) * area.dh + Math.round(fontSize * 0.35);
stampCanvas.drawText(customWm.text, tx, ty, textPaint, font);
}
}
}
+5
View File
@@ -32,6 +32,11 @@ export interface NativeExportOptions {
// Composition aspect-ratio crop ('full'/undefined = none). Native computes the
// center-largest rect after decode because the pixel size is only known there.
aspect?: AspectRatio;
// Custom text watermark overlay — position fractions of the photo area, same
// contract as ExportOptions.watermark. SHIPPED but NOT drawn yet: the Kotlin
// module renders geo captions but has no text-watermark draw (ponytail: add
// it when native parity with the Skia engine is completed).
watermark?: { text: string; x: number; y: number } | null;
}
// The Kotlin module reads plain absolute paths, not file:// URIs.