cursor-thing/index.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>