diff --git a/js/components/image-gallery.js b/js/components/image-gallery.js index 21403ce..244d9fe 100644 --- a/js/components/image-gallery.js +++ b/js/components/image-gallery.js @@ -14,12 +14,14 @@ class ImageGallery extends HTMLElement { this.isDragging = false; this.startX = 0; this.startY = 0; - this.lastTranslateX = 0; - this.lastTranslateY = 0; // Pinch zoom state this.initialPinchDistance = 0; this.initialZoom = 1; + this.pinchCenterX = 0; + this.pinchCenterY = 0; + this.initialTranslateX = 0; + this.initialTranslateY = 0; } connectedCallback() { @@ -117,16 +119,56 @@ class ImageGallery extends HTMLElement { e.preventDefault(); e.stopPropagation(); - const delta = e.deltaY > 0 ? -0.1 : 0.1; - this.zoom(delta); + const delta = e.deltaY > 0 ? -0.15 : 0.15; + + // Get mouse position relative to modal content center + const modalContent = this.shadowRoot.querySelector(".modal-content"); + const rect = modalContent.getBoundingClientRect(); + const mouseX = e.clientX - rect.left - rect.width / 2; + const mouseY = e.clientY - rect.top - rect.height / 2; + + this.zoomToPoint(delta, mouseX, mouseY); + } + + zoomToPoint(delta, pointX, pointY) { + const modalImage = this.shadowRoot.querySelector(".modal-image"); + if (!modalImage) return; + + const prevZoom = this.currentZoom; + const newZoom = Math.max(1, Math.min(4, this.currentZoom + delta)); + + if (newZoom === prevZoom) return; + + // Calculate new translate to zoom towards point + if (newZoom === 1) { + this.translateX = 0; + this.translateY = 0; + } else { + const zoomRatio = newZoom / prevZoom; + this.translateX = pointX - (pointX - this.translateX) * zoomRatio; + this.translateY = pointY - (pointY - this.translateY) * zoomRatio; + } + + this.currentZoom = newZoom; + this.updateImageTransform(false); + modalImage.style.cursor = this.currentZoom > 1 ? "grab" : "default"; } handleTouchStart(e) { if (e.touches.length === 2) { // Pinch zoom start e.preventDefault(); + this.isDragging = false; // Stop any ongoing drag this.initialPinchDistance = this.getPinchDistance(e.touches); this.initialZoom = this.currentZoom; + this.initialTranslateX = this.translateX; + this.initialTranslateY = this.translateY; + + // Get pinch center relative to viewport + const modalContent = this.shadowRoot.querySelector(".modal-content"); + const rect = modalContent.getBoundingClientRect(); + this.pinchCenterX = ((e.touches[0].clientX + e.touches[1].clientX) / 2) - rect.left - rect.width / 2; + this.pinchCenterY = ((e.touches[0].clientY + e.touches[1].clientY) / 2) - rect.top - rect.height / 2; } else if (e.touches.length === 1 && this.currentZoom > 1) { // Single touch drag when zoomed this.startDrag(e); @@ -134,21 +176,27 @@ class ImageGallery extends HTMLElement { } handleTouchMove(e) { - if (e.touches.length === 2) { - // Pinch zoom + if (e.touches.length === 2 && this.initialPinchDistance > 0) { + // Pinch zoom with zoom-to-point e.preventDefault(); const currentDistance = this.getPinchDistance(e.touches); const scale = currentDistance / this.initialPinchDistance; const newZoom = Math.max(1, Math.min(4, this.initialZoom * scale)); + const prevZoom = this.currentZoom; this.currentZoom = newZoom; if (this.currentZoom === 1) { this.translateX = 0; this.translateY = 0; + } else { + // Adjust translate to zoom towards pinch center + const zoomRatio = this.currentZoom / this.initialZoom; + this.translateX = this.pinchCenterX - (this.pinchCenterX - this.initialTranslateX) * zoomRatio; + this.translateY = this.pinchCenterY - (this.pinchCenterY - this.initialTranslateY) * zoomRatio; } - this.updateImageTransform(); + this.updateImageTransform(false); // No transition during pinch const modalImage = this.shadowRoot.querySelector(".modal-image"); if (modalImage) { @@ -209,7 +257,12 @@ class ImageGallery extends HTMLElement { this.translateX = clientX - this.startX; this.translateY = clientY - this.startY; - this.updateImageTransform(); + // Use no transition during drag for smooth movement + const modalImage = this.shadowRoot.querySelector(".modal-image"); + if (modalImage) { + modalImage.style.transition = "none"; + modalImage.style.transform = `scale(${this.currentZoom}) translate(${this.translateX / this.currentZoom}px, ${this.translateY / this.currentZoom}px)`; + } } endDrag() { @@ -224,10 +277,19 @@ class ImageGallery extends HTMLElement { } } - updateImageTransform() { + updateImageTransform(useTransition = true) { const modalImage = this.shadowRoot.querySelector(".modal-image"); if (modalImage) { + if (!useTransition) { + modalImage.style.transition = "none"; + } modalImage.style.transform = `scale(${this.currentZoom}) translate(${this.translateX / this.currentZoom}px, ${this.translateY / this.currentZoom}px)`; + + // Force reflow and restore transition + if (!useTransition) { + modalImage.offsetHeight; // Force reflow + modalImage.style.transition = "transform 0.2s ease"; + } } } @@ -238,13 +300,25 @@ class ImageGallery extends HTMLElement { if (modal && modalImage && this.images[index]) { modalImage.src = this.images[index]; - // Reset zoom and pan + // Reset all state this.currentZoom = 1; this.translateX = 0; this.translateY = 0; - this.updateImageTransform(); + this.isDragging = false; + this.initialPinchDistance = 0; + this.initialZoom = 1; + this.initialTranslateX = 0; + this.initialTranslateY = 0; + // Reset transform without transition + modalImage.style.transition = "none"; + modalImage.style.transform = "scale(1) translate(0px, 0px)"; modalImage.style.cursor = "default"; + + // Force reflow then restore transition + modalImage.offsetHeight; + modalImage.style.transition = "transform 0.2s ease"; + modal.classList.add("open"); document.body.style.overflow = "hidden"; } @@ -278,10 +352,19 @@ class ImageGallery extends HTMLElement { resetZoom() { const modalImage = this.shadowRoot.querySelector(".modal-image"); if (modalImage) { + // Reset all state this.currentZoom = 1; this.translateX = 0; this.translateY = 0; - this.updateImageTransform(); + this.isDragging = false; + this.initialPinchDistance = 0; + this.initialZoom = 1; + this.initialTranslateX = 0; + this.initialTranslateY = 0; + + // Ensure transition is enabled for smooth reset animation + modalImage.style.transition = "transform 0.3s ease"; + modalImage.style.transform = "scale(1) translate(0px, 0px)"; modalImage.style.cursor = "default"; } }