Allow cover crop to shrink below fill so the full image is visible.

Start from fit-to-window scale and lower the zoom-out floor.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Michelle Zan
2026-10-08 13:29:09 +08:00
co-authored by Cursor
parent b4e1a041de
commit aeb3cb0717
+38 -16
View File
@@ -67,7 +67,7 @@
<div class="crop-modal" id="cropModal"> <div class="crop-modal" id="cropModal">
<div class="crop-panel"> <div class="crop-panel">
<strong>裁剪封面</strong> <strong>裁剪封面</strong>
<p class="muted">拖动图片选择区域,比例已锁定为小程序首页封面(约 2.3:1)。裁剪后再上传,预览就不会被裁掉重要内容。</p> <p class="muted">拖动图片选择区域,比例已锁定为小程序首页封面(约 2.3:1)。可放大看细节,也可缩小看到整张原图;空白处上传后为深色底。</p>
<div class="crop-stage" id="cropStage"> <div class="crop-stage" id="cropStage">
<img id="cropImage" alt="裁剪预览" draggable="false" /> <img id="cropImage" alt="裁剪预览" draggable="false" />
<div class="crop-mask" id="cropMask"></div> <div class="crop-mask" id="cropMask"></div>
@@ -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 if (!cropState.naturalW || !cropState.naturalH) return 1
const size = maskSize() const size = maskSize()
return Math.max(size.maskW / cropState.naturalW, size.maskH / cropState.naturalH) 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() { function layoutCrop() {
const stage = document.getElementById('cropStage') const stage = document.getElementById('cropStage')
const img = document.getElementById('cropImage') const img = document.getElementById('cropImage')
@@ -206,15 +233,9 @@
mask.style.left = `${size.maskLeft}px` mask.style.left = `${size.maskLeft}px`
mask.style.top = `${size.maskTop}px` mask.style.top = `${size.maskTop}px`
clampCropPosition()
const w = cropState.naturalW * cropState.scale const w = cropState.naturalW * cropState.scale
const h = cropState.naturalH * 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.width = `${w}px`
img.style.height = `${h}px` img.style.height = `${h}px`
img.style.left = `${cropState.x}px` img.style.left = `${cropState.x}px`
@@ -226,10 +247,10 @@
const size = maskSize() const size = maskSize()
// Modal must be visible first, otherwise stage size is 0 and scale collapses. // Modal must be visible first, otherwise stage size is 0 and scale collapses.
if (size.stageW < 40 || size.stageH < 40) return false if (size.stageW < 40 || size.stageH < 40) return false
const scale = minCropScale() // Start by showing the full original image; user can zoom in to crop.
cropState.scale = scale cropState.scale = fitScale()
cropState.x = (size.stageW - cropState.naturalW * scale) / 2 cropState.x = (size.stageW - cropState.naturalW * cropState.scale) / 2
cropState.y = (size.stageH - cropState.naturalH * scale) / 2 cropState.y = (size.stageH - cropState.naturalH * cropState.scale) / 2
layoutCrop() layoutCrop()
return true return true
} }
@@ -243,12 +264,13 @@
return return
} }
const before = cropState.scale const before = cropState.scale
const minScale = minCropScale() // Allow shrinking well below "cover", so the full original image can be seen.
const next = Math.min(Math.max(before * factor, minScale), minScale * 8) 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 if (!Number.isFinite(next) || Math.abs(next - before) < 0.0001) return
const cx = size.maskLeft + size.maskW / 2 const cx = size.maskLeft + size.maskW / 2
const cy = size.maskTop + size.maskH / 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.x = cx - ((cx - cropState.x) * next) / before
cropState.y = cy - ((cy - cropState.y) * next) / before cropState.y = cy - ((cy - cropState.y) * next) / before
cropState.scale = next cropState.scale = next