summaryrefslogtreecommitdiff
path: root/static/js/canvas.js
blob: f164803576eca9c5eed063ac740f291bffbaa987 (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
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);
  });
})();