web: mirror the frame with H-FLIP and V-FLIP, and stamp a typed place

ROTATE gains the two mirrors: H-FLIP and V-FLIP toggle one at a time and
stay on through the quarter turns and STRAIGHTEN, which makes them compose
with every rotation the strip already offers. ROTATE's own RESET levels the
whole frame, mirrors included.

The flip itself lands last, in screen space, so a mirrored photo is what the
eye sees rather than what the sensor saw; the pixels are copied axis-aligned,
so there is nothing to resample. Session state carries the two flags, so a
reopened photo comes back mirrored.

Also fixes the stamp: a typed PLACE NAME with no GPS fix now prints on its
own (latitude/longitude ride in as NaN), instead of the whole stamp and its
box being skipped for want of coordinates.
This commit is contained in:
2026-09-25 08:46:27 +07:00
parent 9d7a5beec2
commit c70edce8c1
5 changed files with 185 additions and 27 deletions
+41 -2
View File
@@ -76,13 +76,47 @@ export function rotateSkImageBy(image: SkImage | null, degrees: number): SkImage
}
}
// Mirror the photo left<->right (H-FLIP) and/or top<->bottom (V-FLIP). An
// axis-aligned mirror lands whole pixels on whole pixels, so the plain
// drawImage stays exact: no sampling option needed the way STRAIGHTEN's rotated
// edge does. The dimensions never change, so the crop stays valid.
// ponytail: one offscreen surface per call, same trade as rotateSkImage90.
export function flipSkImage(
image: SkImage | null,
horizontal: boolean,
vertical: boolean
): SkImage | null {
if (!image || (!horizontal && !vertical)) return image;
const w = image.width();
const h = image.height();
if (!(w > 0 && h > 0)) return null;
const surface = Skia.Surface.Make(w, h);
if (!surface) return image; // keep the unflipped photo on allocation failure
try {
const canvas = surface.getCanvas();
canvas.clear(Skia.Color('transparent'));
// A negative scale turns every pixel coordinate around, so the translate
// puts the mirrored copy back inside the surface.
canvas.scale(horizontal ? -1 : 1, vertical ? -1 : 1);
canvas.translate(horizontal ? -w : 0, vertical ? -h : 0);
canvas.drawImage(image, 0, 0);
return surface.makeImageSnapshot() ?? image;
} finally {
surface.dispose(); // see rotateSkImage90
}
}
// The whole FRAME-tab photo orientation in one call: the quarter turn first
// (rotateSkImage90 swaps the pixel dimensions, which then propagates through
// every consumer unchanged), then the fine STRAIGHTEN on top of it.
// every consumer unchanged), then the fine STRAIGHTEN on top of it, then the
// two mirrors — a flip is what the user sees, so it mirrors the photo as it
// stands, whatever turn and angle are already on it.
export function applyPhotoRotation(
image: SkImage | null,
quarter = 0,
straighten = 0
straighten = 0,
flipH = false,
flipV = false
): SkImage | null {
let out = image;
const turns = Math.round(quarter / 90) % 4;
@@ -98,5 +132,10 @@ export function applyPhotoRotation(
if (s && s !== out && out !== image) out.dispose(); // see above
out = s ?? out;
}
if (flipH || flipV) {
const f = flipSkImage(out, flipH, flipV);
if (f && f !== out && out !== image) out.dispose(); // see above
out = f ?? out;
}
return out;
}