Filipe: "der hintergrund soll genau so krass und speziell sein, benutz
den hasen und husky."
Gefunden in assets/img: hero-husky.png (Casper, neonblau auf dunkel --
farblich genau die Oberflaeche) und char-hasidog.jpg (HasiDog). Der
Sticker "HasiDog & Casper" hat bestaetigt, dass die beiden zusammen
gehoeren.
Beide sitzen FEST an der Scheibe in den unteren Ecken -- sie scrollen
nicht mit, sondern stehen da wie ein Buehnenbild, vor dem gearbeitet
wird. Die Mitte bleibt frei, dort steht der Inhalt. HasiDog ist weiter
nach aussen geschoben und schwaecher als Casper: Er ist heller, sein
Gesicht zieht den Blick staerker, und direkt neben einer Kachel wuerde
man ihn ansehen statt der Kachel.
AUFBEREITET STATT EINGEBUNDEN (tools/buehne-bauen.mjs):
* stark abgedunkelt und leicht entsaettigt -- Stimmung, kein Motiv
* weiche Raender ins Bild EINGERECHNET. Ein hartes Rechteck saehe nach
aufgeklebtem Foto aus; und eine Maske ueber ein 900-Pixel-Bild kostet
bei jedem Bildaufbau Rechenzeit.
* 290 KB PNG -> 41 KB WebP, 110 KB -> 32 KB
Gerechnet mit dem Browser, der ohnehin fuer die Pruefungen da ist -- kein
Bildprogramm, keine neue Abhaengigkeit, 0 EUR.
GEPRUEFT WIRD NICHT, OB ES HUEBSCH IST, sondern ob der Text noch lesbar
ist -- an echten BILDPUNKTEN aus dem fertigen Bildschirmfoto, nicht an
der Farbangabe im Stil. Die weiss naemlich nichts davon, was
dahinterliegt. Gemessen wird der Untergrund direkt NEBEN jedem sichtbaren
Text, auf fuenf Seiten, auf Computer und Handy: bis zu 34 Stellen je
Seite gegen den WCAG-Massstab (4,5:1, bei grosser Schrift 3:1).
UND DAS HAT SOFORT EINEN ALTEN FEHLER GEFUNDEN: Der leiseste Grauton
(--text-still) lag bei 4,43:1 -- knapp UNTER der Norm. Das war schon
lange so, nur hatte es nie jemand nachgerechnet, weil bisher niemand an
echten Bildpunkten gemessen hat. Von #6d7d92 auf #75859a angehoben; jetzt
4,95:1 auf dem hellsten Untergrund. Das gilt fuer JEDE Seite, nicht nur
fuer die mit dem Hintergrundbild.
Schlechtester Wert jetzt: 4,97:1. Alle 30 Messungen bestanden.
Nebenbei eine Falle in den Pruefungen selbst: Ein haengengebliebener
Testserver auf demselben Port fing die Anfragen ab -- der Test sprach mit
einem ALTEN Prozess und dessen anderer Datenbank, und meldete "no such
table". Der Port ist jetzt ein anderer; die Lehre steht hier, weil das
Bild "Server laeuft" trotzdem erscheint und alles richtig aussieht.
Co-Authored-By: Claude Opus 5 <[email protected]>
127 lines
4.9 KiB
JavaScript
127 lines
4.9 KiB
JavaScript
/* ===================================================================
|
|
Baut die beiden Hintergrundbilder für den Workspace.
|
|
|
|
Filipe: "der hintergrund soll genau so krass und speziell sein,
|
|
benutz den hasen und husky."
|
|
|
|
Warum die Bilder AUFBEREITET werden, statt sie einfach einzubinden:
|
|
|
|
1. Ein Foto hinter einem Arbeitswerkzeug macht Text unlesbar. Die
|
|
Bilder werden deshalb stark abgedunkelt und leicht entsättigt --
|
|
sie sollen eine Stimmung sein, kein Motiv.
|
|
|
|
2. Sie bekommen einen weichen Rand. Ein hartes Rechteck sieht nach
|
|
aufgeklebtem Bild aus; ein auslaufender Rand sieht nach Bühne aus.
|
|
Der Verlauf wird HIER eingerechnet und nicht im Browser: Eine
|
|
Maske über ein 900-Pixel-Bild kostet bei jedem Bildaufbau Rechenzeit.
|
|
|
|
3. Aus 290 KB PNG werden rund 40 KB WebP. Die Seite laedt auf jedem
|
|
Aufruf schneller, und das Bild liegt im Zwischenspeicher.
|
|
|
|
Gerechnet wird mit dem Browser, der ohnehin fuer die Pruefungen da ist
|
|
-- kein zusaetzliches Bildprogramm, keine neue Abhaengigkeit, 0 EUR.
|
|
|
|
Aufrufen mit: node tools/buehne-bauen.mjs
|
|
=================================================================== */
|
|
|
|
import { readFileSync, writeFileSync, existsSync, mkdirSync } from "node:fs";
|
|
import { join, dirname } from "node:path";
|
|
import { fileURLToPath } from "node:url";
|
|
|
|
const WURZEL = join(dirname(fileURLToPath(import.meta.url)), "..");
|
|
const QUELLE = join(WURZEL, "assets", "img");
|
|
const ZIEL = join(WURZEL, "workspace", "assets", "img");
|
|
if (!existsSync(ZIEL)) mkdirSync(ZIEL, { recursive: true });
|
|
|
|
const AUFTRAEGE = [
|
|
{
|
|
/* Casper, der Husky. Neonblau auf Dunkel -- er passt farblich genau
|
|
zur Oberfläche, anders als die warmen Fotomontagen. Sitzt unten
|
|
links und schaut nach rechts in die Seite hinein. */
|
|
quelle: "hero-husky.png",
|
|
ziel: "buehne-husky.webp",
|
|
breite: 900, hoehe: 900,
|
|
ausschnitt: [0, 0, 1, 1],
|
|
spiegeln: false,
|
|
/* Weich ausblenden nach rechts und nach oben -- dorthin, wo der
|
|
Inhalt steht. */
|
|
verlauf: [
|
|
{ art: "linear", von: [0.55, 0], nach: [1, 0], stopps: [[0, 0], [1, 1]] },
|
|
{ art: "linear", von: [0, 0], nach: [0, 0.55], stopps: [[0, 1], [1, 0]] },
|
|
],
|
|
helligkeit: 0.62, saettigung: 1.0,
|
|
},
|
|
{
|
|
/* HasiDog. Das Ganzkoerperbild wird auf Kopf und Oberkoerper
|
|
beschnitten -- die Fuesse traegt am unteren Bildrand niemand. */
|
|
quelle: "char-hasidog.jpg",
|
|
ziel: "buehne-hasi.webp",
|
|
breite: 760, hoehe: 900,
|
|
ausschnitt: [0.02, 0.02, 0.96, 0.78],
|
|
spiegeln: true,
|
|
verlauf: [
|
|
{ art: "linear", von: [0.45, 0], nach: [0, 0], stopps: [[0, 0], [1, 1]] },
|
|
{ art: "linear", von: [0, 0], nach: [0, 0.5], stopps: [[0, 1], [1, 0]] },
|
|
],
|
|
helligkeit: 0.5, saettigung: 0.9,
|
|
},
|
|
];
|
|
|
|
const { chromium } = await import(
|
|
"file:///C:/Users/qciga/Documents/Obelix/Analyse/node_modules/playwright/index.mjs");
|
|
const browser = await chromium.launch();
|
|
const seite = await browser.newPage();
|
|
await seite.setContent("<html><body></body></html>");
|
|
|
|
for (const a of AUFTRAEGE) {
|
|
const roh = readFileSync(join(QUELLE, a.quelle));
|
|
const art = a.quelle.endsWith(".png") ? "image/png" : "image/jpeg";
|
|
const daten = `data:${art};base64,${roh.toString("base64")}`;
|
|
|
|
const webp = await seite.evaluate(async ({ daten, a }) => {
|
|
const bild = new Image();
|
|
bild.src = daten;
|
|
await bild.decode();
|
|
|
|
const c = document.createElement("canvas");
|
|
c.width = a.breite; c.height = a.hoehe;
|
|
const g = c.getContext("2d");
|
|
|
|
const [ax, ay, aw, ah] = a.ausschnitt;
|
|
g.filter = `brightness(${a.helligkeit}) saturate(${a.saettigung}) contrast(1.04)`;
|
|
if (a.spiegeln) { g.translate(a.breite, 0); g.scale(-1, 1); }
|
|
g.drawImage(bild,
|
|
bild.naturalWidth * ax, bild.naturalHeight * ay,
|
|
bild.naturalWidth * aw, bild.naturalHeight * ah,
|
|
0, 0, a.breite, a.hoehe);
|
|
g.setTransform(1, 0, 0, 1, 0, 0);
|
|
g.filter = "none";
|
|
|
|
/* Die weichen Raender. destination-out radiert dort, wo der Verlauf
|
|
deckend ist -- so bleibt das Bild in der Ecke stehen und loest
|
|
sich zur Seite hin auf. */
|
|
g.globalCompositeOperation = "destination-out";
|
|
for (const v of a.verlauf) {
|
|
const gr = g.createLinearGradient(
|
|
v.von[0] * a.breite, v.von[1] * a.hoehe,
|
|
v.nach[0] * a.breite, v.nach[1] * a.hoehe);
|
|
for (const [pos, deckung] of v.stopps) {
|
|
gr.addColorStop(pos, `rgba(0,0,0,${deckung})`);
|
|
}
|
|
g.fillStyle = gr;
|
|
g.fillRect(0, 0, a.breite, a.hoehe);
|
|
}
|
|
g.globalCompositeOperation = "source-over";
|
|
|
|
return c.toDataURL("image/webp", 0.74);
|
|
}, { daten, a });
|
|
|
|
const puffer = Buffer.from(webp.split(",")[1], "base64");
|
|
writeFileSync(join(ZIEL, a.ziel), puffer);
|
|
console.log(`${a.ziel.padEnd(20)} ${String(Math.round(roh.length / 1024)).padStart(4)} KB`
|
|
+ ` -> ${String(Math.round(puffer.length / 1024)).padStart(3)} KB`);
|
|
}
|
|
|
|
await browser.close();
|
|
console.log("\nFertig. Die Dateien liegen in workspace/assets/img/.");
|