Overlay: Kamerarahmen als Entwurf

Rahmen rechts und unten um die Facecam, darunter eine Platte mit Kanal,
Kategorie, Zuschauern und Laufzeit. Soll die Statusleiste abloesen.

Ueber der Kamera liegt nichts: genau 0,0 480x270 wird fuer die Shorts
ausgeschnitten, das Gameplay ab x=515. Der Rahmen nutzt nur den Streifen
dazwischen und den Platz darunter. Bei Abo, Raid und Bits glueht er kurz
in der Farbe des Ereignisses.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
ZeasyAlexandClaude Opus 5.5 committed 2026-09-23 22:58:36 +02:00
1 parent 63ea40d5a8
commit 2c298a3427
2 files changed
+229

No files matched your search

+2
View File
@@ -54,6 +54,8 @@ export const PAGES = [
title: 'Einblendungen', hint: 'Abos, Raids, Bits, Clips. Gehört ganz nach oben.' },
{ path: '/chat', file: 'chat.html', w: 460, h: 900,
title: 'Chat', hint: 'Nachrichten laufen herein und verblassen wieder.' },
{ path: '/kamera', file: 'kamera.html', w: 520, h: 380,
title: 'Kamerarahmen', hint: 'Rahmen um die Facecam mit Zuschauern und Laufzeit. Position 0,0, über der Kamera.' },
{ path: '/status', file: 'status.html', w: 1920, h: 160,
title: 'Statusleiste', hint: 'Titel, Kategorie, Laufzeit, Zuschauer.' },
{ path: '/effects', file: 'effects.html', w: 1920, h: 1080,
+227
View File
@@ -0,0 +1,227 @@
<!doctype html>
<html lang="de">
<head>
<meta charset="utf-8">
<title>abdulive - Kamerarahmen</title>
<link rel="stylesheet" href="/theme.css">
<style>
/* Rahmen um die Facecam, dazu die Werte der alten Statusleiste auf einer
* Platte darunter.
*
* Die Kamera liegt bündig oben links, 480x270 (docs/obs-layout.md). Genau
* dieser Bereich wird für die Shorts ausgeschnitten, und das Gameplay ab
* x=515. Deshalb liegt hier NICHTS über der Kamera: der Rahmen nutzt nur den
* Streifen rechts (480 bis 515) und den freien Platz darunter. Wer den
* Rahmen nach innen zieht, hat ihn in jedem Short.
*
* Oben und links gibt es keinen Rahmen - dort ist der Bildrand.
*
* In OBS: Browser-Quelle 520x380, Position 0,0, über der Kamera. */
:root {
--cam-w: 480px;
--cam-h: 270px;
--dicke: 7px;
--abstand: 4px; /* Luft zwischen Kamera und Rahmen */
--platte-h: 74px;
--verlauf: linear-gradient(135deg, var(--lila), var(--pink), var(--tuerkis), var(--lila));
}
/* Der Rahmen: ein L um die rechte und untere Kante, außen abgerundet.
* Gezeichnet als Fläche mit ausgespartem Inneren, damit der Farbverlauf
* ohne Stoß um die Ecke läuft. */
#rahmen {
position: fixed;
left: 0; top: 0;
width: calc(var(--cam-w) + var(--abstand) + var(--dicke));
height: calc(var(--cam-h) + var(--abstand) + var(--dicke));
border-bottom-right-radius: 22px;
background: var(--verlauf);
background-size: 300% 300%;
animation: wandern 10s linear infinite;
-webkit-mask:
linear-gradient(#000 0 0) top left / calc(var(--cam-w) + var(--abstand)) calc(var(--cam-h) + var(--abstand)) no-repeat,
linear-gradient(#000 0 0);
-webkit-mask-composite: xor;
mask-composite: exclude;
filter: drop-shadow(0 0 10px rgba(169, 112, 255, .45));
transition: filter .4s;
}
@keyframes wandern { to { background-position: 300% 300%; } }
/* Platte unter der Kamera. Hängt am Rahmen, damit beides als ein Stück
* gelesen wird und nicht als zwei Overlays, die zufällig nah beieinander
* liegen. */
#platte {
position: fixed;
left: 0;
top: calc(var(--cam-h) + var(--abstand) + var(--dicke));
width: calc(var(--cam-w) + var(--abstand) + var(--dicke));
height: var(--platte-h);
display: grid;
grid-template-columns: 1fr auto;
align-items: center;
gap: 16px;
padding: 0 18px 0 16px;
background: var(--tinte-klar);
backdrop-filter: blur(14px) saturate(140%);
border-bottom-left-radius: 0;
border-bottom-right-radius: 16px;
box-shadow: var(--schatten), var(--glanz);
}
.links { min-width: 0; }
.zeile1 { display: flex; align-items: center; gap: 10px; }
.punkt {
width: 11px; height: 11px; flex: none;
border-radius: 50%;
background: #555;
}
body.live .punkt { background: var(--rot); animation: klopfen 2s ease-out infinite; }
@keyframes klopfen {
0% { box-shadow: 0 0 0 0 rgba(239, 68, 68, .55); }
70% { box-shadow: 0 0 0 12px rgba(239, 68, 68, 0); }
100% { box-shadow: 0 0 0 0 rgba(239, 68, 68, 0); }
}
.kanal { font-size: 23px; font-weight: 700; letter-spacing: -.01em; line-height: 1.1; }
#kategorie {
font-size: 15px; color: var(--schrift-leise); margin-top: 3px;
white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
.werte { display: flex; gap: 18px; }
.wert { text-align: right; }
.wert .zahl {
display: flex; align-items: center; justify-content: flex-end; gap: 6px;
font: 700 22px/1.1 var(--font-zahl);
font-variant-numeric: tabular-nums;
}
.wert svg { width: 17px; height: 17px; opacity: .8; }
.wert .beschriftung {
font-size: 10.5px; letter-spacing: .14em; text-transform: uppercase;
color: var(--schrift-leise);
}
.zahl.neu { animation: aufleuchten .8s ease-out; }
@keyframes aufleuchten {
0% { color: var(--tuerkis); transform: translateY(-3px); }
100% { color: inherit; transform: none; }
}
/* Nicht live: der Rahmen wird ruhig und grau. So sieht man in der
* Vorschau sofort, ob der Hub den Stream erkannt hat. */
body:not(.live) #rahmen {
background: linear-gradient(135deg, #3a3550, #57507a);
animation: none;
filter: none;
}
/* Aufleuchten bei Abo, Raid, Bits: der Rahmen glüht kurz in der Farbe des
* Ereignisses. Die große Einblendung macht alerts.html - das hier ist nur
* das Echo an der Kamera, damit der Blick dorthin geht, wo Abdu reagiert. */
body.puls #rahmen { animation: wandern 1.2s linear infinite; }
body.puls-lila #rahmen { filter: drop-shadow(0 0 22px var(--lila)); }
body.puls-pink #rahmen { filter: drop-shadow(0 0 22px var(--pink)); }
body.puls-gold #rahmen { filter: drop-shadow(0 0 22px var(--gold)); }
body.puls-tuerkis #rahmen { filter: drop-shadow(0 0 22px var(--tuerkis)); }
/* Nur zum Ansehen im Browser (?probe=1): ein Platzhalter, wo die Kamera
* sitzt. In OBS liegt dort das echte Bild. */
#kamera-probe {
display: none;
position: fixed; left: 0; top: 0;
width: var(--cam-w); height: var(--cam-h);
background:
radial-gradient(40% 55% at 50% 60%, #4a3b6b, transparent 70%),
linear-gradient(160deg, #2b2440, #17131f);
color: var(--schrift-leise);
font: 500 14px/1 var(--font-zahl);
display: none; place-items: center;
}
body.probe #kamera-probe { display: grid; }
</style>
</head>
<body>
<div id="kamera-probe">Facecam 480 × 270</div>
<div id="rahmen"></div>
<div id="platte">
<div class="links">
<div class="zeile1">
<span class="punkt"></span>
<span class="kanal kontur" id="kanal">abdulive_</span>
</div>
<div id="kategorie">offline</div>
</div>
<div class="werte">
<div class="wert">
<span class="zahl" id="zuschauer">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2"
stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
<path d="M2 12s3.6-7 10-7 10 7 10 7-3.6 7-10 7S2 12 2 12z"/><circle cx="12" cy="12" r="3"/>
</svg><span>-</span>
</span>
<span class="beschriftung">Zuschauer</span>
</div>
<div class="wert">
<span class="zahl" id="laufzeit"><span>-</span></span>
<span class="beschriftung">live seit</span>
</div>
</div>
</div>
<div class="getrennt">keine Verbindung</div>
<script src="/client.js"></script>
<script>
(function () {
'use strict';
if (Overlay.param('probe', false)) document.body.classList.add('probe');
var kategorie = document.getElementById('kategorie');
var zuschauer = document.getElementById('zuschauer');
var laufzeit = document.getElementById('laufzeit');
document.getElementById('kanal').textContent = Overlay.param('kanal', 'abdulive_');
var seit = null;
function setze(el, wert) {
var ziel = el.lastElementChild;
if (ziel.textContent === wert) return;
ziel.textContent = wert;
el.classList.remove('neu');
void el.offsetWidth;
el.classList.add('neu');
}
function stand(ev) {
document.body.classList.toggle('live', !!ev.live);
// Die Kategorie statt des Titels: der Titel ist meist zu lang für 490 px
// und steht ohnehin auf der Twitch-Seite direkt unter dem Bild.
kategorie.textContent = ev.live ? (ev.category || 'live') : 'offline';
setze(zuschauer, ev.live ? Overlay.zahl(ev.viewers) : '-');
seit = ev.live ? ev.startedAt : null;
tick();
}
function tick() { laufzeit.lastElementChild.textContent = seit ? Overlay.laufzeit(seit) : '-'; }
setInterval(tick, 10000);
var FARBE = {
sub: 'lila', resub: 'lila', subgift: 'pink', massgift: 'pink',
raid: 'tuerkis', cheer: 'gold', follow: 'tuerkis', reward: 'gold',
};
var pulsTimer = null;
function puls(farbe) {
var b = document.body;
b.className = b.className.replace(/\bpuls(-\w+)?\b/g, '').trim();
void b.offsetWidth;
b.classList.add('puls', 'puls-' + farbe);
clearTimeout(pulsTimer);
pulsTimer = setTimeout(function () {
b.className = b.className.replace(/\bpuls(-\w+)?\b/g, '').trim();
}, 4000);
}
Overlay.on('stream', stand);
Overlay.on('state', function (ev) { stand(ev.stream); });
Overlay.on('alert', function (ev) { puls(FARBE[ev.type] || 'lila'); });
})();
</script>
</body>
</html>