Browse Source

f32 coordinates

infinite
A.Olokhtonov 2 years ago
parent
commit
343008c0af
  1. 350
      client/client_recv.js
  2. 88
      client/client_send.js
  3. 116
      client/index.html
  4. 38
      client/index.js
  5. 2
      client/math.js
  6. 44
      client/old_index.html
  7. 345
      client/recv.js
  8. 110
      client/webgl.html
  9. 103
      client/webgl.js
  10. 92
      client/webgl_geometry.js
  11. 60
      client/webgl_listeners.js
  12. 32
      client/websocket.js
  13. 33
      server/deserializer.js
  14. 1
      server/recv.js
  15. 4
      server/send.js
  16. 15
      server/serializer.js

350
client/client_recv.js

@ -0,0 +1,350 @@ @@ -0,0 +1,350 @@
function deserializer_create(buffer, dataview) {
return {
'offset': 0,
'size': buffer.byteLength,
'buffer': buffer,
'view': dataview,
'strview': new Uint8Array(buffer),
};
}
function des_u8(d) {
const value = d.view.getUint8(d.offset);
d.offset += 1;
return value;
}
function des_u16(d) {
const value = d.view.getUint16(d.offset, true);
d.offset += 2;
return value;
}
function des_u32(d) {
const value = d.view.getUint32(d.offset, true);
d.offset += 4;
return value;
}
function des_f32(d) {
const value = d.view.getFloat32(d.offset, true);
d.offset += 4;
return value;
}
function des_f32array(d, count) {
const result = [];
for (let i = 0; i < count; ++i) {
const item = d.view.getFloat32(d.offset, true);
d.offset += 4;
result.push(item);
}
return result;
}
function des_event(d) {
const event = {};
event.type = des_u8(d);
event.user_id = des_u32(d);
switch (event.type) {
case EVENT.PREDRAW: {
event.x = des_f32(d);
event.y = des_f32(d);
break;
}
case EVENT.STROKE: {
const stroke_id = des_u32(d);
const point_count = des_u16(d);
const width = des_u16(d);
const color = des_u32(d);
const coords = des_f32array(d, point_count * 2);
event.stroke_id = stroke_id;
event.points = [];
for (let i = 0; i < point_count; ++i) {
const x = coords[2 * i + 0];
const y = coords[2 * i + 1];
event.points.push({'x': x, 'y': y});
}
event.color = color;
event.width = width;
break;
}
case EVENT.IMAGE:
case EVENT.IMAGE_MOVE: {
event.image_id = des_u32(d);
event.x = des_f32(d);
event.y = des_f32(d);
break;
}
case EVENT.UNDO:
case EVENT.REDO: {
break;
}
case EVENT.ERASER: {
event.stroke_id = des_u32(d);
break;
}
default: {
console.error('fuck');
}
}
return event;
}
function bitmap_bbox(event) {
const bbox = {
'xmin': event.x,
'xmax': event.x + event.bitmap.width,
'ymin': event.y,
'ymax': event.y + event.bitmap.height
};
return bbox;
}
async function handle_event(state, context, event, relax = false) {
if (config.debug_print) console.debug(`event type ${event.type} from user ${event.user_id}`);
switch (event.type) {
case EVENT.STROKE: {
if (event.user_id != state.me) {
clear_dynamic_stroke(state, context, event.user_id);
}
add_static_stroke(state, context, event, relax);
break;
}
case EVENT.UNDO: {
console.error('todo');
// for (let i = state.events.length - 1; i >=0; --i) {
// const other_event = state.events[i];
// // Users can only undo their own, undeleted (not already undone) events
// if (other_event.user_id === event.user_id && !other_event.deleted) {
// if (other_event.type === EVENT.STROKE) {
// other_event.deleted = true;
// const stats = stroke_stats(other_event.points, state.cursor.width);
// redraw_region(stats.bbox);
// break;
// } else if (other_event.type === EVENT.IMAGE) {
// other_event.deleted = true;
// const item = document.querySelector(`img[data-image-id="${other_event.image_id}"]`);
// if (item) item.remove();
// break;
// } else if (other_event.type === EVENT.ERASER) {
// other_event.deleted = true;
// const erased = find_stroke_backwards(other_event.stroke_id);
// if (erased) {
// erased.deleted = false;
// const stats = stroke_stats(erased.points, state.cursor.width);
// redraw_region(stats.bbox);
// }
// break;
// } else if (other_event.type === EVENT.IMAGE_MOVE) {
// const item = document.querySelector(`img[data-image-id="${other_event.image_id}"]`);
// const ix = state.images[other_event.image_id].x -= other_event.x;
// const iy = state.images[other_event.image_id].y -= other_event.y;
// item.style.transform = `translate(${ix}px, ${iy}px)`;
// break;
// }
// }
// }
break;
}
case EVENT.IMAGE: {
console.error('todo');
// const url = config.image_url + event.image_id;
// const item = document.createElement('img');
// item.classList.add('floating-image');
// item.style['z-index'] = state.events.length;
// item.setAttribute('data-image-id', event.image_id);
// item.setAttribute('src', url);
// item.style.transform = `translate(${event.x}px, ${event.y}px)`;
// elements.images.appendChild(item);
// state.images[event.image_id] = {
// 'x': event.x, 'y': event.y
// };
// const r = await fetch(config.image_url + event.image_id);
// const blob = await r.blob();
// const bitmap = await createImageBitmap(blob);
// event.bitmap = bitmap;
// const bbox = bitmap_bbox(event);
// state.ctx0.drawImage(bitmap, bbox.xmin, bbox.ymin);
break;
}
case EVENT.IMAGE_MOVE: {
console.error('todo');
// // Already moved due to local prediction
// if (event.user_id !== state.me.id) {
// const image_id = event.image_id;
// const item = document.querySelector(`.floating-image[data-image-id="${image_id}"]`);
// const ix = state.images[event.image_id].x += event.x;
// const iy = state.images[event.image_id].y += event.y;
// if (item) {
// item.style.transform = `translate(${ix}px, ${iy}px)`;
// }
// }
break;
}
case EVENT.ERASER: {
console.error('todo');
// if (event.deleted) {
// break;
// }
// for (const other_event of state.events) {
// if (other_event.type === EVENT.STROKE && other_event.stroke_id === event.stroke_id) {
// // Might already be deleted because of local prediction
// if (!other_event.deleted) {
// other_event.deleted = true;
// const stats = stroke_stats(other_event.points, state.cursor.width);
// redraw_region(stats.bbox);
// }
// break;
// }
// }
break;
}
default: {
console.error('fuck');
}
}
}
async function handle_message(state, context, d) {
const message_type = des_u8(d);
let do_draw = false;
if (config.debug_print) console.debug(message_type);
switch (message_type) {
case MESSAGE.JOIN:
case MESSAGE.INIT: {
state.me = des_u32(d);
state.server_lsn = des_u32(d);
if (state.server_lsn > state.lsn) {
// Server knows something that we don't
state.lsn = state.server_lsn;
}
if (message_type === MESSAGE.JOIN) {
localStorage.setItem('sessionId', des_u32(d));
if (config.debug_print) console.debug('join in');
} else {
if (config.debug_print) console.debug('init in');
}
const event_count = des_u32(d);
if (config.debug_print) console.debug(`${event_count} events in init`);
state.events.length = 0;
for (let i = 0; i < event_count; ++i) {
const event = des_event(d);
await handle_event(state, context, event, true);
state.events.push(event);
}
recompute_static_data(context);
do_draw = true;
send_ack(event_count);
sync_queue(state);
break;
}
case MESSAGE.FIRE: {
const user_id = des_u32(d);
const predraw_event = des_event(d);
update_dynamic_stroke(state, context, user_id, {'x': predraw_event.x, 'y': predraw_event.y});
do_draw = true;
break;
}
case MESSAGE.ACK: {
const lsn = des_u32(d);
if (config.debug_print) console.debug(`ack ${lsn} in`);
if (lsn > state.server_lsn) {
// ACKs may arrive out of order
state.server_lsn = lsn;
}
break;
}
case MESSAGE.SYN: {
const sn = des_u32(d);
const count = des_u32(d);
const we_expect = sn - state.sn;
const first = count - we_expect;
if (config.debug_print) console.debug(`syn ${sn} in`);
for (let i = 0; i < count; ++i) {
const event = des_event(d);
if (i >= first) {
handle_event(state, context, event);
state.events.push(event);
}
}
do_draw = true;
state.sn = sn;
send_ack(sn); // await?
break;
}
default: {
console.error('fuck');
return;
}
}
if (do_draw) {
window.requestAnimationFrame(() => draw(state, context));
}
}

