Files
Webklar/scripts/relaunch-bilder.py
T
rootandClaude Opus 5.5 72275db9fa Kundenportal-Bühne: Terminplaner, Heatmap und Pinnwand als Live-Demos statt „Funktionen“
Die Station „Funktionen“ ist durch drei Stationen ersetzt (Gruppe „Funktionen zum
Dazubuchen“). Auf dem MacBook läuft je eine erfundene Kunden-Website als Live-Komponente,
auf dem iPhone die Portal-Ansicht der Funktion (echter Portal-Code, Fake-Daten):

- Terminplaner (Salon Schnittstelle): Buchung im Widget, Bestätigung per Link,
  Eintrag im Kalender-Abo, Mitteilung „Neue Buchung …“ auf dem Handy
- Website-Analyse (Boulderhalle Überhang): simulierte Klicks bauen die Heatmap auf
  (Algorithmus wie analytics/client/h.js); Maus über der Seite zeigt Zone + Klickzahl,
  darunter die Diagnose mit Rangliste und Satz (Beispieldaten)
- News-Pinnwand (Ortsgemeinde Musterbach): „Aufhängen“ im Portal, Zettel fällt auf die Pinnwand

Demos laden als eigene Chunks, laufen nur bei aktiver, sichtbarer Station und ohne
reduzierte Bewegung. Unter 900 px zeigen die Karten die Website in der Handy-Fassung.
Spezifikation: docs/superpowers/specs/2026-10-04-funktionen-demos.md

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-04 14:24:59 +00:00

132 lines
5.9 KiB
Python

