Files
dogfather-universe/workspace/assets/js/spendenkarte.js
T
DogFatherGitandClaude Opus 5 996231af6e Reaction: Material statt Kaesten, Spende in der Mitte, Gaeste werden gefragt
Nach Filipes sieben Bildschirmfotos vom 07.10.2026. Sieben Wuensche,
und einer davon ("alles viel hochwertiger") betrifft alle anderen --
deshalb steht das Fundament zuerst und nicht am Ende.

DAS AUSSEHEN HAT EINEN WERKSTOFF. Jede Flaeche sagt jetzt, ob sie oben
oder unten liegt: Was obenauf liegt, bekommt oben eine helle Kante und
darunter einen Schatten; was vertieft ist, genau umgekehrt. Dazwischen
gibt es nichts mehr. Vorher stand im ganzen Pult zweimal eine
Lichtkante und sonst nirgends -- genau deshalb sahen diese zwei Stellen
gut aus und alles daneben danebengestellt. Dazu: jeder Knopf senkt sich
beim Druecken, jedes Feld ist ein Loch und kein Kasten, und wer mit der
Tastatur bedient, wird zum ersten Mal ueberhaupt gesehen.

UND EIN FUND DABEI: Die drei Kacheln des Regieplatzes hiessen in der
HTML anders als im Stilblatt (kachel--laeuft gegen regiekachel--laeuft,
regieregiekachel__kopf gegen regiekachel__kopf). Die Aufteilung "links
was laeuft, rechts was man eintraegt" hat damit NIE gegriffen -- und
mess-reaktion hat den falschen Zustand gemessen und gruen gemeldet,
weil sie gegen die HTML geschrieben war.

DIE SPENDENKARTE STEHT IN DER MITTE und kommt aus der Tiefe nach vorn.
Das dreht eine Entscheidung vom 28.09. um ("sie deckt nie die Mitte
zu"); die Begruendung war richtig und ist es noch -- sie schuetzt das
Video, und Filipe sieht die Spende als das, was in dem Augenblick
zaehlt. Der alte Platz bleibt einen Knopf weit weg und gilt fuer Saal
UND OBS-Tafel. Die Karte besteht dafuer aus zwei Teilen: `opacity`
unter 1 zwingt ein Element auf `transform-style: flat`, eine Karte mit
Deckkraft-Uebergang waere also genau waehrend des Hereinkommens flach
und spraenge am Ende in einem Bild in die Tiefe.

FERTIGE SAETZE GEHEN SOFORT INS BILD. Der Browser schickt nur die
Kennung, den Text holt der Server -- deshalb darf hier die Bestaetigung
entfallen und beim freien Text nicht. Zwei Schranken bleiben: sofort
nur mit einem der vier Knopfbetraege (Filipes Ansage galt dem SATZ,
nicht der Summe), und GEZEIGT IST NICHT GEBUCHT -- die Dogen gibt es
erst nach dem Tipp der Leitung, sonst stellt sich jeder seine eigene
Waehrung aus.

WER GESPENDET HAT, GLAENZT IM CHAT -- je nach Hoehe. Die Dauer haengt
an der Stufe und nicht an einer zweiten Leiter; 0 heisst "gar nicht".

GAESTE WERDEN GEFRAGT, NICHT GEHOLT. Vorher sprang die Kamera eines
Zuschauers an, sobald jemand "Dazuholen" drueckte -- er hat es daran
gemerkt, dass er sich selbst sah. Jetzt gibt es einen Vorraum: eigenes
Bild, Pegel, Kamera und Mikro vorwaehlen, Geraete waehlen, dann "Dabei
sein" oder "Lieber nicht". Zwei Minuten Frist, und sie verfaellt
SICHTBAR -- eine verschwundene Zeile sieht fuer den Host aus wie ein
Verklicker.

DAS MISCHPULT. Hier stand fest `echoCancellation: true,
noiseSuppression: true`. Fuer ein Laptopmikrofon richtig, ueber einem
RODECaster falsch: Das Pult hat Gate, Kompressor und EQ schon gemacht,
und die Echo-Unterdrueckung legt sich danach als zweite Automatik
darueber und duckt, sobald von der Gegenseite Ton kommt -- genau das
Pumpen vom 14.09. Jetzt: Geraetewahl fuer Mikro, Kamera und Ausgabe
(auch fuer Gaeste), ein Pultmodus, und darunter GEMESSEN, was wirklich
ankommt. Mit und ohne Pult.

DAS ZWEITE VIDEO war nie gesperrt -- es war nur unsichtbar. Jetzt
stehen Vorschaubild, Titel und die gemerkte Stelle da, "Jetzt wechseln"
auch von hier aus, und aus der Warteschlange legt ein Knopf eines
bereit.

UND EIN FUND AUF DEM BILDSCHIRMFOTO: Das Band "Ton einschalten" lag auf
derselben Hoehe wie die Senderleiste. Bei vier Knoepfen reichte die
Leiste bis zur Mitte -- vom Mikro-Knopf war nur noch ein "t" zu sehen.
Das war schon vorher so; der fuenfte Knopf hat es sichtbar gemacht.
Dieselbe Lehre wie am 06.09.: keine neue Zahl suchen, sondern die
Begegnung unmoeglich machen. mess-reaktion misst das jetzt als
UEBERLAPPUNG.

GEPRUEFT: pruef-reaktion 423 -> 478 (0 Fehler), pruef-spenden 172 -> 215
(0), pruef-reaktion-schmal 44 (0), pruef-tippziele 13 (0), dazu gruen:
pruef-css-klassen, pruef-alle-wege, pruef-haus-trennung, pruef-agentur,
pruef-deutsche-texte. Keine Zahl ist gefallen.

Drei Doppelungen hat erst das Pruefen gefunden: `satz_unbekannt` gab es
zweimal (Muenzsatz), `saetze` im Spendenstand war belegt (ebenfalls
Muenzsaetze), und `glanz_minuten` waere in einem frischen Haus immer 0
geblieben -- die Spalte kam per ALTER TABLE, und das laeuft VOR dem
Anlegen der Werksstufen.

Co-Authored-By: Claude Opus 5 <[email protected]>
2026-10-08 01:31:24 +02:00

366 lines
15 KiB
JavaScript

/* =====================================================================
DIE SPENDENKARTE (28.09.2026)
Was im Bild erscheint, wenn jemand etwas gegeben hat: Betrag, Name,
Gruß — und ein Bild dazu.
---------------------------------------------------------------------
WARUM DAS EIN EIGENES STÜCK IST UND NICHT TEIL DER REACTION
Dieselbe Karte läuft an zwei Orten: in der Reaction-Seite, wo alle
zusehen, und später als eigene Seite für OBS und TikTok Studio.
Zweimal gebaut hieße: Bei der nächsten Änderung sieht sie an einem
der beiden Orte anders aus — und man merkt es erst im Livestream.
Deshalb hängt hier nichts an der Reaction. Wer die Karte zeigen
will, gibt ihr einen Kasten und eine Karte, sonst nichts.
---------------------------------------------------------------------
EINE NACH DER ANDEREN
Zwei Spenden gleichzeitig sind keine Seltenheit — wenn es im Live
gut läuft, kommen drei in zehn Sekunden. Übereinander gelegt wäre
keine mehr lesbar, und ausgerechnet die größte ginge unter. Also
eine Schlange: Was ankommt, wartet, bis die vorige durch ist.
DIE SCHLANGE HAT EINE GRENZE. Bei zwanzig wartenden Karten stünde
die Anzeige zwei Minuten hinter dem Geschehen; wer dann noch
spendet, sieht seine Karte, wenn die Sendung vorbei ist. Ab dann
werden die Wartezeiten gekürzt statt die Karten weggeworfen — jede
kommt dran, nur schneller.
===================================================================== */
(() => {
'use strict';
const el = (art, klasse, text) => {
const n = document.createElement(art);
if (klasse) n.className = klasse;
if (text !== undefined) n.textContent = text;
return n;
};
/* Die mitgelieferten Vorlagen. Gezeichnet und nicht als Bilddatei:
Sie müssen in jeder Farbe funktionieren (die Stufe bringt ihre
eigene mit), und eine Zeichnung nimmt jede an. */
const VORLAGEN = {
herz: '<path d="M32 56S8 40.5 8 24.5A12.5 12.5 0 0 1 32 18a12.5 12.5 0 0 1 24 6.5C56 40.5 32 56 32 56z"/>',
welle: '<path d="M4 40c7-14 14-14 21 0s14 14 21 0 14-14 18-7" fill="none" stroke-width="5" stroke-linecap="round"/>'
+ '<path d="M4 22c7-14 14-14 21 0s14 14 21 0 14-14 18-7" fill="none" stroke-width="3.5" stroke-linecap="round" opacity=".45"/>',
krone: '<path d="M8 46h48l4-28-14 10-14-18-14 18-14-10z"/><rect x="8" y="50" width="48" height="6" rx="3"/>',
stern: '<path d="M32 6l7.6 16.4L57 24.6 44.5 37l3.1 17.6L32 46.3 16.4 54.6 19.5 37 7 24.6l17.4-2.2z"/>',
pfote: '<circle cx="18" cy="22" r="7"/><circle cx="32" cy="16" r="7.5"/><circle cx="46" cy="22" r="7"/>'
+ '<path d="M32 30c9 0 15 7 15 13s-6 9-15 9-15-3-15-9 6-13 15-13z"/>',
};
function zeichenBauen(name, farbe) {
const inhalt = VORLAGEN[name] || VORLAGEN.herz;
const svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
svg.setAttribute('viewBox', '0 0 64 64');
svg.setAttribute('aria-hidden', 'true');
svg.setAttribute('fill', farbe);
svg.setAttribute('stroke', farbe);
svg.innerHTML = inhalt;
return svg;
}
/* ==== DAS DOGEN-ZEICHEN ==========================================
Filipe: „die dogen auch mit dem symbol, die symbole schick ich
dir später."
Bis sein Symbol da ist, steht hier eine gezeichnete Münze mit
einer Pfote darin. GEZEICHNET UND NICHT ALS BILDDATEI, aus dem
gleichen Grund wie bei den Stufenzeichen: Sie muss in jeder
Farbe funktionieren und in jeder Größe scharf bleiben.
Ein leerer Platzhalter wäre die schlechtere Zwischenlösung —
man hält ihn für kaputt und sucht den Fehler. */
const ZEICHEN = '<circle cx="32" cy="32" r="27" fill="none" stroke-width="4"/>'
+ '<circle cx="23" cy="25" r="4" stroke="none"/>'
+ '<circle cx="32.5" cy="21.5" r="4.5" stroke="none"/>'
+ '<circle cx="42" cy="25" r="4" stroke="none"/>'
+ '<path stroke="none" d="M32.5 30c5.5 0 9.5 4.4 9.5 8.2s-4 5.3-9.5 5.3'
+ '-9.5-1.5-9.5-5.3S27 30 32.5 30z"/>';
/** Die Zahl in Dogen — ohne Wort, ohne Zeichen.
*
* ==== GERECHNET WIRD IN TAUSENDSTELN, FORMATIERT ERST HIER
*
* Diese Datei kennt den Kurs NICHT und soll ihn nicht kennen. Was
* hereinkommt, sind Tausendstel Dogen; der Euro dahinter bleibt
* auf dem Server. So kann hier gar nicht versehentlich Geld
* erscheinen — Filipe: „es soll auch nie geld da stehen."
*
* HÖCHSTENS ZWEI NACHKOMMASTELLEN. Drei wären beim heutigen Kurs
* exakt, aber „0,155 Dogen" liest niemand in sechs Sekunden. Der
* PUNKTESTAND wird trotzdem aus den genauen Tausendsteln gerechnet
* und nicht aus diesen gerundeten Zahlen — sonst liefe die Summe
* mit jeder Spende ein Stück von den Posten weg. */
function alsDogen(mille) {
const m = Math.round(Number(mille) || 0);
try {
return new Intl.NumberFormat('de-DE', { maximumFractionDigits: 2 }).format(m / 1000);
} catch {
return String(m / 1000).replace('.', ',');
}
}
/** Cent als Geld.
*
* ==== WARUM ES DIESE FUNKTION WIEDER GIBT
*
* Am 28.09. flog sie raus: „es soll auch nie geld da stehen
* sondern Dogen." Das war richtig für alles, was ANZEIGE ist --
* Karte, Stream, Chat, Punktestand. Es war falsch für die eine
* Stelle, an der jemand KAUFT: Wer auf „0,2 Dogen" drückt, muss
* vorher wissen, dass ihn das zwei Euro kostet.
*
* Sie formatiert nur. Ob überhaupt ein Betrag da ist, entscheidet
* der Server -- ein Zuschauer bekommt außerhalb der Kaufknöpfe
* keinen. */
function alsGeld(cent) {
const c = Math.round(Number(cent) || 0);
try {
return new Intl.NumberFormat('de-DE', {
style: 'currency', currency: 'EUR',
minimumFractionDigits: c % 100 === 0 ? 0 : 2,
}).format(c / 100);
} catch {
return (c / 100).toFixed(2).replace('.', ',') + ' €';
}
}
/** „Doge" oder „Dogen" — die Einzahl gibt es nur bei genau einem. */
function dogenWort(mille) {
return Math.abs(Math.round(Number(mille) || 0)) === 1000 ? 'Doge' : 'Dogen';
}
/** Welche Münze zu dieser Zahl gehört.
*
* ==== DIE GRENZEN KOMMEN VOM SERVER, NICHT VON HIER
*
* Filipe: „drei varianten für niedrige spenden. mittlere und hohe
* spenden." Wo „niedrig" aufhört, steht in der Stufenleiter — und
* die kann er ändern. Zwei Zahlen in dieser Datei wären eine
* zweite Antwort darauf, und sie wäre nach der ersten Änderung
* falsch, ohne dass jemand etwas merkt.
*
* Ein fehlender Satz gibt leer zurück, und dann tritt die
* gezeichnete Münze ein. Ein kaputtes Bild neben einer Zahl sieht
* aus wie ein Fehler IN der Zahl. */
function muenzeFuer(satz, mille) {
if (!satz || typeof satz !== 'object') return '';
const g = Array.isArray(satz.grenzen) && satz.grenzen.length === 2
? satz.grenzen : [0, Number.MAX_SAFE_INTEGER];
const m = Math.round(Number(mille) || 0);
if (m < g[0]) return satz.niedrig || '';
if (m < g[1]) return satz.mittel || '';
return satz.hoch || '';
}
/** Zeichen + Zahl + Wort als ein Stück.
*
* AN EINER STELLE GEBAUT. Die Zahl steht auf der Karte, in zwei
* Listen im Pult, an den Spendenknöpfen und beim eigenen Stand.
* Fünfmal zusammengesetzt hieße: Beim nächsten Mal sieht eine
* davon anders aus, und gemerkt wird es im Livestream. */
function dogenStueck(mille, satz, klasse) {
const k = el('span', 'dogen' + (klasse ? ' ' + klasse : ''));
const quelle = muenzeFuer(satz, mille);
if (quelle) {
const b = document.createElement('img');
b.className = 'dogen__zeichen';
b.src = quelle;
b.alt = '';
/* Lädt das hochgeladene Zeichen nicht, tritt die Zeichnung
wieder ein. Ein fehlendes Bild neben einer Zahl sieht aus
wie ein Fehler in der Zahl. */
b.addEventListener('error', () => { b.replaceWith(gezeichnet()); });
k.append(b);
} else {
k.append(gezeichnet());
}
k.append(el('span', 'dogen__zahl', alsDogen(mille)));
k.append(el('span', 'dogen__wort', dogenWort(mille)));
return k;
}
function gezeichnet() {
const svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
svg.setAttribute('viewBox', '0 0 64 64');
svg.setAttribute('aria-hidden', 'true');
svg.setAttribute('class', 'dogen__zeichen');
svg.setAttribute('fill', 'currentColor');
svg.setAttribute('stroke', 'currentColor');
svg.innerHTML = ZEICHEN;
return svg;
}
const schlange = [];
let laeuft = false;
function naechste(kasten) {
if (laeuft || !schlange.length) return;
laeuft = true;
const karte = schlange.shift();
/* BEI VOLLER SCHLANGE WIRD GEKUERZT, NICHT WEGGEWORFEN. Wer
gespendet hat, soll seine Karte sehen -- notfalls kuerzer. */
const eile = Math.min(1, 4 / Math.max(1, schlange.length + 1));
const dauer = Math.max(2200, Math.round((karte.stufe?.dauer_ms || 7000) * eile));
/* ==== DER PLATZ STEHT AM KASTEN, NICHT AN DER KARTE ==========
(08.10.2026)
Er entscheidet die ANORDNUNG -- also etwas, das der Kasten tut
und nicht sein Inhalt. An der Karte stuende dieselbe Auskunft
einmal je Karte, und zwei gleichzeitig angezeigte koennten
sich widersprechen.
Eine unbekannte Angabe faellt auf „mitte" zurueck: Das ist der
Platz, den Filipe bestellt hat, und ein leerer Wert kommt nur
von einer aelteren Fassung des Servers. */
const platz = karte.platz === 'rand' ? 'rand' : 'mitte';
/* AUSSER JEMAND HAT ES FUER DIESE QUELLE AUSDRUECKLICH GESETZT.
Die Spendentafel in OBS kennt `?pos=` -- dort liegt im Stream
oft noch etwas anderes im Bild. Was in der Adresse steht, ist
eine Entscheidung fuer DIESE Szene und muss schwerer wiegen
als die Hauseinstellung; sonst zoege sie bei der naechsten
Spende zurueck, und niemand wuesste, warum. */
if (kasten.dataset.posFest !== 'ja' && kasten.dataset.platz !== platz) {
kasten.dataset.platz = platz;
}
const huelle = bauen(karte);
kasten.append(huelle);
/* Ein Bild in derselben Umdrehung anzuhaengen und gleich zu
bewegen, ueberspringt der Browser -- er fasst beides zusammen.
Ein Bildaufbau dazwischen, dann laeuft es. */
requestAnimationFrame(() => requestAnimationFrame(() => {
huelle.dataset.da = 'ja';
}));
setTimeout(() => {
huelle.dataset.da = 'nein';
setTimeout(() => {
huelle.remove();
laeuft = false;
naechste(kasten);
}, 600);
}, dauer);
}
/** Baut die Huelle und die Karte darin.
*
* ==== WARUM ZWEI ELEMENTE UND NICHT EINS (08.10.2026)
*
* Die Huelle macht das ERSCHEINEN (Deckkraft und Flug aus der
* Tiefe), die Karte darin macht die FORM (Wiegen, Glanz).
*
* Das ist kein Umweg: `opacity` unter 1 zwingt ein Element laut
* CSS-Transforms-2 auf `transform-style: flat`. Eine Karte, die
* beim Hereinkommen ihre Deckkraft aendert, ist in genau dieser
* Zeit flach -- und springt in dem einen Bild, in dem die
* Deckkraft 1 erreicht, in die Tiefe. Dieses Springen sieht man.
* Mit zwei Elementen setzen sich beide Umformungen durch die
* Verschachtelung zusammen, statt einander zu ueberschreiben.
*
* Dass die Werte (`--sp-farbe`, `--sp-gross`) an der HUELLE
* stehen, ist Absicht: Sie werden vererbt, und damit kann auch
* die Huelle sie benutzen, ohne dass sie zweimal gesetzt werden. */
function bauen(karte) {
const farbe = karte.stufe?.farbe || '#8ec9ff';
const huelle = el('div', 'spendenkarte-auftritt');
const tafel = el('div', 'spendenkarte');
huelle.append(tafel);
huelle.style.setProperty('--sp-farbe', farbe);
tafel.style.setProperty('--sp-farbe', farbe);
/* DIE GROESSE KOMMT VON DER STUFE. Genau das ist „spezielle
sachen bei speziellen spenden": Eine Rudel-Legende darf
größer dastehen als ein Danke.
BEGRENZT, obwohl der Server schon prüft -- diese Datei läuft
auch auf der OBS-Tafel, und dort kommt der Wert über den
Bühnenstrom. Eine Zahl, die aus der Leitung kommt, wird nicht
ungeprüft in ein Stilblatt geschrieben. */
const g = Number(karte.stufe?.groesse);
const gross = String(Number.isFinite(g) && g >= 1 && g <= 2.5 ? g : 1);
huelle.style.setProperty('--sp-gross', gross);
tafel.style.setProperty('--sp-gross', gross);
tafel.dataset.stufe = karte.stufe?.name || '';
tafel.setAttribute('role', 'status');
/* ==== WAS GROSS AUF DER KARTE STEHT ==========================
Drei Möglichkeiten, in dieser Reihenfolge:
1. Ein hochgeladenes Bild der Stufe. Wer eines hochlädt,
meint es.
2. Die DOGEN-MÜNZE in der Stufe, die zur Zahl passt —
Filipes drei Varianten. Das ist ab dem 28.09. die
Vorgabe der drei Werksstufen.
3. Eine der gezeichneten Vorlagen (Herz, Welle, Krone …).
Und jede Stufe fällt auf die nächste zurück, wenn sie nicht
lädt. Ein leerer Kasten wäre das Schlimmste von allen. */
const bild = el('div', 'spendenkarte__bild');
const muenze = karte.stufe?.vorlage === 'dogen'
? muenzeFuer(karte.zeichen, karte.dogen_mille) : '';
const alsBild = (quelle, ersatz) => {
const b = document.createElement('img');
b.src = quelle;
b.alt = '';
b.addEventListener('error', () => { b.remove(); ersatz(); });
bild.append(b);
};
if (karte.stufe?.bild) {
alsBild(karte.stufe.bild, () => bild.append(zeichenBauen(karte.stufe?.vorlage, farbe)));
} else if (muenze) {
/* DIE MÜNZE STEHT DANN SCHON GROSS DA. Neben der Zahl ein
zweites Mal wäre sie einmal zu viel -- dieselbe Münze in
zwei Größen auf derselben Karte sieht aus wie ein
Versehen. Das Stilblatt nimmt die kleine daraufhin weg. */
tafel.dataset.muenze = 'ja';
alsBild(muenze, () => {
delete tafel.dataset.muenze;
bild.append(zeichenBauen('herz', farbe));
});
} else {
bild.append(zeichenBauen(karte.stufe?.vorlage, farbe));
}
tafel.append(bild);
const text = el('div', 'spendenkarte__text');
text.append(el('span', 'spendenkarte__wer', karte.name || 'Jemand'));
const betrag = el('strong', 'spendenkarte__betrag');
betrag.append(dogenStueck(karte.dogen_mille, karte.zeichen));
text.append(betrag);
if (karte.stufe?.name) {
text.append(el('span', 'spendenkarte__stufe', karte.stufe.name));
}
if (karte.gruss) text.append(el('p', 'spendenkarte__gruss', karte.gruss));
tafel.append(text);
return huelle;
}
/** Eine Karte zeigen. Kasten und Karte, sonst nichts. */
function zeigen(kasten, karte) {
if (!kasten || !karte) return;
/* DIESELBE KARTE NICHT ZWEIMAL. Der Ereignisstrom wird bei einer
Stoerung wiederholt; ohne diese Sperre liefe eine Spende
doppelt, und zwar ausgerechnet dann, wenn das Netz hakt. */
if (schlange.some((k) => k.id && k.id === karte.id)) return;
schlange.push(karte);
/* HOECHSTENS ZWANZIG WARTEN. Darueber ist die Anzeige nicht mehr
Begleitung, sondern Rueckstand. */
while (schlange.length > 20) schlange.shift();
naechste(kasten);
}
window.Spendenkarte = {
zeigen, alsDogen, dogenWort, dogenStueck, muenzeFuer, alsGeld,
/* „dogen" ist keine Zeichnung, aber eine gueltige Vorlage --
deshalb steht sie hier dabei und nicht in `VORLAGEN`. */
VORLAGEN: ['dogen', ...Object.keys(VORLAGEN)],
};
})();