@@ -189,12 +189,39 @@
}
}
- function minCropScale() {
+ // Cover = fill the crop window (may crop edges). Fit = show the whole image inside the window.
+ function coverScale() {
if (!cropState.naturalW || !cropState.naturalH) return 1
const size = maskSize()
return Math.max(size.maskW / cropState.naturalW, size.maskH / cropState.naturalH)
}
+ function fitScale() {
+ if (!cropState.naturalW || !cropState.naturalH) return 1
+ const size = maskSize()
+ return Math.min(size.maskW / cropState.naturalW, size.maskH / cropState.naturalH)
+ }
+
+ function clampCropPosition() {
+ const size = maskSize()
+ const w = cropState.naturalW * cropState.scale
+ const h = cropState.naturalH * cropState.scale
+ if (w >= size.maskW) {
+ const minX = size.maskLeft + size.maskW - w
+ const maxX = size.maskLeft
+ cropState.x = Math.min(maxX, Math.max(minX, cropState.x))
+ } else {
+ cropState.x = size.maskLeft + (size.maskW - w) / 2
+ }
+ if (h >= size.maskH) {
+ const minY = size.maskTop + size.maskH - h
+ const maxY = size.maskTop
+ cropState.y = Math.min(maxY, Math.max(minY, cropState.y))
+ } else {
+ cropState.y = size.maskTop + (size.maskH - h) / 2
+ }
+ }
+
function layoutCrop() {
const stage = document.getElementById('cropStage')
const img = document.getElementById('cropImage')
@@ -206,15 +233,9 @@
mask.style.left = `${size.maskLeft}px`
mask.style.top = `${size.maskTop}px`
+ clampCropPosition()
const w = cropState.naturalW * cropState.scale
const h = cropState.naturalH * cropState.scale
- // Cover the crop window so zoom/pan never leaves it empty.
- const minX = size.maskLeft + size.maskW - w
- const maxX = size.maskLeft
- const minY = size.maskTop + size.maskH - h
- const maxY = size.maskTop
- cropState.x = Math.min(maxX, Math.max(minX, cropState.x))
- cropState.y = Math.min(maxY, Math.max(minY, cropState.y))
img.style.width = `${w}px`
img.style.height = `${h}px`
img.style.left = `${cropState.x}px`
@@ -226,10 +247,10 @@
const size = maskSize()
// Modal must be visible first, otherwise stage size is 0 and scale collapses.
if (size.stageW < 40 || size.stageH < 40) return false
- const scale = minCropScale()
- cropState.scale = scale
- cropState.x = (size.stageW - cropState.naturalW * scale) / 2
- cropState.y = (size.stageH - cropState.naturalH * scale) / 2
+ // Start by showing the full original image; user can zoom in to crop.
+ cropState.scale = fitScale()
+ cropState.x = (size.stageW - cropState.naturalW * cropState.scale) / 2
+ cropState.y = (size.stageH - cropState.naturalH * cropState.scale) / 2
layoutCrop()
return true
}
@@ -243,12 +264,13 @@
return
}
const before = cropState.scale
- const minScale = minCropScale()
- const next = Math.min(Math.max(before * factor, minScale), minScale * 8)
+ // Allow shrinking well below "cover", so the full original image can be seen.
+ const minScale = Math.max(fitScale() * 0.35, coverScale() * 0.08)
+ const maxScale = coverScale() * 8
+ const next = Math.min(Math.max(before * factor, minScale), maxScale)
if (!Number.isFinite(next) || Math.abs(next - before) < 0.0001) return
const cx = size.maskLeft + size.maskW / 2
const cy = size.maskTop + size.maskH / 2
- // Keep the point under the crop center stable while zooming.
cropState.x = cx - ((cx - cropState.x) * next) / before
cropState.y = cy - ((cy - cropState.y) * next) / before
cropState.scale = next