gallery pinch zoom

This commit is contained in:
babayaga 2025-12-25 14:25:19 +01:00
parent 37b0244517
commit 5df7b5915f

View File

@ -65,16 +65,24 @@ const locale = Astro.currentLocale || "en";
images: ${JSON.stringify(images)},
lastTapTime: 0,
doubleTapDelay: 300,
scale: 1,
panX: 0,
panY: 0,
isPanning: false,
startPanX: 0,
startPanY: 0,
handleSwipe() {
if (!this.isSwiping) return;
if (!this.isSwiping || this.scale > 1) return; // Disable swipe when zoomed
const swipeDistance = this.touchEndX - this.touchStartX;
if (Math.abs(swipeDistance) >= this.minSwipeDistance) {
if (swipeDistance > 0 && this.currentIndex > 0) {
// Swiped right, show previous image
this.currentIndex--;
this.resetZoom();
} else if (swipeDistance < 0 && this.currentIndex < this.total - 1) {
// Swiped left, show next image
this.currentIndex++;
this.resetZoom();
}
}
this.isSwiping = false;
@ -82,6 +90,7 @@ const locale = Astro.currentLocale || "en";
preloadAndOpen() {
if (this.isSwiping) return;
this.lightboxLoaded = false;
this.resetZoom();
let img = new Image();
img.src = this.images[this.currentIndex].src;
img.onload = () => {
@ -91,11 +100,12 @@ const locale = Astro.currentLocale || "en";
},
preloadImage(index) {
// Preload without affecting lightboxLoaded state (for navigation within lightbox)
this.resetZoom();
let img = new Image();
img.src = this.images[index].src;
// No need to wait for load when navigating within lightbox
},
handleThumbnailClick(index) {
handleImageInteraction(index) {
const currentTime = Date.now();
const timeDiff = currentTime - this.lastTapTime;
@ -109,6 +119,46 @@ const locale = Astro.currentLocale || "en";
}
this.lastTapTime = currentTime;
},
resetZoom() {
this.scale = 1;
this.panX = 0;
this.panY = 0;
this.isPanning = false;
},
handleWheel(e) {
e.preventDefault();
const zoomSpeed = 0.1;
const newScale = this.scale - Math.sign(e.deltaY) * zoomSpeed;
this.scale = Math.min(Math.max(1, newScale), 5); // Limit zoom between 1x and 5x
if (this.scale === 1) {
this.panX = 0;
this.panY = 0;
}
},
handlePanStart(e) {
if (this.scale <= 1) return;
this.isPanning = true;
this.startPanX = e.clientX - this.panX;
this.startPanY = e.clientY - this.panY;
e.preventDefault();
},
handlePanMove(e) {
if (!this.isPanning) return;
e.preventDefault();
this.panX = e.clientX - this.startPanX;
this.panY = e.clientY - this.startPanY;
},
handlePanEnd(e) {
this.isPanning = false;
},
toggleZoom(e) {
if (this.scale > 1) {
this.resetZoom();
} else {
this.scale = 2; // Zoom to 2x on double click
}
}
}
`}
@ -125,8 +175,8 @@ const locale = Astro.currentLocale || "en";
class="product-gallery bg-white dark:bg-gray-800 rounded-lg shadow-sm border border-gray-200 dark:border-gray-700 overflow-hidden"><div class="flex flex-col h-full p-4">
<!-- Main Image (with swipe functionality) -->
<div
class="flex-1 flex items-center justify-center cursor-pointer rounded-lg"
@click="preloadAndOpen()"
class="flex-1 flex items-center justify-center cursor-pointer rounded-lg touch-manipulation"
@click="handleImageInteraction(currentIndex)"
@touchstart="touchStartX = $event.touches[0].clientX; isSwiping = true;"
@touchend="touchEndX = $event.changedTouches[0].clientX; handleSwipe();"
@touchcancel="isSwiping = false;"
@ -140,7 +190,6 @@ const locale = Astro.currentLocale || "en";
format="avif"
placeholder="blurred"
sizes={mergedGallerySettings.SIZES_REGULAR}
sizes={mergedGallerySettings.SIZES_REGULAR}
s={s || image.hash}
attributes={{
img: { class: "main-image p-4 rounded-lg max-h-[60vh] aspect-square" }
@ -166,9 +215,9 @@ const locale = Astro.currentLocale || "en";
{images.map((image, index) => (
<button
key={index}
x-on:click={`handleThumbnailClick(${index})`}
x-on:click={`handleImageInteraction(${index})`}
:class={`currentIndex === ${index} ? 'ring-2 ring-orange-500' : ''`}
class="thumbnail thumbnail-btn rounded-lg"
class="thumbnail thumbnail-btn rounded-lg touch-manipulation"
>
<Img
src={image.src}
@ -199,31 +248,44 @@ const locale = Astro.currentLocale || "en";
class="fixed inset-0 bg-black bg-opacity-75 flex items-center justify-center lightbox z-50"
>
<div
class="relative max-w-full max-h-full"
class="relative max-w-full max-h-full transition-transform duration-100 ease-out"
@touchstart="touchStartX = $event.touches[0].clientX; isSwiping = true;"
@touchend="touchEndX = $event.changedTouches[0].clientX; handleSwipe();"
@touchcancel="isSwiping = false;"
@wheel="handleWheel($event)"
@mousedown="handlePanStart($event)"
@mousemove.window="handlePanMove($event)"
@mouseup.window="handlePanEnd($event)"
@dblclick="toggleZoom($event)"
>
{images.map((image, index) => (
<div x-show={`currentIndex === ${index}`} key={index}>
<Img
src={image.src}
alt={image.alt}
placeholder="blurred"
format="avif"
objectFit="contain"
sizes={IMAGE_SETTINGS.LIGHTBOX.SIZES_LARGE}
sizes={IMAGE_SETTINGS.LIGHTBOX.SIZES_LARGE}
s={s || image.hash}
attributes={{
img: { class: "max-w-[90vw] max-h-[90vh] object-contain rounded-lg lightbox-main" }
}}
/>
{ ((mergedLightboxSettings.SHOW_TITLE && image.title && image.title.trim().length > 0) || (mergedLightboxSettings.SHOW_DESCRIPTION && image.description && image.description.trim().length > 0)) && (
<div class="absolute bottom-0 left-1/2 transform -translate-x-1/2 m-[8px] max-h-[32vh] p-2 text-white bg-black/50 rounded-lg" style="width: 90%;">
{ mergedLightboxSettings.SHOW_TITLE && image.title && image.title.trim().length > 0 && ( <h3 class="text-xl"><Translate>{image.title}</Translate></h3>)}
{ mergedLightboxSettings.SHOW_DESCRIPTION && image.description && image.description.trim().length > 0 && (<p><Translate>{image.description}</Translate></p>)} </div>
)}
<div
x-show={`currentIndex === ${index}`}
key={index}
class="w-full h-full flex items-center justify-center p-4"
>
<div
:style="'transform: translate(' + panX + 'px, ' + panY + 'px) scale(' + scale + '); cursor: ' + (scale > 1 ? 'grab' : 'default') + ';'"
class="transition-transform duration-100 ease-out flex items-center justify-center max-w-full max-h-full"
>
<Img
src={image.src}
alt={image.alt}
placeholder="blurred"
format="avif"
objectFit="contain"
sizes={IMAGE_SETTINGS.LIGHTBOX.SIZES_LARGE}
s={s || image.hash}
attributes={{
img: { class: "max-w-[90vw] max-h-[90vh] object-contain rounded-lg lightbox-main select-none" }
}}
/>
{ ((mergedLightboxSettings.SHOW_TITLE && image.title && image.title.trim().length > 0) || (mergedLightboxSettings.SHOW_DESCRIPTION && image.description && image.description.trim().length > 0)) && (
<div class="absolute bottom-0 left-1/2 transform -translate-x-1/2 m-[8px] max-h-[32vh] p-2 text-white bg-black/50 rounded-lg" style="width: 90%;">
{ mergedLightboxSettings.SHOW_TITLE && image.title && image.title.trim().length > 0 && ( <h3 class="text-xl"><Translate>{image.title}</Translate></h3>)}
{ mergedLightboxSettings.SHOW_DESCRIPTION && image.description && image.description.trim().length > 0 && (<p><Translate>{image.description}</Translate></p>)} </div>
)}
</div>
</div>
))}
<!-- Close Button -->