This commit is contained in:
zuevav
2026-04-30 15:14:09 +03:00
parent 08fe53fa5c
commit e5a88665cd
25 changed files with 13697 additions and 0 deletions
@@ -0,0 +1,165 @@
/**
* VH Flickr Mosaic - Styles
* Beautiful photo mosaic with fade animations
*/
.vh-flickr-mosaic {
width: 100%;
overflow: hidden;
background: linear-gradient(180deg, transparent 0%, rgba(0,0,0,0.03) 100%);
padding: 20px 0;
position: relative;
}
.vh-mosaic-container {
display: grid;
gap: 8px;
padding: 0 20px;
justify-content: center;
max-width: 100%;
margin: 0 auto;
}
.vh-mosaic-item {
position: relative;
overflow: hidden;
border-radius: 8px;
background: #f0f0f0;
transition: transform 0.3s ease, box-shadow 0.3s ease;
}
.vh-mosaic-item:hover {
transform: scale(1.05);
box-shadow: 0 8px 25px rgba(0,0,0,0.15);
z-index: 10;
}
.vh-mosaic-item a {
display: block;
width: 100%;
height: 100%;
}
.vh-mosaic-item img {
width: 100%;
height: 100%;
object-fit: cover;
transition: opacity 0.8s ease-in-out;
display: block;
}
/* Fade animation for image swap */
.vh-mosaic-item img.vh-fading-out {
opacity: 0;
}
.vh-mosaic-item img.vh-fading-in {
position: absolute;
top: 0;
left: 0;
opacity: 0;
animation: vhFadeIn 0.8s ease-in-out forwards;
}
@keyframes vhFadeIn {
0% { opacity: 0; }
100% { opacity: 1; }
}
/* Loading state */
.vh-mosaic-item.vh-loading {
background: linear-gradient(90deg, #f0f0f0 25%, #e0e0e0 50%, #f0f0f0 75%);
background-size: 200% 100%;
animation: vhShimmer 1.5s infinite;
}
@keyframes vhShimmer {
0% { background-position: 200% 0; }
100% { background-position: -200% 0; }
}
/* Title overlay on hover */
.vh-mosaic-item .vh-photo-title {
position: absolute;
bottom: 0;
left: 0;
right: 0;
padding: 8px 10px;
background: linear-gradient(transparent, rgba(0,0,0,0.7));
color: white;
font-size: 12px;
opacity: 0;
transition: opacity 0.3s ease;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.vh-mosaic-item:hover .vh-photo-title {
opacity: 1;
}
/* Responsive adjustments */
@media (max-width: 768px) {
.vh-flickr-mosaic {
padding: 15px 0;
}
.vh-mosaic-container {
gap: 6px;
padding: 0 10px;
}
.vh-mosaic-item {
border-radius: 6px;
}
}
@media (max-width: 480px) {
.vh-mosaic-container {
gap: 4px;
padding: 0 5px;
}
.vh-mosaic-item {
border-radius: 4px;
}
.vh-mosaic-item .vh-photo-title {
font-size: 10px;
padding: 5px 8px;
}
}
/* Dark mode support */
@media (prefers-color-scheme: dark) {
.vh-flickr-mosaic {
background: linear-gradient(180deg, transparent 0%, rgba(255,255,255,0.03) 100%);
}
.vh-mosaic-item {
background: #2a2a2a;
}
.vh-mosaic-item.vh-loading {
background: linear-gradient(90deg, #2a2a2a 25%, #3a3a3a 50%, #2a2a2a 75%);
background-size: 200% 100%;
}
}
/* Reduce motion for accessibility */
@media (prefers-reduced-motion: reduce) {
.vh-mosaic-item,
.vh-mosaic-item img {
transition: none;
}
.vh-mosaic-item img.vh-fading-in {
animation: none;
opacity: 1;
}
.vh-mosaic-item.vh-loading {
animation: none;
}
}
@@ -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);
});
})();