perf(website): migrate forest parallax to native CSS scroll-driven animations, fix step contrast, and resolve forced reflows

This commit is contained in:
KoalaDev
2026-07-12 06:18:58 +02:00
parent e27319908d
commit 44f9a78bb0
8 changed files with 191 additions and 106 deletions
+36 -60
View File
@@ -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();
+1 -1
View File
@@ -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;
}
+76 -22
View File
@@ -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);
}
+1 -12
View File
@@ -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 {
+70
View File
@@ -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);
}
+5 -1
View File
@@ -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;
-9
View File
@@ -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;
+2 -1
View File
@@ -10,6 +10,7 @@
<link rel="dns-prefetch" href="https://addons.mozilla.org">
<link rel="dns-prefetch" href="https://github.com">
<link rel="preload" href="{{ASSET_PATH}}landing.min.css" as="style">
<link rel="stylesheet" href="{{ASSET_PATH}}landing.min.css">
<link rel="preload" as="image" href="{{ASSET_PATH}}assets/LookDownKoala.avif" imagesrcset="{{ASSET_PATH}}assets/LookDownKoala-1x.avif 90w, {{ASSET_PATH}}assets/LookDownKoala.avif 205w" imagesizes="90px" type="image/avif" fetchpriority="high">
<link rel="icon" type="image/png" sizes="16x16" href="{{ASSET_PATH}}assets/favicon-16x16.png">
@@ -294,7 +295,7 @@
<nav>
<div class="container nav-content">
<a class="logo-area" href="{{ASSET_PATH}}" aria-label="KoalaSync home">
<img class="nav-logo-img" src="{{ASSET_PATH}}assets/NewLogoIcon_128.webp" srcset="{{ASSET_PATH}}assets/NewLogoIcon_64.webp 64w, {{ASSET_PATH}}assets/NewLogoIcon_128.webp 128w, {{ASSET_PATH}}assets/NewLogoIcon.webp 256w" sizes="64px" alt="KoalaSync Logo" width="64" height="64">
<img class="nav-logo-img" src="{{ASSET_PATH}}assets/NewLogoIcon_128.webp" srcset="{{ASSET_PATH}}assets/NewLogoIcon_64.webp 64w, {{ASSET_PATH}}assets/NewLogoIcon_128.webp 128w, {{ASSET_PATH}}assets/NewLogoIcon.webp 256w" sizes="64px" alt="KoalaSync Logo" width="64" height="64" fetchpriority="high" decoding="async">
<span>KoalaSync</span>
<div id="nav-extension-status" class="nav-ext-status" style="display: none;">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="3" stroke-linecap="round" stroke-linejoin="round" width="12" height="12"><polyline points="20 6 9 17 4 12"></polyline></svg>