mailn
This commit is contained in:
@@ -0,0 +1,235 @@
|
||||
/**
|
||||
* 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) => `
|
||||
<div class="vh-mosaic-item" data-index="${index}" style="width:${this.photoSize}px;height:${this.photoSize}px;">
|
||||
<a href="${photo.page_url || '#'}" target="_blank" rel="noopener noreferrer">
|
||||
<img src="${photo.medium || photo.thumb}" alt="${this.escapeHtml(photo.title || '')}" loading="lazy">
|
||||
${photo.title ? `<span class="vh-photo-title">${this.escapeHtml(photo.title)}</span>` : ''}
|
||||
</a>
|
||||
</div>
|
||||
`).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);
|
||||
});
|
||||
|
||||
})();
|
||||
Reference in New Issue
Block a user