# Grüner Bildgenerator > Browserbasiertes Werkzeug der österreichischen Grünen. Zwei Werkzeuge in einer > Seite: ein Bildgenerator für Social Media, Website-Artikelbilder und Druck, > und ein eigenständiger QR-Code-Generator. Läuft vollständig im Browser, ohne > Server und ohne Anmeldung. Diese Datei richtet sich an KI-Assistenten, die ein Bild erzeugen sollen. Sie beschreibt, wie die Seite ferngesteuert wird — nicht, wie man das Erscheinungsbild nachbaut. ## Der wichtigste Punkt zuerst **Bau das Layout nicht nach. Steuere diese Seite.** Die Hausschriften sind lizenziert und dürfen nicht weitergegeben werden. Sie liegen ausschließlich auf dieser Seite und werden von dort geladen. Es gibt keine öffentliche Webfont-URL, und es wird keine geben. Wer das Layout in eigenem HTML, Canvas-Code oder einer Grafikbibliothek nachbaut, bekommt die Schrift nicht und fällt still auf eine Ersatzschrift zurück. Das Ergebnis sieht auf den ersten Blick richtig aus und verletzt trotzdem das Erscheinungsbild. **Wenn du diese Seite nicht laden kannst, liefere kein Bild aus — sag, dass es nicht ging.** Ein Bild mit Ersatzschrift ist schlechter als kein Bild. Dasselbe gilt für Logos: Die Organisationslogos von Bundesländern, Bezirken, Gebieten, Klubs und Gemeinden liegen hier, korrekt skaliert und positioniert. Nachbauen ist weder nötig noch erlaubt. ## Voraussetzungen Du brauchst eine Umgebung, die JavaScript in einer echten Browser-Seite ausführt — etwa Playwright oder Puppeteer in einer Code-Sandbox. Ein Werkzeug, das nur HTML abrufen kann, reicht nicht: Die Seite baut ihr Bild erst im Browser auf. Falls deine Umgebung Netzwerkzugriffe freischalten muss, werden diese Hosts gebraucht: - `bildgenerator.gruene.at` — die Anwendung selbst - `design-system.gruene.at` — Stylesheet und Kopfbereich - `fonts.googleapis.com`, `fonts.gstatic.com` — die kursive Akzentschrift Die Hausschrift kommt von `bildgenerator.gruene.at` selbst und braucht keine weitere Freigabe. ## Einstieg Seite laden, warten bis `window.Bildgenerator` existiert, dann aufrufen. Alle Methoden sind asynchron und liefern erst zurück, wenn das Canvas seine Ruhelage erreicht hat. Aktuelle Schnittstellenversion: `Bildgenerator.VERSION === 15`. ```js const bild = await window.Bildgenerator.render({ template: 'artikel_23', logo: 'HERZOGENBURG', text: 'Rückenwind für Herzogenburg!', }); // → { dataURL, width, height, dpi } ``` ## Grundgerüst zum Kopieren Jeder Aufrufer schreibt sonst dieselben fünfzehn Zeilen. Dieses Gerüst prüft die Schrift, bevor es ein Bild ausliefert — der wahrscheinliche Fehlerfall ist nicht „Seite lädt nicht", sondern „Seite lädt, Schrift nicht": ```python from playwright.sync_api import sync_playwright import base64 FONT_CHECK = """async () => { await document.fonts.ready; return document.fonts.check('40px "Gotham Narrow"'); }""" RENDER = """async () => await window.Bildgenerator.render({ template: 'artikel_23', logo: 'HERZOGENBURG', text: 'Beispiel', format: 'jpeg', })""" with sync_playwright() as p: browser = p.chromium.launch() page = browser.new_page(viewport={"width": 1400, "height": 1000}) page.goto("https://bildgenerator.gruene.at/", wait_until="networkidle", timeout=90000) page.wait_for_function("() => !!window.Bildgenerator", timeout=30000) if not page.evaluate(FONT_CHECK): raise SystemExit("Hausschrift nicht geladen — kein Bild ausliefern") bild = page.evaluate(RENDER) if not bild["quality"]["ok"]: print("Hinweise:", [f["message"] for f in bild["quality"]["findings"]]) with open("bild.jpg", "wb") as f: f.write(base64.b64decode(bild["dataURL"].split(",", 1)[1])) browser.close() ``` ### Zwei Fallen beim Messen **Gib keine Fabric-Objekte aus `evaluate` zurück.** `lastAdded()` liefert eine Fabric-Instanz mit Ringbezügen. Wer sie direkt zurückgibt, bekommt `nan` statt Zahlen — stillschweigend, ohne Fehler. Lies die Werte **innerhalb** von `evaluate` aus: ```js const B = window.Bildgenerator; await B.setTemplate('artikel_23'); await B.addText('Beispiel'); const o = B.lastAdded(); // richtig: einfache Zahlen zurückgeben const messwert = { breite: Math.round(o.getScaledWidth()) }; // falsch wäre: das Objekt selbst zurückgeben — liefert nan ``` `objects()` ist davon nicht betroffen, es gibt bereits einfache Zahlen zurück. **Prüf das Ergebnis an der ausgelieferten Datei**, nicht an einem Seiten-Screenshot oder einer verkleinerten Vorschau. Mehr als ein vermeintlicher Fehler war am Ende ein Messfehler: Text, der in der Verkleinerung zu klein wirkte, ein Zeichen, das im Ausschnitt abgeschnitten aussah. ### Eine Konsolenwarnung, die nichts bedeutet ``` The provided value 'alphabetical' is not a valid enum value of type CanvasTextBaseline. ``` Die kommt aus Fabric.js, läuft bei jedem Textobjekt durch und ist harmlos. Nicht danach suchen. ## Nachschlagen — frag die Seite, rate nicht Alle gültigen Werte kommen aus der Seite selbst. Verwende diese Listen, statt Werte aus dieser Datei abzuschreiben; sie können sich ändern. ```js Bildgenerator.templates() // alle Formatnamen Bildgenerator.logos() // alle wählbaren Organisationen Bildgenerator.options() // alle übrigen Auswahlmöglichkeiten ``` `options()` liefert: `textColors`, `fontStyles`, `lineHeights`, `alignments`, `shapes`, `clipSizes`, `qrColorsOnImage`, `qrColors`, `qrBackgrounds`, `formats`, `positions`. ## Bildgenerator ### render(spec) — alles auf einmal | Feld | Bedeutung | |---|---| | `template` | **Pflicht.** Name aus `templates()` | | `background` | Hintergrundbild als `data:`-URL oder absolute URL | | `logo` | Organisation aus `logos()`, z. B. `'HERZOGENBURG'` | | `logoEnabled` | `false`, um ganz ohne Organisationslogo zu arbeiten | | `logoKnockout` | Ortsname durchsichtig (`true`) oder grün gefüllt (`false`) | | `text` | eine einzelne Schlagzeile | | `texts` | **mehrere Texte** mit eigener Farbe, Größe und Position — siehe unten | | `textColor` | aus `options().textColors` | | `fontStyle` | aus `options().fontStyles` — `'standard'` oder `'accent'` (kursive Akzentschrift) | | `align` | `left` \| `center` \| `right` — Ausrichtung **innerhalb** des Textblocks | | `textSize` | Skalierung des Textblocks, `1` = wie eingefügt | | `textRotate` | Drehung in Grad | | `lineHeight` | aus `options().lineHeights` | | `shadow` | Schattentiefe als Zahl | | `textPosition` | aus `options().positions` | | `shapes` | Liste aus `options().shapes` — `'pinkCircle'`, `'cross'` (Wahlkreuz). Auch `{ kind, position, size, rotate }` | | `images` | Liste weiterer Bilder. Auch `{ url, position }` | | `qr` | `{ text, color, position }` — QR-Code auf dem Bild | | `format` | `'png'` (Standard) oder `'jpeg'` | | `quality` | 0–1, nur bei JPEG wirksam | | `dpi` | Standard 200 | Rückgabe: `{ dataURL, width, height, dpi }`. Die Reihenfolge liegt fest und ist nicht beliebig: Das Format baut das Canvas neu und verwirft alles darauf, der Hintergrund gehört nach hinten, Text kommt zuletzt, damit er über den Elementen liegt. ### Einzelschritte Wenn `render()` nicht reicht, geht jeder Schritt einzeln. Alle liefern das Objekt zurück, sind also verkettbar. | Aufruf | Wirkung | |---|---| | `setTemplate(name)` | Format wählen; baut das Canvas neu | | `setLogo(name)` | Organisationslogo setzen | | `setLogoEnabled(false)` | Organisationslogo entfernen | | `setLogoKnockout(bool)` | Ortsname im Logo durchsichtig oder grün gefüllt | | `setBackground(url, opts)` | Hintergrundbild setzen, mit Bildausschnitt | | `addText(text, opts)` | Text hinzufügen, beliebig oft — Optionen siehe unten | | `addShape(kind)` | `'pinkCircle'`, `'cross'` oder `'panel'` hinzufügen | | `addPanel(opts)` | grüne Fläche für Text | | `addImage(url)` | weiteres Bild auf das Motiv legen | | `addQRCode({text, color})` | QR-Code auf das Bild setzen | | `place(position)` | das zuletzt hinzugefügte Element positionieren | | `resize(faktor)` | das zuletzt hinzugefügte Element vergrößern oder verkleinern | | `rotate(grad)` | das zuletzt hinzugefügte Element drehen | | `fitInto(index)` | das zuletzt hinzugefügte Element auf ein anderes verkleinern | | `objects()` | alles auflisten, was auf dem Bild liegt | | `select(index)` | ein Element ansprechen | | `update(änderungen)` | ein gesetztes Element ändern | | `remove(index)` | ein Element löschen | | `bringToFront(index)` | ein Element nach vorne holen | | `sendBackwards(index)` | ein Element einen Schritt nach hinten | | `sendToBack(index)` | ein Element ganz nach hinten | | `addCredit(text, opts)` | Bildnachweis oder KI-Kennzeichnung | | `duplicate({count})` | das zuletzt hinzugefügte Element vervielfältigen | | `qrPayload(spec)` | QR-Inhalt für `url`, `email`, `vcard` bauen | | `clipToCircle(size)` | das zuletzt hinzugefügte Objekt kreisrund beschneiden | | `lastAdded()` | das zuletzt hinzugefügte Objekt | | `protectiveMargin()` | die Schutzzone M in Pixeln | | `check()` | das Bild gegen die messbaren Regeln prüfen | | `export(opts)` | Bild erzeugen: `{ dataURL, width, height, dpi, quality }` | | `reset()` | zurück auf einen leeren Entwurf | **Zu `clipToCircle`:** Der Knopf heißt in der Oberfläche „Kreis", fügt aber nichts hinzu — er beschneidet. Der Kreis, den man hinzufügen kann, ist `addShape('pinkCircle')`. ## Text im Detail Der häufigste Stolperstein. Text ist das einzige Element mit vielen Optionen, und zwei davon werden regelmäßig verwechselt. ### Alle Optionen von `addText(text, optionen)` | Option | Werte | Bedeutung | |---|---|---| | `color` | aus `options().textColors` | `'#FFFFFF'` weiß, `'#FFED00'` gelb, `'#000000'` schwarz | | `fontStyle` | aus `options().fontStyles` | `'standard'` oder `'accent'` (kursiv, nur für Zitate und Hervorhebungen) | | `align` | `left`, `center`, `right` | Ausrichtung **innerhalb** des Blocks | | `lineHeight` | aus `options().lineHeights` | `'1.0'`, `'1.15'`, `'1.35'` | | `shadow` | Zahl | Schattentiefe, `0` = kein Schatten | Zeilenumbrüche setzt du mit `\n` im Text selbst. ### `align` ist nicht Positionierung Das ist die Verwechslung, die am meisten Zeit kostet: - **`align`** bestimmt, wie die Zeilen *innerhalb* des Textblocks stehen. Bei einzeiligem Text ist es wirkungslos. - **`place()`** bewegt den Block *auf dem Bild*. Linksbündig am linken Bildrand heißt also beides: `addText(t, { align: 'left' })` **und** `place('left')`. ### Wie lang darf eine Schlagzeile sein **Text bricht nicht automatisch um.** Eine lange Zeile wird stattdessen kleiner skaliert, bis sie hineinpasst — und irgendwann liest sie sich nicht mehr wie eine Schlagzeile. Gemessen für `artikel_23` (Export 3000 px breit): | Zeichen in einer Zeile | Schriftgröße im Export | |---|---| | 20 | 248 px | | 30 | 166 px | | 40 | 124 px | | 60 | 83 px | | 80 | 62 px | **Bis etwa 40 Zeichen pro Zeile** bleibt es eine Schlagzeile. Darüber gehört ein Umbruch hinein — von Hand, mit `\n`: ```js const B = window.Bildgenerator; await B.setTemplate('artikel_23'); await B.setLogoEnabled(false); // Nicht so — 76 Zeichen auf einer Zeile: // await B.addText('Warum wir gegen die verbindliche Bürgerbefragung zur Windkraft stimmten'); // Sondern so: await B.addText('Warum wir gegen die\nBürgerbefragung\nstimmten'); ``` Drei Zeilen sind das Maximum. Passt ein Titel auch dann nicht, gehört er gekürzt — `check()` warnt bei einer einzeiligen Schlagzeile über 45 Zeichen. Die Zahlen gelten für die 1080 px breiten Vorlagen (`artikel_23`, `feed_post_45`, `story`). Bei den Druckformaten ist absolut mehr Platz, das Verhältnis bleibt aber dasselbe. ### Größe Es gibt kein Schriftgrößenfeld. Ein neuer Text wird automatisch so skaliert, dass er in 80 % der Bildbreite Platz findet — je mehr Wörter, desto kleiner wird er. `resize(faktor)` ändert das nachträglich, **relativ zu dieser eingefügten Größe**: `1` lässt alles, wie es ist, `0.5` halbiert, `2` verdoppelt. Der Faktor ist bewusst relativ und kein absoluter Skalierungswert. Die tatsächliche Skalierung einer eingefügten Schlagzeile liegt bei etwa 0,1 — ein absolutes `0.35` würde den Text also rund dreimal **breiter** als das Bild machen, genau umgekehrt zu dem, was die Zahl nahelegt. Für eine kleine Subline unter einer großen Headline ist etwa `0.4` bis `0.5` ein brauchbarer Ausgangswert. Prüf das Ergebnis mit `objects()`: Ist `insideMargin` falsch, passt das Element nicht. ### Mehrere Texte `addText()` kann beliebig oft aufgerufen werden. **Jeder neue Text landet zentriert** — ohne `place()` liegt der zweite auf dem ersten. In `render()` geht das über `texts`: ```js await window.Bildgenerator.render({ template: 'artikel_23', logo: 'HERZOGENBURG', texts: [ { text: 'Windpark\nkommt', color: '#FFED00', position: 'top' }, { text: 'Gemeinderat beschließt Standort', color: '#FFFFFF', size: 0.35, position: 'center' }, ], }); ``` Einzeln geht dasselbe so: ```js const B = window.Bildgenerator; await B.setTemplate('artikel_23'); await B.setLogo('HERZOGENBURG'); await B.addText('Windpark\nkommt', { color: '#FFED00', align: 'left' }); await B.place('top-left'); await B.addText('Gemeinderat beschließt Standort', { color: '#FFFFFF' }); await B.resize(0.35); await B.place({ x: 0, y: 0.45 }); const bild = await B.export(); ``` ### Freie Positionierung `place()` nimmt neben den neun Feldern auch Koordinaten: ```js const B = window.Bildgenerator; await B.setTemplate('artikel_23'); await B.addText('Frei platziert'); await B.resize(0.4); await B.place({ x: 0, y: 0.45 }); // linker Rand, knapp oberhalb der Mitte ``` `x` und `y` laufen von `0` (links beziehungsweise oben) bis `1` (rechts beziehungsweise unten), jeweils innerhalb der Schutzzone. `{ x: 0.5, y: 0.5 }` ist die Mitte. Werte außerhalb von 0 bis 1 werden nicht akzeptiert — die Schutzzone gilt immer. `place({ onto: index })` legt ein Element mittig auf ein anderes — gedacht für den Störer mit Text, siehe unten. ### Drehen `rotate(grad)` dreht das zuletzt hinzugefügte Element im Uhrzeigersinn. Der Wert ist absolut, nicht relativ. ```js const B = window.Bildgenerator; await B.setTemplate('feed_post_45'); await B.addShape('cross'); await B.rotate(15); await B.place('top-right'); ``` In der Oberfläche rastet der Drehgriff bei 0, 90, 180 und 270 Grad ein; über die Schnittstelle wird der Winkel exakt gesetzt, ohne Einrasten. Sparsam einsetzen. Gedrehte Headlines sind im Erscheinungsbild nicht vorgesehen — für Störer und Wahlkreuz ist eine leichte Schräge dagegen üblich. ### Dasselbe Spec ergibt dasselbe Bild `render()` setzt alle Textoptionen, auch die, die du weglässt. Ein Aufruf mit gelbem, rechtsbündigem Text färbt also nicht den nächsten mit ein. Ohne Angabe gilt: weiß, zentriert, engster Zeilenabstand, kein Schatten, Standardschrift. Ebenso das Logo: Nennst du keines und setzt auch `logoEnabled` nicht, wird ohne Logo gerendert. Ein zuvor gesetztes Logo wirkt nicht nach. ### Export ohne Logo Ist das Logo eingeschaltet, aber keine Organisation gewählt, **verweigert** `export()` das Bild — genau wie der Downloadknopf in der Oberfläche. Der Generator zeichnet den weißen Balken nämlich trotzdem, nur der Ortsname fehlt, und das fällt erst nach der Veröffentlichung auf. Wähle also entweder eine Organisation oder setz `logoEnabled: false`. ### Was nicht geht - **Keine freie Schriftgröße in Punkt** — Größe entsteht ausschließlich über `resize()`, relativ zur eingefügten Größe. - **Keine freien Farben** — nur die Werte aus `options().textColors` beziehungsweise `options().qrColors`. - **Kein Textrahmen und keine Hintergrundfläche hinter dem Text.** Wenn eine Anforderung das braucht, sag es, statt etwas Ähnliches zu liefern. ## QR-Code-Generator Das zweite Werkzeug der Seite erzeugt einen QR-Code für sich, ohne Bild drumherum. ```js const qr = await window.Bildgenerator.renderQRCode({ data: 'https://noe.gruene.at/gemeinden/herzogenburg/', color: '#257639', background: 'transparent', }); // → { dataURL, width, height } ``` | Feld | Bedeutung | |---|---| | `data` | fertiger Inhalt als Zeichenkette | | `type` | statt `data`: `text`, `url`, `email` oder `vcard` — dann kommen die Felder dazu | | `color` | aus `options().qrColors` | | `background` | aus `options().qrBackgrounds`, `'transparent'` erlaubt | Die vier Inhaltstypen sind dieselben, die der QR-Assistent in der Oberfläche anbietet. Bau `mailto:`-Zeichenketten oder vCards **nicht selbst** zusammen — die Formatierungsregeln stecken im Werkzeug: ```js const B = window.Bildgenerator; await B.renderQRCode({ type: 'url', url: 'gruene.at' }); await B.renderQRCode({ type: 'email', email: 'buero@gruene.at', subject: 'Anfrage' }); await B.renderQRCode({ type: 'vcard', firstname: 'Anna', lastname: 'Muster', email: 'anna.muster@gruene.at', phone: '+43 1 234567', }); ``` `qrPayload(spec)` gibt dieselbe Zeichenkette zurück, ohne zu rendern. Dieselben Typangaben funktionieren auch bei `addQRCode()` für den QR auf dem Bild. Vorder- und Hintergrundfarbe müssen sich unterscheiden, sonst wird der Aufruf abgelehnt. Soll der QR-Code *auf* ein Bild, ist `addQRCode()` beziehungsweise `render({ qr })` der richtige Weg — nicht dieser hier. ## Gestaltung Die Regeln in diesem Abschnitt stammen aus dem CD-Quickguide der Grünen. Farben, Schriften und Logo setzt der Generator selbst — die kannst du gar nicht verletzen. Hier steht nur, was *du* entscheidest. ### Die eine Regel, die zählt **Typografie steht immer in Kombination mit Grün.** Text wird ausschließlich auf einer grünen Farbfläche platziert. Auf einem Hintergrundfoto ist Text nur zulässig, wenn er auf einer grünen Fläche liegt — oder wenn die abgebildete Person eindeutig vor Grün steht. Ohne Hintergrundbild ist die Fläche ohnehin grün, dann passt es immer. Sobald du ein Foto setzt, gehört unter jeden Text eine grüne Fläche: ```js const B = window.Bildgenerator; const foto = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAIAAAACCAYAAABytg0kAAAAFUlEQVR42mP8z8BQz0AEYBxVSF+FAP5FDvcfRYWgAAAAAElFTkSuQmCC'; await B.setTemplate('artikel_23'); await B.setBackground(foto); await B.addText('Windpark kommt', { panel: true }); await B.place('bottom'); ``` `panel: true` legt die Fläche an, passt sie an den Text an und hält sie darunter — auch wenn du den Text danach verschiebst oder skalierst. Der Text bleibt dabei das aktive Element, `place()` und `resize()` meinen also weiter ihn. Brauchst du die Fläche für sich, etwa als Farbblock ohne Text, gibt es `addPanel({ width, height })` mit Anteilen der nutzbaren Fläche. Weißer Text quer über ein Foto ist CD-widrig, auch wenn er lesbar ist. Hintergründe sind entweder Foto **oder** Farbfläche — nicht beides vermischt. ### Die Schutzzone M = 0,06 × kurze Kante des Formats Kein Element darf in diesen Randstreifen hineinragen. `protectiveMargin()` liefert den Wert für das aktuelle Format; `place()` hält ihn automatisch ein. Wenn du Elemente selbst positionierst, musst du ihn selbst einhalten. Für `artikel_23` (1080×1620) sind das 64,8 Pixel, für einen Feed-Post ebenfalls 64,8, für den Facebook-Header 21,6. ### Wie viel Text Eine Aussage pro Bild. Die Headline wird automatisch so skaliert, dass sie hineinpasst — je mehr Wörter, desto kleiner wird sie und desto schwächer wirkt sie. Drei Zeilen sind das Maximum, zwei sind meistens besser. Die Reihenfolge ist: Headline sagt *was*, eine Subline ergänzt den Kontext, Fließtext nur dann, wenn es ohne ihn nicht geht. Wer alles unterbringen will, bringt nichts unter. ### Wo die Dinge hingehören Alles landet zunächst **in der Mitte** — Text, QR-Code, Störer und zusätzliche Bilder stapeln sich also übereinander, bis du sie mit `place()` auseinanderziehst. Sobald du mehr als ein Element setzt, musst du positionieren. `options().positions` kennt neun Felder: top-left top top-right left center right bottom-left bottom bottom-right Bewährte Verteilungen: - **Nur Headline:** `center` oder `top` — bei `top` bleibt unten Luft für das Logo und ein ruhiger Bildbereich. - **Headline und QR-Code:** Headline auf `top`, QR auf `bottom-left`. Rechts unten sitzt oft das Logo; `place()` weicht ihm zwar aus, optisch wird es trotzdem eng. - **Headline und Störer:** Headline auf `center`, Störer auf `top-right`. Der Störer ist ein Akzent und soll die Headline nicht kreuzen. - **Foto als Hintergrund:** Headline dorthin, wo das Foto ruhig ist — nicht über Gesichter, nicht über unruhige Strukturen. `place()` hält die Schutzzone ein und weicht dem Organisationslogo aus. Es prüft aber nicht, ob zwei Elemente einander überdecken. Setz sie auf gegenüberliegende Felder. **Reihenfolge:** Erst `resize()` und `rotate()`, dann `place()`. Gedreht wird um den Mittelpunkt, dadurch verschiebt sich die Position — wer vorher positioniert, findet das Element hinterher woanders. `render()` hält diese Reihenfolge von selbst ein. ### Störer Der magentafarbene Kreis (`addShape('pinkCircle')`) ist der Störer aus dem CD. Im Quickguide trägt er einen kurzen Text — ein Datum, einen Zuruf, eine Zahl. Text wird immer auf bis zu 80 % der Bildbreite eingefügt und ist damit zunächst viel größer als der Kreis. `fitInto()` verkleinert ihn auf den Kreis, `place({ onto })` legt ihn mittig darauf: ```js const B = window.Bildgenerator; await B.setTemplate('feed_post_45'); await B.addShape('pinkCircle'); await B.place('top-right'); const kreis = B.objects().find(o => o.type === 'circle'); await B.addText('31.8.'); await B.fitInto(kreis.index); // auf 60 % des Kreises verkleinern await B.place({ onto: kreis.index }); // mittig darauflegen ``` Sparsam einsetzen: einer pro Sujet. Mehrere Störer heben sich gegenseitig auf. Der Störer ist ein Akzent und soll die Headline nicht kreuzen. ### Die Akzentschrift `fontStyle: 'accent'` ist für Hervorhebungen innerhalb einer Headline und für Zitate gedacht. Nicht für ganze Fließtexte, nicht als Standardwahl. ### Beschnitt beachten `artikel_23` wird auf den Gemeinde- und Bezirksseiten je nach Block in verschiedenen Seitenverhältnissen ausgespielt, immer aus der Mitte heraus beschnitten. Text und wichtige Bildteile gehören deshalb in die Mitte — bei diesem Format ist `center` die sichere Wahl, `top` und `bottom` können wegfallen. Beim Facebook-Header schneidet Facebook auf Mobilgeräten seitlich zu. Auch dort gehört alles Wichtige in die Mitte. ## Ändern, was schon da ist Bis hierher ging es ums Hinzufügen. Alles, was liegt, lässt sich auch wieder ansehen, ändern, verschieben oder löschen — genau wie in der Oberfläche, wo man ein Objekt anklickt und dann die Regler bedient. ### Nachsehen, was auf dem Bild liegt ```js const B = window.Bildgenerator; await B.setTemplate('artikel_23'); await B.setLogo('HERZOGENBURG'); await B.addText('Windpark kommt'); console.log(B.objects()); ``` `objects()` liefert für jedes Element: | Feld | Bedeutung | |---|---| | `index` | die Nummer, mit der `select`, `update`, `remove`, `bringToFront` es ansprechen | | `type` | `text`, `image`, `circle`, `rect` | | `role` | `canvas` (grüne Fläche), `background` (Foto), `logo`, sonst wie `type` | | `text` | der Inhalt, bei Textelementen | | `color`, `left`, `top`, `width`, `height`, `angle`, `scale` | Zustand | | `editable` | `false` bei Logo, Hintergrund und Grundfläche | | `insideMargin` | ob das Element **vollständig** in der Schutzzone liegt, alle vier Kanten | Elemente mit `editable: false` lassen sich nicht verschieben, skalieren, drehen, umfärben oder löschen — die Aufrufe werfen einen Fehler mit dem Hinweis, was stattdessen zu tun ist. Für das Logo etwa `setLogoEnabled(false)` oder `setLogo()`, für den Hintergrund ein neues `setBackground()`. Alle ändernden Aufrufe nehmen `{ target: index }` und wirken dann auf dieses Element statt auf das zuletzt hinzugefügte. **Verlass dich nicht auf die Reihenfolge.** Das letzte Element in der Liste ist das Organisationslogo, nicht das, was du zuletzt hinzugefügt hast — der Generator holt das Logo nach jedem Schritt nach vorne. `lastAdded()` gibt dir dein Element, `objects()` mit `role` sagt dir, was was ist. ### Ändern statt neu bauen ```js const B = window.Bildgenerator; await B.setTemplate('artikel_23'); await B.addText('Windpark kommt'); await B.update({ text: 'Windpark beschlossen', color: '#FFED00', align: 'left' }); ``` `update()` nimmt `text`, `color`, `align`, `lineHeight`, `shadow` und `fontStyle`. Ohne Angabe wirkt es auf das zuletzt hinzugefügte Element; mit `{ target: index }` auf ein beliebiges. Ein anderes Element sprichst du über seinen Index an: ```js const B = window.Bildgenerator; await B.setTemplate('artikel_23'); await B.addText('Windpark kommt'); const kopfzeile = B.objects().find(o => o.text === 'Windpark kommt'); B.select(kopfzeile.index); await B.update({ color: '#FFFFFF' }); ``` ### Löschen und Reihenfolge ```js const B = window.Bildgenerator; await B.setTemplate('artikel_23'); await B.addText('Erster'); await B.addShape('cross'); await B.remove(); // das zuletzt hinzugefügte const text = B.objects().find(o => o.type === 'text'); await B.bringToFront(text.index); // nach vorne holen ``` Logo und Hintergrundbild lassen sich nicht löschen — genau wie in der Oberfläche. Für das Logo nimm `setLogoEnabled(false)`, für den Hintergrund setz einfach ein anderes Bild. `bringToFront()` holt ein Element nach vorne, lässt das Logo aber oben. ## Wiederholte Elemente Ein Motiv mehrfach aufs Bild zu setzen — etwa Fahrräder für eine Radbörse — geht über `duplicate()`. Das Bild wird dabei **einmal** geladen und danach geklont; sechs Aufrufe von `addImage()` wären sechs Ladevorgänge. ```js const B = window.Bildgenerator; const icon = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAIAAAACCAYAAABytg0kAAAAFUlEQVR42mP8z8BQz0AEYBxVSF+FAP5FDvcfRYWgAAAAAElFTkSuQmCC'; await B.setTemplate('artikel_23'); await B.setLogoEnabled(false); await B.addImage(icon); await B.resize(0.25); const kopien = await B.duplicate({ count: 3 }); // die Kopien liegen zunächst aufeinander — verteilen: [B.lastAdded(), ...kopien].forEach((o, i) => { o.set({ left: 120 + i * 220, top: 900 }); o.setCoords(); }); ``` `duplicate()` liefert die neuen Objekte zurück; das verfolgte Element bleibt das Original. **Emoji sind dafür ungeeignet.** Sie kommen zwar durch, aber aus der Systemschrift statt aus der Hausschrift, und zusammengesetzte Emoji zerfallen in ihre Bestandteile — Flaggen, Familien, Hauttöne, Herz mit Variationsselektor. Ein Symbol als transparentes PNG oder SVG über `addImage()` ist verlässlich, ein Emoji nicht. ## Bildnachweis und Kennzeichnung ```js const B = window.Bildgenerator; await B.setTemplate('artikel_23'); await B.setLogoEnabled(false); await B.addCredit('Symbolfoto — KI-generiert'); ``` `addCredit()` setzt eine kleine Zeile innerhalb der Schutzzone, per Vorgabe unten links. Anders als gewöhnlicher Text wird sie **nicht** auf 80 % der Bildbreite skaliert, sondern auf einen festen kleinen Anteil der kurzen Kante — ein Nachweis in Schlagzeilengröße ist kein Nachweis. Über `{ position, size }` lässt sich beides ändern. ## Reihenfolge und Überlappung Jedes neue Element liegt oben. `sendBackwards()` schiebt eines einen Schritt zurück, `sendToBack()` ganz nach hinten — aber nie hinter die Grundfläche oder das Hintergrundfoto, das würde es unsichtbar machen. ## Der Ortsname im Logo Das Logo trägt den Ortsnamen auf einem weißen Balken. Per Vorgabe ist er **ausgespart**: Die Buchstaben sind durchsichtig und zeigen, was hinter dem Logo liegt. Auf der einfarbig grünen Fläche ist das genau richtig. Über einem Hintergrundfoto ist es falsch — dann zeigt das Foto durch die Buchstaben, und der Ortsname wird unleserlich. **Das erledigt der Generator selbst:** Sobald ein Hintergrundbild gesetzt wird, schaltet er auf die gefüllte Logo-Variante um. Dann sind sowohl der Ortsname als auch „DIE GRÜNEN" und die Innenflächen des G grün gefüllt statt durchsichtig — das Logo bringt sein Grün also selbst mit und bleibt über jedem Foto lesbar. Das passiert genau einmal — wer danach bewusst wieder auf durchsichtig stellt, wird von weiteren Bildern nicht überstimmt. Steuern lässt es sich über `setLogoKnockout(true|false)` oder `render({ logoKnockout })`. In der Oberfläche ist es die Checkbox „Ortsname im Logo durchsichtig" in Schritt 1. ## Hintergrundbilder Das Foto wird formatfüllend skaliert. Eine Achse steht dabei fast immer über und wird beschnitten — ein Hochformat in einer Querformat-Vorlage verliert links und rechts, ein Querformat in einer hohen Vorlage oben und unten. **Welcher Teil überlebt, entscheidest du.** Ohne Angabe wird mittig beschnitten, und das ist oft falsch: Bei einem Porträt in einer breiten Vorlage fehlt dann der Kopf. ```js const B = window.Bildgenerator; const foto = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAIAAAACCAYAAABytg0kAAAAFUlEQVR42mP8z8BQz0AEYBxVSF+FAP5FDvcfRYWgAAAAAElFTkSuQmCC'; await B.setTemplate('event'); await B.setBackground(foto, { focusY: 0 }); // oberer Bildrand, Gesichter await B.setBackground(foto, { focusX: 1 }); // rechter Bildrand await B.setBackground(foto); // mittig, wie bisher ``` `focusX` und `focusY` laufen von `0` (links beziehungsweise oben) über `0.5` (Mitte, Vorgabe) bis `1` (rechts beziehungsweise unten). Werte außerhalb werden abgelehnt. Faustregeln: - **Porträt oder Person:** `focusY: 0` bis `0.3`, damit der Kopf im Bild bleibt. - **Landschaft mit Himmel:** `focusY: 0.6` bis `1`, damit nicht der halbe Himmel das Bild füllt. - **Gruppenbild:** mittig lassen und lieber ein Foto wählen, dessen Seitenverhältnis zur Vorlage passt — ein starker Beschnitt schneidet sonst immer jemanden ab. Steht Text auf dem Foto, gehört eine grüne Fläche darunter — siehe oben. ## Prüfen, ob das Bild etwas taugt Geschmack lässt sich nicht messen. Einiges von dem, was ein Sujet falsch aussehen lässt, aber schon — und genau das prüft `check()`: ```js const B = window.Bildgenerator; await B.render({ template: 'artikel_23', logo: 'HERZOGENBURG', text: 'Windpark kommt' }); const ergebnis = B.check(); // { ok, errors, warnings, findings } ``` Jeder Befund nennt `rule`, `severity` und eine lesbare `message`. **`error`** sind Regelverstöße, keine Geschmacksfragen: | Regel | Was sie erkennt | |---|---| | `margin` | Ein Element ragt in die Schutzzone, rotationsbewusst und an allen vier Kanten | | `overlap` | Zwei Texte überdecken sich, oder ein Element verdeckt ein anderes zu über 35 % | | `textGap` | Zwei Textblöcke stehen enger als ein Viertel Zeilenhöhe | | `textWidth` | Ein Textblock ist breiter als die nutzbare Fläche | | `accents` | Mehr als ein Störer | | `typeOnGreen` | Text liegt auf einem Foto ohne grüne Fläche darunter | **`warning`** sind Hinweise, über die du entscheidest: enge Textabstände, mehr als drei Zeilen, verwaiste Schlusszeile, sehr kleiner Text im Export, mehr als zwei konkurrierende Akzente, eine Komposition die stark aus der Mitte hängt. `export()` führt die Prüfung von selbst aus und legt das Ergebnis als `quality` bei — **das Bild wird trotzdem geliefert.** Ob eine Überlappung von 12 % ein Fehler oder die Absicht ist, weiß nur du. Mit `export({ check: false })` lässt sich die Prüfung abschalten. Alle Regeln rechnen aus dem Objektmodell, ohne zweites Rendering. Die Schwellwerte stammen aus drei unabhängigen externen Reviews; wo sie auseinanderlagen, gilt der großzügigste Wert — damit eine Warnung etwas bedeutet. ## Formate | Name | Canvas | Wofür | |---|---|---| | `feed_post_45` | 1080×1350 | Instagram- und Facebook-Post | | `story` | 1080×1920 | Story, hochkant | | `event` | 1920×1005 | Veranstaltungs-Header | | `facebook_header` | 820×360 | Facebook-Titelbild | | `artikel_23` | 1080×1620 | Artikelbild für Gemeinde- und Bezirksseiten | | `a4` / `a4_quer` | 2480×3508 | A4 hoch / quer | | `a5` / `a5_quer` | 1748×2480 | A5 hoch / quer | | `a6` / `a6_quer` | 1240×1748 | A6 hoch / quer | Angegeben sind die Canvas-Maße. Der Export skaliert sie mit `dpi/72` — bei den Standardwerten wird aus `artikel_23` also ein Bild mit 3000×4500 Pixeln. ### Artikelbilder: keine Schlagzeile ins Bild **Der teuerste Fallstrick, und er kostete bereits eine ganze Runde von acht Bildern.** Gemeinde- und Bezirksseiten legen den Artikeltitel **selbst als Text über das Bild** — auf der Übersichtskarte wie auf der Artikelseite, dort als `