#!/usr/bin/env python3
"""
Relaunch: verkleinert die Rohexporte der Kundenportal-Screenshots für die Website.
docs/relaunch/assets/portal/*.webp (Rohdateien, doppelte Auflösung)
-> public/relaunch/portal/<name>-<breite>.webp
Aufruf (aus dem Projektverzeichnis): python3 scripts/relaunch-bilder.py [NAME ...]
Ohne Argument werden alle Bilder neu geschrieben (alte Ausgaben vorher entfernt). Mit einem oder mehreren NAME-Filtern
(Dateiname ohne .webp, auch als Anfang, z. B. "phone-9" oder "phone-10-analyse") entstehen nur die passenden Bilder;
alle anderen Dateien in public/relaunch/portal bleiben unangetastet.
Nur die Bilder, die der Abschnitt "Kundenportal" (Spezifikation 7.2) wirklich zeigt, landen in public/.
Format und Budgets laut Spezifikation 9.5:
- WebP, Qualität etwa 80, Desktop-Bilder in 800 / 1200 / 1600 px Breite, Handy-Bilder in 390 / 780 px
- ein einzelnes Desktop-Bild höchstens 120 KB, ein einzelnes Handy-Bild höchstens 60 KB
(der lange Screenshot hat ein eigenes, größeres Budget, weil er mehr als dreimal so hoch ist)
- wird ein Budget überschritten, sinkt die Qualität pro Datei schrittweise (nicht unter MIN_QUALITAET)
Braucht Python 3 und Pillow mit WebP-Unterstützung.
"""
from pathlib import Path
import sys
from PIL import Image
WURZEL = Path(__file__).resolve().parent.parent
QUELLE = WURZEL / "docs" / "relaunch" / "assets" / "portal"
ZIEL = WURZEL / "public" / "relaunch" / "portal"
QUALITAET = 80
MIN_QUALITAET = 46
SCHRITT = 4
KB = 1024
BUDGET_DESKTOP = 120 * KB
BUDGET_HANDY = 60 * KB
# Der lange Screenshot ist gut dreimal so hoch wie ein Handy-Bild (5131 statt 1748 px). Sein Budget
# wächst mit der Höhe (je Bildschirmhöhe weiterhin unter 60 KB), sonst wäre der Text nur noch Matsch.
BUDGET_LANG = {390: 80 * KB, 780: 150 * KB}
# Unter der letzten Inhaltszeile (Zeile 5091) ist der Rohexport nur noch schwarz. Abgeschnitten wird
# 40 px darunter, sonst endet die Verschiebung im Handy-Rahmen auf einem leeren Streifen.
LANG_HOEHE = 5131
# Dateiname (ohne .webp) -> (Art, Zielbreiten)
BILDER = {
# Desktop-Fenster (Roh 3024 x 1786 = MacBook-Safari-Fläche 1512 x 893 @2x)
"desktop-1-projekte": ("desktop", (800, 1200, 1600)),
"desktop-2-fortschritt": ("desktop", (800, 1200, 1600)),
"desktop-3b-chat-foto": ("desktop", (800, 1200, 1600)),
"desktop-4-rechnungen": ("desktop", (800, 1200, 1600)),
# Handy (Roh 804 x 1748 = iPhone 16 Pro @2x, Safe Area 59/34; das lange Bild 804 x 5340, beschnitten auf 5131)
"phone-1-projekte": ("handy", (390, 780)),
"phone-3-projektdetails": ("handy", (390, 780)),
"phone-4b-chat-foto": ("handy", (390, 780)),
"phone-5-rechnungen": ("handy", (390, 780)),
# Funktions-Stationen (Spezifikation 2026-10-04-funktionen-demos.md): Handy-Ansicht der Funktion im Kundenportal
"phone-9-termine": ("handy", (390, 780)),
"phone-10-analyse": ("handy", (390, 780)),
"phone-11-news-musterbach": ("handy", (390, 780)),
# langer Screenshot: behält sein hohes Seitenverhältnis (eigenes Budget, siehe BUDGET_LANG)
"phone-2-projekte-lang": ("handy", (390, 780)),
}
def speichern(bild: Image.Image, ziel: Path, budget: int) -> tuple[int, int]:
"""Speichert als WebP; senkt die Qualität, bis das Budget passt. Gibt (Bytes, Qualität) zurück."""
qualitaet = QUALITAET
while True:
bild.save(ziel, "WEBP", quality=qualitaet, method=6)
groesse = ziel.stat().st_size
if groesse <= budget or qualitaet - SCHRITT < MIN_QUALITAET:
return groesse, qualitaet
qualitaet -= SCHRITT
def waehle(filter_: list[str]) -> dict:
"""Alle Bilder ohne Filter, sonst die, deren Name genau so heißt oder mit einem Filter beginnt (z. B. "phone-9")."""
if not filter_:
return BILDER
# "phone-1" soll nicht auch "phone-10-analyse" treffen: Anfang nur bis zum nächsten Bindestrich-Segment
treffer = {
name: wert for name, wert in BILDER.items()
if any(name == f or name.startswith(f + "-") for f in filter_)
}
unbekannt = [f for f in filter_ if not any(n == f or n.startswith(f + "-") for n in BILDER)]
if unbekannt:
raise SystemExit(f"Unbekannter Bildname: {', '.join(unbekannt)}")
return treffer
def main(argv: list[str]) -> int:
ZIEL.mkdir(parents=True, exist_ok=True)
bilder = waehle(argv)
if not argv:
# alte Ausgaben entfernen, damit nichts Verwaistes liegen bleibt (nur bei einem Lauf über alle Bilder)
for alt in ZIEL.glob("*.webp"):
alt.unlink()
fehler = 0
summe = 0
print(f"{'Datei':44} {'Pixel':>11} {'KB':>7} {'Q':>3} Budget")
for name, (art, breiten) in bilder.items():
quelle = QUELLE / f"{name}.webp"
if not quelle.exists():
print(f"FEHLT: {quelle}", file=sys.stderr)
fehler += 1
continue
roh = Image.open(quelle).convert("RGB")
if name.endswith("-lang"):
roh = roh.crop((0, 0, roh.width, LANG_HOEHE))
for breite in breiten:
if name.endswith("-lang"):
budget = BUDGET_LANG[breite]
else:
budget = BUDGET_DESKTOP if art == "desktop" else BUDGET_HANDY
if breite > roh.width:
print(f"übersprungen: {name} {breite} (Rohbild nur {roh.width} px breit)")
continue
hoehe = round(roh.height * breite / roh.width)
klein = roh if breite == roh.width else roh.resize((breite, hoehe), Image.LANCZOS)
datei = ZIEL / f"{name}-{breite}.webp"
groesse, qualitaet = speichern(klein, datei, budget)
summe += groesse
ok = "ok" if groesse <= budget else f"ÜBER {budget // KB} KB"
print(f"{datei.name:44} {breite:>5}x{hoehe:<5} {groesse / KB:7.1f} {qualitaet:>3} {ok}")
print(f"Summe aller Dateien: {summe / KB:.0f} KB")
return 1 if fehler else 0
if __name__ == "__main__":
sys.exit(main(sys.argv[1:]))