From e05b60cee8699570a2ac3e2c921375519f2645b8 Mon Sep 17 00:00:00 2001 From: Kibi Kelburton Date: Tue, 14 Jul 2026 20:45:33 +0200 Subject: [PATCH] change grid rendering --- public/s/css/f0ckm.css | 39 ++++++++++++++++++----- public/s/js/f0ckm.js | 70 +++++++++++++++--------------------------- 2 files changed, 56 insertions(+), 53 deletions(-) diff --git a/public/s/css/f0ckm.css b/public/s/css/f0ckm.css index ab2d681..0272e52 100644 --- a/public/s/css/f0ckm.css +++ b/public/s/css/f0ckm.css @@ -7098,20 +7098,43 @@ button#togglebg { animation: none; } -/* ─── Posts Grid: no skeleton on grid thumbnails — plain dark bg while loading ─────── */ +/* ─── Posts Grid Skeleton: always visible shimmer while image loads ────────────────── + The is always visible — skeleton shimmer shows immediately. + Once the image is loaded (.loaded class added), the shimmer is removed and + the background-image (set via --thumb-bg) is shown. Children stay on top + via their own stacking context. */ + +/* Skeleton shimmer on unloaded thumbs — always visible */ div.posts>a.lazy-thumb:not(.loaded) { - background: rgba(255, 255, 255, 0.04); + background: linear-gradient(90deg, + rgba(255,255,255,0.04) 0%, + rgba(255,255,255,0.09) 45%, + rgba(255,255,255,0.04) 90%); + background-size: 300% 100%; + animation: thumb-shimmer 1.8s ease-in-out infinite; } -/* Smooth GPU-composited fade-in when thumbnail image finishes loading. - opacity transition is always compositor-promoted — no paint cost. */ -div.posts>a.thumb { - opacity: 0; - transition: opacity 0.25s ease-out; +@keyframes thumb-shimmer { + 0% { background-position: 100% 50%; } + 100% { background-position: 0% 50%; } } +/* Once loaded: show the real image and fade in smoothly */ div.posts>a.thumb.loaded { - opacity: 1; + background-image: var(--thumb-bg); + animation: thumb-reveal 0.3s ease-out both; +} + +@keyframes thumb-reveal { + from { opacity: 0.4; } + to { opacity: 1; } +} + +/* Ensure child elements (indicators, labels) are always visible above background */ +div.posts>a.thumb > *, +div.posts>a.thumb .thumb-indicators { + position: relative; + z-index: 1; } diff --git a/public/s/js/f0ckm.js b/public/s/js/f0ckm.js index 2ccf826..c328cb4 100644 --- a/public/s/js/f0ckm.js +++ b/public/s/js/f0ckm.js @@ -118,24 +118,15 @@ window.cancelAnimFrame = (function () { // Clear grid cache to force fresh render on next navigation if (typeof gridCacheMap !== 'undefined') gridCacheMap.clear(); - // Update elements with data-bg (grid items). - // We look for any data-bg or inline style containing the thumbnail path for this ID. - document.querySelectorAll(`[data-bg*="/t/${idStr}.webp"], [data-bg*="/t/${idStr}_blur.webp"], [style*="/t/${idStr}.webp"], [style*="/t/${idStr}_blur.webp"]`).forEach(el => { - // If it has data-bg, update it (this handles lazy-thumb logic) + document.querySelectorAll(`[data-bg*="/t/${idStr}.webp"], [data-bg*="/t/${idStr}_blur.webp"]`).forEach(el => { if (el.dataset.bg) { el.dataset.bg = window.applyThumbCacheBust(el.dataset.bg); } - // If it's already showing the background, update the style directly - if (el.style.backgroundImage || el.getAttribute('style')?.includes('background-image')) { - const currentStyle = el.getAttribute('style') || ''; - // Match url(...) contents - const newStyle = currentStyle.replace(/url\(['"]?([^'"]+)['"]?\)/g, (match, p1) => { - if (p1.includes(`/t/${idStr}.webp`) || p1.includes(`/t/${idStr}_blur.webp`)) { - return `url('${window.applyThumbCacheBust(p1)}')`; - } - return match; - }); - el.setAttribute('style', newStyle); + // Update the --thumb-bg CSS custom property used by the ::after pseudo-element + const currentBg = el.style.getPropertyValue('--thumb-bg'); + if (currentBg && (currentBg.includes(`/t/${idStr}.webp`) || currentBg.includes(`/t/${idStr}_blur.webp`))) { + const newUrl = currentBg.replace(/url\(['"](.*?)['"]\)/, (_, p1) => `url('${window.applyThumbCacheBust(p1)}')`); + el.style.setProperty('--thumb-bg', newUrl); } }); @@ -180,24 +171,26 @@ window.cancelAnimFrame = (function () { return window.applyThumbCacheBust(bg); }; + // Helper: apply thumb image via CSS custom property — picked up by the loaded CSS rule + const applyThumb = (thumb, finalBg) => { + thumb.style.setProperty('--thumb-bg', `url('${finalBg}')`); + thumb.classList.add('loaded'); + thumb.classList.remove('lazy-thumb'); + window.loadedThumbs.add(finalBg); + }; + // Synchronously resolve any already-loaded thumbnails before observer starts document.querySelectorAll('.lazy-thumb').forEach(thumb => { const finalBg = getFinalBg(thumb); if (finalBg && window.loadedThumbs.has(finalBg)) { - thumb.style.backgroundImage = `url('${finalBg}')`; - thumb.classList.add('loaded'); - thumb.classList.remove('lazy-thumb'); + applyThumb(thumb, finalBg); } }); if (!('IntersectionObserver' in window)) { document.querySelectorAll('.lazy-thumb').forEach(thumb => { const finalBg = getFinalBg(thumb); - if (finalBg) { - thumb.style.backgroundImage = `url('${finalBg}')`; - thumb.classList.remove('lazy-thumb'); - window.loadedThumbs.add(finalBg); - } + if (finalBg) applyThumb(thumb, finalBg); }); return; } @@ -207,36 +200,24 @@ window.cancelAnimFrame = (function () { entries.forEach(entry => { if (entry.isIntersecting) { const thumb = entry.target; - let bg = thumb.dataset.bg; - if (bg && !thumb.classList.contains('loaded')) { + if (thumb.dataset.bg && !thumb.classList.contains('loaded')) { const finalBg = getFinalBg(thumb); if (finalBg) { const img = new Image(); img.src = finalBg; if (img.complete && img.naturalWidth > 0) { - thumb.style.backgroundImage = `url('${finalBg}')`; - thumb.classList.add('loaded'); - thumb.classList.remove('lazy-thumb'); - window.loadedThumbs.add(finalBg); + applyThumb(thumb, finalBg); } else { - img.onload = () => { - thumb.style.backgroundImage = `url('${finalBg}')`; - thumb.classList.add('loaded'); - thumb.classList.remove('lazy-thumb'); - window.loadedThumbs.add(finalBg); - }; + img.onload = () => applyThumb(thumb, finalBg); img.onerror = () => { const retries = parseInt(thumb.dataset.retries || '0'); if (retries < 3) { thumb.dataset.retries = retries + 1; - setTimeout(() => { - img.src = finalBg + '?r=' + Date.now(); - }, 1000); + setTimeout(() => { img.src = finalBg + '?r=' + Date.now(); }, 1000); } else { - // All retries exhausted — show fallback for audio items const mime = thumb.dataset.mime || ''; if (mime.startsWith('audio/')) { - thumb.style.backgroundImage = `url('/s/img/audio.webp')`; + thumb.style.setProperty('--thumb-bg', `url('/s/img/audio.webp')`); thumb.classList.add('thumb-fallback'); } thumb.classList.remove('lazy-thumb'); @@ -256,7 +237,6 @@ window.cancelAnimFrame = (function () { window._lazyVisibilityBound = true; document.addEventListener('visibilitychange', () => { if (!document.hidden && typeof window.initLazyLoading === 'function') { - // Clear observation state for pending items to force re-observation document.querySelectorAll('.lazy-thumb:not(.loaded)').forEach(t => { delete t.dataset.lazyObserved; }); @@ -7710,8 +7690,8 @@ class NotificationSystem { thumb.dataset.mode = mode; thumb.dataset.bg = `/t/${data.id}.webp`; thumb.dataset.size = '1'; // New items start with no contributions → tier 1 - thumb.style.backgroundImage = `url('/t/${data.id}.webp')`; - thumb.style.opacity = '0'; + thumb.style.setProperty('--thumb-bg', `url('/t/${data.id}.webp')`); + thumb.classList.add('loaded'); thumb.style.transform = 'scale(0.9)'; thumb.innerHTML = '

' + (data.is_oc ? 'OC' : ''); @@ -10144,7 +10124,7 @@ document.addEventListener('click', (e) => { if (baseBg && shouldBlurThis) { const finalBg = window.applyThumbCacheBust(baseBg.replace('.webp', '_blur.webp')); - thumb.style.backgroundImage = `url('${finalBg}')`; + thumb.style.setProperty('--thumb-bg', `url('${finalBg}')`); } } return; @@ -10171,7 +10151,7 @@ document.addEventListener('click', (e) => { const baseBg = thumb.dataset.bg; if (baseBg && shouldBlurThis) { const finalBg = window.applyThumbCacheBust(baseBg); - thumb.style.backgroundImage = `url('${finalBg}')`; + thumb.style.setProperty('--thumb-bg', `url('${finalBg}')`); } // Dynamically inject the hide-again eye-slash button inside