`. Ein Bild mit eingebrannter Schlagzeile zeigt den Titel damit zweimal, und auf schmalen Schirmen kollidieren die beiden. Für `artikel_23` gilt deshalb: **keine Schlagzeile im Bild.** Was trägt, ist etwas, das der Titel *nicht* schon sagt: - ein reines Motiv ohne Text, oder - eine Faktengrafik — große Kernzahl, darunter eine kurze Erklärzeile. Das ist keine Eigenheit des Generators, sondern der Zielsysteme. Ein Modell, das nur den Generator kennt, kann es nicht wissen — deshalb steht es hier. Für Social-Media-Formate gilt das **nicht**: Dort trägt das Bild die Aussage allein, und eine Schlagzeile gehört hinein. ### Wie `artikel_23` auf der Zielseite beschnitten wird Die Seiten liefern Bilder über Cloudflare Image Resizing aus und schneiden je nach Block anders zu — immer aus der Mitte (`fit=cover, gravity=0.5x0.5`). Gemessen an einer ausgelieferten Artikelseite: | Ort | Zuschnitt | Was von einer 2:3-Quelle bleibt | |---|---|---| | Übersichtskarte | nahe 2:3 | fast alles | | **Artikelseite** | **2:1** (`1600×800`) | **nur 33 % bis 67 % der Höhe** | Ein Bild, das auf der Karte perfekt sitzt, kann auf der Artikelseite oben und unten beschnitten sein. Wer für beides baut, hält **alles Wesentliche im mittleren Drittel** — bei der Faktengrafik also die Kernzahl mittig, die Erklärzeile darunter als das, was notfalls wegfallen darf. ## Schriften Zwei Schriften stehen zur Wahl, beide bereits geladen: - `'standard'` — die Hausschrift. Für Schlagzeilen und Fließtext, also fast immer. - `'accent'` — die kursive Akzentschrift. Nur für Zitate und einzelne Akzente, bewusst und sparsam. Ausführlich: https://bildgenerator.gruene.at/schriften.html ## Was du nicht selbst machen musst Diese Eigenheiten der Oberfläche nimmt dir `window.Bildgenerator` ab. Sie stehen hier, damit du gar nicht erst versuchst, die Bedienelemente direkt anzusprechen: 1. `#meme-input` ist der Datei-Upload für das Hintergrundbild, kein Textfeld. 2. Text läuft über `#text` und wird mit `#add-text` übernommen — der Abschnitt ist anfangs zugeklappt, ebenso die Bereiche für Elemente und QR-Codes. 3. `#logo-selection` ist ein verstecktes `