From 0c025463666461c140816c7fe7a9a7b8ac9ab51c Mon Sep 17 00:00:00 2001 From: Michelle Zan Date: Thu, 8 Oct 2026 13:10:35 +0800 Subject: [PATCH] Fix admin cover crop zoom controls. Remove full-bleed mask inset that blocked visible zoom, and zoom around the crop center. Co-authored-by: Cursor --- server/public/admin.html | 72 ++++++++++++++++++++++++++++++---------- 1 file changed, 54 insertions(+), 18 deletions(-) diff --git a/server/public/admin.html b/server/public/admin.html index 452a4fd..b782f82 100644 --- a/server/public/admin.html +++ b/server/public/admin.html @@ -55,7 +55,7 @@ max-width: none; cursor: grab; } .crop-mask { - position: absolute; inset: 0; pointer-events: none; + position: absolute; left: 0; top: 0; pointer-events: none; box-shadow: 0 0 0 9999px rgba(0, 0, 0, 0.45); border: 2px solid #fff; border-radius: 4px; } @@ -167,11 +167,8 @@ lastY: 0 } - function layoutCrop() { + function maskSize() { const stage = document.getElementById('cropStage') - const img = document.getElementById('cropImage') - const mask = document.getElementById('cropMask') - if (!stage || !img.naturalWidth) return const sw = stage.clientWidth const sh = stage.clientHeight let maskW = sw - 32 @@ -180,10 +177,31 @@ maskH = sh - 32 maskW = maskH * COVER_RATIO } - mask.style.width = `${maskW}px` - mask.style.height = `${maskH}px` - mask.style.left = `${(sw - maskW) / 2}px` - mask.style.top = `${(sh - maskH) / 2}px` + return { + maskW: maskW, + maskH: maskH, + maskLeft: (sw - maskW) / 2, + maskTop: (sh - maskH) / 2 + } + } + + function minCropScale() { + const size = maskSize() + return Math.max(size.maskW / cropState.naturalW, size.maskH / cropState.naturalH) + } + + function layoutCrop() { + const stage = document.getElementById('cropStage') + const img = document.getElementById('cropImage') + const mask = document.getElementById('cropMask') + if (!stage || !img.naturalWidth || !cropState.naturalW) return + const size = maskSize() + mask.style.right = 'auto' + mask.style.bottom = 'auto' + mask.style.width = `${size.maskW}px` + mask.style.height = `${size.maskH}px` + mask.style.left = `${size.maskLeft}px` + mask.style.top = `${size.maskTop}px` img.style.width = `${cropState.naturalW * cropState.scale}px` img.style.height = `${cropState.naturalH * cropState.scale}px` @@ -192,15 +210,28 @@ function fitCropImage() { const stage = document.getElementById('cropStage') - const maskW = Math.min(stage.clientWidth - 32, (stage.clientHeight - 32) * COVER_RATIO) - const maskH = maskW / COVER_RATIO - const scale = Math.max(maskW / cropState.naturalW, maskH / cropState.naturalH) + const scale = minCropScale() cropState.scale = scale cropState.x = (stage.clientWidth - cropState.naturalW * scale) / 2 cropState.y = (stage.clientHeight - cropState.naturalH * scale) / 2 layoutCrop() } + function zoomCrop(factor) { + if (!cropState.naturalW) return + const size = maskSize() + const cx = size.maskLeft + size.maskW / 2 + const cy = size.maskTop + size.maskH / 2 + const before = cropState.scale + const next = Math.min(Math.max(before * factor, minCropScale()), minCropScale() * 8) + if (Math.abs(next - before) < 0.0001) return + // Keep the point under the crop center stable while zooming. + cropState.x = cx - ((cx - cropState.x) / before) * next + cropState.y = cy - ((cy - cropState.y) / before) * next + cropState.scale = next + layoutCrop() + } + function openCoverCrop(file, targetInput, hint) { const modal = document.getElementById('cropModal') const img = document.getElementById('cropImage') @@ -270,14 +301,19 @@ }) stage.addEventListener('pointerup', () => { cropState.dragging = false }) stage.addEventListener('pointercancel', () => { cropState.dragging = false }) - document.getElementById('cropZoomIn').onclick = () => { - cropState.scale *= 1.12 - layoutCrop() + document.getElementById('cropZoomIn').onclick = (event) => { + event.preventDefault() + zoomCrop(1.15) } - document.getElementById('cropZoomOut').onclick = () => { - cropState.scale /= 1.12 - layoutCrop() + document.getElementById('cropZoomOut').onclick = (event) => { + event.preventDefault() + zoomCrop(1 / 1.15) } + stage.addEventListener('wheel', (event) => { + if (!document.getElementById('cropModal').classList.contains('on')) return + event.preventDefault() + zoomCrop(event.deltaY > 0 ? 1 / 1.08 : 1.08) + }, { passive: false }) document.getElementById('cropCancel').onclick = closeCoverCrop document.getElementById('cropConfirm').onclick = async () => { const hint = cropState.hint