Files
OBS-overlay/optimized-controls.html
cottongin 3bd5752cb5 feat: enhance full-room glow with configurable controls and prevent text wrapping
Add white-space: nowrap to header, footer, and code-part elements to
prevent line breaks in narrow viewports. Replace static CSS keyframes
with dynamic generation so glow color, intensity, opacity, outline
thickness, and pulse duration are all configurable from the controls
panel. Double the default outline thickness from 4px to 8px.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-03 01:24:47 -04:00

958 lines
35 KiB
HTML

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Animated Stream Code Display</title>
<style>
body {
margin: 0;
padding: 0;
display: flex;
justify-content: center;
align-items: center;
height: 100vh;
background-color: transparent;
font-family: 'Arial', sans-serif;
overflow: hidden;
}
#container {
text-align: center;
width: 100%;
height: 100%;
display: flex;
flex-direction: column;
justify-content: center;
position: relative;
}
#header {
position: absolute;
width: fit-content;
white-space: nowrap;
left: 50%;
transform: translateX(-50%) translateY(-220px);
color: #f35dcb;
font-size: 80px;
font-weight: bold;
letter-spacing: 2px;
opacity: 0;
transition: opacity 0.5s ease;
background: linear-gradient(to right, #ffffff 0%, #ffffff 0%, #f35dcb 0%, #f35dcb 100%);
-webkit-background-clip: text;
background-clip: text;
-webkit-text-fill-color: transparent;
filter: drop-shadow(3px 3px 8px rgba(0, 0, 0, 0.8));
}
/* meter-full-pulse keyframes are generated dynamically by RoomCodeDisplay */
#footer {
position: absolute;
width: 100%;
white-space: nowrap;
text-align: center;
transform: translateY(160px);
color: #f35dcb;
font-size: 80px;
font-weight: bold;
text-shadow: 3px 3px 8px rgba(0, 0, 0, 0.8);
letter-spacing: 2px;
opacity: 0;
transition: opacity 0.5s ease;
}
.code-part {
font-size: 160px;
font-weight: bold;
color: #fdf935;
text-shadow: 4px 4px 12px rgba(0, 0, 0, 0.7);
letter-spacing: 8px;
position: absolute;
width: 100%;
white-space: nowrap;
text-align: center;
opacity: 0;
transition: opacity 0.5s ease;
}
#code-part1 {
transform: translateY(-100px);
}
#code-part2 {
transform: translateY(40px);
}
/* Redesigned controls */
#controls {
display: none;
position: absolute;
bottom: 50px;
right: 10px;
width: 280px; /* Narrower width */
padding: 10px;
background-color: rgba(0, 0, 0, 0.8);
border-radius: 8px;
z-index: 100;
max-height: 80vh;
overflow-y: auto;
}
.control-section {
margin-bottom: 5px;
border-bottom: 1px solid #555;
}
.section-header {
color: white;
background-color: rgba(0, 0, 0, 0.5);
padding: 5px;
margin: 0;
cursor: pointer;
font-size: 14px;
font-weight: bold;
display: flex;
justify-content: space-between;
align-items: center;
}
.section-content {
padding: 5px 0;
display: none;
}
.control-group {
margin-bottom: 8px;
display: flex;
flex-direction: column;
}
.control-row {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 5px;
}
label {
color: white;
font-size: 12px;
margin-right: 5px;
flex: 1;
}
input[type="text"] {
font-size: 14px;
padding: 3px;
text-align: center;
flex: 1;
text-transform: uppercase;
}
input[type="color"] {
width: 30px;
height: 20px;
vertical-align: middle;
}
input[type="number"] {
width: 50px;
font-size: 12px;
padding: 3px;
}
input[type="range"] {
width: 100px;
}
button {
font-size: 14px;
padding: 6px 12px;
background-color: #4CAF50;
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
margin-top: 5px;
width: 100%;
}
button:hover {
background-color: #45a049;
}
.action-buttons {
display: flex;
justify-content: space-between;
margin-top: 10px;
}
.action-buttons button {
width: 48%;
}
#show-controls-btn {
position: absolute;
bottom: 10px;
right: 10px;
background-color: rgba(0, 0, 0, 0.3);
color: #ffffff;
font-size: 12px;
padding: 4px 8px;
z-index: 100;
width: auto;
margin-top: 0;
}
#toggle-display-btn {
position: absolute;
bottom: 10px;
right: 120px;
background-color: rgba(0, 0, 0, 0.3);
color: #ffffff;
font-size: 12px;
padding: 4px 8px;
z-index: 100;
width: auto;
margin-top: 0;
}
/* Toggle indicator */
.toggle-indicator:after {
content: "▼";
margin-left: 5px;
font-size: 10px;
}
.toggle-indicator.active:after {
content: "▲";
}
/* Wide text fields */
.wide-field {
width: 100%;
}
#controls.bottom-position {
bottom: auto;
top: 80vh; /* Position below the footer */
left: 50%;
transform: translateX(-50%);
width: 80%;
max-width: 600px;
}
/* Style for active section */
.section-header.active {
background-color: rgba(79, 79, 79, 0.7);
}
/* Connection status indicator */
.status-row {
display: flex;
align-items: center;
margin-bottom: 5px;
gap: 6px;
}
.status-dot {
width: 10px;
height: 10px;
border-radius: 50%;
flex-shrink: 0;
}
.status-dot.disconnected {
background-color: #888;
}
.status-dot.connecting {
background-color: #f0ad4e;
animation: pulse 1s infinite;
}
.status-dot.connected {
background-color: #4CAF50;
}
.status-dot.error {
background-color: #d9534f;
}
@keyframes pulse {
0%, 100% { opacity: 1; }
50% { opacity: 0.4; }
}
.status-text {
color: #ccc;
font-size: 11px;
}
input[type="password"] {
font-size: 12px;
padding: 3px;
flex: 1;
width: 100%;
}
#ws-connect-btn {
background-color: #337ab7;
}
#ws-connect-btn:hover {
background-color: #286090;
}
#ws-disconnect-btn {
background-color: #d9534f;
}
#ws-disconnect-btn:hover {
background-color: #c9302c;
}
/* Player list */
#player-list-container {
position: absolute;
display: none;
flex-direction: column;
gap: 4px;
opacity: 0;
z-index: 10;
}
#player-list-container.player-list-position-left {
left: 24px;
top: 50%;
transform: translateY(-50%);
align-items: flex-start;
}
#player-list-container.player-list-position-right {
right: 24px;
top: 50%;
transform: translateY(-50%);
align-items: flex-start;
}
.player-slot {
display: flex;
gap: 8px;
align-items: baseline;
white-space: nowrap;
}
.player-slot-number {
color: rgba(255,255,255,0.5);
font-size: 24px;
font-weight: bold;
min-width: 2em;
text-align: right;
text-shadow: 2px 2px 6px rgba(0,0,0,0.7);
}
.player-slot-name {
font-size: 24px;
font-weight: bold;
text-shadow: 2px 2px 6px rgba(0,0,0,0.7);
}
.player-slot-name.empty {
color: transparent;
font-style: normal;
border-bottom: 2px dashed rgba(255,255,255,0.2);
min-width: 120px;
line-height: 0.8;
}
.player-slot-name.filled {
color: #ffffff;
border-bottom: none;
min-width: 0;
}
/* Debug dashboard */
#manager-dashboard .dashboard-grid {
display: grid;
grid-template-columns: auto 1fr;
gap: 2px 8px;
font-size: 11px;
color: #ccc;
}
#manager-dashboard .dashboard-grid .label {
color: #999;
text-align: right;
}
#manager-state {
display: inline-block;
padding: 2px 8px;
border-radius: 3px;
font-size: 11px;
font-weight: bold;
color: white;
background-color: #888;
}
.component-table {
width: 100%;
border-collapse: collapse;
font-size: 11px;
margin-top: 4px;
}
.component-table th,
.component-table td {
padding: 2px 4px;
text-align: left;
color: #ccc;
border-bottom: 1px solid #444;
}
.component-table th {
color: #999;
font-weight: normal;
}
.component-table select {
font-size: 10px;
background: #333;
color: #ccc;
border: 1px solid #555;
border-radius: 3px;
padding: 1px 2px;
}
#manager-event-log {
max-height: 100px;
overflow-y: auto;
font-size: 10px;
font-family: monospace;
background: rgba(0,0,0,0.3);
padding: 4px;
border-radius: 3px;
margin-top: 4px;
}
.event-log-entry {
color: #aaa;
line-height: 1.4;
}
.event-time {
color: #666;
}
.event-type {
color: #4fc3f7;
}
</style>
</head>
<body>
<div id="container">
<div id="header">ROOM CODE:</div>
<div id="code-part1" class="code-part"></div>
<div id="code-part2" class="code-part"></div>
<div id="footer">Enter @ jackbox.tv</div>
<!-- Add audio element -->
<audio id="theme-sound" preload="auto" loop>
<source src="https://feed.falsefinish.club/HSO/Audio/Room%20Code%20Theme.mp3" type="audio/mp3">
Your browser does not support the audio element.
</audio>
<div id="player-list-container"></div>
<div id="controls">
<!-- Code Settings Section -->
<div class="control-section">
<div class="section-header">
<span>Code Settings</span>
<span class="toggle-indicator active"></span>
</div>
<div class="section-content" style="display: block;">
<div class="control-group">
<div class="control-row">
<label>First Line:</label>
<input type="text" id="code1-input" maxlength="2" value="" style="width: 40px;">
<input type="color" id="color1-input" value="#fdf935">
</div>
<div class="control-row">
<label>Offset:</label>
<input type="number" id="offset1-input" value="-100" step="10">
</div>
</div>
<div class="control-group">
<div class="control-row">
<label>Second Line:</label>
<input type="text" id="code2-input" maxlength="2" value="" style="width: 40px;">
<input type="color" id="color2-input" value="#fdf935">
</div>
<div class="control-row">
<label>Offset:</label>
<input type="number" id="offset2-input" value="40" step="10">
</div>
</div>
<div class="control-row">
<label>Font Size:</label>
<input type="number" id="size-input" value="160" min="10" max="200" step="1">
</div>
<div class="control-row">
<label>Animation Cycle (sec):</label>
<input type="number" id="cycle-input" value="300" min="30" max="600" step="10">
</div>
</div>
</div>
<!-- Header Settings Section -->
<div class="control-section">
<div class="section-header">
<span>Header Settings</span>
<span class="toggle-indicator"></span>
</div>
<div class="section-content">
<div class="control-row">
<label>Text:</label>
<input type="text" id="header-text-input" value="ROOM CODE:" class="wide-field">
</div>
<div class="control-row">
<label>Color:</label>
<input type="color" id="header-color-input" value="#f35dcb">
</div>
<div class="control-row">
<label>Size:</label>
<input type="number" id="header-size-input" value="80" min="12" max="100" step="1">
</div>
<div class="control-row">
<label>Position:</label>
<input type="number" id="header-offset-input" value="-220" step="10">
</div>
</div>
</div>
<!-- Glow Effect Settings Section -->
<div class="control-section">
<div class="section-header">
<span>Glow Effect Settings</span>
<span class="toggle-indicator"></span>
</div>
<div class="section-content">
<div class="control-row">
<label>Glow Color:</label>
<input type="color" id="glow-color-input" value="#f35dcb">
</div>
<div class="control-row">
<label>Glow Intensity (px):</label>
<input type="number" id="glow-intensity-input" value="80" min="10" max="200" step="5">
</div>
<div class="control-row">
<label>Glow Opacity:</label>
<input type="number" id="glow-opacity-input" value="0.6" min="0" max="1" step="0.05">
</div>
<div class="control-row">
<label>White Glow (px):</label>
<input type="number" id="glow-white-intensity-input" value="40" min="0" max="200" step="5">
</div>
<div class="control-row">
<label>Outline Thickness (px):</label>
<input type="number" id="glow-outline-input" value="8" min="0" max="30" step="1">
</div>
<div class="control-row">
<label>Pulse Duration (sec):</label>
<input type="number" id="glow-pulse-duration-input" value="1.2" min="0.2" max="5" step="0.1">
</div>
</div>
</div>
<!-- Footer Settings Section -->
<div class="control-section">
<div class="section-header">
<span>Footer Settings</span>
<span class="toggle-indicator"></span>
</div>
<div class="section-content">
<div class="control-row">
<label>Text:</label>
<input type="text" id="footer-text-input" value="Enter @ jackbox.tv" class="wide-field">
</div>
<div class="control-row">
<label>Color:</label>
<input type="color" id="footer-color-input" value="#f35dcb">
</div>
<div class="control-row">
<label>Size:</label>
<input type="number" id="footer-size-input" value="80" min="12" max="100" step="1">
</div>
<div class="control-row">
<label>Position:</label>
<input type="number" id="footer-offset-input" value="160" step="10">
</div>
</div>
</div>
<!-- Timing Settings Section -->
<div class="control-section">
<div class="section-header">
<span>Timing Settings</span>
<span class="toggle-indicator"></span>
</div>
<div class="section-content">
<div class="control-group">
<div class="control-row">
<label>Header Appear (sec):</label>
<input type="number" id="header-appear-delay" value="10" min="0" max="30">
</div>
<div class="control-row">
<label>Duration:</label>
<input type="number" id="header-appear-duration" value="10" min="1" max="20">
</div>
</div>
<div class="control-group">
<div class="control-row">
<label>Header Hide (sec):</label>
<input type="number" id="header-hide-time" value="240" min="10" max="600">
</div>
<div class="control-row">
<label>Duration:</label>
<input type="number" id="header-hide-duration" value="60" min="1" max="120">
</div>
</div>
<div class="control-group">
<div class="control-row">
<label>Line 1 Appear (sec):</label>
<input type="number" id="line1-appear-delay" value="20" min="0" max="30">
</div>
<div class="control-row">
<label>Duration:</label>
<input type="number" id="line1-appear-duration" value="30" min="1" max="120">
</div>
</div>
<div class="control-group">
<div class="control-row">
<label>Line 1 Hide (sec):</label>
<input type="number" id="line1-hide-time" value="240" min="10" max="600">
</div>
<div class="control-row">
<label>Duration:</label>
<input type="number" id="line1-hide-duration" value="60" min="1" max="120">
</div>
</div>
<div class="control-group">
<div class="control-row">
<label>Line 2 Appear (sec):</label>
<input type="number" id="line2-appear-delay" value="40" min="0" max="300">
</div>
<div class="control-row">
<label>Duration:</label>
<input type="number" id="line2-appear-duration" value="60" min="1" max="120">
</div>
</div>
<div class="control-group">
<div class="control-row">
<label>Line 2 Hide (sec):</label>
<input type="number" id="line2-hide-time" value="240" min="10" max="600">
</div>
<div class="control-row">
<label>Duration:</label>
<input type="number" id="line2-hide-duration" value="60" min="1" max="120">
</div>
</div>
</div>
</div>
<!-- Audio Settings Section -->
<div class="control-section">
<div class="section-header">
<span>Audio Settings</span>
<span class="toggle-indicator"></span>
</div>
<div class="section-content">
<div class="control-row">
<label>Sound Effect:</label>
<input type="checkbox" id="sound-enabled" checked>
</div>
<div class="control-row">
<label>Volume:</label>
<input type="range" id="volume-slider" min="0" max="1" step="0.1" value="0.5">
<span id="volume-value">50%</span>
</div>
<div class="control-row">
<label>Sound URL:</label>
</div>
<div class="control-row">
<input type="text" id="sound-url-input" value="https://feed.falsefinish.club/HSO/Audio/Room%20Code%20Theme.mp3" class="wide-field">
</div>
<div class="control-row">
<button id="test-sound-btn">Test Sound</button>
</div>
</div>
</div>
<!-- Player List Settings Section -->
<div class="control-section">
<div class="section-header">
<span>Player List Settings</span>
<span class="toggle-indicator"></span>
</div>
<div class="section-content">
<div class="control-row">
<label>Enable Player List:</label>
<input type="checkbox" id="player-list-enabled">
</div>
<div class="control-row">
<label>Position:</label>
<select id="player-list-position">
<option value="left">Left Side</option>
<option value="right">Right Side</option>
</select>
</div>
<div class="control-row">
<label>Font Size:</label>
<input type="number" id="player-list-font-size" value="24" min="10" max="60" step="1">
</div>
<div class="control-row">
<label>Text Color:</label>
<input type="color" id="player-list-text-color" value="#ffffff">
</div>
<div class="control-row">
<label>Empty Slot Color:</label>
<input type="color" id="player-list-empty-color" value="#444444">
</div>
<div class="control-row">
<label>Vertical Offset:</label>
<input type="number" id="player-list-offset" value="0" step="10">
</div>
<div class="control-row">
<label>Horizontal Offset:</label>
<input type="number" id="player-list-offset-x" value="0" step="10">
</div>
</div>
</div>
<!-- Connection Settings Section -->
<div class="control-section">
<div class="section-header">
<span>Connection Settings</span>
<span class="toggle-indicator"></span>
</div>
<div class="section-content">
<div class="status-row">
<span class="status-dot disconnected" id="ws-status-dot"></span>
<span class="status-text" id="ws-status-text">Disconnected</span>
</div>
<div class="control-row">
<label>API URL:</label>
</div>
<div class="control-row">
<input type="text" id="api-url-input" placeholder="https://your-api-url" class="wide-field" style="text-transform: none;">
</div>
<div class="control-row">
<label>API Key:</label>
</div>
<div class="control-row">
<input type="password" id="api-key-input" placeholder="Enter API key">
</div>
<div class="control-row">
<button id="ws-connect-btn">Connect</button>
</div>
<div class="control-row" style="display: none;" id="ws-disconnect-row">
<button id="ws-disconnect-btn">Disconnect</button>
</div>
</div>
</div>
<!-- Overlay Manager Section -->
<div class="control-section" id="manager-dashboard">
<div class="section-header">
<span>Overlay Manager</span>
<span class="toggle-indicator"></span>
</div>
<div class="section-content">
<div class="dashboard-grid">
<span class="label">State:</span>
<span><span id="manager-state">IDLE</span></span>
<span class="label">Room:</span>
<span id="manager-room-code"></span>
<span class="label">Session:</span>
<span id="manager-session-id"></span>
<span class="label">Game:</span>
<span id="manager-game-title"></span>
<span class="label">Players:</span>
<span id="manager-player-count">0/?</span>
</div>
<table class="component-table">
<thead>
<tr><th>Component</th><th>Status</th><th>Override</th></tr>
</thead>
<tbody>
<tr>
<td>Room Code</td>
<td id="status-roomCode">Inactive</td>
<td><select id="override-roomCode"></select></td>
</tr>
<tr>
<td>Audio</td>
<td id="status-audio">Inactive</td>
<td><select id="override-audio"></select></td>
</tr>
<tr>
<td>Player List</td>
<td id="status-playerList">Inactive</td>
<td><select id="override-playerList"></select></td>
</tr>
</tbody>
</table>
<div id="manager-event-log"></div>
</div>
</div>
<!-- Position toggle -->
<div class="control-row" style="margin-top: 5px;">
<label>Position Controls Below:</label>
<input type="checkbox" id="position-toggle">
</div>
<!-- Action buttons -->
<div class="action-buttons">
<button id="update-btn">Update</button>
<button id="preview-btn">Preview</button>
</div>
</div>
</div>
<button id="toggle-display-btn">Hide Display</button>
<button id="show-controls-btn">Show Controls</button>
<script src="js/state-manager.js"></script>
<script src="js/websocket-client.js"></script>
<script src="js/room-code-display.js"></script>
<script src="js/audio-controller.js"></script>
<script src="js/player-list.js"></script>
<script src="js/controls.js"></script>
<script>
(function () {
var O = window.OBS;
var manager = new O.OverlayManager();
var roomCodeDisplay = new O.RoomCodeDisplay();
roomCodeDisplay.init(
{
header: document.getElementById('header'),
footer: document.getElementById('footer'),
codePart1: document.getElementById('code-part1'),
codePart2: document.getElementById('code-part2'),
},
{
code1: document.getElementById('code1-input'),
code2: document.getElementById('code2-input'),
color1: document.getElementById('color1-input'),
color2: document.getElementById('color2-input'),
offset1: document.getElementById('offset1-input'),
offset2: document.getElementById('offset2-input'),
size: document.getElementById('size-input'),
cycle: document.getElementById('cycle-input'),
headerText: document.getElementById('header-text-input'),
headerColor: document.getElementById('header-color-input'),
headerSize: document.getElementById('header-size-input'),
headerOffset: document.getElementById('header-offset-input'),
footerText: document.getElementById('footer-text-input'),
footerColor: document.getElementById('footer-color-input'),
footerSize: document.getElementById('footer-size-input'),
footerOffset: document.getElementById('footer-offset-input'),
headerAppearDelay: document.getElementById('header-appear-delay'),
headerAppearDuration: document.getElementById('header-appear-duration'),
headerHideTime: document.getElementById('header-hide-time'),
headerHideDuration: document.getElementById('header-hide-duration'),
line1AppearDelay: document.getElementById('line1-appear-delay'),
line1AppearDuration: document.getElementById('line1-appear-duration'),
line1HideTime: document.getElementById('line1-hide-time'),
line1HideDuration: document.getElementById('line1-hide-duration'),
line2AppearDelay: document.getElementById('line2-appear-delay'),
line2AppearDuration: document.getElementById('line2-appear-duration'),
line2HideTime: document.getElementById('line2-hide-time'),
line2HideDuration: document.getElementById('line2-hide-duration'),
glowColor: document.getElementById('glow-color-input'),
glowIntensity: document.getElementById('glow-intensity-input'),
glowOpacity: document.getElementById('glow-opacity-input'),
glowWhiteIntensity: document.getElementById('glow-white-intensity-input'),
glowOutline: document.getElementById('glow-outline-input'),
glowPulseDuration: document.getElementById('glow-pulse-duration-input'),
}
);
manager.registerComponent('roomCode', roomCodeDisplay);
var audioController = new O.AudioController();
audioController.init(
document.getElementById('theme-sound'),
{
enabled: document.getElementById('sound-enabled'),
volume: document.getElementById('volume-slider'),
soundUrl: document.getElementById('sound-url-input'),
}
);
manager.registerComponent('audio', audioController);
var playerList = new O.PlayerList();
playerList.init(
document.getElementById('player-list-container'),
{
enabled: document.getElementById('player-list-enabled'),
position: document.getElementById('player-list-position'),
fontSize: document.getElementById('player-list-font-size'),
textColor: document.getElementById('player-list-text-color'),
emptyColor: document.getElementById('player-list-empty-color'),
offset: document.getElementById('player-list-offset'),
offsetX: document.getElementById('player-list-offset-x'),
},
{
headerAppearDelay: document.getElementById('header-appear-delay'),
headerAppearDuration: document.getElementById('header-appear-duration'),
}
);
manager.registerComponent('playerList', playerList);
var statusHandler = O.initConnectionStatusHandler();
var wsClient = new O.WebSocketClient({
onStatusChange: statusHandler,
onEvent: function (type, data) { manager.handleEvent(type, data); },
onSessionSubscribed: function (sessionId) {
console.log('[Overlay] Subscribed to session:', sessionId);
},
});
O.initControls(manager, wsClient, {
roomCode: roomCodeDisplay,
audio: audioController,
playerList: playerList,
});
var savedUrl = localStorage.getItem('jackbox-api-url');
var savedKey = localStorage.getItem('jackbox-api-key');
var apiUrlInput = document.getElementById('api-url-input');
var apiKeyInput = document.getElementById('api-key-input');
if (savedUrl && apiUrlInput) apiUrlInput.value = savedUrl;
if (savedKey && apiKeyInput) apiKeyInput.value = savedKey;
if (savedUrl && savedKey) {
setTimeout(function () { wsClient.connect(savedUrl, savedKey); }, 500);
}
window.__overlay = { manager: manager, wsClient: wsClient, roomCodeDisplay: roomCodeDisplay, audioController: audioController, playerList: playerList };
})();
</script>
</body>
</html>