Metadata-Version: 2.4
Name: mockframe
Version: 0.1.0
Summary: Perspektivische Geraetemockups aus App-Screenshots, prozedural gerendert
Author: Moritz Lenhard
License-Expression: MIT
Project-URL: Repository, https://github.com/moOritzl/mockframe
Keywords: mockup,renderer,rasterizer,screenshot,ios
Classifier: Development Status :: 3 - Alpha
Classifier: Environment :: Console
Classifier: Intended Audience :: Developers
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Topic :: Multimedia :: Graphics :: 3D Rendering
Requires-Python: >=3.11
Description-Content-Type: text/markdown
License-File: LICENSE
Requires-Dist: numpy>=1.24
Requires-Dist: Pillow>=10.0
Requires-Dist: scipy>=1.10
Provides-Extra: dev
Requires-Dist: pytest>=7.0; extra == "dev"
Provides-Extra: hdr
Requires-Dist: imageio>=2.31; extra == "hdr"
Dynamic: license-file

# mockframe

Macht aus einem App-Screenshot ein perspektivisch gerendertes Gerätebild. Kein Browser, keine gekauften 3D-Modelle, keine GPU.

<img src="https://raw.githubusercontent.com/moOritzl/mockframe/main/examples/hero_light.png" width="420" alt="Gerendertes iPhone 16 Pro mit einem App-Screenshot auf dem Display, leicht nach links gedreht, vor hellem Verlauf">

```bash
mockframe render heute.png --auto-device --bg light -o hero.png
```

Das Gehäuse entsteht prozedural aus Millimeterangaben und wird mit einem eigenen Software-Rasterizer schattiert. Ein neues Gerät ist deshalb ein Tabelleneintrag, keine Asset-Datei.

Version 0.1, 34 Tests grün. Ein Bild in 1400 x 1750 braucht auf einem Apple-Silicon-Mac rund drei bis vier Sekunden.

## Warum kein fertiges 3D-Modell

Der naheliegende Weg wäre, ein iPhone-Modell zu kaufen und es zu rendern. Drei Gründe dagegen, in dieser Reihenfolge.

**Lizenz.** Modelle von Sketchfab, TurboSquid oder CGTrader sind überwiegend „editorial use only" oder verbieten die Weitergabe im Produkt. Ein Modell in ein Repo zu legen, das andere klonen, ist eine andere Rechtslage als ein Bild damit zu rendern. Prozedurale Geometrie aus öffentlich bekannten Maßen umgeht das Problem, statt es zu verwalten.

**Geometrie.** Ein Telefongehäuse ist ein Rounded Rect, extrudiert entlang eines gekrümmten Kantenprofils. Das sind vierzig Zeilen Code. Ein importiertes Modell bringt Material-Setups, Skalierungsfragen und Dreiecksmüll mit, ohne etwas zu lösen, was hier schwer wäre.

**Gerätewahl.** Wenn die Geometrie aus Zahlen entsteht, ist ein neues Modell ein Tabelleneintrag. Bei importierten Meshes ist es eine neue Datei, ein neues Material und ein neuer Kalibrierungslauf.

