diff --git a/server/public/admin.html b/server/public/admin.html index 16dae4b..056233e 100644 --- a/server/public/admin.html +++ b/server/public/admin.html @@ -67,7 +67,7 @@
裁剪封面 -

拖动图片选择区域,比例已锁定为小程序首页封面(约 2.3:1)。裁剪后再上传,预览就不会被裁掉重要内容。

+

拖动图片选择区域,比例已锁定为小程序首页封面(约 2.3:1)。可放大看细节,也可缩小看到整张原图;空白处上传后为深色底。

裁剪预览
@@ -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