From 44f9a78bb06726ee33a7fdc0615f84e0e0220599 Mon Sep 17 00:00:00 2001 From: KoalaDev <6156589+Shik3i@users.noreply.github.com> Date: Sun, 12 Jul 2026 06:18:58 +0200 Subject: [PATCH] perf(website): migrate forest parallax to native CSS scroll-driven animations, fix step contrast, and resolve forced reflows --- website/app.js | 96 +++++++++++----------------- website/build.cjs | 2 +- website/style.legacy.css | 98 ++++++++++++++++++++++------- website/styles/demo.css | 13 +--- website/styles/foundation.css | 70 +++++++++++++++++++++ website/styles/landing-primary.css | 6 +- website/styles/landing-sections.css | 9 --- website/template.html | 3 +- 8 files changed, 191 insertions(+), 106 deletions(-) diff --git a/website/app.js b/website/app.js index f11d24a..96f53f5 100644 --- a/website/app.js +++ b/website/app.js @@ -60,6 +60,16 @@ document.addEventListener('DOMContentLoaded', () => { } }; + const restartAnimation = (el, className) => { + if (!el) return; + el.classList.remove(className); + requestAnimationFrame(() => { + requestAnimationFrame(() => { + el.classList.add(className); + }); + }); + }; + // Populated by the forest renderer further down (no-op until then) let forestGreet = null; @@ -114,9 +124,7 @@ document.addEventListener('DOMContentLoaded', () => { // Switching into light mode flares the horizon once (sunrise) const scene = document.querySelector('.bg-nature'); if (nextTheme === 'light' && scene && !window.matchMedia('(prefers-reduced-motion: reduce)').matches) { - scene.classList.remove('sunrise'); - void scene.offsetWidth; - scene.classList.add('sunrise'); + restartAnimation(scene, 'sunrise'); setTimeout(() => scene.classList.remove('sunrise'), 1700); } }); @@ -250,45 +258,20 @@ document.addEventListener('DOMContentLoaded', () => { const prefersReducedMotion = window.matchMedia('(prefers-reduced-motion: reduce)').matches; if (bambooFar && bambooNear && !prefersReducedMotion) { const depthDay = document.querySelector('.bg-depth-day'); - const depthDusk = document.querySelector('.bg-depth-dusk'); - const duskTint = document.querySelector('.bg-dusk-tint'); const fireflies = Array.from(document.querySelectorAll('.firefly-wrap'), (el) => ({ el, x: 0, y: 0 })); const canHover = window.matchMedia('(hover: hover)').matches; let mouseX = 0; let mouseY = 0; let pointerX = -9999; let pointerY = -9999; - let depthStep = ''; let framePending = false; const renderForest = () => { framePending = false; - const doc = document.documentElement; - const maxScroll = Math.max(1, doc.scrollHeight - window.innerHeight); - const depth = Math.min(1, window.scrollY / maxScroll); - // Couple the forest directly to the scroll distance. The layers - // follow the content's direction (up-screen while scrolling down) - // but slower — near fastest, far slowest — so the forest recedes - // with believable depth instead of moving against the page. - const shift = Math.min(window.innerHeight * 0.55, window.scrollY * 0.14); - bambooFar.style.transform = `translate(${(-mouseX * 5).toFixed(1)}px, ${(-shift * 0.42).toFixed(1)}px)`; - bambooNear.style.transform = `translate(${(mouseX * 11).toFixed(1)}px, ${(-shift).toFixed(1)}px)`; - if (bambooMid) bambooMid.style.transform = `translate(${(mouseX * 4).toFixed(1)}px, ${(-shift * 0.7).toFixed(1)}px)`; + bambooFar.style.transform = `translateX(${(-mouseX * 5).toFixed(1)}px)`; + bambooNear.style.transform = `translateX(${(mouseX * 11).toFixed(1)}px)`; + if (bambooMid) bambooMid.style.transform = `translateX(${(mouseX * 4).toFixed(1)}px)`; if (depthDay) depthDay.style.transform = `translate(${(-mouseX * 2.5).toFixed(1)}px, ${(mouseY * 1.5).toFixed(1)}px)`; - if (depthDay) depthDay.style.opacity = (1 - depth * 0.75).toFixed(3); - if (depthDusk) depthDusk.style.opacity = (0.35 + depth * 0.65).toFixed(3); - if (duskTint) duskTint.style.opacity = (depth * 0.85).toFixed(3); - - // Depth of field: step the far layer's blur on threshold crossings - // only (filter changes are too expensive to run per frame) - if (forestScene) { - const step = depth < 0.33 ? '' : depth < 0.66 ? 'depth-mid' : 'depth-deep'; - if (step !== depthStep) { - forestScene.classList.remove('depth-mid', 'depth-deep'); - if (step) forestScene.classList.add(step); - depthStep = step; - } - } // Fireflies shy away from the cursor and drift back once it leaves let settling = false; @@ -326,13 +309,9 @@ document.addEventListener('DOMContentLoaded', () => { if (!fireflies.length) return; const wrap = fireflies[helloIndex % fireflies.length].el; helloIndex += 1; - wrap.classList.remove('firefly-hello'); - void wrap.offsetWidth; - wrap.classList.add('firefly-hello'); + restartAnimation(wrap, 'firefly-hello'); setTimeout(() => wrap.classList.remove('firefly-hello'), 1300); }; - - window.addEventListener('scroll', requestForestFrame, { passive: true }); window.addEventListener('resize', requestForestFrame, { passive: true }); if (canHover) { window.addEventListener('pointermove', (e) => { @@ -888,10 +867,7 @@ document.addEventListener('DOMContentLoaded', () => { }; const pulse = (key) => { - const el = tabs[key].root; - el.classList.remove('sync-pulse'); - void el.offsetWidth; // restart the CSS animation - el.classList.add('sync-pulse'); + restartAnimation(tabs[key].root, 'sync-pulse'); }; const NAMES = { a: '🐱 ChillCat', b: '🐶 HappyDog' }; @@ -949,16 +925,15 @@ document.addEventListener('DOMContentLoaded', () => { let seekFlashTimers = []; const flashSeek = (root) => { if (!root) return; - root.classList.remove('demo-seeking'); // Restart every animated layer from frame zero so the jump is visible const film = root.querySelector('.demo-film'); if (film) { film.classList.add('demo-reset'); - void film.offsetWidth; // force reflow so the browser commits the reset - film.classList.remove('demo-reset'); + requestAnimationFrame(() => { + film.classList.remove('demo-reset'); + }); } - void root.offsetWidth; - root.classList.add('demo-seeking'); + restartAnimation(root, 'demo-seeking'); const t = setTimeout(() => root.classList.remove('demo-seeking'), 360); seekFlashTimers.push(t); }; @@ -1051,10 +1026,13 @@ document.addEventListener('DOMContentLoaded', () => { inviteFly.style.left = fromPoint.x + 'px'; inviteFly.style.top = fromPoint.y + 'px'; inviteFly.style.opacity = '1'; - void inviteFly.offsetWidth; - inviteFly.style.transition = ''; - inviteFly.style.left = (toPoint.x - 40) + 'px'; - inviteFly.style.top = (toPoint.y + 2) + 'px'; + requestAnimationFrame(() => { + requestAnimationFrame(() => { + inviteFly.style.transition = ''; + inviteFly.style.left = (toPoint.x - 40) + 'px'; + inviteFly.style.top = (toPoint.y + 2) + 'px'; + }); + }); setTimeout(() => { inviteFly.style.opacity = '0'; resolve(); @@ -1067,9 +1045,7 @@ document.addEventListener('DOMContentLoaded', () => { if (videoSelect.options.length > 1) { videoSelect.selectedIndex = 1; } - videoSelect.classList.remove('demo-attn'); - void videoSelect.offsetWidth; - videoSelect.classList.add('demo-attn'); + restartAnimation(videoSelect, 'demo-attn'); }; if (createRoomBtn) createRoomBtn.addEventListener('click', () => setRoomJoined(true)); @@ -1087,8 +1063,9 @@ document.addEventListener('DOMContentLoaded', () => { setConnected(false); setRoomJoined(false); } - void scene.offsetWidth; - scene.classList.remove('demo-no-anim'); + requestAnimationFrame(() => { + scene.classList.remove('demo-no-anim'); + }); const showHint = () => { if (hint) hint.classList.add('show'); @@ -1133,9 +1110,10 @@ document.addEventListener('DOMContentLoaded', () => { seekFlashTimers.forEach(clearTimeout); seekFlashTimers = []; - // Force reflow and remove demo-no-anim: - void scene.offsetWidth; - scene.classList.remove('demo-no-anim'); + // Remove demo-no-anim on next frame: + requestAnimationFrame(() => { + scene.classList.remove('demo-no-anim'); + }); }; const finishDemo = () => { if (demoFinished) return; @@ -1214,9 +1192,7 @@ document.addEventListener('DOMContentLoaded', () => { if (userTookOver || demoRunId !== myRunId || !el) return false; await moveTo(el, fx); if (userTookOver || demoRunId !== myRunId) return false; - cursor.classList.remove('clicking'); - void cursor.offsetWidth; - cursor.classList.add('clicking'); + restartAnimation(cursor, 'clicking'); await wait(180); if (userTookOver || demoRunId !== myRunId) return false; if (action) action(); else el.click(); diff --git a/website/build.cjs b/website/build.cjs index 05a76cf..a6a110f 100644 --- a/website/build.cjs +++ b/website/build.cjs @@ -107,7 +107,7 @@ async function minifyJS(raw) { const result = await esbuild.transform(raw, { loader: 'js', minify: true, - target: 'es2020' + target: 'es2022' }); return result.code; } diff --git a/website/style.legacy.css b/website/style.legacy.css index 0e8633d..57ecfc1 100644 --- a/website/style.legacy.css +++ b/website/style.legacy.css @@ -2583,7 +2583,7 @@ input:checked + .mock-slider:before { .step-num { font-size: 3.25rem; font-weight: 900; - background: linear-gradient(135deg, oklch(0.68 0.13 150 / 0.5), oklch(0.62 0.14 45 / 0.15)); + background: linear-gradient(135deg, oklch(0.68 0.13 150 / 0.75), oklch(0.62 0.14 45 / 0.5)); -webkit-background-clip: text; -webkit-text-fill-color: transparent; line-height: 1; @@ -2592,6 +2592,10 @@ input:checked + .mock-slider:before { letter-spacing: 0; } +html.theme-light .step-num { + background: linear-gradient(135deg, oklch(0.45 0.10 150), oklch(0.40 0.08 120)); +} + .step-text h3 { font-size: 1.65rem; margin-bottom: 0.75rem; @@ -4231,16 +4235,7 @@ html.theme-light .compat-logo img.compat-logo-disneyplus-mark { border-bottom: 0; } -#self-hosting, -#faq, -#faq + section, -footer { - background: var(--section-tint-strong); -} -#self-hosting { - border-top: 1px solid var(--glass-border); -} .comparison-table-wrapper { overflow-x: auto; @@ -6113,18 +6108,7 @@ html.theme-light .hero-demo-scene .demo-video::after { box-shadow: 0 0 8px var(--accent-glow); } -.scroll-progress-bar::after { - content: ''; - position: absolute; - right: -3px; - top: 50%; - width: 10px; - height: 7px; - border-radius: 0 70% 0 70%; - background: var(--accent-green); - box-shadow: 0 0 6px var(--accent-glow); - transform: translateY(-50%) rotate(24deg); -} + /* Navigation logo pop & spin */ .nav-logo-img { @@ -6535,3 +6519,73 @@ html.theme-light .mobile-demo-close:hover { grid-template-columns: 1fr; } } + +/* ── 2026 Performance Optimizations: Scroll-Driven Animations ── */ +@keyframes scrollParallaxFar { + from { translate: 0 0; } + to { translate: 0 calc(-55vh * 0.42); } +} +@keyframes scrollParallaxMid { + from { translate: 0 0; } + to { translate: 0 calc(-55vh * 0.7); } +} +@keyframes scrollParallaxNear { + from { translate: 0 0; } + to { translate: 0 -55vh; } +} +@keyframes fadeDay { + from { opacity: 1; } + to { opacity: 0.25; } +} +@keyframes fadeDusk { + from { opacity: 0.35; } + to { opacity: 1; } +} +@keyframes fadeTint { + from { opacity: 0; } + to { opacity: 0.85; } +} +@keyframes blurFar { + 0%, 33% { filter: blur(4.5px); } + 34%, 66% { filter: blur(5.5px); } + 67%, 100% { filter: blur(6.5px); } +} +@keyframes blurMid { + 0%, 33% { filter: blur(2px); } + 34%, 66% { filter: blur(3px); } + 67%, 100% { filter: blur(4px); } +} + +#bamboo-far { + animation: scrollParallaxFar linear; + animation-timeline: scroll(root); +} +#bamboo-mid { + animation: scrollParallaxMid linear; + animation-timeline: scroll(root); +} +#bamboo-near { + animation: scrollParallaxNear linear; + animation-timeline: scroll(root); +} +.bg-depth-day { + animation: fadeDay linear; + animation-timeline: scroll(root); +} +.bg-depth-dusk { + animation: fadeDusk linear; + animation-timeline: scroll(root); +} +.bg-dusk-tint { + animation: fadeTint linear; + animation-timeline: scroll(root); +} + +#bamboo-far .bamboo-stalk { + animation: blurFar linear; + animation-timeline: scroll(root); +} +#bamboo-mid .bamboo-stalk { + animation: blurMid linear; + animation-timeline: scroll(root); +} diff --git a/website/styles/demo.css b/website/styles/demo.css index d54346d..890a53c 100644 --- a/website/styles/demo.css +++ b/website/styles/demo.css @@ -1194,18 +1194,7 @@ html.theme-light .hero-demo-scene .demo-video::after { box-shadow: 0 0 8px var(--accent-glow); } -.scroll-progress-bar::after { - content: ''; - position: absolute; - right: -3px; - top: 50%; - width: 10px; - height: 7px; - border-radius: 0 70% 0 70%; - background: var(--accent-green); - box-shadow: 0 0 6px var(--accent-glow); - transform: translateY(-50%) rotate(24deg); -} + /* Navigation logo pop & spin */ .nav-logo-img { diff --git a/website/styles/foundation.css b/website/styles/foundation.css index d6773ad..bc744f7 100644 --- a/website/styles/foundation.css +++ b/website/styles/foundation.css @@ -1419,3 +1419,73 @@ html.theme-light .compat-logo { filter: none; } +/* ── 2026 Performance Optimizations: Scroll-Driven Animations ── */ +@keyframes scrollParallaxFar { + from { translate: 0 0; } + to { translate: 0 calc(-55vh * 0.42); } +} +@keyframes scrollParallaxMid { + from { translate: 0 0; } + to { translate: 0 calc(-55vh * 0.7); } +} +@keyframes scrollParallaxNear { + from { translate: 0 0; } + to { translate: 0 -55vh; } +} +@keyframes fadeDay { + from { opacity: 1; } + to { opacity: 0.25; } +} +@keyframes fadeDusk { + from { opacity: 0.35; } + to { opacity: 1; } +} +@keyframes fadeTint { + from { opacity: 0; } + to { opacity: 0.85; } +} +@keyframes blurFar { + 0%, 33% { filter: blur(4.5px); } + 34%, 66% { filter: blur(5.5px); } + 67%, 100% { filter: blur(6.5px); } +} +@keyframes blurMid { + 0%, 33% { filter: blur(2px); } + 34%, 66% { filter: blur(3px); } + 67%, 100% { filter: blur(4px); } +} + +#bamboo-far { + animation: scrollParallaxFar linear; + animation-timeline: scroll(root); +} +#bamboo-mid { + animation: scrollParallaxMid linear; + animation-timeline: scroll(root); +} +#bamboo-near { + animation: scrollParallaxNear linear; + animation-timeline: scroll(root); +} +.bg-depth-day { + animation: fadeDay linear; + animation-timeline: scroll(root); +} +.bg-depth-dusk { + animation: fadeDusk linear; + animation-timeline: scroll(root); +} +.bg-dusk-tint { + animation: fadeTint linear; + animation-timeline: scroll(root); +} + +#bamboo-far .bamboo-stalk { + animation: blurFar linear; + animation-timeline: scroll(root); +} +#bamboo-mid .bamboo-stalk { + animation: blurMid linear; + animation-timeline: scroll(root); +} + diff --git a/website/styles/landing-primary.css b/website/styles/landing-primary.css index 8d4fc6c..1dfce32 100644 --- a/website/styles/landing-primary.css +++ b/website/styles/landing-primary.css @@ -254,7 +254,7 @@ .step-num { font-size: 3.25rem; font-weight: 900; - background: linear-gradient(135deg, oklch(0.68 0.13 150 / 0.5), oklch(0.62 0.14 45 / 0.15)); + background: linear-gradient(135deg, oklch(0.68 0.13 150 / 0.75), oklch(0.62 0.14 45 / 0.5)); -webkit-background-clip: text; -webkit-text-fill-color: transparent; line-height: 1; @@ -263,6 +263,10 @@ letter-spacing: 0; } +html.theme-light .step-num { + background: linear-gradient(135deg, oklch(0.45 0.10 150), oklch(0.40 0.08 120)); +} + .step-text h3 { font-size: 1.65rem; margin-bottom: 0.75rem; diff --git a/website/styles/landing-sections.css b/website/styles/landing-sections.css index 47fb11c..96bf997 100644 --- a/website/styles/landing-sections.css +++ b/website/styles/landing-sections.css @@ -146,16 +146,7 @@ border-bottom: 0; } -#self-hosting, -#faq, -#faq + section, -footer { - background: var(--section-tint-strong); -} -#self-hosting { - border-top: 1px solid var(--glass-border); -} .comparison-table-wrapper { overflow-x: auto; diff --git a/website/template.html b/website/template.html index ce9964b..69e2098 100644 --- a/website/template.html +++ b/website/template.html @@ -10,6 +10,7 @@ + @@ -294,7 +295,7 @@