Kamerafenster: Groesse und Ecke lassen sich stellen

Filipe: "und danach perfektionierst du auch die verschiedenen
groessen von kamera und so, perfektionier das alles bitte."

Sechs Groessen (0,6x bis 2x) und vier Ecken, beide an der SENDUNG
und nicht am Browser: Was Filipe einstellt, sehen alle. Waere es
eine Einstellung je Geraet, redete er ueber ein Bild, das bei den
Zusehenden anders aussieht -- und im Stream stuende ein drittes.

Anordnung, Groesse und Ecke gehen jetzt EINEN Weg (/layout), weil
sie zusammen eine einzige Frage beantworten: Wie sieht das Bild aus?
Drei getrennte Aufrufe waeren drei Rundrufe, und dazwischen saehen
die Zusehenden eine Mischung -- neue Anordnung, alte Ecke. Eine
falsche Angabe laesst auch das Gute stehen, statt halb umzustellen.

Die Ecke gibt es, weil unten rechts bei TikTok die Knopfreihe liegt,
bei YouTube die Fortschrittsleiste, und Untertitel fast immer unten
stehen. Eine feste Ecke ist eine, die bei jeder zweiten Plattform
im Weg ist.

Die OBS-Videoquelle kennt jetzt die Anordnung und blendet sich bei
"Nur Kamera" aus -- sonst laege im Stream ein stehengebliebenes
YouTube-Bild unter den Kameras, und Filipe saehe es nicht, weil er
auf seine Szene schaut und nicht auf die Quelle.

VIER BEFUNDE, ALLE VON DEN PRUEFUNGEN UND KEINER VOM AUGE:

1. Die Knoepfe fuer Groesse und Ecke gab es gar nicht. HTML und
   Stilblatt waren da, das Programm nicht. Gemessen: "0 von 0
   Eckknoepfen gesperrt", und der Hinweistext stand noch wortgleich
   wie im HTML -- zwei leere Kaesten, die aussahen, als sei alles
   in Ordnung.

2. Bei "Nur Kamera" war der Stapel 493 px breit statt 1072. Der
   neue Deckel max-width: 46% -- richtig gegen ein zu grosses
   Fenster bei "Video gross" -- galt still auch dort, wo die
   Kameras die ganze Flaeche fuellen sollen.

3. Am Handy haette die Groesseneinstellung ueberhaupt nichts
   bewirkt: Dort stand weiterhin width: clamp(88px, 26vw, 140px)
   am Fenster selbst. Das ist die dritte Wiederholung derselben
   Sache an einem Tag (die Saalzeilen, die Tafeln, jetzt die
   Kamerabreite): Zwei Regeln fuer dieselbe Frage sind die
   Garantie, dass eine davon irgendwo falsch gewinnt. Breite und
   Rand gehen jetzt ueber --kam-grund/--kam-rand, --kam wird an
   genau EINER Stelle gerechnet, und eine Pruefung zaehlt das nach.

4. Die Pruefung schlug auf ihren EIGENEN Kommentar an, der die
   entfernte Zeile zitiert. Ein Werkzeug, das bei jedem Lauf
   meckert, wird nach dem zweiten Mal weggeklickt -- samt dem
   echten Befund darin. Gezaehlt werden jetzt Regeln, nicht Prosa.

Gemessen beim Zuschauer und nicht in der Datenbank: 1x -> 208 px,
2x -> 416 px, alle Fenster innerhalb der Leinwand, und jede der
vier Ecken am Abstand zu den Kanten nachgewiesen statt an ihrem
eigenen Namen. Ein Knopf, der gerade nichts bewirken kann, ist
grau, und der Satz darunter sagt warum.

pruef-reaktion 320/0 - pruef-buehne 36/0 - pruef-css-klassen ok -
pruef-tippziele 11/0 - mess-reaktion ohne Beanstandung

