Verwaltung: Zeigerlampe, Prisma-Kanten, schwebende Partikel

Drei Dinge, die zusammen ein Konzept ergeben: Das Bild reagiert auf den
Zeiger, die Kacheln brechen das Licht wie Eis, und im Raum schwebt
etwas.

1. DIE ZEIGERLAMPE

Ein weicher Lichtkegel wandert ueber die Buehne und hellt die Kristalle
dort auf, wo der Zeiger steht. Damit wird das Bild zu einer Flaeche, die
auf einen reagiert, statt nur dazuzuliegen.

Der Kniff steckt in mix-blend-mode: soft-light. Ein normaler heller
Verlauf wuerde das Bild ueberdecken und milchig machen. "soft-light"
rechnet stattdessen mit dem, was darunter liegt -- dunkle Stellen
bleiben dunkel, vorhandene Lichtkanten der Kristalle werden verstaerkt.
Das Bild wird nicht ueberstrahlt, es wird beleuchtet. Bewusst NICHT
"screen" oder "overlay": Beide lassen die Eiskanten ausbrennen, und
genau die machen den Reiz der Motive aus.

2. PRISMA-SCHIMMER AN DEN KACHELKANTEN

Am hellsten Punkt sitzt die Leitfarbe, daneben faechert die Kante in
Nachbartoene auf -- wie Licht, das sich in einer Glaskante bricht.
Bewusst KEIN Regenbogen: Volle Spektralfarben sehen nach Seifenblase
aus, nicht nach geschliffenem Eis. Es bleibt in der kalten Haelfte der
Palette. Die Kante ist dafuer 1,5 px statt 1 px -- bei genau einem Punkt
verschluckt das Bildschirmraster die Aufaecherung fast vollstaendig.

3. SCHWEBENDE PARTIKEL

Neun Lichtpunkte steigen sehr langsam auf, jeder mit eigener Bahn,
Dauer und Startzeit. Rein aus CSS, ohne Zeichenflaeche -- eine
Zeichenflaeche wuerde dauerhaft Rechenzeit kosten, und das auf einer
Seite, auf der man arbeitet. Es soll wirken wie Staub im Lichtkegel,
nicht wie Schneefall.

DER FEHLER, DEN ERST DER SCREENSHOT ZEIGTE:

Die Lampe legte sich als gruenlicher Fleck mitten auf eine Kachel. Ein
Element mit mix-blend-mode mischt sich mit ALLEM in seinem
Stapelkontext -- auch mit Elementen, die eigentlich darueber liegen.
Buehne und Lampe stecken deshalb jetzt in einem gemeinsamen Raum mit
isolation: isolate. Dort endet die Mischung, und die Lampe beleuchtet
nur noch das Bild.

Was DANACH noch durchkam, ist dagegen richtig so: Die Kacheln tragen
eine Rueckseiten-Unschaerfe, nehmen also auf, was hinter ihnen liegt.
Licht, das durch Milchglas scheint. Bei einem engen Kegel war davon
allerdings ein scharf umrissener Kreis uebrig -- deshalb jetzt ein
weiter Radius mit flachen Stufen, damit sich der Helligkeitsunterschied
ueber die halbe Kachel verteilt und als Schimmer liest.

Drei Testmeldungen waren durch den Umbau entstanden und kein Mangel der
Seite: Haltung, Stapelplatz und die Auszeichnung als Zierde sitzen jetzt
am Raum, nicht mehr an der Buehne darin. Der Test prueft sie dort.

Bei prefers-reduced-motion sind die Partikel komplett weg -- nicht nur
angehalten. Ein eingefrorener Punkt mitten im Bild waere ein Fleck ohne
Sinn. Ohne feinen Zeiger entfaellt die Lampe ganz.

Geprueft: 252 Pruefungen gruen (Verwaltung 54, Portfolio 15, CRYONOVA 53,
Blickfang 13, System 5, Abmelden 16, Abbruch 42, Angebot 54).

Co-Authored-By: Claude Opus 5 <[email protected]>
This commit is contained in:
2026-08-25 07:15:06 +02:00
co-authored by Claude Opus 5
parent fdf3e5349e
commit 4c0941c6da
6 changed files with 250 additions and 12 deletions
Binary file not shown.

Before

Width:  |  Height:  |  Size: 298 KiB

After