88
client/send.js → client/client_send.js

@ -19,19 +19,29 @@ function ser_u16(s, value) { @@ -19,19 +19,29 @@ function ser_u16(s, value) {
s.offset += 2;
}
function ser_f32(s, value) {
s.view.setFloat32(s.offset, value, true);
s.offset += 4;
}
function ser_u32(s, value) {
s.view.setUint32(s.offset, value, true);
s.offset += 4;
}
function ser_align(s, to) {
while (s.offset % to != 0) {
s.offset++;
}
}
function ser_event(s, event) {
ser_u8(s, event.type);
ser_u8(s, 0); // padding for 16bit alignment
switch (event.type) {
case EVENT.PREDRAW: {
ser_u16(s, event.x);
ser_u16(s, event.y);
ser_f32(s, event.x);
ser_f32(s, event.y);
break;
}
@ -42,9 +52,11 @@ function ser_event(s, event) { @@ -42,9 +52,11 @@ function ser_event(s, event) {
if (config.debug_print) console.debug('original', event.points);
ser_align(s, 4);
for (const point of event.points) {
ser_u16(s, point.x);
ser_u16(s, point.y);
ser_f32(s, point.x);
ser_f32(s, point.y);
}
break;
@ -54,8 +66,8 @@ function ser_event(s, event) { @@ -54,8 +66,8 @@ function ser_event(s, event) {
case EVENT.IMAGE_MOVE: {
const image_id = parseInt(event.image_id);
ser_u32(s, image_id);
ser_u16(s, event.x);
ser_u16(s, event.y);
ser_f32(s, event.x);
ser_f32(s, event.y);
break;
}
@ -90,39 +102,38 @@ async function send_ack(sn) { @@ -90,39 +102,38 @@ async function send_ack(sn) {
}
}
async function sync_queue() {
async function sync_queue(state) {
if (ws === null) {
if (config.debug_print) console.debug('socket has closed, stopping SYNs');
return;
}
let size = 1 + 1 + 4 + 4; // opcode + lsn + event count
let count = storage.lsn - storage.server_lsn;
let size = 1 + 3 + 4 + 4; // opcode + lsn + event count
let count = state.lsn - state.server_lsn;
if (count === 0) {
if (config.debug_print) console.debug('server ACKed all events, clearing queue');
storage.queue.length = 0;
state.queue.length = 0;
return;
}
for (let i = count - 1; i >= 0; --i) {
const event = storage.queue[storage.queue.length - 1 - i];
const event = state.queue[state.queue.length - 1 - i];
size += event_size(event);
}
const s = serializer_create(size);
ser_u8(s, MESSAGE.SYN);
ser_u8(s, 0); // padding for 16bit alignment
ser_u32(s, storage.lsn);
ser_u32(s, state.lsn);
ser_u32(s, count);
for (let i = count - 1; i >= 0; --i) {
const event = storage.queue[storage.queue.length - 1 - i];
const event = state.queue[state.queue.length - 1 - i];
ser_event(s, event);
}
if (config.debug_print) console.debug(`syn ${storage.lsn} out`);
if (config.debug_print) console.debug(`syn ${state.lsn} out`);
try {
if (ws) await ws.send(s.buffer);
@ -130,19 +141,17 @@ async function sync_queue() { @@ -130,19 +141,17 @@ async function sync_queue() {
ws.close();
}
setTimeout(sync_queue, config.sync_timeout);
setTimeout(() => sync_queue(state), config.sync_timeout);
}
function push_event(event) {
storage.lsn += 1;
function push_event(state, event) {
state.lsn += 1;
switch (event.type) {
case EVENT.STROKE: {
const points = process_stroke(event.points);
storage.queue.push({
state.queue.push({
'type': EVENT.STROKE,
'points': points,
'points': event.points,
'width': event.width,
'color': event.color,
});
@ -152,7 +161,7 @@ function push_event(event) { @@ -152,7 +161,7 @@ function push_event(event) {
case EVENT.RULER: {
event.type = EVENT.STROKE;
storage.queue.push(event);
state.queue.push(event);
break;
}
@ -161,7 +170,7 @@ function push_event(event) { @@ -161,7 +170,7 @@ function push_event(event) {
case EVENT.IMAGE_MOVE:
case EVENT.UNDO:
case EVENT.REDO: {
storage.queue.push(event);
state.queue.push(event);
break;
}
@ -172,18 +181,18 @@ function push_event(event) { @@ -172,18 +181,18 @@ function push_event(event) {
}
// Queue an event and initialize repated sends until ACKed
function queue_event(event, skip = false) {
push_event(event);
function queue_event(state, event, skip = false) {
push_event(state, event);
if (skip) {
return;
}
if (storage.timers.queue_sync) {
clearTimeout(storage.timers.queue_sync);
if (state.timers.queue_sync) {
clearTimeout(state.timers.queue_sync);
}
sync_queue();
sync_queue(state);
}
// Fire and forget. Doesn't do anything if we are offline
@ -198,4 +207,21 @@ async function fire_event(event) { @@ -198,4 +207,21 @@ async function fire_event(event) {
} catch(e) {
ws.close();
}
}
function predraw_event(x, y) {
return {
'type': EVENT.PREDRAW,
'x': x,
'y': y
};
}
function stroke_event(state) {
return {
'type': EVENT.STROKE,
'points': state.current_strokes[state.me].points,
'width': state.current_strokes[state.me].width,
'color': state.current_strokes[state.me].color,
};
}

116
client/index.html

@ -5,23 +5,101 @@ @@ -5,23 +5,101 @@
<title>Desk</title>
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
<link rel="shortcut icon" href="icons/favicon.svg" id="favicon">
<link rel="stylesheet" type="text/css" href="default.css?v=6">
<link rel="stylesheet" type="text/css" href="touch.css?v=4">
<script type="text/javascript" src="index.js?v=10"></script>
<script type="text/javascript" src="cursor.js?v=5"></script>
<script type="text/javascript" src="touch.js?v=20"></script>
<script type="text/javascript" src="websocket.js?v=6"></script>
<script type="text/javascript" src="send.js?v=5"></script>
<script type="text/javascript" src="recv.js?v=5"></script>
<script type="text/javascript" src="math.js?v=5"></script>
<script type="text/javascript" src="draw.js?v=5"></script>
<script type="text/javascript" src="tools.js?v=6"></script>
<script type="text/javascript" src="aux.js?v=5"></script>
<script type="text/javascript" src="tools.js?v=5"></script>
<script type="text/javascript" src="webgl_geometry.js?v=5"></script>
<script type="text/javascript" src="webgl_shaders.js?v=5"></script>
<script type="text/javascript" src="webgl_listeners.js?v=5"></script>
<script type="text/javascript" src="webgl.js?v=5"></script>
<script type="text/javascript" src="client_send.js?v=6"></script>
<script type="text/javascript" src="client_recv.js?v=6"></script>
<script type="text/javascript" src="websocket.js?v=7"></script>
<style>
html, body {
margin: 0;
padding: 0;
width: 100%;
height: 100%;
overflow: hidden;
}
canvas {
width: 100%;
height: 100%;
display: block;
cursor: crosshair;
}
canvas.movemode {
cursor: grab;
}
canvas.movemode.moving {
cursor: grabbing;
}
.tools-wrapper {
position: fixed;
bottom: 0;
width: 100%;
height: 32px;
display: flex;
justify-content: center;
align-items: end;
z-index: 10;
pointer-events: none;
}
.tools {
pointer-events: all;
display: flex;
align-items: center;
justify-content: center;
background: #333;
border-radius: 5px;
border-bottom-right-radius: 0;
border-bottom-left-radius: 0;
height: 42px;
padding-left: 10px;
padding-right: 10px;
}
.tool {
cursor: pointer;
padding-left: 10px;
padding-right: 10px;
height: 100%;
display: flex;
align-items: center;
background: #333;
transition: transform .1s ease-in-out;
user-select: none;
}
.tool:hover {
background: #888;
}
.tool.active {
transform: translateY(-10px);
border-top-right-radius: 5px;
border-top-left-radius: 5px;
background: #333;
}
.tool img {
height: 24px;
width: 24px;
filter: invert(100%);
}
</style>
</head>
<body>
<div class="toolbar">
<input type="color" id="brush-color">
<input type="number" min="1" id="brush-width">
</div>
<canvas id="c"></canvas>
<div class="tools-wrapper">
<div class="tools">
@ -32,13 +110,5 @@ @@ -32,13 +110,5 @@
<!-- <div class="tool" data-tool="redo"><img draggable="false" src="icons/redo.svg"></div> -->
</div>
</div>
<div id="brush-preview" class="dhide"></div>
<canvas class="canvas white" id="canvas0"></canvas>
<canvas class="canvas" id="canvas1"></canvas>
<div class="canvas" id="canvas-images"></div>
<div id="toucher"></div>
</body>
</html>
</html>

38
client/index.js

@ -104,44 +104,6 @@ const elements = { @@ -104,44 +104,6 @@ const elements = {
'active_image': null,
};
function event_size(event) {
let size = 1 + 1; // type + padding
switch (event.type) {
case EVENT.PREDRAW: {
size += 2 * 2;
break;
}
case EVENT.STROKE: {
size += 4 + 2 + 2 + 4 + event.points.length * 2 * 2; // u32 stroke id + u16 (count) + u16 (width) + u32 (color + count * (u16, u16) points
break;
}
case EVENT.UNDO:
case EVENT.REDO: {
break;
}
case EVENT.IMAGE:
case EVENT.IMAGE_MOVE: {
size += 4 + 2 + 2; // file id + x + y
break;
}
case EVENT.ERASER: {
size += 4; // stroke id
break;
}
default: {
console.error('fuck');
}
}
return size;
}
function move_canvas() {
elements.canvas0.style.transform = `translate(${-storage.canvas.offset_x}px, ${-storage.canvas.offset_y}px) scale(${storage.canvas.zoom})`;
elements.canvas1.style.transform = `translate(${-storage.canvas.offset_x}px, ${-storage.canvas.offset_y}px) scale(${storage.canvas.zoom})`;

2
client/math.js

@ -12,7 +12,7 @@ function point_right_of_line(a, b, p) { @@ -12,7 +12,7 @@ function point_right_of_line(a, b, p) {
}
function rdp_find_max(points, start, end) {
const EPS = 0.5;
const EPS = 0.5; // TODO: base this on zoom (and/or "speed")
let result = -1;
let max_dist = 0;

44
client/old_index.html

@ -0,0 +1,44 @@ @@ -0,0 +1,44 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>Desk</title>
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
<link rel="shortcut icon" href="icons/favicon.svg" id="favicon">
<link rel="stylesheet" type="text/css" href="default.css?v=6">
<link rel="stylesheet" type="text/css" href="touch.css?v=4">
<script type="text/javascript" src="index.js?v=10"></script>
<script type="text/javascript" src="cursor.js?v=5"></script>
<script type="text/javascript" src="touch.js?v=20"></script>
<script type="text/javascript" src="websocket.js?v=6"></script>
<script type="text/javascript" src="send.js?v=5"></script>
<script type="text/javascript" src="recv.js?v=5"></script>
<script type="text/javascript" src="math.js?v=5"></script>
<script type="text/javascript" src="draw.js?v=5"></script>
<script type="text/javascript" src="tools.js?v=6"></script>
</head>
<body>
<div class="toolbar">
<input type="color" id="brush-color">
<input type="number" min="1" id="brush-width">
</div>
<div class="tools-wrapper">
<div class="tools">
<div class="tool" data-tool="pencil"><img draggable="false" src="icons/draw.svg"></div>
<div class="tool" data-tool="ruler"><img draggable="false" src="icons/ruler.svg"></div>
<div class="tool" data-tool="eraser"><img draggable="false" src="icons/erase.svg"></div>
<div class="tool" data-tool="undo"><img draggable="false" src="icons/undo.svg"></div>
<!-- <div class="tool" data-tool="redo"><img draggable="false" src="icons/redo.svg"></div> -->
</div>
</div>
<div id="brush-preview" class="dhide"></div>
<canvas class="canvas white" id="canvas0"></canvas>
<canvas class="canvas" id="canvas1"></canvas>
<div class="canvas" id="canvas-images"></div>
<div id="toucher"></div>
</body>
</html>

345
client/recv.js

@ -1,345 +0,0 @@ @@ -1,345 +0,0 @@
function deserializer_create(buffer, dataview) {
return {
'offset': 0,
'size': buffer.byteLength,
'buffer': buffer,
'view': dataview,
'strview': new Uint8Array(buffer),
};
}
function des_u8(d) {
const value = d.view.getUint8(d.offset);
d.offset += 1;
return value;
}
function des_u16(d) {
const value = d.view.getUint16(d.offset, true);
d.offset += 2;
return value;
}
function des_s16(d) {
const value = d.view.getInt16(d.offset, true);
d.offset += 2;
return value;
}
function des_u32(d) {
const value = d.view.getUint32(d.offset, true);
d.offset += 4;
return value;
}
function des_u16array(d, count) {
const result = [];
for (let i = 0; i < count; ++i) {
const item = d.view.getUint16(d.offset, true);
d.offset += 2;
result.push(item);
}
return result;
}
function des_event(d) {
const event = {};
event.type = des_u8(d);
event.user_id = des_u32(d);
switch (event.type) {
case EVENT.PREDRAW: {
event.x = des_u16(d);
event.y = des_u16(d);
break;
}
case EVENT.STROKE: {
const stroke_id = des_u32(d);
const point_count = des_u16(d);
const width = des_u16(d);
const color = des_u32(d);
const coords = des_u16array(d, point_count * 2);
event.stroke_id = stroke_id;
event.points = [];
for (let i = 0; i < point_count; ++i) {
const x = coords[2 * i + 0];
const y = coords[2 * i + 1];
event.points.push({'x': x, 'y': y});
}
event.color = color;
event.width = width;
break;
}
case EVENT.IMAGE:
case EVENT.IMAGE_MOVE: {
event.image_id = des_u32(d);
event.x = des_s16(d); // stored as u16, but actually is s16
event.y = des_s16(d); // stored as u16, but actually is s16
break;
}
case EVENT.UNDO:
case EVENT.REDO: {
break;
}
case EVENT.ERASER: {
event.stroke_id = des_u32(d);
break;
}
default: {
console.error('fuck');
}
}
return event;
}
function bitmap_bbox(event) {
const x = (event.x <= storage.canvas.width ? event.x : event.x - 65536);
const y = (event.y <= storage.canvas.height ? event.y : event.y - 65536);
const bbox = {
'xmin': x,
'xmax': x + event.bitmap.width,
'ymin': y,
'ymax': y + event.bitmap.height
};
return bbox;
}
async function handle_event(event) {
if (config.debug_print) console.debug(`event type ${event.type} from user ${event.user_id}`);
// TODO(@speed): do not handle locally predicted events
switch (event.type) {
case EVENT.STROKE: {
if (event.user_id in storage.predraw || event.user_id === storage.me.id) {
storage.predraw[event.user_id] = [];
redraw_predraw();
}
draw_stroke(event);
break;
}
case EVENT.UNDO: {
for (let i = storage.events.length - 1; i >=0; --i) {
const other_event = storage.events[i];
// Users can only undo their own, undeleted (not already undone) events
if (other_event.user_id === event.user_id && !other_event.deleted) {
if (other_event.type === EVENT.STROKE) {
other_event.deleted = true;
const stats = stroke_stats(other_event.points, storage.cursor.width);
redraw_region(stats.bbox);
break;
} else if (other_event.type === EVENT.IMAGE) {
other_event.deleted = true;
const item = document.querySelector(`img[data-image-id="${other_event.image_id}"]`);
if (item) item.remove();
break;
} else if (other_event.type === EVENT.ERASER) {
other_event.deleted = true;
const erased = find_stroke_backwards(other_event.stroke_id);
if (erased) {
erased.deleted = false;
const stats = stroke_stats(erased.points, storage.cursor.width);
redraw_region(stats.bbox);
}
break;
} else if (other_event.type === EVENT.IMAGE_MOVE) {
const item = document.querySelector(`img[data-image-id="${other_event.image_id}"]`);
const ix = storage.images[other_event.image_id].x -= other_event.x;
const iy = storage.images[other_event.image_id].y -= other_event.y;
item.style.transform = `translate(${ix}px, ${iy}px)`;
break;
}
}
}
break;
}
case EVENT.IMAGE: {
const url = config.image_url + event.image_id;
const item = document.createElement('img');
item.classList.add('floating-image');
item.style['z-index'] = storage.events.length;
item.setAttribute('data-image-id', event.image_id);
item.setAttribute('src', url);
item.style.transform = `translate(${event.x}px, ${event.y}px)`;
elements.images.appendChild(item);
storage.images[event.image_id] = {
'x': event.x, 'y': event.y
};
// const r = await fetch(config.image_url + event.image_id);
// const blob = await r.blob();
// const bitmap = await createImageBitmap(blob);
// event.bitmap = bitmap;
// const bbox = bitmap_bbox(event);
// storage.ctx0.drawImage(bitmap, bbox.xmin, bbox.ymin);
break;
}
case EVENT.IMAGE_MOVE: {
// Already moved due to local prediction
if (event.user_id !== storage.me.id) {
const image_id = event.image_id;
const item = document.querySelector(`.floating-image[data-image-id="${image_id}"]`);
const ix = storage.images[event.image_id].x += event.x;
const iy = storage.images[event.image_id].y += event.y;
if (item) {
item.style.transform = `translate(${ix}px, ${iy}px)`;
}
}
break;
}
case EVENT.ERASER: {
if (event.deleted) {
break;
}
for (const other_event of storage.events) {
if (other_event.type === EVENT.STROKE && other_event.stroke_id === event.stroke_id) {
// Might already be deleted because of local prediction
if (!other_event.deleted) {
other_event.deleted = true;
const stats = stroke_stats(other_event.points, storage.cursor.width);
redraw_region(stats.bbox);
}
break;
}
}
break;
}
default: {
console.error('fuck');
}
}
}
async function handle_message(d) {
const message_type = des_u8(d);
if (config.debug_print) console.debug(message_type);
switch (message_type) {
case MESSAGE.JOIN:
case MESSAGE.INIT: {
elements.canvas0.classList.add('white');
storage.me.id = des_u32(d);
storage.server_lsn = des_u32(d);
if (storage.server_lsn > storage.lsn) {
// Server knows something that we don't
storage.lsn = storage.server_lsn;
}
if (message_type === MESSAGE.JOIN) {
ls.setItem('sessionId', des_u32(d));
if (config.debug_print) console.debug('join in');
} else {
if (config.debug_print) console.debug('init in');
}
const event_count = des_u32(d);
if (config.debug_print) console.debug(`${event_count} events in init`);
storage.ctx0.clearRect(0, 0, storage.ctx0.canvas.width, storage.ctx0.canvas.height);
elements.images.innerHTML = '';
storage.events.length = 0;
for (let i = 0; i < event_count; ++i) {
const event = des_event(d);
await handle_event(event);
storage.events.push(event);
}
elements.canvas0.classList.remove('white');
send_ack(event_count);
sync_queue();
break;
}
case MESSAGE.FIRE: {
const user_id = des_u32(d);
const predraw_event = des_event(d);
predraw_user(user_id, predraw_event);
break;
}
case MESSAGE.ACK: {
const lsn = des_u32(d);
if (config.debug_print) console.debug(`ack ${lsn} in`);
if (lsn > storage.server_lsn) {
// ACKs may arrive out of order
storage.server_lsn = lsn;
}
break;
}
case MESSAGE.SYN: {
const sn = des_u32(d);
const count = des_u32(d);
const we_expect = sn - storage.sn;
const first = count - we_expect;
if (config.debug_print) console.debug(`syn ${sn} in`);
for (let i = 0; i < count; ++i) {
const event = des_event(d);
if (i >= first) {
handle_event(event);
storage.events.push(event);
}
}
storage.sn = sn;
await send_ack(sn);
break;
}
default: {
console.error('fuck');
return;
}
}
}

110
client/webgl.html

@ -1,110 +0,0 @@ @@ -1,110 +0,0 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>Desk</title>
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
<link rel="shortcut icon" href="icons/favicon.svg" id="favicon">
<script type="text/javascript" src="math.js?v=4"></script>
<script type="text/javascript" src="aux.js?v=4"></script>
<script type="text/javascript" src="tools.js?v=4"></script>
<script type="text/javascript" src="webgl_geometry.js?v=4"></script>
<script type="text/javascript" src="webgl_shaders.js?v=4"></script>
<script type="text/javascript" src="webgl_listeners.js?v=5"></script>
<script type="text/javascript" src="webgl.js?v=4"></script>
<style>
html, body {
margin: 0;
padding: 0;
width: 100%;
height: 100%;
overflow: hidden;
}
canvas {
width: 100%;
height: 100%;
display: block;
cursor: crosshair;
}
canvas.movemode {
cursor: grab;
}
canvas.movemode.moving {
cursor: grabbing;
}
.tools-wrapper {
position: fixed;
bottom: 0;
width: 100%;
height: 32px;
display: flex;
justify-content: center;
align-items: end;
z-index: 10;
pointer-events: none;
}
.tools {
pointer-events: all;
display: flex;
align-items: center;
justify-content: center;
background: #333;
border-radius: 5px;
border-bottom-right-radius: 0;
border-bottom-left-radius: 0;
height: 42px;
padding-left: 10px;
padding-right: 10px;
}
.tool {
cursor: pointer;
padding-left: 10px;
padding-right: 10px;
height: 100%;
display: flex;
align-items: center;
background: #333;
transition: transform .1s ease-in-out;
user-select: none;
}
.tool:hover {
background: #888;
}
.tool.active {
transform: translateY(-10px);
border-top-right-radius: 5px;
border-top-left-radius: 5px;
background: #333;
}
.tool img {
height: 24px;
width: 24px;
filter: invert(100%);
}
</style>
</head>
<body>
<canvas id="c"></canvas>
<div class="tools-wrapper">
<div class="tools">
<div class="tool" data-tool="pencil"><img draggable="false" src="icons/draw.svg"></div>
<div class="tool" data-tool="ruler"><img draggable="false" src="icons/ruler.svg"></div>
<div class="tool" data-tool="eraser"><img draggable="false" src="icons/erase.svg"></div>
<div class="tool" data-tool="undo"><img draggable="false" src="icons/undo.svg"></div>
<!-- <div class="tool" data-tool="redo"><img draggable="false" src="icons/redo.svg"></div> -->
</div>
</div>
</body>
</html>

103
client/webgl.js

@ -58,7 +58,7 @@ function draw(state, context) { @@ -58,7 +58,7 @@ function draw(state, context) {
const total_pos_size = context.static_positions_f32.byteLength + context.dynamic_positions_f32.byteLength;
const total_color_size = context.static_colors_u8.byteLength + context.dynamic_colors_u8.byteLength;
const total_point_count = (context.static_positions.length + context.dynamic_positions.length) / 2;
const total_point_count = (context.static_positions.length + total_dynamic_positions(context)) / 2;
gl.bindBuffer(gl.ARRAY_BUFFER, buffers['b_pos']);
gl.vertexAttribPointer(locations['a_pos'], 2, gl.FLOAT, false, 0, 0);
@ -79,16 +79,76 @@ const config = { @@ -79,16 +79,76 @@ const config = {
ws_url: 'ws://192.168.100.2/ws/',
image_url: 'http://192.168.100.2/images/',
sync_timeout: 1000,
ws_reconnect_timeout: 2000,
ws_reconnect_timeout: 10000,
second_finger_timeout: 500,
buffer_first_touchmoves: 5,
debug_print: false,
debug_print: true,
min_zoom: 0.01,
max_zoom: 100.0,
};
const EVENT = Object.freeze({
PREDRAW: 10,
STROKE: 20,
RULER: 21, /* gets re-written with EVENT.STROKE before sending to server */
UNDO: 30,
REDO: 31,
IMAGE: 40,
IMAGE_MOVE: 41,
ERASER: 50,
});
const MESSAGE = Object.freeze({
INIT: 100,
SYN: 101,
ACK: 102,
FULL: 103,
FIRE: 104,
JOIN: 105,
});
function event_size(event) {
let size = 1 + 3; // type + padding
switch (event.type) {
case EVENT.PREDRAW: {
size += 4 * 2;
break;
}
case EVENT.STROKE: {
size += 4 + 2 + 2 + 4 + event.points.length * 4 * 2; // u32 stroke id + u16 (count) + u16 (width) + u32 (color + count * (f32, f32) points
break;
}
case EVENT.UNDO:
case EVENT.REDO: {
break;
}
case EVENT.IMAGE:
case EVENT.IMAGE_MOVE: {
size += 4 + 4 + 4; // file id + x + y
break;
}
case EVENT.ERASER: {
size += 4; // stroke id
break;
}
default: {
console.error('fuck');
}
}
return size;
}
function main() {
const state = {
'me': 333,
'canvas': {
'offset': { 'x': 0, 'y': 0 },
'zoom': 1.0,
@ -99,6 +159,13 @@ function main() { @@ -99,6 +159,13 @@ function main() {
'y': 0,
},
'sn': 0,
'lsn': 0,
'server_lsn': 0,
'color': 0,
'stroke_width': 8,
'touch': {
'moves': 0,
'drawing': false,
@ -116,18 +183,21 @@ function main() { @@ -116,18 +183,21 @@ function main() {
'stroke_width': 8,
'current_stroke': {
'color': 0,
'width': 8,
'points': [],
},
'current_strokes': {},
'strokes': [],
'queue': [],
'events': [],
'tools': {
'active': null,
'active_element': null,
},
'timers': {
'ws_reconnect': null,
},
'players': {},
};
const context = {
@ -137,12 +207,14 @@ function main() { @@ -137,12 +207,14 @@ function main() {
'buffers': {},
'locations': {},
'textures': {},
'static_positions': [],
'dynamic_positions': [],
'dynamic_positions': {},
'dynamic_colors': {},
'quad_positions': [],
'quad_texcoords': [],
'static_positions': [],
'static_colors': [],
'dynamic_colors': [],
'static_positions_f32': new Float32Array(0),
'dynamic_positions_f32': new Float32Array(0),
'static_colors_u8': new Uint8Array(0),
@ -152,9 +224,16 @@ function main() { @@ -152,9 +224,16 @@ function main() {
'bgcolor': {'r': 1.0, 'g': 1.0, 'b': 1.0},
};
const url = new URL(window.location.href);
const parts = url.pathname.split('/');
state.desk_id = parts.length > 0 ? parts[parts.length - 1] : 0;
init_webgl(state, context);
init_listeners(state, context);
init_tools(state, context);
ws_connect(state, context, true);
window.requestAnimationFrame(() => draw(state, context));
}

92
client/webgl_geometry.js

@ -95,39 +95,91 @@ function push_stroke(state, stroke, positions, colors) { @@ -95,39 +95,91 @@ function push_stroke(state, stroke, positions, colors) {
}
function pop_stroke(state, context) {
if (state.strokes.length > 0) {
const popped = state.strokes.pop();
console.error('undo')
// if (state.strokes.length > 0) {
// // TODO: this will not work once we have multiple players
// // because there can be others strokes after mine
// console.error('TODO: multiplayer undo');
// const popped = state.strokes.pop();
context.static_positions.length -= popped.popcount;
context.static_colors.length -= popped.popcount / 2 * 3;
// context.static_positions.length -= popped.popcount;
// context.static_colors.length -= popped.popcount / 2 * 3;
// context.static_positions_f32 = new Float32Array(context.static_positions);
// context.static_colors_u8 = new Uint8Array(context.static_colors);
// }
}
function add_static_stroke(state, context, stroke, relax = false) {
push_stroke(state, stroke, context.static_positions, context.static_colors);
if (!relax) {
context.static_positions_f32 = new Float32Array(context.static_positions);
context.static_colors_u8 = new Uint8Array(context.static_colors);
}
}
function add_static_stroke(state, context, stroke) {
state.strokes.push(stroke);
push_stroke(state, stroke, context.static_positions, context.static_colors);
function recompute_static_data(context) {
context.static_positions_f32 = new Float32Array(context.static_positions);
context.static_colors_u8 = new Uint8Array(context.static_colors);
}
function update_dynamic_stroke(state, context, point) {
state.current_stroke.points.push(point);
context.dynamic_positions.length = 0; // TODO: incremental
context.dynamic_colors.length = 0;
push_stroke(state, state.current_stroke, context.dynamic_positions, context.dynamic_colors);
context.dynamic_positions_f32 = new Float32Array(context.dynamic_positions);
context.dynamic_colors_u8 = new Uint8Array(context.dynamic_colors);
function total_dynamic_positions(context) {
let total_dynamic_length = 0;
for (const player_id in context.dynamic_positions) {
total_dynamic_length += context.dynamic_positions[player_id].length;
}
return total_dynamic_length;
}
function recompute_dynamic_data(state, context) {
const total_dynamic_length = total_dynamic_positions(context);
context.dynamic_positions_f32 = new Float32Array(total_dynamic_length);
context.dynamic_colors_u8 = new Uint8Array(total_dynamic_length / 2 * 3);
let at = 0;
for (const player_id in context.dynamic_positions) {
context.dynamic_positions_f32.set(context.dynamic_positions[player_id], at);
at += context.dynamic_positions[player_id].length;
}
// TODO: preview stroke colors
context.dynamic_colors_u8.fill(0);
}
function update_dynamic_stroke(state, context, player_id, point) {
if (!(player_id in state.current_strokes)) {
state.current_strokes[player_id] = {
'points': [],
'width': 8, // TODO
'color': 0, // TODO
};
context.dynamic_positions[player_id] = [];
context.dynamic_colors[player_id] = [];
}
// TODO: incremental
context.dynamic_positions[player_id].length = 0;
context.dynamic_colors[player_id].length = 0;
state.current_strokes[player_id].points.push(point);
push_stroke(state, state.current_strokes[player_id], context.dynamic_positions[player_id], context.dynamic_colors[player_id]);
recompute_dynamic_data(state, context);
}
function clear_dynamic_stroke(state, context) {
state.current_stroke.points.length = 0;
context.dynamic_positions.length = 0;
context.dynamic_colors.length = 0;
context.dynamic_positions_f32 = new Float32Array(0);
context.dynamic_colors_u8 = new Uint8Array(0);
function clear_dynamic_stroke(state, context, player_id) {
if (player_id in state.current_strokes) {
state.current_strokes[player_id].points.length = 0;
context.dynamic_positions[player_id].length = 0;
recompute_dynamic_data(state, context);
}
}
function add_image(context, bitmap, p) {

60
client/webgl_listeners.js

@ -1,7 +1,7 @@ @@ -1,7 +1,7 @@
function init_listeners(state, context) {
window.addEventListener('keydown', (e) => keydown(e, state, context));
window.addEventListener('keyup', (e) => keyup(e, state, context));
context.canvas.addEventListener('mousedown', (e) => mousedown(e, state, context));
context.canvas.addEventListener('mousemove', (e) => mousemove(e, state, context));
context.canvas.addEventListener('mouseup', (e) => mouseup(e, state, context));
@ -53,8 +53,8 @@ function mousedown(e, state, context) { @@ -53,8 +53,8 @@ function mousedown(e, state, context) {
const screenp = {'x': e.clientX, 'y': e.clientY};
const canvasp = screen_to_canvas(state, screenp);
clear_dynamic_stroke(state, context);
update_dynamic_stroke(state, context, canvasp);
clear_dynamic_stroke(state, context, state.me);
update_dynamic_stroke(state, context, state.me, canvasp);
state.drawing = true;
window.requestAnimationFrame(() => draw(state, context));
@ -73,11 +73,12 @@ function mousemove(e, state, context) { @@ -73,11 +73,12 @@ function mousemove(e, state, context) {
const screenp = {'x': e.clientX, 'y': e.clientY};
const canvasp = screen_to_canvas(state, screenp);
state.cursor = screenp;
if (state.drawing) {
update_dynamic_stroke(state, context, canvasp);
update_dynamic_stroke(state, context, state.me, canvasp);
fire_event(predraw_event(canvasp.x, canvasp.y));
do_draw = true;
}
@ -101,13 +102,16 @@ function mouseup(e, state, context) { @@ -101,13 +102,16 @@ function mouseup(e, state, context) {
if (state.drawing) {
const stroke = {
'color': Math.round(Math.random() * 4294967295),
'width': 8, //Math.round((Math.random() * 20) + 4),
'points': process_stroke(state.current_stroke.points)
'color': state.color,
'width': state.stroke_width,
'points': process_stroke(state.current_strokes[state.me].points),
'user_id': state.me,
};
add_static_stroke(state, context, stroke);
clear_dynamic_stroke(state, context);
queue_event(state, stroke_event(state));
clear_dynamic_stroke(state, context, state.me);
state.drawing = false;
window.requestAnimationFrame(() => draw(state, context));
@ -151,11 +155,11 @@ function touchstart(e, state) { @@ -151,11 +155,11 @@ function touchstart(e, state) {
// Ingore subsequent touches if we are already drawing
return;
}
// First finger(s) down?
if (state.touch.ids.length === 0) {
if (e.changedTouches.length === 1) {
// We give a bit of time to add a second finger
state.touch.waiting_for_second_finger = true;
state.touch.moves = 0;
@ -184,18 +188,22 @@ function touchstart(e, state) { @@ -184,18 +188,22 @@ function touchstart(e, state) {
state.touch.first_finger_position = screenp;
} else if (touch.identifier === state.touch.ids[1]) {
state.touch.second_finger_position = screenp;
}
}
}
}
return;
}
}
}
function touchmove(e, state, context) {
if (state.touch.ids.length === 1) {
const touch = find_touch(e.changedTouches, state.touch.ids[0]);
if (!touch) {
return;
}
const screenp = {'x': window.devicePixelRatio * touch.clientX, 'y': window.devicePixelRatio * touch.clientY};
const canvasp = screen_to_canvas(state, screenp);
@ -219,20 +227,18 @@ function touchmove(e, state, context) { @@ -219,20 +227,18 @@ function touchmove(e, state, context) {
} else {
// Handle buffered moves
if (state.touch.buffered.length > 0) {
clear_dynamic_stroke(state, context);
clear_dynamic_stroke(state, context, state.me);
for (const p of state.touch.buffered) {
update_dynamic_stroke(state, context, p);
// const predraw = predraw_event(p.x, p.y);
// fire_event(predraw);
update_dynamic_stroke(state, context, state.me, p);
fire_event(predraw_event(canvasp.x, canvasp.y));
}
state.touch.buffered.length = 0;
}
// const predraw = predraw_event(x, y);
// fire_event(predraw);
update_dynamic_stroke(state, context, canvasp);
update_dynamic_stroke(state, context, state.me, canvasp);
fire_event(predraw_event(canvasp.x, canvasp.y));
window.requestAnimationFrame(() => draw(state, context));
}
@ -255,14 +261,14 @@ function touchmove(e, state, context) { @@ -255,14 +261,14 @@ function touchmove(e, state, context) {
first_finger_position = screenp;
} else if (touch.identifier === state.touch.ids[1]) {
second_finger_position = screenp;
}
}
}
const old_finger_midpoint = mid_v2(state.touch.first_finger_position, state.touch.second_finger_position);
const new_finger_midpoint = mid_v2(first_finger_position, second_finger_position);
const new_finger_midpoint_canvas = mid_v2(
screen_to_canvas(state, first_finger_position),
screen_to_canvas(state, first_finger_position),
screen_to_canvas(state, second_finger_position)
);
@ -308,13 +314,15 @@ function touchend(e, state, context) { @@ -308,13 +314,15 @@ function touchend(e, state, context) {
// await queue_event(event);
const stroke = {
'color': Math.round(Math.random() * 4294967295),
'width': 8, // Math.round((Math.random() * 20) + 4),
'points': process_stroke(state.current_stroke.points)
'color': state.color,
'width': state.stroke_width,
'points': process_stroke(state.current_strokes[state.me].points),
'user_id': state.me,
};
add_static_stroke(state, context, stroke);
clear_dynamic_stroke(state, context);
queue_event(state, stroke_event(state));
clear_dynamic_stroke(state, context, state.me);
state.touch.drawing = false;
window.requestAnimationFrame(() => draw(state, context))

32
client/websocket.js

@ -6,24 +6,24 @@ @@ -6,24 +6,24 @@
//
// Details best described here: https://github.com/kee-org/KeeFox/issues/189
function ws_connect(first_connect = false) {
const session_id = ls.getItem('sessionId') || '0';
const desk_id = storage.desk_id;
function ws_connect(state, context, first_connect = false) {
const session_id = localStorage.getItem('sessionId') || '0';
const desk_id = state.desk_id;
ws = new WebSocket(`${config.ws_url}?deskId=${desk_id}&sessionId=${session_id}`);
ws.addEventListener('open', on_open);
ws.addEventListener('message', on_message);
ws.addEventListener('error', on_error);
ws.addEventListener('close', on_close);
ws.addEventListener('open', () => on_open(state));
ws.addEventListener('message', (e) => on_message(state, context, e));
ws.addEventListener('error', () => on_error(state, context));
ws.addEventListener('close', () => on_close(state, context));
}
function on_open() {
clearTimeout(storage.timers.ws_reconnect);
function on_open(state) {
clearTimeout(state.timers.ws_reconnect);
if (config.debug_print) console.debug('open')
}
async function on_message(event) {
async function on_message(state, context, event) {
const data = event.data;
let message_data = null;
@ -31,7 +31,7 @@ async function on_message(event) { @@ -31,7 +31,7 @@ async function on_message(event) {
message_data = await data.arrayBuffer();
const view = new DataView(message_data);
const d = deserializer_create(message_data, view);
await handle_message(d);
await handle_message(state, context, d);
} else {
/* For all my Safari < 14 bros out there */
const reader = new FileReader();
@ -39,19 +39,19 @@ async function on_message(event) { @@ -39,19 +39,19 @@ async function on_message(event) {
message_data = e.target.result;
const view = new DataView(message_data);
const d = deserializer_create(message_data, view);
await handle_message(d);
await handle_message(state, context, d);
};
reader.readAsArrayBuffer(data);
}
}
function on_close() {
function on_close(state, context) {
ws = null;
if (config.debug_print) console.debug('close');
storage.timers.ws_reconnect = setTimeout(ws_connect, config.ws_reconnect_timeout);
state.timers.ws_reconnect = setTimeout(() => ws_connect(state, context, false), config.ws_reconnect_timeout);
}
function on_error() {
ws.close();
function on_error(state, context) {
ws.close(state, context);
}

33
server/deserializer.js

@ -26,40 +26,53 @@ export function u32(d) { @@ -26,40 +26,53 @@ export function u32(d) {
return value;
}
function u16array(d, count) {
const array = new Uint16Array(d.view.buffer, d.offset, count);
d.offset += count * 2;
export function f32(d) {
const value = d.view.getFloat32(d.offset, true);
d.offset += 4;
return value;
}
function f32array(d, count) {
const array = new Float32Array(d.view.buffer, d.offset, count);
d.offset += count * 4;
return array;
}
export function align(d, to) {
while (d.offset % to !== 0) {
d.offset++;
}
}
export function event(d) {
const event = {};
event.type = u8(d);
u8(d); // padding
switch (event.type) {
case EVENT.PREDRAW: {
event.x = u16(d);
event.y = u16(d);
event.x = f32(d);
event.y = f32(d);
break;
}
case EVENT.STROKE: {
// point_count + width align to 4 bytes :D
const point_count = u16(d);
const width = u16(d);
const color = u32(d);
align(d, 4);
event.width = width;
event.color = color;
event.points = u16array(d, point_count * 2);
event.points = f32array(d, point_count * 2);
break;
}
case EVENT.IMAGE:
case EVENT.IMAGE_MOVE: {
event.image_id = u32(d);
event.x = u16(d);
event.y = u16(d);
event.x = f32(d);
event.y = f32(d);
break;
}
@ -74,7 +87,7 @@ export function event(d) { @@ -74,7 +87,7 @@ export function event(d) {
}
default: {
console.error('fuck');
console.error('fuck', event.type);
console.trace();
process.exit(1);
}

1
server/recv.js

@ -42,7 +42,6 @@ function handle_event(session, event) { @@ -42,7 +42,6 @@ function handle_event(session, event) {
}
async function recv_syn(d, session) {
const padding = des.u8(d);
const lsn = des.u32(d);
const count = des.u32(d);

4
server/send.js

@ -11,7 +11,7 @@ function event_size(event) { @@ -11,7 +11,7 @@ function event_size(event) {
switch (event.type) {
case EVENT.PREDRAW: {
size += 2 * 2;
size += 4 * 2;
break;
}
@ -23,7 +23,7 @@ function event_size(event) { @@ -23,7 +23,7 @@ function event_size(event) {
case EVENT.IMAGE:
case EVENT.IMAGE_MOVE: {
size += 4 + 2 + 2; // file id + x + y
size += 4 + 4 + 4; // file id + x + y
break;
}

15
server/serializer.js

@ -21,6 +21,11 @@ export function u16(s, value) { @@ -21,6 +21,11 @@ export function u16(s, value) {
s.offset += 2;
}
export function f32(s, value) {
s.view.setFloat32(s.offset, value, true);
s.offset += 4;
}
export function u32(s, value) {
s.view.setUint32(s.offset, value, true);
s.offset += 4;
@ -37,15 +42,15 @@ export function event(s, event) { @@ -37,15 +42,15 @@ export function event(s, event) {
switch (event.type) {
case EVENT.PREDRAW: {
u16(s, event.x);
u16(s, event.y);
f32(s, event.x);
f32(s, event.y);
break;
}
case EVENT.STROKE: {
const points_bytes = event.points;
u32(s, event.stroke_id);
u16(s, points_bytes.byteLength / 2 / 2); // each point is 2 u16s
u16(s, points_bytes.byteLength / 2 / 4); // each point is 2 * f32
u16(s, event.width);
u32(s, event.color);
bytes(s, points_bytes);
@ -55,8 +60,8 @@ export function event(s, event) { @@ -55,8 +60,8 @@ export function event(s, event) {
case EVENT.IMAGE:
case EVENT.IMAGE_MOVE: {
u32(s, event.image_id);
u16(s, event.x);
u16(s, event.y);
f32(s, event.x);
f32(s, event.y);
break;
}

Loading…
Cancel
Save