summaryrefslogtreecommitdiff
path: root/static/js/canvas.js
diff options
context:
space:
mode:
Diffstat (limited to 'static/js/canvas.js')
-rw-r--r--static/js/canvas.js97
1 files changed, 97 insertions, 0 deletions
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);
+ });
+})();