diff --git a/internal/assets/static/main.css b/internal/assets/static/main.css index 7aa9356..a7c1903 100644 --- a/internal/assets/static/main.css +++ b/internal/assets/static/main.css @@ -217,6 +217,18 @@ img, svg { max-width: 100%; } +img[loading=lazy].loaded:not(.finished-transition) { + transition: opacity .4s; +} + +img[loading=lazy].cached:not(.finished-transition) { + transition: none; +} + +img[loading=lazy]:not(.loaded, .cached) { + opacity: 0; +} + html { scrollbar-color: var(--color-text-subdue) transparent; scroll-behavior: smooth; diff --git a/internal/assets/static/main.js b/internal/assets/static/main.js index 3a6ec0b..05dbc45 100644 --- a/internal/assets/static/main.js +++ b/internal/assets/static/main.js @@ -142,6 +142,33 @@ function setupDynamicRelativeTime() { }); } +function setupLazyImages() { + const images = document.querySelectorAll("img[loading=lazy]"); + + if (images.length == 0) { + return; + } + + function imageFinishedTransition(image) { + image.classList.add("finished-transition"); + } + + for (let i = 0; i < images.length; i++) { + const image = images[i]; + + if (image.complete) { + image.classList.add("cached"); + setTimeout(() => imageFinishedTransition(image), 5); + } else { + // TODO: also handle error event + image.addEventListener("load", () => { + image.classList.add("loaded"); + setTimeout(() => imageFinishedTransition(image), 500); + }); + } + } +} + async function setupPage() { const pageElement = document.getElementById("page"); const pageContents = await fetchPageContents(pageData.slug); @@ -152,6 +179,7 @@ async function setupPage() { document.body.classList.add("animate-element-transition"); }, 150); + setTimeout(setupLazyImages, 5); setupCarousels(); setupDynamicRelativeTime(); }