Ein eigenes Bild bringt auch seinen Ausschnitt mit

Hochladen konnte man Plakat, Hintergrund und Schriftzug schon lange — der
Knopf stand da, der Endpunkt nahm an. Nur ankommen tat nichts: die Ablage
baute einen Schlüssel, den sie selbst nicht wiederfand (heute früh
repariert). Was blieb, waren drei Lücken, die man erst sieht, wenn der Weg
überhaupt funktioniert.

**Der Ausschnitt.** Ein Plakat steht hochkant im Verhältnis 2∶3, ein
Hintergrund liegt quer. Was man aussucht, passt selten von selbst — und
den Rest beschnitt bisher der Browser beim Anzeigen: mittig. Jetzt wählt
man selbst, zieht und vergrössert, und hochgeladen wird genau dieser
Ausschnitt: 600×900 beim Plakat, 1920×1080 beim Hintergrund. Gerechnet
wird im Browser; das spart die Leitung und dem Server eine Bildbibliothek.

**Der Schriftzug nicht.** Er ist ein PNG mit durchsichtigem Grund. Durch
dieselbe Mühle geschickt würde aus der Durchsicht ein weisses Rechteck —
ein Aufkleber über dem Bild. Er geht unverändert hinaus, Byte für Byte.

**Die Kachel merkte nichts.** Nach dem Hochladen fehlte der Anstoss nach
aussen; die Oberfläche zeigte weiter das alte Bild, bis man sie neu lud.

