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:
ZeasyAlexandClaude Opus 5 committed 2026-09-18 00:13:28 +02:00
1 parent 3b606e36fb
commit bc2c8f6cbe
24 files changed
+3221 -8

No files matched your search

+156
View File
@@ -0,0 +1,156 @@
<!doctype html>
<html lang="de">
<head>
<meta charset="utf-8">
<title>abdulive - Pausenbild</title>
<link rel="stylesheet" href="/theme.css">
<style>
/* Das einzige Overlay mit deckendem Hintergrund: es ersetzt das Bild, statt
* darueber zu liegen. Gedacht als eigene OBS-Szene fuer "gleich geht es los",
* Pause und Ende. Text ueber ?text= und ?unter=. */
body { background: #0a0813; }
#grund {
position: fixed;
inset: -20%;
background:
radial-gradient(42% 42% at 22% 28%, rgba(169, 112, 255, .55), transparent 70%),
radial-gradient(38% 38% at 78% 32%, rgba(79, 209, 197, .45), transparent 70%),
radial-gradient(45% 45% at 55% 82%, rgba(255, 79, 163, .40), transparent 70%);
filter: blur(48px);
animation: treiben 28s ease-in-out infinite alternate;
}
@keyframes treiben {
0% { transform: translate(0, 0) scale(1) rotate(0deg); }
50% { transform: translate(-3%, 2.5%) scale(1.1) rotate(4deg); }
100% { transform: translate(3%, -2%) scale(1.04) rotate(-3deg); }
}
/* Feines Raster darueber: nimmt dem Verlauf das Billige und gibt dem Bild
* eine Struktur, die auch nach der Twitch-Kompression noch da ist. */
#raster {
position: fixed;
inset: 0;
background-image:
linear-gradient(rgba(255, 255, 255, .045) 1px, transparent 1px),
linear-gradient(90deg, rgba(255, 255, 255, .045) 1px, transparent 1px);
background-size: 64px 64px;
mask-image: radial-gradient(70% 70% at 50% 50%, #000, transparent);
}
#mitte {
position: fixed;
inset: 0;
display: grid;
place-content: center;
justify-items: center;
text-align: center;
gap: 26px;
}
h1 {
margin: 0;
font-size: clamp(64px, 8.5vw, 148px);
font-weight: 800;
letter-spacing: -.035em;
/* Unterlaengen brauchen Platz. `background-clip: text` faerbt nur, was
* innerhalb der Polsterbox liegt - was darueber hinausragt, bleibt
* durchsichtig und sieht abgeschnitten aus. Bei fetter Schrift in dieser
* Groesse reicht eine knappe Zeilenhoehe dafuer nicht.
*
* Nachgemessen am "g" in "Gleich geht es los": mit 1.04 und .06em fehlte
* noch ein Stueck vom Bogen. */
line-height: 1.18;
padding-bottom: .14em;
background: linear-gradient(100deg, #fff 18%, var(--lila) 48%, var(--tuerkis) 78%, #fff 96%);
background-size: 260% 100%;
-webkit-background-clip: text;
background-clip: text;
color: transparent;
animation: schimmern 7s linear infinite;
filter: drop-shadow(0 10px 40px rgba(0, 0, 0, .55));
}
@keyframes schimmern { to { background-position: 260% 0; } }
#unter {
font-size: clamp(20px, 2vw, 30px);
color: var(--schrift-leise);
max-width: 34ch;
}
/* Der Taktgeber: fuenf Balken, die atmen. Ohne bewegtes Element wirkt ein
* Standbild wie ein eingefrorener Stream, und Zuschauer laden neu. */
#takt { display: flex; gap: 12px; align-items: flex-end; height: 68px; margin-top: 10px; }
#takt i {
display: block;
width: 13px;
border-radius: 5px;
background: linear-gradient(180deg, var(--tuerkis), var(--lila));
animation: atmen 1.5s ease-in-out infinite;
}
#takt i:nth-child(1) { height: 40%; animation-delay: 0s; }
#takt i:nth-child(2) { height: 75%; animation-delay: .18s; }
#takt i:nth-child(3) { height: 100%; animation-delay: .36s; }
#takt i:nth-child(4) { height: 65%; animation-delay: .54s; }
#takt i:nth-child(5) { height: 35%; animation-delay: .72s; }
@keyframes atmen {
0%, 100% { transform: scaleY(.35); opacity: .6; }
50% { transform: scaleY(1); opacity: 1; }
}
#fuss {
position: fixed;
left: 0; right: 0; bottom: 42px;
display: flex;
justify-content: center;
gap: 44px;
font-size: 22px;
color: var(--schrift-leise);
}
#fuss b { color: var(--schrift); font-weight: 600; }
#uhr { font-variant-numeric: tabular-nums; }
</style>
</head>
<body>
<div id="grund"></div>
<div id="raster"></div>
<div id="mitte">
<h1 id="gross">Gleich geht es los</h1>
<div id="unter"></div>
<div id="takt"><i></i><i></i><i></i><i></i><i></i></div>
</div>
<div id="fuss">
<span id="uhr"></span>
<span>twitch.tv/<b id="kanal">abdulive_</b></span>
<span id="thema"></span>
</div>
<script src="/client.js"></script>
<script>
(function () {
'use strict';
document.getElementById('gross').textContent = Overlay.param('text', 'Gleich geht es los');
document.getElementById('unter').textContent = Overlay.param('unter', '');
document.getElementById('kanal').textContent = Overlay.param('kanal', 'abdulive_');
var uhr = document.getElementById('uhr');
function zeit() {
uhr.textContent = new Date().toLocaleTimeString('de-DE',
{ hour: '2-digit', minute: '2-digit' });
}
zeit();
setInterval(zeit, 10000);
// Laeuft der Stream schon, steht unten, worum es geht. Das ist der
// Unterschied zwischen einem Pausenbild und einem Standbild.
var thema = document.getElementById('thema');
function stand(s) {
thema.textContent = s.live && s.category ? 'heute: ' + s.category : '';
}
Overlay.on('stream', stand);
Overlay.on('state', function (ev) { stand(ev.stream); });
})();
</script>
</body>
</html>