Verwaltung: das Licht kommt zurueck, die Buehne atmet

Vier Dinge dazu, drei davon Bewegung, eines eine Reparatur.

1. DAS LICHT FOLGT WIEDER DEM ZEIGER

Es war nie weg. --wd-lichtx/--wd-lichty wurden die ganze Zeit gesetzt,
der Lichtkegel stand korrekt an der richtigen Stelle -- er war nur
unsichtbar geworden. Die 28 % Deckkraft aus dem Grundsystem sind fuer
eine dunkle, undurchsichtige Kachel gedacht. Auf einer Glasflaeche, durch
die eine beleuchtete Kristallwelt schimmert, geht das schlicht unter.

Jetzt wirkt es auf zwei Ebenen: der Lichtkegel auf der Flaeche, und die
KANTE der Kachel leuchtet dort auf, wo der Zeiger steht. Zusammen sieht
es aus, als laege eine echte Lichtquelle ueber dem Glas, statt als waere
ein Fleck aufgemalt. Die Farbe ist die Leitfarbe des Bereichs -- im
Kundenbereich leuchtet es Indigo, bei den Zahlungen Gold.

2. DIE BUEHNE BEWEGT SICH GEGEN DEN ZEIGER

Wenige Bildpunkte, gemessen 5,8 px Ausschlag. Gerade genug, dass sich
der Raum echt anfuehlt statt wie eine Tapete -- und wenig genug, dass
beim Lesen nichts im Augenwinkel wandert. Ein Test haelt die Obergrenze
fest.

3. EIN LICHTREFLEX BEIM BEREICHSWECHSEL

Ein einzelner heller Streifen zieht schraeg ueber die Buehne, genau
einmal, dann ist er weg. Ein Moment, kein Dauerflackern.

4. DIE KACHEL HEBT SICH BEIM UEBERFAHREN AN

Zwei Bildpunkte. Sie soll reagieren, nicht huepfen.

DER FEHLER, DEN DER TEST GEFUNDEN HAT:

Die Parallaxe wirkte zuerst gar nicht. Die Werte kamen sauber an
(--vw-px, --vw-py standen korrekt am Element), das Bild stand trotzdem
still. Grund: Die Einblend-Animation animiert "transform" und haelt
ihren Endwert fest (fill-mode both) -- und eine Animation schlaegt jede
normale Regel. Die Verschiebung steht deshalb jetzt in "translate",
einer eigenen Eigenschaft, die VOR "transform" angewendet wird. Beide
koennen sich so nicht mehr in die Quere kommen.

Alles Bewegte ist bei prefers-reduced-motion aus: keine Parallaxe, kein
Reflex, kein Anheben. Die Buehne bleibt aber sichtbar -- abschalten
heisst nicht verschwinden. Auch das wird geprueft.

Die Parallaxe laeuft nur auf Geraeten mit echtem Zeiger und ist ueber
requestAnimationFrame gedrosselt. Ohne die Drosselung rechnet der
Browser bei jeder einzelnen Zeigerbewegung neu, und das merkt man
ausgerechnet beim Scrollen durch lange Listen.

Geprueft: 236 Pruefungen gruen (Verwaltung 38, Portfolio 15, CRYONOVA 53,
Blickfang 13, System 5, Abmelden 16, Abbruch 42, Angebot 54). Die
Lesbarkeit ueber der Buehne liegt weiter bei 11,9 bis 14,6:1, verlangt
sind 4,5:1.

Co-Authored-By: Claude Opus 5 <[email protected]>
This commit is contained in:
2026-08-25 00:50:43 +02:00
co-authored by Claude Opus 5
parent b147ed0d94
commit a74d924445
6 changed files with 232 additions and 7 deletions
Binary file not shown.

Before

Width:  |  Height:  |  Size: 923 KiB

After

Width:  |  Height:  |  Size: 914 KiB

