webapp: introduce Laravel app under webapp/ for scene overlays
Replaces the per-scene HTML directories (landing/, loading/, game/,
desktop/, goodbye/, music-box/, music/) with a single Laravel app
serving every overlay over HTTP. Supervisord runs `php artisan serve`
on 127.0.0.1:1118 and the OBS scene JSON now references HTTP routes
instead of file:// URLs.
Highlights:
- public/css/hud.css consolidates the duplicated HUD chrome,
scanlines/vignette/flicker, terminal styling, and pulse keyframes
that were copy-pasted across all seven scenes.
- Blade partials own hud-strip, crt-overlays, obs-ws-scripts,
camera-frame, screen-frame; the seven scenes extend a shared
overlay layout.
- Artisan commands (`rig:setup`, `rig:telemetry`, `rig:loading`,
`rig:playlist`, `rig:cmd`) replace the shell scripts that wrote
per-rig JSON snapshots. TwitchHelix + HardwareSnapshot services
handle the work the bash + Python helpers used to.
- ObsWsClient + MusicCommandController kill the 250 ms cmd.js poll
in the music daemon: POST /cmd/{skip|prev|pause|resume} opens a
short-lived Pawl WS, authenticates, and broadcasts mpd:cmd.
- AudioController streams files from the configured music dirs so
CEF can load tracks under the HTTP origin (Chromium blocks
HTTP-origin pages from loading file:// media).
- DataController serves /data/playlist.js (with ETag mtime cache)
and /cover.jpg (no-store) so the existing overlays' window.__PLAYLIST
and cover.jpg cache-bust pattern keeps working.
scripts/obs-webapp.supervisord.conf is the supervisord unit; install
to /etc/supervisor.d/obs-webapp.conf.
scripts/rewrite-scene-urls.py is a one-shot tool that rewrites
basic/scenes/Default_Stream_HUD.json from file:// to HTTP URLs.
basic/scenes/Default_Stream_HUD.json.pre-webapp is the rollback
artifact (made with OBS closed; full pre-migration state).
The seven old scene directories, vendor/, and the bash scripts are
still on disk pending visual verification; the next commit will
prune them.
This commit is contained in:
106
webapp/public/js/hud.js
Normal file
106
webapp/public/js/hud.js
Normal file
@@ -0,0 +1,106 @@
|
||||
/* OPHI-118 — shared HUD helpers used by every overlay.
|
||||
*
|
||||
* Exposes:
|
||||
* - HUD.startClock(el) — UTC ticker into the given element.
|
||||
* - HUD.startSignal(el, opts) — signal-bar fluctuator.
|
||||
* - HUD.scrambleReveal(...) — cyberpunk decode/scramble used by landing.
|
||||
*
|
||||
* Designed to no-op if the target element isn't on the page, so the
|
||||
* base layout can include it unconditionally.
|
||||
*/
|
||||
(function (root) {
|
||||
'use strict';
|
||||
|
||||
const pad = n => String(n).padStart(2, '0');
|
||||
|
||||
function startClock(el) {
|
||||
if (!el) return;
|
||||
const tick = () => {
|
||||
const d = new Date();
|
||||
el.textContent =
|
||||
`${pad(d.getUTCHours())}:${pad(d.getUTCMinutes())}:${pad(d.getUTCSeconds())}`;
|
||||
};
|
||||
tick();
|
||||
setInterval(tick, 1000);
|
||||
}
|
||||
|
||||
// n thresholds default to the original landing/loading/game distribution:
|
||||
// r < .06 → 2 bars (occasional drop)
|
||||
// r < .70 → 3 bars (typical)
|
||||
// r < .95 → 4 bars
|
||||
// → 5 bars (occasional spike)
|
||||
function startSignal(el, opts) {
|
||||
if (!el) return;
|
||||
const profile = opts?.profile || 'normal';
|
||||
const FILL = '▮', EMPTY = '▯';
|
||||
const draw = n => FILL.repeat(n) + EMPTY.repeat(5 - n);
|
||||
setInterval(() => {
|
||||
const r = Math.random();
|
||||
let n;
|
||||
if (profile === 'strong') {
|
||||
n = r < 0.15 ? 3 : r < 0.65 ? 4 : 5;
|
||||
} else if (profile === 'degrading') {
|
||||
n = r < 0.45 ? 1 : r < 0.85 ? 2 : r < 0.97 ? 3 : 0;
|
||||
} else {
|
||||
n = r < 0.06 ? 2 : r < 0.70 ? 3 : r < 0.95 ? 4 : 5;
|
||||
}
|
||||
el.textContent = draw(n);
|
||||
}, 1500);
|
||||
}
|
||||
|
||||
// ASCII-only glyph pool — DejaVu Mono renders these at 1ch each, so the
|
||||
// intermediate frames don't shift width.
|
||||
const GLITCH_GLYPHS = '01ABCDEFGHIJKLMNOPQRSTUVWXYZ#@*?!§%';
|
||||
const glyph = () => GLITCH_GLYPHS[(Math.random() * GLITCH_GLYPHS.length) | 0];
|
||||
const _scrambleHandles = new WeakMap();
|
||||
|
||||
function scrambleReveal(el, finalText, opts) {
|
||||
opts = opts || {};
|
||||
const { scrambleMs = 360, stepMs = 50, resolveStepMs = 60 } = opts;
|
||||
const prev = _scrambleHandles.get(el);
|
||||
if (prev) { clearInterval(prev.s); clearInterval(prev.r); }
|
||||
const len = finalText.length;
|
||||
if (!len) {
|
||||
el.textContent = '';
|
||||
el.classList.remove('glitching');
|
||||
_scrambleHandles.delete(el);
|
||||
return;
|
||||
}
|
||||
el.classList.add('glitching');
|
||||
const scrambleSteps = Math.max(1, Math.floor(scrambleMs / stepMs));
|
||||
const handle = { s: null, r: null };
|
||||
_scrambleHandles.set(el, handle);
|
||||
let step = 0;
|
||||
handle.s = setInterval(() => {
|
||||
let s = '';
|
||||
for (let i = 0; i < len; i++) s += glyph();
|
||||
el.textContent = s;
|
||||
if (++step < scrambleSteps) return;
|
||||
clearInterval(handle.s); handle.s = null;
|
||||
let resolved = 0;
|
||||
handle.r = setInterval(() => {
|
||||
resolved++;
|
||||
let s = finalText.slice(0, resolved);
|
||||
for (let i = resolved; i < len; i++) s += glyph();
|
||||
el.textContent = s;
|
||||
if (resolved < len) return;
|
||||
clearInterval(handle.r); handle.r = null;
|
||||
el.textContent = finalText;
|
||||
el.classList.remove('glitching');
|
||||
_scrambleHandles.delete(el);
|
||||
}, resolveStepMs);
|
||||
}, stepMs);
|
||||
}
|
||||
|
||||
root.HUD = { pad, startClock, startSignal, scrambleReveal };
|
||||
|
||||
// Auto-bind: every overlay has #clock and #signal in the shared partial.
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
startClock(document.getElementById('clock'));
|
||||
const sigEl = document.getElementById('signal');
|
||||
if (sigEl) {
|
||||
const profile = sigEl.dataset.profile || 'normal';
|
||||
startSignal(sigEl, { profile });
|
||||
}
|
||||
});
|
||||
})(typeof window !== 'undefined' ? window : globalThis);
|
||||
Reference in New Issue
Block a user