Width:  |  Height:  |  Size: 298 KiB

+76 -4
View File
@@ -129,6 +129,12 @@ srv.listen(4404, async () => {
if (!el) return null;
const st = getComputedStyle(el);
const r = el.getBoundingClientRect();
/* Seit Buehne und Lampe in einem gemeinsamen Raum stecken, sitzen
Haltung, Stapelplatz und die Auszeichnung als Zierde am RAUM --
die Buehne darin ist absolut positioniert. Geprueft wird deshalb
der Raum, sonst misst der Test am falschen Element. */
const raum = el.parentElement;
const rs = getComputedStyle(raum);
const treffer = st.backgroundImage.match(/img\/([a-z0-9-]+\.webp)/i);
return {
datei: treffer ? treffer[1] : "(keins)",
@@ -137,9 +143,10 @@ srv.listen(4404, async () => {
haltung: st.position,
breite: Math.round(r.width),
hoehe: Math.round(r.height),
z: parseInt(st.zIndex, 10),
klicks: st.pointerEvents,
versteckt: el.getAttribute("aria-hidden"),
haltungRaum: rs.position,
z: parseInt(rs.zIndex, 10),
klicks: rs.pointerEvents,
versteckt: raum.getAttribute("aria-hidden"),
};
});
if (!buehne) { pruefe("Buehne vorhanden", false); }
@@ -153,7 +160,7 @@ srv.listen(4404, async () => {
pruefe("die Buehne fuellt den ganzen Bildschirm",
buehne.breite >= 1430 && buehne.hoehe >= 890, `${buehne.breite}x${buehne.hoehe}`);
pruefe("sie steht fest, der Inhalt scrollt darueber",
buehne.haltung === "fixed", buehne.haltung);
buehne.haltungRaum === "fixed", buehne.haltungRaum);
pruefe("sie liegt hinter allem", buehne.z < 0, "z-index " + buehne.z);
pruefe("sie faengt keine Klicks ab", buehne.klicks === "none", buehne.klicks);
pruefe("sie ist als Zierde ausgezeichnet", buehne.versteckt === "true");
@@ -359,6 +366,62 @@ srv.listen(4404, async () => {
new Set(staffel.map((k) => k.versatz)).size === staffel.length,
staffel.map((k) => k.versatz).join(" "));
console.log("\n===== 3f. ZEIGERLAMPE, PRISMA UND PARTIKEL =====");
await bereichAufbauen(seite, "anfragen");
await seite.mouse.move(300, 300, { steps: 8 });
await seite.waitForTimeout(300);
await seite.mouse.move(1050, 380, { steps: 20 });
await seite.waitForTimeout(700);
const extra = await seite.evaluate(() => {
const raum = document.querySelector(".vw-raum");
const lampe = document.querySelector(".vw-lampe");
const flocken = document.querySelector(".vw-flocken");
const karte = document.querySelector("#vw-probe .wd-karte");
const ls = getComputedStyle(lampe);
return {
raumIsoliert: getComputedStyle(raum).isolation,
mischung: ls.mixBlendMode,
lampeAn: lampe.classList.contains("vw-lampe-an"),
mx: lampe.style.getPropertyValue("--vw-mx"),
my: lampe.style.getPropertyValue("--vw-my"),
flockenZahl: flocken ? flocken.children.length : 0,
flockeLaeuft: flocken && flocken.children[0]
? getComputedStyle(flocken.children[0]).animationName : "",
kachelKante: karte ? getComputedStyle(karte).backgroundImage : "",
};
});
/* Die Isolation ist der Kern. Ohne sie mischt sich die Lampe mit
ALLEM in ihrem Stapelkontext -- auch mit Kacheln, die eigentlich
darueber liegen. Beim Bauen lag dadurch ein gruenlicher Fleck
mitten auf einer Karte. */
pruefe("Buehne und Lampe stecken in einem isolierten Raum",
extra.raumIsoliert === "isolate", extra.raumIsoliert);
/* soft-light rechnet mit dem, was darunter liegt: dunkle Stellen
bleiben dunkel, vorhandene Lichtkanten werden verstaerkt. "screen"
oder "overlay" wuerden die Eiskanten ausbrennen lassen. */
pruefe("die Lampe beleuchtet, statt zu ueberdecken",
extra.mischung === "soft-light", extra.mischung);
pruefe("sie folgt dem Zeiger", extra.mx === "1050px" && extra.my === "380px",
extra.mx + " / " + extra.my);
/* Erst zeigen, wenn der Zeiger wirklich da war -- sonst haengt beim
Laden ein Lichtfleck in der Bildmitte, den niemand dorthin gelegt
hat. */
pruefe("und war vor der ersten Zeigerbewegung aus", extra.lampeAn === true);
pruefe("es schweben Partikel im Raum", extra.flockenZahl === 9,
extra.flockenZahl + " Stueck");
pruefe("und sie bewegen sich", extra.flockeLaeuft === "vw-schweben",
extra.flockeLaeuft);
/* Der Prisma-Schimmer faechert die Kante in Nachbartoene auf. Geprueft
wird, dass mehr als nur die Leitfarbe drinsteckt -- sonst waere es
wieder ein einfarbiger Rand. */
const toene = (extra.kachelKante.match(/rgba?\(/g) || []).length;
pruefe("die Kachelkante bricht das Licht in mehrere Toene", toene >= 6,
toene + " Farbstufen");
console.log("\n===== 4. WECHSELT DER RAUM WIRKLICH? =====");
const gesehen = [];
for (const bereich of BEREICHE) {
@@ -440,6 +503,15 @@ srv.listen(4404, async () => {
pruefe("und der Balken gleitet nicht, sondern springt",
laengste <= 0.01, ruhigExtra.balken);
const ruhigFlocken = await ruhig.evaluate(() => {
const f = document.querySelector(".vw-flocken");
return f ? getComputedStyle(f).display : "(fehlt)";
});
/* Komplett weg, nicht nur angehalten: Ein eingefrorener Punkt mitten
im Bild waere ein Fleck ohne Sinn. */
pruefe("keine Partikel bei reduzierter Bewegung",
ruhigFlocken === "none", ruhigFlocken);
pruefe("und kein Lichtreflex",
ruhigeLage.reflex === "none" && ruhigeLage.reflexSichtbar === 0,
ruhigeLage.reflex + ", Deckkraft " + ruhigeLage.reflexSichtbar);
Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.2 MiB

After

Width:  |  Height:  |  Size: 1.3 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 275 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-v39";
const CACHE_NAME = "dogfather-webdesign-v40";
/* Bausteine, die die Oberfläche zum Anzeigen braucht. Bewusst KEINE
HTML-Datei in dieser Liste. */
+173 -7
View File
@@ -151,11 +151,21 @@
steht Text NIE direkt auf dem Bild -- und das Bild muss nirgends
mehr weichen. */
.vw-buehne {
.vw-raum {
position: fixed;
inset: 0;
z-index: -2;
pointer-events: none;
overflow: hidden;
/* Hier endet die Mischung der Lampe. */
isolation: isolate;
}
.vw-buehne {
position: absolute;
inset: 0;
z-index: 0;
pointer-events: none;
background: var(--vw-motiv, url("/assets/img/cryo-wappen.webp"))
center center / cover no-repeat;
/* Kein Zoom: "cover" zeigt bei einem 16:9-Motiv auf einem
@@ -264,6 +274,113 @@
}
}
/* ---------- DIE ZEIGERLAMPE ----------
Ein weicher Lichtkegel wandert ueber die Buehne und hellt die
Kristalle dort auf, wo der Zeiger steht. Damit wird das Bild zu
einer Flaeche, die auf einen reagiert, statt nur dazuzuliegen.
Der Kniff steckt in mix-blend-mode: soft-light. Ein normaler heller
Verlauf wuerde das Bild ueberdecken und milchig machen. "soft-light"
rechnet stattdessen mit dem, was darunter liegt -- dunkle Stellen
bleiben dunkel, vorhandene Lichtkanten der Kristalle werden
verstaerkt. Das Bild wird nicht ueberstrahlt, es wird beleuchtet.
Bewusst NICHT "screen" oder "overlay": Beide reissen die hellen
Stellen zu und lassen die Eiskanten ausbrennen. Genau die machen
aber den Reiz der Motive aus. */
.vw-lampe {
position: absolute;
inset: 0;
z-index: 1;
pointer-events: none;
mix-blend-mode: soft-light;
/* Gross und weich, nicht klein und hell.
Die Kacheln tragen eine Rueckseiten-Unschaerfe -- sie nehmen also
auf, was hinter ihnen liegt, und das ist unter anderem diese
Lampe. Das ist genau richtig so: Licht, das durch Milchglas
scheint. Bei einem engen Kegel war davon aber ein scharf
umrissener Fleck mitten auf der Karte uebrig.
Deshalb ein weiter Radius mit vielen flachen Stufen: Der
Helligkeitsunterschied verteilt sich ueber die halbe Kachel und
liest sich als Schimmer statt als Kreis. */
background: radial-gradient(
1100px circle at var(--vw-mx, 50%) var(--vw-my, 40%),
rgb(var(--vw-farbe) / .34) 0%,
rgb(var(--vw-farbe) / .26) 22%,
rgb(var(--vw-farbe) / .16) 42%,
rgb(255 255 255 / .06) 62%,
rgb(var(--vw-farbe) / .04) 80%,
transparent 100%);
opacity: 0;
transition: opacity .5s ease;
}
/* Erst zeigen, wenn der Zeiger wirklich da war -- sonst haengt beim
Laden ein Lichtfleck in der Bildmitte, den niemand dorthin gelegt
hat. */
.vw-lampe.vw-lampe-an { opacity: 1; }
@media (prefers-reduced-motion: reduce) {
.vw-lampe { transition: none; }
}
/* Ohne feinen Zeiger gibt es kein Schweben -- dann ergibt eine
Zeigerlampe keinen Sinn. */
@media not all and (hover: hover) {
.vw-lampe { display: none; }
}
/* ---------- SCHWEBENDE EISPARTIKEL ----------
Wenige Lichtpunkte steigen sehr langsam auf, zwischen Buehne und
Inhalt. Rein aus CSS, ohne Zeichenflaeche -- eine Zeichenflaeche
wuerde dauerhaft Rechenzeit kosten, und das auf einer Seite, auf
der man arbeitet.
Bewusst nur neun Stueck und sehr langsam. Es soll wirken wie
Staub im Lichtkegel, nicht wie Schneefall. */
.vw-flocken {
position: fixed;
inset: 0;
z-index: -1;
pointer-events: none;
overflow: hidden;
}
.vw-flocken i {
position: absolute;
display: block;
width: 3px; height: 3px;
border-radius: 50%;
background: rgb(var(--vw-farbe) / .75);
box-shadow: 0 0 10px 1px rgb(var(--vw-farbe) / .45);
opacity: 0;
animation: vw-schweben linear infinite;
}
@keyframes vw-schweben {
0% { transform: translateY(0) translateX(0); opacity: 0; }
12% { opacity: .85; }
88% { opacity: .85; }
100% { transform: translateY(-118vh) translateX(28px); opacity: 0; }
}
/* Jede Flocke eigene Bahn, Dauer und Startzeit -- sonst sieht man
sofort, dass es immer dieselbe Bewegung ist. */
.vw-flocken i:nth-child(1) { left: 6%; bottom: -6px; animation-duration: 34s; animation-delay: -2s; }
.vw-flocken i:nth-child(2) { left: 17%; bottom: -6px; animation-duration: 47s; animation-delay: -9s; width: 2px; height: 2px; }
.vw-flocken i:nth-child(3) { left: 29%; bottom: -6px; animation-duration: 39s; animation-delay: -21s; }
.vw-flocken i:nth-child(4) { left: 41%; bottom: -6px; animation-duration: 52s; animation-delay: -14s; width: 2px; height: 2px; }
.vw-flocken i:nth-child(5) { left: 55%; bottom: -6px; animation-duration: 36s; animation-delay: -30s; }
.vw-flocken i:nth-child(6) { left: 67%; bottom: -6px; animation-duration: 44s; animation-delay: -5s; width: 4px; height: 4px; }
.vw-flocken i:nth-child(7) { left: 78%; bottom: -6px; animation-duration: 41s; animation-delay: -25s; }
.vw-flocken i:nth-child(8) { left: 88%; bottom: -6px; animation-duration: 49s; animation-delay: -17s; width: 2px; height: 2px; }
.vw-flocken i:nth-child(9) { left: 96%; bottom: -6px; animation-duration: 37s; animation-delay: -11s; }
@media (prefers-reduced-motion: reduce) {
/* Komplett weg, nicht nur angehalten: Ein eingefrorener Punkt
mitten im Bild waere ein Fleck ohne Sinn. */
.vw-flocken { display: none; }
}
/* ---------- GLAS ----------
Alles, worauf gelesen wird, bekommt eine Rueckseiten-Unschaerfe.
@@ -400,12 +517,29 @@
/* Die Kante: hier steckt der Zeiger drin. Der Verlauf sitzt an
der Mausposition, dadurch wandert der helle Punkt am Rand
entlang mit. */
/* PRISMA-SCHIMMER.
Am hellsten Punkt sitzt die Leitfarbe, daneben faechert die
Kante in Nachbartoene auf -- wie Licht, das sich in einer
Glaskante bricht.
Bewusst KEIN Regenbogen: Volle Spektralfarben sehen nach
Seifenblase aus, nicht nach geschliffenem Eis. Deshalb bleibt
es innerhalb der kalten Haelfte der Palette (Aqua, Ion,
Violet), und die Nachbartoene liegen deutlich unter der
Leitfarbe. */
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;
rgb(255 255 255 / .55) 0%,
rgb(var(--vw-farbe) / .95) 9%,
rgb(var(--wd-aqua-rgb) / .42) 24%,
rgb(var(--wd-ion-rgb) / .30) 40%,
rgb(var(--wd-lila-rgb) / .22) 62%,
rgb(var(--vw-farbe) / .12) 84%,
rgb(var(--wd-lila-rgb) / .14) 100%) border-box;
/* 1,5 px statt 1 px: Bei genau einem Punkt verschluckt das
Bildschirmraster die Farbaufaecherung fast vollstaendig -- man
sieht dann nur noch die Leitfarbe. */
border: 1.5px solid transparent;
box-shadow:
0 24px 60px -30px rgb(0 0 0 / .85),
inset 0 1px 0 rgb(var(--vw-farbe) / .18);
@@ -1609,7 +1743,27 @@
<!-- Rein dekorativ, deshalb aria-hidden: Es gibt hier nichts
vorzulesen, und ein leeres Element im Vorlesefluss ist nur
eine Stolperstelle. -->
<div class="vw-buehne" aria-hidden="true"></div>
<!-- Buehne und Zeigerlampe stecken zusammen in einem eigenen Raum.
Das ist keine Kosmetik, sondern noetig: Die Lampe arbeitet mit
mix-blend-mode und mischt sich sonst mit ALLEM, was in ihrem
Stapelkontext liegt -- auch mit den Kacheln, die eigentlich
darueber liegen. Beim Bauen lag dadurch ein gruenlicher Fleck
mitten auf einer Karte.
Der Raum traegt isolation: isolate. Damit endet die Mischung an
seiner Grenze, und die Lampe beleuchtet nur noch das Bild.
Rein dekorativ, deshalb aria-hidden: Es gibt nichts
vorzulesen, und leere Elemente im Vorlesefluss sind nur
Stolperstellen. -->
<div class="vw-raum" aria-hidden="true">
<div class="vw-buehne"></div>
<div class="vw-lampe"></div>
</div>
<div class="vw-flocken" aria-hidden="true"
><i></i><i></i><i></i><i></i><i></i><i></i><i></i><i></i><i></i
></div>
<div class="wd-container">
@@ -3660,6 +3814,8 @@
var buehne = document.querySelector(".vw-buehne");
if (!buehne) return;
var lampe = document.querySelector(".vw-lampe");
var lampeAn = false;
var offen = false, letzteX = 0, letzteY = 0;
document.addEventListener("pointermove", function (e) {
@@ -3674,6 +3830,16 @@
var py = (letzteY / window.innerHeight) - 0.5;
buehne.style.setProperty("--vw-px", px.toFixed(3));
buehne.style.setProperty("--vw-py", py.toFixed(3));
/* Die Zeigerlampe braucht echte Bildpunkte, nicht den
Anteilswert: Ein radialer Verlauf mit Prozentangaben
verschiebt sich mit der Elementgroesse und laege dann nicht
mehr unter dem Zeiger. */
if (lampe) {
lampe.style.setProperty("--vw-mx", letzteX + "px");
lampe.style.setProperty("--vw-my", letzteY + "px");
if (!lampeAn) { lampe.classList.add("vw-lampe-an"); lampeAn = true; }
}
});
}, { passive: true });
})();