Compare commits
No commits in common. 'infinite' and 'master' have entirely different histories.
@ -1,100 +0,0 @@ |
|||||||
function ui_offline() { |
|
||||||
document.body.classList.add('offline'); |
|
||||||
document.querySelector('.offline-toast').classList.remove('hidden'); |
|
||||||
} |
|
||||||
|
|
||||||
function ui_online() { |
|
||||||
document.body.classList.remove('offline'); |
|
||||||
document.querySelector('.offline-toast').classList.add('hidden'); |
|
||||||
} |
|
||||||
|
|
||||||
async function insert_image(state, context, file) { |
|
||||||
const bitmap = await createImageBitmap(file); |
|
||||||
|
|
||||||
const p = { 'x': state.cursor.x, 'y': state.cursor.y }; |
|
||||||
const canvasp = screen_to_canvas(state, p); |
|
||||||
|
|
||||||
canvasp.x -= bitmap.width / 2; |
|
||||||
canvasp.y -= bitmap.height / 2; |
|
||||||
|
|
||||||
const form_data = new FormData(); |
|
||||||
form_data.append('file', file); |
|
||||||
|
|
||||||
const resp = await fetch(`/api/image?deskId=${state.desk_id}`, { |
|
||||||
method: 'post', |
|
||||||
body: form_data, |
|
||||||
}) |
|
||||||
|
|
||||||
if (resp.ok) { |
|
||||||
const image_id = await resp.text(); |
|
||||||
const event = image_event(image_id, canvasp.x, canvasp.y); |
|
||||||
await queue_event(state, event); |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
function event_size(event) { |
|
||||||
let size = 1 + 3; // type + padding
|
|
||||||
|
|
||||||
switch (event.type) { |
|
||||||
case EVENT.PREDRAW: { |
|
||||||
size += 4 * 2; |
|
||||||
break; |
|
||||||
} |
|
||||||
|
|
||||||
case EVENT.SET_COLOR: { |
|
||||||
size += 4; |
|
||||||
break; |
|
||||||
} |
|
||||||
|
|
||||||
case EVENT.SET_WIDTH: { |
|
||||||
size += 2; |
|
||||||
break; |
|
||||||
} |
|
||||||
|
|
||||||
case EVENT.STROKE: { |
|
||||||
size += 4 + 2 + 2 + 4 + event.points.length * 4 * 2; // u32 stroke id + u16 (count) + u16 (width) + u32 (color + count * (f32, f32) points
|
|
||||||
break; |
|
||||||
} |
|
||||||
|
|
||||||
case EVENT.UNDO: |
|
||||||
case EVENT.REDO: { |
|
||||||
break; |
|
||||||
} |
|
||||||
|
|
||||||
case EVENT.IMAGE: |
|
||||||
case EVENT.IMAGE_MOVE: { |
|
||||||
size += 4 + 4 + 4; // file id + x + y
|
|
||||||
break; |
|
||||||
} |
|
||||||
|
|
||||||
case EVENT.ERASER: { |
|
||||||
size += 4; // stroke id
|
|
||||||
break; |
|
||||||
} |
|
||||||
|
|
||||||
default: { |
|
||||||
console.error('fuck'); |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
return size; |
|
||||||
} |
|
||||||
|
|
||||||
function find_touch(touchlist, id) { |
|
||||||
for (const touch of touchlist) { |
|
||||||
if (touch.identifier === id) { |
|
||||||
return touch; |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
return null; |
|
||||||
} |
|
||||||
|
|
||||||
function find_image(state, image_id) { |
|
||||||
for (let i = state.events.length - 1; i >= 0; --i) { |
|
||||||
const event = state.events[i]; |
|
||||||
if (event.type === EVENT.IMAGE && !event.deleted && event.image_id === image_id) { |
|
||||||
return event; |
|
||||||
} |
|
||||||
} |
|
||||||
} |
|
@ -1,412 +0,0 @@ |
|||||||
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, 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.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); |
|
||||||
|
|
||||||
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: { |
|
||||||
state.server_lsn = des_u32(d); |
|
||||||
state.online = true; |
|
||||||
|
|
||||||
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); |
|
||||||
|
|
||||||
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); |
|
||||||
} |
|
||||||
} |
|
@ -0,0 +1,66 @@ |
|||||||
|
function draw_stroke(stroke) { |
||||||
|
const points = stroke.points; |
||||||
|
|
||||||
|
if (points.length === 0) { |
||||||
|
return; |
||||||
|
} |
||||||
|
|
||||||
|
// if (config.debug_print) console.debug(points)
|
||||||
|
|
||||||
|
storage.ctx0.beginPath(); |
||||||
|
storage.ctx0.moveTo(points[0].x, points[0].y); |
||||||
|
storage.ctx0.strokeStyle = color_from_u32(stroke.color); |
||||||
|
storage.ctx0.lineWidth = stroke.width; |
||||||
|
|
||||||
|
for (let i = 1; i < points.length; ++i) { |
||||||
|
const p = points[i]; |
||||||
|
storage.ctx0.lineTo(p.x, p.y); |
||||||
|
} |
||||||
|
|
||||||
|
storage.ctx0.stroke(); |
||||||
|
} |
||||||
|
|
||||||
|
function redraw_predraw() { |
||||||
|
storage.ctx1.clearRect(0, 0, storage.ctx1.canvas.width, storage.ctx1.canvas.height); |
||||||
|
} |
||||||
|
|
||||||
|
function predraw_user(user_id, event) { |
||||||
|
if (!(user_id in storage.predraw)) { |
||||||
|
storage.predraw[user_id] = []; |
||||||
|
} |
||||||
|
|
||||||
|
storage.ctx1.beginPath(); |
||||||
|
if (storage.predraw[user_id].length > 0) { |
||||||
|
const last = storage.predraw[user_id][storage.predraw[user_id].length - 1]; |
||||||
|
storage.ctx1.moveTo(last.x, last.y); |
||||||
|
storage.ctx1.lineTo(event.x, event.y); |
||||||
|
} else { |
||||||
|
storage.ctx1.moveTo(event.x, event.y); |
||||||
|
} |
||||||
|
storage.ctx1.stroke(); |
||||||
|
|
||||||
|
storage.predraw[user_id].push({ 'x': event.x, 'y': event.y }); |
||||||
|
} |
||||||
|
|
||||||
|
function redraw_region(bbox) { |
||||||
|
if (bbox.xmin === bbox.xmax || bbox.ymin === bbox.ymax) { |
||||||
|
return; |
||||||
|
} |
||||||
|
|
||||||
|
storage.ctx0.save(); |
||||||
|
storage.ctx0.clearRect(bbox.xmin, bbox.ymin, bbox.xmax - bbox.xmin, bbox.ymax - bbox.ymin); |
||||||
|
|
||||||
|
storage.ctx0.beginPath(); |
||||||
|
storage.ctx0.rect(bbox.xmin, bbox.ymin, bbox.xmax - bbox.xmin, bbox.ymax - bbox.ymin); |
||||||
|
storage.ctx0.clip(); |
||||||
|
|
||||||
|
for (const event of storage.events) { |
||||||
|
if (event.type === EVENT.STROKE && !event.deleted) { |
||||||
|
if (stroke_intersects_region(event.points, bbox)) { |
||||||
|
draw_stroke(event); |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
storage.ctx0.restore(); |
||||||
|
} |
After Width: | Height: | Size: 671 B |
After Width: | Height: | Size: 957 B |
Before Width: | Height: | Size: 500 B After Width: | Height: | Size: 2.0 KiB |
Before Width: | Height: | Size: 691 B After Width: | Height: | Size: 3.0 KiB |
Before Width: | Height: | Size: 643 B After Width: | Height: | Size: 1.7 KiB |
Before Width: | Height: | Size: 494 B After Width: | Height: | Size: 1.9 KiB |
Before Width: | Height: | Size: 457 B After Width: | Height: | Size: 1.9 KiB |
@ -0,0 +1,345 @@ |
|||||||
|
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_s16(d) { |
||||||
|
const value = d.view.getInt16(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_u16array(d, count) { |
||||||
|
const result = []; |
||||||
|
|
||||||
|
for (let i = 0; i < count; ++i) { |
||||||
|
const item = d.view.getUint16(d.offset, true); |
||||||
|
d.offset += 2; |
||||||
|
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_u16(d); |
||||||
|
event.y = 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_u16array(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_s16(d); // stored as u16, but actually is s16
|
||||||
|
event.y = des_s16(d); // stored as u16, but actually is s16
|
||||||
|
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 x = (event.x <= storage.canvas.width ? event.x : event.x - 65536); |
||||||
|
const y = (event.y <= storage.canvas.height ? event.y : event.y - 65536); |
||||||
|
|
||||||
|
const bbox = { |
||||||
|
'xmin': x, |
||||||
|
'xmax': x + event.bitmap.width, |
||||||
|
'ymin': y, |
||||||
|
'ymax': y + event.bitmap.height |
||||||
|
}; |
||||||
|
|
||||||
|
return bbox; |
||||||
|
} |
||||||
|
|
||||||
|
async function handle_event(event) { |
||||||
|
if (config.debug_print) console.debug(`event type ${event.type} from user ${event.user_id}`); |
||||||
|
|
||||||
|
// TODO(@speed): do not handle locally predicted events
|
||||||
|
|
||||||
|
switch (event.type) { |
||||||
|
case EVENT.STROKE: { |
||||||
|
if (event.user_id in storage.predraw || event.user_id === storage.me.id) { |
||||||
|
storage.predraw[event.user_id] = []; |
||||||
|
redraw_predraw(); |
||||||
|
} |
||||||
|
|
||||||
|
draw_stroke(event); |
||||||
|
|
||||||
|
break; |
||||||
|
} |
||||||
|
|
||||||
|
case EVENT.UNDO: { |
||||||
|
for (let i = storage.events.length - 1; i >=0; --i) { |
||||||
|
const other_event = storage.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, storage.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, storage.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 = storage.images[other_event.image_id].x -= other_event.x; |
||||||
|
const iy = storage.images[other_event.image_id].y -= other_event.y; |
||||||
|
|
||||||
|
item.style.transform = `translate(${ix}px, ${iy}px)`; |
||||||
|
|
||||||
|
break; |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
break; |
||||||
|
} |
||||||
|
|
||||||
|
case EVENT.IMAGE: { |
||||||
|
const url = config.image_url + event.image_id; |
||||||
|
const item = document.createElement('img'); |
||||||
|
|
||||||
|
item.classList.add('floating-image'); |
||||||
|
item.style['z-index'] = storage.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); |
||||||
|
storage.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);
|
||||||
|
// storage.ctx0.drawImage(bitmap, bbox.xmin, bbox.ymin);
|
||||||
|
|
||||||
|
break; |
||||||
|
} |
||||||
|
|
||||||
|
case EVENT.IMAGE_MOVE: { |
||||||
|
// Already moved due to local prediction
|
||||||
|
if (event.user_id !== storage.me.id) { |
||||||
|
const image_id = event.image_id; |
||||||
|
const item = document.querySelector(`.floating-image[data-image-id="${image_id}"]`); |
||||||
|
|
||||||
|
const ix = storage.images[event.image_id].x += event.x; |
||||||
|
const iy = storage.images[event.image_id].y += event.y; |
||||||
|
|
||||||
|
if (item) { |
||||||
|
item.style.transform = `translate(${ix}px, ${iy}px)`; |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
break; |
||||||
|
} |
||||||
|
|
||||||
|
case EVENT.ERASER: { |
||||||
|
if (event.deleted) { |
||||||
|
break; |
||||||
|
} |
||||||
|
|
||||||
|
for (const other_event of storage.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, storage.cursor.width); |
||||||
|
redraw_region(stats.bbox); |
||||||
|
} |
||||||
|
break; |
||||||
|
} |
||||||
|
} |
||||||
|
break; |
||||||
|
} |
||||||
|
|
||||||
|
default: { |
||||||
|
console.error('fuck'); |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
async function handle_message(d) { |
||||||
|
const message_type = des_u8(d); |
||||||
|
|
||||||
|
if (config.debug_print) console.debug(message_type); |
||||||
|
|
||||||
|
switch (message_type) { |
||||||
|
case MESSAGE.JOIN: |
||||||
|
case MESSAGE.INIT: { |
||||||
|
elements.canvas0.classList.add('white'); |
||||||
|
|
||||||
|
storage.me.id = des_u32(d); |
||||||
|
storage.server_lsn = des_u32(d); |
||||||
|
|
||||||
|
if (storage.server_lsn > storage.lsn) { |
||||||
|
// Server knows something that we don't
|
||||||
|
storage.lsn = storage.server_lsn; |
||||||
|
} |
||||||
|
|
||||||
|
if (message_type === MESSAGE.JOIN) { |
||||||
|
ls.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); |
||||||
|
|
||||||
|
if (config.debug_print) console.debug(`${event_count} events in init`); |
||||||
|
|
||||||
|
storage.ctx0.clearRect(0, 0, storage.ctx0.canvas.width, storage.ctx0.canvas.height); |
||||||
|
elements.images.innerHTML = ''; |
||||||
|
storage.events.length = 0; |
||||||
|
|
||||||
|
for (let i = 0; i < event_count; ++i) { |
||||||
|
const event = des_event(d); |
||||||
|
await handle_event(event); |
||||||
|
storage.events.push(event); |
||||||
|
} |
||||||
|
|
||||||
|
elements.canvas0.classList.remove('white'); |
||||||
|
|
||||||
|
send_ack(event_count); |
||||||
|
|
||||||
|
sync_queue(); |
||||||
|
|
||||||
|
break; |
||||||
|
} |
||||||
|
|
||||||
|
case MESSAGE.FIRE: { |
||||||
|
const user_id = des_u32(d); |
||||||
|
const predraw_event = des_event(d); |
||||||
|
|
||||||
|
predraw_user(user_id, predraw_event); |
||||||
|
|
||||||
|
break; |
||||||
|
} |
||||||
|
|
||||||
|
case MESSAGE.ACK: { |
||||||
|
const lsn = des_u32(d); |
||||||
|
|
||||||
|
if (config.debug_print) console.debug(`ack ${lsn} in`); |
||||||
|
|
||||||
|
if (lsn > storage.server_lsn) { |
||||||
|
// ACKs may arrive out of order
|
||||||
|
storage.server_lsn = lsn; |
||||||
|
} |
||||||
|
|
||||||
|
break; |
||||||
|
} |
||||||
|
|
||||||
|
case MESSAGE.SYN: { |
||||||
|
const sn = des_u32(d); |
||||||
|
const count = des_u32(d); |
||||||
|
|
||||||
|
const we_expect = sn - storage.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) { |
||||||
|
handle_event(event); |
||||||
|
storage.events.push(event); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
storage.sn = sn; |
||||||
|
|
||||||
|
await send_ack(sn); |
||||||
|
|
||||||
|
break; |
||||||
|
} |
||||||
|
|
||||||
|
default: { |
||||||
|
console.error('fuck'); |
||||||
|
return; |
||||||
|
} |
||||||
|
} |
||||||
|
} |
@ -1,86 +1,23 @@ |
|||||||
function switch_tool(state, item) { |
function tools_switch(tool) { |
||||||
const tool = item.getAttribute('data-tool'); |
if (storage.tools.active_element) { |
||||||
|
storage.tools.active_element.classList.remove('active'); |
||||||
if (tool === 'undo') { |
|
||||||
return; |
|
||||||
} |
|
||||||
|
|
||||||
if (state.tools.active_element) { |
|
||||||
state.tools.active_element.classList.remove('active'); |
|
||||||
} |
|
||||||
|
|
||||||
state.tools.active = tool; |
|
||||||
state.tools.active_element = item; |
|
||||||
state.tools.active_element.classList.add('active'); |
|
||||||
} |
|
||||||
|
|
||||||
function switch_color(state, item) { |
|
||||||
const color = item.getAttribute('data-color'); |
|
||||||
|
|
||||||
if (state.colors.active_element) { |
|
||||||
state.colors.active_element.classList.remove('active'); |
|
||||||
} |
|
||||||
|
|
||||||
if (state.me in state.players) { |
|
||||||
const color_u32 = color_to_u32(color); |
|
||||||
state.players[state.me].color = color_u32 |
|
||||||
fire_event(state, color_event(color_u32)); |
|
||||||
} |
|
||||||
|
|
||||||
state.colors.active_element = item; |
|
||||||
state.colors.active_element.classList.add('active'); |
|
||||||
} |
|
||||||
|
|
||||||
function show_stroke_preview(state, size) { |
|
||||||
const preview = document.querySelector('#stroke-preview'); |
|
||||||
|
|
||||||
preview.style.width = size * state.canvas.zoom + 'px'; |
|
||||||
preview.style.height = size * state.canvas.zoom + 'px'; |
|
||||||
preview.style.background = color_from_u32(state.players[state.me].color); |
|
||||||
|
|
||||||
preview.classList.remove('dhide'); |
|
||||||
} |
|
||||||
|
|
||||||
function hide_stroke_preview() { |
|
||||||
document.querySelector('#stroke-preview').classList.add('dhide'); |
|
||||||
} |
|
||||||
|
|
||||||
function switch_stroke_width(e, state) { |
|
||||||
if (!state.online) return; |
|
||||||
|
|
||||||
const value = e.target.value; |
|
||||||
|
|
||||||
state.players[state.me].width = value; |
|
||||||
show_stroke_preview(state, value); |
|
||||||
|
|
||||||
if (state.hide_preview) { |
|
||||||
clearTimeout(state.hide_preview); |
|
||||||
} |
|
||||||
|
|
||||||
state.hide_preview = setTimeout(hide_stroke_preview, config.brush_preview_timeout); |
|
||||||
} |
} |
||||||
|
|
||||||
function broadcast_stroke_width(e, state) { |
storage.tools.active = tool; |
||||||
const value = e.target.value; |
storage.tools.active_element = document.querySelector(`.tool[data-tool="${tool}"]`); |
||||||
fire_event(state, width_event(value)); |
storage.tools.active_element.classList.add('active'); |
||||||
} |
} |
||||||
|
|
||||||
function init_tools(state) { |
function tools_init() { |
||||||
const tools = document.querySelectorAll('.tools .tool'); |
const pencil = document.querySelector('.tool[data-tool="pencil"]'); |
||||||
const colors = document.querySelectorAll('.pallete .color'); |
const ruler = document.querySelector('.tool[data-tool="ruler"]'); |
||||||
|
const eraser = document.querySelector('.tool[data-tool="eraser"]'); |
||||||
tools.forEach((item) => { item.addEventListener('click', () => switch_tool(state, item)); }); |
const undo = document.querySelector('.tool[data-tool="undo"]'); |
||||||
colors.forEach((item) => { item.addEventListener('click', () => switch_color(state, item)); }); |
|
||||||
|
|
||||||
// TODO: from localstorage
|
|
||||||
switch_tool(state, document.querySelector('.tool[data-tool="pencil"]')); |
|
||||||
switch_color(state, document.querySelector('.color[data-color="000000"]')); |
|
||||||
|
|
||||||
const slider = document.querySelector('#stroke-width'); |
|
||||||
|
|
||||||
// slider.value = state.players[state.me].width;
|
pencil.addEventListener('click', () => tools_switch('pencil')); |
||||||
slider.addEventListener('input', (e) => switch_stroke_width(e, state)); |
ruler.addEventListener('click', () => tools_switch('ruler')); |
||||||
slider.addEventListener('change', (e) => broadcast_stroke_width(e, state)); |
eraser.addEventListener('click', () => tools_switch('eraser')); |
||||||
|
undo.addEventListener('click', queue_undo); |
||||||
|
|
||||||
document.querySelector('.phone-extra-controls').addEventListener('click', zenmode); |
tools_switch('pencil'); |
||||||
} |
} |
@ -1,92 +0,0 @@ |
|||||||
function schedule_draw(state, context) { |
|
||||||
if (!state.timers.raf) { |
|
||||||
window.requestAnimationFrame(() => draw(state, context)); |
|
||||||
state.timers.raf = true; |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
function draw(state, context) { |
|
||||||
state.timers.raf = false; |
|
||||||
|
|
||||||
const gl = context.gl; |
|
||||||
const width = window.innerWidth; |
|
||||||
const height = window.innerHeight; |
|
||||||
|
|
||||||
let locations; |
|
||||||
let buffers; |
|
||||||
|
|
||||||
gl.viewport(0, 0, context.canvas.width, context.canvas.height); |
|
||||||
gl.clearColor(context.bgcolor.r, context.bgcolor.g, context.bgcolor.b, 1); |
|
||||||
gl.clear(gl.COLOR_BUFFER_BIT); |
|
||||||
|
|
||||||
// Images
|
|
||||||
locations = context.locations['quad']; |
|
||||||
buffers = context.buffers['quad']; |
|
||||||
|
|
||||||
gl.useProgram(context.programs['quad']); |
|
||||||
|
|
||||||
gl.enableVertexAttribArray(locations['a_pos']); |
|
||||||
gl.enableVertexAttribArray(locations['a_texcoord']); |
|
||||||
|
|
||||||
gl.uniform2f(locations['u_res'], context.canvas.width, context.canvas.height); |
|
||||||
gl.uniform2f(locations['u_scale'], state.canvas.zoom, state.canvas.zoom); |
|
||||||
gl.uniform2f(locations['u_translation'], state.canvas.offset.x, state.canvas.offset.y); |
|
||||||
gl.uniform1i(locations['u_texture'], 0); |
|
||||||
|
|
||||||
gl.bindBuffer(gl.ARRAY_BUFFER, buffers['b_pos']); |
|
||||||
gl.vertexAttribPointer(locations['a_pos'], 2, gl.FLOAT, false, 0, 0); |
|
||||||
gl.bufferData(gl.ARRAY_BUFFER, context.quad_positions_f32, gl.STATIC_DRAW); |
|
||||||
|
|
||||||
gl.bindBuffer(gl.ARRAY_BUFFER, buffers['b_texcoord']); |
|
||||||
gl.vertexAttribPointer(locations['a_texcoord'], 2, gl.FLOAT, false, 0, 0); |
|
||||||
gl.bufferData(gl.ARRAY_BUFFER, context.quad_texcoords_f32, gl.STATIC_DRAW); |
|
||||||
|
|
||||||
const count = Object.keys(context.textures).length; |
|
||||||
let active_image_index = -1; |
|
||||||
|
|
||||||
gl.uniform1i(locations['u_outline'], 0); |
|
||||||
|
|
||||||
for (let key = 0; key < count; ++key) { |
|
||||||
if (context.textures[key].image_id === context.active_image) { |
|
||||||
active_image_index = key; |
|
||||||
continue; |
|
||||||
} |
|
||||||
|
|
||||||
gl.bindTexture(gl.TEXTURE_2D, context.textures[key].texture); |
|
||||||
gl.drawArrays(gl.TRIANGLES, key * 6, 6); |
|
||||||
} |
|
||||||
|
|
||||||
if (active_image_index !== -1) { |
|
||||||
gl.uniform1i(locations['u_outline'], 1); |
|
||||||
gl.bindTexture(gl.TEXTURE_2D, context.textures[active_image_index].texture); |
|
||||||
gl.drawArrays(gl.TRIANGLES, active_image_index * 6, 6); |
|
||||||
} |
|
||||||
|
|
||||||
// Strokes
|
|
||||||
locations = context.locations['stroke']; |
|
||||||
buffers = context.buffers['stroke']; |
|
||||||
|
|
||||||
gl.useProgram(context.programs['stroke']); |
|
||||||
|
|
||||||
gl.enableVertexAttribArray(locations['a_type']); |
|
||||||
gl.enableVertexAttribArray(locations['a_pos']); |
|
||||||
gl.enableVertexAttribArray(locations['a_texcoord']); |
|
||||||
gl.enableVertexAttribArray(locations['a_color']); |
|
||||||
|
|
||||||
gl.uniform2f(locations['u_res'], context.canvas.width, context.canvas.height); |
|
||||||
gl.uniform2f(locations['u_scale'], state.canvas.zoom, state.canvas.zoom); |
|
||||||
gl.uniform2f(locations['u_translation'], state.canvas.offset.x, state.canvas.offset.y); |
|
||||||
|
|
||||||
gl.bindBuffer(gl.ARRAY_BUFFER, buffers['b_packed']); |
|
||||||
|
|
||||||
gl.vertexAttribPointer(locations['a_pos'], 2, gl.FLOAT, false, config.bytes_per_point, 0); |
|
||||||
gl.vertexAttribPointer(locations['a_texcoord'], 2, gl.FLOAT, false, config.bytes_per_point, 8); |
|
||||||
gl.vertexAttribPointer(locations['a_color'], 3, gl.UNSIGNED_BYTE, true, config.bytes_per_point, 16); |
|
||||||
gl.vertexAttribPointer(locations['a_type'], 1, gl.UNSIGNED_BYTE, false, config.bytes_per_point, 19); |
|
||||||
|
|
||||||
gl.bufferData(gl.ARRAY_BUFFER, context.static_stroke_serializer.buffer, gl.STATIC_DRAW); |
|
||||||
gl.drawArrays(gl.TRIANGLES, 0, context.static_stroke_serializer.offset / config.bytes_per_point); |
|
||||||
|
|
||||||
gl.bufferData(gl.ARRAY_BUFFER, context.dynamic_stroke_serializer.buffer, gl.STATIC_DRAW); |
|
||||||
gl.drawArrays(gl.TRIANGLES, 0, context.dynamic_stroke_serializer.offset / config.bytes_per_point); |
|
||||||
} |
|
@ -1,244 +0,0 @@ |
|||||||
function push_point(s, x, y, u, v, r, g, b, type) { |
|
||||||
ser_f32(s, x); |
|
||||||
ser_f32(s, y); |
|
||||||
ser_f32(s, u); |
|
||||||
ser_f32(s, v); |
|
||||||
// ser_u8(s, Math.floor(Math.random() * 255));
|
|
||||||
// ser_u8(s, Math.floor(Math.random() * 255));
|
|
||||||
// ser_u8(s, Math.floor(Math.random() * 255));
|
|
||||||
ser_u8(s, r); |
|
||||||
ser_u8(s, g); |
|
||||||
ser_u8(s, b); |
|
||||||
ser_u8(s, type); |
|
||||||
} |
|
||||||
|
|
||||||
function push_circle(s, cx, cy, radius, r, g, b) { |
|
||||||
push_point(s, cx - radius, cy - radius, 0, 0, r, g, b, 1); |
|
||||||
push_point(s, cx - radius, cy + radius, 0, 1, r, g, b, 1); |
|
||||||
push_point(s, cx + radius, cy - radius, 1, 0, r, g, b, 1); |
|
||||||
|
|
||||||
push_point(s, cx + radius, cy + radius, 1, 1, r, g, b, 1); |
|
||||||
push_point(s, cx + radius, cy - radius, 1, 0, r, g, b, 1); |
|
||||||
push_point(s, cx - radius, cy + radius, 0, 1, r, g, b, 1); |
|
||||||
} |
|
||||||
|
|
||||||
function push_quad(s, p1x, p1y, p2x, p2y, p3x, p3y, p4x, p4y, r, g, b) { |
|
||||||
push_point(s, p1x, p1y, 0, 0, r, g, b, 0); |
|
||||||
push_point(s, p2x, p2y, 0, 1, r, g, b, 0); |
|
||||||
push_point(s, p3x, p3y, 1, 0, r, g, b, 0); |
|
||||||
|
|
||||||
push_point(s, p4x, p4y, 1, 1, r, g, b, 0); |
|
||||||
push_point(s, p3x, p3y, 1, 0, r, g, b, 0); |
|
||||||
push_point(s, p2x, p2y, 0, 1, r, g, b, 0); |
|
||||||
} |
|
||||||
|
|
||||||
function push_stroke(s, stroke) { |
|
||||||
const stroke_width = stroke.width; |
|
||||||
const points = stroke.points; |
|
||||||
const color_u32 = stroke.color; |
|
||||||
|
|
||||||
const r = (color_u32 >> 16) & 0xFF; |
|
||||||
const g = (color_u32 >> 8) & 0xFF; |
|
||||||
const b = color_u32 & 0xFF; |
|
||||||
|
|
||||||
if (points.length === 0) { |
|
||||||
return; |
|
||||||
} |
|
||||||
|
|
||||||
if (points.length === 1) { |
|
||||||
push_circle(s, points[0].x, points[0].y, stroke_width / 2, r, g, b); |
|
||||||
return; |
|
||||||
} |
|
||||||
|
|
||||||
for (let i = 0; i < points.length - 1; ++i) { |
|
||||||
const px = points[i].x; |
|
||||||
const py = points[i].y; |
|
||||||
|
|
||||||
const nextpx = points[i + 1].x; |
|
||||||
const nextpy = points[i + 1].y; |
|
||||||
|
|
||||||
const d1x = nextpx - px; |
|
||||||
const d1y = nextpy - py; |
|
||||||
|
|
||||||
// Perpendicular to (d1x, d1y), points to the LEFT
|
|
||||||
let perp1x = -d1y; |
|
||||||
let perp1y = d1x; |
|
||||||
|
|
||||||
const perpnorm1 = Math.sqrt(perp1x * perp1x + perp1y * perp1y); |
|
||||||
|
|
||||||
perp1x /= perpnorm1; |
|
||||||
perp1y /= perpnorm1; |
|
||||||
|
|
||||||
const s1x = px + perp1x * stroke_width / 2; |
|
||||||
const s1y = py + perp1y * stroke_width / 2; |
|
||||||
const s2x = px - perp1x * stroke_width / 2; |
|
||||||
const s2y = py - perp1y * stroke_width / 2; |
|
||||||
|
|
||||||
const s3x = nextpx + perp1x * stroke_width / 2; |
|
||||||
const s3y = nextpy + perp1y * stroke_width / 2; |
|
||||||
const s4x = nextpx - perp1x * stroke_width / 2; |
|
||||||
const s4y = nextpy - perp1y * stroke_width / 2; |
|
||||||
|
|
||||||
push_quad(s, s2x, s2y, s1x, s1y, s4x, s4y, s3x, s3y, r, g, b); |
|
||||||
push_circle(s, px, py, stroke_width / 2, r, g, b); |
|
||||||
} |
|
||||||
|
|
||||||
const lastp = points[points.length - 1]; |
|
||||||
|
|
||||||
push_circle(s, lastp.x, lastp.y, stroke_width / 2, r, g, b); |
|
||||||
} |
|
||||||
|
|
||||||
function geometry_prepare_stroke(state) { |
|
||||||
if (!state.online) { |
|
||||||
return null; |
|
||||||
} |
|
||||||
|
|
||||||
return { |
|
||||||
'color': state.players[state.me].color, |
|
||||||
'width': state.players[state.me].width, |
|
||||||
'points': process_stroke(state, state.players[state.me].points), |
|
||||||
'user_id': state.me, |
|
||||||
}; |
|
||||||
} |
|
||||||
|
|
||||||
function geometry_add_stroke(state, context, stroke) { |
|
||||||
if (!state.online || !stroke) return; |
|
||||||
|
|
||||||
const bytes_left = context.static_stroke_serializer.size - context.static_stroke_serializer.offset; |
|
||||||
const bytes_needed = (stroke.points.length * 12 + 6) * config.bytes_per_point; |
|
||||||
|
|
||||||
if (bytes_left < bytes_needed) { |
|
||||||
const old_view = context.static_stroke_serializer.strview; |
|
||||||
const old_offset = context.static_stroke_serializer.offset; |
|
||||||
|
|
||||||
const new_size = Math.ceil((context.static_stroke_serializer.size + bytes_needed) * 1.62); |
|
||||||
|
|
||||||
context.static_stroke_serializer = serializer_create(new_size); |
|
||||||
context.static_stroke_serializer.strview.set(old_view); |
|
||||||
context.static_stroke_serializer.offset = old_offset; |
|
||||||
} |
|
||||||
|
|
||||||
push_stroke(context.static_stroke_serializer, stroke); |
|
||||||
} |
|
||||||
|
|
||||||
function recompute_dynamic_data(state, context) { |
|
||||||
let bytes_needed = 0; |
|
||||||
|
|
||||||
for (const player_id in state.players) { |
|
||||||
const player = state.players[player_id]; |
|
||||||
if (player.points.length > 0) { |
|
||||||
bytes_needed += (player.points.length * 12 + 6) * config.bytes_per_point; |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
if (bytes_needed > context.dynamic_stroke_serializer.size) { |
|
||||||
context.dynamic_stroke_serializer = serializer_create(Math.ceil(bytes_needed * 1.62)); |
|
||||||
} else { |
|
||||||
context.dynamic_stroke_serializer.offset = 0; |
|
||||||
} |
|
||||||
|
|
||||||
for (const player_id in state.players) { |
|
||||||
// player has the same data as their current stroke: points, color, width
|
|
||||||
const player = state.players[player_id]; |
|
||||||
if (player.points.length > 0) { |
|
||||||
push_stroke(context.dynamic_stroke_serializer, player); |
|
||||||
} |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
function geometry_add_point(state, context, player_id, point) { |
|
||||||
if (!state.online) return; |
|
||||||
state.players[player_id].points.push(point); |
|
||||||
recompute_dynamic_data(state, context); |
|
||||||
} |
|
||||||
|
|
||||||
function geometry_clear_player(state, context, player_id) { |
|
||||||
if (!state.online) return; |
|
||||||
state.players[player_id].points.length = 0; |
|
||||||
recompute_dynamic_data(state, context); |
|
||||||
} |
|
||||||
|
|
||||||
function add_image(context, image_id, bitmap, p) { |
|
||||||
const x = p.x; |
|
||||||
const y = p.y; |
|
||||||
const gl = context.gl; |
|
||||||
const id = Object.keys(context.textures).length; |
|
||||||
|
|
||||||
context.textures[id] = { |
|
||||||
'texture': gl.createTexture(), |
|
||||||
'image_id': image_id |
|
||||||
}; |
|
||||||
|
|
||||||
gl.bindTexture(gl.TEXTURE_2D, context.textures[id].texture); |
|
||||||
gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA,gl.UNSIGNED_BYTE, bitmap); |
|
||||||
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR); |
|
||||||
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE); |
|
||||||
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE); |
|
||||||
|
|
||||||
context.quad_positions.push(...[ |
|
||||||
x, y, |
|
||||||
x, y + bitmap.height, |
|
||||||
x + bitmap.width, y + bitmap.height, |
|
||||||
|
|
||||||
x + bitmap.width, y, |
|
||||||
x, y, |
|
||||||
x + bitmap.width, y + bitmap.height, |
|
||||||
]); |
|
||||||
|
|
||||||
context.quad_texcoords.push(...[ |
|
||||||
0, 0, |
|
||||||
0, 1, |
|
||||||
1, 1, |
|
||||||
1, 0, |
|
||||||
0, 0, |
|
||||||
1, 1, |
|
||||||
]); |
|
||||||
|
|
||||||
context.quad_positions_f32 = new Float32Array(context.quad_positions); |
|
||||||
context.quad_texcoords_f32 = new Float32Array(context.quad_texcoords); |
|
||||||
} |
|
||||||
|
|
||||||
function move_image(context, image_event) { |
|
||||||
const x = image_event.x; |
|
||||||
const y = image_event.y; |
|
||||||
|
|
||||||
const count = Object.keys(context.textures).length; |
|
||||||
|
|
||||||
for (let id = 0; id < count; ++id) { |
|
||||||
const image = context.textures[id]; |
|
||||||
if (image.image_id === image_event.image_id) { |
|
||||||
context.quad_positions[id * 12 + 0] = x; |
|
||||||
context.quad_positions[id * 12 + 1] = y; |
|
||||||
context.quad_positions[id * 12 + 2] = x; |
|
||||||
context.quad_positions[id * 12 + 3] = y + image_event.height; |
|
||||||
context.quad_positions[id * 12 + 4] = x + image_event.width; |
|
||||||
context.quad_positions[id * 12 + 5] = y + image_event.height; |
|
||||||
|
|
||||||
context.quad_positions[id * 12 + 6] = x + image_event.width; |
|
||||||
context.quad_positions[id * 12 + 7] = y; |
|
||||||
context.quad_positions[id * 12 + 8] = x; |
|
||||||
context.quad_positions[id * 12 + 9] = y; |
|
||||||
context.quad_positions[id * 12 + 10] = x + image_event.width; |
|
||||||
context.quad_positions[id * 12 + 11] = y + image_event.height; |
|
||||||
|
|
||||||
context.quad_positions_f32 = new Float32Array(context.quad_positions); |
|
||||||
|
|
||||||
break; |
|
||||||
} |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
function image_at(state, x, y) { |
|
||||||
for (let i = state.events.length - 1; i >= 0; --i) { |
|
||||||
const event = state.events[i]; |
|
||||||
if (event.type === EVENT.IMAGE && !event.deleted) { |
|
||||||
if ('height' in event && 'width' in event) { |
|
||||||
if (event.x <= x && x <= event.x + event.width && event.y <= y && y <= event.y + event.height) { |
|
||||||
return event; |
|
||||||
} |
|
||||||
} |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
return null; |
|
||||||
} |
|
@ -1,410 +0,0 @@ |
|||||||
function init_listeners(state, context) { |
|
||||||
window.addEventListener('keydown', (e) => keydown(e, state, context)); |
|
||||||
window.addEventListener('keyup', (e) => keyup(e, state, context)); |
|
||||||
window.addEventListener('paste', (e) => paste(e, state, context)); |
|
||||||
|
|
||||||
context.canvas.addEventListener('mousedown', (e) => mousedown(e, state, context)); |
|
||||||
context.canvas.addEventListener('mousemove', (e) => mousemove(e, state, context)); |
|
||||||
context.canvas.addEventListener('mouseup', (e) => mouseup(e, state, context)); |
|
||||||
context.canvas.addEventListener('mouseleave', (e) => mouseup(e, state, context)); |
|
||||||
context.canvas.addEventListener('contextmenu', cancel); |
|
||||||
context.canvas.addEventListener('wheel', (e) => wheel(e, state, context)); |
|
||||||
|
|
||||||
context.canvas.addEventListener('touchstart', (e) => touchstart(e, state, context)); |
|
||||||
context.canvas.addEventListener('touchmove', (e) => touchmove(e, state, context)); |
|
||||||
context.canvas.addEventListener('touchend', (e) => touchend(e, state, context)); |
|
||||||
context.canvas.addEventListener('touchcancel', (e) => touchend(e, state, context)); |
|
||||||
|
|
||||||
context.canvas.addEventListener('drop', (e) => on_drop(e, state, context)); |
|
||||||
context.canvas.addEventListener('dragover', (e) => mousemove(e, state, context)); |
|
||||||
} |
|
||||||
|
|
||||||
function cancel(e) { |
|
||||||
e.preventDefault(); |
|
||||||
return false; |
|
||||||
} |
|
||||||
|
|
||||||
function zenmode() { |
|
||||||
document.querySelector('.pallete-wrapper').classList.toggle('hidden'); |
|
||||||
document.querySelector('.sizer-wrapper').classList.toggle('hidden'); |
|
||||||
} |
|
||||||
|
|
||||||
async function paste(e, state, context) { |
|
||||||
const items = (e.clipboardData || e.originalEvent.clipboardData).items; |
|
||||||
for (const item of items) { |
|
||||||
if (item.kind === 'file') { |
|
||||||
const file = item.getAsFile(); |
|
||||||
await insert_image(state, context, file); |
|
||||||
} |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
function keydown(e, state, context) { |
|
||||||
if (e.code === 'Space' && !state.drawing) { |
|
||||||
state.spacedown = true; |
|
||||||
context.canvas.classList.add('movemode'); |
|
||||||
} else if (e.code === 'Tab') { |
|
||||||
e.preventDefault(); |
|
||||||
zenmode(); |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
function keyup(e, state, context) { |
|
||||||
if (e.code === 'Space' && state.spacedown) { |
|
||||||
state.spacedown = false; |
|
||||||
state.moving = false; |
|
||||||
context.canvas.classList.remove('movemode'); |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
function mousedown(e, state, context) { |
|
||||||
const screenp = {'x': window.devicePixelRatio * e.clientX, 'y': window.devicePixelRatio * e.clientY}; |
|
||||||
const canvasp = screen_to_canvas(state, screenp); |
|
||||||
|
|
||||||
if (e.button === 2) { |
|
||||||
// Right click on image to select it
|
|
||||||
const image_event = image_at(state, canvasp.x, canvasp.y); |
|
||||||
|
|
||||||
if (image_event) { |
|
||||||
context.active_image = image_event.image_id; |
|
||||||
} else { |
|
||||||
context.active_image = null; |
|
||||||
} |
|
||||||
|
|
||||||
schedule_draw(state, context); |
|
||||||
|
|
||||||
return; |
|
||||||
} |
|
||||||
|
|
||||||
if (e.button !== 0) { |
|
||||||
return; |
|
||||||
} |
|
||||||
|
|
||||||
if (context.active_image) { |
|
||||||
// Move selected image with left click
|
|
||||||
const image_event = image_at(state, canvasp.x, canvasp.y); |
|
||||||
if (image_event && image_event.image_id === context.active_image) { |
|
||||||
state.moving_image = image_event; |
|
||||||
return; |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
if (state.spacedown) { |
|
||||||
state.moving = true; |
|
||||||
context.canvas.classList.add('moving'); |
|
||||||
return; |
|
||||||
} |
|
||||||
|
|
||||||
geometry_clear_player(state, context, state.me); |
|
||||||
geometry_add_point(state, context, state.me, canvasp); |
|
||||||
|
|
||||||
state.drawing = true; |
|
||||||
context.active_image = null; |
|
||||||
|
|
||||||
schedule_draw(state, context); |
|
||||||
} |
|
||||||
|
|
||||||
function mousemove(e, state, context) { |
|
||||||
e.preventDefault(); |
|
||||||
|
|
||||||
let do_draw = false; |
|
||||||
|
|
||||||
if (state.moving) { |
|
||||||
state.canvas.offset.x += e.movementX; |
|
||||||
state.canvas.offset.y += e.movementY; |
|
||||||
do_draw = true; |
|
||||||
} |
|
||||||
|
|
||||||
if (state.moving_image) { |
|
||||||
state.moving_image.x += e.movementX / state.canvas.zoom; |
|
||||||
state.moving_image.y += e.movementY / state.canvas.zoom; |
|
||||||
move_image(context, state.moving_image); |
|
||||||
do_draw = true; |
|
||||||
} |
|
||||||
|
|
||||||
const screenp = {'x': window.devicePixelRatio * e.clientX, 'y': window.devicePixelRatio * e.clientY}; |
|
||||||
const canvasp = screen_to_canvas(state, screenp); |
|
||||||
|
|
||||||
state.cursor = screenp; |
|
||||||
|
|
||||||
if (state.drawing) { |
|
||||||
geometry_add_point(state, context, state.me, canvasp); |
|
||||||
fire_event(state, predraw_event(canvasp.x, canvasp.y)); |
|
||||||
do_draw = true; |
|
||||||
} |
|
||||||
|
|
||||||
if (do_draw) { |
|
||||||
schedule_draw(state, context); |
|
||||||
} |
|
||||||
|
|
||||||
return false; |
|
||||||
} |
|
||||||
|
|
||||||
function mouseup(e, state, context) { |
|
||||||
if (e.button !== 0) { |
|
||||||
return; |
|
||||||
} |
|
||||||
|
|
||||||
if (state.moving_image) { |
|
||||||
schedule_draw(state, context); |
|
||||||
queue_event(state, image_move_event(context.active_image, state.moving_image.x, state.moving_image.y)); |
|
||||||
state.moving_image = null; |
|
||||||
return; |
|
||||||
} |
|
||||||
|
|
||||||
if (state.moving) { |
|
||||||
state.moving = false; |
|
||||||
context.canvas.classList.remove('moving'); |
|
||||||
return; |
|
||||||
} |
|
||||||
|
|
||||||
if (state.drawing) { |
|
||||||
const stroke = geometry_prepare_stroke(state); |
|
||||||
|
|
||||||
if (stroke) { |
|
||||||
geometry_add_stroke(state, context, stroke); |
|
||||||
queue_event(state, stroke_event(state)); |
|
||||||
geometry_clear_player(state, context, state.me); |
|
||||||
schedule_draw(state, context); |
|
||||||
} |
|
||||||
|
|
||||||
state.drawing = false; |
|
||||||
|
|
||||||
return; |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
function wheel(e, state, context) { |
|
||||||
const screenp = {'x': window.devicePixelRatio * e.clientX, 'y': window.devicePixelRatio * e.clientY}; |
|
||||||
const canvasp = screen_to_canvas(state, screenp); |
|
||||||
|
|
||||||
const dz = (e.deltaY < 0 ? 0.1 : -0.1); |
|
||||||
const old_zoom = state.canvas.zoom; |
|
||||||
|
|
||||||
state.canvas.zoom *= (1.0 + dz); |
|
||||||
|
|
||||||
if (state.canvas.zoom > config.max_zoom) { |
|
||||||
state.canvas.zoom = old_zoom; |
|
||||||
return; |
|
||||||
} |
|
||||||
|
|
||||||
if (state.canvas.zoom < config.min_zoom) { |
|
||||||
state.canvas.zoom = old_zoom; |
|
||||||
return; |
|
||||||
} |
|
||||||
|
|
||||||
const zoom_offset_x = Math.round((dz * old_zoom) * canvasp.x); |
|
||||||
const zoom_offset_y = Math.round((dz * old_zoom) * canvasp.y); |
|
||||||
|
|
||||||
state.canvas.offset.x -= zoom_offset_x; |
|
||||||
state.canvas.offset.y -= zoom_offset_y; |
|
||||||
|
|
||||||
schedule_draw(state, context); |
|
||||||
} |
|
||||||
|
|
||||||
function touchstart(e, state) { |
|
||||||
e.preventDefault(); |
|
||||||
|
|
||||||
if (state.touch.drawing) { |
|
||||||
// Ingore subsequent touches if we are already drawing
|
|
||||||
return; |
|
||||||
} |
|
||||||
|
|
||||||
// First finger(s) down?
|
|
||||||
if (state.touch.ids.length === 0) { |
|
||||||
if (e.changedTouches.length === 1) { |
|
||||||
|
|
||||||
// We give a bit of time to add a second finger
|
|
||||||
state.touch.waiting_for_second_finger = true; |
|
||||||
state.touch.moves = 0; |
|
||||||
state.touch.buffered.length = 0; |
|
||||||
state.touch.ids.push(e.changedTouches[0].identifier); |
|
||||||
|
|
||||||
setTimeout(() => { |
|
||||||
state.touch.waiting_for_second_finger = false; |
|
||||||
}, config.second_finger_timeout); |
|
||||||
} else { |
|
||||||
console.error('Two touchstarts at the same time are not yet supported'); |
|
||||||
} |
|
||||||
|
|
||||||
return; |
|
||||||
} |
|
||||||
|
|
||||||
// There are touches already
|
|
||||||
if (state.touch.waiting_for_second_finger) { |
|
||||||
if (e.changedTouches.length === 1) { |
|
||||||
state.touch.ids.push(e.changedTouches[0].identifier); |
|
||||||
|
|
||||||
for (const touch of e.touches) { |
|
||||||
const screenp = {'x': window.devicePixelRatio * touch.clientX, 'y': window.devicePixelRatio * touch.clientY}; |
|
||||||
|
|
||||||
if (touch.identifier === state.touch.ids[0]) { |
|
||||||
state.touch.first_finger_position = screenp; |
|
||||||
} else if (touch.identifier === state.touch.ids[1]) { |
|
||||||
state.touch.second_finger_position = screenp; |
|
||||||
} |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
return; |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
function touchmove(e, state, context) { |
|
||||||
if (state.touch.ids.length === 1) { |
|
||||||
const touch = find_touch(e.changedTouches, state.touch.ids[0]); |
|
||||||
|
|
||||||
if (!touch) { |
|
||||||
return; |
|
||||||
} |
|
||||||
|
|
||||||
const screenp = {'x': window.devicePixelRatio * touch.clientX, 'y': window.devicePixelRatio * touch.clientY}; |
|
||||||
const canvasp = screen_to_canvas(state, screenp); |
|
||||||
|
|
||||||
if (state.touch.moving) { |
|
||||||
// Can happen if we have been panning the canvas and lifted one finger,
|
|
||||||
// but not the second one
|
|
||||||
return; |
|
||||||
} |
|
||||||
|
|
||||||
if (!state.touch.drawing) { |
|
||||||
// Buffer this move
|
|
||||||
state.touch.moves += 1; |
|
||||||
|
|
||||||
if (state.touch.moves > config.buffer_first_touchmoves) { |
|
||||||
// Start drawing, no more waiting
|
|
||||||
state.touch.waiting_for_second_finger = false; |
|
||||||
state.touch.drawing = true; |
|
||||||
} else { |
|
||||||
state.touch.buffered.push(canvasp); |
|
||||||
} |
|
||||||
} else { |
|
||||||
// Handle buffered moves
|
|
||||||
if (state.touch.buffered.length > 0) { |
|
||||||
geometry_clear_player(state, context, state.me); |
|
||||||
|
|
||||||
for (const p of state.touch.buffered) { |
|
||||||
geometry_add_point(state, context, state.me, p); |
|
||||||
fire_event(state, predraw_event(p.x, p.y)); |
|
||||||
} |
|
||||||
|
|
||||||
state.touch.buffered.length = 0; |
|
||||||
} |
|
||||||
|
|
||||||
geometry_add_point(state, context, state.me, canvasp); |
|
||||||
fire_event(state, predraw_event(canvasp.x, canvasp.y)); |
|
||||||
|
|
||||||
schedule_draw(state, context); |
|
||||||
} |
|
||||||
|
|
||||||
return; |
|
||||||
} |
|
||||||
|
|
||||||
if (state.touch.ids.length === 2) { |
|
||||||
state.touch.moving = true; |
|
||||||
|
|
||||||
let first_finger_position = null; |
|
||||||
let second_finger_position = null; |
|
||||||
|
|
||||||
// A separate loop because touches might be in different order ? (question mark)
|
|
||||||
// IMPORTANT: e.touches, not e.changedTouches!
|
|
||||||
for (const touch of e.touches) { |
|
||||||
const screenp = {'x': window.devicePixelRatio * touch.clientX, 'y': window.devicePixelRatio * touch.clientY}; |
|
||||||
|
|
||||||
if (touch.identifier === state.touch.ids[0]) { |
|
||||||
first_finger_position = screenp; |
|
||||||
} else if (touch.identifier === state.touch.ids[1]) { |
|
||||||
second_finger_position = screenp; |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
const old_finger_midpoint = mid_v2(state.touch.first_finger_position, state.touch.second_finger_position); |
|
||||||
const new_finger_midpoint = mid_v2(first_finger_position, second_finger_position); |
|
||||||
|
|
||||||
const new_finger_midpoint_canvas = mid_v2( |
|
||||||
screen_to_canvas(state, first_finger_position), |
|
||||||
screen_to_canvas(state, second_finger_position) |
|
||||||
); |
|
||||||
|
|
||||||
const old_finger_distance = dist_v2(state.touch.first_finger_position, state.touch.second_finger_position); |
|
||||||
const new_finger_distance = dist_v2(first_finger_position, second_finger_position); |
|
||||||
|
|
||||||
const dx = new_finger_midpoint.x - old_finger_midpoint.x; |
|
||||||
const dy = new_finger_midpoint.y - old_finger_midpoint.y; |
|
||||||
|
|
||||||
const old_zoom = state.canvas.zoom; |
|
||||||
|
|
||||||
state.canvas.offset.x += dx; |
|
||||||
state.canvas.offset.y += dy; |
|
||||||
|
|
||||||
// console.log(new_finger_distance, state.touch.finger_distance);
|
|
||||||
|
|
||||||
const scale_by = new_finger_distance / old_finger_distance; |
|
||||||
const dz = state.canvas.zoom * (scale_by - 1.0); |
|
||||||
|
|
||||||
const zoom_offset_x = dz * new_finger_midpoint_canvas.x; |
|
||||||
const zoom_offset_y = dz * new_finger_midpoint_canvas.y; |
|
||||||
|
|
||||||
if (config.min_zoom <= state.canvas.zoom * scale_by && state.canvas.zoom * scale_by <= config.max_zoom) { |
|
||||||
state.canvas.zoom *= scale_by; |
|
||||||
state.canvas.offset.x -= zoom_offset_x; |
|
||||||
state.canvas.offset.y -= zoom_offset_y; |
|
||||||
} |
|
||||||
|
|
||||||
state.touch.first_finger_position = first_finger_position; |
|
||||||
state.touch.second_finger_position = second_finger_position; |
|
||||||
|
|
||||||
schedule_draw(state, context); |
|
||||||
|
|
||||||
return; |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
function touchend(e, state, context) { |
|
||||||
for (const touch of e.changedTouches) { |
|
||||||
if (state.touch.drawing) { |
|
||||||
if (state.touch.ids[0] == touch.identifier) { |
|
||||||
// const event = stroke_event();
|
|
||||||
// await queue_event(event);
|
|
||||||
|
|
||||||
const stroke = geometry_prepare_stroke(state); |
|
||||||
|
|
||||||
if (stroke) { |
|
||||||
geometry_add_stroke(state, context, stroke); |
|
||||||
queue_event(state, stroke_event(state)); |
|
||||||
geometry_clear_player(state, context, state.me); |
|
||||||
schedule_draw(state, context); |
|
||||||
} |
|
||||||
|
|
||||||
state.touch.drawing = false; |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
const index = state.touch.ids.indexOf(touch.identifier); |
|
||||||
|
|
||||||
if (index !== -1) { |
|
||||||
state.touch.ids.splice(index, 1); |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
if (state.touch.ids.length === 0) { |
|
||||||
// Only allow drawing again when ALL fingers have been lifted
|
|
||||||
state.touch.moving = false; |
|
||||||
waiting_for_second_finger = false; |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
async function on_drop(e, state, context) { |
|
||||||
e.preventDefault(); |
|
||||||
|
|
||||||
if (e.dataTransfer.files.length !== 1) { |
|
||||||
return; |
|
||||||
} |
|
||||||
|
|
||||||
const file = e.dataTransfer.files[0]; |
|
||||||
await insert_image(state, context, file); |
|
||||||
|
|
||||||
schedule_draw(state, context); |
|
||||||
|
|
||||||
return false; |
|
||||||
} |
|
@ -1,193 +0,0 @@ |
|||||||
const stroke_vs_src = ` |
|
||||||
attribute float a_type; |
|
||||||
attribute vec2 a_pos; |
|
||||||
attribute vec2 a_texcoord; |
|
||||||
attribute vec3 a_color; |
|
||||||
|
|
||||||
uniform vec2 u_scale; |
|
||||||
uniform vec2 u_res; |
|
||||||
uniform vec2 u_translation; |
|
||||||
|
|
||||||
varying vec3 v_color; |
|
||||||
varying vec2 v_texcoord; |
|
||||||
varying float v_type; |
|
||||||
|
|
||||||
void main() { |
|
||||||
vec2 screen01 = (a_pos * u_scale + u_translation) / u_res; |
|
||||||
vec2 screen02 = screen01 * 2.0; |
|
||||||
|
|
||||||
screen02.y = 2.0 - screen02.y; |
|
||||||
|
|
||||||
v_color = a_color; |
|
||||||
v_texcoord = a_texcoord; |
|
||||||
v_type = a_type; |
|
||||||
|
|
||||||
gl_Position = vec4(screen02 - 1.0, 0, 1); |
|
||||||
} |
|
||||||
`;
|
|
||||||
|
|
||||||
const stroke_fs_src = ` |
|
||||||
#extension GL_OES_standard_derivatives : enable |
|
||||||
|
|
||||||
precision mediump float; |
|
||||||
|
|
||||||
varying vec3 v_color; |
|
||||||
varying vec2 v_texcoord; |
|
||||||
varying float v_type; |
|
||||||
|
|
||||||
void main() { |
|
||||||
vec2 uv = v_texcoord * 2.0 - 1.0; |
|
||||||
float sdf = 1.0 - mix(abs(uv.y), length(uv), v_type); |
|
||||||
float pd = fwidth(sdf); |
|
||||||
float alpha = 1.0 - smoothstep(pd, 0.0, sdf); |
|
||||||
|
|
||||||
gl_FragColor = vec4(v_color * alpha, alpha); |
|
||||||
} |
|
||||||
`;
|
|
||||||
|
|
||||||
const tquad_vs_src = ` |
|
||||||
attribute vec2 a_pos; |
|
||||||
attribute vec2 a_texcoord; |
|
||||||
|
|
||||||
uniform vec2 u_scale; |
|
||||||
uniform vec2 u_res; |
|
||||||
uniform vec2 u_translation; |
|
||||||
|
|
||||||
varying vec2 v_texcoord; |
|
||||||
|
|
||||||
void main() { |
|
||||||
vec2 screen01 = (a_pos * u_scale + u_translation) / u_res; |
|
||||||
vec2 screen02 = screen01 * 2.0; |
|
||||||
screen02.y = 2.0 - screen02.y; |
|
||||||
vec2 screen11 = screen02 - 1.0; |
|
||||||
v_texcoord = a_texcoord; |
|
||||||
gl_Position = vec4(screen11, 0, 1); |
|
||||||
} |
|
||||||
`;
|
|
||||||
|
|
||||||
const tquad_fs_src = ` |
|
||||||
precision mediump float; |
|
||||||
|
|
||||||
varying vec2 v_texcoord; |
|
||||||
|
|
||||||
uniform sampler2D u_texture; |
|
||||||
uniform bool u_outline; |
|
||||||
|
|
||||||
void main() { |
|
||||||
if (!u_outline) { |
|
||||||
gl_FragColor = texture2D(u_texture, v_texcoord); |
|
||||||
} else { |
|
||||||
gl_FragColor = mix(texture2D(u_texture, v_texcoord), vec4(0.7, 0.7, 0.95, 1), 0.5); |
|
||||||
} |
|
||||||
} |
|
||||||
`;
|
|
||||||
|
|
||||||
function init_webgl(state, context) { |
|
||||||
context.canvas = document.querySelector('#c'); |
|
||||||
context.gl = context.canvas.getContext('webgl', { |
|
||||||
'preserveDrawingBuffer': true, |
|
||||||
'desynchronized': true, |
|
||||||
'antialias': false, |
|
||||||
}); |
|
||||||
|
|
||||||
const gl = context.gl; |
|
||||||
|
|
||||||
gl.enable(gl.BLEND); |
|
||||||
gl.blendFunc(gl.ONE, gl.ONE_MINUS_SRC_ALPHA); |
|
||||||
gl.getExtension('OES_standard_derivatives'); |
|
||||||
|
|
||||||
const stroke_vs = create_shader(gl, gl.VERTEX_SHADER, stroke_vs_src); |
|
||||||
const stroke_fs = create_shader(gl, gl.FRAGMENT_SHADER, stroke_fs_src); |
|
||||||
|
|
||||||
const quad_vs = create_shader(gl, gl.VERTEX_SHADER, tquad_vs_src); |
|
||||||
const quad_fs = create_shader(gl, gl.FRAGMENT_SHADER, tquad_fs_src); |
|
||||||
|
|
||||||
context.programs['stroke'] = create_program(gl, stroke_vs, stroke_fs); |
|
||||||
context.programs['quad'] = create_program(gl, quad_vs, quad_fs); |
|
||||||
|
|
||||||
context.locations['stroke'] = { |
|
||||||
'a_type': gl.getAttribLocation(context.programs['stroke'], 'a_type'), |
|
||||||
'a_pos': gl.getAttribLocation(context.programs['stroke'], 'a_pos'), |
|
||||||
'a_texcoord': gl.getAttribLocation(context.programs['stroke'], 'a_texcoord'), |
|
||||||
'a_color': gl.getAttribLocation(context.programs['stroke'], 'a_color'), |
|
||||||
|
|
||||||
'u_res': gl.getUniformLocation(context.programs['stroke'], 'u_res'), |
|
||||||
'u_scale': gl.getUniformLocation(context.programs['stroke'], 'u_scale'), |
|
||||||
'u_translation': gl.getUniformLocation(context.programs['stroke'], 'u_translation'), |
|
||||||
}; |
|
||||||
|
|
||||||
context.locations['quad'] = { |
|
||||||
'a_pos': gl.getAttribLocation(context.programs['quad'], 'a_pos'), |
|
||||||
'a_texcoord': gl.getAttribLocation(context.programs['quad'], 'a_texcoord'), |
|
||||||
'u_res': gl.getUniformLocation(context.programs['quad'], 'u_res'), |
|
||||||
'u_scale': gl.getUniformLocation(context.programs['quad'], 'u_scale'), |
|
||||||
'u_translation': gl.getUniformLocation(context.programs['quad'], 'u_translation'), |
|
||||||
'u_outline': gl.getUniformLocation(context.programs['quad'], 'u_outline'), |
|
||||||
'u_texture': gl.getUniformLocation(context.programs['quad'], 'u_texture'), |
|
||||||
}; |
|
||||||
|
|
||||||
context.buffers['stroke'] = { |
|
||||||
'b_packed': context.gl.createBuffer(), |
|
||||||
}; |
|
||||||
|
|
||||||
context.buffers['quad'] = { |
|
||||||
'b_pos': context.gl.createBuffer(), |
|
||||||
'b_texcoord': context.gl.createBuffer(), |
|
||||||
}; |
|
||||||
|
|
||||||
const resize_canvas = (entries) => { |
|
||||||
// https://www.khronos.org/webgl/wiki/HandlingHighDPI
|
|
||||||
const entry = entries[0]; |
|
||||||
|
|
||||||
let width; |
|
||||||
let height; |
|
||||||
|
|
||||||
if (entry.devicePixelContentBoxSize) { |
|
||||||
width = entry.devicePixelContentBoxSize[0].inlineSize; |
|
||||||
height = entry.devicePixelContentBoxSize[0].blockSize; |
|
||||||
} else if (entry.contentBoxSize) { |
|
||||||
// fallback for Safari that will not always be correct
|
|
||||||
width = Math.round(entry.contentBoxSize[0].inlineSize * devicePixelRatio); |
|
||||||
height = Math.round(entry.contentBoxSize[0].blockSize * devicePixelRatio); |
|
||||||
} |
|
||||||
|
|
||||||
context.canvas.width = width; |
|
||||||
context.canvas.height = height; |
|
||||||
|
|
||||||
schedule_draw(state, context); |
|
||||||
} |
|
||||||
|
|
||||||
const resize_observer = new ResizeObserver(resize_canvas); |
|
||||||
resize_observer.observe(context.canvas); |
|
||||||
} |
|
||||||
|
|
||||||
function create_shader(gl, type, source) { |
|
||||||
const shader = gl.createShader(type); |
|
||||||
|
|
||||||
gl.shaderSource(shader, source); |
|
||||||
gl.compileShader(shader); |
|
||||||
|
|
||||||
if (gl.getShaderParameter(shader, gl.COMPILE_STATUS)) { |
|
||||||
return shader; |
|
||||||
} |
|
||||||
|
|
||||||
console.error(type, ':', gl.getShaderInfoLog(shader)); |
|
||||||
|
|
||||||
gl.deleteShader(shader); |
|
||||||
} |
|
||||||
|
|
||||||
function create_program(gl, vs, fs) { |
|
||||||
const program = gl.createProgram(); |
|
||||||
|
|
||||||
gl.attachShader(program, vs); |
|
||||||
gl.attachShader(program, fs); |
|
||||||
gl.linkProgram(program); |
|
||||||
|
|
||||||
if (gl.getProgramParameter(program, gl.LINK_STATUS)) { |
|
||||||
return program; |
|
||||||
} |
|
||||||
|
|
||||||
console.error('link:', gl.getProgramInfoLog(program)); |
|
||||||
|
|
||||||
gl.deleteProgram(program); |
|
||||||
} |
|
@ -1,21 +0,0 @@ |
|||||||
This is pdfTeX, Version 3.141592653-2.6-1.40.24 (TeX Live 2022/Debian) (preloaded format=pdflatex 2023.4.13) 16 APR 2023 21:20 |
|
||||||
entering extended mode |
|
||||||
restricted \write18 enabled. |
|
||||||
%&-line parsing enabled. |
|
||||||
** |
|
||||||
|
|
||||||
! Emergency stop. |
|
||||||
<*> |
|
||||||
|
|
||||||
End of file on the terminal! |
|
||||||
|
|
||||||
|
|
||||||
Here is how much of TeX's memory you used: |
|
||||||
3 strings out of 476091 |
|
||||||
111 string characters out of 5794081 |
|
||||||
1849330 words of memory out of 5000000 |
|
||||||
20488 multiletter control sequences out of 15000+600000 |
|
||||||
512287 words of font info for 32 fonts, out of 8000000 for 9000 |
|
||||||
1141 hyphenation exceptions out of 8191 |
|
||||||
0i,0n,0p,1b,6s stack positions out of 10000i,1000n,20000p,200000b,200000s |
|
||||||
! ==> Fatal error occurred, no output PDF file produced! |
|