mirror of
https://github.com/snapotter-hq/SnapOtter.git
synced 2026-08-03 07:46:42 +02:00
feat(passport-photo): true WYSIWYG - zoom and drag affect the download
- Zoom now controls the actual crop: zoom in = tighter crop, zoom out = more body - Drag adjusts face position within the frame - Backend receives zoom value and applies the same zoomed crop - Download produces exactly what the user sees on the canvas - Zoom range 0.5x-3x (can zoom out to show more body) - "What you see is what you download" label
This commit is contained in:
@@ -37,6 +37,7 @@ const generateSettingsSchema = z.object({
|
|||||||
dpi: z.number().min(72).max(600).default(300),
|
dpi: z.number().min(72).max(600).default(300),
|
||||||
customWidthMm: z.number().optional(),
|
customWidthMm: z.number().optional(),
|
||||||
customHeightMm: z.number().optional(),
|
customHeightMm: z.number().optional(),
|
||||||
|
zoom: z.number().min(0.5).max(3).default(1),
|
||||||
adjustX: z.number().default(0),
|
adjustX: z.number().default(0),
|
||||||
adjustY: z.number().default(0),
|
adjustY: z.number().default(0),
|
||||||
landmarks: landmarksSchema,
|
landmarks: landmarksSchema,
|
||||||
@@ -295,6 +296,7 @@ export function registerPassportPhoto(app: FastifyInstance) {
|
|||||||
dpi: userDpi,
|
dpi: userDpi,
|
||||||
customWidthMm,
|
customWidthMm,
|
||||||
customHeightMm,
|
customHeightMm,
|
||||||
|
zoom: userZoom,
|
||||||
adjustX,
|
adjustX,
|
||||||
adjustY,
|
adjustY,
|
||||||
landmarks: rawLandmarks,
|
landmarks: rawLandmarks,
|
||||||
@@ -358,8 +360,15 @@ export function registerPassportPhoto(app: FastifyInstance) {
|
|||||||
const photoWidthPx = photoHeightPx * aspectRatio;
|
const photoWidthPx = photoHeightPx * aspectRatio;
|
||||||
|
|
||||||
// Position: eye line should be at eyeLineFromBottom from photo bottom
|
// Position: eye line should be at eyeLineFromBottom from photo bottom
|
||||||
const topY = eyeYPx - photoHeightPx * (1 - docSpec.eyeLineFromBottom);
|
const baseTopY = eyeYPx - photoHeightPx * (1 - docSpec.eyeLineFromBottom);
|
||||||
const leftX = faceCenterXPx - photoWidthPx / 2;
|
const baseLeftX = faceCenterXPx - photoWidthPx / 2;
|
||||||
|
|
||||||
|
// Apply zoom: zoom > 1 = tighter crop (less body), zoom < 1 = wider (more body)
|
||||||
|
// The zoomed region is centered on the base crop
|
||||||
|
const zoomedW = photoWidthPx / userZoom;
|
||||||
|
const zoomedH = photoHeightPx / userZoom;
|
||||||
|
const leftX = baseLeftX + (photoWidthPx - zoomedW) / 2;
|
||||||
|
const topY = baseTopY + (photoHeightPx - zoomedH) / 2;
|
||||||
|
|
||||||
// Parse background color
|
// Parse background color
|
||||||
const hex = bgColor.replace("#", "");
|
const hex = bgColor.replace("#", "");
|
||||||
@@ -381,8 +390,8 @@ export function registerPassportPhoto(app: FastifyInstance) {
|
|||||||
// image with background color so the full intended region is available.
|
// image with background color so the full intended region is available.
|
||||||
const rawLeft = Math.round(leftX);
|
const rawLeft = Math.round(leftX);
|
||||||
const rawTop = Math.round(topY);
|
const rawTop = Math.round(topY);
|
||||||
const rawW = Math.round(photoWidthPx);
|
const rawW = Math.round(zoomedW);
|
||||||
const rawH = Math.round(photoHeightPx);
|
const rawH = Math.round(zoomedH);
|
||||||
|
|
||||||
const padLeft = Math.max(0, -rawLeft);
|
const padLeft = Math.max(0, -rawLeft);
|
||||||
const padTop = Math.max(0, -rawTop);
|
const padTop = Math.max(0, -rawTop);
|
||||||
|
|||||||
@@ -361,6 +361,7 @@ export function PassportPhotoSettings() {
|
|||||||
setAnalyzing,
|
setAnalyzing,
|
||||||
generating,
|
generating,
|
||||||
setGenerating,
|
setGenerating,
|
||||||
|
zoom,
|
||||||
adjustX,
|
adjustX,
|
||||||
adjustY,
|
adjustY,
|
||||||
} = usePassportPhotoStore();
|
} = usePassportPhotoStore();
|
||||||
@@ -485,6 +486,7 @@ export function PassportPhotoSettings() {
|
|||||||
bgColor,
|
bgColor,
|
||||||
maxFileSizeKb,
|
maxFileSizeKb,
|
||||||
dpi,
|
dpi,
|
||||||
|
zoom,
|
||||||
adjustX,
|
adjustX,
|
||||||
adjustY,
|
adjustY,
|
||||||
landmarks: analyzeResult.landmarks,
|
landmarks: analyzeResult.landmarks,
|
||||||
@@ -1063,7 +1065,7 @@ export function PassportPhotoPreview() {
|
|||||||
const handleWheel = useCallback(
|
const handleWheel = useCallback(
|
||||||
(e: React.WheelEvent<HTMLCanvasElement>) => {
|
(e: React.WheelEvent<HTMLCanvasElement>) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
setZoom(Math.max(1, Math.min(3, zoom + (e.deltaY > 0 ? -0.1 : 0.1))));
|
setZoom(Math.max(0.5, Math.min(3, zoom + (e.deltaY > 0 ? -0.1 : 0.1))));
|
||||||
},
|
},
|
||||||
[zoom, setZoom],
|
[zoom, setZoom],
|
||||||
);
|
);
|
||||||
@@ -1107,7 +1109,7 @@ export function PassportPhotoPreview() {
|
|||||||
<div className="flex items-center gap-2 shrink-0">
|
<div className="flex items-center gap-2 shrink-0">
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => setZoom(Math.max(1, zoom - 0.25))}
|
onClick={() => setZoom(Math.max(0.5, zoom - 0.25))}
|
||||||
className="p-1.5 rounded-lg border border-border text-muted-foreground hover:text-foreground hover:bg-muted transition-colors"
|
className="p-1.5 rounded-lg border border-border text-muted-foreground hover:text-foreground hover:bg-muted transition-colors"
|
||||||
title="Zoom out"
|
title="Zoom out"
|
||||||
>
|
>
|
||||||
@@ -1139,11 +1141,9 @@ export function PassportPhotoPreview() {
|
|||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{zoom !== 1 && (
|
|
||||||
<p className="text-[10px] text-muted-foreground text-center -mt-2">
|
<p className="text-[10px] text-muted-foreground text-center -mt-2">
|
||||||
Zoom is for inspection only. Download is always the full photo.
|
What you see is what you download
|
||||||
</p>
|
</p>
|
||||||
)}
|
|
||||||
|
|
||||||
{/* Canvas preview */}
|
{/* Canvas preview */}
|
||||||
<div className="flex-1 flex items-center justify-center overflow-auto min-h-0 w-full">
|
<div className="flex-1 flex items-center justify-center overflow-auto min-h-0 w-full">
|
||||||
|
|||||||
Reference in New Issue
Block a user