From 5e675f683f16529aed973e3b7ccf0f59290a50e0 Mon Sep 17 00:00:00 2001 From: Bryan Van Deusen Date: Fri, 20 Mar 2026 08:44:13 -0400 Subject: [PATCH] feat: add stagger entry animation to showcase on load and shuffle Co-Authored-By: Claude Sonnet 4.6 --- app/static/js/showcase.js | 27 ++++++++++++++------------- app/static/style.css | 18 ++++++++++++++++++ 2 files changed, 32 insertions(+), 13 deletions(-) diff --git a/app/static/js/showcase.js b/app/static/js/showcase.js index 0d422fe..a9e69ee 100644 --- a/app/static/js/showcase.js +++ b/app/static/js/showcase.js @@ -4,8 +4,6 @@ (function() { 'use strict'; - // Use shared TagUtils module - const { formatTagHtml, createTagChipHtml } = window.TagUtils || {}; const container = document.getElementById('showcaseGrid'); if (!container) return; @@ -59,7 +57,7 @@ try { 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) { console.error('Failed to parse initial images:', e); } @@ -174,20 +172,23 @@ return item; } - function distributeImages(images, useLazyLoading = false) { - images.forEach(img => { + function distributeImages(images, useLazyLoading = false, animate = false) { + images.forEach((img, i) => { const colIndex = getShortestColumnIndex(); 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); - - // Update height estimate (will be corrected after image loads) columnHeights[colIndex] += 300; - - // Correct height after image loads const imgEl = item.querySelector('img'); - imgEl.onload = () => { - updateColumnHeights(); - }; + imgEl.onload = () => { updateColumnHeights(); }; }); document.dispatchEvent(new CustomEvent('showcaseUpdated')); @@ -236,7 +237,7 @@ columns.forEach(col => col.innerHTML = ''); columnHeights = columnHeights.map(() => 0); - distributeImages(data.images); + distributeImages(data.images, false, true); // animate on shuffle // Scroll to top window.scrollTo({ top: 0, behavior: 'smooth' }); diff --git a/app/static/style.css b/app/static/style.css index 7fc6fb6..5b40c6c 100644 --- a/app/static/style.css +++ b/app/static/style.css @@ -1394,6 +1394,24 @@ header { 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 ------------------------------------------------------------------------------*/