Files
obs-config/archived/scenes/game/index.html
Jakub Zych 0b4f121a92 webapp: archive pre-migration scene dirs + bash scripts, update docs
Moves the seven HTML scene dirs (landing/, loading/, game/, desktop/,
goodbye/, music-box/, music/) and the superseded bash helpers (setup.sh,
loading.sh, playlist.sh, telemetry.sh) into archived/ rather than deleting.
The Laravel webapp/ replaces all of them; archived/README.md spells out
the rollback procedure.

Also:
 - rewrite the relevant sections of CLAUDE.md so it points at webapp/
   blade views, the Artisan commands, and the supervisord lifecycle
   (`supervisorctl restart obs-webapp` after route / controller / .env
   changes; Blade view edits are still safe-while-running via OBS's
   Refresh cache).
 - extend scripts/deploy-rig.sh to install php + composer + supervisor,
   run `composer install`, copy obs-webapp.supervisord.conf into
   /etc/supervisor.d/, start the program, and call `php artisan
   rig:setup` + `rig:telemetry --collect` instead of the old bash.
 - .gitignore catches the generated machine-local files that came along
   when the old scene dirs moved (telemetry.js, loading.json, playlist.js,
   cmd.js, obs-config.js).
 - daemon.blade.php is now passive — listens to mpd:state but does not
   play audio or broadcast its own queue, so it stops fighting with
   bridges/mpd-state.py (the post-browser-daemon-migration source of
   truth for mpd:state).
 - nc.blade.php overrides .terminal { overflow: hidden } from hud.css
   so the `┤ TERMINAL ├` and `┤ NOW PLAYING ├` pane-tabs stick above
   the pane border instead of being clipped.
2026-05-21 13:08:49 +02:00

