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
+39 -2
View File
@@ -4,7 +4,7 @@ import Slider from '@react-native-community/slider';
import * as Haptics from 'expo-haptics';
import { Trash2 } from 'lucide-react-native';
import { Skia } from '@shopify/react-native-skia';
import { Recipe, ColorAdjustments, CustomWatermark, FrameId, BaseFilter } from '../types';
import { Recipe, ColorAdjustments, CustomWatermark, GpsWatermark, FrameId, BaseFilter } from '../types';
import { FRAMES } from '../utils/frameUtils';
import { PARAM_DEFS, ContinuousParamDef } from '../utils/paramDefs';
import { FILM_SIMS, filmSimToRecipe } from '../utils/defaultRecipes';
@@ -19,7 +19,16 @@ import RecipeCreateModal from './RecipeCreateModal';
type TabId = 'recipes' | 'iq' | 'wb' | 'filters' | 'frame' | 'watermark';
// Choice-group keys that open a strip above the chip row: color chrome groups
// (wb tab) and the D.RANGE selector (iq tab).
type GroupKey = 'cx' | 'cxb' | 'dr' | 'wmColor' | 'wmSize' | 'wmFont';
type GroupKey =
| 'cx'
| 'cxb'
| 'dr'
| 'wmColor'
| 'wmSize'
| 'wmFont'
| 'gpsColor'
| 'gpsName'
| 'gpsTime';
interface AdjustmentPanelProps {
activeTab: TabId;
@@ -59,6 +68,10 @@ interface AdjustmentPanelProps {
// input and the export options all read the same object.
customWm: CustomWatermark;
onUpdateCustomWm: (wm: CustomWatermark) => void;
// GPS mark styling — the SAME object the preview and the export read, so a
// chip press repaints both.
gpsWm: GpsWatermark;
onUpdateGpsWm: (wm: GpsWatermark) => void;
// ACCEPT: copy the live draft into the mark the export burns in. Edits stay a
// draft (viewfinder preview only) until this is pressed.
onAcceptCustomWm: () => void;
@@ -152,6 +165,8 @@ export default function AdjustmentPanel({
onDeleteRecipe,
customWm,
onUpdateCustomWm,
gpsWm,
onUpdateGpsWm,
onAcceptCustomWm,
onCancelCustomWm,
wmEditNonce,
@@ -388,6 +403,25 @@ export default function AdjustmentPanel({
options: wmFontOptions,
onPick: (v) => onUpdateCustomWm({ ...customWm, font: v === 'DEFAULT' ? null : v }),
},
// GPS mark's own features — one chip each, opening its options above (the
// same one-row-at-a-time strip the custom mark's styling uses).
gpsColor: {
label: 'COLOR', off: WM_DEFAULT_COLOR,
value: gpsWm.color ?? WM_DEFAULT_COLOR, options: WM_COLORS,
onPick: (v) => onUpdateGpsWm({ ...gpsWm, color: v }),
},
gpsName: {
label: 'PLACE NAME', off: 'ON',
value: gpsWm.showName === false ? 'OFF' : 'ON',
options: [{ v: 'on', d: 'ON' }, { v: 'off', d: 'OFF' }],
onPick: (v) => onUpdateGpsWm({ ...gpsWm, showName: v === 'on' }),
},
gpsTime: {
label: 'TIME', off: 'ON',
value: gpsWm.showTime === false ? 'OFF' : 'ON',
options: [{ v: 'on', d: 'ON' }, { v: 'off', d: 'OFF' }],
onPick: (v) => onUpdateGpsWm({ ...gpsWm, showTime: v === 'on' }),
},
};
const groupChip = (g: GroupKey): ChipDef => ({
key: `grp-${g}`,
@@ -537,6 +571,9 @@ export default function AdjustmentPanel({
active: useGeotag,
onPress: () => onToggleGeotag(!useGeotag),
})}
{useGeotag && renderChip(groupChip('gpsColor'))}
{useGeotag && renderChip(groupChip('gpsName'))}
{useGeotag && renderChip(groupChip('gpsTime'))}
{renderChip({
key: 'custom-wm',
label: customWm.enabled ? 'CUSTOM WATERMARK ON' : 'CUSTOM WATERMARK OFF',
+21 -14
View File
@@ -108,7 +108,7 @@ interface ViewfinderProps {
// GPS mark placement (WATERMARK tab drag + pinch): the same 0..1 fractions
// of the same area customWm uses, TOP-LEFT anchored; size multiplies the
// base 3.2%-of-width size. Absent = the historical fixed top-left look.
gpsWm?: { x: number; y: number; size: number };
gpsWm?: { x: number; y: number; size: number; color?: string; showName?: boolean; showTime?: boolean };
onGpsWmChange?: (x: number, y: number, size: number) => void;
// Pinch on the custom mark: its SIZE, exactly like the panel's SIZE chip.
onWmSizeChange?: (size: number) => void;
@@ -1720,25 +1720,32 @@ const Viewfinder = forwardRef<ViewfinderHandle, ViewfinderProps>(function Viewfi
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())}`;
// TOP-LEFT anchored two-line block at the dragged fraction of the same area
// the custom mark uses (frame card / photo window / screen). A framed
// export's canvas IS that frame, so the fractions carry straight to the
// burn-in — exportEngine step 8 draws with the identical formula.
// TOP-LEFT anchored block at the dragged fraction of the same area the
// custom mark uses (frame card / photo window / screen). A framed export's
// canvas IS that frame, so the fractions carry straight to the burn-in -
// exportEngine step 8 draws with the identical formula. Each enabled line
// (place name / shot time) stacks under the previous one.
const lines: { icon: string; text: string }[] = [];
if (gpsWm?.showName !== false) lines.push({ icon: '📍', text: locationName });
if (gpsWm?.showTime !== false) lines.push({ icon: '📷', text: timestampStr });
if (!lines.length) return null;
const { ax, ay, iconSize } = gpsAnchor();
const row1Y = ay + Math.round(iconSize * 1.1);
const row2Y = ay + Math.round(iconSize * 2.3);
const textColor = gpsWm?.color ?? '#f59e0b';
// One clear space (0.6em ~ a mono space) between each icon and its text.
const gap = (w: number) => Math.round(Math.max(w, iconSize) + iconSize * 0.6);
const pinW = gpsIconFont.measureText('📍');
const camW = gpsIconFont.measureText('📷');
// The old fixed row1/row2 spacing, generalised to N rows.
const rowY = (i: number) => ay + Math.round(iconSize * (1.1 + i * 1.2));
return (
<Group origin={{ x: ax, y: ay }} transform={wmRotation ? [{ rotate: wmRad }] : undefined}>
{/* Icons white so they read as glyphs, not part of the amber text */}
<SkiaText x={ax} y={row1Y} text="📍" font={gpsIconFont} color="#ffffff" />
<SkiaText x={ax + gap(pinW.width)} y={row1Y} text={locationName} font={gpsFont} color="#f59e0b" />
<SkiaText x={ax} y={row2Y} text="📷" font={gpsIconFont} color="#ffffff" />
<SkiaText x={ax + gap(camW.width)} y={row2Y} text={timestampStr} font={gpsFont} color="#f59e0b" />
{lines.map((line, i) => (
<React.Fragment key={line.icon}>
{/* Icons white so they read as glyphs, not part of the coloured text */}
<SkiaText x={ax} y={rowY(i)} text={line.icon} font={gpsIconFont} color="#ffffff" />
<SkiaText x={ax + gap(gpsIconFont.measureText(line.icon).width)} y={rowY(i)} text={line.text} font={gpsFont} color={textColor} />
</React.Fragment>
))}
</Group>
);
};