Bilder bewegen sich unter dem Zeiger

Faehrt der Zeiger ueber ein Bild, tritt es leicht naeher und wandert ein
Stueck GEGEN die Zeigerrichtung -- als schaue man durch ein Fenster und
lehne sich zur Seite.

Warum gegen die Richtung: Bewegt sich das Bild MIT dem Zeiger, wirkt es
wie ein Aufkleber, der verrutscht. Nur die Gegenbewegung liest das Auge
als Tiefe hinter dem Rahmen. Derselbe Grund wie bei der Buehne im
Verwaltungsbereich, eine Ebene kleiner.

Der Effekt liegt im GRUNDSYSTEM, nicht in einer einzelnen Seite. Er gilt
damit ueberall: Portfolio, Startseite, "Ueber mich", Zugangswand und die
Vorschaubilder im Verwaltungsbereich. Eine Sonderloesung je Seite waere
beim naechsten neuen Bild wieder vergessen worden.

Zwei Zahlen, die bewusst klein sind:

- Der Ausschlag liegt bei hoechstens 14 Punkten (gemessen 13,9).
- Der Zoom bei 1,055.

Zusammen ergibt das Bewegung, ohne dass ein Bild beim blossen
Vorbeifahren seinen Ausschnitt merklich aendert. Ein groesserer Wert
waere kein Effekt mehr, sondern ein Bildsprung.

Umgerechnet wird auf die Groesse des jeweiligen Bildes (-1 bis +1), nicht
in festen Bildpunkten. Sonst wanderten ein Vorschaubild von 1200 Punkten
Breite und ein Logo von 80 gleich weit -- beim kleinen saehe das aus wie
ein Ruck.

Zwei Faelle, die im Code ausdruecklich abgefangen sind:

- Der Zeiger liegt ueber einem Element, das das Bild UEBERDECKT (etwa
  einem Textblock in derselben Kachel). Ohne Behandlung bliebe das Bild
  stehen, sobald man den Rahmen verlaesst, aber die Kachel noch nicht.
- Der Zeiger steht neben dem Bild, aber noch in der Kachel. Die Werte
  liefen dann weit ueber 1 hinaus und das Bild schoesse aus dem Rahmen.
  Deshalb wird auf -1 bis +1 begrenzt.

Beim Verlassen stellt sich alles zurueck -- sonst bliebe das Bild
verschoben stehen, nachdem der Zeiger laengst weg ist.

Bei prefers-reduced-motion ist der Effekt aus.

Geprueft: 265 Pruefungen gruen (Portfolio 20, Verwaltung 62, CRYONOVA 53,
Blickfang 13, System 5, Abmelden 16, Abbruch 42, Angebot 54). Der Test
prueft ausdruecklich die RICHTUNG der Bewegung, nicht nur, dass sich
ueberhaupt etwas tut.

