Untertitel klebten am Bildrand, und ihr Zeilenabstand blieb gross
Zwei Fehler, die sich gegenseitig verstärkt haben.
**Der Zeilenabstand kam nicht mit.** `::cue { font-size }` ändert die
Schrift, nicht die Zeilenhöhe — die rechnet der Browser weiter aus seiner
eigenen Grösse. Bei 50 Prozent stand deshalb winziger Text mit dem
Abstand von grossem darunter; zwischen zwei Zeilen klaffte eine dritte.
`line-height` als Vielfaches der eigenen Grösse hält das Verhältnis.
**Und sie sassen ganz unten.** Ohne Angabe setzt der Browser jede
Einblendung an den unteren Rand des Videos. Dort liegt bei uns die
Bedienleiste — bei zwei Zeilen verschwand die untere darunter. Die
Unterkante sitzt jetzt bei neunzig Prozent der Videohöhe.
Gestellt wird das am `VTTCue`, nicht über CSS: `::cue` gestaltet den
Text, aber nicht seinen Platz. `lineAlign: "end"` ist dabei der Punkt —
damit meint `line` die Unterkante. Ohne ihn stünde ein zweizeiliger
Untertitel wieder tiefer als ein einzeiliger, also genau der Fall, um den
es ging.
Eine Einblendung, die ihre Stelle selbst mitbringt, behält sie:
Schilder und Aufschriften stehen dort, wo sie im Bild stehen.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
c5ab200846
commit
dbd66592a6
@@ -8,6 +8,7 @@ import {
|
|||||||
GROESSE_GROESST, GROESSE_KLEINST, GROESSE_SCHRITT, HINTERGRUENDE, SCHRIFTFARBEN,
|
GROESSE_GROESST, GROESSE_KLEINST, GROESSE_SCHRITT, HINTERGRUENDE, SCHRIFTFARBEN,
|
||||||
stilAnwenden, stilGeaendert, stilLesen, stilSchreiben, stilVorgabe,
|
stilAnwenden, stilGeaendert, stilLesen, stilSchreiben, stilVorgabe,
|
||||||
type Untertitelstil,
|
type Untertitelstil,
|
||||||
|
einblendungenAusrichten,
|
||||||
} from './untertitelstil';
|
} from './untertitelstil';
|
||||||
import { icons, icon } from './icons';
|
import { icons, icon } from './icons';
|
||||||
|
|
||||||
@@ -864,6 +865,14 @@ export function spieler(anfrage: Spieleranfrage): void {
|
|||||||
});
|
});
|
||||||
menuesAuffrischen();
|
menuesAuffrischen();
|
||||||
|
|
||||||
|
// **Auch hier, nicht nur beim Laden.** Wer eine Spur aus- und wieder
|
||||||
|
// einschaltet, bekommt keine zweite `load`-Meldung — die Datei liegt
|
||||||
|
// schon da. Ein zweiter Durchlauf über dieselben Einblendungen kostet
|
||||||
|
// nichts.
|
||||||
|
[...video.textTracks].forEach(t => {
|
||||||
|
if (t.mode === 'showing') einblendungenAusrichten(t);
|
||||||
|
});
|
||||||
|
|
||||||
// Geholt wird erst beim Einschalten: einen Untertitel aus einer grossen
|
// Geholt wird erst beim Einschalten: einen Untertitel aus einer grossen
|
||||||
// Datei zu ziehen kostet den Server das Lesen der ganzen Datei, und die
|
// Datei zu ziehen kostet den Server das Lesen der ganzen Datei, und die
|
||||||
// meisten Spuren schaltet nie jemand ein.
|
// meisten Spuren schaltet nie jemand ein.
|
||||||
@@ -888,6 +897,9 @@ export function spieler(anfrage: Spieleranfrage): void {
|
|||||||
// steht ohnehin in `label`.
|
// steht ohnehin in `label`.
|
||||||
if (s.language?.length === 2) t.srclang = s.language;
|
if (s.language?.length === 2) t.srclang = s.language;
|
||||||
t.dataset.spur = String(s.index);
|
t.dataset.spur = String(s.index);
|
||||||
|
// **Erst wenn die Datei geladen ist, gibt es Einblendungen.** Vorher
|
||||||
|
// ist `track.cues` leer, und ein Ausrichten liefe ins Nichts.
|
||||||
|
t.addEventListener('load', () => einblendungenAusrichten(t.track));
|
||||||
video.appendChild(t);
|
video.appendChild(t);
|
||||||
});
|
});
|
||||||
if (untertitel !== null) untertitelWaehlen(untertitel);
|
if (untertitel !== null) untertitelWaehlen(untertitel);
|
||||||
|
|||||||
@@ -50,6 +50,18 @@ export const GROESSE_KLEINST = 30;
|
|||||||
export const GROESSE_GROESST = 120;
|
export const GROESSE_GROESST = 120;
|
||||||
export const GROESSE_SCHRITT = 5;
|
export const GROESSE_SCHRITT = 5;
|
||||||
|
|
||||||
|
/** Das Vielfache der Schriftgrösse, das eine Zeile hoch ist. */
|
||||||
|
const ZEILENABSTAND = 1.25;
|
||||||
|
|
||||||
|
/** Wo die Unterkante des Untertitels sitzt, in Prozent der Videohöhe.
|
||||||
|
*
|
||||||
|
* **Nicht am Bildrand.** Der Browser setzt Einblendungen von sich aus ganz
|
||||||
|
* nach unten. Dort liegt bei uns die Bedienleiste des Spielers — und bei
|
||||||
|
* einem zweizeiligen Untertitel verschwand die untere Zeile darunter. Zehn
|
||||||
|
* Prozent Abstand halten sie frei, unabhängig von der Schriftgrösse und
|
||||||
|
* davon, ob eine oder zwei Zeilen dastehen. */
|
||||||
|
const UNTERKANTE = 90;
|
||||||
|
|
||||||
export type Untertitelstil = {
|
export type Untertitelstil = {
|
||||||
/** In Prozent der Größe, die der Browser von sich aus wählt. */
|
/** In Prozent der Größe, die der Browser von sich aus wählt. */
|
||||||
groesse: number;
|
groesse: number;
|
||||||
@@ -112,8 +124,16 @@ export function stilAnwenden(stil: Untertitelstil): void {
|
|||||||
? 'text-shadow: 0 0 4px rgba(0, 0, 0, .9), 0 1px 2px rgba(0, 0, 0, 1);'
|
? 'text-shadow: 0 0 4px rgba(0, 0, 0, .9), 0 1px 2px rgba(0, 0, 0, 1);'
|
||||||
: 'text-shadow: none;';
|
: 'text-shadow: none;';
|
||||||
|
|
||||||
|
// **Der Zeilenabstand muss mitschrumpfen.**
|
||||||
|
//
|
||||||
|
// Ohne diese Zeile rechnet der Browser ihn aus *seiner* Schriftgrösse,
|
||||||
|
// nicht aus der eingestellten. Bei 50 Prozent stand deshalb winziger Text
|
||||||
|
// mit dem Abstand von grossem darunter — zwischen zwei Zeilen klaffte eine
|
||||||
|
// dritte. Ein Vielfaches der eigenen Grösse hält das Verhältnis, egal
|
||||||
|
// welche gewählt ist.
|
||||||
const regeln = `video::cue {
|
const regeln = `video::cue {
|
||||||
font-size: ${stil.groesse}%;
|
font-size: ${stil.groesse}%;
|
||||||
|
line-height: ${ZEILENABSTAND};
|
||||||
color: ${stil.farbe};
|
color: ${stil.farbe};
|
||||||
background-color: ${stil.hintergrund};
|
background-color: ${stil.hintergrund};
|
||||||
${rand}
|
${rand}
|
||||||
@@ -134,3 +154,32 @@ function zahlImRahmen(wert: unknown, vorgabe: number): number {
|
|||||||
const gerundet = Math.round(wert / GROESSE_SCHRITT) * GROESSE_SCHRITT;
|
const gerundet = Math.round(wert / GROESSE_SCHRITT) * GROESSE_SCHRITT;
|
||||||
return Math.max(GROESSE_KLEINST, Math.min(GROESSE_GROESST, gerundet));
|
return Math.max(GROESSE_KLEINST, Math.min(GROESSE_GROESST, gerundet));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Hebt die Einblendungen einer Spur vom Bildrand ab.
|
||||||
|
*
|
||||||
|
* **Warum über die Cues und nicht über CSS.** `::cue` gestaltet den Text,
|
||||||
|
* aber nicht seinen Platz: die Position steht in der Datei, und der Browser
|
||||||
|
* setzt sie ohne Angabe ganz nach unten. Erreichbar ist sie nur hier, am
|
||||||
|
* fertig geladenen `VTTCue`.
|
||||||
|
*
|
||||||
|
* **`lineAlign: "end"` ist der eigentliche Trick.** Damit gibt `line` die
|
||||||
|
* Lage der *Unterkante* an. Ohne ihn wäre es die Oberkante — und dann
|
||||||
|
* stünde ein zweizeiliger Untertitel wieder weiter unten als ein
|
||||||
|
* einzeiliger, also genau der Fall, der das Problem war.
|
||||||
|
*
|
||||||
|
* Kennt ein Browser `lineAlign` nicht, bleibt der Wert einfach liegen; die
|
||||||
|
* Einblendung sitzt dann etwas höher als nötig, aber nie zu tief. */
|
||||||
|
export function einblendungenAusrichten(spur: TextTrack | null | undefined): void {
|
||||||
|
if (!spur?.cues) return;
|
||||||
|
|
||||||
|
for (const roh of Array.from(spur.cues)) {
|
||||||
|
const cue = roh as VTTCue;
|
||||||
|
// Eine Einblendung, die ihre Stelle selbst mitbringt, behält sie:
|
||||||
|
// Schilder und Aufschriften stehen dort, wo sie im Bild stehen.
|
||||||
|
if (cue.line !== 'auto') continue;
|
||||||
|
|
||||||
|
cue.snapToLines = false;
|
||||||
|
cue.lineAlign = 'end';
|
||||||
|
cue.line = UNTERKANTE;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user