+74
View File
@@ -231,6 +231,62 @@ srv.listen(4404, async () => {
v >= 4.5, v.toFixed(2) + ":1");
}
console.log("\n===== 3b. FOLGT DAS LICHT DER MAUS? =====");
await bereichAufbauen(seite, "kunden");
const karte = seite.locator("#vw-probe .wd-karte");
const kasten = await karte.boundingBox();
/* WICHTIG: mit steps bewegen. Ein Sprung ohne Zwischenschritte
erzeugt kein pointermove -- der Test wuerde "Licht folgt nicht"
melden, obwohl alles stimmt. Diese Falle hat hier schon einmal
einen Fehlalarm ausgeloest. */
await seite.mouse.move(kasten.x + 30, kasten.y + 20, { steps: 8 });
await seite.waitForTimeout(320);
const lichtA = await karte.evaluate((e) => ({
x: getComputedStyle(e).getPropertyValue("--wd-lichtx").trim(),
y: getComputedStyle(e).getPropertyValue("--wd-lichty").trim(),
}));
await seite.mouse.move(kasten.x + kasten.width - 40,
kasten.y + kasten.height - 20, { steps: 16 });
await seite.waitForTimeout(400);
const lichtB = await karte.evaluate((e) => {
const st = getComputedStyle(e);
const na = getComputedStyle(e, "::after");
return {
x: st.getPropertyValue("--wd-lichtx").trim(),
y: st.getPropertyValue("--wd-lichty").trim(),
lumen: st.getPropertyValue("--wd-lumen").trim(),
kante: st.backgroundImage.includes("radial-gradient"),
kegel: na.backgroundImage,
};
});
pruefe("das Licht wandert mit dem Zeiger",
lichtA.x !== lichtB.x && lichtA.y !== lichtB.y,
lichtA.x + "/" + lichtA.y + " -> " + lichtB.x + "/" + lichtB.y);
pruefe("die Kachelkante leuchtet an der Zeigerstelle", lichtB.kante);
/* Der Lichtkegel muss die Leitfarbe des Bereichs tragen, nicht die
Grundfarbe -- sonst leuchtet im Kundenbereich Baby Blue statt
Indigo. */
pruefe("der Lichtkegel traegt die Leitfarbe",
/97,\s*92,\s*255/.test(lichtB.kegel), lichtB.lumen);
console.log("\n===== 3c. BEWEGT SICH DIE BUEHNE MIT? =====");
await seite.mouse.move(160, 780, { steps: 12 });
await seite.waitForTimeout(650);
const lage1 = await seite.evaluate(() =>
getComputedStyle(document.querySelector(".vw-buehne")).translate);
await seite.mouse.move(1320, 130, { steps: 20 });
await seite.waitForTimeout(750);
const lage2 = await seite.evaluate(() =>
getComputedStyle(document.querySelector(".vw-buehne")).translate);
pruefe("die Buehne verschiebt sich gegen den Zeiger",
lage1 !== lage2 && lage1 !== "none", lage1 + " -> " + lage2);
/* Der Ausschlag muss klein bleiben. Waere er gross, wuerde beim Lesen
staendig etwas im Augenwinkel wandern. */
const weite = Math.max(...String(lage2).match(/-?[\d.]+/g).map((n) => Math.abs(+n)));
pruefe("und zwar nur wenige Bildpunkte", weite <= 16, weite.toFixed(1) + "px");
console.log("\n===== 4. WECHSELT DER RAUM WIRKLICH? =====");
const gesehen = [];
for (const bereich of BEREICHE) {
@@ -273,6 +329,24 @@ srv.listen(4404, async () => {
bewegung.animation === "none", bewegung.animation);
/* Abschalten heisst NICHT verschwinden. */
pruefe("die Buehne ist trotzdem da", bewegung.sichtbar === "visible", bewegung.sichtbar);
await ruhig.mouse.move(200, 700, { steps: 10 });
await ruhig.waitForTimeout(400);
await ruhig.mouse.move(1200, 200, { steps: 16 });
await ruhig.waitForTimeout(600);
const ruhigeLage = await ruhig.evaluate(() => {
const b = document.querySelector(".vw-buehne");
const vor = getComputedStyle(b, "::before");
return {
translate: getComputedStyle(b).translate,
reflex: vor.animationName,
reflexSichtbar: parseFloat(vor.opacity),
};
});
pruefe("keine Parallaxe bei reduzierter Bewegung",
ruhigeLage.translate === "none", ruhigeLage.translate);
pruefe("und kein Lichtreflex",
ruhigeLage.reflex === "none" && ruhigeLage.reflexSichtbar === 0,
ruhigeLage.reflex + ", Deckkraft " + ruhigeLage.reflexSichtbar);
await ruhig.close();
console.log("\n===== 6. HANDY =====");
Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.3 MiB

After

Width:  |  Height:  |  Size: 1.3 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 301 KiB

After

Width:  |  Height:  |  Size: 277 KiB

+1 -1
View File
@@ -34,7 +34,7 @@
Start alle alten Zwischenspeicher weg. Muss bei jeder Änderung an den
Dateien unten hochgezählt werden, sonst hängen Nutzer auf einem alten
Stand fest. */
const CACHE_NAME = "dogfather-webdesign-v37";
const CACHE_NAME = "dogfather-webdesign-v38";
/* Bausteine, die die Oberfläche zum Anzeigen braucht. Bewusst KEINE
HTML-Datei in dieser Liste. */
+157 -6
View File
@@ -176,15 +176,74 @@
rgb(3 8 16 / .14) 0%, rgb(6 19 35 / .20) 55%, rgb(3 8 16 / .40) 100%);
}
/* ---------- DIE BUEHNE ATMET MIT ----------
Eine sehr leichte Gegenbewegung zum Mauszeiger. Der Ausschlag liegt
bei wenigen Bildpunkten -- gerade genug, dass sich der Raum echt
anfuehlt statt wie eine Tapete, aber nicht so viel, dass beim Lesen
etwas wackelt.
Die Buehne ist dafuer minimal groesser als der Bildschirm skaliert.
Ohne diesen Vorhalt wuerde beim Verschieben an einer Kante ein
schwarzer Streifen auftauchen. */
.vw-buehne {
/* Die Verschiebung steht bewusst in "translate" und NICHT in
"transform".
Grund: Die Einblend-Animation weiter unten animiert "transform"
und haelt ihren Endwert fest (fill-mode both). Eine Animation
schlaegt jede normale Regel -- die Parallaxe waere damit
wirkungslos gewesen. Genau das ist beim Bauen passiert: Die Werte
kamen sauber an, das Bild stand trotzdem still.
"translate" ist eine eigene Eigenschaft und wird VOR "transform"
angewendet. Beide koennen sich so nicht mehr in die Quere kommen. */
translate: calc(var(--vw-px, 0) * -14px) calc(var(--vw-py, 0) * -10px);
transform: scale(1.045);
transition: translate .45s cubic-bezier(.2,.7,.3,1);
will-change: translate;
}
/* Beim Bereichswechsel wechselt der ganze Raum. */
@keyframes vw-buehne-ein {
from { opacity: 0; transform: scale(1.06); }
to { opacity: 1; transform: none; }
from { opacity: 0; transform: scale(1.10); }
to { opacity: 1; transform: scale(1.045); }
}
.vw-buehne { animation: vw-buehne-ein .75s cubic-bezier(.16,.8,.3,1) both; }
/* ---------- DER LICHTREFLEX BEIM WECHSEL ----------
Ein einzelner heller Streifen zieht schraeg ueber die Buehne, wenn
der Bereich wechselt. Er laeuft genau einmal und ist danach weg --
kein Dauerflackern, sondern ein Moment. */
.vw-buehne::before {
content: "";
position: absolute;
inset: -20%;
background: linear-gradient(108deg,
transparent 38%,
rgb(var(--vw-farbe) / .16) 47%,
rgb(255 255 255 / .22) 50%,
rgb(var(--vw-farbe) / .16) 53%,
transparent 62%);
opacity: 0;
animation: vw-reflex 1.05s cubic-bezier(.3,.6,.2,1) .12s both;
}
@keyframes vw-reflex {
0% { opacity: 0; transform: translateX(-62%); }
22% { opacity: 1; }
78% { opacity: 1; }
100% { opacity: 0; transform: translateX(62%); }
}
.vw-buehne { animation: vw-buehne-ein .7s cubic-bezier(.16,.8,.3,1) both; }
@media (prefers-reduced-motion: reduce) {
.vw-buehne { animation: none; }
/* Alles Bewegte aus. Die Buehne bleibt, der Reflex entfaellt --
er ist reine Zugabe und traegt keine Information. */
.vw-buehne {
animation: none; transition: none;
transform: scale(1.045); translate: none;
}
.vw-buehne::before { animation: none; opacity: 0; }
}
@media (max-width: 900px) {
@@ -249,11 +308,63 @@
/* Die Glaskacheln bekommen eine Leuchtkante in der Leitfarbe. Das
bindet Inhalt und Buehne zusammen, statt die Kacheln wie
aufgeklebte Zettel wirken zu lassen. */
/* ---------- DAS LICHT FOLGT DER MAUS ----------
Das Lumen-Licht war nie weg: --wd-lichtx/--wd-lichty werden weiter
gesetzt, der Lichtkegel steht korrekt. Er war nur unsichtbar
geworden. Die 28 % Deckkraft aus dem Grundsystem waren fuer eine
dunkle, undurchsichtige Kachel gedacht -- auf einer Glasflaeche, durch
die eine beleuchtete Kristallwelt schimmert, geht das unter.
Hier bekommt es deshalb Kraft, und zwar auf ZWEI Ebenen:
1. Der Lichtkegel auf der Flaeche (das ::after aus dem System).
2. Die KANTE der Kachel leuchtet dort auf, wo der Zeiger steht.
Zusammen sieht es aus, als laege eine echte Lichtquelle ueber dem
Glas -- und nicht, als waere ein Fleck aufgemalt. */
#vw-bereich .wd-karte {
border-color: rgb(var(--vw-farbe) / .30);
background:
/* Die Flaeche: dicht genug, dass Text darauf ruhig steht. */
linear-gradient(180deg,
rgb(var(--wd-glas-rgb) / .52), rgb(6 19 35 / .68)) padding-box,
/* Die Kante: hier steckt der Zeiger drin. Der Verlauf sitzt an
der Mausposition, dadurch wandert der helle Punkt am Rand
entlang mit. */
radial-gradient(340px circle at var(--wd-lichtx, 50%) var(--wd-lichty, 0%),
rgb(var(--vw-farbe) / .95) 0%,
rgb(var(--vw-farbe) / .40) 38%,
rgb(var(--vw-farbe) / .12) 70%,
rgb(var(--wd-lila-rgb) / .16) 100%) border-box;
border: 1px solid transparent;
box-shadow:
0 24px 60px -30px rgb(0 0 0 / .85),
inset 0 1px 0 rgb(var(--vw-farbe) / .16);
inset 0 1px 0 rgb(var(--vw-farbe) / .18);
}
/* Der Lichtkegel auf der Flaeche, kraeftiger als im Grundsystem. */
@media (hover: hover) and (pointer: fine) {
#vw-bereich .wd-karte::after {
background: radial-gradient(300px circle at var(--wd-lichtx) var(--wd-lichty),
rgb(var(--vw-farbe) / .30) 0%,
rgb(var(--vw-farbe) / .12) 42%,
transparent 72%);
}
/* Beim Ueberfahren hebt sich die Kachel minimal an. Sehr wenig --
es soll sich anfuehlen, als reagiere sie, nicht als huepfe sie. */
#vw-bereich .wd-karte {
transition: transform var(--wd-zeit, .22s) ease, box-shadow var(--wd-zeit, .22s) ease;
}
#vw-bereich .wd-karte:hover {
transform: translateY(-2px);
box-shadow:
0 30px 70px -28px rgb(0 0 0 / .9),
0 0 40px -14px rgb(var(--vw-farbe) / .40),
inset 0 1px 0 rgb(var(--vw-farbe) / .26);
}
}
@media (prefers-reduced-motion: reduce) {
#vw-bereich .wd-karte { transition: none; }
#vw-bereich .wd-karte:hover { transform: none; }
}
/* Der Titel steht als einziges frei auf der Buehne. Damit er das
@@ -3340,6 +3451,46 @@
signaturSetzen(aktiverTab);
/* ---------------------------------------------------------------------
Die Buehne bewegt sich leicht gegen den Mauszeiger.
Nur auf Geraeten mit echtem Zeiger: Auf einem Telefon gibt es kein
Schweben, dort waere der Effekt entweder wirkungslos oder wuerde bei
jeder Beruehrung zucken.
Gedrosselt ueber requestAnimationFrame -- ohne das rechnet der
Browser bei jeder einzelnen Zeigerbewegung neu, und das merkt man
ausgerechnet beim Scrollen durch lange Listen.
--------------------------------------------------------------------- */
(function buehneAtmet() {
var feinerZeiger = false;
try {
feinerZeiger = window.matchMedia &&
window.matchMedia("(hover: hover) and (pointer: fine)").matches;
if (window.matchMedia("(prefers-reduced-motion: reduce)").matches) return;
} catch (e) { return; }
if (!feinerZeiger) return;
var buehne = document.querySelector(".vw-buehne");
if (!buehne) return;
var offen = false, letzteX = 0, letzteY = 0;
document.addEventListener("pointermove", function (e) {
letzteX = e.clientX;
letzteY = e.clientY;
if (offen) return;
offen = true;
requestAnimationFrame(function () {
offen = false;
/* -0,5 bis +0,5 -- der Nullpunkt liegt in der Bildschirmmitte. */
var px = (letzteX / window.innerWidth) - 0.5;
var py = (letzteY / window.innerHeight) - 0.5;
buehne.style.setProperty("--vw-px", px.toFixed(3));
buehne.style.setProperty("--vw-py", py.toFixed(3));
});
}, { passive: true });
})();
document.querySelectorAll("[data-tab]").forEach(function (b) {
b.addEventListener("click", function () {
aktiverTab = b.dataset.tab;