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:
1 parent
63ea40d5a8
commit
2c298a3427
2 files changed
+229
No files matched your search
@@ -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,
|
||||
|
||||
@@ -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>
|
||||
Reference in new issue
Block a user