Keep cover crop image visible while zooming.

Do not revoke the preview blob URL until the dialog closes, so resizing the image on zoom does not blank it.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Michelle Zan
2026-10-08 13:19:15 +08:00
co-authored by Cursor
parent 0edd9fedf2
commit b4e1a041de
+21 -9
View File
@@ -158,6 +158,7 @@
const cropState = { const cropState = {
targetInput: null, targetInput: null,
hint: null, hint: null,
objectUrl: '',
naturalW: 0, naturalW: 0,
naturalH: 0, naturalH: 0,
scale: 1, scale: 1,
@@ -207,6 +208,13 @@
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`
@@ -250,31 +258,35 @@
function openCoverCrop(file, targetInput, hint) { function openCoverCrop(file, targetInput, hint) {
const modal = document.getElementById('cropModal') const modal = document.getElementById('cropModal')
const img = document.getElementById('cropImage') const img = document.getElementById('cropImage')
if (cropState.objectUrl) URL.revokeObjectURL(cropState.objectUrl)
cropState.targetInput = targetInput cropState.targetInput = targetInput
cropState.hint = hint cropState.hint = hint
cropState.scale = 0 cropState.scale = 1
cropState.x = 0 cropState.x = 0
cropState.y = 0 cropState.y = 0
const url = URL.createObjectURL(file) // Keep the blob URL alive until the dialog closes. Revoking it on load
// makes the image vanish the next time width/height change (zoom).
cropState.objectUrl = URL.createObjectURL(file)
img.onload = () => { img.onload = () => {
cropState.naturalW = img.naturalWidth cropState.naturalW = img.naturalWidth
cropState.naturalH = img.naturalHeight cropState.naturalH = img.naturalHeight
URL.revokeObjectURL(url)
modal.classList.add('on') modal.classList.add('on')
requestAnimationFrame(() => { requestAnimationFrame(() => {
if (!fitCropImage()) { if (!fitCropImage()) setTimeout(() => fitCropImage(), 50)
setTimeout(() => fitCropImage(), 50)
}
}) })
} }
img.src = url img.src = cropState.objectUrl
} }
function closeCoverCrop() { function closeCoverCrop() {
document.getElementById('cropModal').classList.remove('on') document.getElementById('cropModal').classList.remove('on')
document.getElementById('cropImage').removeAttribute('src') const img = document.getElementById('cropImage')
img.removeAttribute('src')
if (cropState.objectUrl) {
URL.revokeObjectURL(cropState.objectUrl)
cropState.objectUrl = ''
}
cropState.targetInput = null cropState.targetInput = null
cropState.scale = 0
} }
function exportCroppedFile() { function exportCroppedFile() {