HiveGrid Social: Design-Library und Post-Generator
Autarke Werkbank für Social-Grafiken: Slides als HTML, Bearbeitung im Canvas, Export als Instagram-taugliche PNGs. - Assets nach brand/partner/mockups/screens sortiert, Karteileichen entfernt - G-Marke als eigene Datei statt nur als Polygon im Code - Partner-Logos und Geerkens-Logo ergänzt - Keine externen Referenzen: Schrift, Bibliotheken und Bilder liegen lokal
This commit is contained in:
commit
124524482b
34 files changed
+6129
No files matched your search
@@ -0,0 +1,561 @@
|
||||
# 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** → Tiefe über `filter: drop-shadow()`, nie `box-shadow`
|
||||
- `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 |
|
||||
|
||||
### 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. Schatten über `filter: drop-shadow()` auf dem Container, nicht im PNG
|
||||
|
||||
---
|
||||
|
||||
## 7. Export & Bildqualität
|
||||
|
||||
### Die zwei Regeln, aus denen alles folgt
|
||||
|
||||
**1. Genau 1080 px breit hochladen.** Instagram behält bei Breiten von 320 bis
|
||||
1080 die Originalauflösung. Alles darüber wird **von Instagram selbst**
|
||||
verkleinert und danach neu komprimiert — Qualitätsverlust in zwei Runden statt
|
||||
einer. Ein 4000-px-Upload ist deshalb schlechter als ein 1080-px-Upload, nicht
|
||||
besser.
|
||||
|
||||
**2. Trotzdem größer rendern.** Wir rastern intern bei **2×** (2160 × 2700) und
|
||||
skalieren selbst auf exakt 1080 × 1350 herunter. Das ist kein Widerspruch zu
|
||||
Regel 1, sondern der eigentliche Trick: Wir kontrollieren das Downsampling,
|
||||
statt es Instagram zu überlassen.
|
||||
|
||||
```
|
||||
2160 × 2700 → Halbierung → 1080 × 1350 → Upload
|
||||
render exakt
|
||||
```
|
||||
|
||||
### 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.
|
||||
|
||||
### Warum stufenweises Halbieren
|
||||
|
||||
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 statt in einem Sprung. 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.
|
||||
Reference in new issue
Block a user