<!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>