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:
co-authored by
Claude Opus 5
parent
327e2e9195
commit
6dc7be9822
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user