feat: add stagger entry animation to showcase on load and shuffle

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-20 08:44:13 -04:00
parent d4258d2ff1
commit 5e675f683f
2 changed files with 32 additions and 13 deletions
+14 -13
View File
@@ -4,8 +4,6 @@
(function() { (function() {
'use strict'; 'use strict';
// Use shared TagUtils module
const { formatTagHtml, createTagChipHtml } = window.TagUtils || {};
const container = document.getElementById('showcaseGrid'); const container = document.getElementById('showcaseGrid');
if (!container) return; if (!container) return;
@@ -59,7 +57,7 @@
try { try {
const images = JSON.parse(dataScript.textContent); const images = JSON.parse(dataScript.textContent);
distributeImages(images, true); // Use lazy loading for initial images (some may be below fold) distributeImages(images, true, true); // animate on initial load
} catch (e) { } catch (e) {
console.error('Failed to parse initial images:', e); console.error('Failed to parse initial images:', e);
} }
@@ -174,20 +172,23 @@
return item; return item;
} }
function distributeImages(images, useLazyLoading = false) { function distributeImages(images, useLazyLoading = false, animate = false) {
images.forEach(img => { images.forEach((img, i) => {
const colIndex = getShortestColumnIndex(); const colIndex = getShortestColumnIndex();
const item = createImageElement(img, useLazyLoading); const item = createImageElement(img, useLazyLoading);
if (animate) {
item.style.setProperty('--stagger-index', i);
item.classList.add('animating-in');
// Remove class after animation completes so it doesn't block interactions
const delay = i * 60;
setTimeout(() => item.classList.remove('animating-in'), delay + 300);
}
columns[colIndex].appendChild(item); columns[colIndex].appendChild(item);
// Update height estimate (will be corrected after image loads)
columnHeights[colIndex] += 300; columnHeights[colIndex] += 300;
// Correct height after image loads
const imgEl = item.querySelector('img'); const imgEl = item.querySelector('img');
imgEl.onload = () => { imgEl.onload = () => { updateColumnHeights(); };
updateColumnHeights();
};
}); });
document.dispatchEvent(new CustomEvent('showcaseUpdated')); document.dispatchEvent(new CustomEvent('showcaseUpdated'));
@@ -236,7 +237,7 @@
columns.forEach(col => col.innerHTML = ''); columns.forEach(col => col.innerHTML = '');
columnHeights = columnHeights.map(() => 0); columnHeights = columnHeights.map(() => 0);
distributeImages(data.images); distributeImages(data.images, false, true); // animate on shuffle
// Scroll to top // Scroll to top
window.scrollTo({ top: 0, behavior: 'smooth' }); window.scrollTo({ top: 0, behavior: 'smooth' });
+18
View File
@@ -1394,6 +1394,24 @@ header {
transition: opacity 0.3s ease; transition: opacity 0.3s ease;
} }
/* Stagger entry animation for showcase items */
@keyframes itemFadeIn {
from {
opacity: 0;
transform: translateY(12px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
.masonry-item.animating-in {
animation: itemFadeIn 0.25s ease forwards;
animation-delay: calc(var(--stagger-index, 0) * 60ms);
opacity: 0; /* Start hidden until animation fires */
}
/*------------------------------------------------------------------------------ /*------------------------------------------------------------------------------
Forms Forms
------------------------------------------------------------------------------*/ ------------------------------------------------------------------------------*/