pong / static /index.html
chrisxx's picture
Default to 12 FPS
8a7b312
<!doctype html>
<html>
<head>
<meta charset="utf-8" />
<title>Pong</title>
<meta name="viewport" content="width=device-width, initial-scale=1" />
<!-- Socket.IO client library (CDN) -->
<script src="https://cdn.socket.io/4.5.4/socket.io.min.js"></script>
<style>
html, body { margin:0; height:100%; background:#111; color:#eee; font-family: system-ui, sans-serif; }
#overlay {
position: fixed; inset: 0; display: flex; align-items: center; justify-content: center;
background: rgba(0,0,0,0.8); z-index: 9999; transition: opacity 200ms ease;
}
#overlay.hidden { opacity: 0; pointer-events: none; }
.spinner {
width: 64px; height: 64px; border: 6px solid #444; border-top-color: #09f; border-radius: 50%;
animation: spin 0.9s linear infinite;
}
@keyframes spin { to { transform: rotate(360deg); } }
#statusText { margin-top: 12px; color: #aaa; text-align: center; font-size: 14px; white-space: pre-line; }
#app { padding: 16px; }
button { padding: 8px 12px; background:#09f; color:#fff; border:none; border-radius:8px; cursor:pointer; }
button:disabled { opacity: .5; cursor: not-allowed; }
img#frame { image-rendering: pixelated; width: 240px; height: 240px; background:#222; display:block; margin-top:12px; }
</style>
</head>
<body>
<div id="overlay">
<div>
<div class="spinner"></div>
<div id="statusText">Loading model…</div>
</div>
</div>
<div id="app">
<h1>Pong</h1>
<div style="margin-bottom: 12px;">
<label style="display: block; margin-bottom: 8px;">
FPS: <input type="number" id="fpsInput" value="12" min="1" max="30" step="1" style="width: 60px; padding: 4px; margin-left: 8px;" />
<span style="color: #aaa; font-size: 12px; margin-left: 8px;">frames per second</span>
</label>
<label style="display: block; margin-bottom: 8px;">
Steps: <input type="number" id="stepsInput" value="4" min="1" max="10" step="1" style="width: 60px; padding: 4px; margin-left: 8px;" />
<span style="color: #aaa; font-size: 12px; margin-left: 8px;">diffusion steps</span>
</label>
</div>
<div>
<button id="startBtn" disabled>Start Stream</button>
<button id="stopBtn" disabled>Stop Stream</button>
</div>
<img id="frame" alt="Latest frame" />
<div id="actionDisplay" style="margin-top: 12px; font-size: 16px; font-family: monospace;">
Action: <span id="actionValue">-</span>
</div>
<div id="fpsDisplay" style="margin-top: 8px; font-size: 16px; font-family: monospace;">
Achieved FPS: <span id="fpsValue">-</span>
</div>
<div id="waitingMessage" style="margin-top: 12px; padding: 8px; background: #333; border-radius: 4px; display: none; color: #ffa500;">
⏳ Another player is currently using the stream. Please wait for them to finish.
</div>
<div style="margin-top: 12px; padding: 8px; background: #222; border-radius: 4px; border-left: 3px solid #09f; color: #ccc; font-size: 13px;">
💡 <strong>Tip:</strong> Click anywhere on this page to enable keyboard controls. Use <strong>↑/↓ Arrow Keys</strong> or <strong>W/S</strong> to control the paddle.
</div>
<div style="margin-top: 12px; font-size: 14px; color: #aaa; line-height: 1.5;">
This demo uses a small transformer model trained with rectified flow matching to simulate Pong game frames conditioned on user inputs. The model generates 24×24 pixel frames in real-time using diffusion sampling with configurable steps. Performance targets ~16 FPS with 4 diffusion steps on GPU hardware.
</div>
</div>
<script>
// If you serve socket.io client at /socket.io/socket.io.js you can use global io():
const socket = io({ transports: ['websocket', 'polling'] });
const overlay = document.getElementById('overlay');
const statusText = document.getElementById('statusText');
const startBtn = document.getElementById('startBtn');
const stopBtn = document.getElementById('stopBtn');
const frameImg = document.getElementById('frame');
function setStatus(isReady) {
if (!isReady) {
// Model is still loading
overlay.classList.remove('hidden');
startBtn.disabled = true;
stopBtn.disabled = true;
statusText.textContent = 'Loading model…';
} else {
// Server is ready and available
overlay.classList.add('hidden');
startBtn.disabled = false;
stopBtn.disabled = false;
statusText.textContent = 'Ready';
}
}
// Initial state: assume not ready (show spinner)
setStatus(false);
socket.on('connect', () => {
// server will immediately emit 'server_status' with current readiness
console.log('connected');
});
// Backend broadcasts readiness changes
socket.on('server_status', (payload) => {
const ready = !!(payload && payload.ready);
console.log('Server status:', { ready });
setStatus(ready);
});
// Start/stop controls
startBtn.addEventListener('click', () => {
const fps = parseInt(document.getElementById('fpsInput').value) || 16;
const n_steps = parseInt(document.getElementById('stepsInput').value) || 1;
socket.emit('start_stream', { n_steps: n_steps, cfg: 0.0, fps: fps, clamp: true });
});
stopBtn.addEventListener('click', () => {
socket.emit('stop_stream');
});
const actionValue = document.getElementById('actionValue');
const fpsValue = document.getElementById('fpsValue');
const waitingMessage = document.getElementById('waitingMessage');
// Handle stream busy/available events
socket.on('stream_busy', (data) => {
console.log('Stream is busy:', data);
waitingMessage.style.display = 'block';
startBtn.disabled = true;
});
socket.on('stream_available', (data) => {
console.log('Stream is available:', data);
waitingMessage.style.display = 'none';
startBtn.disabled = false;
});
// Incoming frames
socket.on('frame', ({ frame, frame_index, action, fps }) => {
frameImg.src = `data:image/png;base64,${frame}`;
// Display action: 0=NOOP, 1=UP, 2=DOWN
const actionLabels = ['START','NOOP', 'UP', 'DOWN'];
actionValue.textContent = `${action} (${actionLabels[action] || 'UNKNOWN'})`;
// Display achieved FPS
if (fps !== undefined) {
fpsValue.textContent = fps.toFixed(1);
}
});
socket.on('error', (e) => {
console.warn('server error', e);
if (e && e.message) {
console.error('Server error message:', e.message);
// Show error message to user
if (e.message.includes('Another player') || e.message.includes('not the current player')) {
waitingMessage.style.display = 'block';
waitingMessage.textContent = '⏳ ' + e.message;
startBtn.disabled = true;
} else {
alert('Error: ' + e.message);
}
}
});
// Keyboard controls for paddle
// Actions: 0=NOOP, 1=UP, 2=DOWN
document.addEventListener('keydown', (e) => {
let action = null;
if (e.key === 'ArrowUp' || e.key === 'w' || e.key === 'W') {
action = 2; // UP
} else if (e.key === 'ArrowDown' || e.key === 's' || e.key === 'S') {
action = 3; // DOWN
}
if (action !== null) {
socket.emit('action', { action });
e.preventDefault();
}
});
document.addEventListener('keyup', (e) => {
if (['ArrowUp', 'ArrowDown', 'w', 'W', 's', 'S'].includes(e.key)) {
socket.emit('action', { action: 1 }); // NOOP when key released
e.preventDefault();
}
});
</script>
</body>
</html>