feat(wm): GPS watermark color + place-name/time toggles, "commune, city" locality

This commit is contained in:
2026-09-10 18:05:57 +07:00
parent f13fd8e0f3
commit 299d81bade
6 changed files with 114 additions and 32 deletions
+13 -8
View File
@@ -53,7 +53,7 @@ export interface ExportOptions {
// WATERMARK tab drag + pinch on the GPS lines: 0..1 fractions of the same
// area the custom mark uses (a framed export's canvas IS the frame) plus a
// multiplier on the base 3.2%-of-width size. Absent = the old top-left look.
gpsWatermark?: { x: number; y: number; size: number } | null;
gpsWatermark?: { x: number; y: number; size: number; color?: string; showName?: boolean; showTime?: boolean } | null;
// WALL FRAME hung the other way round (4000x3117, artwork un-rotated).
wallframeLandscape?: boolean;
// Library framed-window photo reposition (Viewfinder.getFrameWindowZoom):
@@ -606,18 +606,23 @@ export async function processAndExportPhoto(
const gpsGap = Math.round(gpsSize * 1.6); // icon box (~1em) + one clear mono space (~0.6em)
const baseX = clamp01(gpsPos?.x ?? 0.05) * stampW;
const baseY = clamp01(gpsPos?.y ?? 0.1) * stampH;
const row1Y = baseY + Math.round(gpsSize * 1.1);
const row2Y = baseY + Math.round(gpsSize * 2.3);
// Each enabled line (place name / shot time) stacks under the
// previous one, exactly as the viewfinder draws it.
const lines: { icon: string; text: string }[] = [];
if (gpsPos?.showName !== false) lines.push({ icon: '📍', text: locationName });
if (gpsPos?.showTime !== false) lines.push({ icon: '📷', text: timestampStr });
textPaint.setColor(Skia.Color(gpsPos?.color ?? '#f59e0b')); // user-chosen, amber by default
// Spin the whole two-line block around its top-left (the anchor the
// Spin the whole block around its top-left (the anchor the
// viewfinder uses). 0 = the historical upright look.
const wmRot = options?.watermarkRotation ?? 0;
stampCanvas.save();
if (wmRot) stampCanvas.rotate(wmRot, baseX, baseY);
stampCanvas.drawText('📍', baseX, row1Y, iconPaint, gpsIconFont);
stampCanvas.drawText(locationName, baseX + gpsGap, row1Y, textPaint, gpsFont);
stampCanvas.drawText('📷', baseX, row2Y, iconPaint, gpsIconFont);
stampCanvas.drawText(timestampStr, baseX + gpsGap, row2Y, textPaint, gpsFont);
lines.forEach((line, i) => {
const rowY = baseY + Math.round(gpsSize * (1.1 + i * 1.2));
stampCanvas.drawText(line.icon, baseX, rowY, iconPaint, gpsIconFont);
stampCanvas.drawText(line.text, baseX + gpsGap, rowY, textPaint, gpsFont);
});
stampCanvas.restore();
}
+10 -5
View File
@@ -49,12 +49,17 @@ export async function getCurrentGPS(): Promise<GPSInfo | null> {
}
}
// The one place-name picker: city first, because "the place" on a stamp means
// the town people would name, while subregion is Android's subAdminArea — the
// county/district, i.e. the wrong name for the same coordinates. Shared so the
// camera stamp and a photo's stamp can never disagree.
// The one place-name picker, shared so the camera stamp and a photo's stamp can
// never disagree. A Vietnamese geocoder answers with the commune/ward in
// subregion (post-2025 two-level administration: the commune is the district-
// level name) and the province/city in region, while city and district come
// back null, so the stamp reads "COMMUNE, CITY" instead of a bare city.
export function localityName(address: Location.LocationGeocodedAddress): string | null {
const name = address.city || address.subregion || address.district || address.region;
const district = address.subregion || address.district;
const city = address.city || address.region;
// Same name in both slots (one field echoed twice) must not print twice.
const parts = district && district !== city ? [district, city] : [district || city];
const name = parts.filter(Boolean).join(', ');
return name ? name.toUpperCase() : null;
}