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
+65 -12
View File
@@ -32,6 +32,7 @@ type GroupKey =
| 'gpsName'
| 'gpsTime'
| 'crop'
| 'rotate'
// Not a strip: the WATERMARK row REPLACES the frame chips (sub-panel).
| 'wm';
@@ -81,10 +82,13 @@ interface AdjustmentPanelProps {
onToggleFavorite: (id: string) => void;
onUpdateAdjustments: (adjustments: Partial<ColorAdjustments>) => void;
onUpdateFrame: (frameId: FrameId) => void;
// FRAME tab: turn the inserted library photo 90deg (landscape <-> portrait)
// so it fits the chosen frame better.
photoRotation: 0 | 90;
onToggleRotation: () => void;
// FRAME tab: turn the inserted library photo by a quarter turn (0/90/180/
// 270deg clockwise) so it fits the chosen frame better, plus a fine
// STRAIGHTEN offset (-45..45deg) applied on top of that turn.
photoRotation: 0 | 90 | 180 | 270;
photoStraighten: number;
onPickRotation: (deg: 0 | 90 | 180 | 270) => void;
onStraighten: (deg: number) => void;
// FRAME tab (WALL FRAME only): hang the artwork landscape instead of the
// default portrait rotation.
wallLandscape: boolean;
@@ -213,6 +217,9 @@ export default function AdjustmentPanel({
onUpdateAdjustments,
onUpdateFrame,
photoRotation,
photoStraighten,
onPickRotation,
onStraighten,
wallLandscape,
onToggleWallLandscape,
onToggleGeotag,
@@ -224,7 +231,6 @@ export default function AdjustmentPanel({
onAcceptCustomWm,
onCancelCustomWm,
wmEditNonce,
onToggleRotation,
wmRotation,
onRotateWm,
onWmPanelChange,
@@ -421,7 +427,21 @@ export default function AdjustmentPanel({
: p
),
filters: toRows(PARAM_DEFS.filters),
frame: [],
// FRAME carries one slider row: the fine STRAIGHTEN angle. The ROTATE
// strip's STRAIGHTEN option opens it, exactly like COLOR TEMP's.
frame: [
{
key: 'straighten',
label: 'STRAIGHTEN',
value: photoStraighten,
default: 0,
min: -45,
max: 45,
step: 0.5,
display: (v: number) => `${v > 0 ? '+' : ''}${v}°`,
onChange: (v: number) => onStraighten(v),
},
],
};
// TEMP chip's value: the remembered preset while the engine pair still
@@ -546,6 +566,31 @@ export default function AdjustmentPanel({
],
onPick: (v) => onUpdateCropRatio(v as CropRatio),
},
// ROTATE: the quarter turns plus the fine STRAIGHTEN slider in one strip.
// Its RESET undoes this parameter only (quarter turn + fine angle).
rotate: {
label: 'ROTATE', off: '0',
// 'straighten' while its slider row is open, so that chip highlights.
value: openParam === 'straighten' ? 'straighten' : String(photoRotation),
options: [
{ v: 'reset', d: 'RESET' },
{ v: '0', d: '0' },
{ v: '90', d: '90' },
{ v: '180', d: '180' },
{ v: '270', d: '270' },
{ v: 'straighten', d: 'STRAIGHTEN (±45°)' },
],
onPick: (v) => {
if (v === 'reset') {
onPickRotation(0);
onStraighten(0);
} else if (v === 'straighten') {
toggleParam('straighten');
} else {
onPickRotation(Number(v) as 0 | 90 | 180 | 270);
}
},
},
// PHOTO STYLE: the eight film simulations, collapsed into one strip chip.
// Its value is the applied sim's id — or '' when a saved recipe is active
// (then the chip reads as "not the startup sim" and glows amber). The label
@@ -591,6 +636,19 @@ export default function AdjustmentPanel({
label: wbTempLabel(),
});
// ROTATE's chip names the applied turn — and the fine angle once it is set
// (the strip holds the options; nothing else names the angle).
const rotateChip = (): ChipDef => {
const label = `ROTATE ${photoRotation}`;
return {
...groupChip('rotate'),
label: photoStraighten
? `${label} · ${photoStraighten > 0 ? '+' : ''}${photoStraighten}°`
: label,
amberValue: openGroup !== 'rotate' && (photoRotation !== 0 || photoStraighten !== 0),
};
};
// WATERMARK is a sub-panel of FRAME now (the rail tab is gone): its chip swaps
// the frame chips for the watermark controls, behind a breadcrumb back.
const wmOn = useGeotag || customWm.enabled;
@@ -777,12 +835,7 @@ export default function AdjustmentPanel({
(val) => onUpdateFrame(val as FrameId)
)}
{cropEnabled && renderChip(groupChip('crop'))}
{renderChip({
key: 'rotate',
label: photoRotation === 0 ? 'ROTATE 90' : 'ROTATE 0',
active: photoRotation !== 0,
onPress: onToggleRotation,
})}
{renderChip(rotateChip())}
{selectedFrame === 'wallframe' &&
renderChip({
key: 'wall-orient',
+10 -6
View File
@@ -32,7 +32,7 @@ import { getSkiaColorMatrix, applyExposureGain } from '../utils/colorUtils';
import { TONE_SKSL, getToneUniforms, toneUniformArray } from '../utils/toneShader';
import { CINEMA_SKSL, getCinemaUniforms, cinemaOffFlat } from '../utils/cinemaShader';
import { polaroidLayout, wallframeLayout, WALLFRAME_PNG_W, WALLFRAME_PNG_H } from '../utils/frameUtils';
import { rotateSkImage90 } from '../utils/skiaImage';
import { applyPhotoRotation } from '../utils/skiaImage';
interface ViewfinderProps {
mode: 'camera' | 'library';
@@ -49,8 +49,11 @@ interface ViewfinderProps {
// custom-watermark position) hides under an open panel. Camera ignores this:
// its feed rects are normalized against the real window.
availableHeight?: number | null;
// Library only: user rotation of the inserted photo, 0 or 90deg clockwise.
photoRotation?: 0 | 90;
// Library only: user rotation of the inserted photo — the quarter turn in
// degrees clockwise (0/90/180/270) plus the fine STRAIGHTEN offset, applied
// on top of that turn.
photoRotation?: 0 | 90 | 180 | 270;
photoStraighten?: number;
// WATERMARK tab: double-tap ON the mark re-opens the text editor (App bumps
// a nonce so the panel puts the caret back in its input).
onWmEdit?: () => void;
@@ -230,6 +233,7 @@ const Viewfinder = forwardRef<ViewfinderHandle, ViewfinderProps>(function Viewfi
onWmEdit,
wmEditing = false,
photoRotation = 0,
photoStraighten = 0,
wmRotation = 0,
wallLandscape = false,
}: ViewfinderProps,
@@ -327,8 +331,8 @@ const Viewfinder = forwardRef<ViewfinderHandle, ViewfinderProps>(function Viewfi
// flows through every derived rect (polaroid window aspect, cover crop,
// watermark area) and the export matches the preview for free.
const skiaImage = useMemo(
() => (photoRotation === 90 ? rotateSkImage90(loadedImage) : loadedImage),
[loadedImage, photoRotation]
() => applyPhotoRotation(loadedImage, photoRotation, photoStraighten),
[loadedImage, photoRotation, photoStraighten]
);
// Wall-frame PNG artwork (project root). Loaded lazily so other frames pay
// no asset cost; source switches to '' when unselected (useImage → null).
@@ -1196,7 +1200,7 @@ const Viewfinder = forwardRef<ViewfinderHandle, ViewfinderProps>(function Viewfi
// stretched, and the card keeps its dimensions.
const polaroidFit: 'cover' | 'contain' | 'fill' = ratioWH
? 'cover'
: photoRotation === 90
: photoRotation === 90 || photoRotation === 270
? 'contain'
: 'fill';
const polaroidGeo = useMemo(() => {