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, bitmap.width. bitmap.height); await queue_event(state, event); } } function event_size(event) { let size = 4; // type switch (event.type) { case EVENT.PREDRAW: case EVENT.MOVE_CURSOR: { size += 4 * 2; break; } case EVENT.MOVE_CANVAS: { size += 4 * 2 + 4; break; } case EVENT.ZOOM_CANVAS: { size += 4 + 4 * 2; break; } case EVENT.USER_JOINED: case EVENT.LEAVE: case EVENT.CLEAR: { break; } case EVENT.SET_COLOR: { size += 4; break; } case EVENT.SET_WIDTH: { size += 2; break; } case EVENT.STROKE: { // u32 stroke id + u16 (count) + u16 (width) + u32 (color) + count * (f32, f32) points + count (u8) pressures size += 4 + 2 + 2 + 4 + event.points.length * 4 * 2 + round_to_pow2(event.points.length, 4); break; } case EVENT.UNDO: case EVENT.REDO: { break; } case EVENT.IMAGE: case EVENT.IMAGE_MOVE: { size += 4 + 4 + 4 + 4 + 4; // file id + x + y + width + height break; } case EVENT.IMAGE_SCALE: { size += 4 + 4 + 4 + 4; // file_id + corner + 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; } } } // TODO: move these to a file? TypedVector function tv_create(class_name, capacity) { return { 'class_name': class_name, 'data': new class_name(capacity), 'capacity': capacity, 'size': 0, }; } function tv_create_on(class_name, capacity, buffer, offset) { return { 'class_name': class_name, 'data': new class_name(buffer, offset, capacity), 'capacity': capacity, 'size': 0, }; } function tv_data(tv) { return tv.data.subarray(0, tv.size); } function tv_bytes(tv) { return new Uint8Array(tv.data.buffer, 0, tv.size * tv.data.BYTES_PER_ELEMENT); } function tv_ensure(tv, capacity) { if (tv.capacity < capacity) { const new_data = new tv.class_name(capacity); new_data.set(tv_data(tv)); tv.capacity = capacity; tv.data = new_data; } } function tv_ensure_by(tv, by) { tv_ensure(tv, round_to_pow2(tv.size + by, 4096)); } function tv_add(tv, item) { tv.data[tv.size++] = item; } function tv_add2(tv, item) { tv_ensure_by(tv, 1); tv_add(tv, item); } function tv_pop(tv) { const result = tv.data[tv.size - 1]; tv.size--; return result; } function tv_append(tv, typedarray) { tv.data.set(typedarray, tv.size); tv.size += typedarray.length; } function tv_clear(tv) { tv.size = 0; } function HTML(html) { const template = document.createElement('template'); template.innerHTML = html.trim(); return template.content.firstChild; } function toggle_follow_player(state, player_id) { document.querySelectorAll('.player-list .player').forEach(p => p.classList.remove('following')); if (state.following_player === null) { state.following_player = player_id; } else { if (player_id === state.following_player) { state.following_player = null; } else { state.following_player = player_id; } } const player_element = document.querySelector(`.player-list .player[data-player-id="${state.following_player}"]`); if (player_element) player_element.classList.add('following'); send_follow(state.following_player); } function insert_player_cursor(state, player_id) { const color = random_bright_color_from_seed(parseInt(player_id)); const path_copy = state.cursor_path.cloneNode(); path_copy.style.fill = color; const cursor = HTML(``); const player = HTML(`
`); player.style.background = color; player.addEventListener('click', () => { toggle_follow_player(state, player_id); }); document.querySelector('.html-hud').appendChild(cursor); document.querySelector('.player-list').appendChild(player); document.querySelector('.player-list').classList.remove('vhide'); return cursor; } async function load_player_cursor_template(state) { const resp = await fetch('icons/player-cursor.svg'); const text = await resp.text(); const parser = new DOMParser(); const parsed_xml = parser.parseFromString(text, 'image/svg+xml'); const path = parsed_xml.querySelector('path'); state.cursor_path = path; } function get_image(context, key) { for (const entry of context.images) { if (entry.key === key) { return entry; } } return null; } function grid_snap_step(state) { const zoom_log2 = Math.log2(state.canvas.zoom); const zoom_previous = Math.pow(2, Math.floor(zoom_log2)); const zoom_next = Math.pow(2, Math.ceil(zoom_log2)); if (Math.abs(state.canvas.zoom - zoom_previous) < Math.abs(state.canvas.zoom - zoom_next)) { return 32 / zoom_previous; } else { return 32 / zoom_next; } }