updated
This commit is contained in:
40
server.py
40
server.py
@@ -28,6 +28,8 @@ captchas = {} # captcha_id -> {answer, expires}
|
||||
reconnect_tokens = {} # token -> {username, expires}
|
||||
session_start = None
|
||||
trippy_mode = False
|
||||
schizo_mode = False
|
||||
pong_mode = False
|
||||
|
||||
ALLOWED_CHARS = string.ascii_letters + string.digits + '_-'
|
||||
|
||||
@@ -37,7 +39,20 @@ DIAL_CODES = {
|
||||
'*420#': 'trippy_toggle', # toggles UI trippy mode globally for everyone
|
||||
'*1337#': 'rainbow_nick_toggle', # toggles the dialer's own rainbow nick
|
||||
'*101#': 'knock', # plays a knock sound for everyone in the room
|
||||
'*666#': 'schizo_toggle', # toggles schizo mode (subtle UI shake/wiggle/morph)
|
||||
'*9059#': 'pong_toggle', # toggles pong mode (webcam tiles bounce around)
|
||||
'*#06#': 'show_codes', # privately reveals all dial codes to the dialer
|
||||
}
|
||||
|
||||
# Human-readable descriptions, sent privately to the dialer who hits *#06#.
|
||||
DIAL_CODE_DESCRIPTIONS = [
|
||||
('*420#', 'Toggle trippy color mode (everyone)'),
|
||||
('*1337#', 'Toggle rainbow nickname (just you)'),
|
||||
('*101#', 'Play a knock sound (everyone)'),
|
||||
('*666#', 'Toggle schizo mode (everyone)'),
|
||||
('*9059#', 'Toggle pong mode (everyone)'),
|
||||
('*#06#', 'Show this list (just you)'),
|
||||
]
|
||||
DIAL_MAX_LEN = 32
|
||||
|
||||
|
||||
@@ -239,7 +254,7 @@ async def handle_message(client_id: str, data: dict):
|
||||
:param data: The data from the client
|
||||
'''
|
||||
|
||||
global session_start, trippy_mode
|
||||
global session_start, trippy_mode, schizo_mode, pong_mode
|
||||
msg_type = data.get('type')
|
||||
|
||||
if msg_type == 'join':
|
||||
@@ -287,7 +302,9 @@ async def handle_message(client_id: str, data: dict):
|
||||
'session_start' : session_start,
|
||||
'max_cameras' : config.MAX_CAMERAS,
|
||||
'reconnect_token' : reconnect_token,
|
||||
'trippy_mode' : trippy_mode
|
||||
'trippy_mode' : trippy_mode,
|
||||
'schizo_mode' : schizo_mode,
|
||||
'pong_mode' : pong_mode
|
||||
})
|
||||
|
||||
await broadcast(client_id, {
|
||||
@@ -351,7 +368,9 @@ async def handle_message(client_id: str, data: dict):
|
||||
'session_start' : session_start,
|
||||
'max_cameras' : config.MAX_CAMERAS,
|
||||
'reconnect_token' : new_token,
|
||||
'trippy_mode' : trippy_mode
|
||||
'trippy_mode' : trippy_mode,
|
||||
'schizo_mode' : schizo_mode,
|
||||
'pong_mode' : pong_mode
|
||||
})
|
||||
|
||||
await broadcast(client_id, {
|
||||
@@ -436,6 +455,21 @@ async def handle_message(client_id: str, data: dict):
|
||||
elif action == 'knock':
|
||||
logging.info(f'[{client_id}] Knock')
|
||||
await broadcast_all({'type': 'play_sound', 'sound': 'knock'})
|
||||
elif action == 'schizo_toggle':
|
||||
schizo_mode = not schizo_mode
|
||||
logging.info(f'[{client_id}] Schizo mode -> {schizo_mode}')
|
||||
await broadcast_all({'type': 'schizo_status', 'enabled': schizo_mode})
|
||||
elif action == 'pong_toggle':
|
||||
pong_mode = not pong_mode
|
||||
logging.info(f'[{client_id}] Pong mode -> {pong_mode}')
|
||||
await broadcast_all({'type': 'pong_status', 'enabled': pong_mode})
|
||||
elif action == 'show_codes':
|
||||
# Private reply to just the dialer - other clients never see the codes.
|
||||
logging.info(f'[{client_id}] Dial code list requested')
|
||||
await clients[client_id]['ws'].send_json({
|
||||
'type' : 'dial_codes_list',
|
||||
'codes' : [{'code': c, 'desc': d} for (c, d) in DIAL_CODE_DESCRIPTIONS]
|
||||
})
|
||||
elif action == 'rainbow_nick_toggle':
|
||||
# Per-user toggle: only flips the dialer's own nick. Broadcast so every
|
||||
# other client renders the rainbow effect on this user in their list.
|
||||
|
||||
@@ -335,6 +335,21 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Dial codes list modal (shown only to the dialer who hits *#06#) -->
|
||||
<div id="dial-codes-modal" class="modal hidden">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h3>Dial Codes</h3>
|
||||
<button id="dial-codes-close" class="modal-close-btn" title="Close">
|
||||
<svg viewBox="0 0 24 24" fill="currentColor">
|
||||
<path d="M19 6.41L17.59 5 12 10.59 6.41 5 5 6.41 10.59 12 5 17.59 6.41 19 12 13.41 17.59 19 19 17.59 13.41 12z"/>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
<ul id="dial-codes-list" class="dial-codes-list"></ul>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Hidden container holding one <audio> element per remote peer. Audio playback flows
|
||||
through these elements (not AudioContext.destination) so mobile autoplay works
|
||||
reliably after the user clicks Connect. Kept off-screen, not display:none, so
|
||||
|
||||
@@ -330,8 +330,10 @@ function handleSignal(data) {
|
||||
state.maxCameras = data.max_cameras;
|
||||
state.reconnectToken = data.reconnect_token || null;
|
||||
|
||||
// Adopt server's current trippy state (set by another user before we joined).
|
||||
// Adopt server's current dial-code modes (set by other users before we joined).
|
||||
setTrippyMode(!!data.trippy_mode);
|
||||
setSchizoMode(!!data.schizo_mode);
|
||||
setPongMode(!!data.pong_mode);
|
||||
|
||||
// Save username on successful login
|
||||
saveUsername(state.username);
|
||||
@@ -491,10 +493,22 @@ function handleSignal(data) {
|
||||
setTrippyMode(!!data.enabled);
|
||||
break;
|
||||
|
||||
case 'schizo_status':
|
||||
setSchizoMode(!!data.enabled);
|
||||
break;
|
||||
|
||||
case 'pong_status':
|
||||
setPongMode(!!data.enabled);
|
||||
break;
|
||||
|
||||
case 'play_sound':
|
||||
playSound(data.sound);
|
||||
break;
|
||||
|
||||
case 'dial_codes_list':
|
||||
showDialCodes(data.codes || []);
|
||||
break;
|
||||
|
||||
case 'nick_status':
|
||||
// Per-user rainbow nick toggle. Server tells us when ANY user (including
|
||||
// us) flips theirs - we just mirror it into local state and re-render.
|
||||
|
||||
@@ -15,6 +15,11 @@ function initDialListeners() {
|
||||
$('dial-clear')?.addEventListener('click', clearDial);
|
||||
$('dial-submit')?.addEventListener('click', submitDial);
|
||||
|
||||
$('dial-codes-close')?.addEventListener('click', closeDialCodes);
|
||||
$('dial-codes-modal')?.addEventListener('click', (e) => {
|
||||
if (e.target.id === 'dial-codes-modal') closeDialCodes();
|
||||
});
|
||||
|
||||
document.querySelectorAll('.dial-key').forEach(btn => {
|
||||
btn.addEventListener('click', () => pressDialKey(btn.dataset.key));
|
||||
});
|
||||
@@ -61,9 +66,164 @@ function submitDial() {
|
||||
closeDial();
|
||||
}
|
||||
|
||||
// Server reply to *#06# - private to the dialer. Renders the codes into the modal.
|
||||
function showDialCodes(codes) {
|
||||
const list = $('dial-codes-list');
|
||||
if (!list) return;
|
||||
list.innerHTML = '';
|
||||
codes.forEach(({ code, desc }) => {
|
||||
const li = document.createElement('li');
|
||||
const codeEl = document.createElement('code');
|
||||
codeEl.textContent = code;
|
||||
const descEl = document.createElement('span');
|
||||
descEl.textContent = desc;
|
||||
li.appendChild(codeEl);
|
||||
li.appendChild(descEl);
|
||||
list.appendChild(li);
|
||||
});
|
||||
$('dial-codes-modal')?.classList.remove('hidden');
|
||||
}
|
||||
|
||||
function closeDialCodes() {
|
||||
$('dial-codes-modal')?.classList.add('hidden');
|
||||
}
|
||||
|
||||
// Apply or remove the trippy-mode class on <body>. Server tells us when to flip via
|
||||
// 'trippy_status' broadcasts; new joiners get the current value in the 'users' message.
|
||||
function setTrippyMode(enabled) {
|
||||
state.trippyMode = !!enabled;
|
||||
document.body.classList.toggle('trippy-mode', state.trippyMode);
|
||||
}
|
||||
|
||||
function setSchizoMode(enabled) {
|
||||
state.schizoMode = !!enabled;
|
||||
document.body.classList.toggle('schizo-mode', state.schizoMode);
|
||||
}
|
||||
|
||||
function setPongMode(enabled) {
|
||||
state.pongMode = !!enabled;
|
||||
document.body.classList.toggle('pong-mode', state.pongMode);
|
||||
if (state.pongMode) startPong();
|
||||
else stopPong();
|
||||
}
|
||||
|
||||
// ---------- Pong mode: webcam tiles bounce around ----------
|
||||
// Tracks each .video-tile's position+velocity and runs an rAF loop.
|
||||
// Tiles get position:absolute via CSS; we update transform each frame.
|
||||
|
||||
const pong = {
|
||||
raf: null,
|
||||
state: new Map(), // tileId -> { x, y, vx, vy, w, h }
|
||||
container: null
|
||||
};
|
||||
|
||||
function startPong() {
|
||||
pong.container = document.getElementById('video-grid');
|
||||
if (!pong.container) return;
|
||||
pongRefreshTiles();
|
||||
if (!pong.raf) pong.raf = requestAnimationFrame(pongTick);
|
||||
}
|
||||
|
||||
function stopPong() {
|
||||
if (pong.raf) cancelAnimationFrame(pong.raf);
|
||||
pong.raf = null;
|
||||
document.querySelectorAll('#video-grid .video-tile').forEach(t => {
|
||||
t.style.transform = '';
|
||||
t.style.left = '';
|
||||
t.style.top = '';
|
||||
});
|
||||
pong.state.clear();
|
||||
pong.container = null;
|
||||
}
|
||||
|
||||
// Re-sync the tile map with what's actually in the DOM. Called whenever
|
||||
// updateVideoGrid rebuilds tiles while pong mode is active.
|
||||
function pongRefreshTiles() {
|
||||
if (!pong.container) return;
|
||||
const tiles = pong.container.querySelectorAll('.video-tile');
|
||||
const seen = new Set();
|
||||
const cw = pong.container.clientWidth;
|
||||
const ch = pong.container.clientHeight;
|
||||
|
||||
tiles.forEach(t => {
|
||||
seen.add(t.id);
|
||||
if (!pong.state.has(t.id)) {
|
||||
const w = t.offsetWidth || 240;
|
||||
const h = t.offsetHeight || 180;
|
||||
const angle = Math.random() * Math.PI * 2;
|
||||
const speed = 60 + Math.random() * 40; // px/sec — slow glide
|
||||
pong.state.set(t.id, {
|
||||
x: Math.random() * Math.max(1, cw - w),
|
||||
y: Math.random() * Math.max(1, ch - h),
|
||||
vx: Math.cos(angle) * speed,
|
||||
vy: Math.sin(angle) * speed,
|
||||
w, h
|
||||
});
|
||||
} else {
|
||||
const s = pong.state.get(t.id);
|
||||
s.w = t.offsetWidth || s.w;
|
||||
s.h = t.offsetHeight || s.h;
|
||||
}
|
||||
});
|
||||
|
||||
for (const id of pong.state.keys()) {
|
||||
if (!seen.has(id)) pong.state.delete(id);
|
||||
}
|
||||
}
|
||||
|
||||
let pongLastT = 0;
|
||||
function pongTick(now) {
|
||||
if (!state.pongMode || !pong.container) {
|
||||
pong.raf = null;
|
||||
return;
|
||||
}
|
||||
|
||||
const dt = pongLastT ? Math.min(0.05, (now - pongLastT) / 1000) : 0.016;
|
||||
pongLastT = now;
|
||||
|
||||
const cw = pong.container.clientWidth;
|
||||
const ch = pong.container.clientHeight;
|
||||
|
||||
const items = [];
|
||||
pong.state.forEach((s, id) => {
|
||||
s.x += s.vx * dt;
|
||||
s.y += s.vy * dt;
|
||||
|
||||
if (s.x <= 0) { s.x = 0; s.vx = Math.abs(s.vx); }
|
||||
if (s.x + s.w >= cw) { s.x = cw-s.w; s.vx = -Math.abs(s.vx); }
|
||||
if (s.y <= 0) { s.y = 0; s.vy = Math.abs(s.vy); }
|
||||
if (s.y + s.h >= ch) { s.y = ch-s.h; s.vy = -Math.abs(s.vy); }
|
||||
|
||||
items.push({ id, s });
|
||||
});
|
||||
|
||||
// Tile-vs-tile collisions (cheap O(n^2), fine for ~10 tiles).
|
||||
for (let i = 0; i < items.length; i++) {
|
||||
for (let j = i + 1; j < items.length; j++) {
|
||||
const a = items[i].s, b = items[j].s;
|
||||
if (a.x < b.x + b.w && a.x + a.w > b.x &&
|
||||
a.y < b.y + b.h && a.y + a.h > b.y) {
|
||||
const overlapX = Math.min(a.x + a.w - b.x, b.x + b.w - a.x);
|
||||
const overlapY = Math.min(a.y + a.h - b.y, b.y + b.h - a.y);
|
||||
if (overlapX < overlapY) {
|
||||
const push = overlapX / 2;
|
||||
if (a.x < b.x) { a.x -= push; b.x += push; }
|
||||
else { a.x += push; b.x -= push; }
|
||||
const tmp = a.vx; a.vx = b.vx; b.vx = tmp;
|
||||
} else {
|
||||
const push = overlapY / 2;
|
||||
if (a.y < b.y) { a.y -= push; b.y += push; }
|
||||
else { a.y += push; b.y -= push; }
|
||||
const tmp = a.vy; a.vy = b.vy; b.vy = tmp;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
items.forEach(({ id, s }) => {
|
||||
const el = document.getElementById(id);
|
||||
if (el) el.style.transform = `translate(${s.x}px, ${s.y}px)`;
|
||||
});
|
||||
|
||||
pong.raf = requestAnimationFrame(pongTick);
|
||||
}
|
||||
|
||||
@@ -35,6 +35,8 @@ const state = {
|
||||
configLoaded: false,
|
||||
defconMode: false, // Auto-mute and hide video for new users
|
||||
trippyMode: false, // UI hue-shift animation - toggled via server-side dial codes
|
||||
schizoMode: false, // Subtle UI shake/wiggle - toggled via *666#
|
||||
pongMode: false, // Webcam tiles bounce around - toggled via *9059#
|
||||
// Reconnection state
|
||||
reconnectToken: null,
|
||||
wsReconnectAttempts: 0,
|
||||
|
||||
@@ -143,6 +143,11 @@ function updateVideoGrid() {
|
||||
attachStreamToTile(user.id, user.stream, user.isLocal);
|
||||
});
|
||||
}
|
||||
|
||||
// Pong mode tracks tiles by id; grid was just rebuilt so re-sync.
|
||||
if (state.pongMode && typeof pongRefreshTiles === 'function') {
|
||||
pongRefreshTiles();
|
||||
}
|
||||
}
|
||||
|
||||
function createVideoTile(user, isMaximized) {
|
||||
|
||||
119
static/style.css
119
static/style.css
@@ -1954,3 +1954,122 @@ body.trippy-mode .video-tile video {
|
||||
0% { background-position: 0% 50%; }
|
||||
100% { background-position: 200% 50%; }
|
||||
}
|
||||
|
||||
/* ========== SCHIZO MODE (*666#) ==========
|
||||
Subtle, slow UI shake + wiggle + skew. Cameras drift independently. */
|
||||
|
||||
@keyframes schizo-body {
|
||||
0% { transform: translate(0, 0) rotate(0deg) skew(0deg, 0deg); }
|
||||
20% { transform: translate(-2px, 1px) rotate(-0.15deg) skew(0.3deg, -0.2deg); }
|
||||
40% { transform: translate(1px, -2px) rotate(0.2deg) skew(-0.3deg, 0.2deg); }
|
||||
60% { transform: translate(2px, 2px) rotate(-0.1deg) skew(0.2deg, 0.3deg); }
|
||||
80% { transform: translate(-1px, -1px) rotate(0.15deg) skew(-0.4deg, -0.1deg); }
|
||||
100% { transform: translate(0, 0) rotate(0deg) skew(0deg, 0deg); }
|
||||
}
|
||||
|
||||
@keyframes schizo-tile-a {
|
||||
0%, 100% { transform: translate(0, 0) rotate(0deg) scale(1); }
|
||||
25% { transform: translate(-3px, 2px) rotate(-0.4deg) scale(1.005); }
|
||||
50% { transform: translate(2px, -3px) rotate(0.5deg) scale(0.995); }
|
||||
75% { transform: translate(3px, 3px) rotate(-0.3deg) scale(1.008); }
|
||||
}
|
||||
|
||||
@keyframes schizo-tile-b {
|
||||
0%, 100% { transform: translate(0, 0) rotate(0deg) scale(1); }
|
||||
20% { transform: translate(2px, -2px) rotate(0.5deg) scale(1.006); }
|
||||
55% { transform: translate(-3px, 2px) rotate(-0.4deg) scale(0.994); }
|
||||
80% { transform: translate(-2px, -3px) rotate(0.3deg) scale(1.004); }
|
||||
}
|
||||
|
||||
@keyframes schizo-video {
|
||||
0%, 100% { transform: scale(1.04) translate(0, 0); filter: hue-rotate(0deg); }
|
||||
33% { transform: scale(1.06) translate(-4px, 3px); filter: hue-rotate(8deg); }
|
||||
66% { transform: scale(1.05) translate(3px, -4px); filter: hue-rotate(-6deg); }
|
||||
}
|
||||
|
||||
body.schizo-mode #app {
|
||||
animation: schizo-body 9s ease-in-out infinite;
|
||||
}
|
||||
|
||||
body.schizo-mode .video-tile {
|
||||
animation: schizo-tile-a 9s ease-in-out infinite;
|
||||
}
|
||||
|
||||
body.schizo-mode .video-tile:nth-child(2n) {
|
||||
animation: schizo-tile-b 8s ease-in-out infinite;
|
||||
animation-delay: -2s;
|
||||
}
|
||||
|
||||
body.schizo-mode .video-tile:nth-child(3n) {
|
||||
animation: schizo-tile-a 10s ease-in-out infinite;
|
||||
animation-delay: -4s;
|
||||
}
|
||||
|
||||
body.schizo-mode .video-tile video {
|
||||
animation: schizo-video 12s ease-in-out infinite;
|
||||
}
|
||||
|
||||
body.schizo-mode .video-tile:nth-child(2n) video {
|
||||
animation-delay: -3s;
|
||||
animation-duration: 14s;
|
||||
}
|
||||
|
||||
/* ========== PONG MODE (*9059#) ==========
|
||||
Tiles get position:absolute and JS animates their transform. */
|
||||
|
||||
body.pong-mode #video-grid {
|
||||
display: block;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
body.pong-mode .video-tile {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 0;
|
||||
width: 240px;
|
||||
height: 180px;
|
||||
will-change: transform;
|
||||
transition: none;
|
||||
}
|
||||
|
||||
/* Pong + schizo together: don't fight over transform. Pong wins. */
|
||||
body.pong-mode.schizo-mode .video-tile,
|
||||
body.pong-mode.schizo-mode .video-tile video {
|
||||
animation: none;
|
||||
}
|
||||
|
||||
/* ========== DIAL CODES LIST (*#06#) ========== */
|
||||
|
||||
.dial-codes-list {
|
||||
list-style: none;
|
||||
margin: 0;
|
||||
padding: 0.5rem 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.5rem;
|
||||
max-height: 60vh;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.dial-codes-list li {
|
||||
display: flex;
|
||||
align-items: baseline;
|
||||
gap: 0.75rem;
|
||||
padding: 0.5rem 0.75rem;
|
||||
background: var(--bg-primary);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 6px;
|
||||
}
|
||||
|
||||
.dial-codes-list code {
|
||||
font-family: 'JetBrains Mono', monospace;
|
||||
color: var(--acid);
|
||||
min-width: 5.5rem;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.dial-codes-list span {
|
||||
color: var(--text-primary);
|
||||
font-size: 0.85rem;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user