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>
132 lines
5.9 KiB
Python
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:]))
|