diff --git a/app.js b/app.js index caecb23..9332253 100644 --- a/app.js +++ b/app.js @@ -26,8 +26,17 @@ let photoDate = new Date(); let locationLabel = null; // "City, Country" from EXIF GPS + server-side reverse geocode, or null + // Slack the fixed 3:2 crop leaves to pan within, in original-photo pixel + // space — recomputed every rebuildSource(), read by the drag handler. + let cropBounds = { iw: 0, ih: 0, cropW: 0, cropH: 0, outW: 0, outH: 0 }; + const panHint = document.getElementById('pan-hint'); + const params = { template: 'cream', + // Normalized 0-1 position of the crop window within whatever slack the + // fixed 3:2 aspect leaves — 0.5/0.5 is centered. + cropOffsetX: 0.5, + cropOffsetY: 0.5, caption: '', }; @@ -73,6 +82,8 @@ editorCard.hidden = false; resultPanel.hidden = true; torSeed = Math.random(); + params.cropOffsetX = 0.5; + params.cropOffsetY = 0.5; rebuildSource(); render(); draftCaption(); @@ -107,10 +118,10 @@ const currentAspect = iw / ih; if (currentAspect > targetAspect) { cropW = Math.round(ih * targetAspect); - cropX = Math.round((iw - cropW) / 2); + cropX = Math.round((iw - cropW) * params.cropOffsetX); } else { cropH = Math.round(iw / targetAspect); - cropY = Math.round((ih - cropH) / 2); + cropY = Math.round((ih - cropH) * params.cropOffsetY); } const maxDim = 1800; @@ -126,6 +137,9 @@ sourceCtx.clearRect(0, 0, outW, outH); sourceCtx.drawImage(originalImg, cropX, cropY, cropW, cropH, 0, 0, outW, outH); + cropBounds = { iw, ih, cropW, cropH, outW, outH }; + updatePanAffordance(); + // Full card = photo area + a border margin all around, matching a // physical postcard's white/cream frame. const margin = Math.round(outW * 0.06); @@ -133,6 +147,72 @@ canvas.height = outH + margin * 2; } + function hasPanSlack() { + return cropBounds.iw > cropBounds.cropW || cropBounds.ih > cropBounds.cropH; + } + + function updatePanAffordance() { + const pannable = hasPanSlack(); + canvas.classList.toggle('pannable', pannable); + if (panHint) panHint.hidden = !pannable; + } + + // Drag-to-reposition, same trick as latent: convert pointer movement in + // CSS pixels to original-photo pixels via crop-width/display-width, + // through the outW downscale (canvas's own pixel space === outW/outH, + // not cropW/cropH) and the canvas's CSS scaling, so it stays 1:1 + // regardless of either. Dragging from anywhere on the canvas is fine + // even though the border margin isn't photo -- the gesture still reads + // as "drag the photo," and the margin is only ~6% per side. + let dragState = null; + let panRaf = null; + + canvas.addEventListener('pointerdown', (e) => { + if (!hasPanSlack()) return; + const { iw, ih, cropW, cropH } = cropBounds; + dragState = { + pointerId: e.pointerId, + startX: e.clientX, + startY: e.clientY, + cropX0: Math.round((iw - cropW) * params.cropOffsetX), + cropY0: Math.round((ih - cropH) * params.cropOffsetY), + }; + canvas.setPointerCapture(e.pointerId); + canvas.classList.add('panning'); + }); + + canvas.addEventListener('pointermove', (e) => { + if (!dragState || e.pointerId !== dragState.pointerId) return; + const rect = canvas.getBoundingClientRect(); + const { iw, ih, cropW, cropH, outW, outH } = cropBounds; + const cropPxPerCssPxX = (canvas.width / rect.width) * (cropW / outW); + const cropPxPerCssPxY = (canvas.height / rect.height) * (cropH / outH); + const deltaX = (e.clientX - dragState.startX) * cropPxPerCssPxX; + const deltaY = (e.clientY - dragState.startY) * cropPxPerCssPxY; + + const slackX = iw - cropW; + const slackY = ih - cropH; + const newCropX = Math.min(Math.max(dragState.cropX0 - deltaX, 0), slackX); + const newCropY = Math.min(Math.max(dragState.cropY0 - deltaY, 0), slackY); + params.cropOffsetX = slackX > 0 ? newCropX / slackX : 0.5; + params.cropOffsetY = slackY > 0 ? newCropY / slackY : 0.5; + + if (panRaf) return; + panRaf = requestAnimationFrame(() => { + panRaf = null; + rebuildSource(); + render(); + }); + }); + + function endPan(e) { + if (!dragState || (e && e.pointerId !== dragState.pointerId)) return; + canvas.classList.remove('panning'); + dragState = null; + } + canvas.addEventListener('pointerup', endPan); + canvas.addEventListener('pointercancel', endPan); + function render() { if (!originalImg) return; const w = canvas.width; diff --git a/index.html b/index.html index ac8912b..a67925f 100644 --- a/index.html +++ b/index.html @@ -32,6 +32,7 @@
Drag the photo to reposition the crop