Files
dogfather-universe/workspace/assets/js/spendenkarte.js
T
DogFatherGitandClaude Opus 5 96bdd79a91 Die Dogen-Muenzen: drei Saetze zu je drei Stufen
Filipe, 28.09.2026: "ich werde die drei varianten schicken fuer
niedrige spenden. mittlere und hohe spenden. alle drei varianten will
ich drauf so dass ich sie aussuchen kann wie ich will. aber pass sie
sofort diesen 3 kategorien an."

DIE ZUORDNUNG IST GELESEN, NICHT GERATEN

In allen drei Saetzen geht es nacktes Metall -> Steinkranz ->
Vollbesatz; Filipes Dateinummern (01/02/03) und die Uhrzeiten seiner
Entwuerfe laufen genau mit. Klassik: Palladium, Gold mit Saphir,
Diamant. Amethyst: Stahl, Rosegold, Vollbesatz. Neon: Chrom auf
Schwarz, Steinkranz, Vollbesatz.

AUS 25 MB WURDEN 443 KB

Je Bild 320 px WebP statt 1254 px PNG, Faktor 58. Die Zahl ist
gemessen: Groesster Fall in der Seite 110 px (Karte, hoechste Stufe),
auf der OBS-Tafel 189 px, bei doppelter Bildschirmdichte rund 220.
Drei Megabyte fuer ein 24-Pixel-Zeichen waeren ein Ladebalken mitten
in der Sendung -- wer auf dem Handy zusieht, bekaeme die Karte, wenn
sie schon wieder weg ist. Die Originale liegen neben den
Datenbanksicherungen, nicht im Repo: 26 MB, die bei jedem Klon
mitkaemen und die niemand ausliefert.

WELCHE MUENZE WANN -- ABGELEITET STATT GEPFLEGT

"Niedrig, mittel, hoch" gibt es im Haus schon: die Stufenleiter. Zwei
eigene Grenzen daneben waeren eine zweite Antwort auf dieselbe Frage,
und spaetestens beim Verschieben einer Stufe zeigte die Muenze etwas
anderes an als der Name auf der Karte. Die Leiter wird deshalb in
DRITTEL geteilt -- bei den drei Werksstufen genau eine je Muenze, bei
sechs zwei je Muenze, bei einer einzigen ueberall die mittlere.

DIE MUENZE STEHT AUCH GROSS AUF DER KARTE

Sonst haette Filipe neun Bilder fuer ein 24-Pixel-Zeichen gezeichnet.
"dogen" ist dafuer eine neue Vorlage neben Herz, Welle und Krone --
und die drei Werksstufen bekommen sie EINMAL zugeteilt, nur wo noch
die Werksvorlage steht und kein eigenes Bild hochgeladen ist. Wer
danach ein Herz zurueckstellt, findet es morgen nicht wieder als
Muenze vor: Eine Einstellung, die sich gegen den Benutzer durchsetzt,
wird abgeschafft.

Und steht sie gross da, nimmt das Stilblatt die kleine neben der Zahl
weg -- dieselbe Muenze in zwei Groessen auf einer Karte sieht aus wie
ein Versehen.

AM BILDSCHIRMFOTO NACHGEBESSERT

Bei 0,92em blieb an den Spendenknoepfen ein 13,5-Pixel-Fleck uebrig;
bei einem flachen Symbol reicht das, bei einer Muenze mit Pfote,
Schriftzug und Steinen nicht. Jetzt 1,05em ueberall und 1,6em auf den
Knoepfen. In der Auswahl sahen "Amethyst" und "Neon" bei 40 px
praktisch gleich aus -- und genau sie auseinanderzuhalten ist der
Zweck dieser Liste. Jetzt 56/64/72 px. Und was gewaehlt ist, steht
als WORT da: Neben neun glaenzenden Muenzen geht ein ruhiger Rahmen
unter, und wer Farben schlecht unterscheidet, sieht ihn gar nicht.

EIN SATZWECHSEL ERREICHT ALLE

Die Karten trugen ihren Satz immer selbst mit und waren richtig. Die
Muenzen an den Spendenknoepfen und beim eigenen Stand aber nicht --
die holt eine Seite nur bei einer Spende neu. Eine Einstellung, die
nur dort ankommt, wo sie gemacht wurde, ist keine Einstellung des
Hauses.

ZWEI BEFUNDE GEGEN DIE PRUEFUNG SELBST

1. Sie verlangte "erste Grenze ECHT kleiner als zweite". Bei genau
   zwei Stufen fallen beide absichtlich zusammen -- sie meldete einen
   Fehler, wo das Verhalten richtig ist.
2. Sie prueft die Muenzverteilung jetzt auf einer FRISCHEN Datenbank.
   Vorher lief sie auf der, die ein frueherer Abschnitt umgebaut
   hatte: Eine Pruefung, die den eigenen Kollateralschaden misst,
   misst sich selbst. Dazu eine Gegenprobe, dass eine selbst
   gewaehlte Vorlage NICHT ueberschrieben wird.

UND DREI AN DER MESSUNG

Sie suchte noch die gezeichnete Muenze (svg), wo jetzt ein Bild
liegt. Umgestellt nicht auf "ist ein img da", sondern auf
`naturalWidth > 0` -- ob es etwas ZEIGT. Ein img, das 404 antwortet,
steht genauso im Dokument wie eines, das laedt; auf dem Bildschirm
ist an seiner Stelle nichts. Und beim Satzwechsel las sie die vorige
Karte, die noch acht Sekunden stand: Die Karten laufen in einer
Schlange, also wird auf das Merkmal gewartet, das nur die neue hat.

pruef-spenden 167/0 (vorher 136) - pruef-reaktion 321/0 -
pruef-css-klassen ok - pruef-tippziele 11/0 -
mess-reaktion und mess-buehne ohne Beanstandung

Co-Authored-By: Claude Opus 5 <[email protected]>
2026-09-28 19:36:05 +02:00

297 lines
12 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('.', ',');
}
}
/** „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,
/* „dogen" ist keine Zeichnung, aber eine gueltige Vorlage --
deshalb steht sie hier dabei und nicht in `VORLAGEN`. */
VORLAGEN: ['dogen', ...Object.keys(VORLAGEN)],
};
})();