Was das kostet: der Rasterizer kann genau eine Klasse von Objekten, und Dinge, die ein fertiges Modell mitbringt, fehlen hier noch — Kamerabuckel, Antennenlinien, echte Glasrefraktion. Die Abwägung steht ausführlich in [PROJEKT.md](https://github.com/moOritzl/mockframe/blob/main/PROJEKT.md).

## Architektur

```mermaid
graph TD
    CLI["cli.py — Kommandozeile"]
    SCENE["scene.py — Komposition, Presets"]
    GEO["geometry.py — Mesh, Projektion"]
    RAST["raster.py — Z-Buffer, Culling"]
    SHADE["shading.py — Material, Umgebungen"]
    SCR["screen.py — Warp, Aspect-Pruefung"]
    DEV["devices.py — Geraetetabelle, Kantenprofil"]

    CLI --> SCENE
    SCENE --> GEO
    SCENE --> RAST
    SCENE --> SCR
    RAST --> SHADE
    GEO --> DEV
    SCR --> DEV
    SCENE --> DEV
```

`devices.py` importiert nichts und wird von allem gelesen. Deshalb ist ein neues Gerät eine Tabellenzeile und keine Codeänderung.

Zwei Entscheidungen prägen den Rest. Die Glasebene wird bewusst **nicht** mitrasterisiert, sondern separat per Vier-Punkt-Perspektivwarp komponiert: sie ist planar, deshalb ist der Warp exakt und schärfer als eine Texturinterpolation über Dreiecke. Und die Normalen werden analytisch berechnet statt über Nachbarfacetten gemittelt — das ist der Grund, warum auf der schmalen Seitenschiene keine Facettenspuren auftreten.

Der vollständige Ablauf eines Renderings, inklusive Abbruchzweig bei falschem Seitenverhältnis: [ARCHITECTURE.md](https://github.com/moOritzl/mockframe/blob/main/ARCHITECTURE.md).

## Benutzen

Mit Claude Code — einmalig, danach genügt „mach mir ein Hero-Bild":

```bash
/plugin marketplace add moOritzl/claude-plugins
```

```bash
/plugin install mockframe@moritzlenhard
```

Ohne Claude Code, oder mit einem anderen Agenten:

```bash
uvx mockframe render shot.png --auto-device -o hero.png
```

`uvx` lädt das Paket beim ersten Aufruf und legt im Projekt nichts ab. Ohne uv:
`brew install uv` auf macOS, sonst die [Anleitung von
Astral](https://docs.astral.sh/uv/getting-started/installation/).

Das Unterkommando `capture` braucht zusätzlich macOS mit Xcode, alles andere
läuft plattformunabhängig.

## Entwicklung

Python 3.11 oder neuer. Alles landet in einer venv im Projektordner, nichts im
System-Python.

```bash
python3 -m venv .venv
source .venv/bin/activate
pip install -e ".[dev]"
```

`-e` installiert editierbar, Änderungen an `src/mockframe/` wirken sofort.
`[dev]` zieht pytest mit; ohne den Zusatz kommen nur die
Laufzeitabhängigkeiten numpy, Pillow und scipy.

Der Skill ruft bewusst immer `uvx mockframe`, also die veröffentlichte
Version. Beim Entwickeln deshalb `.venv/bin/mockframe ...` direkt aufrufen.

## Kommandos

`mockframe` steht hier und im Rest der Datei für den Aufruf, den du oben
gewählt hast: `uvx mockframe` ohne Installation, `.venv/bin/mockframe` aus dem
Projektordner, oder das blanke `mockframe` bei aktivierter venv.

```bash
mockframe devices                                  # Tabelle mit Quellenangabe
mockframe render shot.png --device iphone-16-pro --preset hero-left
mockframe render shot.png --auto-device --bg dark
mockframe hdri-synth studio.npy                    # synthetische Environment-Map
mockframe capture --screens today,history --render   # nur macOS
```

`--auto-device` leitet das Gerät aus der Screenshot-Auflösung ab. 1206 x 2622 ist ein iPhone 16 Pro, und das Werkzeug weiß das.

Passt der Screenshot nicht zum Gerät, bricht der Lauf ab statt still zu strecken:

```
Abbruch: Screenshot 1920x1080 (Verhaeltnis 1.7778) passt nicht zu
iPhone 16 Pro (Natural Titanium) (erwartet 0.4600).
```

Das ist Absicht. Ein gestrecktes Rendering sieht fast richtig aus, und der Fehler fällt oft erst auf, wenn das Bild schon veröffentlicht ist.

## HDRI

Die eingebaute Umgebung ist eine Formel aus Gaußkeulen. Sie ist sauber, aber glatt, weil eine Formel keine Struktur hat. Der Wechsel auf eine echte Environment Map ist der größte Qualitätssprung pro Zeile Code, und er ersetzt genau eine Funktion — die Schnittstelle ist `sample(R)`, ein Reflexionsvektor rein, lineare Radianz raus.

| Analytische Studioumgebung | Synthetische HDRI |
|---|---|
| <img src="https://raw.githubusercontent.com/moOritzl/mockframe/main/examples/hero_light.png" width="260" alt="Rendering mit analytischer Studioumgebung, helle Titanschiene"> | <img src="https://raw.githubusercontent.com/moOritzl/mockframe/main/examples/hero_hdri.png" width="260" alt="Dasselbe Rendering mit synthetischer HDRI, dunklere Schiene mit hartem Lichtabriss"> |
| Standard, keine Datei nötig | `--hdri studio.npy` |

Beide Bilder sind derselbe Aufruf, dasselbe Gerät, dasselbe Preset. Nur die Umgebung unterscheidet sich. Die synthetische Map ist nicht automatisch die schönere Wahl — sie hat einen dunklen Grundton mit einer einzelnen Softbox, was die Schiene kontrastreicher, aber auch härter macht. Sie existiert, um den Sampling-Pfad zu belegen:

```bash
mockframe hdri-synth studio.npy
mockframe render shot.png --hdri studio.npy
```

Für Produktbilder eine echte Studio-HDRI von Poly Haven nehmen, die stehen unter CC0. Für `.exr` oder `.hdr` zusätzlich `pip install -e ".[hdr]"`, die Datei selbst einlesen und an `HDRIEnvironment(array)` geben. Das ist der einzige Weg hier, der eine eigene Installation braucht statt `uvx` — er läuft nicht über die Kommandozeile, sondern im eigenen Python.

Environment Maps sind nicht eingecheckt, `*.npy` ist ignoriert. Die
synthetische ist 6 MB groß und aus dem Code bitgleich reproduzierbar; ein
Test hält den Hash fest.

## Tests

```bash
pytest -q
```

`test_convergence.py` ist der Test, auf den es ankommt. Bei korrekter Interpolation darf das Bild nicht von der Dreieckszahl abhängen. Er existiert, weil im Prototyp die barycentrischen Gewichte um eine Position rotiert zugeordnet waren: `w0` ist die Kantenfunktion für v0 nach v1 und damit das Gewicht von v2, nicht von v1. Der Fehler war im Bild als Leitermuster auf der Seitenschiene deutlich sichtbar, aber Hochfrequenzmetriken fielen dadurch nur von 2,93 auf 2,47. Der Konvergenztest fällt eindeutig durch.

Bei Renderern ist das generell der Test, der trägt: variiere einen Parameter, der das Ergebnis nicht verändern darf, und prüfe, dass er es nicht tut.

## Genauigkeit der Maße

Breite, Höhe und Dicke stammen aus Apples Tech-Specs. Eckradius und Bezelbreite sind nicht offiziell dokumentiert und sind Näherungen. Jeder Tabelleneintrag hat ein `source`-Feld, und ein Test schlägt fehl, wenn es leer ist. Ohne das weiß in drei Monaten niemand mehr, welche Zahl geprüft ist und welche geraten.

## Stand

Fünf Geräte, vier Kamerapresets, vier Hintergründe, analytische Studioumgebung, HDRI-Sampling, Seitenverhältnis-Prüfung, Aufnahme aus dem iOS-Simulator.

Was fehlt: Kamerabuckel auf der Rückseite, Antennenlinien, Bodenreflexion, das Duo-Preset. Für das Duo braucht es zuerst den Kamerabuckel, weil dort ein Gerät angeschnitten sichtbar ist.

Der Rasterizer ist eine Python-Schleife über Dreiecke. Vektorisierung über Kacheln oder Numba würde die Renderzeit spürbar drücken, ist aber Komfort und kommt deshalb nach der Bildqualität. Die begründete Reihenfolge steht in [PROJEKT.md](https://github.com/moOritzl/mockframe/blob/main/PROJEKT.md).

## Lizenz und Rechtliches

MIT, siehe [LICENSE](https://github.com/moOritzl/mockframe/blob/main/LICENSE).

[NOTICE](https://github.com/moOritzl/mockframe/blob/main/NOTICE) hält fest, was die Lizenz nicht abdeckt: das Projekt ist nicht mit Apple verbunden, das Repo enthält keine Assets Dritter, und für Bilder, die in den App Store gehen, gelten zusätzlich Apples eigene Marketingvorgaben.
