|
|
|
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);
|
|
|
|
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:
|
|
|
|
case EVENT.LIFT: {
|
|
|
|
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(`<svg viewBox="0 0 28 28" xmlns="http://www.w3.org/2000/svg" class="player-cursor" data-player-id="${player_id}">${path_copy.outerHTML}</svg>`);
|
|
|
|
const player = HTML(`<div class="player" data-player-id="${player_id}"><img src="icons/player.svg"></div>`);
|
|
|
|
|
|
|
|
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;
|
|
|
|
}
|
|
|
|
}
|