tick
This commit is contained in:
parent
9f8b5dcdb0
commit
950a6fdc43
4
.gitignore
vendored
4
.gitignore
vendored
@ -1,3 +1,5 @@
|
||||
node_modules
|
||||
tmp
|
||||
pack.tar
|
||||
pack.tar
|
||||
nextid
|
||||
record.jsonl
|
280
index.html
280
index.html
@ -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);
|
||||
});
|
||||
|
||||
|
||||
|
||||
|
39
server.mjs
39
server.mjs
@ -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;
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user