**Und zurück ging es gar nicht.** „Von Hand gesetzt" heisst auch: beim
Auffrischen bleibt es stehen. Ohne Rückweg wäre ein Fehlgriff ein
Dauerzustand — jetzt steht „Wieder automatisch" daneben, sobald etwas
festgesetzt ist.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Scarriffle
2026-10-06 19:41:04 +02:00
co-authored by Claude Opus 5
parent 327e2e9195
commit 6dc7be9822
5 changed files with 137 additions and 40 deletions
+47 -27
View File
@@ -2,19 +2,34 @@ import { knopf } from './util';
import { icons } from './icons';
import { beimSeitenwechselSchliessen } from './dialog';
/** Schneidet ein Bild auf ein Quadrat zu, bevor es hochgeladen wird.
/** Was für ein Ausschnitt gebraucht wird. */
export type Zuschnitt = {
/** Breite geteilt durch Höhe. 1 für ein Quadrat, 2/3 für ein Plakat. */
verhaeltnis: number;
/** Wie breit das Ergebnis wird; die Höhe folgt aus dem Verhältnis. */
breite: number;
/** Rund gezeigt — beim Profilbild, weil es rund dasteht. */
rund?: boolean;
titel?: string;
};
/** Schneidet ein Bild zu, bevor es hochgeladen wird.
*
* **Weil ein Profilbild rund ist.** Was hochgeladen wird, ist selten
* quadratisch — ein Foto ist hochkant, ein Ausschnitt quer. Der Browser
* beschnitt es mit `object-fit: cover` von selbst: mittig, und das trifft bei
* einem Porträt den Hals. Wer sein Bild aussucht, soll auch aussuchen, welcher
* Teil davon zu sehen ist.
* **Weil ein Bild eine Form hat.** Ein Profilbild ist rund, ein Plakat steht
* hochkant im Verhältnis 2∶3, ein Hintergrund liegt quer. Was hochgeladen
* wird, passt selten von selbst — und der Browser beschnitt es mit
* `object-fit: cover` mittig, was bei einem Porträt den Hals trifft. Wer ein
* Bild aussucht, soll auch aussuchen, welcher Teil davon zu sehen ist.
*
* **Im Browser gerechnet, nicht auf dem Server.** Hochgeladen wird genau der
* Ausschnitt — ein Quadrat von 512 Pixeln. Das spart die Leitung, den Platz
* und dem Server eine Bildbibliothek, die er sonst nirgends braucht.
* Ausschnitt. Das spart die Leitung, den Platz und dem Server eine
* Bildbibliothek, die er sonst nirgends braucht.
*
* **Nicht für Schriftzüge.** Die liegen als PNG mit durchsichtigem Grund vor;
* sie durch diese Mühle zu schicken hiesse, aus der Durchsicht ein weisses
* Rechteck zu machen. Sie gehen unverändert hinaus.
*/
export function zuschneiden(datei: File): Promise<Blob | null> {
export function zuschneiden(datei: File, o: Zuschnitt): Promise<Blob | null> {
return new Promise(fertig => {
const bild = new Image();
const adresse = URL.createObjectURL(datei);
@@ -31,7 +46,7 @@ export function zuschneiden(datei: File): Promise<Blob | null> {
const kopf = document.createElement('div');
kopf.className = 'dlg-kopf';
const h = document.createElement('h2');
h.textContent = 'Ausschnitt wählen';
h.textContent = o.titel ?? 'Ausschnitt wählen';
const zu = document.createElement('button');
zu.className = 'dlg-zu';
zu.innerHTML = icons.schliessen;
@@ -42,12 +57,17 @@ export function zuschneiden(datei: File): Promise<Blob | null> {
const buehne = document.createElement('div');
buehne.className = 'zuschnitt-buehne';
const leinwand = document.createElement('canvas');
// Die Vorschau ist so gross, wie sie dasteht; gerechnet wird am Ende
// auf 512 — zweimal dieselbe Zahl wäre eine zum Verwechseln.
const SEITE = 320;
leinwand.width = SEITE;
leinwand.height = SEITE;
leinwand.className = 'zuschnitt-feld';
// **Die Vorschau passt in ein Quadrat von 320**, egal welches Verhältnis
// — ein Plakat wird hoch und schmal, ein Hintergrund breit und flach,
// und beides bleibt im Dialog.
const LANG = 320;
const feldB = o.verhaeltnis >= 1 ? LANG : Math.round(LANG * o.verhaeltnis);
const feldH = o.verhaeltnis >= 1 ? Math.round(LANG / o.verhaeltnis) : LANG;
leinwand.width = feldB;
leinwand.height = feldH;
leinwand.className = 'zuschnitt-feld' + (o.rund ? ' zuschnitt-rund' : '');
leinwand.style.width = `${feldB}px`;
leinwand.style.height = `${feldH}px`;
buehne.appendChild(leinwand);
const regler = document.createElement('input');
@@ -82,7 +102,7 @@ export function zuschneiden(datei: File): Promise<Blob | null> {
// **Der Nullpunkt ist „ganz drin".** Die kleinere Seite füllt das
// Quadrat; alles darüber ist Vergrösserung. Damit ist der Ausschnitt
// am Anfang immer gültig, egal wie das Bild liegt.
const grund = Math.max(SEITE / bild.width, SEITE / bild.height);
const grund = Math.max(feldB / bild.width, feldH / bild.height);
let faktor = 1;
let x = 0;
let y = 0;
@@ -91,15 +111,15 @@ export function zuschneiden(datei: File): Promise<Blob | null> {
const breite = bild.width * grund * faktor;
const hoehe = bild.height * grund * faktor;
// Keine Lücke am Rand: der Ausschnitt bleibt im Bild.
x = Math.min(0, Math.max(SEITE - breite, x));
y = Math.min(0, Math.max(SEITE - hoehe, y));
x = Math.min(0, Math.max(feldB - breite, x));
y = Math.min(0, Math.max(feldH - hoehe, y));
}
function zeichnen(): void {
const stift = leinwand.getContext('2d');
if (!stift) return;
begrenzen();
stift.clearRect(0, 0, SEITE, SEITE);
stift.clearRect(0, 0, feldB, feldH);
stift.drawImage(bild, x, y, bild.width * grund * faktor, bild.height * grund * faktor);
}
@@ -109,9 +129,10 @@ export function zuschneiden(datei: File): Promise<Blob | null> {
const neu = Number(regler.value) / 100;
// Um die Mitte vergrössern, nicht um die linke obere Ecke — sonst
// wandert das Motiv beim Ziehen am Regler aus dem Bild.
const mitte = SEITE / 2;
x = mitte - (mitte - x) * (neu / faktor);
y = mitte - (mitte - y) * (neu / faktor);
const mx = feldB / 2;
const my = feldH / 2;
x = mx - (mx - x) * (neu / faktor);
y = my - (my - y) * (neu / faktor);
faktor = neu;
zeichnen();
};
@@ -138,14 +159,13 @@ export function zuschneiden(datei: File): Promise<Blob | null> {
let ergebnis: Blob | null = null;
nehmen.onclick = () => {
const ZIEL = 512;
const aus = document.createElement('canvas');
aus.width = ZIEL;
aus.height = ZIEL;
aus.width = o.breite;
aus.height = Math.round(o.breite / o.verhaeltnis);
const stift = aus.getContext('2d');
if (!stift) { d.close(); return; }
const massstab = ZIEL / SEITE;
const massstab = o.breite / feldB;
stift.drawImage(
bild,
x * massstab, y * massstab,