Skip to content

Instantly share code, notes, and snippets.

@simonw
Created July 13, 2026 22:27
Show Gist options
  • Select an option

  • Save simonw/7c78184476fccd4b70b02f7f9048dffa to your computer and use it in GitHub Desktop.

Select an option

Save simonw/7c78184476fccd4b70b02f7f9048dffa to your computer and use it in GitHub Desktop.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>DoomQL — frame_pixels viewer</title>
<style>
:root {
--bg: #07090f;
--line: #1d2740;
--text: #e8eef5;
--dim: #8fa3bf;
--accent: #ffbe4a;
}
* { box-sizing: border-box; }
body {
margin: 0;
padding: 1.25rem 1rem 2rem;
background: var(--bg);
color: var(--text);
font-family: ui-monospace, "Cascadia Mono", Menlo, Consolas, monospace;
display: flex;
flex-direction: column;
align-items: center;
}
header { text-align: center; margin-bottom: 1rem; }
h1 {
margin: 0 0 .3rem;
font-size: 1rem;
letter-spacing: .22em;
text-transform: uppercase;
color: var(--accent);
}
header p { margin: 0; color: var(--dim); font-size: .75rem; }
.stage {
display: flex;
flex-wrap: wrap;
gap: 1rem;
justify-content: center;
align-items: flex-start;
}
.viewport { width: min(92vw, 700px); }
.screen-wrap {
position: relative;
border: 1px solid var(--line);
background: #000;
line-height: 0;
box-shadow: 0 0 48px rgba(0, 0, 0, .65);
}
.screen-wrap::after {
content: "";
position: absolute;
inset: 0;
pointer-events: none;
background: repeating-linear-gradient(to bottom, rgba(0,0,0,.18) 0 1px, transparent 1px 3px);
}
#screen {
display: block;
width: 100%;
image-rendering: pixelated;
}
.hud { width: 100%; font-size: .74rem; white-space: pre; overflow-x: auto; }
.hud div { padding: .34rem .6rem; }
#hud-main { background: rgb(10,14,22); color: rgb(232,238,245); }
#hud-message { background: rgb(18,9,13); color: rgb(255,190,74); }
#hud-footer { background: rgb(6,8,13); color: rgb(116,199,214); }
.minimap-panel {
width: min(92vw, 300px);
border: 1px solid var(--line);
background: #0a0d15;
padding: .55rem;
}
.panel-title {
font-size: .66rem;
letter-spacing: .2em;
color: var(--dim);
text-align: center;
margin-bottom: .5rem;
}
#minimap { display: block; width: 100%; background: #05070d; }
.legend {
margin-top: .55rem;
display: flex;
flex-wrap: wrap;
gap: .25rem .7rem;
font-size: .62rem;
color: var(--dim);
}
.legend i {
display: inline-block;
width: .58rem;
height: .58rem;
border-radius: 2px;
margin-right: .3rem;
vertical-align: -1px;
}
.statusbar {
margin-top: 1rem;
display: flex;
flex-wrap: wrap;
gap: .6rem 1rem;
align-items: center;
justify-content: center;
font-size: .8rem;
}
.badge {
padding: .15rem .55rem;
border-radius: 3px;
font-weight: 700;
letter-spacing: .08em;
background: #333;
}
#stats { color: var(--dim); font-size: .74rem; }
#error {
margin-top: .8rem;
max-width: min(92vw, 900px);
background: #2a1013;
border: 1px solid #703038;
color: #ffb3b8;
padding: .6rem .8rem;
font-size: .78rem;
white-space: pre-wrap;
}
</style>
</head>
<body>
<header>
<h1>DoomQL</h1>
<p>auto-refreshing once a second · frame and tactical map straight from SQL</p>
</header>
<div class="stage">
<div class="viewport">
<div class="screen-wrap"><canvas id="screen" width="120" height="80"></canvas></div>
<div class="hud">
<div id="hud-main"> …</div>
<div id="hud-message"> …</div>
<div id="hud-footer"> READ-ONLY VIEWER · SELECT x, y, r, g, b FROM frame_pixels</div>
</div>
</div>
<aside class="minimap-panel">
<div class="panel-title">TACTICAL MAP</div>
<canvas id="minimap"></canvas>
<div class="legend">
<span><i style="background:#ecf5ff"></i>you</span>
<span><i style="background:#ff5054"></i>enemy</span>
<span><i style="background:#ffd257"></i>pickup</span>
<span><i style="background:#e0483f"></i>locked door</span>
<span><i style="background:#37c2a8"></i>door</span>
<span><i style="background:#3ddc84"></i>exit</span>
</div>
</aside>
</div>
<div class="statusbar">
<span class="badge" id="state">—</span>
<span id="stats">Rendering first frame…</span>
</div>
<div id="error" hidden></div>
<script>
(function () {
const DB = "doomql";
const REFRESH_MS = 1000;
const canvas = document.getElementById("screen");
const ctx = canvas.getContext("2d");
const mm = document.getElementById("minimap");
const mctx = mm.getContext("2d");
const $ = (id) => document.getElementById(id);
const META_SQL = [
"select s.screen_width as w, s.screen_height as h, s.horizontal_fov as fov,",
" p.x as px, p.y as py, p.angle,",
" p.health, p.max_health, p.ammo, p.has_token,",
" gs.score, gs.state, gs.message, gs.level_id, gc.tick",
"from settings as s",
"join player as p on p.id = 1",
"join game_status as gs on gs.id = 1",
"join game_clock as gc on gc.id = 1",
"where s.id = 1"
].join("\n");
// One row per scanline keeps the result far below any row limits.
const FRAME_SQL = [
"select y, group_concat(x || ',' || r || ',' || g || ',' || b, ';') as line",
"from frame_pixels",
"group by y",
"order by y"
].join("\n");
// Static level geometry, compressed the same way (one row per map row).
const MAP_SQL = [
"select mc.y, group_concat(",
" mc.x || ',' || mc.solid || ',' || mc.is_exit || ',' ||",
" m.base_r || ',' || m.base_g || ',' || m.base_b, ';') as line",
"from map_cells as mc",
"join materials as m on m.id = mc.material_id",
"group by mc.y",
"order by mc.y"
].join("\n");
// Dynamic minimap overlay: doors + live entities in one query.
const OVERLAY_SQL = [
"select 'door' as k, x * 1.0 as x, y * 1.0 as y, open_fraction as a, locked as b",
"from doors",
"union all",
"select case when et.kind = 'pickup' then 'pickup'",
" when et.is_boss = 1 then 'boss'",
" else 'enemy' end as k,",
" e.x, e.y, 0.0 as a, 0 as b",
"from entities as e",
"join entity_types as et on et.id = e.type_id",
"where e.active = 1"
].join("\n");
const STATE_COLORS = {
running: "#1d7a44",
paused: "#8a6414",
dead: "#8a1d24",
won: "#155e6b"
};
let mapData = null;
let mapLevel = null;
let baseLayer = null;
const pad = (n, len) => String(n ?? 0).padStart(len, "0");
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
function drawFrame(w, h, rows) {
if (canvas.width !== w) canvas.width = w;
if (canvas.height !== h) canvas.height = h;
const img = ctx.createImageData(w, h);
const d = img.data;
for (let i = 3; i < d.length; i += 4) d[i] = 255; // opaque black base
let painted = 0;
for (const row of rows) {
const y = Number(row.y);
if (!Number.isInteger(y) || y < 0 || y >= h || !row.line) continue;
const cells = String(row.line).split(";");
for (const cell of cells) {
const parts = cell.split(",");
const x = Number(parts[0]);
if (!(x >= 0 && x < w)) continue;
const i = (y * w + x) * 4;
d[i] = Number(parts[1]) | 0;
d[i + 1] = Number(parts[2]) | 0;
d[i + 2] = Number(parts[3]) | 0;
painted++;
}
}
ctx.putImageData(img, 0, 0);
return painted;
}
function parseMap(rows) {
const cells = [];
let minX = Infinity, maxX = -Infinity, minY = Infinity, maxY = -Infinity;
for (const row of rows) {
const y = Number(row.y);
if (!row.line) continue;
for (const chunk of String(row.line).split(";")) {
const p = chunk.split(",");
const x = Number(p[0]);
cells.push({ x, y, solid: +p[1], exit: +p[2], r: +p[3], g: +p[4], b: +p[5] });
if (x < minX) minX = x; if (x > maxX) maxX = x;
if (y < minY) minY = y; if (y > maxY) maxY = y;
}
}
if (!cells.length) throw new Error("map_cells is empty");
const w = maxX - minX + 1;
const h = maxY - minY + 1;
const solid = new Uint8Array(w * h);
for (const c of cells) if (c.solid) solid[(c.y - minY) * w + (c.x - minX)] = 1;
const cell = Math.max(6, Math.min(24, Math.round(560 / w))); // ~2x the CSS size, crisp
return { cells, minX, minY, w, h, solid, cell };
}
function solidAt(x, y) {
const { w, h, minX, minY, solid } = mapData;
const gx = x - minX, gy = y - minY;
if (gx < 0 || gy < 0 || gx >= w || gy >= h) return 0;
return solid[gy * w + gx];
}
function buildBaseLayer(doorKeys) {
const { cells, minX, minY, w, h, cell } = mapData;
baseLayer = document.createElement("canvas");
baseLayer.width = w * cell;
baseLayer.height = h * cell;
const g = baseLayer.getContext("2d");
g.fillStyle = "#05070d";
g.fillRect(0, 0, baseLayer.width, baseLayer.height);
for (const c of cells) {
const px = (c.x - minX) * cell, py = (c.y - minY) * cell;
if (c.solid && !doorKeys.has(c.x + "," + c.y)) {
g.fillStyle = "rgb(" + Math.round(c.r * 0.9) + "," + Math.round(c.g * 0.9) + "," + Math.round(c.b * 0.9) + ")";
g.fillRect(px, py, cell, cell);
} else {
g.fillStyle = "#10141d";
g.fillRect(px, py, cell - 1, cell - 1); // 1px gap = subtle floor grid
if (c.exit) {
g.fillStyle = "rgba(61,220,132,.3)";
g.fillRect(px, py, cell - 1, cell - 1);
g.strokeStyle = "rgba(61,220,132,.85)";
g.lineWidth = 1;
g.strokeRect(px + .5, py + .5, cell - 2, cell - 2);
}
}
}
}
function dot(x, y, r, color) {
mctx.fillStyle = color;
mctx.beginPath();
mctx.arc(x, y, r, 0, Math.PI * 2);
mctx.fill();
}
function drawMinimap(meta, overlay) {
if (!mapData) return;
const { minX, minY, w, h, cell } = mapData;
const W = w * cell, H = h * cell;
if (mm.width !== W) mm.width = W;
if (mm.height !== H) mm.height = H;
const doors = [], enemies = [], pickups = [];
const doorKeys = new Set();
for (const r of overlay) {
if (r.k === "door") { doors.push(r); doorKeys.add(r.x + "," + r.y); }
else if (r.k === "pickup") pickups.push(r);
else enemies.push(r);
}
if (!baseLayer) buildBaseLayer(doorKeys);
mctx.drawImage(baseLayer, 0, 0);
// Doors: sliding bars, split apart as open_fraction grows.
for (const d of doors) {
const px = (d.x - minX) * cell, py = (d.y - minY) * cell;
const horizontal = solidAt(d.x - 1, d.y) && solidAt(d.x + 1, d.y);
const t = Math.max(2, cell * 0.34);
const half = (cell * (1 - d.a)) / 2;
if (half <= 0.4) continue; // fully open
mctx.fillStyle = d.b ? "#e0483f" : "#37c2a8";
if (horizontal) {
const yb = py + (cell - t) / 2;
mctx.fillRect(px, yb, half, t);
mctx.fillRect(px + cell - half, yb, half, t);
} else {
const xb = px + (cell - t) / 2;
mctx.fillRect(xb, py, t, half);
mctx.fillRect(xb, py + cell - half, t, half);
}
}
for (const p of pickups) {
dot((p.x - minX) * cell, (p.y - minY) * cell, cell * 0.22, "#ffd257");
}
for (const e of enemies) {
const x = (e.x - minX) * cell, y = (e.y - minY) * cell;
if (e.k === "boss") {
dot(x, y, cell * 0.5, "#ff5054");
mctx.strokeStyle = "#fff";
mctx.lineWidth = Math.max(1, cell * 0.1);
mctx.beginPath();
mctx.arc(x, y, cell * 0.5, 0, Math.PI * 2);
mctx.stroke();
} else {
dot(x, y, cell * 0.3, "#ff5054");
}
}
// Player: FOV cone + heading triangle.
const px = (meta.px - minX) * cell, py = (meta.py - minY) * cell;
const a = Number(meta.angle) || 0;
const f = Number(meta.fov) || 1.2;
const L = cell * 4.5;
mctx.fillStyle = "rgba(148,205,255,.16)";
mctx.beginPath();
mctx.moveTo(px, py);
mctx.lineTo(px + Math.cos(a - f / 2) * L, py + Math.sin(a - f / 2) * L);
mctx.lineTo(px + Math.cos(a + f / 2) * L, py + Math.sin(a + f / 2) * L);
mctx.closePath();
mctx.fill();
const s = cell * 0.62;
mctx.fillStyle = "#ecf5ff";
mctx.strokeStyle = "#0a0f18";
mctx.lineWidth = 1;
mctx.beginPath();
mctx.moveTo(px + Math.cos(a) * s, py + Math.sin(a) * s);
mctx.lineTo(px + Math.cos(a + 2.6) * s * 0.8, py + Math.sin(a + 2.6) * s * 0.8);
mctx.lineTo(px + Math.cos(a - 2.6) * s * 0.8, py + Math.sin(a - 2.6) * s * 0.8);
mctx.closePath();
mctx.fill();
mctx.stroke();
}
function updateHud(m) {
$("hud-main").textContent =
" HP " + pad(m.health, 3) + "/" + pad(m.max_health, 3) +
" AMMO " + pad(m.ammo, 3) +
" SCORE " + pad(m.score, 5) +
" INDEX " + (m.has_token ? "ONLINE " : "MISSING") +
" TICK " + pad(m.tick, 7);
$("hud-message").textContent = " " + (m.message || "");
const badge = $("state");
badge.textContent = String(m.state || "?").toUpperCase();
badge.style.background = STATE_COLORS[m.state] || "#333";
}
async function refresh() {
const t0 = performance.now();
const [metaR, frameR, overlayR] = await Promise.all([
datasette.query(DB, META_SQL),
datasette.query(DB, FRAME_SQL),
datasette.query(DB, OVERLAY_SQL)
]);
const meta = metaR.rows[0];
if (!meta) throw new Error("settings / player / game_status rows not found");
if (!mapData || mapLevel !== meta.level_id) {
const mapR = await datasette.query(DB, MAP_SQL);
mapData = parseMap(mapR.rows);
mapLevel = meta.level_id;
baseLayer = null;
}
const queryMs = performance.now() - t0;
const painted = drawFrame(meta.w, meta.h, frameR.rows);
updateHud(meta);
drawMinimap(meta, overlayR.rows);
const hostiles = overlayR.rows.filter((r) => r.k === "enemy" || r.k === "boss").length;
const expected = meta.w * meta.h;
$("stats").textContent =
meta.w + "×" + meta.h +
" · " + painted.toLocaleString() +
(painted === expected ? "" : " of " + expected.toLocaleString()) +
" pixels · " + hostiles + (hostiles === 1 ? " hostile" : " hostiles") +
" · query " + Math.round(queryMs).toLocaleString() + " ms" +
" · refreshing every " + (REFRESH_MS / 1000) + " s";
}
async function loop() {
for (;;) {
const started = performance.now();
try {
await refresh();
$("error").hidden = true;
} catch (err) {
$("error").hidden = false;
$("error").textContent = err && err.message ? err.message : String(err);
}
const elapsed = performance.now() - started;
await sleep(Math.max(0, REFRESH_MS - elapsed));
}
}
loop();
})();
</script>
</body>
</html>
@ByblosCentury

Copy link
Copy Markdown

Claude 4.6 thinks you write nice code.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment