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:
+38
-16
@@ -67,7 +67,7 @@
|
||||
<div class="crop-modal" id="cropModal">
|
||||
<div class="crop-panel">
|
||||
<strong>裁剪封面</strong>
|
||||
<p class="muted">拖动图片选择区域,比例已锁定为小程序首页封面(约 2.3:1)。裁剪后再上传,预览就不会被裁掉重要内容。</p>
|
||||
<p class="muted">拖动图片选择区域,比例已锁定为小程序首页封面(约 2.3:1)。可放大看细节,也可缩小看到整张原图;空白处上传后为深色底。</p>
|
||||
<div class="crop-stage" id="cropStage">
|
||||
<img id="cropImage" alt="裁剪预览" draggable="false" />
|
||||
<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
|
||||
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
|
||||
|
||||
Reference in New Issue
Block a user