swactor/examples/ping-pong/index.html

160 lines
5.5 KiB
HTML
Raw Permalink Normal View History

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>swactor · ping-pong</title>
<style>
:root {
--bg: #0d1117; --surface: #161b22; --border: #30363d;
--text: #c9d1d9; --dim: #8b949e; --accent: #58a6ff;
--green: #3fb950; --yellow: #d29922; --purple: #bc8cff;
--font: 'SF Mono', 'Cascadia Code', 'Fira Code', ui-monospace, monospace;
}
* { box-sizing: border-box; }
body {
margin: 0; padding: 32px 20px;
font-family: var(--font); background: var(--bg); color: var(--text);
line-height: 1.5;
}
h1 { font-size: 1.5em; margin: 0 0 2px; }
h1 span { color: var(--accent); }
.subtitle { color: var(--dim); font-size: 0.85em; margin-bottom: 24px; }
.card {
background: var(--surface); border: 1px solid var(--border);
border-radius: 8px; padding: 20px; max-width: 640px; margin: 0 auto;
}
.controls { display: flex; gap: 8px; align-items: center; margin-bottom: 16px; flex-wrap: wrap; }
label { color: var(--dim); font-size: 0.8em; }
input {
background: var(--bg); border: 1px solid var(--border); color: var(--text);
padding: 6px 10px; border-radius: 4px; font-family: var(--font);
font-size: 0.85em; width: 72px;
}
input:focus { outline: none; border-color: var(--accent); }
.btn {
padding: 6px 16px; border: 1px solid var(--border); background: var(--surface);
color: var(--text); border-radius: 4px; cursor: pointer; font-family: var(--font);
font-size: 0.85em; transition: 0.15s;
}
.btn:hover:not(:disabled) { border-color: var(--accent); color: var(--accent); }
.btn:active:not(:disabled) { transform: scale(0.97); }
.btn:disabled { opacity: 0.45; cursor: default; }
#status { color: var(--dim); font-size: 0.8em; margin-left: auto; }
#status .v { color: var(--green); font-weight: bold; }
#log {
background: var(--bg); border: 1px solid var(--border); border-radius: 6px;
padding: 12px; font-size: 0.85em; line-height: 1.7; height: 360px;
overflow-y: auto; white-space: pre;
}
.log-line { animation: fadeIn 0.2s ease; }
.log-line.ping .side { color: var(--accent); }
.log-line.pong .side { color: var(--purple); }
.log-line.done .side, .log-line.done { color: var(--yellow); }
.log-line.stop { color: var(--dim); }
@keyframes fadeIn { from { opacity: 0; } to { opacity: 1; } }
#loading { text-align: center; color: var(--dim); padding: 48px; }
.err { color: #f85149; }
.dash { color: var(--accent); text-decoration: none; border-bottom: 1px dotted var(--accent); }
.dash:hover { color: var(--green); border-color: var(--green); }
</style>
</head>
<body>
<h1><span>swactor</span> · ping-pong</h1>
<p class="subtitle">
Two actors volley a ball on a WebAssembly actor runtime. The browser drives it
one <code>tick()</code> at a time; each line below is one actor message.<br>
<a class="dash" href="http://localhost:9090/" target="_blank" rel="noopener">swactor dashboard &nearr;</a>
&mdash; live actor stats, served alongside this page.
</p>
<div class="card">
<div class="controls">
<label for="volleys">volleys</label>
<input id="volleys" type="number" min="2" max="999" value="10">
<button id="serve" class="btn">Serve</button>
<span id="status"></span>
</div>
<div id="log"></div>
</div>
<div id="loading">Loading wasm…</div>
<script type="module">
import init, { App } from "./pkg-web/pingpong.js";
let app;
try {
await init();
app = new App();
} catch (e) {
document.getElementById("loading").innerHTML =
'<span class="err">Failed to load wasm.</span><br><br>' +
'Serve this directory over HTTP (run <code>./serve.sh</code>), ' +
'don\'t open the file directly.';
throw e;
}
document.getElementById("loading").style.display = "none";
const logEl = document.getElementById("log");
const serveBtn = document.getElementById("serve");
const volleysInput = document.getElementById("volleys");
const statusEl = document.getElementById("status");
const TICK_MS = 240;
function classify(line) {
if (line.startsWith("ping")) return "ping";
if (line.startsWith("pong")) return "pong";
if (line.startsWith("done")) return "done";
return "stop";
}
function appendLine(line) {
const cls = classify(line);
const div = document.createElement("div");
div.className = "log-line " + cls;
// Highlight the side token (ping/pong/done) for readability.
const side = line.slice(0, 4).trimEnd();
div.innerHTML = '<span class="side">' + side + "</span>" + line.slice(4);
logEl.appendChild(div);
logEl.scrollTop = logEl.scrollHeight;
}
function setStatus(actors, msgs) {
statusEl.innerHTML = "actors <span class=\"v\">" + actors + "</span> · messages <span class=\"v\">" + msgs + "</span>";
}
function serve() {
// Reset for a fresh rally.
logEl.innerHTML = "";
app = new App();
const max = Math.max(2, Number(volleysInput.value) || 10);
const log = app.new_log();
const pong = app.spawn_pong(log.addr(), max);
app.spawn_ping(pong, log.addr(), max);
serveBtn.disabled = true;
setStatus(app.actor_count(), 0);
const iv = setInterval(() => {
app.tick();
let line;
while ((line = log.try_recv())) appendLine(line);
setStatus(app.actor_count(), app.total_messages());
if (app.actor_count() === 0) {
clearInterval(iv);
serveBtn.disabled = false;
}
}, TICK_MS);
}
serveBtn.addEventListener("click", serve);
// Auto-play the first rally on load.
serve();
</script>
</body>
</html>