You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 

475 lines
14 KiB

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_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_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.LEAVE:
case EVENT.CLEAR: {
break;
}
case EVENT.MOVE_CURSOR: {
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);
state.coordinates = tv_ensure_by(state.coordinates, coords.length);
event.coords_from = state.coordinates.size;
event.coords_to = state.coordinates.size + point_count * 2;
tv_append(state.coordinates, coords);
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.PREDRAW: {
geometry_add_point(state, context, event.user_id, {'x': event.x, 'y': event.y});
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.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: {
// 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: {
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: {
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;
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);
state.me = parseInt(localStorage.getItem('sessionId'));
if (config.debug_print) console.debug('init in');
}
init_player_defaults(state, state.me);
const color_code = color_from_u32(color).substring(1);
switch_color(state, document.querySelector(`.color[data-color="${color_code}"]`));
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);
state.coordinates = tv_create(Float32Array, 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);
init_player_defaults(state, user_id, user_color, user_width);
}
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');
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);
}
}