432 lines
15 KiB
HTML
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>OPHI-118 / GAME</title>
<style>
:root {
--ink: #e8e8e0;
--ink-dim: rgba(232, 232, 224, 0.62);
--hud: #4fd2ff;
--hud-dim: #2a7fa6;
--hud-edge: rgba(79, 210, 255, 0.85);
--hud-glow: rgba(79, 210, 255, 0.40);
--accent: #e63a2e;
--warn: #ffd000;
--term-fg: #5fdc62;
--term-fg-dim:#2c8d2f;
--mono: 'DejaVu Sans Mono', 'Liberation Mono', 'Consolas', monospace;
--display: 'DejaVu Sans', 'Liberation Sans', 'Helvetica', 'Arial', sans-serif;
--pad-y: 40px;
--pad-x: 72px;
/* Camera frame transform — auto-synced from OBS at runtime via
GetSceneItemTransform + SceneItemTransformChanged. These defaults
are only the first-paint fallback; once the WS connects the box
snaps to wherever the Camera source actually is in the Game scene. */
--cam-x: 1855px;
--cam-y: 128px;
--cam-w: 580px;
--cam-h: 326px;
/* Visual outset around the camera so the cyan border + corner ticks
breathe and stay visible on all four sides (rather than getting
overdrawn by the camera image). Applied symmetrically. */
--cam-pad: 10px;
}
* { margin: 0; padding: 0; box-sizing: border-box; }
html, body {
width: 100%; height: 100%;
background: transparent; /* overlay only — game capture shows through */
color: var(--ink);
font-family: var(--display);
overflow: hidden;
user-select: none;
}
/* ───────── Top HUD ───────── */
.hud {
position: absolute;
top: var(--pad-y);
left: var(--pad-x);
right: var(--pad-x);
display: grid;
grid-template-columns: 1fr 1fr 1fr;
align-items: center;
font-family: var(--mono);
font-size: 20px;
font-weight: 700;
color: var(--hud);
letter-spacing: 0.08em;
text-shadow: 0 0 8px rgba(7, 8, 13, 0.9), 0 0 4px rgba(7, 8, 13, 0.9);
}
.hud .group { display: flex; gap: 24px; align-items: center; }
.hud > .group:nth-child(1) { justify-self: start; }
.hud > .group:nth-child(2) { justify-self: center; }
.hud > .group:nth-child(3) { justify-self: end; }
.hud .dim { color: var(--hud-dim); }
.hud .group > span { display: inline-flex; align-items: center; gap: 10px; }
.hud .led {
display: inline-block;
width: 10px; height: 10px;
border-radius: 50%;
background: var(--accent);
box-shadow: 0 0 10px var(--accent);
/* Small upward nudge: flex centers on line-box mid, but caps-only text
reads centered around cap-height mid which sits a touch higher. */
margin-top: -0.08em;
animation: rec-blink 1.6s ease-in-out infinite;
}
/* The ▮/▯ glyphs in DejaVu Sans Mono are designed centered on x-height,
not cap-height, so against all-caps SIGNAL their visual middle sits low.
Lift by ~(cap-mid x-mid) ≈ 0.10em to put the glyph center on cap center. */
#signal {
display: inline-block;
transform: translateY(-0.10em);
}
@keyframes rec-blink {
0%, 55% { opacity: 1; }
65%, 100% { opacity: 0.2; }
}
/* ───────── Camera frame ─────────
Decorative chrome drawn UNDER the OBS Camera scene item. The four
--cam-* CSS vars are auto-synced from the Camera item's transform via
OBS WebSocket (see syncCamera below), so wherever you drag/resize the
camera in OBS, this frame snaps to match. The dark backdrop + centered
placeholder show through only until the WS connects or while the camera
is hidden. Requires the HUD browser source to render at canvas
resolution (2560×1336) so 1 CSS px == 1 canvas px. */
.camera-frame {
position: absolute;
top: calc(var(--cam-y) - var(--cam-pad));
left: calc(var(--cam-x) - var(--cam-pad));
width: calc(var(--cam-w) + var(--cam-pad) * 2);
height: calc(var(--cam-h) + var(--cam-pad) * 2);
pointer-events: none;
background: rgba(6, 10, 18, 0.85);
border: 1.5px solid rgba(79, 210, 255, 0.55);
box-shadow:
0 0 28px rgba(79, 210, 255, 0.20),
inset 0 0 60px rgba(0, 0, 0, 0.55);
}
.camera-frame .tick {
position: absolute;
width: 24px; height: 24px;
border: 3px solid var(--hud-edge);
box-shadow: 0 0 8px var(--hud-glow);
}
.camera-frame .tick.tl { top: -2px; left: -2px; border-right: none; border-bottom: none; }
.camera-frame .tick.tr { top: -2px; right: -2px; border-left: none; border-bottom: none; }
.camera-frame .tick.bl { bottom: -2px; left: -2px; border-right: none; border-top: none; }
.camera-frame .tick.br { bottom: -2px; right: -2px; border-left: none; border-top: none; }
.camera-frame .label {
position: absolute;
top: 10px; left: 14px;
font-family: var(--mono);
font-size: 12px;
color: var(--hud);
letter-spacing: 0.32em;
text-shadow: 0 0 4px rgba(7, 8, 13, 0.9);
}
.camera-frame .placeholder {
position: absolute; inset: 0;
display: flex;
align-items: center;
justify-content: center;
flex-direction: column;
gap: 14px;
font-family: var(--mono);
color: var(--hud);
pointer-events: none;
}
.camera-frame .placeholder svg { opacity: 0.6; }
.camera-frame .placeholder .ph-text {
font-size: 13px;
letter-spacing: 0.45em;
color: rgba(79, 210, 255, 0.7);
}
.camera-frame .placeholder .ph-coords {
font-size: 11px;
letter-spacing: 0.20em;
color: var(--hud-dim);
}
.camera-frame.off { display: none; }
/* ───────── Bottom status bar ───────── */
.status-bar {
position: absolute;
bottom: 0;
left: 0; right: 0;
padding: 22px var(--pad-x) 26px;
/* Solid darker base + cyan top-edge so the bar reads cleanly on any
gameplay background. Top fade is short so the strip feels grounded. */
background:
linear-gradient(
to top,
rgba(7, 8, 13, 0.96) 0%,
rgba(7, 8, 13, 0.96) 65%,
rgba(7, 8, 13, 0.50) 90%,
rgba(7, 8, 13, 0) 100%
);
border-top: 1px solid rgba(79, 210, 255, 0.28);
box-shadow: 0 -2px 16px rgba(0, 0, 0, 0.5);
font-family: var(--mono);
letter-spacing: 0.08em;
}
.status-line { display: flex; align-items: center; gap: 18px; flex-wrap: wrap; }
.status-line + .status-line { margin-top: 8px; }
.status-line .sep { color: var(--hud-dim); }
.live-block {
display: flex; align-items: center; gap: 10px;
color: var(--accent);
font-weight: 700;
letter-spacing: 0.14em;
font-size: 18px;
}
.live-dot {
width: 11px; height: 11px;
border-radius: 50%;
background: var(--accent);
box-shadow: 0 0 10px var(--accent);
animation: rec-blink 1.6s ease-in-out infinite;
}
.game-name {
color: var(--ink);
font-weight: 700;
font-size: 22px;
letter-spacing: 0.10em;
}
.game-mode {
color: var(--hud);
font-size: 17px;
letter-spacing: 0.18em;
}
.meta-line {
color: var(--ink-dim);
font-size: 16px;
}
.meta-line .icon { color: var(--hud); }
.np-block { display: flex; align-items: center; gap: 10px; visibility: hidden; }
.np-block.on { visibility: visible; }
.np-block .arrow {
color: var(--term-fg);
animation: np-pulse 1.05s ease-in-out infinite alternate;
text-shadow: 0 0 6px rgba(80, 220, 100, 0.4);
}
.np-block .title { color: var(--ink); }
.np-block .time { color: var(--ink-dim); }
@keyframes np-pulse {
0% { opacity: 0.45; }
100% { opacity: 1.00; }
}
</style>
</head>
<body>
<header class="hud">
<div class="group">
<span><span class="led"></span>TRANSMISSION</span>
<span>OPHI-118 // LIVE</span>
</div>
<div class="group">
<span class="dim">SIGNAL</span>
<span id="signal">▮▮▮▯▯</span>
</div>
<div class="group">
<span class="dim">UTC</span>
<span id="clock">--:--:--</span>
</div>
</header>
<div class="camera-frame" id="cam">
<span class="tick tl"></span>
<span class="tick tr"></span>
<span class="tick bl"></span>
<span class="tick br"></span>
<span class="label">CAM 01</span>
<div class="placeholder">
<svg width="64" height="48" viewBox="0 0 64 48" xmlns="http://www.w3.org/2000/svg" fill="none">
<rect x="2" y="8" width="44" height="32" rx="2" stroke="#4fd2ff" stroke-width="2"/>
<path d="M46 18 L60 10 L60 38 L46 30 Z" stroke="#4fd2ff" stroke-width="2" stroke-linejoin="round"/>
<circle cx="14" cy="14" r="2" fill="#e63a2e"/>
</svg>
<span class="ph-text">— WAITING FOR CAMERA —</span>
<span class="ph-coords">auto-syncs from OBS scene transform</span>
</div>
</div>
<footer class="status-bar">
<div class="status-line">
<div class="live-block">
<span class="live-dot"></span>
<span>LIVE</span>
</div>
<span class="sep">·</span>
<span class="game-name" id="game-name"></span>
<span class="sep" id="game-mode-sep">·</span>
<span class="game-mode" id="game-mode"></span>
</div>
<div class="status-line meta-line">
<span class="icon"></span>
<span class="elapsed" id="elapsed">00:00:00</span>
<span class="sep">·</span>
<span class="icon" id="mic-icon" title="microphone">🎙</span>
<span id="mic-state"></span>
<span class="sep">·</span>
<div class="np-block" id="np">
<span class="arrow"></span>
<span class="title" id="np-title"></span>
<span class="time" id="np-time">[--:-- / --:--]</span>
</div>
</div>
</footer>
<!-- Manifest from Project Loading. Same loading.js the Project Loading scene
uses, so the Game scene picks up the game / subtitle / camera / mic the
streamer already entered. -->
<script src="../loading/loading.js"></script>
<script src="../vendor/obs-config.js"></script>
<script src="../vendor/obs-ws-mini.js"></script>
<script>
'use strict';
const pad = n => String(n).padStart(2, '0');
// ── UTC clock ──
const clockEl = document.getElementById('clock');
function tickClock() {
const d = new Date();
clockEl.textContent =
`${pad(d.getUTCHours())}:${pad(d.getUTCMinutes())}:${pad(d.getUTCSeconds())}`;
}
tickClock(); setInterval(tickClock, 1000);
// ── Signal bar fluctuation ──
const sigEl = document.getElementById('signal');
setInterval(() => {
const r = Math.random();
const n = r < 0.06 ? 2 : r < 0.70 ? 3 : r < 0.95 ? 4 : 5;
sigEl.textContent = '▮'.repeat(n) + '▯'.repeat(5 - n);
}, 1500);
// ── Manifest ──
const m = window.__LOADING || {};
const gameName = (m.game || '—').toUpperCase();
const subtitle = m.subtitle ? m.subtitle.toUpperCase() : null;
const cameraOn = m.camera ?? true;
const micOn = m.microphone ?? true;
document.getElementById('game-name').textContent = gameName;
if (subtitle) {
document.getElementById('game-mode').textContent = subtitle;
} else {
document.getElementById('game-mode-sep').style.display = 'none';
document.getElementById('game-mode').style.display = 'none';
}
document.getElementById('mic-state').textContent = micOn ? 'ON' : 'MUTED';
document.getElementById('mic-state').style.color = micOn ? 'var(--term-fg)' : 'var(--accent)';
if (!cameraOn) document.getElementById('cam').classList.add('off');
// ── Stream elapsed time (since page load / scene activation) ──
const streamStart = Date.now();
function tickElapsed() {
const s = Math.floor((Date.now() - streamStart) / 1000);
const h = Math.floor(s / 3600);
const mm = Math.floor((s / 60) % 60);
const ss = s % 60;
document.getElementById('elapsed').textContent = `${pad(h)}:${pad(mm)}:${pad(ss)}`;
}
tickElapsed(); setInterval(tickElapsed, 1000);
// ── OBS WS: music sync + live camera visibility ──
// URL params let you rename the camera source / scene without editing JS:
// ?camera=Webcam&scene=Game
const params = new URLSearchParams(location.search);
const CAM_SOURCE = params.get('camera') || 'Camera';
const CAM_SCENE = params.get('scene') || 'Game';
function fmtTime(s) {
if (!isFinite(s) || s < 0) return '--:--';
return `${pad(Math.floor(s / 60))}:${pad(Math.floor(s % 60))}`;
}
const npBlock = document.getElementById('np');
const npTitle = document.getElementById('np-title');
const npTime = document.getElementById('np-time');
const camEl = document.getElementById('cam');
const rootStyle = document.documentElement.style;
function setCameraVisible(visible) { camEl.classList.toggle('off', !visible); }
// Map an OBS sceneItemTransform into the four CSS vars driving the frame.
// Assumes alignment 5 (top-left) so positionX/Y is the top-left corner —
// OBS's default for newly added items, and what the Camera source uses.
function setCameraTransform(t) {
if (!t) return;
const w = (t.sourceWidth ?? 0) * (t.scaleX ?? 1);
const h = (t.sourceHeight ?? 0) * (t.scaleY ?? 1);
if (!(w > 0 && h > 0)) return;
rootStyle.setProperty('--cam-x', `${t.positionX}px`);
rootStyle.setProperty('--cam-y', `${t.positionY}px`);
rootStyle.setProperty('--cam-w', `${w}px`);
rootStyle.setProperty('--cam-h', `${h}px`);
}
async function syncCamera(obs) {
let camItemId = null;
try {
const r = await obs.call('GetSceneItemId',
{ sceneName: CAM_SCENE, sourceName: CAM_SOURCE });
camItemId = r.sceneItemId;
const e = await obs.call('GetSceneItemEnabled',
{ sceneName: CAM_SCENE, sceneItemId: camItemId });
setCameraVisible(e.sceneItemEnabled);
const t = await obs.call('GetSceneItemTransform',
{ sceneName: CAM_SCENE, sceneItemId: camItemId });
setCameraTransform(t.sceneItemTransform);
} catch (err) {
// Source not found — keep manifest value, don't block other features.
console.warn(`[game] camera sync init failed (${CAM_SCENE}/${CAM_SOURCE}):`, err.message);
return;
}
obs.addEventListener('event', (ev) => {
const d = ev.detail || {};
if (d.eventData?.sceneName !== CAM_SCENE
|| d.eventData?.sceneItemId !== camItemId) return;
if (d.eventType === 'SceneItemEnableStateChanged') {
setCameraVisible(d.eventData.sceneItemEnabled);
} else if (d.eventType === 'SceneItemTransformChanged') {
setCameraTransform(d.eventData.sceneItemTransform);
}
});
}
async function connectOBS() {
if (!window.__OBSWS || !window.OBSWSMini) return;
try {
const obs = new OBSWSMini(window.__OBSWS.url, window.__OBSWS.password);
await obs.connect();
obs.addEventListener('close', () => {
npBlock.classList.remove('on');
setTimeout(connectOBS, 2500);
});
// Music sync
obs.onCustom('mpd:state', (s) => {
if (!s.title) return;
npBlock.classList.add('on');
npTitle.textContent = s.title;
npTime.textContent = `[${fmtTime(s.currentTime)} / ${fmtTime(s.duration)}]`;
});
// Live camera visibility
await syncCamera(obs);
} catch (e) {
// OBS WS optional — silently retry; meanwhile manifest values stand.
setTimeout(connectOBS, 2500);
}
}
connectOBS();
</script>
</body>
</html>