353 lines
6.6 KiB
HTML
353 lines
6.6 KiB
HTML
<!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> |