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_s32(d) { const value = d.view.getInt32(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_align(d, to) { // TODO: non-stupid version of this while (d.offset % to != 0) { d.offset++; } } function des_f32array(d, count) { const result = new Float32Array(d.buffer, d.offset, count); d.offset += 4 * count; return result; } function des_u8array(d, count) { const result = new Uint8Array(d.buffer, d.offset, count); d.offset += count; return result; } function des_event(d, state = null) { const event = {}; event.type = des_u32(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.USER_JOINED: case EVENT.LEAVE: case EVENT.CLEAR: { break; } case EVENT.MOVE_CURSOR: { event.x = des_f32(d); event.y = des_f32(d); break; } case EVENT.MOVE_CANVAS: { event.offset_x = des_s32(d); event.offset_y = des_s32(d); event.zoom_level = des_s32(d); break; } case EVENT.ZOOM_CANVAS: { event.zoom_level = des_s32(d); event.zoom_cx = des_f32(d); event.zoom_cy = 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); event.coords = des_f32array(d, point_count * 2); event.press = des_u8array(d, point_count); des_align(d, 4); // TODO: remove, this is duplicate data event.stroke_id = stroke_id; 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, color = config.default_color, width = config.default_width) { state.players[player_id] = { 'color': color, 'width': width, 'points': [], 'online': false, 'cursor': {'x': 0, 'y': 0}, }; } function handle_event(state, context, event, options = {}) { 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.USER_JOINED: { state.players[event.user_id].online = true; draw_html(state); break; } case EVENT.PREDRAW: { geometry_add_point(state, context, event.user_id, {'x': event.x, 'y': event.y, 'pressure': 128}, false); // TODO: add pressure to predraw events need_draw = true; break; } case EVENT.CLEAR: { geometry_clear_player(state, context, event.user_id); break; } case EVENT.LEAVE: { if (event.user_id in state.players) { state.players[event.user_id].online = false; draw_html(state); } break; } case EVENT.MOVE_CURSOR: { if (event.user_id in state.players) { state.players[event.user_id].cursor.x = event.x; state.players[event.user_id].cursor.y = event.y; state.players[event.user_id].online = true; } // Should we syncronize this to RAF? draw_html(state); break; } case EVENT.MOVE_CANVAS: { // Double-check just in case // Non-triple equals in on purpose if (event.user_id == state.following_player) { state.canvas.offset.x = event.offset_x; state.canvas.offset.y = event.offset_y; const zoom_level = event.zoom_level; const dz = (zoom_level > 0 ? config.zoom_delta : -config.zoom_delta); const zoom = Math.pow(1.0 + dz, Math.abs(zoom_level)) state.canvas.zoom_level = zoom_level; state.canvas.zoom = zoom; state.canvas.target_zoom = zoom; need_draw = true; } break; } case EVENT.ZOOM_CANVAS: { if (event.user_id == state.following_player) { const zoom_level = event.zoom_level; const zoom_center = {'x': event.zoom_cx, 'y': event.zoom_cy}; const dz = (zoom_level > 0 ? config.zoom_delta : -config.zoom_delta); const zoom = Math.pow(1.0 + dz, Math.abs(zoom_level)) state.canvas.zoom_level = zoom_level; state.canvas.target_zoom = zoom; state.canvas.zoom_screenp = canvas_to_screen(state, zoom_center); 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: { const point_count = event.coords.length / 2; if (point_count === 0) { break; } let last_stroke = null; for (let i = state.events.length - 1; i >= 0; --i) { if (state.events[i].type === EVENT.STROKE) { last_stroke = state.events[i]; break; } } const index_difference = state.events.length - (last_stroke === null ? 0 : last_stroke.index); wasm_ensure_by(state, index_difference, event.coords.length); const pressures = state.wasm.buffers['pressures']; const xs = state.wasm.buffers['xs']; const ys = state.wasm.buffers['ys']; event.coords_from = xs.tv.size; event.coords_to = xs.tv.size + point_count; for (let i = 0; i < index_difference - 1; ++i) { // Create empty records for all non-stroke events that happened since the last stroke tv_add(state.wasm.buffers['coords_from'].tv, xs.tv.size); state.wasm.buffers['coords_from'].used += 4; // 4 bytes, not 4 ints } // Create actual records for this stroke tv_add(state.wasm.buffers['coords_from'].tv, xs.tv.size + point_count); state.wasm.buffers['coords_from'].used += 4; // 4 bytes, not 4 ints for (let i = 0; i < event.coords.length; i += 2) { tv_add(xs.tv, event.coords[i + 0]); tv_add(ys.tv, event.coords[i + 1]); } state.wasm.buffers['xs'].used += point_count * 4; state.wasm.buffers['ys'].used += point_count * 4; tv_append(pressures.tv, event.press); state.wasm.buffers['pressures'].used += point_count; delete event.coords; delete event.press; // TODO: do not do this for my own strokes when we bake locally geometry_clear_player(state, context, event.user_id); need_draw = true; event.index = state.events.length; geometry_add_stroke(state, context, event, state.events.length, options.skip_bvh === true); state.stroke_count++; break; } case EVENT.UNDO: { geometry_add_dummy_stroke(context); 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; if (other_event.bvh_node) { bvh_delete_stroke(state, other_event); } need_draw = true; break; } else if (other_event.type === EVENT.UNDO) { // do not undo an undo, we are not Notepad } else { console.error('cant undo event type', other_event.type); break; } } } break; } case EVENT.IMAGE: { try { (async () => { const url = config.image_url + event.image_id; const r = await fetch(config.image_url + event.image_id); const blob = await r.blob(); const bitmap = await createImageBitmap(blob); const p = {'x': event.x, 'y': event.y}; event.width = bitmap.width; event.height = bitmap.height; geometry_add_dummy_stroke(context); add_image(context, event.image_id, bitmap, p); // God knows when this will actually complete (it loads the image from the server) // so do not set need_draw. Instead just schedule the draw ourselves when done schedule_draw(state, context); })(); } catch (e) { console.log('Could not load image bitmap:', e); } break; } case EVENT.IMAGE_MOVE: { // Already moved due to local prediction if (event.user_id !== state.me) { const image_id = event.image_id; const image_event = find_image(state, image_id); if (image_event) { // if (config.debug_print) console.debug('move image', image_id, 'to', image_event.x, image_event.y); image_event.x = event.x; image_event.y = event.y; move_image(context, image_event); need_draw = true; } } 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_u32(d); let do_draw = false; // if (config.debug_print) console.debug(message_type); switch (message_type) { case MESSAGE.JOIN: case MESSAGE.INIT: { console.time('init'); state.online = true; state.server_lsn = des_u32(d); if (state.server_lsn > state.lsn) { // Server knows something that we don't state.lsn = state.server_lsn; } let color = config.default_color; let width = config.default_width; if (message_type === MESSAGE.JOIN) { localStorage.setItem('sessionId', des_u32(d)); if (config.debug_print) console.debug('join in'); } else { color = des_u32(d); width = des_u16(d); if (config.debug_print) console.debug('init in'); } state.me = parseInt(localStorage.getItem('sessionId')); init_player_defaults(state, state.me); set_color_u32(state, color); document.querySelector('#stroke-width').value = width; fire_event(state, width_event(width)); const event_count = des_u32(d); const user_count = des_u32(d); const total_points = des_u32(d); wasm_ensure_by(state, event_count, round_to_pow2(total_points * 2, 4096)); 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); const user_online = des_u8(d); init_player_defaults(state, user_id, user_color, user_width); state.players[user_id].online = user_online === 1; } des_align(d, 4); for (let i = 0; i < event_count; ++i) { const event = des_event(d, state); handle_event(state, context, event, {'skip_bvh': true}); if (event.type !== EVENT.STROKE || event.coords_to - event.coords_from > 0) { state.events.push(event); } } state.sn = event_count; bvh_construct(state); document.getElementById('debug-render-from').max = state.stroke_count; document.getElementById('debug-render-to').max = state.stroke_count; do_draw = true; send_ack(event_count); sync_queue(state); console.timeEnd('init'); update_cursor(state); draw_html(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, state); 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); } }