Re-derive the preview source at the crop's own resolution
A committed crop kept only its share of the 1600px whole-photo copy, so the stage then showed a 2x upscale and the photo read as broken. While a crop is live the source is remade 1/f larger (f = the crop's longest side as a fraction of the photo's), capped so the decode stays bounded; UNDO/CANCEL and a new photo put the 1600px copy back.
This commit is contained in:
@@ -30,6 +30,12 @@ import type { MsgKey } from './i18n/vi';
|
||||
// Preview renders on a downscaled copy; export always re-renders the original
|
||||
// bytes, so nothing shown here is baked into the file.
|
||||
const PREVIEW_MAX = 1600;
|
||||
// A committed crop keeps only a fraction of the photo, so the 1600px copy
|
||||
// leaves too few pixels behind and the result reads as blocky once the stage
|
||||
// scales it up. While a crop is live the source is re-derived larger, inverted
|
||||
// by that fraction, and capped here so the decode stays bounded.
|
||||
const CROP_PREVIEW_MAX = 3200;
|
||||
const CROP_PREVIEW_MIN_FRAC = 0.05;
|
||||
const PREVIEW_DEBOUNCE_MS = 120;
|
||||
|
||||
const FRAMES: { id: FrameId; label: MsgKey }[] = [
|
||||
@@ -166,6 +172,27 @@ export function Workspace() {
|
||||
const cropEditing = crop !== 'none' && !cropApplied;
|
||||
const cropLive = crop !== 'none' && cropApplied;
|
||||
const cropWH = crop !== 'none' && crop !== 'free' ? CROP_W_H[crop] : null;
|
||||
// A committed crop keeps only part of the photo, so the 1600px copy leaves
|
||||
// too few pixels behind and the stage shows a blocky upscale. While the crop
|
||||
// is live the source is re-derived larger — 1/f, where f is the crop's
|
||||
// longest side as a fraction of the photo's, capped so the decode stays
|
||||
// bounded. cropRect is in the rotated frame, so the shape is swapped first.
|
||||
const cropLongFrac = (() => {
|
||||
const d = photoDims;
|
||||
const turn = rotation % 180 !== 0;
|
||||
const f = Math.max(
|
||||
(turn ? d?.h ?? 0 : d?.w ?? 0) * cropRect.w,
|
||||
(turn ? d?.w ?? 0 : d?.h ?? 0) * cropRect.h
|
||||
);
|
||||
const long = Math.max(d?.w ?? 0, d?.h ?? 0);
|
||||
return long > 0 ? f / long : Math.min(cropRect.w, cropRect.h);
|
||||
})();
|
||||
const previewMax = cropLive
|
||||
? Math.min(
|
||||
CROP_PREVIEW_MAX,
|
||||
Math.max(PREVIEW_MAX, Math.round(PREVIEW_MAX / Math.max(cropLongFrac, CROP_PREVIEW_MIN_FRAC)))
|
||||
)
|
||||
: PREVIEW_MAX;
|
||||
|
||||
// --- undo -----------------------------------------------------------------
|
||||
// The stack holds whole looks. remember() reads a ref written during render,
|
||||
@@ -224,6 +251,10 @@ export function Workspace() {
|
||||
(new URLSearchParams(window.location.search).has('auth') ? 'signup' : null));
|
||||
|
||||
const renderToken = useRef(0);
|
||||
// Which previewMax the bytes in previewBytes were made at, so the upgrade
|
||||
// effect does not redo the load-time 1600px copy.
|
||||
const previewMaxRef = useRef(PREVIEW_MAX);
|
||||
const previewJobRef = useRef(0);
|
||||
|
||||
// --- boot -----------------------------------------------------------------
|
||||
useEffect(() => {
|
||||
@@ -308,6 +339,20 @@ export function Workspace() {
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [engine, previewBytes, recipe, frameId, cropEditing, cropLive, cropRect, rotation, useGeotag, gps, gpsStyle, markOn, markText, markStyle]);
|
||||
|
||||
// The source copy behind previewBytes, re-made only when previewMax moves:
|
||||
// applying a crop upgrades it, CANCEL/UNDO brings the 1600px one back.
|
||||
useEffect(() => {
|
||||
if (!source || previewMaxRef.current === previewMax) return;
|
||||
const job = ++previewJobRef.current;
|
||||
resizedJpeg(source.bytes, previewMax)
|
||||
.then((bytes) => {
|
||||
if (job !== previewJobRef.current) return;
|
||||
previewMaxRef.current = previewMax;
|
||||
setPreviewBytes(bytes);
|
||||
})
|
||||
.catch(() => undefined);
|
||||
}, [source, previewMax]);
|
||||
|
||||
// --- input ---------------------------------------------------------------
|
||||
// One photo, two ways in: a drop/pick (which also stores it for the next
|
||||
// visit) and the session restore, whose params are already in state and so
|
||||
@@ -330,6 +375,9 @@ export function Workspace() {
|
||||
if (prev) URL.revokeObjectURL(prev.url);
|
||||
return { bytes, name, url: URL.createObjectURL(blob) };
|
||||
});
|
||||
// The new photo's copy is the 1600px one; a live crop then upgrades it.
|
||||
previewMaxRef.current = PREVIEW_MAX;
|
||||
previewJobRef.current++;
|
||||
setPreviewBytes(preview);
|
||||
setGps(found);
|
||||
// A new photo starts a new history whose floor is the default look, so
|
||||
|
||||
Reference in New Issue
Block a user