From b5606797ef03205dc93e8ddd2756e56cfc700ae5 Mon Sep 17 00:00:00 2001 From: Dane Albaugh Date: Sun, 10 Aug 2025 09:11:50 -0400 Subject: [PATCH 1/2] Fix possible background tearing during fast scroll by isolating gradient in its own compositing layer --- dependency_registry_test.go | 3 + layouts/main.tmpl.html | 1 + views/_scroll_gradient_js.tmpl.html | 204 ++++++++++++++++++++++++++++ 3 files changed, 208 insertions(+) create mode 100644 views/_scroll_gradient_js.tmpl.html diff --git a/dependency_registry_test.go b/dependency_registry_test.go index ef6043a9c..382de2eaf 100644 --- a/dependency_registry_test.go +++ b/dependency_registry_test.go @@ -20,6 +20,7 @@ func TestDependencyRegistryParseGoTemplate(t *testing.T) { "views/_tailwind_stylesheets.tmpl.html", "views/_twitter.tmpl.html", "views/_dark_mode_js.tmpl.html", + "views/_scroll_gradient_js.tmpl.html", "views/_analytics_js.tmpl.html", "views/_shiki_js.tmpl.html", }, dependencies) @@ -34,6 +35,7 @@ func TestDependencyRegistryParseGoTemplate(t *testing.T) { "views/_tailwind_stylesheets.tmpl.html", "views/_twitter.tmpl.html", "views/_dark_mode_js.tmpl.html", + "views/_scroll_gradient_js.tmpl.html", "views/_analytics_js.tmpl.html", "views/_shiki_js.tmpl.html", }, dependencies) @@ -49,6 +51,7 @@ func TestDependencyRegistryParseGoTemplate(t *testing.T) { "views/_tailwind_stylesheets.tmpl.html", "views/_twitter.tmpl.html", "views/_dark_mode_js.tmpl.html", + "views/_scroll_gradient_js.tmpl.html", "views/_analytics_js.tmpl.html", "views/_shiki_js.tmpl.html", }, dependencies) diff --git a/layouts/main.tmpl.html b/layouts/main.tmpl.html index 920499e70..eea2c4dc2 100644 --- a/layouts/main.tmpl.html +++ b/layouts/main.tmpl.html @@ -32,6 +32,7 @@ {{template "views/_dark_mode_js.tmpl.html" .}} + {{template "views/_scroll_gradient_js.tmpl.html" .}} {{block "content" .}}{{end}} diff --git a/views/_scroll_gradient_js.tmpl.html b/views/_scroll_gradient_js.tmpl.html new file mode 100644 index 000000000..2e399fb05 --- /dev/null +++ b/views/_scroll_gradient_js.tmpl.html @@ -0,0 +1,204 @@ + + + \ No newline at end of file From 98ed78b3a50813c2d3623a19cbf0de03a717fe3b Mon Sep 17 00:00:00 2001 From: Dane Albaugh Date: Sun, 10 Aug 2025 10:36:22 -0400 Subject: [PATCH 2/2] Fix scrolling gradient SPA navigation --- views/_scroll_gradient_js.tmpl.html | 176 ++++++++++++++++++---------- 1 file changed, 117 insertions(+), 59 deletions(-) diff --git a/views/_scroll_gradient_js.tmpl.html b/views/_scroll_gradient_js.tmpl.html index 2e399fb05..2b542a833 100644 --- a/views/_scroll_gradient_js.tmpl.html +++ b/views/_scroll_gradient_js.tmpl.html @@ -32,26 +32,21 @@ (function () { try { - // Early exit if browser doesn't support required features - if (!('requestAnimationFrame' in window) || !document.body) return; + // Early exit if page isn't ready for DOM work + if (!document.body) return; var body = document.body; - // Only run on pages that have the gradient background class - if (!body.classList.contains('bg-gradient-to-br')) return; - // Prevent multiple initializations - if (window.__sorgScrollGradientInitialized) return; - window.__sorgScrollGradientInitialized = true; - // Check if browser supports CSS containment for better performance var supportsContain = (window.CSS && CSS.supports && (CSS.supports('contain: paint') || CSS.supports('contain: strict'))) || false; - // DOM elements and state variables + // DOM elements and state variables (kept in closure so we can cleanup and re-init) var container = null; var inner = null; var rafId = null; var lastScrollY = -1; var resizeObserver = null; + var mo = null; // Track original body background to restore later var bodyInlineBackgroundImageOriginal = ''; @@ -116,6 +111,16 @@ */ function onScroll() { if (!inner) return; + // If rAF is not available or not a function (e.g. monkey patched), + // fall back to synchronous update to avoid errors. + if (typeof window.requestAnimationFrame !== 'function') { + var dprSync = window.devicePixelRatio || 1; + var ySync = Math.round(window.scrollY * dprSync) / dprSync; + if (ySync === lastScrollY) return; + lastScrollY = ySync; + inner.style.transform = 'translateY(' + (-ySync) + 'px)'; + return; + } if (rafId != null) return; rafId = window.requestAnimationFrame(function () { rafId = null; @@ -135,66 +140,119 @@ onScroll(); } - // Keep gradient in sync with theme/class changes - var mo = new MutationObserver(function (mutations) { - for (var i = 0; i < mutations.length; i++) { - if (mutations[i].type === 'attributes' && mutations[i].attributeName === 'class') { - copyComputedBackground(inner ? inner : container); - break; + /** + * Install a mutation observer to watch for class changes on the document + * and body elements. When a class change is detected, we copy the computed + * background styles to the gradient overlay. + */ + function installMutationObserver() { + if (mo) mo.disconnect(); + mo = new MutationObserver(function (mutations) { + for (var i = 0; i < mutations.length; i++) { + if (mutations[i].type === 'attributes' && mutations[i].attributeName === 'class') { + copyComputedBackground(inner ? inner : container); + break; + } } + }); + mo.observe(document.documentElement, { attributes: true, attributeFilter: ['class'] }); + mo.observe(body, { attributes: true, attributeFilter: ['class'] }); + } + + /** + * Cleanup the scroll gradient effect. + */ + function cleanup() { + try { + if (window.__sorgScrollGradientDebug) console.log('[scroll-gradient] cleanup pagehide'); + if (rafId != null && typeof window.cancelAnimationFrame === 'function') window.cancelAnimationFrame(rafId); + window.removeEventListener('scroll', onScroll); + window.removeEventListener('resize', onResize); + if (resizeObserver && typeof resizeObserver.disconnect === 'function') resizeObserver.disconnect(); + resizeObserver = null; + if (container && container.parentNode) container.parentNode.removeChild(container); + container = null; + inner = null; + if (bodyBackgroundOverridden) body.style.backgroundImage = bodyInlineBackgroundImageOriginal || ''; + bodyBackgroundOverridden = false; + body.classList.remove('has-scroll-gradient'); + if (mo) mo.disconnect(); + window.__sorgScrollGradientInitialized = false; + } catch (_) {} + } + + /** + * Initialize the scroll gradient effect. + */ + function initialize() { + // Only run on pages that have the gradient background class + if (!body.classList.contains('bg-gradient-to-br')) return; + // Prevent multiple initializations + if (window.__sorgScrollGradientInitialized) return; + window.__sorgScrollGradientInitialized = true; + if (window.__sorgScrollGradientDebug) console.log('[scroll-gradient] initialize'); + + // Remove any stale overlays if present (safety net) + var stale = document.querySelectorAll('.scroll-gradient-bg'); + for (var i = 0; i < stale.length; i++) { + if (stale[i] && stale[i].parentNode) stale[i].parentNode.removeChild(stale[i]); } - }); - mo.observe(document.documentElement, { attributes: true, attributeFilter: ['class'] }); - mo.observe(body, { attributes: true, attributeFilter: ['class'] }); - - // --- Initialization --- - if (supportsContain) { - // Create the gradient overlay container - container = document.createElement('div'); - container.className = 'scroll-gradient-bg'; - - // Create the inner element that will move with scroll - inner = document.createElement('div'); - inner.className = 'scroll-gradient-bg__inner'; - container.appendChild(inner); - - // Copy current background styles and set initial dimensions - copyComputedBackground(inner); - setHeight(); - onScroll(); - // Add the overlay to the page - body.appendChild(container); + installMutationObserver(); + + if (supportsContain) { + // Create the gradient overlay container + container = document.createElement('div'); + container.className = 'scroll-gradient-bg'; + + // Create the inner element that will move with scroll + inner = document.createElement('div'); + inner.className = 'scroll-gradient-bg__inner'; + container.appendChild(inner); - // Store original body background and clear it - bodyInlineBackgroundImageOriginal = body.style.backgroundImage; - body.style.backgroundImage = 'none'; - bodyBackgroundOverridden = true; + // Copy current background styles and set initial dimensions + copyComputedBackground(inner); + setHeight(); + onScroll(); - // Ensure all body children paint above the overlay - body.classList.add('has-scroll-gradient'); + // Add the overlay to the page + body.appendChild(container); - // Set up resize observer for dynamic content changes - if ('ResizeObserver' in window) { - resizeObserver = new ResizeObserver(function () { setHeight(); }); - resizeObserver.observe(document.documentElement); + // Store original body background and clear it + bodyInlineBackgroundImageOriginal = body.style.backgroundImage; + body.style.backgroundImage = 'none'; + bodyBackgroundOverridden = true; + + // Ensure all body children paint above the overlay + body.classList.add('has-scroll-gradient'); + + // Set up resize observer for dynamic content changes (if available) + if (typeof window.ResizeObserver === 'function') { + try { + resizeObserver = new window.ResizeObserver(function () { setHeight(); }); + resizeObserver.observe(document.documentElement); + } catch (_) { + resizeObserver = null; + } + } + // Add event listeners for scroll and resize + window.addEventListener('scroll', onScroll, { passive: true }); + window.addEventListener('resize', onResize); } - // Add event listeners for scroll and resize - window.addEventListener('scroll', onScroll, { passive: true }); - window.addEventListener('resize', onResize); } - // Cleanup function to remove all event listeners and DOM elements + // Initial run + initialize(); + + // Cleanup on pagehide (works for both unload and BFCache) window.addEventListener('pagehide', function () { - if (rafId != null) cancelAnimationFrame(rafId); - window.removeEventListener('scroll', onScroll); - window.removeEventListener('resize', onResize); - if (resizeObserver) resizeObserver.disconnect(); - if (container && container.parentNode) container.parentNode.removeChild(container); - if (bodyBackgroundOverridden) body.style.backgroundImage = bodyInlineBackgroundImageOriginal || ''; - body.classList.remove('has-scroll-gradient'); - mo.disconnect(); - window.__sorgScrollGradientInitialized = false; + cleanup(); + }); + + // Re-initialize on pageshow, especially when restored from BFCache + window.addEventListener('pageshow', function (e) { + if (window.__sorgScrollGradientDebug) console.log('[scroll-gradient] pageshow persisted=', !!(e && e.persisted)); + initialize(); }); } catch { // If we get an error, we can fail silently as we fallback to the