<!DOCTYPE html><html><head> <link href="https://fonts.googleapis.com/css?family=Noto+Sans+Symbols+2&display=swap" rel="stylesheet" /> <style> body { background-color: gray; position: fixed; top: 0px; left: 0px; width: 100%; height: 100%; margin: 0px; user-select: none; } canvas { width: 100%; height: 100%; } #hud_topleft { position: fixed; top: 8px; left: 8px; } #hud_topright { position: fixed; top: 8px; right: 8px; } .cursor { position: fixed; user-select: none; color: gray; text-shadow: 1px 1px 1px black, -1px -1px 1px black, 1px -1px 1px black, -1px 1px 1px black; } .pointer { font-family: "Noto Sans Symbols 2"; position: relative; top: -3px; } .pressed { position: relative; top: 0px; } .txt { position: absolute; width: 300px; bottom: 30px; left: -150px; text-align: center; word-wrap: break-word; white-space: break-spaces; font-family: monospace; color: white; } </style> </head><body> <canvas></canvas> <div id="hud_topleft"> <div id="hud_time"> <div>t: <span id="hud_t"></span></div> <div>x: <span id="hud_x"></span></div> <div>y: <span id="hud_y"></span></div> </div> </div> <div id="hud_topright"> <div id="hud_coords" style="white-space: pre"></div> </div> <script> var canvas = { element: document.querySelector("canvas"), lines: [], mkline(x1, y1, x2, y2, color) { this.lines.push({x1, y1, x2, y2, color}); this.drawline(x1, y1, x2, y2, color); }, drawline(x1, y1, x2, y2, color) { x1 = (x1 + window.innerWidth / 2) * devicePixelRatio; y1 = (y1 + window.innerHeight / 2) * devicePixelRatio; x2 = (x2 + window.innerWidth / 2) * devicePixelRatio; y2 = (y2 + window.innerHeight / 2) * devicePixelRatio; var ctx = this.element.getContext("2d"); ctx.lineWidth = devicePixelRatio; ctx.beginPath(); ctx.moveTo(x1, y1); ctx.lineTo(x2, y2); ctx.strokeStyle = color || "gray"; ctx.shadowColor = "black"; ctx.shadowBlur = 2; ctx.shadowOffsetX = 1; ctx.shadowOffsetY = 1; ctx.stroke(); }, redraw() { var ctx = this.element.getContext("2d"); ctx.clearRect(0, 0, this.element.width, this.element.height); for (var {x1, y1, x2, y2, color} of this.lines) { this.drawline(x1, y1, x2, y2, color); } } }; (onresize = () => { canvas.element.width = innerWidth * devicePixelRatio; canvas.element.height = innerHeight * devicePixelRatio; canvas.redraw(); })(); class Cursor { constructor(id) { this.rootDiv = document.createElement("div"); this.rootDiv.className = "cursor"; this.pointerDiv = document.createElement("div"); this.pointerDiv.className = "pointer"; this.pointerDiv.innerText = "🮰"; this.rootDiv.appendChild(this.pointerDiv); this.txtDiv = document.createElement("div"); this.txtDiv.className = "txt"; this.rootDiv.appendChild(this.txtDiv); this.rootDiv.instance = this; document.body.appendChild(this.rootDiv); this.rootDiv.dataset.id = id; this.x = 0; this.y = 0; } get screenX() { return this.x + window.innerWidth / 2; } get screenY() { return this.y + window.innerHeight / 2; } set screenX(x) { this.x = x - window.innerWidth / 2; } set screenY(y) { this.y = y - window.innerHeight / 2; } get color() { return this.rootDiv.style.color; } set color(color) { this.rootDiv.style.color = color; } get pressed() { return this.pointerDiv.classList.contains("pressed"); } set pressed(pressed) { if (pressed) { this.pointerDiv.classList.add("pressed"); } else { this.pointerDiv.classList.remove("pressed"); } } get text() { return this.txtDiv.innerText; } set text(text) { this.txtDiv.innerText = text; } move(x = this.x, y = this.y) { if (this.pressed) canvas.mkline(this.x, this.y, x, y, this.color); this.x = x; this.y = y; this.rootDiv.style.left = this.screenX + "px"; this.rootDiv.style.top = this.screenY + "px"; } addText(chars) { for (var char of chars) { if (char == "␛") { this.txtDiv.innerText = ""; } else if (char == "⌫") { this.txtDiv.innerText = this.txtDiv.innerText.slice(0, -1); } else if (char == "⌦") { this.txtDiv.innerText = this.txtDiv.innerText.slice(1); } else { this.txtDiv.innerText += char; } } } remove() { this.rootDiv.remove(); } } function getCursorById(id) { return document.querySelector(`.cursor[data-id="${id}"]`)?.instance; }; var ws = new WebSocket(location.origin.replace('http','ws')); ws.onopen = () => load().then(run);; ws.onclose = stop; var records = {}; async function load() { var data = await fetch("record.jsonl").then(res => res.text()); if (!data) return; data = data.trim().split('\n').map(JSON.parse); for (var line of data) { records[line.i] ||= []; records[line.i].push(line); } } var selfCursor = new Cursor("self"); var last = {}, next = { color: localStorage.color ||= `rgb(${[Math.floor(Math.random()*256),Math.floor(Math.random()*256),Math.floor(Math.random()*256)]})`, x: null, y: null, pressed: false, text: null }; onmousemove = ({x,y}) => { next.x = x - innerWidth / 2; next.y = y - innerHeight / 2; }; onmousedown = () => next.pressed = true; onmouseup = () => next.pressed = false; onkeypress = event => { var text = event.key.replace("Escape", "␛").replace("Enter", "\n").replace("Backspace", "⌫").replace("Delete", "⌦") next.text ||= ""; next.text += text; }; onkeydown = event => { if (["Backspace", "Delete", "Escape"].includes(event.key)) onkeypress(event); }; var ticks = 0; function tick() { var tick = ticks++; hud_t.innerText = tick; var update = {a: tick}; if (last.x != next.x || last.y != next.y) { selfCursor.move(next.x, next.y); hud_x.innerText = update.x = next.x; hud_y.innerText = update.y = next.y; } if (last.pressed != next.pressed) { selfCursor.pressed = next.pressed; update.p = Number(next.pressed); } if (next.text) { selfCursor.addText(next.text); update.t = next.text; next.text = null; } if (last.color != next.color) { update.c = selfCursor.color = next.color; } if (Object.keys(update).length > 1) ws.send(JSON.stringify(update)); Object.assign(last, next); for (var id in records) { var cursor = getCursorById(id); if (!cursor) cursor = new Cursor(id); var entry; while ((entry = records[id][0]) && entry.a <= tick) { var {x, y, p, c, t} = entry; if (x || y) cursor.move(x, y); if (p) cursor.pressed = true; else if (p != null) cursor.pressed = false; if (t) cursor.addText(t); if (c) cursor.color = c; records[id].shift(); } if (!entry) { cursor?.remove(); delete records[id]; } } } var interval; function run() { interval = setInterval(tick, 1000/60); } function stop() { clearInterval(interval); } </script></body></html>