/* ===================================================================== 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: '', welle: '' + '', krone: '', stern: '', pfote: '' + '', }; 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 = '' + '' + '' + '' + ''; /** 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)); const tafel = bauen(karte); kasten.append(tafel); /* 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(() => { tafel.dataset.da = 'ja'; })); setTimeout(() => { tafel.dataset.da = 'nein'; setTimeout(() => { tafel.remove(); laeuft = false; naechste(kasten); }, 600); }, dauer); } function bauen(karte) { const farbe = karte.stufe?.farbe || '#8ec9ff'; const tafel = el('div', 'spendenkarte'); 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); tafel.style.setProperty('--sp-gross', String(Number.isFinite(g) && g >= 1 && g <= 2.5 ? g : 1)); 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 tafel; } /** 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)], }; })();