Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
72 changes: 64 additions & 8 deletions e2e/api.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -401,17 +401,43 @@ test.describe('Bildgenerator facade', () => {
expect(result[1].top).toBeGreaterThan(result[0].top);
});

test('resize() changes an element without moving it off the canvas', async ({ page }) => {
test('resize() is relative to the inserted size, not an absolute scale', async ({ page }) => {
const result = await page.evaluate(async () => {
await window.Bildgenerator.setTemplate('feed_post_45');
await window.Bildgenerator.addText('Test');
const before = window.Bildgenerator.lastAdded().getScaledWidth();
await window.Bildgenerator.resize(0.4);
const after = window.Bildgenerator.lastAdded().getScaledWidth();
return { before, after };
const B = window.Bildgenerator;
await B.setTemplate('artikel_23');
await B.addText('Gemeinderat beschliesst Standort');
const inserted = B.lastAdded().getScaledWidth();

await B.resize(0.5);
const half = B.lastAdded().getScaledWidth();
await B.resize(1);
const back = B.lastAdded().getScaledWidth();
return { inserted, half, back, canvasWidth: canvas.width };
});

expect(result.after).toBeLessThan(result.before);
// The app inserts a long headline at roughly scale 0.1. Treating the
// factor as an absolute fabric scale made resize(0.35) roughly three
// times WIDER than the canvas — the opposite of what the number says.
expect(result.half).toBeCloseTo(result.inserted / 2, 0);
expect(result.back).toBeCloseTo(result.inserted, 0);
expect(result.half).toBeLessThan(result.canvasWidth);
});

test('a text scaled down in render() stays inside the canvas', async ({ page }) => {
const text = await page.evaluate(async () => {
await window.Bildgenerator.render({
template: 'artikel_23',
logoEnabled: false,
texts: [{ text: 'Gemeinderat beschliesst Standort', size: 0.35, position: 'center' }],
dpi: 72,
});
return window.Bildgenerator.objects().find((o) => o.type === 'text');
});

// This is the combination that produced a headline three canvases wide.
expect(text.width).toBeLessThan(1080);
expect(text.left).toBeGreaterThanOrEqual(0);
expect(text.insideMargin).toBe(true);
});

test('place() accepts free coordinates and still honours the margin', async ({ page }) => {
Expand Down Expand Up @@ -496,6 +522,36 @@ test.describe('Bildgenerator facade', () => {
expect(angles.cross).toBe(20);
});

test('a text can be fitted onto the Stoerer, as the brand guide shows it', async ({ page }) => {
const result = await page.evaluate(async () => {
const B = window.Bildgenerator;
await B.setTemplate('feed_post_45');
await B.setLogoEnabled(false);
await B.addShape('pinkCircle');
await B.place('top-right');

const kreis = B.objects().find((o) => o.type === 'circle');
await B.addText('31.8.');
const raw = B.lastAdded().getScaledWidth();
await B.fitInto(kreis.index);
await B.place({ onto: kreis.index });

const text = B.lastAdded();
const circle = canvas.getObjects()[kreis.index];
return {
raw,
circle: { w: circle.getScaledWidth(), cx: circle.left + circle.getScaledWidth() / 2 },
text: { w: text.getScaledWidth(), cx: text.left + text.getScaledWidth() / 2 },
};
});

// Inserted far wider than the circle, then shrunk to sit inside it.
expect(result.raw).toBeGreaterThan(result.circle.w);
expect(result.text.w).toBeLessThan(result.circle.w);
// And centred on it.
expect(result.text.cx).toBeCloseTo(result.circle.cx, 0);
});

// --------------------------------------------------------------- failures

test('rejects an unknown template by name and lists the valid ones', async ({ page }) => {
Expand Down
53 changes: 37 additions & 16 deletions llms.txt
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ weitere Freigabe.

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 === 6`.
Ruhelage erreicht hat. Aktuelle Schnittstellenversion: `Bildgenerator.VERSION === 8`.

```js
const bild = await window.Bildgenerator.render({
Expand Down Expand Up @@ -126,6 +126,7 @@ Objekt zurück, sind also verkettbar.
| `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 |
Expand Down Expand Up @@ -172,15 +173,21 @@ Linksbündig am linken Bildrand heißt also beides: `addText(t, { align: 'left'

### Größe

Es gibt kein Schriftgrößenfeld. Ein neuer Text wird automatisch auf 80 % der
Bildbreite skaliert — je mehr Wörter, desto kleiner wird er.
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: `1` ist die eingefügte Größe, `0.5`
die Hälfte, `2` das Doppelte. Der Regler in der Oberfläche erlaubt 0,1 bis 2;
darüber hinaus wird es unbrauchbar.
`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.
ein brauchbarer Ausgangswert. Prüf das Ergebnis mit `objects()`: Ist
`insideMargin` falsch, passt das Element nicht.

### Mehrere Texte

Expand Down Expand Up @@ -236,6 +243,9 @@ 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
Expand Down Expand Up @@ -383,16 +393,27 @@ gegenüberliegende Felder.

### Störer

Der magentafarbene Kreis (`addShape('pinkCircle')`) ist der Störer aus dem CD,
allerdings **nur als Form**. Text hineinzusetzen funktioniert nicht: Der Kreis
misst rund 30 % der Bildbreite, während Text immer auf bis zu 80 % skaliert
wird — selbst „31.8." wird deutlich größer als der Kreis. Die Schriftgröße
lässt sich über die Schnittstelle nicht einzeln steuern.
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
```

Setz den Kreis also als reinen Farbakzent ein, und sparsam: einer pro Sujet.
Mehrere Störer heben sich gegenseitig auf. Wenn ein Störer mit Text gebraucht
wird, ist das mit diesem Werkzeug derzeit nicht möglich — sag das, statt etwas
auszuliefern, das danebensitzt.
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

Expand Down
70 changes: 62 additions & 8 deletions resources/js/api.js
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@
* breakage surfaces in e2e/api.spec.js instead of in somebody else's script.
*/
const Bildgenerator = {
VERSION: 6,
VERSION: 8,

/** The object the most recent add* call put on the canvas. */
_lastAdded: null,
Expand Down Expand Up @@ -302,6 +302,21 @@ const Bildgenerator = {
// Free positioning: { x, y } as fractions of the canvas, 0 = left/top
// edge of the usable area, 1 = right/bottom. Still clamped to the
// protective margin, because that is a brand rule and not a default.
// Centre on another element — the Stoerer pattern from the brand guide
// is a magenta circle with a short text on it, which needs exactly
// this and nothing else.
if (position && typeof position === "object" && position.onto !== undefined) {
const host = this._resolveTarget(position.onto);
if (host === target) throw new Error("place({ onto }) needs a different element");
target.set({
left: host.left + (host.getScaledWidth() - width) / 2,
top: host.top + (host.getScaledHeight() - height) / 2,
});
target.setCoords();
canvas.renderAll();
return this;
}

if (position && typeof position === "object") {
const inRange = function (v) {
return v === undefined || (typeof v === "number" && v >= 0 && v <= 1);
Expand Down Expand Up @@ -339,17 +354,23 @@ const Bildgenerator = {
}

const [row, column] = this._resolvePosition(position);
const left = {
let left = {
start: margin,
centre: (canvas.width - width) / 2,
end: canvas.width - margin - width,
}[column];
const top = {
let top = {
start: margin,
centre: (canvas.height - height) / 2,
end: bottomLimit - height,
}[row];

// An element wider or taller than the usable area would otherwise be
// pushed off the canvas entirely. Clamp instead — objects() still
// reports insideMargin false, so the caller can see it does not fit.
left = Math.max(0, left);
top = Math.max(0, top);

target.set({ left: left, top: top });
target.setCoords();
canvas.renderAll();
Expand All @@ -361,21 +382,47 @@ const Bildgenerator = {
* object; there is no font-size field, so this is how text gets bigger or
* smaller too.
*
* A new text object is scaled to fit 80 % of the canvas width, so 1 is
* "as added". The UI slider allows 0.1 to 2.
* The factor is RELATIVE TO THE SIZE IT WAS ADDED AT: 1 leaves it as the
* app placed it, 0.5 is half, 2 is double.
*
* Relative on purpose. The app inserts a text at whatever scale makes it
* fit 80 % of the canvas — for a longer headline that is around 0.1. An
* absolute resize(0.35) would therefore make it roughly three times WIDER
* than the canvas, the opposite of what the number suggests.
*
* @param {number} factor absolute scale, not a multiplier
* @param {number} factor relative to the inserted size, must be positive
* @param {Object} [options]
* @param {fabric.Object} [options.target] default: the last element added
*/
async resize(factor, options) {
const opts = options || {};
const target = opts.target || this.lastAdded();
if (!target) throw new Error("resize() needs an element that was added first");
if (typeof factor !== "number" || factor <= 0) {
if (typeof factor !== "number" || !(factor > 0)) {
throw new Error("resize() needs a positive number");
}
target.scale(factor).setCoords();
const base = target._gatBaseScale || target.scaleX || 1;
target.scale(base * factor).setCoords();
canvas.renderAll();
return this;
},

/**
* Scale an element so it fits inside another one, leaving a little air.
* Pairs with place({ onto }) for the Stoerer pattern.
*
* @param {number|Object} host index from objects(), or an element
* @param {number} [ratio=0.6] share of the host's width to occupy
*/
async fitInto(host, ratio) {
const target = this.lastAdded();
if (!target) throw new Error("fitInto() needs an element that was added first");
const into = this._resolveTarget(host);
const share = typeof ratio === "number" ? ratio : 0.6;
const current = target.getScaledWidth();
if (!current) throw new Error("fitInto() cannot measure the element");
const wanted = into.getScaledWidth() * share;
target.scale((target.scaleX || 1) * (wanted / current)).setCoords();
canvas.renderAll();
return this;
},
Expand Down Expand Up @@ -816,6 +863,13 @@ const Bildgenerator = {
);
await this._waitUntilQuiet();
this._lastAdded = canvas.getObjects().find((o) => !before.has(o)) || null;
if (this._lastAdded) {
// The size the app gave it. resize() is relative to THIS, not to
// fabric's raw scale: a text is inserted at roughly 0.1, so an
// absolute resize(0.35) would make it three times wider than the
// canvas instead of a third of its size.
this._lastAdded._gatBaseScale = this._lastAdded.scaleX || 1;
}
return this._lastAdded;
},

Expand Down
Loading