feat(frame): give ROTATE quarter turns plus a fine straighten

This commit is contained in:
2026-09-11 11:17:39 +07:00
parent 6788d51fb4
commit 9ddaeac260
5 changed files with 141 additions and 29 deletions
+13 -7
View File
@@ -9,7 +9,7 @@ import { TONE_SKSL, getToneUniforms, toneIsActive, toneUniformArray } from './to
import { CINEMA_SKSL, getCinemaUniforms, cinemaIsActive } from './cinemaShader';
import { drawFrameOnCanvas, polaroidLayout, POLAROID_CARD, POLAROID_WIN_W, wallframeLayout, WALLFRAME_W, WALLFRAME_H, WALLFRAME_LAND_W, WALLFRAME_LAND_H } from './frameUtils';
import { patchJpegDpi } from './jpegDpi';
import { rotateSkImage90 } from './skiaImage';
import { applyPhotoRotation } from './skiaImage';
export interface ExportResult {
uri: string;
@@ -70,10 +70,12 @@ export interface ExportOptions {
// visible through the opening, so the file matches the preview. Null /
// undefined = centered 1x (the historical full-image draw).
frameWindowZoom?: { s: number; u: number; v: number } | null;
// Library FRAME-tab rotation (90 = clockwise). Applied to the decoded source
// before the ratio crop, so the frame layout and cover maths see exactly the
// orientation the preview showed.
photoRotation?: 0 | 90;
// Library FRAME-tab rotation: the quarter turn in degrees clockwise
// (0/90/180/270) and the fine STRAIGHTEN offset in degrees, applied to the
// decoded source before the ratio crop, so the frame layout and cover maths
// see exactly the orientation the preview showed.
photoRotation?: 0 | 90 | 180 | 270;
photoStraighten?: number;
}
// Lightroom-style "Screen" output sharpen: subtle unsharp mask. A 3x3
@@ -139,7 +141,8 @@ export async function processAndExportPhoto(
let skImage = skImage0;
// FRAME-tab rotation first: the pixel-dimension swap then propagates
// through the ratio crop, frame layout and cover maths unchanged.
if (options?.photoRotation === 90) skImage = rotateSkImage90(skImage) ?? skImage;
skImage =
applyPhotoRotation(skImage, options?.photoRotation, options?.photoStraighten) ?? skImage;
let width = skImage.width();
let height = skImage.height();
// 1b. Aspect-ratio crop (composition): center-largest upright crop of the
@@ -445,7 +448,10 @@ export async function processAndExportPhoto(
// Card proportions follow the UN-rotated source, so rotating the print
// inside the frame never resizes the frame (same rule as the viewfinder).
const frameAspect =
ratioWH ?? (options?.photoRotation === 90 ? height / width : width / height);
ratioWH ??
(options?.photoRotation === 90 || options?.photoRotation === 270
? height / width
: width / height);
const outH = Math.round(
width * (POLAROID_CARD.sideMargin + POLAROID_CARD.bottomDeck) +
(width * POLAROID_WIN_W) / frameAspect
+41
View File
@@ -25,3 +25,44 @@ export function rotateSkImage90(image: SkImage | null): SkImage | null {
canvas.drawImage(image, 0, 0);
return surface.makeImageSnapshot() ?? image;
}
// Fine rotation about the centre (the FRAME tab's STRAIGHTEN), keeping the
// original pixel dimensions: the content is scaled up just enough that the
// rotated rectangle still covers the frame, so no empty corner shows. The
// scale is the axis-aligned wrap of the counter-rotated frame — slightly
// conservative, which crops a hair more rather than leaking a corner.
// ponytail: one offscreen surface per call, same trade as rotateSkImage90.
export function rotateSkImageBy(image: SkImage | null, degrees: number): SkImage | null {
if (!image) return null;
const w = image.width();
const h = image.height();
if (!(w > 0 && h > 0)) return null;
if (!degrees) return image;
const rad = (degrees * Math.PI) / 180;
const cos = Math.abs(Math.cos(rad));
const sin = Math.abs(Math.sin(rad));
const k = Math.max((w * cos + h * sin) / w, (w * sin + h * cos) / h);
const surface = Skia.Surface.Make(w, h);
if (!surface) return image; // keep the unrotated photo on allocation failure
const canvas = surface.getCanvas();
canvas.clear(Skia.Color('transparent'));
canvas.translate(w / 2, h / 2);
canvas.rotate(degrees, 0, 0);
canvas.scale(k, k);
canvas.drawImage(image, -w / 2, -h / 2);
return surface.makeImageSnapshot() ?? image;
}
// 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.
export function applyPhotoRotation(
image: SkImage | null,
quarter = 0,
straighten = 0
): SkImage | null {
let out = image;
for (let i = 0; i < Math.round(quarter / 90) % 4; i++) out = rotateSkImage90(out);
if (straighten) out = rotateSkImageBy(out, straighten);
return out;
}