let ws = null; let ls = window.localStorage; document.addEventListener('DOMContentLoaded', main); const EVENT = Object.freeze({ PREDRAW: 10, STROKE: 20, RULER: 21, /* gets re-written with EVENT.STROKE before sending to server */ UNDO: 30, REDO: 31, IMAGE: 40, IMAGE_MOVE: 41, ERASER: 50, }); const MESSAGE = Object.freeze({ INIT: 100, SYN: 101, ACK: 102, FULL: 103, FIRE: 104, JOIN: 105, }); const config = { ws_url: 'ws://192.168.100.2/ws/', image_url: 'http://192.168.100.2/images/', sync_timeout: 1000, ws_reconnect_timeout: 2000, second_finger_timeout: 500, buffer_first_touchmoves: 5, debug_print: false, }; const storage = { 'state': { 'drawing': false, 'moving': false, 'moving_image': false, 'mousedown': false, 'spacedown': false, }, 'moving_image_original_x': null, 'moving_image_original_y': null, 'touch': { 'moves': 0, 'drawing': false, 'moving': false, 'waiting_for_second_finger': false, 'position': { 'x': null, 'y': null }, 'screen_position': { 'x': null, 'y': null }, 'finger_distance': null, 'buffered': [], 'ids': [], }, 'tools': { 'active': null, 'active_element': null, }, 'ruler_origin': {}, 'erased': [], 'predraw': {}, 'timers': {}, 'me': {}, 'sn': 0, // what WE think SERVER SN is (we tell this to the server, it uses to decide how much stuff to SYN to us) 'server_lsn': 0, // what SERVER said LSN is (used to decide how much stuff to SYN) 'lsn': 0, // what actual LSN is (can't just use length of local queue because it gets cleared) 'queue': [], // to server 'events': [], // from server 'current_stroke': [], 'desk_id': 123, 'max_zoom': 4, 'min_zoom': 0.2, 'images': {}, 'canvas': { 'zoom': 1, 'width': 1500, 'height': 4000, 'offset_x': 0, 'offset_y': 0, }, 'cursor': { 'width': 8, 'color': 'rgb(0, 0, 0)', 'x': 0, 'y': 0, } }; const elements = { 'cursor': null, 'canvas0': null, 'canvas1': null, 'active_image': null, }; function event_size(event) { let size = 1 + 1; // type + padding switch (event.type) { case EVENT.PREDRAW: { size += 2 * 2; break; } case EVENT.STROKE: { size += 4 + 2 + 2 + 4 + event.points.length * 2 * 2; // u32 stroke id + u16 (count) + u16 (width) + u32 (color + count * (u16, u16) points break; } case EVENT.UNDO: case EVENT.REDO: { break; } case EVENT.IMAGE: case EVENT.IMAGE_MOVE: { size += 4 + 2 + 2; // file id + x + y break; } case EVENT.ERASER: { size += 4; // stroke id break; } default: { console.error('fuck'); } } return size; } function move_canvas() { elements.canvas0.style.transform = `translate(${-storage.canvas.offset_x}px, ${-storage.canvas.offset_y}px) scale(${storage.canvas.zoom})`; elements.canvas1.style.transform = `translate(${-storage.canvas.offset_x}px, ${-storage.canvas.offset_y}px) scale(${storage.canvas.zoom})`; elements.images.style.transform = `translate(${-storage.canvas.offset_x}px, ${-storage.canvas.offset_y}px) scale(${storage.canvas.zoom})`; } function image_at(x, y) { let image_hit = null; for (let i = storage.events.length - 1; i >= 0; --i) { if (!storage.events[i].deleted && storage.events[i].type === EVENT.IMAGE) { const event = storage.events[i]; const item = document.querySelector(`img[data-image-id="${event.image_id}"]`); if (item) { const left = storage.images[event.image_id].x; const right = left + item.width; const top = storage.images[event.image_id].y; const bottom = top + item.height; if (left <= x && x <= right && top <= y && y <= bottom) { return item; } } } } return null; } function activate_image(item) { if (item === null) { elements.canvas1.classList.remove('disabled'); if (elements.active_image) { elements.active_image.classList.remove('activated'); elements.active_image = null; } return; } elements.canvas1.classList.add('disabled'); if (elements.active_image) { if (elements.active_image === item) { return; } elements.active_image.classList.remove('activated'); } elements.active_image = item; item.classList.add('activated'); } function predraw_event(x, y) { return { 'type': EVENT.PREDRAW, 'x': x, 'y': y }; } function stroke_event() { return { 'type': EVENT.STROKE, 'points': storage.current_stroke, 'width': storage.cursor.width, 'color': color_to_u32(storage.cursor.color), }; } function ruler_event(x, y) { const points = []; points.push(predraw_event(storage.ruler_origin.x, storage.ruler_origin.y)); points.push(predraw_event(x, y)); return { 'type': EVENT.RULER, 'points': points, 'width': storage.cursor.width, 'color': color_to_u32(storage.cursor.color), }; } function undo_event() { return { 'type': EVENT.UNDO }; } function redo_event() { return { 'type': EVENT.REDO }; } function image_event(image_id, x, y) { return { 'type': EVENT.IMAGE, 'image_id': image_id, 'x': x, 'y': y, } } function image_move_event(image_id, x, y) { return { 'type': EVENT.IMAGE_MOVE, 'image_id': image_id, 'x': x, 'y': y, } } function eraser_events() { const result = []; for (const stroke_id of storage.erased) { result.push({ 'type': EVENT.ERASER, 'stroke_id': stroke_id, }); } return result; } // Generally doesn't return null function find_stroke_backwards(stroke_id) { for (let i = storage.events.length - 1; i >= 0; --i) { const event = storage.events[i]; if (event.type === EVENT.STROKE && event.stroke_id === stroke_id) { return event; } } return null; } function queue_undo() { const event = undo_event(); queue_event(event); } function main() { const url = new URL(window.location.href); const parts = url.pathname.split('/'); storage.desk_id = parts.length > 0 ? parts[parts.length - 1] : 0; ws_connect(true); elements.canvas0 = document.getElementById('canvas0'); elements.canvas1 = document.getElementById('canvas1'); elements.images = document.getElementById('canvas-images'); tools_init(); // TODO: remove elements.brush_color = document.getElementById('brush-color'); elements.brush_width = document.getElementById('brush-width'); elements.brush_preview = document.getElementById('brush-preview'); elements.toucher = document.getElementById('toucher'); elements.brush_color.value = storage.cursor.color; elements.brush_width.value = storage.cursor.width; update_brush(); storage.canvas.offset_x = window.scrollX; storage.canvas.offset_y = window.scrollY; storage.canvas.max_scroll_x = storage.canvas.width - window.innerWidth; storage.canvas.max_scroll_y = storage.canvas.height - window.innerHeight; storage.ctx0 = elements.canvas0.getContext('2d'); storage.ctx1 = elements.canvas1.getContext('2d'); storage.ctx1.canvas.width = storage.ctx0.canvas.width = storage.canvas.width; storage.ctx1.canvas.height = storage.ctx0.canvas.height = storage.canvas.height; storage.ctx1.lineJoin = storage.ctx1.lineCap = storage.ctx0.lineJoin = storage.ctx0.lineCap = 'round'; storage.ctx1.lineWidth = storage.ctx0.lineWidth = storage.cursor.width; elements.toucher.addEventListener('mousedown', on_down) elements.toucher.addEventListener('mousemove', on_move) elements.toucher.addEventListener('mouseup', on_up); elements.toucher.addEventListener('keydown', on_keydown); elements.toucher.addEventListener('keyup', on_keyup); elements.toucher.addEventListener('resize', on_resize); elements.toucher.addEventListener('contextmenu', cancel); elements.toucher.addEventListener('wheel', on_wheel); elements.toucher.addEventListener('touchstart', on_touchstart); elements.toucher.addEventListener('touchmove', on_touchmove); elements.toucher.addEventListener('touchend', on_touchend); elements.toucher.addEventListener('touchcancel', on_touchend); elements.brush_color.addEventListener('input', update_brush); elements.brush_width.addEventListener('input', update_brush); elements.canvas0.addEventListener('dragover', on_move); elements.canvas0.addEventListener('drop', on_drop); elements.canvas0.addEventListener('mouseleave', on_leave); }