From 3fedeea71601a7dcb741a1abee83729f520f948f Mon Sep 17 00:00:00 2001 From: Dogfather Date: Mon, 28 Sep 2026 14:35:02 +0200 Subject: [PATCH] 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 --- server/mess-reaktion.mjs | 167 +++++++++++++++++++++++ server/pruef-buehne.mjs | 5 +- server/pruef-reaktion.mjs | 216 ++++++++++++++++++++++++++++++ server/reaktion-tabellen.js | 36 +++++ server/workspace-buehne.js | 5 + server/workspace-reaktion.js | 57 +++++++- workspace/anruf-probe.html | 10 +- workspace/app.webmanifest | 8 +- workspace/assets/css/reaktion.css | 212 +++++++++++++++++++++++++++-- workspace/assets/js/buehne.js | 29 +++- workspace/assets/js/meldung.js | 4 + workspace/assets/js/reaktion.js | 132 +++++++++++++++++- workspace/aufgaben.html | 38 +++--- workspace/automation.html | 32 ++--- workspace/befinden.html | 34 ++--- workspace/bereich.html | 44 +++--- workspace/bewerben.html | 38 +++--- workspace/bewerbungen.html | 36 ++--- workspace/buehne.html | 14 +- workspace/calls.html | 34 ++--- workspace/chat.html | 40 +++--- workspace/content.html | 34 ++--- workspace/crew-index.html | 12 +- workspace/crew.webmanifest | 8 +- workspace/dateien.html | 34 ++--- workspace/entwicklung.html | 36 ++--- workspace/hilfe.html | 30 ++--- workspace/index.html | 10 +- workspace/kalender.html | 34 ++--- workspace/leistung.html | 32 ++--- workspace/material.html | 32 ++--- workspace/notizen.html | 28 ++-- workspace/personen.html | 32 ++--- workspace/profil.html | 36 ++--- workspace/reaktion.html | 57 +++++--- workspace/rechte.html | 34 ++--- workspace/report.html | 36 ++--- workspace/scouting.html | 32 ++--- workspace/start.html | 32 ++--- workspace/startcheck.html | 34 ++--- workspace/steckbrief.html | 36 ++--- workspace/support.html | 28 ++-- workspace/tafel.html | 6 +- workspace/talente.html | 36 ++--- workspace/team.html | 32 ++--- workspace/teamlage.html | 34 ++--- workspace/teilen.html | 30 ++--- workspace/treff-moderation.html | 32 ++--- workspace/treff-regeln.html | 32 ++--- workspace/uebersicht.html | 32 ++--- workspace/unsere-seiten.html | 30 ++--- workspace/unterstuetzen.html | 32 ++--- workspace/werdegang.html | 32 ++--- workspace/willkommen.html | 30 ++--- workspace/wissen.html | 34 ++--- 55 files changed, 1537 insertions(+), 693 deletions(-) diff --git a/server/mess-reaktion.mjs b/server/mess-reaktion.mjs index ceec9ca6..389751de 100644 --- a/server/mess-reaktion.mjs +++ b/server/mess-reaktion.mjs @@ -685,6 +685,173 @@ if (zuschauer.konsole.length) console.log(" KONSOLE:", zuschauer.konsole.slice Abzeichen UND ein zweiter Knopf dazugekommen. Genau so ist am 28.09. die Regie 197 px uebergelaufen: Die Rechnung ging auf, die Wirklichkeit nicht. */ +/* ---- Groesse und Ecke der Kamerafenster ------------------------- + + Filipe: „und danach perfektionierst du auch die verschiedenen + groessen von kamera und so." + + GEMESSEN WIRD BEIM ZUSCHAUER. Dass in der Datenbank 1,5 steht, + sagt nichts darueber, ob das Fenster bei den anderen groesser + wird -- dazwischen liegen ein Rundruf, eine Variable im Stilblatt + und eine Rechnung mit `clamp`. */ +console.log(""); +console.log(" --- Kameragroesse und Ecke ---"); +{ + /* Sauberer Anfang: „Video gross", damit die Ecke ueberhaupt gilt. */ + await roh("/workspace/api/reaktion/layout", { + method: "POST", keks: k.admin, rumpf: { layout: "kino" }, + }); + await zuschauer.seite.waitForTimeout(900); + + const messen = () => zuschauer.seite.evaluate(() => { + const kino = document.getElementById("teil-live"); + const kk = document.querySelector(".kameras"); + const ein = document.querySelector(".kamera"); + const lein = document.querySelector(".leinwand"); + if (!kk || !ein || !lein) return null; + const r = ein.getBoundingClientRect(); + const lr = lein.getBoundingClientRect(); + const fenster = [...document.querySelectorAll(".kamera")]; + return { + gross: kino.style.getPropertyValue("--kam-gross") || "(?)", + ecke: kino.dataset.ecke || "(?)", + breit: Math.round(r.width), + /* Wo sitzt der Stapel? Als Abstand zu den vier Kanten der + Leinwand -- daran erkennt man die Ecke ohne Vorwissen. */ + links: Math.round(kk.getBoundingClientRect().left - lr.left), + rechts: Math.round(lr.right - kk.getBoundingClientRect().right), + oben: Math.round(kk.getBoundingClientRect().top - lr.top), + unten: Math.round(lr.bottom - kk.getBoundingClientRect().bottom), + /* Und liegt noch alles IN der Leinwand? Sie hat + `overflow: hidden` -- was daneben liegt, ist unsichtbar, + nicht abgeschnitten sichtbar. */ + drin: fenster.filter((f) => { + const q = f.getBoundingClientRect(); + return q.left >= lr.left - 1 && q.right <= lr.right + 1 + && q.top >= lr.top - 1 && q.bottom <= lr.bottom + 1; + }).length, + fenster: fenster.length, + leinwand: Math.round(lr.width), + }; + }); + + const normal = await messen(); + console.log(` 1×: Fenster ${normal?.breit} px, Ecke ${normal?.ecke}, ` + + `Abstände l/r/o/u ${normal?.links}/${normal?.rechts}/${normal?.oben}/${normal?.unten}, ` + + `${normal?.drin}/${normal?.fenster} im Bild`); + + /* ---- DIE GROESSE MUSS WIRKEN ---- */ + const { KAMERA_GROESSEN } = await import("./reaktion-tabellen.js"); + const gross = KAMERA_GROESSEN[KAMERA_GROESSEN.length - 1]; + await roh("/workspace/api/reaktion/layout", { + method: "POST", keks: k.admin, rumpf: { kamera_gross: gross }, + }); + await zuschauer.seite.waitForTimeout(900); + const dick = await messen(); + console.log(` ${gross}×: Fenster ${dick?.breit} px, ` + + `${dick?.drin}/${dick?.fenster} im Bild (Leinwand ${dick?.leinwand} px)`); + + if (!normal || !dick) { + console.log(" ACHTUNG: Die Kamerafenster liessen sich nicht messen."); + ausgang = 1; + } else { + /* NICHT AUF DEN FAKTOR GENAU. `clamp` deckelt die Grundbreite, + und `max-width: 46%` deckelt den Stapel -- beides ist Absicht. + Geprueft wird, dass es SPUERBAR waechst und im Bild bleibt. */ + const wuchs = dick.breit / normal.breit; + console.log(` Das Fenster wuchs um das ${wuchs.toFixed(2)}-fache`); + if (wuchs < 1.3) { + console.log(" ACHTUNG: Die Groesse wirkt nicht."); + ausgang = 1; + } + if (dick.drin !== dick.fenster) { + console.log(" ACHTUNG: Ein Fenster liegt ausserhalb der Leinwand."); + ausgang = 1; + } + if (dick.breit > dick.leinwand * 0.5) { + console.log(" ACHTUNG: Ein Fenster deckt mehr als die halbe Leinwand zu."); + ausgang = 1; + } + } + + /* ---- DIE VIER ECKEN ---- + Geprueft wird der ABSTAND zu den Kanten, nicht der Name der + Ecke. Eine Einstellung, die nur ihren eigenen Namen bestaetigt, + bestaetigt nichts. */ + await roh("/workspace/api/reaktion/layout", { + method: "POST", keks: k.admin, rumpf: { kamera_gross: 1 }, + }); + const erwartet = { + ol: ["links", "oben"], or: ["rechts", "oben"], + ul: ["links", "unten"], ur: ["rechts", "unten"], + }; + for (const [ecke, [waag, senk]] of Object.entries(erwartet)) { + await roh("/workspace/api/reaktion/layout", { + method: "POST", keks: k.admin, rumpf: { kamera_ecke: ecke }, + }); + await zuschauer.seite.waitForTimeout(800); + const m = await messen(); + const nahWaag = m[waag] < (waag === "links" ? m.rechts : m.links); + const nahSenk = m[senk] < (senk === "oben" ? m.unten : m.oben); + console.log(` Ecke ${ecke}: l/r/o/u ${m.links}/${m.rechts}/${m.oben}/${m.unten} ` + + `-> ${waag}+${senk} ${nahWaag && nahSenk ? "stimmt" : "STIMMT NICHT"}`); + if (!nahWaag || !nahSenk || m.ecke !== ecke) { + console.log(` ACHTUNG: Die Ecke „${ecke}" kommt nicht an.`); + ausgang = 1; + } + } + await bild(zuschauer.seite, "22-kamera-ecke"); + + /* ---- EIN KNOPF, DER NICHTS TUN KANN, IST GRAU ---- + Die Ecke wirkt nur bei „Video gross"; die Groesse wirkt ueberall, + wo es Kamerafenster GIBT -- bei „Nur Video" gibt es keine. + + GEMESSEN WIRD DER ZUSTAND DER KNOEPFE, nicht der Satz darunter. + Ein Hinweis, der die Wahrheit sagt, waehrend der Knopf daneben + bedienbar bleibt, ist die schlechtere Haelfte: Man liest ihn + nicht und drueckt trotzdem. */ + const knoepfe = () => host.seite.evaluate(() => { + const e = [...document.querySelectorAll("#kamecke .kamecke__knopf")]; + const g = [...document.querySelectorAll("#kamgroesse .kamgroesse__knopf")]; + return { + ecken: e.length, eckenGrau: e.filter((x) => x.disabled).length, + groessen: g.length, groessenGrau: g.filter((x) => x.disabled).length, + hinweis: document.getElementById("kamecke-hinweis")?.textContent || "", + }; + }); + + for (const [anordnung, sollEcken, sollGroessen, wort] of [ + ["kamera", "alle", "keine", "Mitte"], + ["nur_video", "alle", "alle", "Nur Video"], + ["kino", "keine", "keine", "Ecke"], + ]) { + await roh("/workspace/api/reaktion/layout", { + method: "POST", keks: k.admin, rumpf: { layout: anordnung }, + }); + await host.seite.waitForTimeout(800); + const m = await knoepfe(); + console.log(` „${anordnung}": ${m.eckenGrau}/${m.ecken} Ecken grau, ` + + `${m.groessenGrau}/${m.groessen} Größen grau`); + console.log(` Hinweis: „${m.hinweis.replace(/\s+/g, " ").trim()}"`); + const soll = (art, wieviel, da) => (wieviel === "alle" ? art === da : art === 0); + if (m.ecken !== 4 || m.groessen < 4) { + console.log(" ACHTUNG: Die Knoepfe fuer Groesse oder Ecke fehlen."); + ausgang = 1; + } else if (!soll(m.eckenGrau, sollEcken, m.ecken) + || !soll(m.groessenGrau, sollGroessen, m.groessen)) { + console.log(` ACHTUNG: Bei „${anordnung}" stimmt das Grauwerden nicht.`); + ausgang = 1; + } else if (!m.hinweis.includes(wort)) { + console.log(` ACHTUNG: Der Hinweis sagt nichts von „${wort}".`); + ausgang = 1; + } + } + await roh("/workspace/api/reaktion/layout", { + method: "POST", keks: k.admin, rumpf: { layout: "kino", kamera_ecke: "ur" }, + }); + await host.seite.waitForTimeout(700); +} + /* ---- Die Regieleiste ueber dem Saal ----------------------------- Filipe, 28.09.2026: „ich haette das gerne oben also nicht in dieser diff --git a/server/pruef-buehne.mjs b/server/pruef-buehne.mjs index bbe12671..95e5fc26 100644 --- a/server/pruef-buehne.mjs +++ b/server/pruef-buehne.mjs @@ -172,7 +172,10 @@ melde("=== 2. Der Schluessel ==="); wird von Hand gepflegt UND ist absichtlich abschliessend: Sie ist die einzige Stelle, an der auffaellt, wenn jemand dem Stand ein Feld hinzufuegt, das ohne Anmeldung abrufbar waere. */ - const erlaubt = ["gesendet", "laeuft", "sekunde", "stand", "tempo", + /* `layout` kam am 28.09. dazu: Bei „nur Kamera" muss die Quelle + das Bild abschalten, sonst liegt im Stream ein + stehengebliebenes YouTube-Bild unter den Kameras. */ + const erlaubt = ["gesendet", "laeuft", "layout", "sekunde", "stand", "tempo", "titel", "video", "video_titel"]; ok(JSON.stringify(felder) === JSON.stringify(erlaubt), `die Auskunft hat genau die erlaubten Felder (${felder.join(", ")})`); diff --git a/server/pruef-reaktion.mjs b/server/pruef-reaktion.mjs index 89a89176..ebe9ba0d 100644 --- a/server/pruef-reaktion.mjs +++ b/server/pruef-reaktion.mjs @@ -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"); diff --git a/server/reaktion-tabellen.js b/server/reaktion-tabellen.js index 8d3d7938..8af22ec8 100644 --- a/server/reaktion-tabellen.js +++ b/server/reaktion-tabellen.js @@ -93,6 +93,35 @@ export const STAENDE = ["zu", "vorbereitung", "live"]; * nichts doppelt zeigen. */ export const LAYOUTS = ["kino", "gleich", "kamera", "nur_video", "nur_kamera"]; +/** Wie gross die Kamerafenster sind (Faktor auf die Grundgroesse). + * + * Filipe: „die verschiedenen groessen von kamera." + * + * SECHS STUFEN UND KEIN SCHIEBER. Ein Schieber verspricht eine + * stufenlose Einstellung; im Live sucht dann jemand mitten in der + * Sendung nach „dem richtigen Wert" statt zu senden. Sechs Stufen + * trifft man blind. + * + * DIE GRENZEN SIND GEMESSEN. Unter 0,6 ist der Name unter dem Bild + * nicht mehr zu lesen (das Schild steht bei 11,52 px und waechst + * nicht mit); ueber 2 deckt ein einzelnes Fenster bei „Video gross" + * mehr als die Haelfte der Leinwand zu, und dann ist es keine + * Anordnung mehr, sondern die falsche. */ +export const KAMERA_GROESSEN = [0.6, 0.8, 1, 1.25, 1.5, 2]; + +/** In welcher Ecke die Kamerafenster liegen. + * + * NUR BEI „VIDEO GROSS" -- in den anderen Anordnungen stehen sie + * mittig oder fuellen die Flaeche, da gibt es keine Ecke. Genau das + * sagt die Regie auch, statt einen Knopf anzubieten, der nichts + * tut. + * + * WARUM ES DIESE EINSTELLUNG UEBERHAUPT BRAUCHT: Unten rechts liegt + * bei TikTok die Knopfreihe, bei YouTube die Fortschrittsleiste, + * und Untertitel stehen fast immer unten. Eine feste Ecke ist eine, + * die bei jeder zweiten Plattform falsch ist. */ +export const KAMERA_ECKEN = ["ol", "or", "ul", "ur"]; + /** Die Geschwindigkeiten, die es gibt. * * YouTube kann 0,25 bis 2. Die 0,25 fehlt hier mit Absicht: Bei @@ -348,6 +377,13 @@ export function reaktionTabellen(d) { d.exec("ALTER TABLE reaktion_dabei ADD COLUMN mikro_aus INTEGER NOT NULL DEFAULT 0"); console.log("[reaktion] Spalte mikro_aus angelegt."); } + if (!da.has("kamera_gross")) { + /* Groesse und Ecke der Kamerafenster. Beide gelten fuer die + ganze Sendung -- wer sendet, stellt das Bild fuer alle ein. */ + d.exec("ALTER TABLE reaktion ADD COLUMN kamera_gross REAL NOT NULL DEFAULT 1"); + d.exec("ALTER TABLE reaktion ADD COLUMN kamera_ecke TEXT NOT NULL DEFAULT 'ur'"); + console.log("[reaktion] Spalten fuer die Kameragroesse angelegt."); + } if (!da.has("tempo")) { /* Die Geschwindigkeit gilt fuer die ganze Sendung. REAL und nicht INTEGER: 0,75 und 1,25 sind die beiden, die man diff --git a/server/workspace-buehne.js b/server/workspace-buehne.js index 47bdf325..13f57333 100644 --- a/server/workspace-buehne.js +++ b/server/workspace-buehne.js @@ -196,6 +196,11 @@ buehneRouter.get("/workspace/api/buehne/lage", mitSchluessel, (req, res) => { waeren das zweieinhalb Minuten Unterschied, und was Filipe kommentiert, haetten die Zuschauer noch gar nicht gesehen. */ tempo: Number(s?.tempo) || 1, + /* DIE ANORDNUNG GEHOERT AUF DIE BUEHNE. Die Videoquelle zeigt + nur das Video -- aber sie muss wissen, ob es ueberhaupt zu + sehen sein soll: Bei „nur Kamera" darf im Stream kein + stehengebliebenes YouTube-Bild liegen. */ + layout: s?.layout || "kino", gesendet: Date.now(), titel: s?.titel || "", }); diff --git a/server/workspace-reaktion.js b/server/workspace-reaktion.js index b5435569..728c63d0 100644 --- a/server/workspace-reaktion.js +++ b/server/workspace-reaktion.js @@ -48,6 +48,7 @@ import { einstellung } from "./workspace.js"; import { mitZugangsdaten, wegeAusfaechern } from "./workspace-turn.js"; import { SICHT_PLAETZE, GAESTE_MAX, TEXT_MAX, SCHREIB_BREMSE, STAENDE, LAYOUTS, TEMPI, + KAMERA_GROESSEN, KAMERA_ECKEN, MELDUNG_TAGE, VERLAUF_TAGE, LISTE_MAX, CHAT_MODI, STUMM_MAX_MINUTEN, MASSNAHMEN, } from "./reaktion-tabellen.js"; @@ -257,6 +258,14 @@ function lage(person) { nach fuenf Minuten zwei Minuten woanders. */ tempo: Number(s.tempo) || 1, tempi: TEMPI, + /* GROESSE UND ECKE DER KAMERAS GEHEN AN ALLE. Sie sind Teil des + Bildes, nicht der Regie -- wer sie nicht bekaeme, saehe die + Fenster woanders und in einer anderen Groesse als der, der + darueber redet. */ + kamera_gross: Number(s.kamera_gross) || 1, + kamera_ecke: s.kamera_ecke || "ur", + kamera_groessen: KAMERA_GROESSEN, + kamera_ecken: KAMERA_ECKEN, gestartet_am: s.gestartet_am, /* WER GERADE SEIN BILD AUS HAT -- fuer den Host und fuer jeden Gast. Ohne diese Auskunft waere ein abgeschaltetes Bild von @@ -1265,11 +1274,49 @@ reaktionRouter.post("/workspace/api/reaktion/chatmodus", nurHost, express.json({ reaktionRouter.post("/workspace/api/reaktion/layout", nurHost, express.json({ limit: "2kb" }), (req, res) => { try { - const l = String(req.body?.layout ?? ""); - if (!LAYOUTS.includes(l)) return res.status(400).json({ fehler: "layout_unbekannt" }); - db().prepare("UPDATE reaktion SET layout = ? WHERE id = 1").run(l); - melden("reaktion", { was: "layout", layout: l }); - res.json({ ok: true, layout: l }); + /* ==== DREI EINSTELLUNGEN, EIN WEG ========================= + + Anordnung, Groesse und Ecke beantworten zusammen eine + einzige Frage: „Wie sieht das Bild aus?" Drei Wege dafuer + waeren drei Rundrufe, und zwischen ihnen gaebe es + Augenblicke, in denen die Zusehenden eine Mischung sehen -- + neue Anordnung, alte Ecke. + + JEDES FELD IST FREIWILLIG, aber eines muss dabei sein. Ein + Aufruf, der nichts aendert, ist ein Rundruf ohne Anlass. */ + const aendern = []; + const werte = []; + + if (req.body?.layout !== undefined) { + const l = String(req.body.layout); + if (!LAYOUTS.includes(l)) return res.status(400).json({ fehler: "layout_unbekannt" }); + aendern.push("layout = ?"); werte.push(l); + } + if (req.body?.kamera_gross !== undefined) { + const g = Number(req.body.kamera_gross); + if (!KAMERA_GROESSEN.includes(g)) { + return res.status(400).json({ fehler: "kameragroesse_unklar" }); + } + aendern.push("kamera_gross = ?"); werte.push(g); + } + if (req.body?.kamera_ecke !== undefined) { + const e = String(req.body.kamera_ecke); + if (!KAMERA_ECKEN.includes(e)) { + return res.status(400).json({ fehler: "ecke_unklar" }); + } + aendern.push("kamera_ecke = ?"); werte.push(e); + } + if (!aendern.length) return res.status(400).json({ fehler: "nichts_zu_tun" }); + + db().prepare(`UPDATE reaktion SET ${aendern.join(", ")} WHERE id = 1`).run(...werte); + const n = sendung(); + const bild = { + layout: n.layout, + kamera_gross: Number(n.kamera_gross) || 1, + kamera_ecke: n.kamera_ecke || "ur", + }; + melden("reaktion", { was: "layout", ...bild }); + res.json({ ok: true, ...bild }); } catch (fehler) { console.error("[reaktion] Layout:", fehler?.message); res.status(503).json({ fehler: "nicht_verfuegbar" }); diff --git a/workspace/anruf-probe.html b/workspace/anruf-probe.html index f5a5d74f..4598ba7d 100644 --- a/workspace/anruf-probe.html +++ b/workspace/anruf-probe.html @@ -16,8 +16,8 @@ verdeckt. Gefunden hat es pruef-struktur. Richtig sind `gate.css` (die Farbwerte des Hauses) und `haus.css`. --> - - + + - +