From 21383b8b9c5e05112b370c6b8e0025a1d0f2c888 Mon Sep 17 00:00:00 2001
From: KoalaDev <6156589+Shik3i@users.noreply.github.com>
Date: Fri, 10 Jul 2026 20:42:54 +0200
Subject: [PATCH] Polish animated forest background
---
website/alternatives/index.html | 20 +++-
website/alternatives/kosmi.html | 20 +++-
website/alternatives/scener.html | 20 +++-
website/alternatives/screen-sharing.html | 20 +++-
website/alternatives/teleparty.html | 20 +++-
website/alternatives/twoseven.html | 20 +++-
website/alternatives/watch2gether.html | 20 +++-
website/app.js | 91 ++++++++++++++-
website/datenschutz-de.html | 20 +++-
website/impressum-de.html | 20 +++-
website/imprint.html | 20 +++-
website/join.html | 20 +++-
website/privacy.html | 20 +++-
website/style.css | 142 +++++++++++++++++++++--
website/template.html | 20 +++-
15 files changed, 402 insertions(+), 91 deletions(-)
diff --git a/website/alternatives/index.html b/website/alternatives/index.html
index 8c31896..aaac15e 100644
--- a/website/alternatives/index.html
+++ b/website/alternatives/index.html
@@ -123,6 +123,8 @@
+
+
@@ -138,6 +140,12 @@
+
+
+
+
+
+
@@ -155,12 +163,12 @@
-
-
-
-
-
-
+
+
+
+
+
+
diff --git a/website/alternatives/kosmi.html b/website/alternatives/kosmi.html
index fe203bd..1d91e35 100644
--- a/website/alternatives/kosmi.html
+++ b/website/alternatives/kosmi.html
@@ -129,6 +129,8 @@
+
+
@@ -144,6 +146,12 @@
+
+
+
+
+
+
@@ -161,12 +169,12 @@
-
-
-
-
-
-
+
+
+
+
+
+
diff --git a/website/alternatives/scener.html b/website/alternatives/scener.html
index 9a71030..f56bb81 100644
--- a/website/alternatives/scener.html
+++ b/website/alternatives/scener.html
@@ -129,6 +129,8 @@
+
+
@@ -144,6 +146,12 @@
+
+
+
+
+
+
@@ -161,12 +169,12 @@
-
-
-
-
-
-
+
+
+
+
+
+
diff --git a/website/alternatives/screen-sharing.html b/website/alternatives/screen-sharing.html
index 6f05e4c..0ea0b04 100644
--- a/website/alternatives/screen-sharing.html
+++ b/website/alternatives/screen-sharing.html
@@ -129,6 +129,8 @@
+
+
@@ -144,6 +146,12 @@
+
+
+
+
+
+
@@ -161,12 +169,12 @@
-
-
-
-
-
-
+
+
+
+
+
+
diff --git a/website/alternatives/teleparty.html b/website/alternatives/teleparty.html
index 6c3c4c6..9bb3b48 100644
--- a/website/alternatives/teleparty.html
+++ b/website/alternatives/teleparty.html
@@ -129,6 +129,8 @@
+
+
@@ -144,6 +146,12 @@
+
+
+
+
+
+
@@ -161,12 +169,12 @@
-
-
-
-
-
-
+
+
+
+
+
+
diff --git a/website/alternatives/twoseven.html b/website/alternatives/twoseven.html
index c6b125e..9003f8c 100644
--- a/website/alternatives/twoseven.html
+++ b/website/alternatives/twoseven.html
@@ -129,6 +129,8 @@
+
+
@@ -144,6 +146,12 @@
+
+
+
+
+
+
@@ -161,12 +169,12 @@
-
-
-
-
-
-
+
+
+
+
+
+
diff --git a/website/alternatives/watch2gether.html b/website/alternatives/watch2gether.html
index 3ea86da..5cc901f 100644
--- a/website/alternatives/watch2gether.html
+++ b/website/alternatives/watch2gether.html
@@ -129,6 +129,8 @@
+
+
@@ -144,6 +146,12 @@
+
+
+
+
+
+
@@ -161,12 +169,12 @@
-
-
-
-
-
-
+
+
+
+
+
+
diff --git a/website/app.js b/website/app.js
index a2ee7ae..b30f1bc 100644
--- a/website/app.js
+++ b/website/app.js
@@ -60,6 +60,9 @@ document.addEventListener('DOMContentLoaded', () => {
}
};
+ // Populated by the forest renderer further down (no-op until then)
+ let forestGreet = null;
+
// Theme toggle (saved preference wins; otherwise follows system preference,
// pre-paint class is applied by lang-init.js to avoid a flash)
const themeToggle = document.getElementById('theme-toggle');
@@ -108,6 +111,14 @@ document.addEventListener('DOMContentLoaded', () => {
applyTheme(nextTheme);
safeSetLocalStorage(themeStorageKey, nextTheme);
syncThemeMeta();
+ // 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');
+ setTimeout(() => scene.classList.remove('sunrise'), 1700);
+ }
});
}
@@ -200,6 +211,7 @@ document.addEventListener('DOMContentLoaded', () => {
entries.forEach(entry => {
if (entry.isIntersecting) {
history.replaceState(null, null, '#' + entry.target.id);
+ if (forestGreet) forestGreet();
}
});
}, { threshold: 0.3 });
@@ -220,11 +232,18 @@ document.addEventListener('DOMContentLoaded', () => {
// pointer devices the layers tilt slightly toward the cursor.
const bambooFar = document.getElementById('bamboo-far');
const bambooNear = document.getElementById('bamboo-near');
- if (bambooFar && bambooNear && !window.matchMedia('(prefers-reduced-motion: reduce)').matches) {
+ const forestScene = document.querySelector('.bg-nature');
+ 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 pointerX = -9999;
+ let pointerY = -9999;
+ let depthStep = '';
let framePending = false;
const renderForest = () => {
@@ -238,6 +257,39 @@ document.addEventListener('DOMContentLoaded', () => {
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;
+ for (const fly of fireflies) {
+ const cx = fly.el.offsetLeft + 2;
+ const cy = fly.el.offsetTop + 2;
+ const dx = cx - pointerX;
+ const dy = cy - pointerY;
+ const dist = Math.hypot(dx, dy);
+ let tx = 0;
+ let ty = 0;
+ if (dist < 130 && dist > 0.001) {
+ const push = ((130 - dist) / 130) * 46;
+ tx = (dx / dist) * push;
+ ty = (dy / dist) * push;
+ }
+ fly.x += (tx - fly.x) * 0.14;
+ fly.y += (ty - fly.y) * 0.14;
+ if (Math.abs(tx - fly.x) > 0.4 || Math.abs(ty - fly.y) > 0.4) settling = true;
+ fly.el.style.transform = `translate(${fly.x.toFixed(1)}px, ${fly.y.toFixed(1)}px)`;
+ }
+ if (settling) requestForestFrame();
};
const requestForestFrame = () => {
@@ -246,16 +298,51 @@ document.addEventListener('DOMContentLoaded', () => {
requestAnimationFrame(renderForest);
}
};
+ // Let one firefly greet newly reached sections (used by the section
+ // observer below; index rotates so it is not always the same one)
+ let helloIndex = 0;
+ forestGreet = () => {
+ 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');
+ setTimeout(() => wrap.classList.remove('firefly-hello'), 1300);
+ };
window.addEventListener('scroll', requestForestFrame, { passive: true });
window.addEventListener('resize', requestForestFrame, { passive: true });
- if (window.matchMedia('(hover: hover)').matches) {
+ if (canHover) {
window.addEventListener('pointermove', (e) => {
mouseX = (e.clientX / window.innerWidth - 0.5) * 2;
+ pointerX = e.clientX;
+ pointerY = e.clientY;
requestForestFrame();
}, { passive: true });
+ window.addEventListener('pointerleave', () => {
+ pointerX = -9999;
+ pointerY = -9999;
+ requestForestFrame();
+ });
}
renderForest();
+
+ // Wind gusts: every 12-24s the sway variables ramp up for ~3s. The
+ // custom properties are registered via @property, so they transition
+ // smoothly inside the running keyframes instead of jumping.
+ if (forestScene) {
+ const scheduleGust = () => {
+ setTimeout(() => {
+ forestScene.classList.add('gusting');
+ setTimeout(() => {
+ forestScene.classList.remove('gusting');
+ scheduleGust();
+ }, 3000);
+ }, 12000 + Math.random() * 12000);
+ };
+ scheduleGust();
+ }
}
// Invite Detection & Bridge
diff --git a/website/datenschutz-de.html b/website/datenschutz-de.html
index 6160106..7df088c 100644
--- a/website/datenschutz-de.html
+++ b/website/datenschutz-de.html
@@ -67,6 +67,8 @@
+
+
@@ -82,6 +84,12 @@
+
+
+
+
+
+
@@ -99,12 +107,12 @@
-
-
-
-
-
-
+
+
+
+
+
+
diff --git a/website/impressum-de.html b/website/impressum-de.html
index f44fb02..9b54bf5 100644
--- a/website/impressum-de.html
+++ b/website/impressum-de.html
@@ -67,6 +67,8 @@
+
+
@@ -82,6 +84,12 @@
+
+
+
+
+
+
@@ -99,12 +107,12 @@
-
-
-
-
-
-
+
+
+
+
+
+
diff --git a/website/imprint.html b/website/imprint.html
index 0ac7508..6aeb49a 100644
--- a/website/imprint.html
+++ b/website/imprint.html
@@ -67,6 +67,8 @@
+
+
@@ -82,6 +84,12 @@
+
+
+
+
+
+
@@ -99,12 +107,12 @@
-
-
-
-
-
-
+
+
+
+
+
+
diff --git a/website/join.html b/website/join.html
index 8262bc3..93a0ccd 100644
--- a/website/join.html
+++ b/website/join.html
@@ -52,6 +52,8 @@
+
+
@@ -67,6 +69,12 @@
+
+
+
+
+
+
@@ -84,12 +92,12 @@
-
-
-
-
-
-
+
+
+
+
+
+
diff --git a/website/privacy.html b/website/privacy.html
index 24bc63b..084bc56 100644
--- a/website/privacy.html
+++ b/website/privacy.html
@@ -67,6 +67,8 @@
+
+
@@ -82,6 +84,12 @@
+
+
+
+
+
+
@@ -99,12 +107,12 @@
-
-
-
-
-
-
+
+
+
+
+
+
diff --git a/website/style.css b/website/style.css
index e175bfc..d2782bb 100644
--- a/website/style.css
+++ b/website/style.css
@@ -115,6 +115,30 @@ body {
}
/* --- Animated Bamboo Forest Background (v3 Greens Update) --- */
+/* Registered so wind gusts can TRANSITION these instead of jumping */
+@property --sway {
+ syntax: '';
+ initial-value: 1.2deg;
+ inherits: true;
+}
+
+@property --leaf-drift {
+ syntax: '';
+ initial-value: 26px;
+ inherits: true;
+}
+
+.bg-nature {
+ --sway: 1.2deg;
+ --leaf-drift: 26px;
+ transition: --sway 1.6s ease-in-out, --leaf-drift 1.6s ease-in-out;
+}
+
+.bg-nature.gusting {
+ --sway: 3.4deg;
+ --leaf-drift: 68px;
+}
+
.bg-nature {
position: fixed;
top: 0;
@@ -143,12 +167,37 @@ body {
height: 140%;
border-radius: 999px;
filter: blur(5.5px);
+ transition: filter 1.2s ease;
background: linear-gradient(180deg, oklch(0.32 0.05 145 / 0.35), oklch(0.24 0.04 140 / 0.15));
}
#bamboo-far .far-1 { left: 2%; width: 26px; }
#bamboo-far .far-2 { left: 7.5%; width: 18px; }
#bamboo-far .far-3 { right: 4%; width: 30px; }
+#bamboo-far .far-4 { left: 13%; width: 20px; }
+#bamboo-far .far-5 { right: 10.5%; width: 16px; }
+
+/* Two barely-there trunks break up the empty middle without competing with
+ the content. They share the far layer's single parallax transform. */
+#bamboo-far::before,
+#bamboo-far::after {
+ content: '';
+ position: absolute;
+ top: -20%;
+ height: 140%;
+ border-radius: 999px;
+ filter: blur(8px);
+ opacity: 0.28;
+ background: linear-gradient(180deg, oklch(0.34 0.05 145 / 0.3), oklch(0.23 0.04 140 / 0.1));
+}
+
+#bamboo-far::before { left: 24%; width: 13px; }
+#bamboo-far::after { right: 25%; width: 17px; }
+
+/* Depth of field: the far layer blurs further as you descend (stepped by
+ JS on threshold crossings, never per frame) */
+.bg-nature.depth-mid #bamboo-far .bamboo-stalk { filter: blur(6.5px); }
+.bg-nature.depth-deep #bamboo-far .bamboo-stalk { filter: blur(8px); }
/* (b) Near bamboo layer: sharp stalks with nodes and leaves, gentle sway */
#bamboo-near {
@@ -169,6 +218,7 @@ body {
#bamboo-near .near-1 { left: 4.5%; width: 13px; animation-duration: 9.2s; }
#bamboo-near .near-2 { right: 7%; width: 11px; animation-duration: 11s; animation-delay: -3.5s; }
+#bamboo-near .near-3 { left: 10%; width: 9px; animation-duration: 12.5s; animation-delay: -6s; opacity: 0.75; }
#bamboo-near .bamboo-node {
position: absolute;
@@ -194,10 +244,11 @@ body {
#bamboo-near .near-1 .leaf-1 { top: 27.5%; left: 10px; transform: rotate(18deg); }
#bamboo-near .near-1 .leaf-2 { top: 47.5%; left: -28px; transform: rotate(-160deg); width: 22px; height: 10px; }
#bamboo-near .near-2 .leaf-1 { top: 44%; left: -26px; transform: rotate(-155deg); }
+#bamboo-near .near-3 .leaf-1 { top: 33%; left: 7px; transform: rotate(24deg); width: 18px; height: 9px; }
@keyframes bambooSway {
0%, 100% { transform: rotate(0); }
- 50% { transform: rotate(1.2deg); }
+ 50% { transform: rotate(var(--sway, 1.2deg)); }
}
/* (d) Falling leaves: outer element falls + rotates, inner leaf sways */
@@ -245,7 +296,7 @@ body {
@keyframes leafSway {
0%, 100% { margin-left: 0; }
- 50% { margin-left: 26px; }
+ 50% { margin-left: var(--leaf-drift, 26px); }
}
/* Scroll journey: JS drives the opacity of these two groups so the page
@@ -377,11 +428,32 @@ body {
50% { opacity: 0.85; }
}
-/* (i) Fireflies */
-.firefly {
+/* Theme switch to light: the horizon flares up once like a sunrise */
+.bg-nature.sunrise .bg-horizon {
+ animation:
+ horizonBreathe 10s ease-in-out infinite,
+ sunriseFlash 1.6s ease-out;
+}
+
+@keyframes sunriseFlash {
+ 0% { opacity: 0.5; }
+ 30% { opacity: 1; }
+ 100% { opacity: 0.5; }
+}
+
+/* (i) Fireflies: the wrapper holds the base position and is what the JS
+ pushes away from the cursor; the inner dot pulses (transform) and
+ wanders (translate) independently. */
+.firefly-wrap {
position: absolute;
width: 4px;
height: 4px;
+}
+
+.firefly {
+ display: block;
+ width: 4px;
+ height: 4px;
border-radius: 50%;
background: oklch(0.68 0.13 150);
box-shadow: 0 0 8px oklch(0.68 0.13 150);
@@ -390,19 +462,38 @@ body {
fireflyWander 21s ease-in-out infinite alternate;
}
-.firefly:nth-child(even) {
+.fw-2 .firefly,
+.fw-4 .firefly,
+.fw-6 .firefly {
width: 3px;
height: 3px;
background: oklch(0.62 0.14 45);
box-shadow: 0 0 8px oklch(0.62 0.14 45);
}
-.firefly-1 { top: 32%; left: 12%; animation-delay: -0.4s, -4s; }
-.firefly-2 { top: 58%; left: 22%; animation-duration: 4.4s, 26s; animation-delay: -2.1s, -11s; }
-.firefly-3 { top: 44%; left: 46%; animation-duration: 3.4s, 18s; animation-delay: -1.2s, -7s; }
-.firefly-4 { top: 70%; left: 64%; animation-duration: 4.1s, 29s; animation-delay: -3s, -19s; }
-.firefly-5 { top: 26%; left: 78%; animation-duration: 3.6s, 23s; animation-delay: -1.8s, -14s; }
-.firefly-6 { top: 62%; left: 90%; animation-duration: 4.2s, 17s; animation-delay: -0.9s, -2s; }
+.fw-1 { top: 32%; left: 12%; }
+.fw-2 { top: 58%; left: 22%; }
+.fw-3 { top: 44%; left: 46%; }
+.fw-4 { top: 70%; left: 64%; }
+.fw-5 { top: 26%; left: 78%; }
+.fw-6 { top: 62%; left: 90%; }
+
+.fw-1 .firefly { animation-delay: -0.4s, -4s; }
+.fw-2 .firefly { animation-duration: 4.4s, 26s; animation-delay: -2.1s, -11s; }
+.fw-3 .firefly { animation-duration: 3.4s, 18s; animation-delay: -1.2s, -7s; }
+.fw-4 .firefly { animation-duration: 4.1s, 29s; animation-delay: -3s, -19s; }
+.fw-5 .firefly { animation-duration: 3.6s, 23s; animation-delay: -1.8s, -14s; }
+.fw-6 .firefly { animation-duration: 4.2s, 17s; animation-delay: -0.9s, -2s; }
+
+/* One firefly greets you when a new section scrolls into view */
+.firefly-wrap.firefly-hello {
+ animation: fireflyHello 1.2s ease-out;
+}
+
+@keyframes fireflyHello {
+ 0%, 100% { transform: scale(1); filter: brightness(1); }
+ 35% { transform: scale(2.4); filter: brightness(1.7); }
+}
@keyframes fireflyPulse {
0%, 100% { opacity: 0.2; transform: scale(1); }
@@ -443,6 +534,22 @@ body {
background-size: 160px 160px;
}
+@media (max-width: 768px) {
+ /* Half the leaves, no god rays / light sweep / extra stalks: the scene
+ stays recognisable while the animation count roughly halves. */
+ .fall-leaf-5,
+ .fall-leaf-6,
+ .fall-leaf-7,
+ .fall-leaf-8,
+ .bg-godrays,
+ .bg-light-sweep,
+ #bamboo-far .far-4,
+ #bamboo-far .far-5,
+ #bamboo-near .near-3 {
+ display: none;
+ }
+}
+
@media (prefers-reduced-motion: reduce) {
.bg-nature *,
.bg-nature *::before,
@@ -5265,6 +5372,19 @@ header[id] {
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 {
animation: logo-entrance 1.2s cubic-bezier(0.34, 1.56, 0.64, 1) forwards;
diff --git a/website/template.html b/website/template.html
index 6ec5708..71bd297 100644
--- a/website/template.html
+++ b/website/template.html
@@ -232,6 +232,8 @@
+
+
@@ -247,6 +249,12 @@
+
+
+
+
+
+
@@ -264,12 +272,12 @@
-
-
-
-
-
-
+
+
+
+
+
+