This commit is contained in:
Lamp 2023-11-16 17:12:35 -08:00
parent 9f8b5dcdb0
commit 950a6fdc43
3 changed files with 108 additions and 215 deletions

4
.gitignore vendored

@ -1,3 +1,5 @@
node_modules
tmp
pack.tar
pack.tar
nextid
record.jsonl

@ -120,7 +120,7 @@ var canvas = {
class Cursor {
constructor() {
constructor(id) {
this.rootDiv = document.createElement("div");
this.rootDiv.className = "cursor";
@ -136,6 +136,7 @@ class Cursor {
this.rootDiv.instance = this;
document.body.appendChild(this.rootDiv);
this.rootDiv.dataset.id = id;
this.x = 0;
this.y = 0;
}
@ -171,6 +172,13 @@ class Cursor {
}
}
get text() {
return this.txtDiv.innerText;
}
set text(text) {
this.txtDiv.innerText = text;
}
move(x, y) {
if (this.pressed) canvas.mkline(this.x, this.y, x, y, this.color);
this.x = x;
@ -196,227 +204,42 @@ class Cursor {
}
}
var client = {
ws: null,
connect() {
return new Promise((resolve, reject) => {
this.ws = new WebSocket("ws://localhost:2834");
this.ws.binaryType = "arraybuffer";
this.ws.onopen = () => {
resolve();
};
this.ws.onmessage = event => {
};
this.ws.onclose = () => {
thing.stop();
reject();
};
this.ws.onerror = error => {
reject(error);
}
});
},
send(data) {
if (this.ws?.readyState == WebSocket.OPEN) this.ws.send(data);
},
function getCursorById(id) {
return document.querySelector(`.cursor[data-id="${id}"]`)?.instance;
};
var thing = {
records: [],
running: false,
startTime: null,
stopTime: null,
get elapsed() {
if (this.running) return Date.now() - this.startTime;
else return this.stopTime;
},
start() {
this.startTime = Date.now();
this.running = true;
(function timeDisplayLoop() {
if (!thing.running) return;
hud_t.innerText = thing.elapsed;
requestAnimationFrame(timeDisplayLoop);
})();
this.records.forEach(record => {
this.playRecord(record);
});
},
stop() {
this.stopTime = this.elapsed;
this.running = false;
},
recordCoordinates(x, y) {
var ab = new ArrayBuffer(9);
var dv = new DataView(ab);
dv.setUint32(0, this.elapsed);
dv.setUint8(4, 0);
dv.setInt16(5, x);
dv.setInt16(7, y);
client.send(ab);
},
recordCode(code) {
var ab = new ArrayBuffer(5);
var dv = new DataView(ab);
dv.setUint32(0, this.elapsed);
dv.setUint8(4, code);
client.send(ab);
},
recordPress() { this.recordCode(1) },
recordRelease() { this.recordCode(2) },
recordBlur() { this.recordCode(3) },
recordFocus() { this.recordCode(4) },
recordColor(colorString) {
var [r, g, b] = colorString.match(/\d+/g);
var ab = new ArrayBuffer(8);
var dv = new DataView(ab);
dv.setUint32(0, this.elapsed);
dv.setUint8(4, 6);
dv.setUint8(5, r);
dv.setUint8(6, g);
dv.setUint8(7, b);
client.send(ab);
},
recordText(text) {
text = text.substring(0, 85);
var encoder = new TextEncoder();
var data = encoder.encode(text);
var ab = new ArrayBuffer(6);
var dv = new DataView(ab);
dv.setUint32(0, this.elapsed);
dv.setUint8(4, 5);
dv.setUint8(5, data.length);
var blob = new Blob([ab, data]);
client.send(blob);
},
async load() {
var data = await fetch("pack.tar").then(res => res.arrayBuffer());
var extractedFiles = await untar(data);
for (var file of extractedFiles) {
if (!file.buffer.byteLength) continue;
this.records.push(file.buffer);
}
},
playRecord(data) {
var cursor = new Cursor();
var dv = new DataView(data);
(function readLoop(offset){
if (offset >= data.byteLength) {
cursor.remove();
return;
}
var timestamp = dv.getUint32(offset); offset += 4;
var msToNextEvent = timestamp - thing.elapsed;
if (msToNextEvent < 0) {
console.warn("behind!", msToNextEvent);
}
setTimeout(() => {
if (!thing.running) return;
var opcode = dv.getUint8(offset); offset += 1;
switch (opcode) {
case 0:
// move
let x = dv.getInt16(offset); offset += 2;
let y = dv.getInt16(offset); offset += 2;
cursor.move(x, y);
break;
case 1:
//press
cursor.pressed = true;
break;
case 2:
//release
cursor.pressed = false;
break;
case 3:
//blur
cursor.blurred = true;
break;
case 4:
//focus
cursor.blurred = false;
break;
case 6:
// color
let rgb = [
dv.getUint8(offset++),
dv.getUint8(offset++),
dv.getUint8(offset++)
];
cursor.color = `rgb(${rgb})`;
break;
case 5:
//text
let length = dv.getUint8(offset); offset += 1;
let textBuffer = data.slice(offset, offset += length);
let decoder = new TextDecoder();
let text = decoder.decode(textBuffer);
cursor.addText(text);
break;
}
readLoop(offset);
}, msToNextEvent);
})(0);
}
}
var selfCursor = new Cursor();
var selfCursor = new Cursor("self");
selfCursor.color = localStorage.color ||= `rgb(${[Math.floor(Math.random()*256),Math.floor(Math.random()*256),Math.floor(Math.random()*256)]})`;
onmousemove = ({x, y}) => {
if (!thing.running) return;
x = x - innerWidth / 2;
y = y - innerHeight / 2;
selfCursor.move(x, y);
hud_x.innerText = x;
hud_y.innerText = y;
thing.recordCoordinates(x, y);
};
onmousedown = event => {
if (!thing.running) return;
selfCursor.pressed = true;
thing.recordPress();
};
onmouseup = event => {
if (!thing.running) return;
selfCursor.pressed = false;
thing.recordRelease();
};
onblur = event => {
if (!thing.running) return;
selfCursor.blurred = true;
thing.recordBlur();
}
onfocus = event => {
if (!thing.running) return;
selfCursor.blurred = false;
thing.recordFocus();
};
onkeypress = event => {
switch (event.key) {
@ -436,7 +259,6 @@ onkeypress = event => {
var txt = event.key;
}
selfCursor.addText(txt);
thing.recordText(txt);
};
onkeydown = event => {
if (["Backspace", "Delete", "Escape"].includes(event.key)) onkeypress(event);
@ -447,17 +269,79 @@ onkeydown = event => {
var ws = new WebSocket("ws://localhost:2834");
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 ticks = 0;
var last = {};
function tick() {
var tick = ticks++;
hud_t.innerText = tick;
var update = {a: tick};
if (last.x != selfCursor.x) update.x = selfCursor.x;
if (last.y != selfCursor.y) update.y = selfCursor.y;
if (last.p != selfCursor.pressed) update.p = Number(selfCursor.pressed);
if (last.c != selfCursor.color) update.c = selfCursor.color;
if (last.t != selfCursor.text) update.t = selfCursor.text;
if (Object.keys(update).length > 1) ws.send(JSON.stringify(update));
Object.assign(last, update);
for (var id in records) {
var entry;
while ((entry = records[id][0]) && entry.a <= tick) {
var {x, y, p, c, t} = entry;
var cursor = getCursorById(id);
if (!cursor) cursor = new Cursor(id);
if (x || y) cursor.move(x, y);
if (p) cursor.pressed = true;
else if (p != null) cursor.pressed = false;
if (c) cursor.color = c;
if (t) cursor.text = t;
records[id].shift();
}
if (!entry) delete records[id];
}
}
var interval;
function run() {
interval = setInterval(tick, 1000/60);
}
function stop() {
clearInterval(interval);
}
client.connect().then(() => {
thing.load().then(() => {
thing.start();
});
thing.recordColor(selfCursor.color);
});

@ -1,32 +1,39 @@
import WebSocket, { WebSocketServer } from "ws";
import fs from "fs";
import path from "path";
import tar from "tar";
var record = fs.createWriteStream("record.jsonl");
var wss = new WebSocketServer({
port: 2834
});
wss.on("connection", (ws, req) => {
var filepath = path.join("tmp", Math.random().toString(36).slice(2));
var writestream = fs.createWriteStream(filepath);
var id = dispenseId();
ws.on("message", (data, isBinary) => {
writestream.write(data);
if (isBinary) return ws.close(1003);
try {
data = JSON.parse(data);
data.i = id;
data = JSON.stringify(data);
record.write(data + '\n');
} catch(error) {
console.error(error.message);
ws.close(1002);
}
});
ws.on("close", () => {
writestream.close();
pack(filepath);
});
ws.on("error", error => console.error(error.message));
});
function pack(file) {
tar.r({
file: "pack.tar",
gzip: false,
sync: true
}, [file]);
function dispenseId() {
try {
var id = Number(fs.readFileSync("nextid", "utf8"));
} catch (e) {}
if (!id || isNaN(id)) id = 0;
fs.writeFileSync("nextid", String(id + 1));
return id;
}