Add smooth drag-to-slide gallery with 60% threshold and real-time tracking

This commit is contained in:
2025-11-24 14:39:33 +09:00
parent 3293a06ed7
commit c46170fc2b

View File

@@ -68,14 +68,48 @@
background: #f8f9fa;
border-radius: 12px;
overflow: hidden;
touch-action: pan-y pinch-zoom;
}
.gallery-slides-wrapper {
position: relative;
width: 100%;
height: 600px;
overflow: hidden;
cursor: grab;
}
.gallery-slides-wrapper.dragging {
cursor: grabbing;
}
.gallery-slides-container {
display: flex;
height: 100%;
transition: transform 0.3s cubic-bezier(0.4, 0, 0.2, 1);
will-change: transform;
}
.gallery-slides-container.no-transition {
transition: none;
}
.gallery-slide {
flex: 0 0 100%;
height: 100%;
display: flex;
align-items: center;
justify-content: center;
background: #000;
}
.gallery-main-image {
width: 100%;
height: 600px;
height: 100%;
object-fit: contain;
cursor: zoom-in;
background: #000;
user-select: none;
pointer-events: none;
}
/* Navigation Arrows */
@@ -159,7 +193,7 @@
height: 80px;
}
.gallery-main-image {
.gallery-slides-wrapper {
height: 400px;
}
@@ -186,6 +220,24 @@
flex-wrap: wrap;
margin-bottom: 1rem;
}
/* Drag indicator */
.gallery-slides-wrapper::after {
content: '';
position: absolute;
bottom: 20px;
left: 50%;
transform: translateX(-50%);
width: 40px;
height: 4px;
background: rgba(255, 255, 255, 0.3);
border-radius: 2px;
pointer-events: none;
}
.gallery-slides-wrapper.dragging::after {
background: rgba(255, 255, 255, 0.6);
}
</style>
{% endblock %}
@@ -258,12 +310,31 @@
<i class="fas fa-chevron-right"></i>
</button>
<!-- Main Image -->
<img src="{% if item.image %}{{ item.image.url }}{% else %}{{ item.gallery_images.first.image.url }}{% endif %}"
<!-- Slides Wrapper -->
<div class="gallery-slides-wrapper" id="galleryWrapper">
<div class="gallery-slides-container" id="gallerySlides">
<!-- Slides will be generated by JavaScript -->
{% if item.image %}
<div class="gallery-slide">
<img src="{{ item.image.url }}"
class="gallery-main-image"
id="galleryMainImage"
data-lightbox="portfolio-gallery"
data-title="{{ item.title }}"
alt="{{ item.title }}">
</div>
{% endif %}
{% for gallery_image in item.gallery_images.all %}
<div class="gallery-slide">
<img src="{{ gallery_image.image.url }}"
class="gallery-main-image"
data-lightbox="portfolio-gallery"
data-title="{{ gallery_image.caption|default:item.title }}"
alt="{{ gallery_image.caption }}">
</div>
{% endfor %}
</div>
</div>
<!-- Gallery Counter -->
<div class="gallery-counter" id="galleryCounter">
@@ -309,11 +380,11 @@
});
</script>
<!-- Advanced Gallery Navigation Script -->
<!-- Advanced Gallery Navigation Script with Smooth Drag -->
<script>
document.addEventListener('DOMContentLoaded', function() {
const galleryMain = document.getElementById('galleryMain');
const mainImage = document.getElementById('galleryMainImage');
const galleryWrapper = document.getElementById('galleryWrapper');
const gallerySlides = document.getElementById('gallerySlides');
const thumbnails = document.querySelectorAll('.thumbnail-item');
const prevBtn = document.getElementById('galleryPrev');
const nextBtn = document.getElementById('galleryNext');
@@ -323,22 +394,49 @@ document.addEventListener('DOMContentLoaded', function() {
let currentIndex = 0;
const totalImages = thumbnails.length;
// Update gallery display
function updateGallery(index) {
// Drag/Swipe variables
let isDragging = false;
let startX = 0;
let currentTranslate = 0;
let prevTranslate = 0;
let animationID = 0;
// Threshold for slide change (60% of wrapper width)
const slideThreshold = 0.6;
// Update gallery position
function setSliderPosition() {
gallerySlides.style.transform = `translateX(${currentTranslate}px)`;
}
// Smooth animation
function animation() {
setSliderPosition();
if (isDragging) requestAnimationFrame(animation);
}
// Update gallery to specific index
function updateGallery(index, smooth = true) {
if (index < 0) index = totalImages - 1;
if (index >= totalImages) index = 0;
currentIndex = index;
// Calculate position
const wrapperWidth = galleryWrapper.offsetWidth;
currentTranslate = -currentIndex * wrapperWidth;
prevTranslate = currentTranslate;
// Update with or without transition
if (smooth) {
gallerySlides.classList.remove('no-transition');
}
setSliderPosition();
// Update thumbnail
const currentThumbnail = thumbnails[index];
const fullUrl = currentThumbnail.dataset.full;
const caption = currentThumbnail.dataset.caption;
// Update main image
mainImage.src = fullUrl;
mainImage.setAttribute('href', fullUrl);
// Update active thumbnail
thumbnails.forEach(thumb => thumb.classList.remove('active'));
currentThumbnail.classList.add('active');
@@ -357,12 +455,84 @@ document.addEventListener('DOMContentLoaded', function() {
currentThumbnail.scrollIntoView({ behavior: 'smooth', block: 'nearest' });
}
// Thumbnail click handlers
// Mouse/Touch start
function handleDragStart(e) {
isDragging = true;
galleryWrapper.classList.add('dragging');
gallerySlides.classList.add('no-transition');
startX = e.type.includes('mouse') ? e.clientX : e.touches[0].clientX;
animationID = requestAnimationFrame(animation);
}
// Mouse/Touch move
function handleDragMove(e) {
if (!isDragging) return;
const currentX = e.type.includes('mouse') ? e.clientX : e.touches[0].clientX;
const deltaX = currentX - startX;
currentTranslate = prevTranslate + deltaX;
// Add resistance at edges
const wrapperWidth = galleryWrapper.offsetWidth;
const maxTranslate = 0;
const minTranslate = -(totalImages - 1) * wrapperWidth;
if (currentTranslate > maxTranslate) {
// Resistance at start
currentTranslate = maxTranslate + (currentTranslate - maxTranslate) * 0.3;
} else if (currentTranslate < minTranslate) {
// Resistance at end
currentTranslate = minTranslate + (currentTranslate - minTranslate) * 0.3;
}
}
// Mouse/Touch end
function handleDragEnd(e) {
if (!isDragging) return;
isDragging = false;
galleryWrapper.classList.remove('dragging');
cancelAnimationFrame(animationID);
const movedBy = currentTranslate - prevTranslate;
const wrapperWidth = galleryWrapper.offsetWidth;
const threshold = wrapperWidth * slideThreshold;
// Check if moved enough to change slide
if (Math.abs(movedBy) > threshold) {
if (movedBy < 0) {
// Moved left - next slide
updateGallery(currentIndex + 1);
} else {
// Moved right - previous slide
updateGallery(currentIndex - 1);
}
} else {
// Return to current slide
updateGallery(currentIndex);
}
}
// Mouse events
galleryWrapper.addEventListener('mousedown', handleDragStart);
galleryWrapper.addEventListener('mousemove', handleDragMove);
galleryWrapper.addEventListener('mouseup', handleDragEnd);
galleryWrapper.addEventListener('mouseleave', handleDragEnd);
// Touch events
galleryWrapper.addEventListener('touchstart', handleDragStart, { passive: true });
galleryWrapper.addEventListener('touchmove', handleDragMove, { passive: true });
galleryWrapper.addEventListener('touchend', handleDragEnd, { passive: true });
// Thumbnail clicks
thumbnails.forEach((thumbnail, index) => {
thumbnail.addEventListener('click', () => updateGallery(index));
});
// Navigation button handlers
// Navigation buttons
prevBtn.addEventListener('click', (e) => {
e.preventDefault();
updateGallery(currentIndex - 1);
@@ -382,45 +552,22 @@ document.addEventListener('DOMContentLoaded', function() {
}
});
// Touch/Swipe support for mobile devices
let touchStartX = 0;
let touchEndX = 0;
const swipeThreshold = 50;
// Prevent context menu on long press
galleryWrapper.addEventListener('contextmenu', (e) => {
if (isDragging) e.preventDefault();
});
galleryMain.addEventListener('touchstart', (e) => {
touchStartX = e.changedTouches[0].screenX;
}, { passive: true });
// Initialize gallery
updateGallery(0, false);
galleryMain.addEventListener('touchend', (e) => {
touchEndX = e.changedTouches[0].screenX;
handleSwipe();
}, { passive: true });
function handleSwipe() {
const swipeDistance = touchStartX - touchEndX;
if (Math.abs(swipeDistance) > swipeThreshold) {
if (swipeDistance > 0) {
// Swipe left - next image
updateGallery(currentIndex + 1);
} else {
// Swipe right - previous image
updateGallery(currentIndex - 1);
}
}
}
// Mouse wheel support (optional)
galleryMain.addEventListener('wheel', (e) => {
if (Math.abs(e.deltaY) > 10) {
e.preventDefault();
if (e.deltaY > 0) {
updateGallery(currentIndex + 1);
} else {
updateGallery(currentIndex - 1);
}
}
}, { passive: false });
// Handle window resize
let resizeTimer;
window.addEventListener('resize', () => {
clearTimeout(resizeTimer);
resizeTimer = setTimeout(() => {
updateGallery(currentIndex, false);
}, 250);
});
});
</script>
{% endblock %}