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.SET_COLOR: { event.color = des_u32(d); break; } case EVENT.SET_WIDTH: { event.width = 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_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; } function init_player_defaults(state, player_id) { state.players[player_id] = { 'color': config.default_color, 'width': config.default_width, }; } function handle_event(state, context, event, relax = false) { if (config.debug_print) console.debug(`event type ${event.type} from user ${event.user_id}`); let need_draw = false; if (!(event.user_id in state.players)) { init_player_defaults(state, event.user_id); } switch (event.type) { case EVENT.PREDRAW: { update_dynamic_stroke(state, context, event.user_id, {'x': event.x, 'y': event.y}); need_draw = true; break; } case EVENT.SET_COLOR: { state.players[event.user_id].color = event.color; break; } case EVENT.SET_WIDTH: { state.players[event.user_id].width = event.width; break; } case EVENT.STROKE: { if (event.user_id != state.me) { clear_dynamic_stroke(state, context, event.user_id); need_draw = true; } add_static_stroke(state, context, event, relax); break; } case EVENT.UNDO: { need_draw = true; 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: { need_draw = true; 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: { need_draw = true; 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: { need_draw = true; 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'); } } return need_draw; } 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); state.online = true; init_player_defaults(state, state.me); 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); const user_count = des_u32(d); if (config.debug_print) console.debug(`${event_count} events in init`); state.events.length = 0; for (let i = 0; i < user_count; ++i) { const user_id = des_u32(d); const user_color = des_u32(d); const user_width = des_u16(d); state.players[user_id] = { 'color': user_color, 'width': user_width, }; } for (let i = 0; i < event_count; ++i) { const event = des_event(d); handle_event(state, context, event, true); state.events.push(event); } do_draw = true; recompute_static_data(context); send_ack(event_count); sync_queue(state); break; } case MESSAGE.FIRE: { const event = des_event(d); const need_draw = handle_event(state, context, event); do_draw = do_draw || need_draw; 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) { const need_draw = handle_event(state, context, event); do_draw = do_draw || need_draw; state.events.push(event); } } state.sn = sn; send_ack(sn); // await? break; } default: { console.error('fuck'); return; } } if (do_draw) { schedule_draw(state, context); } }