Let the GPS and custom watermarks be rotated
This commit is contained in:
@@ -83,6 +83,9 @@ export default function App() {
|
|||||||
// live Viewfinder overlay, the panel input and every export share one state.
|
// live Viewfinder overlay, the panel input and every export share one state.
|
||||||
const DEFAULT_CUSTOM_WM: CustomWatermark = { enabled: false, text: '', x: 0.5, y: 0.5 };
|
const DEFAULT_CUSTOM_WM: CustomWatermark = { enabled: false, text: '', x: 0.5, y: 0.5 };
|
||||||
const [customWm, setCustomWm] = useState<CustomWatermark>({ ...DEFAULT_CUSTOM_WM });
|
const [customWm, setCustomWm] = useState<CustomWatermark>({ ...DEFAULT_CUSTOM_WM });
|
||||||
|
// WATERMARK tab ROTATE chip: clockwise degrees, applied to the GPS lines and
|
||||||
|
// the custom text in both the preview and the exported file.
|
||||||
|
const [wmRotation, setWmRotation] = useState(0);
|
||||||
// The mark the EXPORT burns in. The chip above is the live draft (typing +
|
// The mark the EXPORT burns in. The chip above is the live draft (typing +
|
||||||
// dragging update it); only ACCEPT in the WATERMARK tab copies the draft in
|
// dragging update it); only ACCEPT in the WATERMARK tab copies the draft in
|
||||||
// here — until then the file keeps the previous stamp (or none).
|
// here — until then the file keeps the previous stamp (or none).
|
||||||
@@ -774,6 +777,7 @@ export default function App() {
|
|||||||
evFromCamera: true,
|
evFromCamera: true,
|
||||||
aspect: aspectRatio === 'full' ? undefined : aspectRatio,
|
aspect: aspectRatio === 'full' ? undefined : aspectRatio,
|
||||||
watermark: wmForExport(),
|
watermark: wmForExport(),
|
||||||
|
watermarkRotation: wmRotation,
|
||||||
};
|
};
|
||||||
return NATIVE_EXPORT
|
return NATIVE_EXPORT
|
||||||
? processAndExportPhotoNative(sourceUri, recipeArgs, selectedFrame, useGeotag, captureGps, options)
|
? processAndExportPhotoNative(sourceUri, recipeArgs, selectedFrame, useGeotag, captureGps, options)
|
||||||
@@ -841,6 +845,7 @@ export default function App() {
|
|||||||
{
|
{
|
||||||
aspect: libraryAspect,
|
aspect: libraryAspect,
|
||||||
watermark: wmForExport(),
|
watermark: wmForExport(),
|
||||||
|
watermarkRotation: wmRotation,
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
: await processAndExportPhoto(
|
: await processAndExportPhoto(
|
||||||
@@ -859,6 +864,7 @@ export default function App() {
|
|||||||
{
|
{
|
||||||
aspect: libraryAspect,
|
aspect: libraryAspect,
|
||||||
watermark: wmForExport(),
|
watermark: wmForExport(),
|
||||||
|
watermarkRotation: wmRotation,
|
||||||
// Pinch/drag inside a polaroid card window or wall-frame opening:
|
// Pinch/drag inside a polaroid card window or wall-frame opening:
|
||||||
// save the same crop the preview showed (null = centered 1x).
|
// save the same crop the preview showed (null = centered 1x).
|
||||||
frameWindowZoom: viewfinderRef.current?.getFrameWindowZoom?.() ?? null,
|
frameWindowZoom: viewfinderRef.current?.getFrameWindowZoom?.() ?? null,
|
||||||
@@ -1052,6 +1058,7 @@ export default function App() {
|
|||||||
meteringAdjustments={meteringAdj}
|
meteringAdjustments={meteringAdj}
|
||||||
imageAdjustTarget={imageAdjustTarget}
|
imageAdjustTarget={imageAdjustTarget}
|
||||||
customWm={customWm}
|
customWm={customWm}
|
||||||
|
wmRotation={wmRotation}
|
||||||
// The mark only captures image touches while its panel is open, so a
|
// The mark only captures image touches while its panel is open, so a
|
||||||
// watermark left ON cannot block tap-to-focus or pinch/zoom.
|
// watermark left ON cannot block tap-to-focus or pinch/zoom.
|
||||||
wmEditing={openTab === 'watermark'}
|
wmEditing={openTab === 'watermark'}
|
||||||
@@ -1108,6 +1115,8 @@ export default function App() {
|
|||||||
onAcceptCustomWm={handleAcceptCustomWm}
|
onAcceptCustomWm={handleAcceptCustomWm}
|
||||||
onCancelCustomWm={handleCancelCustomWm}
|
onCancelCustomWm={handleCancelCustomWm}
|
||||||
wmEditNonce={wmEditNonce}
|
wmEditNonce={wmEditNonce}
|
||||||
|
wmRotation={wmRotation}
|
||||||
|
onRotateWm={() => setWmRotation((r) => (r + 90) % 360)}
|
||||||
/>
|
/>
|
||||||
</View>
|
</View>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -64,6 +64,10 @@ interface AdjustmentPanelProps {
|
|||||||
onCancelCustomWm: () => void;
|
onCancelCustomWm: () => void;
|
||||||
// Bumped by a double-tap on the mark: put the caret back in the text input.
|
// Bumped by a double-tap on the mark: put the caret back in the text input.
|
||||||
wmEditNonce: number;
|
wmEditNonce: number;
|
||||||
|
// WATERMARK tab ROTATE chip: clockwise degrees (0/90/180/270) applied to both
|
||||||
|
// the GPS lines and the custom text, in the preview and in the export.
|
||||||
|
wmRotation: number;
|
||||||
|
onRotateWm: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface ChipDef {
|
interface ChipDef {
|
||||||
@@ -146,6 +150,8 @@ export default function AdjustmentPanel({
|
|||||||
onCancelCustomWm,
|
onCancelCustomWm,
|
||||||
wmEditNonce,
|
wmEditNonce,
|
||||||
onToggleRotation,
|
onToggleRotation,
|
||||||
|
wmRotation,
|
||||||
|
onRotateWm,
|
||||||
}: AdjustmentPanelProps) {
|
}: AdjustmentPanelProps) {
|
||||||
// Double-tap on the mark in the viewfinder bumps wmEditNonce -> focus the
|
// Double-tap on the mark in the viewfinder bumps wmEditNonce -> focus the
|
||||||
// input again so the text can be retyped after the keyboard was dismissed.
|
// input again so the text can be retyped after the keyboard was dismissed.
|
||||||
@@ -524,6 +530,13 @@ export default function AdjustmentPanel({
|
|||||||
active: customWm.enabled,
|
active: customWm.enabled,
|
||||||
onPress: () => onUpdateCustomWm({ ...customWm, enabled: !customWm.enabled }),
|
onPress: () => onUpdateCustomWm({ ...customWm, enabled: !customWm.enabled }),
|
||||||
})}
|
})}
|
||||||
|
{(useGeotag || customWm.enabled) &&
|
||||||
|
renderChip({
|
||||||
|
key: 'wm-rotate',
|
||||||
|
label: `ROTATE ${wmRotation}°`,
|
||||||
|
active: wmRotation !== 0,
|
||||||
|
onPress: onRotateWm,
|
||||||
|
})}
|
||||||
{customWm.enabled &&
|
{customWm.enabled &&
|
||||||
renderChip({
|
renderChip({
|
||||||
key: 'custom-wm-accept',
|
key: 'custom-wm-accept',
|
||||||
|
|||||||
@@ -105,6 +105,10 @@ interface ViewfinderProps {
|
|||||||
y: number;
|
y: number;
|
||||||
};
|
};
|
||||||
onWmPositionChange?: (x: number, y: number) => void;
|
onWmPositionChange?: (x: number, y: number) => void;
|
||||||
|
// WATERMARK tab ROTATE chip: clockwise degrees (0/90/180/270). Rotates the
|
||||||
|
// custom text AND the GPS lines around their own anchors — same angle the
|
||||||
|
// export burns in.
|
||||||
|
wmRotation?: number;
|
||||||
// True while the WATERMARK panel is open — that panel IS the mark's edit
|
// True while the WATERMARK panel is open — that panel IS the mark's edit
|
||||||
// session. The mark draws whenever the CUSTOM chip is ON with text, but it
|
// session. The mark draws whenever the CUSTOM chip is ON with text, but it
|
||||||
// only CAPTURES image touches (placement drags in camera mode, click-through
|
// only CAPTURES image touches (placement drags in camera mode, click-through
|
||||||
@@ -196,6 +200,7 @@ const Viewfinder = forwardRef<ViewfinderHandle, ViewfinderProps>(function Viewfi
|
|||||||
onWmEdit,
|
onWmEdit,
|
||||||
wmEditing = false,
|
wmEditing = false,
|
||||||
photoRotation = 0,
|
photoRotation = 0,
|
||||||
|
wmRotation = 0,
|
||||||
}: ViewfinderProps,
|
}: ViewfinderProps,
|
||||||
ref
|
ref
|
||||||
) {
|
) {
|
||||||
@@ -1113,6 +1118,19 @@ const Viewfinder = forwardRef<ViewfinderHandle, ViewfinderProps>(function Viewfi
|
|||||||
if (mode === 'library' && imageFitRect) return imageFitRect;
|
if (mode === 'library' && imageFitRect) return imageFitRect;
|
||||||
return { dx: 0, dy: 0, dw: vw, dh: vh };
|
return { dx: 0, dy: 0, dw: vw, dh: vh };
|
||||||
};
|
};
|
||||||
|
// The drawn mark's baseline origin. Everything that has to agree with the
|
||||||
|
// glyphs — the render, the double-tap box, the export — goes through here,
|
||||||
|
// and the ROTATE chip spins around this same point.
|
||||||
|
const wmAnchor = () => {
|
||||||
|
const area = wmArea();
|
||||||
|
const fracX = wmDragPos?.x ?? customWm.x;
|
||||||
|
const fracY = wmDragPos?.y ?? customWm.y;
|
||||||
|
return {
|
||||||
|
ax: area.dx + fracX * area.dw,
|
||||||
|
ay: area.dy + fracY * area.dh + Math.round(wmFontSize * 0.35),
|
||||||
|
};
|
||||||
|
};
|
||||||
|
const wmRad = (wmRotation * Math.PI) / 180;
|
||||||
const wmFontSize = Math.round(vw * 0.032 * (customWm.size ?? 1));
|
const wmFontSize = Math.round(vw * 0.032 * (customWm.size ?? 1));
|
||||||
const wmColor = customWm.color ?? '#f59e0b';
|
const wmColor = customWm.color ?? '#f59e0b';
|
||||||
// FONT chip: an OS family resolved through the platform font manager; no
|
// FONT chip: an OS family resolved through the platform font manager; no
|
||||||
@@ -1154,24 +1172,31 @@ const Viewfinder = forwardRef<ViewfinderHandle, ViewfinderProps>(function Viewfi
|
|||||||
// so a double-tap ON the text re-opens the editor instead of moving it.
|
// so a double-tap ON the text re-opens the editor instead of moving it.
|
||||||
const wmBox = () => {
|
const wmBox = () => {
|
||||||
if (!wmVisible || !wmFont) return null;
|
if (!wmVisible || !wmFont) return null;
|
||||||
const area = wmArea();
|
const { ax, ay } = wmAnchor();
|
||||||
const fracX = wmDragPos?.x ?? customWm.x;
|
|
||||||
const fracY = wmDragPos?.y ?? customWm.y;
|
|
||||||
const tx = area.dx + fracX * area.dw;
|
|
||||||
const ty = area.dy + fracY * area.dh + Math.round(wmFontSize * 0.35);
|
|
||||||
const w = Math.max(wmFont.measureText(customWm.text.trim()).width, wmFontSize * 2);
|
const w = Math.max(wmFont.measureText(customWm.text.trim()).width, wmFontSize * 2);
|
||||||
return { x: tx, y: ty - wmFontSize, w, h: Math.round(wmFontSize * 1.4) };
|
return { x: ax, y: ay - wmFontSize, w, h: Math.round(wmFontSize * 1.4) };
|
||||||
};
|
};
|
||||||
const wmBoxHit = (
|
const wmBoxHit = (
|
||||||
box: { x: number; y: number; w: number; h: number } | null,
|
box: { x: number; y: number; w: number; h: number } | null,
|
||||||
x: number,
|
x: number,
|
||||||
y: number
|
y: number
|
||||||
) =>
|
) => {
|
||||||
!!box &&
|
if (!box) return false;
|
||||||
x >= box.x - 8 &&
|
// A rotated mark needs the tap un-spun around the same anchor, otherwise
|
||||||
x <= box.x + box.w + 8 &&
|
// the box stops covering the visible glyphs.
|
||||||
y >= box.y - 8 &&
|
if (wmRotation) {
|
||||||
y <= box.y + box.h + 8;
|
const { ax, ay } = wmAnchor();
|
||||||
|
const c = Math.cos(-wmRad);
|
||||||
|
const s = Math.sin(-wmRad);
|
||||||
|
const dx = x - ax;
|
||||||
|
const dy = y - ay;
|
||||||
|
x = ax + dx * c - dy * s;
|
||||||
|
y = ay + dx * s + dy * c;
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
x >= box.x - 8 && x <= box.x + box.w + 8 && y >= box.y - 8 && y <= box.y + box.h + 8
|
||||||
|
);
|
||||||
|
};
|
||||||
// Library framed (polaroid/wall) returns reuse the SAME responder handlers as
|
// Library framed (polaroid/wall) returns reuse the SAME responder handlers as
|
||||||
// plain library (see the framed JSX below): single-finger drags place/move
|
// plain library (see the framed JSX below): single-finger drags place/move
|
||||||
// the text watermark while armed, and otherwise DRAG the photo inside the
|
// the text watermark while armed, and otherwise DRAG the photo inside the
|
||||||
@@ -1464,15 +1489,13 @@ const Viewfinder = forwardRef<ViewfinderHandle, ViewfinderProps>(function Viewfi
|
|||||||
|
|
||||||
const renderCustomWatermark = () => {
|
const renderCustomWatermark = () => {
|
||||||
if (!wmVisible || !wmFont) return null;
|
if (!wmVisible || !wmFont) return null;
|
||||||
const area = wmArea();
|
|
||||||
const fracX = wmDragPos?.x ?? customWm.x;
|
|
||||||
const fracY = wmDragPos?.y ?? customWm.y;
|
|
||||||
// Baseline = dragged anchor + ~0.35em drop (the grabbed anchor reads as the
|
// Baseline = dragged anchor + ~0.35em drop (the grabbed anchor reads as the
|
||||||
// text's visual center). Must match exportEngine step 8's formula.
|
// text's visual center). Must match exportEngine step 8's formula.
|
||||||
const tx = area.dx + fracX * area.dw;
|
const { ax, ay } = wmAnchor();
|
||||||
const ty = area.dy + fracY * area.dh + Math.round(wmFontSize * 0.35);
|
|
||||||
return (
|
return (
|
||||||
<SkiaText x={tx} y={ty} text={customWm.text.trim()} font={wmFont} color={wmColor} />
|
<Group origin={{ x: ax, y: ay }} transform={wmRotation ? [{ rotate: wmRad }] : undefined}>
|
||||||
|
<SkiaText x={ax} y={ay} text={customWm.text.trim()} font={wmFont} color={wmColor} />
|
||||||
|
</Group>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -1530,7 +1553,10 @@ const Viewfinder = forwardRef<ViewfinderHandle, ViewfinderProps>(function Viewfi
|
|||||||
const camW = iconFont.measureText('📷');
|
const camW = iconFont.measureText('📷');
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Group>
|
<Group
|
||||||
|
origin={{ x: xOffset, y: topBase }}
|
||||||
|
transform={wmRotation ? [{ rotate: wmRad }] : undefined}
|
||||||
|
>
|
||||||
{/* Icons white so they read as glyphs, not part of the amber text */}
|
{/* Icons white so they read as glyphs, not part of the amber text */}
|
||||||
<SkiaText x={xOffset} y={row1Y} text="📍" font={iconFont} color="#ffffff" />
|
<SkiaText x={xOffset} y={row1Y} text="📍" font={iconFont} color="#ffffff" />
|
||||||
<SkiaText x={xOffset + gap(pinW.width)} y={row1Y} text={locationName} font={customFont} color="#f59e0b" />
|
<SkiaText x={xOffset + gap(pinW.width)} y={row1Y} text={locationName} font={customFont} color="#f59e0b" />
|
||||||
|
|||||||
@@ -46,6 +46,10 @@ export interface ExportOptions {
|
|||||||
size?: number;
|
size?: number;
|
||||||
font?: string | null;
|
font?: string | null;
|
||||||
} | null;
|
} | null;
|
||||||
|
// WATERMARK tab ROTATE chip: clockwise degrees (0/90/180/270) applied to
|
||||||
|
// BOTH the GPS lines and the custom text, each around its own anchor (the
|
||||||
|
// same anchor Viewfinder rotates around, so the preview matches the file).
|
||||||
|
watermarkRotation?: number;
|
||||||
// Library framed-window photo reposition (Viewfinder.getFrameWindowZoom):
|
// Library framed-window photo reposition (Viewfinder.getFrameWindowZoom):
|
||||||
// after pinch/drag inside a polaroid card window or a wall-frame opening,
|
// after pinch/drag inside a polaroid card window or a wall-frame opening,
|
||||||
// s = scale and u/v = the fraction of the photo's cover plane sitting at the
|
// s = scale and u/v = the fraction of the photo's cover plane sitting at the
|
||||||
@@ -596,10 +600,16 @@ export async function processAndExportPhoto(
|
|||||||
baseY = stampH - fontSize * 1.5;
|
baseY = stampH - fontSize * 1.5;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Spin the whole two-line 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 - Math.round(fontSize * 2.4));
|
||||||
stampCanvas.drawText('📍', baseX, baseY - fontSize * 1.4, iconPaint, iconFont);
|
stampCanvas.drawText('📍', baseX, baseY - fontSize * 1.4, iconPaint, iconFont);
|
||||||
stampCanvas.drawText(locationName, baseX + gap, baseY - fontSize * 1.4, textPaint, font);
|
stampCanvas.drawText(locationName, baseX + gap, baseY - fontSize * 1.4, textPaint, font);
|
||||||
stampCanvas.drawText('📷', baseX, baseY, iconPaint, iconFont);
|
stampCanvas.drawText('📷', baseX, baseY, iconPaint, iconFont);
|
||||||
stampCanvas.drawText(timestampStr, baseX + gap, baseY, textPaint, font);
|
stampCanvas.drawText(timestampStr, baseX + gap, baseY, textPaint, font);
|
||||||
|
stampCanvas.restore();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Custom watermark: baseline at the dragged fraction of the photo/window
|
// Custom watermark: baseline at the dragged fraction of the photo/window
|
||||||
@@ -623,7 +633,11 @@ export async function processAndExportPhoto(
|
|||||||
wmPaint.setColor(Skia.Color(customWm.color ?? '#f59e0b'));
|
wmPaint.setColor(Skia.Color(customWm.color ?? '#f59e0b'));
|
||||||
const tx = area.dx + clamp01(customWm.x) * area.dw;
|
const tx = area.dx + clamp01(customWm.x) * area.dw;
|
||||||
const ty = area.dy + clamp01(customWm.y) * area.dh + Math.round(wmSize * 0.35);
|
const ty = area.dy + clamp01(customWm.y) * area.dh + Math.round(wmSize * 0.35);
|
||||||
|
const wmRot = options?.watermarkRotation ?? 0;
|
||||||
|
stampCanvas.save();
|
||||||
|
if (wmRot) stampCanvas.rotate(wmRot, tx, ty);
|
||||||
stampCanvas.drawText(customWm.text, tx, ty, wmPaint, wmFont);
|
stampCanvas.drawText(customWm.text, tx, ty, wmPaint, wmFont);
|
||||||
|
stampCanvas.restore();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user