diff --git a/client/client_recv.js b/client/client_recv.js new file mode 100644 index 0000000..8a19afc --- /dev/null +++ b/client/client_recv.js @@ -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)); + } +} diff --git a/client/send.js b/client/client_send.js similarity index 66% rename from client/send.js rename to client/client_send.js index 85002b6..3d793bb 100644 --- a/client/send.js +++ b/client/client_send.js @@ -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) { 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) { 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) { } } -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() { 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) { case EVENT.RULER: { event.type = EVENT.STROKE; - storage.queue.push(event); + state.queue.push(event); break; } @@ -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) { } // 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) { } 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, + }; } \ No newline at end of file diff --git a/client/index.html b/client/index.html index 17f41ac..283ee99 100644 --- a/client/index.html +++ b/client/index.html @@ -5,23 +5,101 @@