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.
427 lines
12 KiB
427 lines
12 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_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.CLEAR: { |
|
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, color = config.default_color, width = config.default_width) { |
|
state.players[player_id] = { |
|
'color': color, |
|
'width': width, |
|
'points': [], |
|
}; |
|
} |
|
|
|
function handle_event(state, context, event) { |
|
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.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) { |
|
geometry_clear_player(state, context, event.user_id); |
|
need_draw = true; |
|
} |
|
|
|
geometry_add_stroke(state, context, event, state.events.length); |
|
|
|
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_u8(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); |
|
|
|
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); |
|
} |
|
|
|
for (let i = 0; i < event_count; ++i) { |
|
const event = des_event(d); |
|
handle_event(state, context, event); |
|
state.events.push(event); |
|
} |
|
|
|
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); |
|
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); |
|
} |
|
}
|
|
|