Files
OBS-Overlays/blackout-bingo/overlay.html
T

222 lines
7.9 KiB
HTML

<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Blackout Bingo Overlay</title>
<style>
:root {
--ink: #050505;
--coal: #0b0b0a;
--charcoal: #151514;
--slate: #242424;
--stone: #3a3935;
--line: rgba(238, 235, 225, 0.18);
--line-strong: rgba(238, 235, 225, 0.36);
--text: #efede6;
--muted: #aaa69d;
--paper: #d9d6cc;
--p1: #2f8cff;
--p2: #35d06f;
--both: #ffffff;
}
* { box-sizing: border-box; }
body {
margin: 0;
min-height: 100vh;
background:
radial-gradient(circle at 30% 18%, rgba(100, 100, 94, .20), transparent 35%),
radial-gradient(circle at 80% 80%, rgba(60, 60, 58, .22), transparent 42%),
linear-gradient(135deg, #000 0%, #090909 44%, #161614 100%);
font-family: Inter, "Segoe UI", system-ui, sans-serif;
color: var(--text);
overflow: hidden;
}
body::before {
content: "";
position: fixed;
inset: 0;
pointer-events: none;
background-image:
linear-gradient(rgba(255,255,255,.035) 1px, transparent 1px),
linear-gradient(90deg, rgba(255,255,255,.025) 1px, transparent 1px);
background-size: 46px 46px, 46px 46px;
mask-image: linear-gradient(to bottom, rgba(0,0,0,.45), transparent 78%);
}
.wrap {
width: 100vw;
height: 100vh;
padding: 12px;
display: grid;
place-items: center;
}
.card {
width: min(94vw, 980px);
background:
linear-gradient(145deg, rgba(34, 34, 32, .98), rgba(7, 7, 7, .99) 58%, rgba(18, 18, 17, .98)),
#0b0b0a;
border: 1px solid var(--line-strong);
border-radius: 18px;
padding: 16px;
box-shadow: 0 26px 80px rgba(0,0,0,.64), inset 0 0 0 1px rgba(255,255,255,.05);
position: relative;
overflow: hidden;
}
.card::before {
content: "BLACKOUT DOSSIER";
position: absolute;
right: 18px;
top: 14px;
color: rgba(238,235,225,.08);
font-family: Georgia, "Times New Roman", serif;
font-size: 54px;
font-weight: 800;
letter-spacing: -.05em;
line-height: .8;
pointer-events: none;
white-space: nowrap;
}
header {
position: relative;
display: grid;
grid-template-columns: 1fr auto;
align-items: end;
gap: 18px;
margin-bottom: 16px;
padding: 12px 2px 14px;
border-bottom: 1px solid var(--line);
}
h1 {
margin: 0;
font-family: Georgia, "Times New Roman", serif;
font-size: clamp(28px, 3.7vw, 46px);
line-height: .98;
letter-spacing: -.045em;
color: var(--paper);
text-transform: none;
text-shadow: 0 2px 0 #000, 0 20px 36px rgba(0,0,0,.55);
}
.sub {
color: var(--muted);
font-size: 11px;
letter-spacing: .22em;
text-transform: uppercase;
margin-bottom: 8px;
}
.legend { display:flex; gap: 8px; align-items:center; color: var(--muted); font-size: 12px; flex-wrap: wrap; justify-content: end; max-width: 430px; }
.pill { border: 1px solid var(--line); border-radius: 999px; padding: 6px 10px; background: rgba(255,255,255,.045); box-shadow: inset 0 1px 0 rgba(255,255,255,.05); }
.p1 { color: var(--p1); } .p2 { color: var(--p2); } .both { color: white; }
.grid {
display: grid;
grid-template-columns: repeat(5, 1fr);
gap: 8px;
}
.cell {
position: relative;
min-height: clamp(78px, 11.6vh, 104px);
padding: 14px 10px 30px;
display: grid;
place-items: center;
text-align: center;
border-radius: 10px;
border: 1px solid rgba(238,235,225,.14);
background:
linear-gradient(155deg, rgba(255,255,255,.07), rgba(255,255,255,.018) 42%, rgba(0,0,0,.18)),
#161615;
box-shadow: 0 10px 24px rgba(0,0,0,.22), inset 0 1px 0 rgba(255,255,255,.05);
font-weight: 760;
font-size: clamp(12px, 1.45vw, 17px);
line-height: 1.12;
overflow: hidden;
color: #e8e5dc;
}
.cell::before {
content: attr(data-index);
position: absolute;
top: 7px;
left: 8px;
color: rgba(238,235,225,.22);
font-size: 9px;
letter-spacing: .18em;
font-weight: 800;
}
.cell.p1done {
border-color: rgba(47,140,255,.86);
background:
radial-gradient(circle at 50% 24%, rgba(47,140,255,.34), transparent 45%),
linear-gradient(145deg, rgba(47,140,255,.22), rgba(34,34,32,.78));
color: white;
}
.cell.p2done {
border-color: rgba(53,208,111,.80);
background:
repeating-linear-gradient(-45deg, rgba(53,208,111,.16) 0 5px, transparent 5px 13px),
linear-gradient(145deg, rgba(53,208,111,.25), rgba(16,16,15,.88));
color: #f1eee6;
}
.cell.bothdone {
border-color: rgba(115,236,255,.92);
background:
radial-gradient(circle at 30% 28%, rgba(47,140,255,.38), transparent 42%),
radial-gradient(circle at 72% 70%, rgba(53,208,111,.34), transparent 44%),
linear-gradient(145deg, rgba(210,207,197,.22), rgba(45,45,43,.74));
color: white;
box-shadow: 0 13px 32px rgba(0,0,0,.34), inset 0 0 0 1px rgba(115,236,255,.20);
}
.marks { position:absolute; left: 9px; right: 9px; bottom: 8px; display:flex; justify-content:center; gap: 6px; }
.mark { width: 24px; height: 7px; border-radius: 999px; opacity: .38; background: rgba(238,235,225,.26); border: 1px solid rgba(238,235,225,.15); }
.mark.on.p1m { opacity:1; background: #2f8cff; box-shadow: 0 0 16px rgba(47,140,255,.45); }
.mark.on.p2m { opacity:1; background: #35d06f; box-shadow: 0 0 16px rgba(53,208,111,.40); }
.empty { color: var(--muted); padding: 60px 20px; text-align: center; border: 1px dashed var(--line); border-radius: 12px; background: rgba(255,255,255,.035); }
</style>
</head>
<body>
<main class="wrap">
<section class="card">
<header>
<div>
<div class="sub">Stream Challenge</div>
<h1>Blackout Bingo</h1>
</div>
<div class="legend" id="legend"></div>
</header>
<div id="board" class="grid" aria-label="Bingo board"></div>
</section>
</main>
<script>
async function loadBingo() {
const res = await fetch('/api/bingo?ts=' + Date.now(), { cache: 'no-store' });
const json = await res.json();
return json.data;
}
function classFor(cell, players) {
const p1 = cell.completedBy?.includes(players[0]);
const p2 = cell.completedBy?.includes(players[1]);
if (p1 && p2) return 'bothdone';
if (p1) return 'p1done';
if (p2) return 'p2done';
return '';
}
function render(data) {
const players = data.players || ['Player 1', 'Player 2'];
document.getElementById('legend').innerHTML = `<span class="pill p1">${players[0]}</span><span class="pill p2">${players[1]}</span><span class="pill both">Both = blackout square</span>`;
const board = document.getElementById('board');
if (!data.board || data.board.length === 0) {
board.className = '';
board.innerHTML = '<div class="empty">No board generated yet. Open the editor, add at least 25 tasks, then generate a card.</div>';
return;
}
board.className = 'grid';
board.innerHTML = data.board.map((cell, i) => {
const p1 = cell.completedBy?.includes(players[0]);
const p2 = cell.completedBy?.includes(players[1]);
return `<div class="cell ${classFor(cell, players)}" data-index="${String(i + 1).padStart(2, '0')}"><span>${cell.task}</span><div class="marks"><i class="mark p1m ${p1 ? 'on' : ''}"></i><i class="mark p2m ${p2 ? 'on' : ''}"></i></div></div>`;
}).join('');
}
async function refresh() { try { render(await loadBingo()); } catch (e) { console.error(e); } }
refresh();
setInterval(refresh, 1500);
</script>
</body>
</html>