Der Export staucht nicht mehr auf 1080 x 1350. Die Slide entstand bei Renderqualitaet k longst in 1080k x 1350k und wurde unmittelbar vor dem Speichern wieder heruntergerechnet -- eine Vorlage mit 2390 px kam so mit 916 px an, obwohl sie bereits in 2748 px gerendert vorlag. Gespeichert wird jetzt, was gerendert wurde; bei 3x sind das 3240 x 4050. Die Statuszeile nennt die Groesse, und wer exakt 1080 will, stellt die Stufe auf 1x. Die Kontrolle vor dem Speichern hat ihren Zweck verfehlt. Sie prueft, ob jede Bildflaeche im Ergebnis Pixel traegt, sah dafuer aber das achsenparallele Umrechteck an. Die Szenen-Displays stehen per matrix3d schraeg im Raum, in ihrem Umrechteck liegen Rasterlinien, Hintergrund und das Nachbargeraet -- deren Kontrast reichte immer, um durchzukommen. Eine Slide mit voellig leerem Display wurde deshalb stumm gespeichert. Jetzt werden die echten vier Ecken ausgemessen (ueber 1-px-Marken, Chrome kennt getBoxQuads nicht) und nur die Ueberlappung von Bild und Rahmen abgetastet. Beides einzeln fuehrt in die Irre: das Szenen-Display ist groesser als der sichtbare Ausschnitt, das Feature-Mockup umgekehrt. Die Geraeterahmen stehen neu mit auf der Liste, geprueft ueber den aus der Vorlage geeichten Dunkelanteil. Nachgestellt mit opacity 0: vorher still gespeichert, jetzt Abbruch mit Nennung der Flaeche. 11 von 11 echten Ausfuhren laufen ohne Fehlalarm und ohne Wiederholung durch. Die Szenen-Screenshots fuellen ihr Display randlos. Sie lagen als kleinere Rechtecke an handgemessenen Prozentwerten darin, die nicht aus der Glasflaeche des Mockups stammten: 656 px blieben rechts frei, 470 px unten, und die Werkzeugleiste des Boards verschwand oben trotzdem unter der Blende. Weil Prozentwerte in translate() an der Bildkante haengen, loesten Vorschau und Export sie unterschiedlich auf -- die Luecke war im Export groesser als im Canvas. object-fit uebernimmt das jetzt, es bleibt kein Versatz, der auseinanderlaufen koennte. Dazu behoben: post1-alt.html konnte vier Laptop-Layouts und Slide 2 ueberhaupt nicht exportieren. workflow.png haengt an keiner Slide mehr und fehlte in der Ladeliste, im Bild stand src="undefined". Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
729 lines
31 KiB
Markdown
729 lines
31 KiB
Markdown
# Post-Generator — Regeln & Konventionen
|
||
|
||
Arbeitsgrundlage für alles, was in diesem Repo an Social-Grafiken entsteht.
|
||
**Stand:** 31.07.2026
|
||
|
||
Der Generator läuft lokal: `./serve.sh` → <http://127.0.0.1:4321/post1.html>
|
||
(Post 1, Carousel) bzw. `/index.html` (Post 2, Statement).
|
||
|
||
---
|
||
|
||
## 1. Wo die Wahrheit steht
|
||
|
||
Alle Marken-Werte stammen aus der Website, nicht aus diesem Repo. Bei Zweifel
|
||
**dort nachschauen, nicht hier raten**:
|
||
|
||
| Was | Quelle |
|
||
|---|---|
|
||
| Farben, Radien, Abstände | `HiveGrid-Website/app/globals.css` (`:root`) |
|
||
| Chamfer-Form | `HiveGrid-Website/lib/chamfer.ts` |
|
||
| Schrift | `HiveGrid-Website/app/layout.tsx` |
|
||
| Badge-Styling | `HiveGrid-Website/components/sections/ScreenshotAccordion.tsx` |
|
||
| Feature-Texte | `HiveGrid-Website/app/playground/PlaygroundClient.tsx` → `solutionModules` |
|
||
|
||
Die Werte sind hier gespiegelt, weil der Export keine Tailwind-/oklch-Farben
|
||
verträgt (siehe §7). Ändert sich die Website, müssen sie **hier nachgezogen
|
||
werden** — das passiert nicht automatisch.
|
||
|
||
---
|
||
|
||
## 2. Farben
|
||
|
||
### Marke
|
||
|
||
| Token | Wert | Verwendung |
|
||
|---|---|---|
|
||
| `--accent-yellow` | `#F1B729` | Primärer Akzent, Marker, G-Marke |
|
||
| `--accent-yellow-light` | `#FFE082` | Badge-Hintergrund |
|
||
| `--accent-yellow-dark` | `#3E2C0A` | Badge-Text |
|
||
| `--accent-teal` | `#0D6E6E` | Sekundär, aktuell ungenutzt |
|
||
| `--dark-bg` | `#30313D` | Dunkle Variante + Formen-Ebene |
|
||
|
||
### Flächen & Text
|
||
|
||
| Token | Wert |
|
||
|---|---|
|
||
| `--surface` | `#FFFFFF` |
|
||
| `--bg-primary` | `#FAFAFA` |
|
||
| `--text-primary` | `#1A1A1A` |
|
||
| `--text-secondary` | `#555555` |
|
||
| `--text-on-dark` | `#C8D0DC` |
|
||
|
||
### Pro Variante
|
||
|
||
**Hell** (Grund `#FFFFFF`)
|
||
|
||
```
|
||
--head-color #1A1A1A --frame-bg #E8EBF0
|
||
--body-color #555555 --shape-fill #30313D (deckend!)
|
||
--accent-on #C8901A --shape-line rgba(48,49,61,.13)
|
||
--marker #F1B729 --shape-accent #F1B729
|
||
--pat-color #30313D --gmark-color rgba(48,49,61,.16)
|
||
```
|
||
|
||
**Dunkel** (Grund `#30313D`)
|
||
|
||
```
|
||
--head-color #FFFFFF --frame-bg rgba(255,255,255,.11)
|
||
--body-color #B9BDC9 --shape-fill #414252
|
||
--accent-on #F1B729 --shape-line rgba(255,255,255,.10)
|
||
--marker #F1B729 --shape-accent #F1B729
|
||
--pat-color #FFFFFF --gmark-color rgba(255,255,255,.13)
|
||
```
|
||
|
||
> **`--accent-on` ist nicht `#F1B729`, sondern `#C8901A` auf Hell.** Reines
|
||
> Markengelb trägt auf Weiß bei kleinem Text nicht. Der abgedunkelte Ton ist
|
||
> Absicht, kein Fehler.
|
||
|
||
> **Der Block oben rechts ist auf Hell deckendes `#30313D`.** Das ist derselbe
|
||
> Wert wie die dunkle Story-Sektion der Website. Er darf **nicht** über die
|
||
> Intensitätsstufe aufgehellt werden, sonst ist es nicht mehr die Markenfarbe
|
||
> (siehe §5).
|
||
|
||
---
|
||
|
||
## 3. Geometrie
|
||
|
||
### Chamfer
|
||
|
||
Die Marken-Form: abgeschnittene Ecken, Echo der Honigwabe. Auf der Website
|
||
16 px, hier hochskaliert, weil die Fläche viermal so groß ist.
|
||
|
||
```css
|
||
clip-path: polygon(
|
||
Xpx 0, calc(100% - Xpx) 0, 100% Xpx, 100% calc(100% - Xpx),
|
||
calc(100% - Xpx) 100%, Xpx 100%, 0 calc(100% - Xpx), 0 Xpx
|
||
);
|
||
```
|
||
|
||
| Element | X |
|
||
|---|---|
|
||
| Badge | 11 px |
|
||
| Mockup-Rahmen außen | 22 px |
|
||
| Mockup-Rahmen innen | 14 px |
|
||
|
||
**Drei Fallstricke** (stehen auch in `chamfer.ts`):
|
||
|
||
- `clip-path` beschneidet **auch den Schatten** → wäre über `filter: drop-shadow()` zu lösen, nie über `box-shadow`. Praktisch irrelevant, seit die Posts schattenfrei sind (siehe §5)
|
||
- `clip-path` beschneidet **auch den Rahmen** → Rand über eine Hintergrundfläche lösen, nicht über `border`
|
||
- Kinder, die aus der Box ragen (Popups, Tooltips), werden abgeschnitten → solche Elemente nicht chamfern
|
||
|
||
### Hexagon
|
||
|
||
Flat-Top, also flache Ober- und Unterkante mit Spitzen links und rechts:
|
||
|
||
```css
|
||
clip-path: polygon(25% 0%, 75% 0%, 100% 50%, 75% 100%, 25% 100%, 0% 50%);
|
||
```
|
||
|
||
**Seitenverhältnis 384,5 : 332,9 = 1,155 (= 2/√3).** Aus der Logomarke
|
||
abgeleitet, nicht geschätzt. Wer davon abweicht, staucht das Sechseck.
|
||
|
||
### G-Marke
|
||
|
||
Ein einziges Polygon, aus `logo.svg` extrahiert (dort die einzige Fläche mit
|
||
`class="st3"`), auf 0,0 normalisiert:
|
||
|
||
```
|
||
384.5,166.5 336.4,83.2 192.2,166.5 288.3,166.5 240.3,249.7 144.2,249.7
|
||
96.1,166.5 144.2,83.2 336.4,83.2 298,16.8 288.3,0 96.1,0 0,166.5
|
||
96.1,332.9 288.3,332.9
|
||
```
|
||
|
||
Immer **inline** als SVG einsetzen, nie als `<img>` — nur so lässt sie sich per
|
||
`currentColor` pro Variante umfärben, und nur so überlebt sie den Export
|
||
zuverlässig (§7).
|
||
|
||
### Wabenraster
|
||
|
||
Nahtlose Kachel aus derselben G-Form. Bei Flat-Top-Hexagonen springen die
|
||
Spalten um ¾ der Breite, benachbarte Spalten sitzen eine halbe Reihe versetzt.
|
||
Eine Kachel = zwei Spalten breit, eine Reihe hoch. **Die über den Kachelrand
|
||
hinausragenden Kopien sind Pflicht** — ohne sie sieht man die Nähte.
|
||
|
||
---
|
||
|
||
## 4. Typografie
|
||
|
||
### Die Schrift
|
||
|
||
**Manrope** — dieselbe wie auf der Website. Bewusst nicht Inter: Der Kommentar in
|
||
`app/layout.tsx` nennt Inter „the default AI/website-builder font"; Manrope ist
|
||
eine Spur wärmer und geometrischer.
|
||
|
||
```css
|
||
font-family: 'Manrope', system-ui, sans-serif;
|
||
```
|
||
|
||
| | |
|
||
|---|---|
|
||
| Schnitte | 400, 500, 600, 700, 800 |
|
||
| Ablage | `assets/fonts/manrope.css` + 6 `.woff2` |
|
||
| Subsets | latin, latin-ext, cyrillic, cyrillic-ext, greek, vietnamese |
|
||
| Herkunft | Google Fonts, einmalig heruntergeladen |
|
||
|
||
**Warum lokal und nicht per CDN:** Der Export darf zur Laufzeit an keiner
|
||
fremden Origin hängen. Ein Font von `fonts.gstatic.com` wäre cross-origin, würde
|
||
den Export vom Netz abhängig machen und im Zweifel den Canvas verunreinigen. Die
|
||
URLs in `manrope.css` sind deshalb auf relative Pfade umgeschrieben.
|
||
|
||
> **Vor jedem Export auf `document.fonts.ready` warten.** Sonst rendert der erste
|
||
> Durchgang still in einer System-Fallback-Schrift, und man sieht es erst in der
|
||
> fertigen Datei. Der Download-Button bleibt bis dahin deaktiviert.
|
||
|
||
### Größen
|
||
|
||
| Element | Größe | Weight | Sonstiges |
|
||
|---|---|---|---|
|
||
| Headline | 74 px | 800 | `line-height: 1.06`, `letter-spacing: -0.03em` |
|
||
| Titel-Tagline | 74 px | 800 | identisch zur Headline, bewusst |
|
||
| Fließtext | 30 px | 500 | `line-height: 1.48`, `max-width: 830px` |
|
||
| Badge | 26 px | 600 | `letter-spacing: 0.1em` |
|
||
| Claim (Titel) | 31 px | 500 | `line-height: 1.44` |
|
||
|
||
### Akzent-Wörter
|
||
|
||
Zwei Behandlungen, **nie beide im selben Textblock**:
|
||
|
||
- **Marker** — handgezeichneter SVG-Strich als `background-image`, wellige
|
||
Kanten, angeschrägtes linkes Ende. Für Headlines und Payoffs.
|
||
Ein CSS-Verlauf ist hier **verboten**: er kann geometrisch nur ein perfektes
|
||
Rechteck erzeugen und sieht gedruckt statt gemalt aus.
|
||
- **Farbe** (`--accent-on`) — für kleinere Zeilen.
|
||
|
||
Pro Slide höchstens **ein** markiertes Wort. Zwei Marker in einem Block waren
|
||
schon zweimal zu viel.
|
||
|
||
### Sprache
|
||
|
||
- Alles Sichtbare auf **Deutsch**, Code-Kommentare auf **Englisch**
|
||
- **Keine Em-Dashes (—)** in sichtbaren Texten. Komma, Punkt oder `·`
|
||
- Feature-Texte wortgleich von der Website. Headlines dürfen gekürzt und
|
||
umgestellt werden, aber **keine neuen Wörter erfinden**
|
||
|
||
---
|
||
|
||
## 5. Aufbau der Slides
|
||
|
||
### Formate
|
||
|
||
**1080 × 1350 px (4:5)** — das hochkantigste offiziell unterstützte
|
||
Feed-Format. Belegt: Instagram unterstützt 1.91:1 bis 4:5, Breiten 320–1080
|
||
behalten die Originalauflösung, alles außerhalb wird beschnitten.
|
||
|
||
- **Alle Slides eines Carousels müssen dasselbe Seitenverhältnis haben** — die
|
||
erste Slide setzt es, der Rest wird hart darauf beschnitten
|
||
- Safe Zone: nichts Lesbares näher als ~92 px an den Rand
|
||
- Der **Profil-Grid** zeigt seit 2025 Hochformat und beschneidet auf 3:4 —
|
||
im Grid fehlen links und rechts je ~33 px. Für die drei angepinnten Cover
|
||
relevant, im Feed nicht
|
||
|
||
### Layout-Template: Tag, Überschrift, Beschreibung, Bild
|
||
|
||
Jede Feature-Slide besteht aus denselben vier Bausteinen in derselben Reihenfolge.
|
||
Das ist keine Geschmacksfrage — Wiedererkennung über fünf Slides entsteht genau
|
||
dadurch, dass sich das Gerüst nicht bewegt.
|
||
|
||
```
|
||
92 ┌──────────────────────────────────────┐
|
||
│ ▌ TAG │ ← ausgerückt (siehe unten)
|
||
│ │
|
||
│ Überschrift, zwei Zeilen │ ← dominiert die Slide
|
||
│ mit einem markierten Wort │
|
||
│ │
|
||
│ Beschreibung, zwei bis drei │ ← kürzer als man denkt
|
||
│ Zeilen Fließtext │
|
||
│ │
|
||
│ ┌────────────────────────────────── │
|
||
│ │ Bild │ ← ein dominanter Blickfang
|
||
│ │ │
|
||
└──┴───────────────────────────────────┘
|
||
```
|
||
|
||
**Größenverhältnisse.** Die Hierarchie muss ohne Nachdenken lesbar sein — was
|
||
zuerst, was zweitens, was drittens. Bei uns: Überschrift 74 px, Fließtext 30 px,
|
||
Tag 26 px. Das Verhältnis Überschrift zu Fließtext liegt bei rund **2,5 : 1**;
|
||
gängige Empfehlungen für Carousels nennen Überschrift 48–72 pt gegen Fließtext
|
||
24–36 pt, also dieselbe Größenordnung. Weniger Abstand als Faktor 2 und die
|
||
Hierarchie kippt.
|
||
|
||
**Ein Blickfang pro Slide.** Genau ein Element, auf das das Auge sofort springt —
|
||
bei uns die Überschrift, danach das Bild. Zwei gleich starke Elemente lassen den
|
||
Betrachter raten und wegwischen.
|
||
|
||
**Weißraum ist kein verschenkter Platz**, sondern das, was die Blöcke trennt.
|
||
Der Abstand zwischen Tag und Überschrift ist deshalb größer als der zwischen
|
||
Überschrift und Beschreibung: getrennte Gedanken brauchen mehr Luft als
|
||
zusammengehörige.
|
||
|
||
#### Das Tag wird ausgerückt
|
||
|
||
Der Tag hat 20 px Innenabstand. Steht sein **Kasten** bündig zur Inhaltskante,
|
||
beginnt sein **Text** 20 px weiter rechts als die Überschrift — und genau das
|
||
sieht man:
|
||
|
||
```
|
||
vorher nachher
|
||
92 │▌ WORKFLOW-ENGINE 72 │▌ WORKFLOW-ENGINE
|
||
92 │Wenn-dann 92 │Wenn-dann
|
||
└ Kasten bündig, └ Text bündig,
|
||
Text 20 px versetzt Kasten ragt aus
|
||
```
|
||
|
||
Gelöst über `margin-left: -20px`, also genau den Innenabstand. Das ist dasselbe
|
||
Prinzip wie hängende Interpunktion im Satz: Ausgerichtet wird nach der
|
||
**wahrgenommenen** Kante, nicht nach der geometrischen. Der Kasten steht dann bei
|
||
x 72 — immer noch weit innerhalb der Safe Zone.
|
||
|
||
> Wer die Innenabstände des Tags ändert, muss das negative Margin mitziehen.
|
||
> Die beiden Werte gehören zusammen.
|
||
|
||
### Ebenen (von hinten nach vorn)
|
||
|
||
| z-index | Ebene |
|
||
|---|---|
|
||
| 0 | Hintergrund: Formen, Wabenraster, Punkte, Raster, G-Marke |
|
||
| 1 | Mockup |
|
||
| 2 | Inhalt: Badge, Headline, Fließtext |
|
||
| 3 | Swipe-Hinweis |
|
||
|
||
### Keine Schatten
|
||
|
||
**Nichts wirft einen Schatten** — weder Bilder noch Mockups, Logos, Zettel oder
|
||
Text. Kein `box-shadow`, kein `filter: drop-shadow()`, kein `text-shadow`.
|
||
|
||
Tiefe entsteht stattdessen über:
|
||
|
||
- **Überlappung** — ein zweites Bild vor dem ersten liest sich auch ohne Schlagschatten als davor
|
||
- **Rahmenfarbe** — der chamferte Rahmen (`--frame-bg`) setzt das Bild von der Fläche ab
|
||
- **Versatz und Drehung** — beim Stapel und beim Notizzettel
|
||
|
||
**Ausnahme, und nur diese:** Die Blätter im Stapel-Rahmen bekommen eine
|
||
Haarlinie (`1px rgba(48,49,61,.18)`). Ohne sie läge auf der hellen Variante
|
||
Weiß auf Weiß und der Stapel wäre nicht mehr zu erkennen. Eine Linie ist kein
|
||
Schatten.
|
||
|
||
Warum: Weichgezeichnete Schatten sind das erste, was einen Post nach Vorlage
|
||
aussehen lässt. Flächig gesetzt wirkt es ruhiger und passt zur Website, die
|
||
denselben Weg geht. Nebenbei ist es beim Export billiger — jeder unscharfe
|
||
Filter muss beim 3-fachen Rendern dreifach berechnet werden.
|
||
|
||
### Was global gilt und was pro Slide
|
||
|
||
| Einstellung | Geltung | Warum |
|
||
|---|---|---|
|
||
| Variante (Hell/Dunkel) | **global** | gemischt fällt das Carousel auseinander |
|
||
| Mockup-Perspektive | **global** | dito |
|
||
| Renderqualität | **global** | Exportstufe, kein Gestaltungsmittel (§7) |
|
||
| Layout | pro Slide | jede Slide hat einen eigenen Katalog |
|
||
| Muster | pro Slide | |
|
||
| Intensität | pro Slide | |
|
||
|
||
### Durchgehendes Carousel (Panorama)
|
||
|
||
Der Post ist dann kein Stapel von fünf Bildern mehr, sondern **ein einziges
|
||
Blatt von 5400 × 1350 px**, das in fünf 1080er Fenster geschnitten wird. Wer
|
||
swipet, sieht Elemente über die Kante hinauslaufen und auf der nächsten Slide
|
||
weitergehen.
|
||
|
||
```
|
||
Slide 1 │ Slide 2 │ Slide 3 │ Slide 4 │ Slide 5
|
||
0 ─────── 1080 ────── 2160 ────── 3240 ────── 4320 ────── 5400
|
||
↑ ↑ ↑ ↑
|
||
die vier Nähte
|
||
```
|
||
|
||
**Technisch:** Eine Ebene von 5400 px Breite liegt in jeder Slide, verschoben um
|
||
`index × -1080 px`. Jede Slide zeigt dadurch ihr Fenster desselben Blatts. Kein
|
||
Zuschneiden von Hand, kein Splitter-Tool — die Slides *sind* die Schnitte.
|
||
|
||
**Regeln, die dabei zählen:**
|
||
|
||
1. **Slide 1 muss allein funktionieren.** Sie ist das Cover im Profil-Grid. Sie
|
||
darf nicht wie „das linke Fünftel von etwas" wirken
|
||
2. **Die Naht ist nie unsichtbar.** Instagram zeigt beim Swipen ein paar Pixel
|
||
Abstand zwischen den Slides. Das lässt sich nicht abstellen und ist auch nicht
|
||
schlimm — der Effekt trägt trotzdem
|
||
3. **Nichts Wichtiges auf die Naht legen.** Keine Schrift, kein Gesicht, kein
|
||
Logo. Der Schnitt gehört in ruhige Zonen
|
||
4. **Aber etwas Auffälliges über die Naht laufen lassen.** Eine halbierte Form
|
||
zieht das Auge weiter und lenkt vom Spalt ab. Genau dafür sitzen unsere
|
||
Sechsecke **mittig auf 1080 / 2160 / 3240 / 4320** — je zur Hälfte links und
|
||
rechts der Naht
|
||
5. **Nur Standbilder.** Ein Video mitten im Carousel bricht den Fluss
|
||
|
||
**Was bei uns auf dem Blatt liegt:** vier Sechsecke auf den vier Nähten
|
||
(abwechselnd massiv und Kontur, unterschiedliche Höhen), die G-Marke auf der Naht
|
||
2|3, und ein durchgehendes Band über die volle Breite als Klammer.
|
||
|
||
Das ist ein **globaler** Schalter, kein Muster pro Slide — er wirkt naturgemäß
|
||
auf alle fünf. Die Muster pro Slide bleiben zusätzlich verfügbar; für den reinen
|
||
Panorama-Look überall auf „Aus" stellen.
|
||
|
||
> Gängige Empfehlung sind 2–4 Slides fürs Panorama. Wir haben fünf. Das
|
||
> funktioniert, verlangt aber eine ruhigere Gestaltung — bei fünf Fenstern wird
|
||
> jede zusätzliche Form schnell unruhig.
|
||
|
||
### Intensität
|
||
|
||
Die Stufe „Dezent" (Faktor 0,55) wirkt **nur auf Nebenelemente** — Hexagon-Kontur,
|
||
Waben, Punkt- und Linienraster, G-Marke. Der große Block oben rechts bleibt
|
||
immer deckend, sonst wäre er nicht mehr exakt `#30313D`.
|
||
|
||
---
|
||
|
||
## 6. Mockups
|
||
|
||
Kein fotorealistischer Laptop-Rahmen für die Feature-Slides: Die Website rahmt
|
||
jeden Screenshot mit dem Chamfer (`.screenshot-frame` in `globals.css`). Das ist
|
||
die markeneigene Mockup-Form und verbindet die Posts mit der Seite.
|
||
|
||
Der Laptop auf der Titel-Slide ist die Ausnahme und bewusst ein Blickfang.
|
||
|
||
### Geometrie pro Slide
|
||
|
||
Breite, Seitenverhältnis und Position sind CSS-Variablen mit Defaults:
|
||
|
||
```
|
||
--mock-w 1090px (ragt ~100 px über die rechte Kante)
|
||
--mock-ar 1.6 (16:10)
|
||
--mock-left 92px
|
||
```
|
||
|
||
**Regel: Das Format des Rahmens folgt dem Inhalt des Screenshots, nicht
|
||
umgekehrt.** Der Form Builder ist eine schmale, zentrierte Spalte mit breiten
|
||
leeren Rändern — in einem 16:10-Rahmen zeigt er vor allem Leere. Er bekommt
|
||
deshalb 540 × 720 (3:4), mittig gesetzt, und einen Zoom von 3,8 direkt auf die
|
||
Spalte.
|
||
|
||
### Screenshots zuschneiden
|
||
|
||
Quellen sind ~2000 px breit und im Vollbild unlesbar. Jeder Screen wird auf
|
||
seinen aussagekräftigen Bereich gezoomt. Die Werte sind gerechnet, nicht geraten:
|
||
|
||
```
|
||
Rahmenbreite = 1 Einheit, Bild bei scale 1 = 1 Einheit breit
|
||
Bildhöhe = 1/BildAR Rahmenhöhe = 1/RahmenAR
|
||
|
||
Punkt fx horizontal mittig: tx = 0.5/s − fx
|
||
Punkt fy vertikal mittig: ty = (RahmenH/2)/(s · BildH) − fy
|
||
```
|
||
|
||
Mindest-Zoom zum Füllen: `RahmenH / BildH`. Darunter entstehen Lücken.
|
||
|
||
**Ausnahme Titel-Slide:** Der Laptop zeigt den Screen **1:1**, ohne Zoom. Dort
|
||
soll ein Bildschirm zeigen, was ein Bildschirm zeigt. Die Displayfläche misst
|
||
739 × 489 (AR 1,511), `workflow.png` hat 1,504 — passt praktisch deckungsgleich.
|
||
|
||
### Neue Geräte-Mockups einbauen
|
||
|
||
1. Braucht **echte Transparenz**. Ein Screenshot eines transparenten PNGs ist
|
||
keine Transparenz — das Karomuster steckt dann als Pixel drin und lässt sich
|
||
nicht sauber entfernen (einmal versucht, Ergebnis unbrauchbar)
|
||
2. Ist die Displayfläche deckend, muss sie **ausgeknockt** werden, damit der
|
||
Screenshot dahinter liegen kann. Nur so bleiben Notch und Rundungen oben
|
||
3. Displayfläche ausmessen und als Prozentwerte eintragen — sie hängt am
|
||
konkreten Bild
|
||
4. **Kein Schatten im PNG** — die Posts sind schattenfrei (§5), ein eingebrannter
|
||
Schlagschatten im Mockup-Bild lässt sich nicht mehr abschalten
|
||
|
||
### Szenen-Mockup (Perspektive)
|
||
|
||
`assets/mockups/macbook-szene.png` — Macbook Air + iPad Pro, freigestellt aus
|
||
einem Pixeden-PSD. Anders als `laptop.png` steht die Szene schräg, die
|
||
Displayflächen sind also **Vierecke, keine Rechtecke**.
|
||
|
||
Damit reicht die übliche Prozent-Box nicht. Der Screenshot wird per
|
||
`matrix3d` in die Fläche projiziert:
|
||
|
||
| | Macbook | iPad |
|
||
|---|---|---|
|
||
| Design-Größe | 3456 × 2234 | 1438 × 2164 |
|
||
| Format | 1,547 | **0,6645** |
|
||
| Eckradius | 0 | 57 px |
|
||
| Viereck aus | Smart-Object | vermessene Glasfläche |
|
||
|
||
**Zulieferung: ein iPad-Screenshot muss 0,6645 haben** (1 : 1,505), also z. B.
|
||
1440 × 2167. Beim Macbook 1,547, dort verschwinden die unteren 15 % hinter der
|
||
Tastaturschale.
|
||
|
||
Die Matrizen bilden **Design-Pixel auf Bildpixel des 5742 px breiten
|
||
Originals** ab; `--sk` skaliert von dort auf die Breite in der Slide und wird
|
||
in `applyEdits()` gesetzt, damit eine Größenänderung mitläuft. Die Eckpunkte
|
||
stammen aus dem `transform_box` der Smart-Objekte im PSD, sind also gemessen
|
||
und nicht angepasst.
|
||
|
||
**Beide Displays sind ausgeknockt**, der Ausschnitt am Macbook endet dort, wo
|
||
das iPad davor liegt. Sonst läge der Screenshot vor dem Tablet. Der Ausschnitt
|
||
kommt aus der Deckung der Screen-Ebene, **nicht** aus dem Design-Rechteck: das
|
||
reicht hinter das Gehäuse und würde die Tastaturschale anschneiden.
|
||
|
||
**Die Geräte standen auf einer Glasplatte.** Mit dem Hintergrund fällt die
|
||
Platte weg, ihre Spiegelung bleibt aber als zweiter Körper stehen, und der
|
||
Spalt dazwischen wird auf hellem Grund zur weißen Linie.
|
||
|
||
**Der Spalt bekommt keinen festen Farbwert, sondern wird spaltenweise
|
||
interpoliert.** Für jede Bildspalte: Medianfarbe der fünf Pixel über dem
|
||
Spalt, Medianfarbe der fünf darunter, linear dazwischen. Am Display bleibt so
|
||
Schwarz, am Gehäuse das Metallic, an der Kante der Verlauf. Es gibt keinen
|
||
Farbsprung mehr und damit auch keinen Streifen.
|
||
|
||
> Ein einheitlicher Ton reicht nicht. Der Spalt läuft quer über Displayrand
|
||
> **und** Aluminium, also über zwei völlig verschiedene Töne. Jeder feste Wert
|
||
> passt an einer Stelle und sitzt an der anderen als Fremdkörper.
|
||
|
||
> Nicht wegschneiden: die Spiegelung hängt am Fuß des iPads, ein Schnitt nach
|
||
> Zusammenhangskomponenten trennt den mit ab. Das war der erste Versuch.
|
||
|
||
Die Füllung läuft über die **volle Breite**, auch unter dem iPad: dessen
|
||
eigener Spalt ist sonst als weiße Linie unter dem Rahmen zu sehen. Mit einem
|
||
festen Ton ging das nicht, da saß dort ein harter dunkler Block auf dem
|
||
Aluminium; mit der Interpolation passt es.
|
||
|
||
**Das iPad-Viereck kommt nicht aus dem Smart-Object.** Das sitzt im PSD zu
|
||
hoch und lässt unten einen dreimal so breiten schwarzen Streifen stehen wie
|
||
oben. Statt dessen wird die schwarze Glasfläche vermessen:
|
||
|
||
1. Randpunkte der vier Kanten sammeln, **nur im mittleren Drittel** — an den
|
||
Ecken liegen sie auf den Rundungen
|
||
2. Ausgleichsgerade je Kante, benachbarte schneiden → die **echten** Ecken.
|
||
Über Extrempunkte kommt ein zu kleines, verdrehtes Viereck heraus, weil
|
||
die Extrema auf den Rundungen sitzen
|
||
3. Eckradius aus dem Abstand Ecke zu Glas: bei einem rechten Winkel liegt der
|
||
Scheitel der Rundung bei `r · (√2 − 1)` → **Glas 1514 × 2240, r = 95**
|
||
4. **38 px** Rand nach innen (2,5 % der Breite) → Display 1438 × 2164
|
||
5. Innenradius **konzentrisch**: `95 − 38 = 57`. Nur so bleibt der schwarze
|
||
Rand auch in den Ecken gleich breit
|
||
|
||
Der Ausschnitt wird als gerundetes Rechteck im Design-Raum gezeichnet und dann
|
||
durch die Homographie geschoben, im CSS macht `border-radius: 57px` mit
|
||
`overflow: hidden` dasselbe. Ohne das stößt eine scharfe Bildecke in die runde
|
||
Glasecke, und genau das sieht man sofort.
|
||
|
||
Beim Macbook stimmt das Smart-Object, dort bleibt es dabei.
|
||
|
||
Belegung: Kanban-Board auf dem Macbook, Projekt-Chat hochkant auf dem iPad.
|
||
|
||
### Vom Macbook-Display ist nicht alles zu sehen
|
||
|
||
Oben verdeckt die Blende des Deckels samt Kameraaussparung, unten die
|
||
Tastaturschale, rechts das davorstehende iPad. Die Ecken des Smart-Objekts
|
||
sagen darüber nichts: daraus geschätzt kommt oben 3,8 % heraus, tatsächlich
|
||
sind es **6,9 %**.
|
||
|
||
Deshalb wird gemessen statt geschätzt:
|
||
|
||
1. Deckung der Screen-Ebene per **Umkehr-Homographie in den Design-Raum**
|
||
zurückrechnen — dort ist die Verdeckung eine Maske, kein Rätsel
|
||
2. Darin das **größte achsenparallele Rechteck** suchen (Histogramm-Verfahren
|
||
je Zeile) → **2726 × 1682 bei (74, 154)**, also 78,9 % der Breite und
|
||
75,3 % der Höhe
|
||
3. Aufnahme darin zentriert einpassen, `contain`, **nie** `cover`
|
||
|
||
Daher `width: 78.877%` und der Versatz. **Nichts wird abgeschnitten und nichts
|
||
gezoomt** — was frei bleibt, trägt den aus den Bildecken gemessenen
|
||
Hintergrundton der Aufnahme (`#F5F4F4` Board, `#FAFAFA` Chat). Schon fünf
|
||
Stufen Unterschied zeichnen die freie Fläche als Rand nach.
|
||
|
||
Das Zielformat für eine Macbook-Aufnahme ist damit **1,62** statt 1,547; wer
|
||
näher dran liegt, füllt mehr Fläche. Passen muss es nicht, es wird ohnehin
|
||
eingepasst.
|
||
**Beide liegen originalgetreu auf ihrem Display, ohne Zoom.** Sie sind flacher
|
||
als der Bildschirm, es bleibt also Fläche frei; die trägt mit `#FAFAFA` den
|
||
Hintergrundton der Anwendung und ist deshalb nicht als Rand zu erkennen.
|
||
|
||
> **Das Board rückt 5,15 % nach unten.** Das Smart-Object des PSD ragt oben
|
||
> hinter die Blende des Deckels. Ohne Versatz verschwindet genau die Kopfzeile
|
||
> der Anwendung, und der Screen sieht angeschnitten aus, obwohl gar nicht
|
||
> gezoomt wurde. Unten wird der Screen von der Tastaturschale beschnitten —
|
||
> das ist so gewollt und im Pixeden-Original genauso.
|
||
|
||
> **Lizenz beachten.** Pixeden-Ressourcen sind für kommerzielle Projekte frei
|
||
> und ohne Namensnennung nutzbar, dürfen aber **nicht weitergegeben** werden.
|
||
> Das PSD gehört deshalb nicht ins Repo, nur das für uns abgeleitete PNG.
|
||
|
||
### Nachgebaute UI-Bausteine
|
||
|
||
Manche Produkt-Elemente liegen **nicht** als Screenshot, sondern als HTML im
|
||
Generator. Bisher: der **Notizzettel** auf der Workflow-Slide.
|
||
|
||
Nachbau aus `hivegrid-pitch/ui/components/StickyNote/StickyNote.tsx`,
|
||
Variante `purple`:
|
||
|
||
| Teil | Wert |
|
||
|---|---|
|
||
| Kopfleiste | `#ce93d8` |
|
||
| Fläche | `#f3e5f5` |
|
||
| Text | `#424242`, Gewicht 400, Zeilenhöhe 1,5 |
|
||
| Menü | drei Punkte (FeatherMoreVertical), kein Balken |
|
||
| Anfasser | zwei Ecken unten rechts, 45 % und 65 % Deckkraft |
|
||
|
||
Die Vorlage ist 200 px breit bei 14 px Text; auf dem 1080er Blatt sind alle
|
||
Maße rund 1,6× hochskaliert. **Farbwerte nicht neu erfinden** — sie stammen aus
|
||
der Software und müssen wiedererkennbar bleiben.
|
||
|
||
Warum HTML und nicht Screenshot: Kanten und Schrift bleiben beim 3×-Export
|
||
genauso scharf wie der Rest der Slide, der Text ist austauschbar, und es gibt
|
||
keine Scrollbalken oder Cursor-Reste im Bild.
|
||
|
||
---
|
||
|
||
## 7. Export & Bildqualität
|
||
|
||
### Der Export behält die gerenderte Auflösung
|
||
|
||
**Gespeichert wird in 1080·k × 1350·k**, wobei k die Renderqualität ist. Bei 3×
|
||
sind das 3240 × 4050. Die Datei ist damit so groß wie das, was tatsächlich
|
||
gerendert wurde.
|
||
|
||
```
|
||
Renderqualität 1× → 1080 × 1350
|
||
Renderqualität 2× → 2160 × 2700
|
||
Renderqualität 3× → 3240 × 4050
|
||
```
|
||
|
||
**Früher stand hier das Gegenteil**, und die Begründung war das Upload-Verhalten
|
||
von Instagram: Breiten von 320 bis 1080 bleiben unangetastet, alles darüber wird
|
||
von Instagram verkleinert und neu komprimiert, also lieber selbst sauber auf
|
||
1080 herunterrechnen. Der Preis dafür war aber, dass jede gelieferte Aufnahme
|
||
auf die Breite ihres Rahmens im 1080er Blatt eingedampft wurde — eine Vorlage
|
||
mit 2390 px kam mit 916 px an, obwohl sie intern schon in 2748 px vorlag.
|
||
|
||
**Das wiegt schwerer als der Upload-Pfad.** Wer eine hoch aufgelöste Aufnahme
|
||
liefert, muss sie im Ergebnis wiederfinden. Kleiner rechnen kann jedes
|
||
Zielsystem selbst, und wer bewusst exakt 1080 × 1350 will, stellt die
|
||
Renderqualität auf 1×.
|
||
|
||
### Warum das Supersampling hier hilft
|
||
|
||
Nicht wegen des Textes — den rastert der Browser bei nativer Größe ohnehin
|
||
sauber, teils sogar schärfer als supersampled. Der Gewinn liegt woanders:
|
||
|
||
- **Transformationen** — der gekippte Laptop, gedrehte Formen, Chamfer-Kanten
|
||
und Hexagon-Clips werden bei doppelter Auflösung gerastert und danach
|
||
gemittelt. Das ist echtes Anti-Aliasing statt Treppenkanten
|
||
- **Screenshots** — unsere Quellen sind ~2000 px breit und werden auf ~1090 px
|
||
gezeigt. Bei 2× zeichnet der Browser sie näher an ihrer nativen Auflösung, das
|
||
anschließende Downsampling ist unser eigenes und damit besser
|
||
- **Marker und Wabenraster** — SVG-Kanten profitieren identisch
|
||
|
||
> Bei reinen Textslides ohne Mockup ist der Unterschied gering bis null. Deshalb
|
||
> ist die Stufe umschaltbar (1× / 2× / 3×), nicht fest verdrahtet. **2× ist der
|
||
> Standard**, 3× kostet spürbar Zeit und Speicher bei kaum sichtbarem Gewinn.
|
||
|
||
### Falls doch einmal verkleinert werden muss
|
||
|
||
Nicht in einem Sprung. Browser skalieren Canvas standardmäßig schnell statt gut:
|
||
Pixel werden direkt übernommen, Nachbarinformationen gehen verloren, das
|
||
Ergebnis rauscht. Zwei Gegenmaßnahmen, beide nötig:
|
||
|
||
```js
|
||
ctx.imageSmoothingEnabled = true;
|
||
ctx.imageSmoothingQuality = "high"; // Default ist "low"
|
||
```
|
||
|
||
Und **in Halbierungsschritten** verkleinern. Ein direkter Sprung von 3240 auf
|
||
1080 überspringt Zwischenpixel; zweimal halbieren mittelt jeweils über 2 × 2 und
|
||
kommt einer echten Flächenmittelung nahe.
|
||
|
||
`createImageBitmap()` mit `resizeQuality` wäre der elegantere Weg, ist aber in
|
||
Browsern uneinheitlich umgesetzt — teils wird die Option ignoriert. `drawImage`
|
||
mit `imageSmoothingQuality` ist der verlässliche Pfad.
|
||
|
||
### Format
|
||
|
||
**PNG, nicht JPEG.** Instagram konvertiert am Ende ohnehin zu JPEG — aber unsere
|
||
Slides sind Text, flache Flächen und UI-Screenshots mit harten Kanten. Genau die
|
||
zerlegt JPEG-Kompression zuerst. Wer als PNG hochlädt, gibt Instagram ein
|
||
verlustfreies Original und zahlt nur **einmal** Kompression statt zweimal.
|
||
|
||
Für Fotoinhalte wäre JPEG bei 85–95 % die bessere Wahl. Haben wir hier nicht.
|
||
|
||
### Farbraum
|
||
|
||
**sRGB, ausschließlich.** Instagram wirft andere Profile weg und konvertiert
|
||
nach sRGB. Wer in Adobe RGB oder P3 exportiert, bekommt Farbverschiebungen bei
|
||
dieser Konvertierung. Canvas-Exporte aus dem Browser sind standardmäßig sRGB —
|
||
also nichts zu tun, solange niemand ein Farbprofil anfasst.
|
||
|
||
### Weiteres
|
||
|
||
- Auf `document.fonts.ready` warten, sonst rendert der erste Export in einer
|
||
Fallback-Schrift
|
||
- Dateinamen mit `01-`…`05-` präfixen: Instagram übernimmt beim Hochladen die
|
||
Dateisortierung
|
||
- Dateigröße im Auge behalten. Für JPEG gilt die Faustregel „unter 1 MB", sonst
|
||
komprimiert Instagram härter. Für unsere PNGs mit Screenshots ist das nicht zu
|
||
halten und auch nicht nötig
|
||
- Vom Desktop hochladen, wenn möglich — gilt als schonender als der mobile Upload
|
||
|
||
### Quellenlage, ehrlich
|
||
|
||
**Von Meta dokumentiert:** die unterstützten Seitenverhältnisse (1.91:1 bis 4:5),
|
||
die Auflösungsgrenze 320–1080, der Beschnitt außerhalb, und dass alle
|
||
Carousel-Slides auf die erste beschnitten werden.
|
||
|
||
**Community-Konsens, nicht offiziell:** die konkreten Kompressionsprozente, die
|
||
1-MB-Faustregel, Desktop-vs-Mobile, und PNG-vs-JPEG-Empfehlungen. Plausibel und
|
||
breit reproduziert, aber von Meta nirgends bestätigt. Im Zweifel selbst
|
||
gegentesten: dieselbe Slide in zwei Varianten hochladen und vergleichen.
|
||
|
||
### Vier Dinge, die den Export brechen
|
||
|
||
**1. Tailwind- und `oklch()`-Farben.** Die Export-Bibliothek kann sie nicht
|
||
serialisieren. Der Slide-Baum benutzt deshalb ausschließlich Hex und `rgba()`.
|
||
|
||
**2. PNGs im `foreignObject`.** Der Export serialisiert die Slide in eine
|
||
`data:image/svg+xml`-URL und malt sie als **ein** Bild. Raster-Kinder dekodieren
|
||
asynchron und verpassen diesen Paint regelmäßig — der Screenshot fehlt dann,
|
||
während der Rest stimmt. SVGs dekodieren synchron und sind immer da.
|
||
|
||
> Deshalb ist der Export **verifiziert statt gehofft**: Jede Screenshot-Fläche
|
||
> wird nach dem Rendern abgetastet. Eine solche Fläche ist nie glatt, sie trägt
|
||
> UI und Farbe. Liegt die Luminanz-Spanne unter 12, fehlt das Bild und es wird
|
||
> neu gerendert, bis zu fünf Versuche.
|
||
|
||
**3. Browser-Downloads.** `data:`-URLs öffneten einen leeren Tab, `blob:`-URLs
|
||
das Bild im Tab — das `download`-Attribut wurde beide Male ignoriert. Der
|
||
Generator schickt die Bytes deshalb per POST an **`/save`**, und `serve.py`
|
||
schreibt direkt nach `~/Downloads`.
|
||
|
||
**4. Stille Skriptfehler.** Ein Fehler auf oberster Ebene (Syntax, Temporal Dead
|
||
Zone) tötet das ganze Skript: Das Panel rendert aus statischem HTML weiter, aber
|
||
nichts wird gebaut. Beide Seiten haben einen globalen `error`-Listener, der so
|
||
etwas ins Statusfeld schreibt. **Nicht entfernen.**
|
||
|
||
---
|
||
|
||
## 8. Fallen, in die wir schon getappt sind
|
||
|
||
| Symptom | Ursache |
|
||
|---|---|
|
||
| Logo unsichtbar auf Weiß | Sichtbarkeitsregel vertauscht — auf Hell wurde `logo-white.svg` gezeigt |
|
||
| Logo bildschirmbreit statt zentriert | `.laptop-screen img` galt auch fürs Logo und hat es überschrieben. Solche Regeln **immer auf eine Klasse eingrenzen** |
|
||
| Element eine Zeile zu tief | `max-width` erzwang einen zusätzlichen Umbruch. Bei gesetzten `<br>` gehört `white-space: nowrap` dazu |
|
||
| Farbe „stimmt nicht" | Deckkraft der Ebene lag darüber. Eine Farbe ist nur exakt, wenn nichts sie multipliziert |
|
||
| Muster wirkt zufällig hingeworfen | Formen liefen quer durch den Text. Dekoration bleibt in den Rändern, verankert und angeschnitten |
|
||
| Nichts lädt, Bühne leer | Konstante vor ihrer Deklaration benutzt (TDZ) |
|
||
|
||
---
|
||
|
||
## 9. Ordner
|
||
|
||
```
|
||
posts/
|
||
├── serve.py / serve.sh Statischer Server + /save-Endpunkt (Port 4321)
|
||
├── post1.html Post 1 · Lösung (Carousel, 5 Slides)
|
||
├── index.html Post 2 · Statement (Einzelbild)
|
||
├── rules.md diese Datei
|
||
├── assets/
|
||
│ ├── logo.svg, logo-white.svg, cta-honeycomb.svg
|
||
│ ├── laptop.png Mockup, Display ausgeknockt
|
||
│ ├── fonts/ Manrope, lokal
|
||
│ └── screens/ Software-Screenshots
|
||
└── vendor/ html-to-image, JSZip (lokal, kein CDN)
|
||
```
|
||
|
||
Alles läuft **offline**. Keine CDN-Verweise, keine Google-Fonts zur Laufzeit —
|
||
sonst hängt der Export an einer Netzwerkverbindung und an fremden Origins.
|