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 @@