Overlay: fuenf Browser-Sources fuer OBS
Einblendungen, Chat, Statusleiste, Effekte, Pausenbild. Kein Build, kein Framework - OBS laedt die Dateien direkt, und jede Zwischenstufe waere ein Ding, das mitten im Stream kaputtgehen kann. Server-Sent Events statt WebSocket, entgegen der Skizze in chat-control.md: Node bringt einen WebSocket-*Client* mit, aber keinen Server, `EventSource` verbindet sich von allein neu, und die Richtung stimmt - das Overlay hoert nur zu. Gegen die tote Browser-Source nach einem Neustart stehen drei Dinge: der eingebaute Reconnect, ein Herzschlag als echtes Ereignis (ein SSE-Kommentar haelt die Verbindung offen, ist im Browser aber unsichtbar), und ein Zustands-Schnappschuss beim Verbinden. Abos, Raids, Bits und Loeschungen kommen ueber IRC-USERNOTICE und CLEARCHAT herein; das wurde bisher weggeworfen. Follows fehlen weiter, die gibt es nur ueber EventSub. GIFs im Chat: Twitch schickt dafuer einen Tag `gifs`, den wir nicht kannten. gifs=0-22|Pnd7qLFuGCOntZcNKB|https://media0.giphy.com/media/.../giphy.gif PRIVMSG #abdulive_ :[Idk GIF by Respective] Der Nachrichtentext ist nur ein Platzhalter, der Bereich markiert ihn. Ausgewertet wird er jetzt; ohne das stand woertlich "[Idk GIF by Respective]" im Bild. Gefunden wurde der Tag durch einen Rohmitschnitt - in 3094 Nachrichten aus acht grossen Kanaelen war kein einziges GIF, weil die Adresse eben nicht im Text steht. Zwei Fehler, die beim Durchklicken im Browser aufgefallen sind: - Die Aufraeumfunktion im Chat zaehlte Zeilen, die erst nach der Ausblend-Animation aus dem DOM verschwinden, und lief in eine Endlosschleife. In OBS waere die Quelle beim vierzehnten Chatbeitrag eingefroren. - Der Bereichsparser der GIFs war zu nachsichtig: `-1-4` zerfaellt beim Trennen in ['', '1', '4'], und `Number('')` ist 0 - der Bereich saehe gueltig aus und wuerde den falschen Textteil ersetzen. Auf der Uebersichtsseite steht fuer jede Einblendung, jede Chat-Variante und jeden Effekt ein Knopf. Ohne den liesse sich eine Raid-Einblendung nur pruefen, indem man auf einen echten Raid wartet. Was einen Zustand setzt, ist waehrend eines Streams gesperrt; "Stand zuruecksetzen" ausdruecklich nicht, sonst kaeme man aus einem erfundenen Zustand nicht mehr heraus. build-package.ts nimmt jetzt auch html/css/js mit - sonst liefert das Paket 404 statt Overlay. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
1 parent
3b606e36fb
commit
bc2c8f6cbe
24 files changed
+3221
-8
No files matched your search
@@ -0,0 +1,196 @@
|
||||
<!doctype html>
|
||||
<html lang="de">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>abdulive - Chat</title>
|
||||
<link rel="stylesheet" href="/theme.css">
|
||||
<style>
|
||||
/* Von unten nach oben, wie im Chat selbst: die neueste Nachricht steht
|
||||
* unten, wo das Auge sie erwartet. Deshalb column-reverse und nicht ein
|
||||
* Scrollcontainer - der haette einen Balken und wuerde mitwandern. */
|
||||
#liste {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
padding: 12px 14px;
|
||||
display: flex;
|
||||
flex-direction: column-reverse;
|
||||
justify-content: flex-start;
|
||||
gap: 8px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.zeile {
|
||||
padding: 9px 14px 10px;
|
||||
border-radius: 12px;
|
||||
background: rgba(18, 14, 29, .66);
|
||||
backdrop-filter: blur(10px);
|
||||
box-shadow: 0 6px 20px rgba(0, 0, 0, .45);
|
||||
font-size: 19px;
|
||||
line-height: 1.38;
|
||||
word-break: break-word;
|
||||
animation: herein .42s cubic-bezier(.16, 1.1, .3, 1) both;
|
||||
transition: opacity .6s, transform .6s;
|
||||
}
|
||||
@keyframes herein {
|
||||
from { opacity: 0; transform: translateX(-26px) scale(.97); }
|
||||
to { opacity: 1; transform: none; }
|
||||
}
|
||||
.zeile.geht { opacity: 0; transform: translateX(-20px); }
|
||||
|
||||
.name { font-weight: 700; margin-right: .45em; }
|
||||
.text .emote { height: 1.55em; vertical-align: -.4em; margin: 0 .05em; }
|
||||
|
||||
/* Ein GIF ist kein Emote: es steht fuer sich, in eigener Zeile, und wird
|
||||
* begrenzt. Ohne Deckel schiebt ein 500 Pixel breites GIF die ganze Spalte
|
||||
* auf - in einer 460 Pixel schmalen Browser-Source faellt das sofort auf. */
|
||||
.text .gif {
|
||||
display: block;
|
||||
margin: 6px 0 2px;
|
||||
max-width: 100%;
|
||||
max-height: 180px;
|
||||
width: auto;
|
||||
border-radius: 8px;
|
||||
background: rgba(0, 0, 0, .35);
|
||||
}
|
||||
|
||||
/* Abzeichen statt Bilder: Twitchs Abzeichen-URLs haengen am Kanal und muessten
|
||||
* ueber Helix aufgeloest werden. Ein farbiger Punkt sagt dasselbe. */
|
||||
.abzeichen {
|
||||
display: inline-block;
|
||||
width: .5em; height: .5em;
|
||||
border-radius: 50%;
|
||||
margin-right: .4em;
|
||||
vertical-align: .08em;
|
||||
}
|
||||
.abzeichen.mod { background: var(--gruen); }
|
||||
.abzeichen.abo { background: var(--lila); }
|
||||
.abzeichen.host { background: var(--rot); }
|
||||
|
||||
/* Wer zum ersten Mal schreibt, wird hervorgehoben - das ist der Moment, in
|
||||
* dem eine Begruessung am meisten bringt. Ohne den Balken an der linken Kante
|
||||
* tragen das jetzt der Farbverlauf und die Zeile darueber allein, deshalb ist
|
||||
* der Verlauf kraeftiger als vorher. */
|
||||
.zeile.erstmals {
|
||||
background: linear-gradient(90deg, rgba(74, 222, 128, .24), rgba(18, 14, 29, .66) 65%);
|
||||
}
|
||||
.zeile.erstmals::before {
|
||||
content: "zum ersten Mal hier";
|
||||
display: block;
|
||||
font-size: 12px;
|
||||
letter-spacing: .12em;
|
||||
text-transform: uppercase;
|
||||
color: var(--gruen);
|
||||
margin-bottom: 3px;
|
||||
}
|
||||
.zeile.bits { background: linear-gradient(90deg, rgba(255, 197, 61, .22), rgba(18, 14, 29, .66) 65%); }
|
||||
.zeile.bits .name { color: var(--gold) !important; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div id="liste"></div>
|
||||
|
||||
<script src="/client.js"></script>
|
||||
<script>
|
||||
(function () {
|
||||
'use strict';
|
||||
|
||||
var liste = document.getElementById('liste');
|
||||
var maximal = Overlay.param('max', 14);
|
||||
var lebensdauer = Overlay.param('sekunden', 120) * 1000;
|
||||
var befehleZeigen = Overlay.param('befehle', false);
|
||||
|
||||
/* Twitchs Farben werden auf dunklem Grund teils unlesbar - dunkelblau auf
|
||||
* Schwarz ist nicht zu entziffern. Deshalb wird die Helligkeit angehoben
|
||||
* statt die Farbe zu verwerfen: die Farbe ist Wiedererkennung. */
|
||||
function lesbar(hex) {
|
||||
if (!hex || !/^#[0-9a-f]{6}$/i.test(hex)) return '#c9a7ff';
|
||||
var r = parseInt(hex.slice(1, 3), 16), g = parseInt(hex.slice(3, 5), 16),
|
||||
b = parseInt(hex.slice(5, 7), 16);
|
||||
var hell = (0.299 * r + 0.587 * g + 0.114 * b) / 255;
|
||||
if (hell >= 0.5) return hex;
|
||||
var f = 0.5 / Math.max(hell, 0.08);
|
||||
var an = function (v) { return Math.min(255, Math.round(v * f)); };
|
||||
return '#' + [an(r), an(g), an(b)].map(function (v) {
|
||||
return v.toString(16).padStart(2, '0');
|
||||
}).join('');
|
||||
}
|
||||
|
||||
function entferne(el) {
|
||||
// Doppelt aufrufbar: das Zeitlimit und das Nachruecken koennen dieselbe
|
||||
// Zeile treffen.
|
||||
if (!el || !el.parentNode || el.classList.contains('geht')) return;
|
||||
el.classList.add('geht');
|
||||
setTimeout(function () { el.remove(); }, 620);
|
||||
}
|
||||
|
||||
/**
|
||||
* Zeilen, die ueber die Obergrenze hinausgehen, ausblenden.
|
||||
*
|
||||
* Gezaehlt wird nur, was noch nicht auf dem Weg nach draussen ist: eine
|
||||
* ausblendende Zeile haengt noch eine halbe Sekunde im DOM. Wer stattdessen
|
||||
* `liste.children.length` zaehlt und in einer while-Schleife entfernt,
|
||||
* bekommt eine Endlosschleife - die Laenge sinkt ja erst nach der
|
||||
* Animation. In OBS friert die Quelle damit beim vierzehnten Chatbeitrag
|
||||
* ein, und im Browser sieht man einen haengenden Tab.
|
||||
*/
|
||||
function aufraeumen() {
|
||||
var bleibend = [];
|
||||
var zeilen = liste.children;
|
||||
for (var i = 0; i < zeilen.length; i++) {
|
||||
if (!zeilen[i].classList.contains('geht')) bleibend.push(zeilen[i]);
|
||||
}
|
||||
// index 0 ist die neueste Zeile, also faellt das Ende zuerst.
|
||||
for (var j = maximal; j < bleibend.length; j++) entferne(bleibend[j]);
|
||||
}
|
||||
|
||||
function zeige(ev) {
|
||||
if (!befehleZeigen && ev.text.charAt(0) === '!') return;
|
||||
|
||||
var el = document.createElement('div');
|
||||
el.className = 'zeile' + (ev.first ? ' erstmals' : '') + (ev.bits > 0 ? ' bits' : '');
|
||||
el.dataset.id = ev.id;
|
||||
el.dataset.user = ev.userId;
|
||||
|
||||
var abzeichen = '';
|
||||
if (ev.broadcaster) abzeichen += '<span class="abzeichen host"></span>';
|
||||
else if (ev.mod) abzeichen += '<span class="abzeichen mod"></span>';
|
||||
if (ev.sub) abzeichen += '<span class="abzeichen abo"></span>';
|
||||
|
||||
el.innerHTML =
|
||||
abzeichen +
|
||||
'<span class="name" style="color:' + lesbar(ev.color) + '">' +
|
||||
Overlay.esc(ev.name) + '</span>' +
|
||||
'<span class="text">' + Overlay.nachrichtHtml(ev.text, ev.gifs, ev.emotes) + '</span>';
|
||||
|
||||
// prepend, weil column-reverse die Liste ohnehin umdreht.
|
||||
liste.insertBefore(el, liste.firstChild);
|
||||
aufraeumen();
|
||||
|
||||
if (lebensdauer > 0) setTimeout(function () { entferne(el); }, lebensdauer);
|
||||
}
|
||||
|
||||
Overlay.on('chat', zeige);
|
||||
|
||||
// Loeschen durch einen Mod muss durchschlagen: eine geloeschte Beleidigung,
|
||||
// die noch zwei Minuten im Bild steht, steht auch in der Aufzeichnung.
|
||||
Overlay.on('clear', function (ev) {
|
||||
var alle = Array.prototype.slice.call(liste.children);
|
||||
for (var i = 0; i < alle.length; i++) {
|
||||
var el = alle[i];
|
||||
if (!ev.messageId && !ev.userId) entferne(el);
|
||||
else if (ev.messageId && el.dataset.id === ev.messageId) entferne(el);
|
||||
else if (ev.userId && el.dataset.user === ev.userId) entferne(el);
|
||||
}
|
||||
});
|
||||
|
||||
// Nach dem Neuverbinden den letzten Stand zeigen, damit die Quelle nicht
|
||||
// leer im Bild steht, bis jemand schreibt.
|
||||
Overlay.on('state', function (ev) {
|
||||
liste.innerHTML = '';
|
||||
var letzte = ev.chat.slice(-maximal);
|
||||
for (var i = 0; i < letzte.length; i++) zeige(letzte[i]);
|
||||
});
|
||||
})();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
Reference in new issue
Block a user