mirror of
https://github.com/Shik3i/KoalaSync.git
synced 2026-07-26 12:08:15 +00:00
perf(website): migrate forest parallax to native CSS scroll-driven animations, fix step contrast, and resolve forced reflows
This commit is contained in:
+30
-54
@@ -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
|
||||
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;
|
||||
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;
|
||||
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;
|
||||
// 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
@@ -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
@@ -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
@@ -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 {
|
||||
|
||||
Vendored
+70
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user