Co-Authored-By: Claude Opus 5 <[email protected]>
This commit is contained in:
2026-08-25 09:48:17 +02:00
co-authored by Claude Opus 5
parent ae8c498672
commit 76aea6ac3f
10 changed files with 175 additions and 2 deletions
+56
View File
@@ -624,6 +624,62 @@ a.wd-karte:hover, .wd-karte--klickbar:hover {
.wd-tag--indigo { background: rgb(var(--wd-indigo-rgb) / .16); border-color: rgb(var(--wd-indigo-rgb) / .40); color: rgb(var(--wd-chrome-rgb)); }
.wd-tags { display: flex; flex-wrap: wrap; gap: .45rem; }
/* =====================================================================
BILDER, DIE SICH BEWEGEN
Faehrt der Zeiger ueber ein Bild, tritt es leicht naeher und wandert
ein Stueck gegen die Zeigerrichtung -- als schaue man durch ein
Fenster und lehne sich zur Seite.
Warum GEGEN die Richtung: Bewegt sich das Bild mit dem Zeiger, wirkt
es wie ein Aufkleber, der verrutscht. Bewegt es sich dagegen, liest
das Auge es als Tiefe hinter dem Rahmen. Derselbe Grund wie bei der
Buehne im Verwaltungsbereich.
Der Ausschlag ist klein (hoechstens 14 Punkte) und der Zoom knapp
ueber eins. Beides zusammen ergibt Bewegung, ohne dass ein Bild beim
Ueberfahren seinen Ausschnitt merklich aendert -- sonst waere jedes
Vorbeifahren mit der Maus ein Bildsprung.
===================================================================== */
@media (hover: hover) and (pointer: fine) {
/* Der Rahmen muss den Ueberstand abschneiden, sonst schiebt sich das
vergroesserte Bild ueber die Kachelkante hinaus. */
.wd-bildrahmen,
.pfo-projekt,
.wd-karte:has(> img:first-child) {
overflow: hidden;
}
.pfo-bild,
.wd-projekt-bild,
.wd-bildrahmen > img,
.wd-karte > img {
transform:
scale(var(--wd-bildzoom, 1))
translate3d(calc(var(--wd-bx, 0) * -14px), calc(var(--wd-by, 0) * -10px), 0);
transition: transform .55s cubic-bezier(.2,.75,.25,1);
will-change: transform;
}
/* Der Zoom kommt erst beim Ueberfahren dazu. Als Dauerzustand waere
jedes Bild permanent leicht beschnitten. */
.pfo-projekt:hover .pfo-bild,
.wd-bildrahmen:hover > img,
.wd-karte:hover > img,
.pfo-bild:hover,
.wd-projekt-bild:hover {
--wd-bildzoom: 1.055;
}
}
@media (prefers-reduced-motion: reduce) {
.pfo-bild, .wd-projekt-bild, .wd-bildrahmen > img, .wd-karte > img {
transform: none;
transition: none;
}
}
/* ---------------------------------------------------------------------
7. PREISKARTEN (Masterplan S.8)
Preis, Dauer und Personalisierungshinweis gehören sichtbar zusammen —
+66
View File
@@ -946,6 +946,72 @@
k.style.setProperty("--wd-lichty", (letzte.y - r.top) + "px");
});
}, { passive: true });
bilderBewegen();
}
/* ===================================================================
BILDER, DIE SICH BEWEGEN
Faehrt der Zeiger ueber ein Bild, wandert es ein Stueck GEGEN die
Zeigerrichtung. Bewegte es sich mit, wirkte es wie ein Aufkleber,
der verrutscht; dagegen liest das Auge es als Tiefe hinter dem
Rahmen.
Gesetzt werden Werte von -1 bis +1, umgerechnet auf die Groesse des
Bildes. Ein fester Wert in Bildpunkten waere falsch: Ein
Vorschaubild von 1200 Punkten Breite und ein Kundenlogo von 80
wuerden sonst gleich weit wandern, was beim kleinen wie ein Ruck
aussieht.
=================================================================== */
function bilderBewegen() {
var WAHL = ".pfo-bild, .wd-projekt-bild, .wd-bildrahmen > img, .wd-karte > img";
var offen = false, letztes = null, zx = 0, zy = 0;
document.addEventListener("pointermove", function (e) {
var bild = null;
if (e.target && e.target.matches && e.target.matches(WAHL)) {
bild = e.target;
} else if (e.target && e.target.closest) {
/* Auch dann greifen, wenn der Zeiger ueber einem Element LIEGT,
das das Bild ueberdeckt -- etwa einem Textblock in derselben
Kachel. Sonst bliebe das Bild stehen, sobald man den Rahmen
verlaesst, aber die Kachel noch nicht. */
var rahmen = e.target.closest(".wd-bildrahmen, .pfo-projekt, .wd-karte");
if (rahmen) bild = rahmen.querySelector(WAHL);
}
if (!bild) return;
letztes = bild; zx = e.clientX; zy = e.clientY;
if (offen) return;
offen = true;
requestAnimationFrame(function () {
offen = false;
if (!letztes) return;
var r = letztes.getBoundingClientRect();
if (!r.width || !r.height) return;
var bx = ((zx - r.left) / r.width - 0.5) * 2;
var by = ((zy - r.top) / r.height - 0.5) * 2;
/* Auf -1..+1 begrenzen: Steht der Zeiger neben dem Bild, aber
noch in der Kachel, kaemen sonst Werte weit darueber heraus
und das Bild schoesse aus dem Rahmen. */
letztes.style.setProperty("--wd-bx", Math.max(-1, Math.min(1, bx)).toFixed(3));
letztes.style.setProperty("--wd-by", Math.max(-1, Math.min(1, by)).toFixed(3));
});
}, { passive: true });
/* Beim Verlassen zuruecksetzen, sonst bleibt das Bild verschoben
stehen, nachdem der Zeiger laengst weg ist. */
document.addEventListener("pointerout", function (e) {
if (!e.target || !e.target.closest) return;
var rahmen = e.target.closest(".wd-bildrahmen, .pfo-projekt, .wd-karte");
if (!rahmen) return;
if (e.relatedTarget && rahmen.contains(e.relatedTarget)) return;
var bild = rahmen.querySelector(WAHL);
if (!bild) return;
bild.style.setProperty("--wd-bx", "0");
bild.style.setProperty("--wd-by", "0");
if (letztes === bild) letztes = null;
}, { passive: true });
}
/* Nach außen sichtbare Schnittstelle für seiteneigene Skripte. */
Binary file not shown.

Before

Width:  |  Height:  |  Size: 302 KiB

After

Width:  |  Height:  |  Size: 302 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 298 KiB

After

Width:  |  Height:  |  Size: 298 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.3 MiB

After

Width:  |  Height:  |  Size: 4.1 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 578 KiB

After

Width:  |  Height:  |  Size: 578 KiB

+52 -1
View File
@@ -189,7 +189,58 @@ srv.listen(4403, async () => {
pruefe(`Schild "${d.text}" ist gut lesbar (AA = 4,5:1)`, v >= 4.5, v.toFixed(2) + ":1");
}
console.log("\n5. KEINE FEHLER, KEINE FEHLENDEN DATEIEN");
console.log("\n5. BEWEGEN SICH DIE BILDER?");
const bild = seite.locator(".pfo-bild").first();
await bild.scrollIntoViewIfNeeded();
await seite.waitForTimeout(800);
const rahmen = await bild.boundingBox();
/* Mit steps bewegen, sonst entsteht kein pointermove. */
await seite.mouse.move(rahmen.x + 30, rahmen.y + 30, { steps: 10 });
await seite.waitForTimeout(600);
const linksOben = await bild.evaluate((e) => ({
bx: parseFloat(e.style.getPropertyValue("--wd-bx")),
versatz: getComputedStyle(e).transform,
}));
await seite.mouse.move(rahmen.x + rahmen.width - 30,
rahmen.y + rahmen.height - 30, { steps: 18 });
await seite.waitForTimeout(650);
const rechtsUnten = await bild.evaluate((e) => ({
bx: parseFloat(e.style.getPropertyValue("--wd-bx")),
versatz: getComputedStyle(e).transform,
}));
pruefe("das Bild reagiert auf den Zeiger",
linksOben.bx < -0.4 && rechtsUnten.bx > 0.4,
linksOben.bx + " -> " + rechtsUnten.bx);
/* Die Richtung ist der Kern: Bewegt sich das Bild MIT dem Zeiger,
wirkt es wie ein verrutschter Aufkleber. Nur die Gegenbewegung
liest das Auge als Tiefe hinter dem Rahmen. */
const holeX = (m) => parseFloat(m.slice(m.indexOf("(") + 1, -1).split(",")[4]);
const xLinks = holeX(linksOben.versatz);
const xRechts = holeX(rechtsUnten.versatz);
pruefe("und wandert GEGEN die Zeigerrichtung",
xLinks > 0 && xRechts < 0,
xLinks.toFixed(1) + "px / " + xRechts.toFixed(1) + "px");
/* Klein halten: Ein grosser Ausschnittswechsel beim blossen
Vorbeifahren waere ein Bildsprung, kein Effekt. */
pruefe("der Ausschlag bleibt klein",
Math.abs(xLinks) <= 18 && Math.abs(xRechts) <= 18,
Math.max(Math.abs(xLinks), Math.abs(xRechts)).toFixed(1) + "px");
const zoom = parseFloat(rechtsUnten.versatz.slice(
rechtsUnten.versatz.indexOf("(") + 1, -1).split(",")[0]);
pruefe("und das Bild tritt leicht naeher", zoom > 1.01 && zoom < 1.12,
zoom.toFixed(3) + "-fach");
await seite.mouse.move(10, 10, { steps: 12 });
await seite.waitForTimeout(700);
const zurueck = await bild.evaluate((e) => e.style.getPropertyValue("--wd-bx"));
pruefe("beim Verlassen stellt es sich zurueck", zurueck === "0", zurueck);
console.log("\n6. KEINE FEHLER, KEINE FEHLENDEN DATEIEN");
pruefe("kein Skriptfehler", skriptfehler.length === 0, skriptfehler[0] || "");
const verpasst = fehlt.filter((f) => /\.(jpg|webp|png|css|js|svg)$/.test(f));
pruefe("keine fehlende Datei", verpasst.length === 0, verpasst.join(", ") || "keine");
Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.2 MiB

After

Width:  |  Height:  |  Size: 1.2 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 280 KiB

After

Width:  |  Height:  |  Size: 280 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-v42";
const CACHE_NAME = "dogfather-webdesign-v43";
/* Bausteine, die die Oberfläche zum Anzeigen braucht. Bewusst KEINE
HTML-Datei in dieser Liste. */