diff options
Diffstat (limited to 'assets/js/photo-utils.js')
| -rw-r--r-- | assets/js/photo-utils.js | 476 |
1 files changed, 476 insertions, 0 deletions
diff --git a/assets/js/photo-utils.js b/assets/js/photo-utils.js new file mode 100644 index 0000000..32ede76 --- /dev/null +++ b/assets/js/photo-utils.js @@ -0,0 +1,476 @@ +(function() { + 'use strict'; + + // Internal state + let currentPhotoIndex = 0; + let photosData = []; + let isOpen = false; + let exifData = {}; + let options = { + swipeEnabled: true, + keyboardEnabled: true, + extractEXIF: true, + onOpen: null, + onClose: null, + }; + + // Lightbox DOM elements (created on first init) + let lightboxModal = null; + let lightboxImage = null; + let lightboxCaption = null; + let lightboxSidebar = null; + + /** + * Initialize lightbox for a photo collection + * @param {string} containerSelector - CSS selector for photo container + * @param {Array} photos - Array of photo objects: {src, alt, caption, location} + * @param {Object} opts - Options: {swipeEnabled, keyboardEnabled, extractEXIF, onOpen, onClose} + * @returns {Object} - {openLightbox, closeLightbox} + */ + function initLightbox(containerSelector, photos, opts = {}) { + photosData = photos; + options = { ...options, ...opts }; + + // Merge options with defaults + const container = document.querySelector(containerSelector); + if (!container) { + console.error('Photo container not found:', containerSelector); + return { openLightbox: () => {}, closeLightbox: () => {} }; + } + + // Attach click listeners to all figures in the container + const figures = container.querySelectorAll('figure[data-photo-index]'); + figures.forEach((figure) => { + figure.addEventListener('click', () => { + const index = parseInt(figure.dataset.photoIndex, 10); + openLightbox(index); + }); + }); + + // Return public API + return { + openLightbox, + closeLightbox, + }; + } + + /** + * Open lightbox to a specific photo + * @param {number} photoIndex - 0-based index into photosData array + */ + function openLightbox(photoIndex) { + if (photoIndex < 0 || photoIndex >= photosData.length) { + console.warn('Invalid photo index:', photoIndex); + return; + } + + currentPhotoIndex = photoIndex; + isOpen = true; + + // Create lightbox DOM if it doesn't exist + if (!lightboxModal) { + createLightboxDOM(); + } + + // Populate lightbox with current photo + updateLightboxContent(); + + // Show modal + lightboxModal.classList.add('visible'); + document.body.style.overflow = 'hidden'; + + // Extract EXIF if enabled + if (options.extractEXIF) { + extractEXIFForCurrentPhoto(); + } + + // Attach event listeners + attachEventListeners(); + + // Callback + if (options.onOpen) { + options.onOpen(photoIndex); + } + } + + /** + * Close lightbox + */ + function closeLightbox() { + if (!isOpen || !lightboxModal) return; + + isOpen = false; + lightboxModal.classList.remove('visible'); + document.body.style.overflow = ''; + + // Detach event listeners + detachEventListeners(); + + // Callback + if (options.onClose) { + options.onClose(); + } + } + + /** + * Create lightbox HTML structure and inject into DOM + */ + function createLightboxDOM() { + lightboxModal = document.createElement('div'); + lightboxModal.className = 'photo-lightbox'; + + lightboxModal.innerHTML = ` + <div class="photo-lightbox-backdrop"></div> + <div class="photo-lightbox-container"> + <button class="photo-lightbox-close" aria-label="Close lightbox">×</button> + + <button class="photo-lightbox-prev" aria-label="Previous photo">←</button> + + <div class="photo-lightbox-content"> + <img class="photo-lightbox-image" src="" alt=""> + <div class="photo-lightbox-caption"></div> + </div> + + <button class="photo-lightbox-next" aria-label="Next photo">→</button> + + <div class="photo-lightbox-sidebar"> + <div class="photo-lightbox-sidebar-content"></div> + </div> + </div> + `; + + document.body.appendChild(lightboxModal); + + // Cache element references + lightboxImage = lightboxModal.querySelector('.photo-lightbox-image'); + lightboxCaption = lightboxModal.querySelector('.photo-lightbox-caption'); + lightboxSidebar = lightboxModal.querySelector('.photo-lightbox-sidebar-content'); + + // Attach close button and backdrop click + lightboxModal.querySelector('.photo-lightbox-close').addEventListener('click', closeLightbox); + lightboxModal.querySelector('.photo-lightbox-backdrop').addEventListener('click', closeLightbox); + + // Attach navigation buttons + lightboxModal.querySelector('.photo-lightbox-prev').addEventListener('click', () => navigate(-1)); + lightboxModal.querySelector('.photo-lightbox-next').addEventListener('click', () => navigate(1)); + } + + /** + * Update lightbox content with current photo + */ + function updateLightboxContent() { + const photo = photosData[currentPhotoIndex]; + if (!photo) return; + + lightboxImage.src = photo.src; + lightboxImage.alt = photo.alt || ''; + + // Caption + if (photo.caption) { + lightboxCaption.textContent = photo.caption; + lightboxCaption.style.display = 'block'; + } else { + lightboxCaption.style.display = 'none'; + } + + // Update prev/next button states + const prevBtn = lightboxModal.querySelector('.photo-lightbox-prev'); + const nextBtn = lightboxModal.querySelector('.photo-lightbox-next'); + prevBtn.disabled = currentPhotoIndex === 0; + nextBtn.disabled = currentPhotoIndex === photosData.length - 1; + } + + /** + * Navigate to previous or next photo + * @param {number} direction - -1 for prev, 1 for next + */ + function navigate(direction) { + const newIndex = currentPhotoIndex + direction; + if (newIndex >= 0 && newIndex < photosData.length) { + currentPhotoIndex = newIndex; + updateLightboxContent(); + + // Reset EXIF data and re-extract if enabled + exifData = {}; + if (options.extractEXIF) { + extractEXIFForCurrentPhoto(); + } + } + } + + /** + * Extract EXIF data from current photo image element + */ + function extractEXIFForCurrentPhoto() { + extractEXIF(lightboxImage).then((data) => { + exifData = data; + updateSidebarContent(); + }); + } + + /** + * Update sidebar with EXIF and location data + */ + function updateSidebarContent() { + const photo = photosData[currentPhotoIndex]; + let sidebarHTML = ''; + + // Always show location if provided + if (photo.location) { + sidebarHTML += ` + <div class="photo-metadata-field"> + <div class="photo-metadata-label">Location</div> + <div class="photo-metadata-value">${photo.location}</div> + </div> + `; + } + + // Show EXIF fields if available + if (Object.keys(exifData).length > 0) { + sidebarHTML += ` + <div class="photo-metadata-divider"></div> + <div class="photo-metadata-label">Camera</div> + `; + + if (exifData.camera) { + sidebarHTML += ` + <div class="photo-metadata-value">${exifData.camera}</div> + `; + } + if (exifData.lens) { + sidebarHTML += ` + <div class="photo-metadata-field"> + <div class="photo-metadata-label">Lens</div> + <div class="photo-metadata-value">${exifData.lens}</div> + </div> + `; + } + if (exifData.focalLength) { + sidebarHTML += ` + <div class="photo-metadata-field"> + <div class="photo-metadata-label">Focal Length</div> + <div class="photo-metadata-value">${exifData.focalLength}</div> + </div> + `; + } + if (exifData.aperture) { + sidebarHTML += ` + <div class="photo-metadata-field"> + <div class="photo-metadata-label">Aperture</div> + <div class="photo-metadata-value">${exifData.aperture}</div> + </div> + `; + } + if (exifData.shutterSpeed) { + sidebarHTML += ` + <div class="photo-metadata-field"> + <div class="photo-metadata-label">Shutter Speed</div> + <div class="photo-metadata-value">${exifData.shutterSpeed}</div> + </div> + `; + } + if (exifData.iso) { + sidebarHTML += ` + <div class="photo-metadata-field"> + <div class="photo-metadata-label">ISO</div> + <div class="photo-metadata-value">${exifData.iso}</div> + </div> + `; + } + if (exifData.dateTaken) { + sidebarHTML += ` + <div class="photo-metadata-field"> + <div class="photo-metadata-label">Date Taken</div> + <div class="photo-metadata-value">${exifData.dateTaken}</div> + </div> + `; + } + } + + // Show or hide sidebar based on whether there's content + const sidebarContainer = lightboxModal.querySelector('.photo-lightbox-sidebar'); + if (sidebarHTML) { + lightboxSidebar.innerHTML = sidebarHTML; + sidebarContainer.classList.add('visible'); + } else { + sidebarContainer.classList.remove('visible'); + } + } + + /** + * Extract EXIF data from an image element using exif-js library + * @param {HTMLImageElement} imageElement - Image to read EXIF from + * @returns {Promise} - Resolves to {camera, lens, iso, aperture, shutterSpeed, focalLength, dateTaken} + */ + function extractEXIF(imageElement) { + return new Promise((resolve) => { + // If exif-js is not loaded, return empty object + if (typeof EXIF === 'undefined') { + console.warn('exif-js library not loaded'); + resolve({}); + return; + } + + EXIF.getData(imageElement, function() { + const data = { + camera: EXIF.getTag(this, 'Model'), + lens: EXIF.getTag(this, 'LensModel'), + iso: EXIF.getTag(this, 'ISOSpeedRatings'), + aperture: formatAperture(EXIF.getTag(this, 'FNumber')), + shutterSpeed: formatShutterSpeed(EXIF.getTag(this, 'ExposureTime')), + focalLength: formatFocalLength(EXIF.getTag(this, 'FocalLength')), + dateTaken: EXIF.getTag(this, 'DateTime'), + }; + + // Filter out undefined values + Object.keys(data).forEach(key => { + if (data[key] === undefined) { + delete data[key]; + } + }); + + resolve(data); + }); + }); + } + + /** + * Format aperture value (f-number) + */ + function formatAperture(value) { + if (!value) return null; + if (typeof value === 'object' && value.numerator !== undefined) { + return `f/${(value.numerator / value.denominator).toFixed(1)}`; + } + return `f/${value}`; + } + + /** + * Format shutter speed (exposure time) + */ + function formatShutterSpeed(value) { + if (!value) return null; + if (typeof value === 'object' && value.numerator !== undefined) { + const speed = value.numerator / value.denominator; + if (speed >= 1) { + return `${speed.toFixed(1)}s`; + } + return `1/${Math.round(1 / speed)}`; + } + return value; + } + + /** + * Format focal length + */ + function formatFocalLength(value) { + if (!value) return null; + if (typeof value === 'object' && value.numerator !== undefined) { + return `${(value.numerator / value.denominator).toFixed(0)}mm`; + } + return `${value}mm`; + } + + /** + * Attach event listeners (swipe, keyboard, etc.) + */ + function attachEventListeners() { + if (options.swipeEnabled) { + attachSwipeListeners(); + } + if (options.keyboardEnabled) { + attachKeyboardListeners(); + } + } + + /** + * Detach event listeners to prevent memory leaks + */ + function detachEventListeners() { + if (options.swipeEnabled) { + detachSwipeListeners(); + } + if (options.keyboardEnabled) { + detachKeyboardListeners(); + } + } + + /** + * Touch swipe event listeners + */ + let swipeStartX = 0; + let swipeStartY = 0; + + function onTouchStart(e) { + swipeStartX = e.touches[0].clientX; + swipeStartY = e.touches[0].clientY; + } + + function onTouchEnd(e) { + const swipeEndX = e.changedTouches[0].clientX; + const swipeEndY = e.changedTouches[0].clientY; + const diffX = swipeEndX - swipeStartX; + const diffY = swipeEndY - swipeStartY; + + // Only consider horizontal swipe if delta-x > delta-y + if (Math.abs(diffX) > Math.abs(diffY) && Math.abs(diffX) > 50) { + if (diffX > 0) { + // Swiped right: go to previous + navigate(-1); + } else { + // Swiped left: go to next + navigate(1); + } + } + } + + function attachSwipeListeners() { + lightboxModal.addEventListener('touchstart', onTouchStart, false); + lightboxModal.addEventListener('touchend', onTouchEnd, false); + } + + function detachSwipeListeners() { + lightboxModal.removeEventListener('touchstart', onTouchStart, false); + lightboxModal.removeEventListener('touchend', onTouchEnd, false); + } + + /** + * Keyboard event listeners + */ + function onKeyDown(e) { + if (!isOpen) return; + + switch (e.key) { + case 'ArrowLeft': + navigate(-1); + e.preventDefault(); + break; + case 'ArrowRight': + navigate(1); + e.preventDefault(); + break; + case 'Escape': + closeLightbox(); + e.preventDefault(); + break; + default: + break; + } + } + + function attachKeyboardListeners() { + document.addEventListener('keydown', onKeyDown); + } + + function detachKeyboardListeners() { + document.removeEventListener('keydown', onKeyDown); + } + + // Expose public API + window.PhotoUtils = { + initLightbox, + extractEXIF, + openLightbox, + closeLightbox, + }; +})(); |
