/** * VH Flickr Mosaic - JavaScript * Beautiful photo mosaic with fade animations */ (function() { 'use strict'; class VHFlickrMosaic { constructor(container) { this.container = container; this.mosaicEl = container.querySelector('.vh-mosaic-container'); this.photos = []; this.displayedPhotos = []; this.rows = parseInt(container.dataset.rows) || vhMosaicConfig.rows || 2; this.photoSize = parseInt(container.dataset.size) || vhMosaicConfig.photoSize || 150; this.animationSpeed = parseFloat(container.dataset.speed) || vhMosaicConfig.animationSpeed || 5; this.apiUrl = vhMosaicConfig.apiUrl; this.animationInterval = null; this.isVisible = false; this.init(); } async init() { // Set up intersection observer for lazy loading this.setupVisibilityObserver(); // Load photos await this.loadPhotos(); // Initial render this.render(); // Start animation when visible if (this.isVisible) { this.startAnimation(); } } setupVisibilityObserver() { const observer = new IntersectionObserver((entries) => { entries.forEach(entry => { this.isVisible = entry.isIntersecting; if (this.isVisible && this.photos.length > 0) { this.startAnimation(); } else { this.stopAnimation(); } }); }, { threshold: 0.1 }); observer.observe(this.container); } async loadPhotos() { if (!this.apiUrl) { console.error('VH Flickr Mosaic: API URL not configured'); return; } try { const response = await fetch(this.apiUrl); const data = await response.json(); if (data.success && data.photos) { this.photos = data.photos; } else { console.error('VH Flickr Mosaic: Failed to load photos', data.error); } } catch (error) { console.error('VH Flickr Mosaic: API error', error); } } calculateGrid() { const containerWidth = this.mosaicEl.offsetWidth || window.innerWidth; const cols = Math.floor(containerWidth / (this.photoSize + 8)); // 8px gap return { cols: Math.max(cols, 3), total: Math.max(cols, 3) * this.rows }; } render() { if (this.photos.length === 0) { this.mosaicEl.innerHTML = ''; return; } const { cols, total } = this.calculateGrid(); // Set grid columns this.mosaicEl.style.gridTemplateColumns = `repeat(${cols}, ${this.photoSize}px)`; // Select random photos for display this.displayedPhotos = this.getRandomPhotos(total); // Create HTML this.mosaicEl.innerHTML = this.displayedPhotos.map((photo, index) => `
${this.escapeHtml(photo.title || '')} ${photo.title ? `${this.escapeHtml(photo.title)}` : ''}
`).join(''); } getRandomPhotos(count) { const shuffled = [...this.photos].sort(() => Math.random() - 0.5); return shuffled.slice(0, Math.min(count, shuffled.length)); } startAnimation() { if (this.animationInterval) return; if (this.photos.length <= this.displayedPhotos.length) return; this.animationInterval = setInterval(() => { this.swapRandomPhoto(); }, this.animationSpeed * 1000); } stopAnimation() { if (this.animationInterval) { clearInterval(this.animationInterval); this.animationInterval = null; } } swapRandomPhoto() { if (!this.isVisible || this.photos.length === 0) return; const items = this.mosaicEl.querySelectorAll('.vh-mosaic-item'); if (items.length === 0) return; // Pick random item to swap const randomIndex = Math.floor(Math.random() * items.length); const item = items[randomIndex]; // Find a photo not currently displayed const currentIds = this.displayedPhotos.map(p => p.id); const availablePhotos = this.photos.filter(p => !currentIds.includes(p.id)); if (availablePhotos.length === 0) return; const newPhoto = availablePhotos[Math.floor(Math.random() * availablePhotos.length)]; // Animate the swap this.animatePhotoSwap(item, newPhoto, randomIndex); } animatePhotoSwap(item, newPhoto, index) { const oldImg = item.querySelector('img'); const link = item.querySelector('a'); if (!oldImg || !link) return; // Create new image const newImg = document.createElement('img'); newImg.src = newPhoto.medium || newPhoto.thumb; newImg.alt = newPhoto.title || ''; newImg.className = 'vh-fading-in'; newImg.loading = 'lazy'; // Start fade out of old image oldImg.classList.add('vh-fading-out'); // Add new image link.appendChild(newImg); // Update link href link.href = newPhoto.page_url || '#'; // Update title let titleEl = item.querySelector('.vh-photo-title'); if (newPhoto.title) { if (titleEl) { titleEl.textContent = newPhoto.title; } else { titleEl = document.createElement('span'); titleEl.className = 'vh-photo-title'; titleEl.textContent = newPhoto.title; link.appendChild(titleEl); } } else if (titleEl) { titleEl.remove(); } // After animation, clean up setTimeout(() => { oldImg.remove(); newImg.classList.remove('vh-fading-in'); }, 800); // Update displayed photos array this.displayedPhotos[index] = newPhoto; } escapeHtml(text) { const div = document.createElement('div'); div.textContent = text; return div.innerHTML; } } // Initialize all mosaics on page function initMosaics() { document.querySelectorAll('.vh-flickr-mosaic').forEach(container => { new VHFlickrMosaic(container); }); } // Initialize when DOM is ready if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', initMosaics); } else { initMosaics(); } // Handle window resize let resizeTimeout; window.addEventListener('resize', () => { clearTimeout(resizeTimeout); resizeTimeout = setTimeout(() => { document.querySelectorAll('.vh-flickr-mosaic').forEach(container => { const mosaic = container._vhMosaic; if (mosaic) { mosaic.render(); } }); }, 250); }); })();