function serializer_create(size) { const buffer = new ArrayBuffer(size); return { 'offset': 0, 'size': size, 'buffer': buffer, 'view': new DataView(buffer), 'strview': new Uint8Array(buffer), 'need_gpu_allocate': true, // need to call glBufferData to create a GPU buffer of size serializer.size 'gpu_upload_from': 0, // need to call glBufferSubData for bytes in [serializer.gpu_upload_from, serializer.offset) }; } function ser_ensure(s, size) { if (s.size < size) { const new_s = serializer_create(Math.ceil(size * 2)); new_s.strview.set(s.strview); new_s.offset = s.offset; return new_s; } return s; } function ser_ensure_by(s, by) { if (s.offset + by > s.size) { const new_s = serializer_create(Math.ceil((s.size + by) * 2)); new_s.strview.set(s.strview); new_s.offset = s.offset; return new_s; } return s; } function ser_clear(s) { s.offset = 0; s.gpu_upload_from = 0; } function ser_u8(s, value) { s.view.setUint8(s.offset, value); s.offset += 1; } function ser_u16(s, value) { s.view.setUint16(s.offset, value, true); 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_s32(s, value) { s.view.setInt32(s.offset, value, true); s.offset += 4; } function ser_align(s, to) { // TODO: non-stupid version of this while (s.offset % to != 0) { s.offset++; } } function ser_event(s, event) { ser_u32(s, event.type); switch (event.type) { case EVENT.PREDRAW: { ser_f32(s, event.x); ser_f32(s, event.y); break; } case EVENT.CLEAR: case EVENT.LIFT: { break; } case EVENT.MOVE_CURSOR: { ser_f32(s, event.x); ser_f32(s, event.y); break; } case EVENT.MOVE_CANVAS: { ser_u32(s, event.offset_x); ser_u32(s, event.offset_y); ser_s32(s, event.zoom_level); break; } case EVENT.ZOOM_CANVAS: { ser_s32(s, event.zoom_level); ser_f32(s, event.zoom_cx); ser_f32(s, event.zoom_cy); break; } case EVENT.SET_COLOR: { ser_u32(s, event.color); break; } case EVENT.SET_WIDTH: { ser_u16(s, event.width); break; } case EVENT.STROKE: { ser_u16(s, event.points.length); ser_u16(s, event.width); ser_u32(s, event.color); if (config.debug_print) console.debug('original', event.points); for (const point of event.points) { ser_f32(s, point.x); ser_f32(s, point.y); } for (const point of event.points) { ser_u8(s, point.pressure); } ser_align(s, 4); break; } case EVENT.IMAGE: case EVENT.IMAGE_MOVE: { const image_id = parseInt(event.image_id); ser_u32(s, image_id); ser_f32(s, event.x); ser_f32(s, event.y); ser_u32(s, event.width); ser_u32(s, event.height); break; } case EVENT.IMAGE_SCALE: { const image_id = parseInt(event.image_id); ser_u32(s, image_id); ser_u32(s, event.corner); // which corner was moved ser_f32(s, event.x); // where corner was moved to (canvas coordinates) ser_f32(s, event.y); break; } case EVENT.UNDO: case EVENT.REDO: { break; } case EVENT.ERASER: { ser_u32(s, event.stroke_id); break; } default: { console.error('fuck'); } } } async function send_ack(sn) { const s = serializer_create(4 + 4); ser_u32(s, MESSAGE.ACK); ser_u32(s, sn); if (config.debug_print) console.debug(`ack ${sn} out`); try { if (ws) await ws.send(s.buffer); } catch(e) { ws.close(); } } async function send_follow(player_id) { const s = serializer_create(4 + 4); player_id = player_id === null ? -1 : player_id; ser_u32(s, MESSAGE.FOLLOW); ser_u32(s, player_id); if (config.debug_print) console.debug(`follow ${player_id} out`); try { if (ws) await ws.send(s.buffer); } catch (e) { ws.close(); } } async function sync_queue(state) { if (ws === null) { if (config.debug_print) console.debug('socket has closed, stopping SYNs'); return; } let size = 4 + 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'); state.queue.length = 0; return; } for (let i = count - 1; i >= 0; --i) { const event = state.queue[state.queue.length - 1 - i]; size += event_size(event); } const s = serializer_create(size); ser_u32(s, MESSAGE.SYN); ser_u32(s, state.lsn); ser_u32(s, count); for (let i = count - 1; i >= 0; --i) { const event = state.queue[state.queue.length - 1 - i]; ser_event(s, event); } if (config.debug_print) console.debug(`syn ${state.lsn} out`); try { if (ws) await ws.send(s.buffer); } catch(e) { ws.close(); } setTimeout(() => sync_queue(state), config.sync_timeout); } function push_event(state, event) { state.lsn += 1; switch (event.type) { case EVENT.STROKE: { state.queue.push({ 'type': EVENT.STROKE, 'points': event.points, 'width': event.width, 'color': event.color, }); break; } case EVENT.RULER: { event.type = EVENT.STROKE; state.queue.push(event); break; } case EVENT.ERASER: case EVENT.IMAGE: case EVENT.IMAGE_MOVE: case EVENT.IMAGE_SCALE: case EVENT.UNDO: case EVENT.REDO: { state.queue.push(event); break; } default: { console.error('fuck'); } } } // Queue an event and initialize repated sends until ACKed function queue_event(state, event, skip = false) { if (!state.online) { return; } push_event(state, event); if (skip) { return; } if (state.timers.queue_sync) { clearTimeout(state.timers.queue_sync); } sync_queue(state); } // Fire and forget. Doesn't do anything if we are offline async function fire_event(state, event) { if (!state.online) { return; } const s = serializer_create(4 + event_size(event)); ser_u32(s, MESSAGE.FIRE); ser_event(s, event); try { if (ws) await ws.send(s.buffer); } catch(e) { ws.close(); } } function predraw_event(x, y) { return { 'type': EVENT.PREDRAW, 'x': x, 'y': y }; } function lift_event() { return { 'type': EVENT.LIFT, }; } function color_event(color_u32) { return { 'type': EVENT.SET_COLOR, 'color': color_u32, }; } function width_event(width) { return { 'type': EVENT.SET_WIDTH, 'width': width, }; } function image_event(image_id, x, y, width, height) { return { 'type': EVENT.IMAGE, 'image_id': image_id, 'x': x, 'y': y, 'width': width, 'height': height, }; } function image_move_event(image_id, x, y) { return { 'type': EVENT.IMAGE_MOVE, 'image_id': image_id, 'x': x, 'y': y, }; } function image_scale_event(image_id, corner, x, y) { return { 'type': EVENT.IMAGE_SCALE, 'image_id': image_id, 'corner': corner, 'x': x, 'y': y, }; } function stroke_event(state) { const stroke = geometry_prepare_stroke(state); return { 'type': EVENT.STROKE, 'points': stroke.points, 'width': stroke.width, 'color': stroke.color, }; } function clear_event(state) { return { 'type': EVENT.CLEAR }; } function movecursor_event(x, y) { return { 'type': EVENT.MOVE_CURSOR, 'x': x, 'y': y, }; } function movecanvas_event(state) { return { 'type': EVENT.MOVE_CANVAS, 'offset_x': state.canvas.offset.x, 'offset_y': state.canvas.offset.y, 'zoom_level': state.canvas.zoom_level, }; } function zoomcanvas_event(state, zoom_cx, zoom_cy) { return { 'type': EVENT.ZOOM_CANVAS, 'zoom_level': state.canvas.zoom_level, 'zoom_cx': zoom_cx, 'zoom_cy': zoom_cy, }; } function undo_event(state) { return { 'type': EVENT.UNDO, }; } function eraser_event(stroke_id) { return { 'type': EVENT.ERASER, 'stroke_id': stroke_id, } }