From fb653292612eddca5b088ed88466c546d1597107 Mon Sep 17 00:00:00 2001 From: Elizabeth Alexander Hunt Date: Sat, 20 Jun 2026 09:40:07 -0700 Subject: Initial commit --- static/js/canvas.js | 97 +++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 97 insertions(+) create mode 100644 static/js/canvas.js (limited to 'static/js/canvas.js') diff --git a/static/js/canvas.js b/static/js/canvas.js new file mode 100644 index 0000000..f164803 --- /dev/null +++ b/static/js/canvas.js @@ -0,0 +1,97 @@ +(function () { + const cfg = window.CANVAS; + const canvas = document.getElementById("canvas"); + const ctx = canvas.getContext("2d"); + canvas.style.width = cfg.cols * cfg.scale + "px"; + canvas.style.height = cfg.rows * cfg.scale + "px"; + + // palette picker + let selected = 0; + const paletteEl = document.getElementById("palette"); + cfg.palette.forEach((hex, i) => { + const sw = document.createElement("div"); + sw.className = "swatch" + (i === 0 ? " selected" : ""); + sw.style.background = hex; + sw.title = hex; + sw.addEventListener("click", () => { + selected = i; + [...paletteEl.children].forEach((c) => c.classList.remove("selected")); + sw.classList.add("selected"); + }); + paletteEl.appendChild(sw); + }); + + const hexFromInt = (c) => "#" + c.toString(16).padStart(6, "0"); + const drawPixel = (x, y, hex) => { + ctx.fillStyle = hex; + ctx.fillRect(x, y, 1, 1); + }; + + // The server is authoritative: we only paint pixels it broadcasts back. Open + // the socket first and queue messages until the initial state is painted, so + // nothing placed during the load is lost. + let loaded = false; + const queue = []; + function apply(msg) { + if (msg.type === "place") drawPixel(msg.x, msg.y, hexFromInt(msg.c)); + else if (msg.type === "cooldown") startCooldown(msg.ms); + } + + let ws; + function connect() { + const proto = location.protocol === "https:" ? "wss" : "ws"; + ws = new WebSocket(`${proto}://${location.host}/canvas/ws`); + ws.onmessage = (e) => { + const msg = JSON.parse(e.data); + loaded ? apply(msg) : queue.push(msg); + }; + ws.onclose = () => setTimeout(connect, 1000); + ws.onerror = () => ws.close(); + } + connect(); + + fetch("/canvas/state") + .then((r) => r.arrayBuffer()) + .then((buf) => { + const bytes = new Uint8Array(buf); + const img = ctx.createImageData(cfg.cols, cfg.rows); + for (let p = 0; p < cfg.cols * cfg.rows; p++) { + img.data[p * 4] = bytes[p * 3]; + img.data[p * 4 + 1] = bytes[p * 3 + 1]; + img.data[p * 4 + 2] = bytes[p * 3 + 2]; + img.data[p * 4 + 3] = 255; + } + ctx.putImageData(img, 0, 0); + loaded = true; + queue.forEach(apply); + queue.length = 0; + }); + + // cooldown indicator (server enforces; this is UI feedback) + let cooldownUntil = 0; + const cdEl = document.getElementById("cooldown"); + function startCooldown(ms) { + cooldownUntil = Math.max(cooldownUntil, Date.now() + ms); + tick(); + } + function tick() { + const rem = cooldownUntil - Date.now(); + if (rem > 0) { + cdEl.textContent = `cooldown: ${(rem / 1000).toFixed(1)}s`; + requestAnimationFrame(tick); + } else { + cdEl.textContent = ""; + } + } + + canvas.addEventListener("click", (e) => { + if (Date.now() < cooldownUntil) return; + if (!ws || ws.readyState !== WebSocket.OPEN) return; + const rect = canvas.getBoundingClientRect(); + const x = Math.floor(((e.clientX - rect.left) / rect.width) * cfg.cols); + const y = Math.floor(((e.clientY - rect.top) / rect.height) * cfg.rows); + if (x < 0 || x >= cfg.cols || y < 0 || y >= cfg.rows) return; + ws.send(JSON.stringify({ type: "place", x, y, i: selected })); + startCooldown(cfg.cooldownMs); + }); +})(); -- cgit v1.3