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.
282 lines
7.3 KiB
282 lines
7.3 KiB
let ws = null; |
|
let ls = window.localStorage; |
|
|
|
document.addEventListener('DOMContentLoaded', main); |
|
|
|
const EVENT = Object.freeze({ |
|
PREDRAW: 10, |
|
STROKE: 20, |
|
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: 'wss://desk.local/ws/', |
|
image_url: 'https://desk.local/images/', |
|
sync_timeout: 1000, |
|
ws_reconnect_timeout: 2000, |
|
}; |
|
|
|
const storage = { |
|
'state': { |
|
'drawing': false, |
|
'moving': false, |
|
'moving_image': false, |
|
'mousedown': false, |
|
'spacedown': false, |
|
}, |
|
|
|
'erased': [], |
|
'tool': 'brush', |
|
'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': 4096, |
|
'height': 4096, |
|
'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 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; |
|
} |
|
|
|
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(); |
|
|
|
elements.canvas0 = document.getElementById('canvas0'); |
|
elements.canvas1 = document.getElementById('canvas1'); |
|
elements.images = document.getElementById('canvas-images'); |
|
elements.brush_color = document.getElementById('brush-color'); |
|
elements.brush_width = document.getElementById('brush-width'); |
|
elements.brush_preview = document.getElementById('brush-preview'); |
|
|
|
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; |
|
|
|
window.addEventListener('pointerdown', on_down) |
|
window.addEventListener('pointermove', on_move) |
|
window.addEventListener('pointerup', on_up); |
|
window.addEventListener('pointercancel', on_up); |
|
window.addEventListener('keydown', on_keydown); |
|
window.addEventListener('keyup', on_keyup); |
|
window.addEventListener('resize', on_resize); |
|
window.addEventListener('wheel', on_wheel); |
|
window.addEventListener('touchstart', cancel); |
|
window.addEventListener('contextmenu', cancel); |
|
|
|
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('pointerleave', on_leave); |
|
}
|
|
|