//// // version: 30ae804 // src/js/dom.ts var content2 = $("content"); var cmdLine = $("command-line"); var cmdInput = $("command-textbox"); var scrollback = $("scrollback"); function $(id) { return document.getElementById(id); } var $$ = (tag, innerHTML = "") => { let name = tag; let classList = []; if (tag.startsWith(".")) tag = "div" + tag; if (tag.includes(".")) [name, ...classList] = tag.split(".").map((x) => x.trim()); const el = document.createElement(name); if (innerHTML) el.innerHTML = innerHTML; if (classList.length) el.classList.add(...classList); return el; }; // src/js/resize.ts var content3 = document.getElementById("content"); function initResize() { window.addEventListener("resize", resize); resize(); } function resize() { if (document.body.dataset.mode === "tall") { resizeTall(); } else { resizeCinema(); } } function resizeTall() { const scale = Math.min(1, window.innerWidth / 960); content3.style.transformOrigin = "top center"; content3.style.transform = `scaleX(${scale})`; } function resizeCinema() { const scale = Math.min(window.innerWidth / 960, window.innerHeight / 540); content3.style.transformOrigin = "center center"; content3.style.transform = `scale(${scale})`; } // src/shared/utils.ts function randomId() { return Math.random().toString(36).slice(7); } // src/js/scrollback.ts function initScrollback() { window.addEventListener("click", handleInputClick); } function autoScroll() {} function insert(node) { scrollback.append(node); } function addInput(id, input) { const parent = $$("li.input"); const status = $$("span.status.yellow", "•"); const content4 = $$("span.content", input); parent.append(status, content4); parent.dataset.id = id; insert(parent); scrollback.scrollTop = scrollback.scrollHeight - scrollback.clientHeight; } function setStatus(id, status) { const statusEl = document.querySelector(`[data-id="${id}"].input .status`); if (!statusEl) return; const colors = { waiting: "yellow", streaming: "purple", ok: "green", error: "red" }; statusEl.classList.remove(...Object.values(colors)); statusEl.classList.add(colors[status]); } function addOutput(id, output2) { const item = $$("li"); item.classList.add("output"); item.dataset.id = id || randomId(); const [format, content4] = processOutput(output2); if (format === "html") item.innerHTML = content4; else item.textContent = content4; const input = document.querySelector(`[data-id="${id}"].input`); if (input instanceof HTMLLIElement) { input.parentNode.insertBefore(item, input.nextSibling); } else { insert(item); } autoScroll(); } function addErrorMessage(message) { addOutput("", { html: `${message}` }); } function appendOutput(id, output2) { const item = document.querySelector(`[data-id="${id}"].output`); if (!item) { console.error(`output id ${id} not found`); return; } const [format, content4] = processOutput(output2); if (format === "html") item.innerHTML += content4; else item.textContent += content4; autoScroll(); } function replaceOutput(id, output2) { const item = document.querySelector(`[data-id="${id}"].output`); if (!item) { console.error(`output id ${id} not found`); return; } const [format, content4] = processOutput(output2); if (format === "html") item.innerHTML = content4; else item.textContent = content4; autoScroll(); } function processOutput(output) { let content = ""; let html = false; if (typeof output === "string") { content = output; } else if (Array.isArray(output)) { content = output.join(" "); } else if (typeof output === "object" && "html" in output) { html = true; content = output.html; if (output.script) eval(output.script); } else if (typeof output === "object" && "text" in output) { content = output.text; if (output.script) eval(output.script); } else if (typeof output === "object" && "script" in output) { eval(output.script); } else { content = JSON.stringify(output); } return [html ? "html" : "text", content]; } function handleInputClick(e) { const target = e.target; if (!(target instanceof HTMLElement)) return; if (target.matches(".input .content")) { cmdInput.value = target.textContent; } } function handleOutput(msg) { const result = msg.data; setStatus(msg.id, result.status); addOutput(msg.id, result.output); } // src/js/session.ts var sessionId = randomId(); // src/js/stream.ts function handleStreamStart(msg) { const id = msg.id; const status = document.querySelector(`[data-id="${id}"].input .status`); if (!status) return; addOutput(id, msg.data); status.classList.remove("yellow"); status.classList.add("purple"); } function handleStreamAppend(msg) { appendOutput(msg.id, msg.data); } function handleStreamReplace(msg) { replaceOutput(msg.id, msg.data); } function handleStreamEnd(_msg) {} // src/shared/game.ts class GameContext { ctx; constructor(ctx) { this.ctx = ctx; } width = 960; height = 540; clear(color) { if (color) this.rectfill(0, 0, this.width, this.height, color); else this.ctx.clearRect(0, 0, this.ctx.canvas.width, this.ctx.canvas.height); } text(msg, x, y, color = "black", size = 16, font = "C64ProMono") { const c = this.ctx; c.save(); c.fillStyle = color; c.font = `${size}px ${font}`; c.textBaseline = "top"; c.fillText(msg, x, y); c.restore(); } centerText(msg, color = "black", size = 16, font = "C64ProMono") { const c = this.ctx; c.save(); c.fillStyle = color; c.font = `${size}px ${font}`; c.textBaseline = "middle"; c.textAlign = "center"; c.fillText(msg, this.width / 2, this.height / 2); c.restore(); } centerTextX(msg, y, color = "black", size = 16, font = "C64ProMono") { const c = this.ctx; c.save(); c.fillStyle = color; c.font = `${size}px ${font}`; c.textBaseline = "middle"; c.textAlign = "center"; c.fillText(msg, this.width / 2, y); c.restore(); } centerTextY(msg, x, color = "black", size = 16, font = "C64ProMono") { const c = this.ctx; c.save(); c.fillStyle = color; c.font = `${size}px ${font}`; c.textBaseline = "middle"; c.textAlign = "center"; c.fillText(msg, x, this.height / 2); c.restore(); } circ(x, y, r, color = "black") { const c = this.ctx; c.save(); c.beginPath(); c.strokeStyle = color; c.arc(x, y, r, 0, Math.PI * 2); c.stroke(); c.restore(); } circfill(x, y, r, color = "black") { const c = this.ctx; c.save(); c.beginPath(); c.fillStyle = color; c.arc(x, y, r, 0, Math.PI * 2); c.fill(); c.restore(); } line(x0, y0, x1, y1, color = "black") { const c = this.ctx; c.save(); c.beginPath(); c.strokeStyle = color; c.moveTo(x0, y0); c.lineTo(x1, y1); c.stroke(); c.restore(); } oval(x0, y0, x1, y1, color = "black") { const c = this.ctx; const w = x1 - x0; const h = y1 - y0; c.save(); c.beginPath(); c.strokeStyle = color; c.ellipse(x0 + w / 2, y0 + h / 2, Math.abs(w) / 2, Math.abs(h) / 2, 0, 0, Math.PI * 2); c.stroke(); c.restore(); } ovalfill(x0, y0, x1, y1, color = "black") { const c = this.ctx; const w = x1 - x0; const h = y1 - y0; c.save(); c.beginPath(); c.fillStyle = color; c.ellipse(x0 + w / 2, y0 + h / 2, Math.abs(w) / 2, Math.abs(h) / 2, 0, 0, Math.PI * 2); c.fill(); c.restore(); } rect(x0, y0, x1, y1, color = "black") { const c = this.ctx; c.save(); c.beginPath(); c.strokeStyle = color; c.rect(x0, y0, x1 - x0, y1 - y0); c.stroke(); c.restore(); } rectfill(x0, y0, x1, y1, color = "black") { const c = this.ctx; c.save(); this.ctx.fillStyle = color; this.ctx.fillRect(x0, y0, x1 - x0, y1 - y0); c.restore(); } rrect(x, y, w, h, r, color = "black") { const c = this.ctx; c.save(); c.beginPath(); c.strokeStyle = color; this.roundRectPath(x, y, w, h, r); c.stroke(); c.restore(); } rrectfill(x, y, w, h, r, color = "black") { const c = this.ctx; c.save(); c.beginPath(); c.fillStyle = color; this.roundRectPath(x, y, w, h, r); c.fill(); c.restore(); } trianglefill(x0, y0, x1, y1, x2, y2, color = "black") { return this.polygonfill([ [x0, y0], [x1, y1], [x2, y2] ], color); } polygonfill(points, color = "black") { if (points.length < 3) return; const c = this.ctx; c.save(); c.beginPath(); c.fillStyle = color; c.moveTo(points[0][0], points[0][1]); for (let i = 1;i < points.length; i++) { c.lineTo(points[i][0], points[i][1]); } c.closePath(); c.fill(); c.restore(); } roundRectPath(x, y, w, h, r) { const c = this.ctx; c.moveTo(x + r, y); c.lineTo(x + w - r, y); c.quadraticCurveTo(x + w, y, x + w, y + r); c.lineTo(x + w, y + h - r); c.quadraticCurveTo(x + w, y + h, x + w - r, y + h); c.lineTo(x + r, y + h); c.quadraticCurveTo(x, y + h, x, y + h - r); c.lineTo(x, y + r); c.quadraticCurveTo(x, y, x + r, y); } } // src/js/focus.ts function initFocus() { window.addEventListener("click", focusHandler); focusInput(); } function focusInput() { cmdInput.focus(); } function focusHandler(e) { const target = e.target; if (!(target instanceof HTMLElement)) { focusInput(); return; } if (["INPUT", "TEXTAREA", "CANVAS", "A"].includes(target.tagName)) return false; const selection = window.getSelection() || ""; if (selection.toString() === "") focusInput(); e.preventDefault(); } // src/js/game.ts var FPS = 30; var HEIGHT = 540; var WIDTH = 980; var oldMode = "cinema"; var running = false; var canvas; var pressedStack = new Set; var pressed = { key: "", shift: false, ctrl: false, meta: false, pressed: pressedStack, prevPressed: new Set, justPressed: new Set, justReleased: new Set }; async function handleGameStart(msg) { const msgId = msg.id; const name = msg.data; let game; try { game = await import(`/source/${name}`); } catch (err) { setStatus(msgId, "error"); addOutput(msgId, `Error: ${err.message ? err.message : err}`); return; } if (document.body.dataset.mode === "tall") { browserCommands.mode?.(); oldMode = "tall"; } canvas = createCanvas(); canvas.focus(); setStatus(msgId, "ok"); window.addEventListener("keydown", handleKeydown); window.addEventListener("keyup", handleKeyup); window.addEventListener("resize", resizeCanvas); resizeCanvas(); gameLoop(new GameContext(canvas.getContext("2d")), game); } function createCanvas() { const canvas2 = $$("canvas.game.active"); canvas2.id = randomId(); canvas2.height = HEIGHT; canvas2.width = WIDTH; canvas2.tabIndex = 0; const main = document.querySelector("main"); main?.classList.add("game"); main?.parentNode?.insertBefore(canvas2, main); return canvas2; } function handleKeydown(e) { e.preventDefault(); if (e.key === "Escape" || e.ctrlKey && e.key === "c") { endGame(); } else { pressedStack.add(e.key); pressed.key = e.key; pressed.ctrl = e.ctrlKey; pressed.shift = e.shiftKey; pressed.meta = e.metaKey; } } function handleKeyup(e) { pressedStack.delete(e.key); if (pressedStack.size === 0) { pressed.key = ""; pressed.ctrl = false; pressed.shift = false; pressed.meta = false; } } function updateInputState() { pressed.justPressed = new Set([...pressed.pressed].filter((k) => !pressed.prevPressed.has(k))); pressed.justReleased = new Set([...pressed.prevPressed].filter((k) => !pressed.pressed.has(k))); pressed.prevPressed = new Set(pressed.pressed); } function resizeCanvas() { const scale = Math.min(window.innerWidth / 960, window.innerHeight / 540); canvas.width = 960 * scale; canvas.height = 540 * scale; const ctx = canvas.getContext("2d"); ctx.setTransform(scale, 0, 0, scale, 0, 0); } function gameLoop(ctx, game) { running = true; let last = 0; if (game.init) game.init(); function loop(ts) { if (!running) return; const delta = ts - last; if (delta >= 1000 / FPS) { updateInputState(); if (game.update) game.update(delta, pressed); if (game.draw) game.draw(ctx); last = ts; } requestAnimationFrame(loop); } requestAnimationFrame(loop); } function endGame() { running = false; window.removeEventListener("keydown", handleKeydown); window.removeEventListener("keyup", handleKeyup); window.removeEventListener("resize", resizeCanvas); if (oldMode === "tall") browserCommands.mode?.(); const main = document.querySelector("main"); main?.classList.remove("game"); canvas.classList.remove("active"); canvas.style.height = HEIGHT / 2 + "px"; canvas.style.width = WIDTH / 2 + "px"; const output2 = $$("li.output"); output2.append(canvas); insert(output2); focusInput(); } // src/js/dispatch.ts async function dispatchMessage(msg) { switch (msg.type) { case "output": handleOutput(msg); break; case "commands": cacheCommands(msg.data); break; case "error": console.error(msg.data); break; case "stream:start": handleStreamStart(msg); break; case "stream:end": handleStreamEnd(msg); break; case "stream:append": handleStreamAppend(msg); break; case "stream:replace": handleStreamReplace(msg); break; case "game:start": await handleGameStart(msg); break; case "ui:mode": browserCommands.mode?.(msg.data); break; default: console.error("unknown message type", msg); } } // src/js/websocket.ts var MAX_RETRIES = 5; var retries = 0; var connected = false; var msgQueue = []; var ws = null; function startConnection() { const url = new URL(`/ws?session=${sessionId}`, location.href); url.protocol = url.protocol.replace("http", "ws"); ws = new WebSocket(url); ws.onmessage = receive; ws.onclose = retryConnection; ws.onerror = () => ws?.close(); ws.onopen = () => { connected = true; msgQueue.forEach((msg) => send(msg)); msgQueue.length = 0; }; } function send(msg) { if (!connected) { msgQueue.push(msg); startConnection(); return; } if (!msg.session) msg.session = sessionId; ws?.readyState === 1 && ws.send(JSON.stringify(msg)); console.log("-> send", msg); } async function receive(e) { const data = JSON.parse(e.data); console.log("<- receive", data); await dispatchMessage(data); } function retryConnection() { connected = false; if (retries >= MAX_RETRIES) { addErrorMessage(`Failed to reconnect ${retries} times. Server is down.`); if (ws) ws.onclose = () => {}; return; } retries++; addErrorMessage(`Connection lost. Retrying...`); setTimeout(startConnection, 2000); } // src/js/commands.ts var commands = []; var browserCommands = { "browser-session": () => sessionId, clear: () => scrollback.innerHTML = "", commands: () => { return { html: "