zebra-spaces: games are proper spotlight tiles — click broadcasts + iframe loads big
Per fox: each game is its own tile in the left column. Click makes it
the spotlight (big middle slot) AND broadcasts via the same spotlight
channel cameras/screens use, so the log says 'alice now viewing
unmario' and popularity-sort can rank games by viewer count.
Changes:
- GAMES = { unmario, cake } map (id → label + src) — single source
of truth, easy to extend
- buildGameThumb(id) creates a 16:9 clickable card with the game name
and a 'viewing' badge that surfaces when spotlit
- renderGamesColumn() rebuilds #games-thumbs from the GAMES map at
page init (idempotent, can re-run on config change)
- thumbElementFor(kind, pubHex) unified thumb lookup so spotlight
unmark-previous works across camera / screen / game without case
branches in the hot path
- setSpotlight refactored: previous-thumb cleanup runs first, then
branches on kind=game (build iframe-backed big tile) vs kind=
camera/screen (existing video-backed path). Both still go through
the same spotlights.set(myUUID, ...) + broadcastSpotlight() +
reorderTiles() at the end, so the broadcast path is unified.
- ownerLabel('game', id) → game label so log lines read 'alice now
viewing unmario' instead of 'alice now viewing <hex>'s game'
- old #game-frame + game-tabs DOM and handler removed; their CSS too
This commit is contained in:
parent
6a72e77518
commit
04e13e1f3a
1 changed files with 123 additions and 54 deletions
|
|
@ -176,26 +176,44 @@
|
|||
position: relative;
|
||||
}
|
||||
.controls { min-width: 0; }
|
||||
/* games live in the LEFT column now (under cameras + screens-thumbs)
|
||||
* so people can play casually during a meeting without competing
|
||||
* with screen-shares for the middle slot. The tabs wrap if the
|
||||
* column is narrow; the iframe is sized for the column. */
|
||||
/* games live in the LEFT column under cameras + screens-thumbs as
|
||||
* proper tile-thumbs. Click one to spotlight it in the middle (an
|
||||
* iframe loads at full size); the click also broadcasts the spotlight
|
||||
* choice through the same channel cameras/screens use, so the log
|
||||
* shows 'alice now viewing unmario' and the thumb-order popularity
|
||||
* sort knows people are watching. */
|
||||
.games-h2 { margin-top: 0.5rem; }
|
||||
.game-tabs {
|
||||
display: grid; grid-auto-columns: minmax(0, 1fr); grid-auto-flow: row;
|
||||
gap: 0.25rem; margin-bottom: 0.35rem;
|
||||
#games-thumbs { display: grid; grid-template-columns: 1fr; gap: 0.45rem; }
|
||||
.game-thumb {
|
||||
border: 1px solid #ddd; background: #fff; padding: 0;
|
||||
display: grid; grid-template-columns: 1fr; gap: 0;
|
||||
cursor: pointer; position: relative;
|
||||
aspect-ratio: 16 / 9;
|
||||
}
|
||||
.game-tabs button {
|
||||
background: #fff; color: #000; border: 1px solid #000;
|
||||
padding: 0.25rem 0.5rem; font-family: monospace; font-size: 0.72rem;
|
||||
cursor: pointer; text-align: left;
|
||||
.game-thumb:hover { outline: 1px solid #888; }
|
||||
.game-thumb .game-label {
|
||||
display: grid; place-items: center; text-align: center;
|
||||
font-family: monospace; font-size: 0.85rem; padding: 0.5rem;
|
||||
background: #f0f0f0; color: #333;
|
||||
}
|
||||
.game-tabs button.active { background: #000; color: #fff; }
|
||||
.game-tabs button:hover:not(.active) { background: #f0f0f0; }
|
||||
.game-tile-frame {
|
||||
width: 100%; aspect-ratio: 4 / 3;
|
||||
border: 1px solid #ddd; background: #fff; display: block;
|
||||
/* spotlit game shows the iframe at full size in the middle column */
|
||||
#spotlight > .game-tile {
|
||||
border: 1px solid #ddd; background: #fff;
|
||||
display: grid; grid-template-rows: 1fr auto;
|
||||
}
|
||||
#spotlight > .game-tile iframe {
|
||||
width: 100%; height: 80vh;
|
||||
border: none; background: #fff; display: block;
|
||||
}
|
||||
#spotlight > .game-tile .screen-meta {
|
||||
background: #f0f0f0; color: #333; font-size: 0.7rem; padding: 0.3rem 0.5rem;
|
||||
display: grid; grid-template-columns: 1fr auto; align-items: center;
|
||||
}
|
||||
html.theme-dark .game-thumb { background: #000; border-color: #000; }
|
||||
html.theme-dark .game-thumb .game-label { background: #111; color: #ddd; }
|
||||
html.theme-dark #spotlight > .game-tile { background: #000; border-color: #000; }
|
||||
html.theme-dark #spotlight > .game-tile iframe { background: #000; }
|
||||
html.theme-dark #spotlight > .game-tile .screen-meta { background: #111; color: #ddd; }
|
||||
@media (max-width: 800px) {
|
||||
body { padding: 1.25rem; }
|
||||
/* mobile: single column, all three sections stack to grid-column:1 */
|
||||
|
|
@ -513,17 +531,7 @@ try {
|
|||
<h2 class="screens-thumbs-h2 hidden">screens</h2>
|
||||
<div id="screens-thumbs"></div>
|
||||
<h2 class="games-h2">games</h2>
|
||||
<div class="game-tabs" id="game-tabs">
|
||||
<button type="button" class="active" data-game="unmario"
|
||||
data-src="https://unmario.com/">unmario</button>
|
||||
<button type="button" data-game="cake"
|
||||
data-src="https://cuppcb.com/games/cake-murder-adventure/">cake murder adventure</button>
|
||||
</div>
|
||||
<iframe id="game-frame" class="game-tile-frame"
|
||||
src="https://unmario.com/"
|
||||
title="game"
|
||||
loading="lazy"
|
||||
referrerpolicy="no-referrer"></iframe>
|
||||
<div id="games-thumbs"></div>
|
||||
</aside>
|
||||
|
||||
<main class="timeline">
|
||||
|
|
@ -1505,11 +1513,50 @@ function memberOf(pubHex){
|
|||
}
|
||||
return null;
|
||||
}
|
||||
/* games are tiles too. The 'pubHex' for a game is its short id below;
|
||||
* spotlight broadcasts use 'game:<id>' as the key, so the room sees
|
||||
* 'alice now viewing unmario' in the log just like 'alice now viewing
|
||||
* bob's screen'. */
|
||||
const GAMES = {
|
||||
unmario: { label: 'unmario', src: 'https://unmario.com/' },
|
||||
cake: { label: 'cake murder adventure', src: 'https://cuppcb.com/games/cake-murder-adventure/' },
|
||||
};
|
||||
function ownerLabel(kind, pubHex){
|
||||
if (kind === 'game'){
|
||||
const g = GAMES[pubHex];
|
||||
return g ? g.label : pubHex;
|
||||
}
|
||||
const m = memberOf(pubHex);
|
||||
const handle = m ? (m.mm.handle || shortHex(pubHex)) : shortHex(pubHex);
|
||||
return handle + "'s " + (TILE_KINDS[kind] ? TILE_KINDS[kind].labelPrefix : kind);
|
||||
}
|
||||
/* game thumbs live in #games-thumbs and click → setSpotlight('game', id) */
|
||||
function buildGameThumb(id){
|
||||
const g = GAMES[id]; if (!g) return null;
|
||||
const tile = document.createElement('div');
|
||||
tile.className = 'game-thumb';
|
||||
tile.dataset.pubHex = id;
|
||||
tile.dataset.kind = 'game';
|
||||
const label = document.createElement('div');
|
||||
label.className = 'game-label';
|
||||
label.textContent = g.label;
|
||||
tile.appendChild(label);
|
||||
const view = document.createElement('div'); view.className = 'viewing-badge'; view.textContent = 'viewing';
|
||||
tile.appendChild(view);
|
||||
tile.addEventListener('click', () => {
|
||||
if (spotlight && spotlight.kind === 'game' && spotlight.pubHex === id) return;
|
||||
setSpotlight('game', id);
|
||||
});
|
||||
return tile;
|
||||
}
|
||||
function renderGamesColumn(){
|
||||
const c = $('games-thumbs'); if (!c) return;
|
||||
c.innerHTML = '';
|
||||
for (const id of Object.keys(GAMES)){
|
||||
const t = buildGameThumb(id);
|
||||
if (t) c.appendChild(t);
|
||||
}
|
||||
}
|
||||
/* tile score = role-based boost on the OWNER + count of current viewers.
|
||||
* Host always at top, co-hosts second, then speakers sorted by viewers.
|
||||
* Listeners can't publish so they never appear. */
|
||||
|
|
@ -1605,24 +1652,52 @@ function buildTile(kind, pubHex, label, opts){
|
|||
return { tile, video, fsBtn };
|
||||
}
|
||||
|
||||
function setSpotlight(kind, pubHex){
|
||||
const entry = getEntry(kind, pubHex);
|
||||
if (!entry){ if (spotlight) clearSpotlightDOM(); spotlight = null; updateContainerVisibility(); return; }
|
||||
/* unmark the previous spotlight's thumb, tear its big tile DOM */
|
||||
if (spotlight){
|
||||
const prev = getEntry(spotlight.kind, spotlight.pubHex);
|
||||
if (prev) prev.tile.classList.remove('viewing');
|
||||
clearSpotlightDOM();
|
||||
/* find a thumb element by kind+pubHex regardless of whether it's a
|
||||
* camera/screen tile or a game card. Returned element gets the
|
||||
* 'viewing' class toggle so the thumb→spotlight mapping is obvious. */
|
||||
function thumbElementFor(kind, pubHex){
|
||||
if (kind === 'game'){
|
||||
return document.querySelector('.game-thumb[data-pub-hex="'+pubHex+'"]');
|
||||
}
|
||||
const e = getEntry(kind, pubHex);
|
||||
return e ? e.tile : null;
|
||||
}
|
||||
|
||||
function setSpotlight(kind, pubHex){
|
||||
/* unmark previous spotlight's thumb + tear its big tile */
|
||||
if (spotlight){
|
||||
const prevThumb = thumbElementFor(spotlight.kind, spotlight.pubHex);
|
||||
if (prevThumb) prevThumb.classList.remove('viewing');
|
||||
clearSpotlightDOM();
|
||||
spotlight = null;
|
||||
}
|
||||
if (kind === 'game'){
|
||||
const g = GAMES[pubHex]; if (!g){ updateContainerVisibility(); return; }
|
||||
const big = document.createElement('div'); big.className = 'game-tile';
|
||||
const iframe = document.createElement('iframe');
|
||||
iframe.src = g.src;
|
||||
iframe.title = g.label;
|
||||
iframe.setAttribute('referrerpolicy', 'no-referrer');
|
||||
iframe.loading = 'eager';
|
||||
const meta = document.createElement('div'); meta.className = 'screen-meta';
|
||||
const who = document.createElement('span'); who.textContent = 'game: ' + g.label;
|
||||
meta.appendChild(who);
|
||||
big.appendChild(iframe); big.appendChild(meta);
|
||||
$('spotlight').appendChild(big);
|
||||
const thumb = thumbElementFor('game', pubHex);
|
||||
if (thumb) thumb.classList.add('viewing');
|
||||
spotlight = { kind, pubHex, tile: big, video: null };
|
||||
} else {
|
||||
const entry = getEntry(kind, pubHex);
|
||||
if (!entry){ updateContainerVisibility(); return; }
|
||||
const big = buildTile(kind, pubHex, entry.label, { thumb: false });
|
||||
big.video.srcObject = entry.video.srcObject;
|
||||
try { const p = big.video.play(); if (p && p.catch) p.catch(()=>{ big.tile.classList.add('needs-tap'); }); } catch(_){}
|
||||
$('spotlight').appendChild(big.tile);
|
||||
entry.tile.classList.add('viewing');
|
||||
spotlight = { kind, pubHex, tile: big.tile, video: big.video };
|
||||
}
|
||||
/* build a fresh big tile for the new selection, hand it the same stream */
|
||||
const big = buildTile(kind, pubHex, entry.label, { thumb: false });
|
||||
big.video.srcObject = entry.video.srcObject;
|
||||
try { const p = big.video.play(); if (p && p.catch) p.catch(()=>{ big.tile.classList.add('needs-tap'); }); } catch(_){}
|
||||
$('spotlight').appendChild(big.tile);
|
||||
entry.tile.classList.add('viewing');
|
||||
spotlight = { kind, pubHex, tile: big.tile, video: big.video };
|
||||
updateContainerVisibility();
|
||||
/* track our own choice, broadcast it, log it, re-sort */
|
||||
if (myUUID){
|
||||
spotlights.set(myUUID, spotlightKey());
|
||||
logSpotlightChange(myUUID, spotlightKey());
|
||||
|
|
@ -3214,15 +3289,9 @@ $('btn-copy-share').addEventListener('click', async () => {
|
|||
setTimeout(() => { joinSpace().catch(e => logLine('err', 'auto-rejoin failed: ' + e.message)); }, 100);
|
||||
})();
|
||||
|
||||
/* game-tabs: click a button → swap iframe src + mark active. data-src is
|
||||
* the only source of truth so new games just need a button. */
|
||||
$('game-tabs').addEventListener('click', (ev) => {
|
||||
const b = ev.target.closest('button[data-src]');
|
||||
if (!b) return;
|
||||
const frame = $('game-frame');
|
||||
if (frame.src !== b.dataset.src) frame.src = b.dataset.src;
|
||||
for (const el of $('game-tabs').querySelectorAll('button')) el.classList.toggle('active', el === b);
|
||||
});
|
||||
/* games as left-column tiles — click to spotlight, broadcast through
|
||||
* the same channel cameras/screens use so the room sees the choice */
|
||||
renderGamesColumn();
|
||||
$('btn-leave').addEventListener('click', async () => {
|
||||
wantConnected = false;
|
||||
roomMachines.call.send('LEAVE');
|
||||
|
|
@ -3262,8 +3331,8 @@ logLine('', 'ready — pick a handle, type a rendezvous code, enter the space');
|
|||
|
||||
<footer style="margin:2.2rem auto 0;font-size:0.65rem;color:#999;line-height:1.7;word-break:break-all;font-family:monospace">
|
||||
<span id="pi-seal" style="color:#777;cursor:default;user-select:none" title="">page integrity</span> · built <span class="stamp-date">2026-06-02</span><br>
|
||||
md5 <span class="stamp-md5">02bfc336d703a55e23a2ce5f1a69adf1</span><br>
|
||||
sha256 <span class="stamp-sha">f554d4c9c6b226f5b79954222c75a48a38f031154d6de8cd6b4df0fd8bd07184</span><br>
|
||||
md5 <span class="stamp-md5">435fd27480ef8c017d88914d3aa9f308</span><br>
|
||||
sha256 <span class="stamp-sha">2757004e72c5cc250a93476d1bd642e1e6669e589e76387eedd185cb6d5d5aa2</span><br>
|
||||
<span style="color:#bbb">hashes are of this page with these two fields zeroed — to verify, blank them and re-hash</span><br>
|
||||
<span style="color:#bbb">one self-contained file — <strong>save a copy</strong> and verify against these hashes; point at your own servers with ?signal= and ?turncred=, or <a href="host-your-own.html" style="color:#999">host your own community</a></span>
|
||||
</footer>
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue