Einstellungen hat jeder, Verwaltung nicht
Die Seite hiess „Verwaltung" und war für alle anderen gar nicht erreichbar — „Meine Sprache" lag dadurch hinter einer Tür, die den meisten verschlossen ist. Jetzt heisst sie **Einstellungen**, steht jedem offen und hat zwei beschriftete Gruppen: oben, was einem selbst gehört (Mein Konto, Geschaut), darunter — nur für Administratoren — **Verwaltung**. Die Überschrift ist hier die halbe Aussage: sie sagt ohne weiteren Satz, wem der untere Teil gehört. In der Hauptleiste steht der Eintrag jetzt unten, ohne Gruppenüberschrift. Eine Überschrift über einem einzigen Eintrag ist eine Überschrift über nichts, und am Fuss sucht man Einstellungen ohnehin. `#/profil` und `#/verwaltung/…` leiten weiter; ein Lesezeichen merkt davon nichts. **Ein kurzes Menü am Bild.** Erst meldete der Klick ab — ohne Rückfrage. Dann führte er auf die eigene Seite, und zum Abmelden musste man erst dorthin. Beides sind zwei Punkte; ein Menü mit zwei Punkten ist ehrlicher als eine Tür, hinter der eine zweite liegt. **Und das Profilbild lässt sich zuschneiden.** Ein Profilbild ist rund, was hochgeladen wird selten quadratisch — den Ausschnitt dem Browser zu überlassen hiesse: mittig, und das trifft bei einem Porträt den Hals. Der Dialog zeigt den runden Ausschnitt, man zieht und vergrössert, und hochgeladen wird genau das: ein Quadrat von 512 Pixeln. Gemessen, es kommt auch so an. **Dazu die Spielerleiste.** Beide Knopfgruppen durften schrumpfen, ihre Knöpfe aber nicht — wird es eng, steht der äusserste halb hinter dem Fensterrand. Rechts sind es nur Knöpfe, da gibt es nichts nachzugeben; links schon. Dazu seitlich `env(safe-area-inset-*)`, damit auf einem Gerät mit runden Ecken nichts darunter liegt. Gemessen: vorher riss es ab 420 Pixeln, jetzt bis 320 nicht mehr. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
9c2e2b5f47
commit
327e2e9195
@@ -0,0 +1,169 @@
|
||||
import { knopf } from './util';
|
||||
import { icons } from './icons';
|
||||
import { beimSeitenwechselSchliessen } from './dialog';
|
||||
|
||||
/** Schneidet ein Bild auf ein Quadrat 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.
|
||||
*
|
||||
* **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.
|
||||
*/
|
||||
export function zuschneiden(datei: File): Promise<Blob | null> {
|
||||
return new Promise(fertig => {
|
||||
const bild = new Image();
|
||||
const adresse = URL.createObjectURL(datei);
|
||||
|
||||
bild.onerror = () => {
|
||||
URL.revokeObjectURL(adresse);
|
||||
fertig(null);
|
||||
};
|
||||
|
||||
bild.onload = () => {
|
||||
const d = document.createElement('dialog');
|
||||
d.className = 'dlg zuschnitt-dlg';
|
||||
|
||||
const kopf = document.createElement('div');
|
||||
kopf.className = 'dlg-kopf';
|
||||
const h = document.createElement('h2');
|
||||
h.textContent = 'Ausschnitt wählen';
|
||||
const zu = document.createElement('button');
|
||||
zu.className = 'dlg-zu';
|
||||
zu.innerHTML = icons.schliessen;
|
||||
zu.setAttribute('aria-label', 'Schliessen');
|
||||
zu.onclick = () => d.close();
|
||||
kopf.append(h, zu);
|
||||
|
||||
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';
|
||||
buehne.appendChild(leinwand);
|
||||
|
||||
const regler = document.createElement('input');
|
||||
regler.type = 'range';
|
||||
regler.className = 'feld';
|
||||
regler.min = '100';
|
||||
regler.max = '300';
|
||||
regler.value = '100';
|
||||
regler.setAttribute('aria-label', 'Vergrössern');
|
||||
|
||||
const hinweis = document.createElement('p');
|
||||
hinweis.className = 'hinweis';
|
||||
hinweis.textContent = 'Ziehen verschiebt den Ausschnitt, der Regler vergrössert.';
|
||||
|
||||
const fuss = document.createElement('div');
|
||||
fuss.className = 'dlg-fuss';
|
||||
const abbrechen = knopf('Abbrechen', 'btn');
|
||||
const nehmen = knopf('Übernehmen', 'btn btn-primary');
|
||||
fuss.append(abbrechen, nehmen);
|
||||
|
||||
const koerper = document.createElement('div');
|
||||
koerper.className = 'dlg-koerper';
|
||||
koerper.append(buehne, regler, hinweis);
|
||||
|
||||
d.append(kopf, koerper, fuss);
|
||||
document.body.appendChild(d);
|
||||
d.showModal();
|
||||
beimSeitenwechselSchliessen(d);
|
||||
|
||||
/* ---------- Was gezeigt wird ---------- */
|
||||
|
||||
// **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);
|
||||
let faktor = 1;
|
||||
let x = 0;
|
||||
let y = 0;
|
||||
|
||||
function begrenzen(): void {
|
||||
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));
|
||||
}
|
||||
|
||||
function zeichnen(): void {
|
||||
const stift = leinwand.getContext('2d');
|
||||
if (!stift) return;
|
||||
begrenzen();
|
||||
stift.clearRect(0, 0, SEITE, SEITE);
|
||||
stift.drawImage(bild, x, y, bild.width * grund * faktor, bild.height * grund * faktor);
|
||||
}
|
||||
|
||||
zeichnen();
|
||||
|
||||
regler.oninput = () => {
|
||||
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);
|
||||
faktor = neu;
|
||||
zeichnen();
|
||||
};
|
||||
|
||||
/* ---------- Ziehen ---------- */
|
||||
|
||||
let zieht: { x: number; y: number } | null = null;
|
||||
leinwand.addEventListener('pointerdown', e => {
|
||||
zieht = { x: e.clientX - x, y: e.clientY - y };
|
||||
leinwand.setPointerCapture(e.pointerId);
|
||||
});
|
||||
leinwand.addEventListener('pointermove', e => {
|
||||
if (!zieht) return;
|
||||
x = e.clientX - zieht.x;
|
||||
y = e.clientY - zieht.y;
|
||||
zeichnen();
|
||||
});
|
||||
const loslassen = (): void => { zieht = null; };
|
||||
leinwand.addEventListener('pointerup', loslassen);
|
||||
leinwand.addEventListener('pointercancel', loslassen);
|
||||
|
||||
/* ---------- Ergebnis ---------- */
|
||||
|
||||
let ergebnis: Blob | null = null;
|
||||
|
||||
nehmen.onclick = () => {
|
||||
const ZIEL = 512;
|
||||
const aus = document.createElement('canvas');
|
||||
aus.width = ZIEL;
|
||||
aus.height = ZIEL;
|
||||
const stift = aus.getContext('2d');
|
||||
if (!stift) { d.close(); return; }
|
||||
|
||||
const massstab = ZIEL / SEITE;
|
||||
stift.drawImage(
|
||||
bild,
|
||||
x * massstab, y * massstab,
|
||||
bild.width * grund * faktor * massstab,
|
||||
bild.height * grund * faktor * massstab);
|
||||
|
||||
aus.toBlob(b => { ergebnis = b; d.close(); }, 'image/jpeg', 0.9);
|
||||
};
|
||||
abbrechen.onclick = () => d.close();
|
||||
|
||||
d.addEventListener('close', () => {
|
||||
d.remove();
|
||||
URL.revokeObjectURL(adresse);
|
||||
fertig(ergebnis);
|
||||
});
|
||||
d.addEventListener('click', e => { if (e.target === d) d.close(); });
|
||||
};
|
||||
|
||||
bild.src = adresse;
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user