diff --git a/assets/css/webdesign.css b/assets/css/webdesign.css index ea663488..fa3030c1 100644 --- a/assets/css/webdesign.css +++ b/assets/css/webdesign.css @@ -624,6 +624,97 @@ 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; } +/* ===================================================================== + KACHELN ALS GESCHLIFFENE GLASPLATTEN + + Was zuerst nur im Verwaltungsbereich stand, gilt jetzt fuer jede + Kachel auf jeder Seite. Eine Sonderloesung fuer einen Bereich waere + beim naechsten neuen Abschnitt wieder vergessen worden -- und die + Seite saehe an zwei Stellen unterschiedlich aus, ohne dass es einen + Grund dafuer gaebe. + + Die Farbe kommt aus --wd-lumen. Jede Kachel bringt die schon mit + (Token-Regel 02: Kachelfarbe steuert Lumenfarbe), der + Verwaltungsbereich ueberschreibt sie mit der Farbe des Bereichs. + Dadurch braucht es hier keine einzige Sonderregel. + ===================================================================== */ +@media (hover: hover) and (pointer: fine) { + + /* Die Perspektive steckt in der Kachel selbst, NICHT an einem + Elternteil. + + Der naheliegende Weg waere "perspective" auf dem Abschnitt -- dann + teilen benachbarte Kacheln einen gemeinsamen Fluchtpunkt. Genau das + hatte aber eine boese Nebenwirkung: "perspective" macht ein Element + zum Bezugsrahmen fuer position:fixed in seinem Inneren, exakt wie + "transform" oder "filter". Die bildschirmfuellende Buehne im + Verwaltungsbereich lag danach nicht mehr am Fenster, sondern am + Abschnitt -- gemessen 472 statt 900 Punkte Hoehe. + + Als Funktion im transform gilt die Perspektive nur fuer diese eine + Kachel. Der gemeinsame Fluchtpunkt entfaellt damit, was bei + hoechstens fuenf Grad Neigung niemand sieht. */ + .wd-karte { + transform-style: preserve-3d; + transform: + perspective(1400px) + rotateX(calc(var(--wd-ny, 0) * -5deg)) + rotateY(calc(var(--wd-nx, 0) * 5deg)); + transition: + transform .32s cubic-bezier(.2,.8,.3,1), + box-shadow .32s ease; + } + /* Beim Ueberfahren kommt die Kachel dem Betrachter entgegen und wirft + einen laengeren Schatten. Erst das macht aus der Neigung ein Objekt + im Raum statt eines schraegen Bildes. */ + .wd-karte:hover { + transform: + perspective(1400px) + rotateX(calc(var(--wd-ny, 0) * -5deg)) + rotateY(calc(var(--wd-nx, 0) * 5deg)) + translateZ(20px); + box-shadow: + 0 40px 84px -34px rgb(0 0 0 / .92), + 0 0 52px -18px rgb(var(--wd-lumen) / .38), + inset 0 1px 0 rgb(var(--wd-lumen) / .26); + } + + /* Ueberschriften stehen weiter vorn als Fliesstext. Dadurch entsteht + beim Neigen echte Staffelung statt einer flachen Ebene, die sich + mitdreht -- und der Text bleibt fast senkrecht zum Auge, also + scharf, obwohl die Flaeche unter ihm schraeg liegt. */ + .wd-karte > * { transform: translateZ(14px); } + .wd-karte h2, .wd-karte h3 { transform: translateZ(28px); } + + + /* Ein schmales Licht, das ueber die Platte laeuft und mit der Neigung + wandert. */ + .wd-karte::before { + content: ""; + position: absolute; + inset: 0; + z-index: 0; + border-radius: inherit; + pointer-events: none; + background: linear-gradient( + calc(112deg + var(--wd-nx, 0) * 26deg), + transparent 34%, + rgb(255 255 255 / .06) 46%, + rgb(var(--wd-lumen) / .14) 50%, + rgb(255 255 255 / .06) 54%, + transparent 66%); + opacity: 0; + transition: opacity .3s ease; + } + .wd-karte:hover::before { opacity: 1; } +} + +@media (prefers-reduced-motion: reduce) { + .wd-karte, .wd-karte:hover { transform: none; transition: none; } + .wd-karte > *, .wd-karte h2, .wd-karte h3 { transform: none; } + .wd-karte::before { display: none; } +} + /* ===================================================================== BILDER, DIE SICH BEWEGEN @@ -671,6 +762,23 @@ a.wd-karte:hover, .wd-karte--klickbar:hover { .wd-projekt-bild:hover { --wd-bildzoom: 1.055; } + + /* AUSNAHME: Bilder INNERHALB einer Kachel bekommen nur den Zoom. + + Die Kachel kippt bereits, das Bild bewegt sich also ohnehin mit. + Beides zusammen schaukelt sich auf: Die kippende Kachel schiebt das + Bild unter dem Zeiger weg, der landet auf einem Nachbarelement, das + Bild springt zurueck auf null -- und beim naechsten Zucken von + vorne. Beim Bauen sass der Wert an einer Ecke deshalb dauerhaft + auf 0, waehrend er an der anderen sauber lief. + + Diese Regel steht bewusst ganz am Ende der Bildregeln: Sie hat + dieselbe Gewichtung wie ".pfo-bild" und gewinnt nur, weil sie + spaeter kommt. Wird sie nach oben verschoben, ist der Aufschaukel- + Effekt sofort zurueck. */ + .wd-karte img { + transform: scale(var(--wd-bildzoom, 1)); + } } @media (prefers-reduced-motion: reduce) { @@ -925,12 +1033,24 @@ a.wd-karte:hover, .wd-karte--klickbar:hover { Skriptfehler bleibt dadurch ALLES lesbar — der häufigste schwere Fehler bei Scroll-Animationen ist eine dauerhaft unsichtbare Seite. --------------------------------------------------------------------- */ +/* Die Einblendung nutzt "translate" als EIGENE Eigenschaft, nicht + "transform". + + Grund: Kacheln neigen sich beim Ueberfahren, und das laeuft ueber + "transform". Diese Regel hier hat mit drei Klassen die hoehere + Spezifitaet -- ihr "transform: none" hat die Neigung stillschweigend + geloescht. Sichtbar war das nur auf Seiten, deren Kacheln ein + .wd-auf tragen: Auf "ablauf" kippte die Kachel, auf "leistungen" + nicht, und der Wert kam in beiden Faellen korrekt an. + + "translate" wird VOR "transform" angewendet. Beide koennen sich so + nicht mehr in die Quere kommen. */ .wd-bereit .wd-auf { opacity: 0; - transform: translateY(18px); - transition: opacity var(--wd-zeit-lang) ease, transform var(--wd-zeit-lang) cubic-bezier(.22, 1, .36, 1); + translate: 0 18px; + transition: opacity var(--wd-zeit-lang) ease, translate var(--wd-zeit-lang) cubic-bezier(.22, 1, .36, 1); } -.wd-bereit .wd-auf.wd-sichtbar { opacity: 1; transform: none; } +.wd-bereit .wd-auf.wd-sichtbar { opacity: 1; translate: none; } /* --------------------------------------------------------------------- 14. KOPF- UND FUSSZEILE des Webdesign-Bereichs @@ -1162,7 +1282,7 @@ a.wd-karte:hover, .wd-karte--klickbar:hover { } /* Wichtig: die Einblend-Elemente müssen dann SOFORT sichtbar sein, nicht "unsichtbar mit 0s Übergang". */ - .wd-bereit .wd-auf { opacity: 1; transform: none; } + .wd-bereit .wd-auf { opacity: 1; translate: none; transform: none; } .wd-btn:hover, a.wd-karte:hover, .wd-karte--klickbar:hover { transform: none; } } diff --git a/assets/js/wd-core.js b/assets/js/wd-core.js index ddffb47c..55d0522b 100644 --- a/assets/js/wd-core.js +++ b/assets/js/wd-core.js @@ -944,9 +944,43 @@ var r = k.getBoundingClientRect(); k.style.setProperty("--wd-lichtx", (letzte.x - r.left) + "px"); k.style.setProperty("--wd-lichty", (letzte.y - r.top) + "px"); + + /* Und gleich die Neigung mitsetzen. + + Das Licht braucht Bildpunkte ab der Kachelecke, die Neigung + dagegen einen Wert unabhaengig von der Kachelgroesse: -1 am + linken Rand, +1 am rechten. Sonst kippte eine breite Kachel + staerker als eine schmale, obwohl der Zeiger relativ an + derselben Stelle steht. + + Beides in einem Durchgang, weil beides denselben Messwert + braucht -- ein zweiter Horcher wuerde dieselbe Rechnung + nochmal anstellen. */ + if (r.width && r.height) { + var nx = ((letzte.x - r.left) / r.width - 0.5) * 2; + var ny = ((letzte.y - r.top) / r.height - 0.5) * 2; + k.style.setProperty("--wd-nx", nx.toFixed(3)); + k.style.setProperty("--wd-ny", ny.toFixed(3)); + } }); }, { passive: true }); + /* Beim Verlassen zuruecksetzen, sonst bleibt die Kachel schraeg + stehen, nachdem der Zeiger laengst weg ist. + + pointerout statt mouseleave: Es meldet zuverlaessig auch dann, + wenn der Zeiger die Kachel ueber ein Kindelement verlaesst. */ + document.addEventListener("pointerout", function (e) { + var karte = e.target && e.target.closest ? e.target.closest(".wd-karte") : null; + if (!karte) return; + /* Nur zuruecksetzen, wenn der Zeiger die Kachel WIRKLICH verlaesst + und nicht bloss von einem Kind zum naechsten wandert. */ + if (e.relatedTarget && karte.contains(e.relatedTarget)) return; + karte.style.setProperty("--wd-nx", "0"); + karte.style.setProperty("--wd-ny", "0"); + if (letzte && letzte.karte === karte) letzte = null; + }, { passive: true }); + bilderBewegen(); } diff --git a/blickfang-portfolio-handy.png b/blickfang-portfolio-handy.png index 885e5873..d036f770 100644 Binary files a/blickfang-portfolio-handy.png and b/blickfang-portfolio-handy.png differ diff --git a/blickfang-ueber-handy.png b/blickfang-ueber-handy.png index 3ed9b083..a47d229a 100644 Binary files a/blickfang-ueber-handy.png and b/blickfang-ueber-handy.png differ diff --git a/pruef-bewegung.mjs b/pruef-bewegung.mjs new file mode 100644 index 00000000..44bd17ef --- /dev/null +++ b/pruef-bewegung.mjs @@ -0,0 +1,155 @@ +/* ===================================================================== + Prueft, dass Kacheln und Bilder auf ALLEN Seiten auf den Zeiger + reagieren -- nicht nur im Verwaltungsbereich. + + Warum als eigener Durchlauf: Die Effekte liegen im Grundsystem + (webdesign.css und wd-core.js) und gelten damit fuer jede Seite. Eine + Pruefung, die nur eine Seite ansieht, wuerde nicht auffallen lassen, + wenn eine andere Seite den Effekt still ueberschreibt. Genau das ist + passiert: ".wd-bereit .wd-auf.wd-sichtbar" setzt "transform: none" + und hat mit hoeherer Spezifitaet die Neigung geloescht -- sichtbar + nur auf Seiten, deren Kacheln eine Einblendung tragen. + ===================================================================== */ +import { chromium } from "playwright"; +import http from "http"; +import fs from "fs"; +import path from "path"; + +const TYP = { + ".html": "text/html", ".css": "text/css", ".js": "text/javascript", + ".png": "image/png", ".svg": "image/svg+xml", ".webp": "image/webp", + ".json": "application/json", ".ico": "image/x-icon", ".jpg": "image/jpeg", +}; + +const srv = http.createServer((q, r) => { + const p = path.join(process.cwd(), decodeURIComponent(q.url.split("?")[0])); + if (fs.existsSync(p) && !p.endsWith("/")) { + r.writeHead(200, { "content-type": TYP[path.extname(p)] || "text/plain" }); + return r.end(fs.readFileSync(p)); + } + r.writeHead(404); + r.end(""); +}); + +let ok = 0, fehl = 0; +const pruefe = (name, gut, info) => { + if (gut) { ok++; console.log(" ok " + name + (info ? " -> " + info : "")); } + else { fehl++; console.log(" FEHL " + name + (info ? " -> " + info : "")); } +}; + +const SEITEN = ["index", "leistungen", "ablauf", "portfolio", "ueber", "portal"]; + +srv.listen(4405, async () => { + const browser = await chromium.launch(); + + console.log("1. NEIGEN SICH DIE KACHELN AUF JEDER SEITE?"); + for (const name of SEITEN) { + const seite = await browser.newPage({ viewport: { width: 1440, height: 900 } }); + const skriptfehler = []; + seite.on("pageerror", (e) => skriptfehler.push(String(e).slice(0, 110))); + await seite.goto(`http://127.0.0.1:4405/webdesign/${name}.html`); + /* Die Startseite legt beim Laden einen Vorhang ueber alles. Wer zu + frueh misst, trifft den Vorhang statt der Kachel. */ + await seite.waitForTimeout(4500); + + const karte = seite.locator(".wd-karte").first(); + if (await karte.count() === 0) { + pruefe(`${name}: Kachel vorhanden`, false, "keine .wd-karte"); + await seite.close(); + continue; + } + await karte.scrollIntoViewIfNeeded(); + await seite.waitForTimeout(700); + + /* Einen Punkt suchen, der WIRKLICH auf der Kachel liegt und im + Fenster sichtbar ist. Eine Portfolio-Kachel ist ueber 1400 Punkte + hoch -- ein fester Anteil ihrer Hoehe landet ausserhalb des + Bildschirms, und dann misst der Test das Nichts. */ + const ziel = await seite.evaluate(() => { + const k = document.querySelector(".wd-karte"); + const r = k.getBoundingClientRect(); + const x = Math.round(r.left + Math.min(r.width * 0.25, 200)); + const y = Math.round(Math.max(80, Math.min(r.top + 60, window.innerHeight - 80))); + const e = document.elementFromPoint(x, y); + return { x, y, trifft: !!(e && e.closest(".wd-karte")) }; + }); + if (!ziel.trifft) { + pruefe(`${name}: Messpunkt liegt auf der Kachel`, false, JSON.stringify(ziel)); + await seite.close(); + continue; + } + + /* Mit steps bewegen, sonst entsteht kein pointermove. */ + await seite.mouse.move(ziel.x + 80, ziel.y + 30, { steps: 8 }); + await seite.mouse.move(ziel.x, ziel.y, { steps: 14 }); + await seite.waitForTimeout(600); + + const d = await karte.evaluate((e) => ({ + nx: parseFloat(e.style.getPropertyValue("--wd-nx")), + matrix: getComputedStyle(e).transform, + lumen: getComputedStyle(e).getPropertyValue("--wd-lumen").trim(), + })); + pruefe(`${name}: die Kachel neigt sich`, + d.matrix.startsWith("matrix3d") && !Number.isNaN(d.nx), + `nx ${d.nx}`); + + /* Klein halten: Alles ueber neun Grad verzerrt die Schrift. */ + if (d.matrix.startsWith("matrix3d")) { + const zahlen = d.matrix.slice(d.matrix.indexOf("(") + 1, -1).split(",").map(Number); + const winkel = Math.abs(Math.asin(Math.max(-1, Math.min(1, zahlen[2]))) * 180 / Math.PI); + pruefe(`${name}: und bleibt flach genug`, winkel <= 9, winkel.toFixed(1) + " Grad"); + } + + /* Beim Verlassen zuruecksetzen. */ + await seite.mouse.move(5, 5, { steps: 12 }); + await seite.waitForTimeout(600); + const zurueck = await karte.evaluate((e) => e.style.getPropertyValue("--wd-nx")); + pruefe(`${name}: stellt sich beim Verlassen zurueck`, zurueck === "0", zurueck); + + pruefe(`${name}: kein Skriptfehler`, skriptfehler.length === 0, skriptfehler[0] || ""); + await seite.close(); + } + + console.log("\n2. DIE EINBLENDUNG DARF DIE NEIGUNG NICHT LOESCHEN"); + const s2 = await browser.newPage({ viewport: { width: 1440, height: 900 } }); + await s2.goto("http://127.0.0.1:4405/webdesign/leistungen.html"); + await s2.waitForTimeout(2500); + const einblendung = await s2.evaluate(() => { + const k = document.querySelector(".wd-auf.wd-sichtbar") || + document.querySelector(".wd-auf"); + if (!k) return null; + const st = getComputedStyle(k); + return { translate: st.translate, transform: st.transform }; + }); + /* Der Kern: Die Einblendung muss ueber "translate" laufen. Nutzt sie + "transform", loescht ihr hoeher gewichtetes "transform: none" die + Neigung -- und zwar still, ohne Fehlermeldung. */ + pruefe("die Einblendung nutzt translate, nicht transform", + einblendung && einblendung.translate !== undefined, + einblendung ? `translate: ${einblendung.translate}` : "kein .wd-auf gefunden"); + await s2.close(); + + console.log("\n3. BEI REDUZIERTER BEWEGUNG IST ALLES AUS"); + const ruhig = await browser.newPage({ + viewport: { width: 1440, height: 900 }, reducedMotion: "reduce" }); + await ruhig.goto("http://127.0.0.1:4405/webdesign/leistungen.html"); + await ruhig.waitForTimeout(2000); + const still = await ruhig.evaluate(() => { + const k = document.querySelector(".wd-karte"); + return { + transform: getComputedStyle(k).transform, + sichtbar: getComputedStyle(k).opacity, + }; + }); + pruefe("keine Neigung", still.transform === "none", still.transform); + /* Abschalten heisst NICHT verschwinden: Die Kachel muss sichtbar + bleiben, auch wenn die Einblendung nicht laeuft. */ + pruefe("die Kachel ist trotzdem sichtbar", parseFloat(still.sichtbar) > 0.9, + still.sichtbar); + await ruhig.close(); + + console.log(`\n===== ${ok} bestanden, ${fehl} fehlgeschlagen =====`); + await browser.close(); + srv.close(); + process.exit(fehl ? 1 : 0); +}); diff --git a/pruef-portfolio-computer.png b/pruef-portfolio-computer.png index 43089747..8bd6d9c2 100644 Binary files a/pruef-portfolio-computer.png and b/pruef-portfolio-computer.png differ diff --git a/pruef-portfolio-handy.png b/pruef-portfolio-handy.png index 799bfdc7..6b049c58 100644 Binary files a/pruef-portfolio-handy.png and b/pruef-portfolio-handy.png differ diff --git a/pruef-portfolio.mjs b/pruef-portfolio.mjs index 09ecc9a3..4229df7d 100644 --- a/pruef-portfolio.mjs +++ b/pruef-portfolio.mjs @@ -210,25 +210,26 @@ srv.listen(4403, async () => { versatz: getComputedStyle(e).transform, })); - pruefe("das Bild reagiert auf den Zeiger", - linksOben.bx < -0.4 && rechtsUnten.bx > 0.4, - linksOben.bx + " -> " + rechtsUnten.bx); + /* Die Portfolio-Bilder stecken in einer Kachel. Dort bewegt sich die + KACHEL, das Bild bekommt nur den Zoom -- beides zusammen schaukelt + sich auf: Die kippende Kachel schiebt das Bild unter dem Zeiger + weg, der landet auf einem Nachbarelement, das Bild springt zurueck + auf null. Genau das war hier messbar: an einer Ecke sauber, an der + anderen dauerhaft 0. - /* 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. */ + Geprueft wird deshalb die Arbeitsteilung, nicht mehr die + Bildverschiebung. */ 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, + pruefe("das Bild in der Kachel verschiebt sich NICHT selbst", + Math.abs(xLinks) < 1 && Math.abs(xRechts) < 1, 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 kachel = await seite.locator(".pfo-projekt").first() + .evaluate((e) => getComputedStyle(e).transform); + pruefe("dafuer kippt die Kachel um das Bild herum", + kachel.startsWith("matrix3d"), kachel.slice(0, 24) + "..."); const zoom = parseFloat(rechtsUnten.versatz.slice( rechtsUnten.versatz.indexOf("(") + 1, -1).split(",")[0]); diff --git a/pruef-vw-schmuck-computer.png b/pruef-vw-schmuck-computer.png index de3fc880..ebd291ee 100644 Binary files a/pruef-vw-schmuck-computer.png and b/pruef-vw-schmuck-computer.png differ diff --git a/pruef-vw-schmuck-handy.png b/pruef-vw-schmuck-handy.png index 6f4fcf4a..d57584b9 100644 Binary files a/pruef-vw-schmuck-handy.png and b/pruef-vw-schmuck-handy.png differ diff --git a/webdesign/sw.js b/webdesign/sw.js index 69cb8c5c..45046bb9 100644 --- a/webdesign/sw.js +++ b/webdesign/sw.js @@ -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-v43"; +const CACHE_NAME = "dogfather-webdesign-v44"; /* Bausteine, die die Oberfläche zum Anzeigen braucht. Bewusst KEINE HTML-Datei in dieser Liste. */