Co-Authored-By: Claude Opus 5 <[email protected]>
This commit is contained in:
2026-09-28 14:35:02 +02:00
co-authored by Claude Opus 5
parent 2b02d88767
commit 3fedeea716
55 changed files with 1537 additions and 693 deletions
+216
View File
@@ -1652,6 +1652,222 @@ melde("=== 18. Die Regieleiste ===");
"nur wer die Sendung fuehrt, sieht sie");
}
/* =======================================================================
19. GROESSE UND ECKE DER KAMERAFENSTER
Filipe, 28.09.2026: „und danach perfektionierst du auch die
verschiedenen groessen von kamera und so."
Die Anordnung sagt das VERHAELTNIS von Video und Kameras. Was
fehlte, sind die zwei Dinge daneben: wie gross genau, und wo.
======================================================================= */
melde("");
melde("=== 19. Groesse und Ecke der Kamerafenster ===");
{
const { KAMERA_GROESSEN, KAMERA_ECKEN } = await import("./reaktion-tabellen.js");
/* ---- DIE LISTEN SELBST ----
Sechs Stufen statt eines Schiebers: Ein Schieber verspricht eine
stufenlose Einstellung, und dann sucht mitten in der Sendung
jemand nach „dem richtigen Wert", statt zu senden. */
ok(KAMERA_GROESSEN.length >= 4 && KAMERA_GROESSEN.includes(1),
`es gibt ${KAMERA_GROESSEN.length} Groessen: ${KAMERA_GROESSEN.join(", ")}`);
ok(KAMERA_GROESSEN.every((g) => g >= 0.5 && g <= 2),
"und alle liegen zwischen halb und doppelt");
/* Aufsteigend, weil die Knoepfe in dieser Reihenfolge stehen. Eine
Reihe, in der 1,5 vor 0,8 steht, sieht aus wie ein Fehler und
ist keiner -- man sucht trotzdem danach. */
ok(KAMERA_GROESSEN.every((g, i) => i === 0 || g > KAMERA_GROESSEN[i - 1]),
"die Groessen stehen aufsteigend");
ok(KAMERA_ECKEN.length === 4 && ["ol", "or", "ul", "ur"].every((e) => KAMERA_ECKEN.includes(e)),
`und vier Ecken: ${KAMERA_ECKEN.join(", ")}`);
/* ---- DIE VORGABE DER DATENBANK MUSS IN DER LISTE STEHEN ----
Sonst startet eine frische Anlage mit einem Wert, den der Server
selbst ablehnen wuerde -- und der erste Klick auf einen anderen
Knopf waere die einzige Gelegenheit, es zu merken. */
{
const tab = readFileSync(join(HIER, "reaktion-tabellen.js"), "utf8");
const vGross = tab.match(/kamera_gross REAL NOT NULL DEFAULT ([\d.]+)/);
const vEcke = tab.match(/kamera_ecke TEXT NOT NULL DEFAULT '([a-z]+)'/);
ok(!!vGross && KAMERA_GROESSEN.includes(Number(vGross[1])),
`die Vorgabe der Spalte (${vGross?.[1]}) steht in der Liste`);
ok(!!vEcke && KAMERA_ECKEN.includes(vEcke[1]),
`und die Vorgabe der Ecke (${vEcke?.[1]}) auch`);
}
/* ---- JEDE GROESSE LAESST SICH STELLEN ---- */
for (const g of KAMERA_GROESSEN) {
const a = await roh(CREW, "/workspace/api/reaktion/layout", {
method: "POST", keks: k.admin, rumpf: { kamera_gross: g },
});
ok(a.code === 200 && a.j?.kamera_gross === g, `Groesse ${g} (${a.code})`);
}
for (const e of KAMERA_ECKEN) {
const a = await roh(CREW, "/workspace/api/reaktion/layout", {
method: "POST", keks: k.admin, rumpf: { kamera_ecke: e },
});
ok(a.code === 200 && a.j?.kamera_ecke === e, `Ecke ${e} (${a.code})`);
}
/* ---- UND WAS NICHT IN DER LISTE STEHT, WIRD ABGELEHNT ----
Die Gegenprobe zu oben. Eine Pruefung, die nur bestaetigt, dass
das Erlaubte geht, bestaetigt nichts ueber die Grenze. */
const zuGross = await roh(CREW, "/workspace/api/reaktion/layout", {
method: "POST", keks: k.admin, rumpf: { kamera_gross: 8 },
});
ok(zuGross.code === 400 && zuGross.j?.fehler === "kameragroesse_unklar",
`achtfach wird abgelehnt (${zuGross.code}/${zuGross.j?.fehler})`);
const quatschE = await roh(CREW, "/workspace/api/reaktion/layout", {
method: "POST", keks: k.admin, rumpf: { kamera_ecke: "mitte" },
});
ok(quatschE.code === 400 && quatschE.j?.fehler === "ecke_unklar",
`eine erfundene Ecke wird abgelehnt (${quatschE.code}/${quatschE.j?.fehler})`);
const leer = await roh(CREW, "/workspace/api/reaktion/layout", {
method: "POST", keks: k.admin, rumpf: {},
});
ok(leer.code === 400 && leer.j?.fehler === "nichts_zu_tun",
`ein Aufruf ohne Inhalt loest keinen Rundruf aus (${leer.code})`);
const fremd = await roh(CREW, "/workspace/api/reaktion/layout", {
method: "POST", keks: k.modi, rumpf: { kamera_gross: 2 },
});
ok(fremd.code === 403, `ein Modi stellt die Groesse nicht (${fremd.code})`);
/* ---- DREI EINSTELLUNGEN IN EINEM AUFRUF ----
Das ist der Grund, warum es EIN Weg ist. Drei getrennte Aufrufe
waeren drei Rundrufe, und zwischen ihnen saehen die Zusehenden
eine Mischung: neue Anordnung, alte Ecke. */
const alles = await roh(CREW, "/workspace/api/reaktion/layout", {
method: "POST", keks: k.admin,
rumpf: { layout: "gleich", kamera_gross: 1.5, kamera_ecke: "ol" },
});
ok(alles.code === 200 && alles.j?.layout === "gleich"
&& alles.j?.kamera_gross === 1.5 && alles.j?.kamera_ecke === "ol",
`Anordnung, Groesse und Ecke in einem Zug (${alles.code})`);
/* UND EINE FALSCHE ANGABE AENDERT GAR NICHTS. Sonst waere eine
abgelehnte Ecke eine halb durchgefuehrte Umstellung -- die
Anordnung stuende neu, die Ecke alt, und die Fehlermeldung
saehe aus, als sei nichts passiert. */
const halb = await roh(CREW, "/workspace/api/reaktion/layout", {
method: "POST", keks: k.admin,
rumpf: { layout: "kino", kamera_ecke: "diagonal" },
});
const danach = await roh(CREW, "/workspace/api/reaktion", { keks: k.admin });
ok(halb.code === 400 && danach.j?.layout === "gleich",
`eine falsche Angabe laesst auch das Gute stehen (${danach.j?.layout})`);
/* ---- DER STAND TRAEGT BEIDES UND DIE AUSWAHL DAZU ----
Die Regie baut ihre Knoepfe daraus. Baute sie sie aus einer
eigenen Liste, waere das eine zweite Liste, die irgendwann von
der ersten abweicht -- und dann steht ein Knopf da, den der
Server ablehnt. */
await roh(CREW, "/workspace/api/reaktion/layout", {
method: "POST", keks: k.admin,
rumpf: { layout: "kino", kamera_gross: 1, kamera_ecke: "ur" },
});
const stand = await roh(CREW, "/workspace/api/reaktion", { keks: k.admin });
ok(stand.j?.kamera_gross === 1 && stand.j?.kamera_ecke === "ur",
"der Stand traegt Groesse und Ecke");
ok(Array.isArray(stand.j?.kamera_groessen)
&& stand.j.kamera_groessen.length === KAMERA_GROESSEN.length
&& Array.isArray(stand.j?.kamera_ecken)
&& stand.j.kamera_ecken.length === 4,
"und die Auswahl kommt vom Server, nicht aus dem Browser");
/* ---- AUCH DIE ZUSEHENDEN BEKOMMEN ES ----
Es ist Teil des Bildes, nicht der Regie. Wer es nicht bekaeme,
saehe die Fenster woanders als der, der darueber redet. */
const gast = await roh(CREW, "/workspace/api/reaktion", { keks: k.gast });
ok(gast.j?.kamera_gross === 1 && gast.j?.kamera_ecke === "ur",
"auch ein Zuschauer bekommt Groesse und Ecke");
/* ---- WAS DER QUELLTEXT HERGIBT ---------------------------------- */
{
const css = readFileSync(join(HIER, "..", "workspace", "assets", "css", "reaktion.css"), "utf8");
const js = readFileSync(join(HIER, "..", "workspace", "assets", "js", "reaktion.js"), "utf8");
const mld = readFileSync(join(HIER, "..", "workspace", "assets", "js", "meldung.js"), "utf8");
/* ==== KOMMENTARE RAUS, BEVOR IRGENDETWAS GEZAEHLT WIRD ========
Beim ersten Lauf meldete die Pruefung „1x steht eine feste
Breite am Fenster" -- und fand sie in einem Kommentar, der
genau davor warnt und die entfernte Zeile zitiert. Ein
Werkzeug, das bei jedem Lauf meckert, wird nach dem zweiten
Mal weggeklickt, zusammen mit dem echten Befund darin.
Gezaehlt werden Regeln, nicht Prosa. */
const regeln = css.replace(/\/\*[\s\S]*?\*\//g, " ");
/* JEDE ECKE BRAUCHT EINE REGEL -- sonst ist sie ein Knopf, der
nichts tut. */
const ohneEcke = KAMERA_ECKEN.filter((e) => !regeln.includes(`[data-ecke="${e}"]`));
ok(ohneEcke.length === 0,
ohneEcke.length ? `OHNE REGEL: ${ohneEcke.join(", ")}` : "jede Ecke hat eine eigene Regel");
ok(!regeln.includes('[data-ecke="mitte"]'),
"Gegenprobe: eine erfundene Ecke hat keine");
/* ---- EINE BREITE, EINE STELLE ----
Bis zum 28.09. stand die Breite eines Kamerafensters zweimal im
Stilblatt: als Grundregel und noch einmal fuer „Kameras gross".
Wer sie einstellbar macht, muesste beide mitdenken, und die
zweite wird vergessen. Das ist im selben Haus am selben Tag
zweimal passiert (die Saalzeilen, die Tafeln). */
const breiten = [...regeln.matchAll(/\.kamera\s*\{[^}]*?\bwidth:\s*clamp\(/g)];
ok(breiten.length === 0,
breiten.length
? `${breiten.length}x steht eine feste Breite am Fenster statt an --kam`
: "die Breite eines Fensters steht nur als --kam da");
ok(/--kam:\s*calc\([\s\S]*?var\(--kam-gross\)/.test(regeln),
"und der Faktor der Regie geht dort ein");
/* UND GERECHNET WIRD NUR EINMAL. `--kam-grund` darf jede
Anordnung und jede Bildschirmbreite fuer sich setzen -- die
Rechnung daraus steht an einer Stelle. Wer sie ein zweites Mal
hinschreibt, verliert den Faktor der Regie genau dort, wo er
es nicht sieht. Am 28.09. war das der Handy-Block. */
const rechnungen = [...regeln.matchAll(/--kam:\s*/g)];
ok(rechnungen.length === 1,
`die Breite wird ${rechnungen.length}x gerechnet (soll: 1x)`);
/* ---- EIN DECKEL, DER NICHT VOM FAKTOR ABHAENGT ----
Bei zweifacher Groesse auf einem schmalen Bildschirm deckte ein
einzelnes Fenster sonst die halbe Leinwand zu. Die Messung
nebenan prueft das am Bild; hier steht, dass es ueberhaupt
einen Deckel gibt. */
const stapel = regeln.match(/\.kameras \{[^}]*\}/);
ok(!!stapel && /max-width:\s*\d+%/.test(stapel[0]),
"der Stapel hat einen Deckel in Prozent der Leinwand");
/* ---- DIE REGIE BAUT AUS DER SERVERLISTE ---- */
ok(/lage\.kamera_groessen/.test(js) && /lage\.kamera_ecken/.test(js),
"die Regie baut ihre Knoepfe aus der Liste des Servers");
ok(!/\[\s*0\.6\s*,\s*0\.8\s*,\s*1\s*,/.test(js),
"Gegenprobe: sie haelt keine zweite Liste vor");
/* ---- EIN KNOPF, DER NICHTS TUT, WIRD NICHT ANGEBOTEN ----
Die Ecke wirkt nur bei „Video gross"; sonst stehen die Fenster
mittig oder fuellen die Flaeche. */
ok(/k\.disabled = !eckeGilt/.test(js),
"ausserhalb von „Video gross“ ist die Ecke nicht zu stellen");
/* UND DIE GROESSE BEI „NUR VIDEO“: Dort gibt es gar keine
Kamerafenster. Wer dann auf 2x drueckt, sieht nichts passieren
und sucht den Fehler bei sich. */
ok(/k\.disabled = !kamerasDa/.test(js),
"und bei „Nur Video“ ist auch die Groesse nicht zu stellen");
ok(/kamecke-hinweis/.test(js), "und die Regie sagt auch, warum");
/* ---- DER RUNDRUF BRINGT DAS GANZE BILD ----
Sonst saehen die Zusehenden die neue Anordnung mit der alten
Ecke, bis zufaellig etwas anderes den ganzen Stand nachzieht. */
ok(/was === 'layout'/.test(js) && /d\.kamera_gross/.test(js) && /d\.kamera_ecke/.test(js),
"die Meldung „layout“ bringt Anordnung, Groesse und Ecke");
/* ---- UND DIE ABLEHNUNG HAT EINEN DEUTSCHEN SATZ ----
Ein Fehlerkuerzel auf dem Bildschirm ist keine Antwort. */
for (const wort of ["kameragroesse_unklar", "ecke_unklar", "nichts_zu_tun"]) {
ok(mld.includes(wort), `„${wort}" hat einen deutschen Satz`);
}
}
}
melde("");
melde(`${geprueft} Pruefungen, ${fehler} Fehler`);
melde(fehler === 0 ? "ALLES IN ORDNUNG" : "NICHT IN ORDNUNG");