feat(frame): CROP ratios in the library editor, and the framed-photo fixes
CROP chip on the FRAME tab for the library's plain frames: NONE, FREE (drag the box) and the fixed ratios 1:1, 2:3, 3:2, 3:4, 4:3, 16:9. The preview draws the keep-rectangle over the photo and the export honours it pixel-for-pixel, pinching and dragging inside the band to pick the framing. - Viewfinder: build frameRect from the normalised rect so the Skia canvas never sees undefined width/height (nothing to commit until now), keep the watermark drag target across the WATERMARK tab so a zoomed photo no longer resets, and derive the crop band + export rect from the photo fit rect. - exportEngine: crop by fraction rect (or the ratio fallback) before the frame is composited. - Leaving a plain frame clears the crop, and the ratio strip closes with the CROP chip it belongs to.
This commit is contained in:
@@ -3,7 +3,7 @@ import { File } from 'expo-file-system';
|
||||
import * as FileSystem from 'expo-file-system/legacy';
|
||||
import * as MediaLibrary from 'expo-media-library/legacy';
|
||||
import { Asset } from 'expo-asset';
|
||||
import { Recipe, GPSInfo, FrameId, AspectRatio, ASPECT_RATIO_W_H } from '../types';
|
||||
import { Recipe, GPSInfo, FrameId, AspectRatio, ASPECT_RATIO_W_H, CropRect } from '../types';
|
||||
import { getSkiaColorMatrix, applyExposureGain } from './colorUtils';
|
||||
import { TONE_SKSL, getToneUniforms, toneIsActive } from './toneShader';
|
||||
import { CINEMA_SKSL, getCinemaUniforms, cinemaIsActive } from './cinemaShader';
|
||||
@@ -35,6 +35,13 @@ export interface ExportOptions {
|
||||
// viewfinder displayed. WALL FRAME ignores this (frame keeps its fixed
|
||||
// opening and export size). 'full'/undefined = no crop.
|
||||
aspect?: AspectRatio;
|
||||
// FRAME-tab CROP, fixed ratio: width/height of the kept area, center-cropped
|
||||
// exactly like `aspect` but for every non-full ratio. Wins over `aspect`.
|
||||
cropWH?: number;
|
||||
// FRAME-tab CROP, FREE: the kept rectangle as 0..1 fractions of the
|
||||
// (post-rotation) source image. Wins over `cropWH` and `aspect`. WALL FRAME
|
||||
// ignores both (fixed frame opening, cover-cropped by the artwork itself).
|
||||
cropRect?: CropRect | null;
|
||||
// Custom text watermark: typed text + 0..1 fractions of the photo area
|
||||
// (BASELINE anchor + 0.35em drop — same formula as the viewfinder overlay,
|
||||
// so the export lands where the preview showed it). Amber mono, no icons.
|
||||
@@ -139,18 +146,28 @@ export async function processAndExportPhoto(
|
||||
// requested ratio. Runs BEFORE any processing so frame/GPS/grain/sharpen
|
||||
// all apply to exactly the pixels the user framed. Wall Frame is exempt —
|
||||
// its export is the fixed frame artwork whose opening cover-crops anyway.
|
||||
const ratioWH = options?.aspect && options.aspect !== 'full' ? ASPECT_RATIO_W_H[options.aspect] : null;
|
||||
if (ratioWH && frameId !== 'wallframe') {
|
||||
const aspectWH = options?.aspect && options.aspect !== 'full' ? ASPECT_RATIO_W_H[options.aspect] : null;
|
||||
// FRAME-tab CROP (the App only passes it for the plain frames): an explicit
|
||||
// FREE rectangle wins, then a fixed crop ratio, then the camera composition
|
||||
// ratio.
|
||||
const cropRect = options?.cropRect ?? null;
|
||||
const ratioWH = cropRect ? null : options?.cropWH ?? aspectWH;
|
||||
if ((cropRect || ratioWH) && frameId !== 'wallframe') {
|
||||
const curAspect = width / height;
|
||||
let cropW = width;
|
||||
let cropH = height;
|
||||
let srcX = 0;
|
||||
let srcY = 0;
|
||||
if (curAspect > ratioWH) {
|
||||
cropW = Math.round(height * ratioWH);
|
||||
if (cropRect) {
|
||||
srcX = Math.max(0, Math.min(width - 1, Math.round(cropRect.x * width)));
|
||||
srcY = Math.max(0, Math.min(height - 1, Math.round(cropRect.y * height)));
|
||||
cropW = Math.max(1, Math.min(width - srcX, Math.round(cropRect.w * width)));
|
||||
cropH = Math.max(1, Math.min(height - srcY, Math.round(cropRect.h * height)));
|
||||
} else if (curAspect > (ratioWH as number)) {
|
||||
cropW = Math.round(height * (ratioWH as number));
|
||||
srcX = Math.round((width - cropW) / 2);
|
||||
} else {
|
||||
cropH = Math.round(width / ratioWH);
|
||||
cropH = Math.round(width / (ratioWH as number));
|
||||
srcY = Math.round((height - cropH) / 2);
|
||||
}
|
||||
const cropSurface = createSurface(cropW, cropH);
|
||||
|
||||
Reference in New Issue
Block a user