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-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
|
||||||
|
|||||||
Reference in New Issue
Block a user