Compare commits

..

No commits in common. 'sdf' and 'master' have entirely different histories.
sdf ... master

  1. 6
      .gitignore
  2. 5
      Caddyfile
  3. 24
      LICENSE
  4. 104
      README.txt
  5. 268
      client/aux.js
  6. 427
      client/bvh.js
  7. 593
      client/client_recv.js
  8. 436
      client/client_send.js
  9. 37
      client/config.js
  10. 315
      client/cursor.js
  11. 416
      client/default.css
  12. 66
      client/draw.js
  13. BIN
      client/favicon2.png
  14. BIN
      client/favicon_old.png
  15. 130
      client/heapify.js
  16. 8
      client/icons/cheeseburga.svg
  17. 281
      client/icons/crosshair.svg
  18. 26
      client/icons/cursor.svg
  19. 54
      client/icons/draw.svg
  20. 77
      client/icons/erase.svg
  21. 52
      client/icons/favicon.svg
  22. 7
      client/icons/pen.svg
  23. 2
      client/icons/perfect-bullet.svg
  24. 105
      client/icons/picker.svg
  25. 54
      client/icons/player-cursor.svg
  26. 57
      client/icons/player.svg
  27. 52
      client/icons/pointer.svg
  28. 54
      client/icons/redo.svg
  29. 51
      client/icons/ruler.svg
  30. 57
      client/icons/undo.svg
  31. 253
      client/index.html
  32. 464
      client/index.js
  33. 36
      client/index.log
  34. 47
      client/lod_worker.js
  35. 481
      client/math.js
  36. 345
      client/recv.js
  37. 201
      client/send.js
  38. 236
      client/speed.js
  39. 175
      client/tools.js
  40. 359
      client/touch.js
  41. 132
      client/undo.js
  42. 1
      client/wasm/compile_command
  43. 351
      client/wasm/lod.c
  44. BIN
      client/wasm/lod.wasm
  45. 613
      client/webgl_draw.js
  46. 626
      client/webgl_geometry.js
  47. 839
      client/webgl_listeners.js
  48. 460
      client/webgl_shaders.js
  49. 50
      client/websocket.js
  50. 2
      server/config.js
  51. BIN
      server/data-local.sqlite
  52. 101
      server/deserializer.js
  53. 18
      server/enums.js
  54. 10
      server/http.js
  55. 4
      server/math.js
  56. 85
      server/milton.js
  57. 159
      server/recv.js
  58. 156
      server/send.js
  59. 87
      server/serializer.js
  60. 16
      server/server.js
  61. 180
      server/storage.js

6
.gitignore vendored

@ -1,9 +1,3 @@
server/images server/images
server/server.log
doca.txt doca.txt
data/ data/
client/*.dot
server/points.txt
server/scripts.js
*.o
*.out

5
Caddyfile

@ -1,9 +1,4 @@
desk.local { desk.local {
header {
Cross-Origin-Opener-Policy same-origin
Cross-Origin-Embedder-Policy require-corp
}
redir /ws /ws/ redir /ws /ws/
redir /desk /desk/ redir /desk /desk/

24
LICENSE

@ -1,24 +0,0 @@
This is free and unencumbered software released into the public domain.
Anyone is free to copy, modify, publish, use, compile, sell, or
distribute this software, either in source code form or as a compiled
binary, for any purpose, commercial or non-commercial, and by any
means.
In jurisdictions that recognize copyright laws, the author or authors
of this software dedicate any and all copyright interest in the
software to the public domain. We make this dedication for the benefit
of the public at large and to the detriment of our heirs and
successors. We intend this dedication to be an overt act of
relinquishment in perpetuity of all present and future rights to this
software under copyright law.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR
OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,
ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
OTHER DEALINGS IN THE SOFTWARE.
For more information, please refer to <http://unlicense.org>

104
README.txt

@ -1,104 +0,0 @@
Release:
* Engine
+ Benchmark harness
+ Reuse points, pack "nodraw" in high bit of stroke id (probably have at least one more bit, so up to 4 flag configurations)
+ Draw dynamic data (strokes in progress)
+ Webassembly for core LOD generation
+ Webassembly for final buffers
+ Do not copy memory to wasm, instead use wasm memory to store data in the first place
+ SIMD for LOD?
+ Multithreading for LOD
+ Textured quads (pictures, code already written in older version
+ Resize and move pictures (draw handles)
- Z-prepass fringe bug (also, when do we enable the prepass?)
- Frame-independent lerp where applicable
+ Bugs
+ GC stalls!!!
+ Stroke previews get connected when drawn without panning on touch devices
+ Redraw HTML (cursors) on local canvas moves
+ New strokes dissapear on the HMH desk
+ Undo history of moving and scaling images seems messed up
- Nothing get's drawn if we enable snapping and draw a curve where first and last point match
- Weird clipping on HMH desk full zoomout after running "benchmark"
- Stuck in color picker mode when mouse leaves screen
- Debug
* Debug view for BVH
- Restore ability to limit event range
* Listeners/events/multiplayer
+ Fix multiplayer LUL
+ Fix blinking own stroke inbetween SYN->server and SYN->client
+ Drag with mouse button 3
+ Investigate skipped inputs on mobile (panning, zooming) [Events were not actually getting skipped. The stroke previews were just not being drawn]
+ Smooth zoom
+ Infinite background pattern
+ Be able to have multiple "current" strokes per player. In case of bad internet this can happen!
- Do NOT use session id as player id LUL
- Save events to indexeddb (as some kind of a blob), restore on reconnect and page reload
- Handle out of space
- Local prediction for tools!
- Immediately commit a stroke to the canvas, change order if earlier strokes arrive
- Show my own image immediately, show placeholders while images are loading (add bitmap size to event)
- undo immediately, this one can not arrive out of order, because noone else is going to undo MY actions
* Missing features I do not consider bonus
+ Player pointers
+ Pretty player pointers
+ Player list
+ Follow player
+ Color picker (or at the very least an Open Color color pallete)
+ EYE DROPPER!
+ Dynamic svg cursor to represent the brush
+ Eraser
* Line drawing
+ Live preview
~ Alignment (horizontal, vertical, diagonal, etc) [kinda gets covered by the snapping? question mark?]
+ Undo
+ Undo for eraser
+ Undo for images (add, move, scale)
- Redo
+ Snapping to grid
- Snapping to other points?
- Color picker should work for ruler
- Show previous color in color picker preview
- Stick picker preview to cursor
* Polish
+ Use typedvector where appropriate
- Show what's happening while the desk is loading (downloading, processing, uploading to gpu)
- Settings panel for config values (including the setting for "offline mode")
- Set up VAOs
- We are calling "geometry_prepare_stroke" twice for some reason
- Replace "geometry_add_dummy_stroke" with something not [so] cursed
- Automatically extract locations from shaders (see nitka project for code examples)
- Presentation / "marketing"
- Title (InfiNotes? MegaDesk?)
- Icon
- Product page (github readme, demo videos)
Bonus:
* Handle pressure
+ Add pressure data to quads
+ Account for pressure in quad/bbox calc
+ Adjust curve simplification to include pressure info
+ Migrate old non-pressure desks
- Check out e.pressure on touch devices
- Send pressure in PREDRAW event
- Stroke smoothing
https://github.com/xournalpp/xournalpp/issues/2320
https://www.digital-epigraphy.com/tutorials/the-most-useful-new-features-of-photoshop-cc-using-brush-stroke-smoothing-for-digital-inking
https://stackoverflow.com/questions/20618804/how-to-smooth-a-curve-for-a-dataset
- Curve modification
- Select curves (with a lasso?)
- Move whole curve
- Move single point
- Move multiple points
* Customizable background
+ Dots pattern
+ Grid pattern
- Menu option
- Offline mode
- Only one user
- No server, everything applied immediately
- Allow export to file
- Save to browser storage (probaby indexed db + recent events in localstorage)
Bonus-bonus:
- Actually infinite canvas (replace floats with something, some kind of fixed point scheme? chunks? multilevel scheme?)

268
client/aux.js

@ -1,268 +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, 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;
}
}

427
client/bvh.js

@ -1,427 +0,0 @@
function bvh_make_leaf(bvh, index, stroke) {
const leaf = {
'stroke_index': index,
'bbox': stroke.bbox,
'area': stroke.area,
'parent_index': null,
'is_leaf': true,
};
bvh.nodes.push(leaf);
return bvh.nodes.length - 1;
}
function bvh_make_internal(bvh) {
const node = {
'child1': null,
'child2': null,
'parent_index': null,
'is_leaf': false,
};
bvh.nodes.push(node);
return bvh.nodes.length - 1;
}
function bvh_compute_sah(bvh, new_leaf, potential_sibling, only_parent = false) {
let cost = 0;
let union_box;
if (!only_parent) {
union_box = quad_union(new_leaf.bbox, potential_sibling.bbox);
const internal_node_would_be = { 'bbox': union_box };
const new_internal_node_cost = (union_box.x2 - union_box.x1) * (union_box.y2 - union_box.y1);
cost += new_internal_node_cost;
} else {
union_box = new_leaf.bbox;
}
let parent_index = potential_sibling.parent_index;
while (parent_index !== null) {
const current_node = bvh.nodes[parent_index];
const old_cost = current_node.area;
union_box = quad_union(current_node.bbox, union_box);
const new_cost = (union_box.x2 - union_box.x1) * (union_box.y2 - union_box.y1);
cost += new_cost - old_cost;
parent_index = current_node.parent_index;
}
return cost;
}
function bvh_find_best_sibling(bvh, leaf_index) {
// branch and bound
const leaf = bvh.nodes[leaf_index];
const leaf_cost = (leaf.bbox.x2 - leaf.bbox.x1) * (leaf.bbox.y2 - leaf.bbox.y1);
let best_cost = bvh_compute_sah(bvh, leaf, bvh.nodes[bvh.root]);
let best_index = bvh.root;
bvh.pqueue.clear();
bvh.pqueue.push(best_index, best_cost);
while (bvh.pqueue.size > 0) {
const current_index = bvh.pqueue.pop();
const current_node = bvh.nodes[current_index];
const cost = bvh_compute_sah(bvh, current_node, leaf);
if (cost < best_cost) {
best_cost = cost;
best_index = current_index;
}
if (!current_node.is_leaf) {
const child1 = bvh.nodes[current_node.child1];
const lower_bound_for_children = bvh_compute_sah(bvh, child1, leaf, true) + leaf_cost;
if (lower_bound_for_children < best_cost) {
bvh.pqueue.push(current_node.child1, lower_bound_for_children);
bvh.pqueue.push(current_node.child2, lower_bound_for_children);
}
}
}
return best_index;
}
function bvh_add_stroke(state, bvh, index, stroke) {
const leaf_index = bvh_make_leaf(bvh, index, stroke);
stroke.bvh_node = leaf_index;
if (bvh.nodes.length === 1) {
bvh.root = leaf_index;
return;
}
if (bvh.pqueue.capacity < Math.ceil(bvh.nodes.length * 1.2)) {
bvh.pqueue = new MinQueue(bvh.nodes.length * 2);
}
// It's as easy as 1-2-3
// 1. Find best sibling for leaf
const sibling = bvh_find_best_sibling(bvh, leaf_index);
// 2. Create new parent
const old_parent = bvh.nodes[sibling].parent_index;
const new_parent = bvh_make_internal(bvh);
bvh.nodes[new_parent].parent_index = old_parent;
bvh.nodes[new_parent].bbox = quad_union(stroke.bbox, bvh.nodes[sibling].bbox);
if (old_parent !== null) {
// The sibling was not the root
if (bvh.nodes[old_parent].child1 === sibling) {
bvh.nodes[old_parent].child1 = new_parent;
} else {
bvh.nodes[old_parent].child2 = new_parent;
}
bvh.nodes[new_parent].child1 = sibling;
bvh.nodes[new_parent].child2 = leaf_index;
bvh.nodes[sibling].parent_index = new_parent;
bvh.nodes[leaf_index].parent_index = new_parent;
} else {
// The sibling was the root
bvh.nodes[new_parent].child1 = sibling;
bvh.nodes[new_parent].child2 = leaf_index;
bvh.nodes[sibling].parent_index = new_parent;
bvh.nodes[leaf_index].parent_index = new_parent;
bvh.root = new_parent;
}
const new_bbox = quad_union(bvh.nodes[bvh.nodes[new_parent].child1].bbox, bvh.nodes[bvh.nodes[new_parent].child2].bbox);
bvh.nodes[new_parent].bbox = new_bbox;
bvh.nodes[new_parent].area = (new_bbox.x2 - new_bbox.x1) * (new_bbox.y2 - new_bbox.y1);
// 3. Refit and insert in fullnode
let refit_index = bvh.nodes[leaf_index].parent_index;
while (refit_index !== null) {
const child1 = bvh.nodes[refit_index].child1;
const child2 = bvh.nodes[refit_index].child2;
bvh.nodes[refit_index].bbox = quad_union(bvh.nodes[child1].bbox, bvh.nodes[child2].bbox);
if (bvh.nodes[refit_index].is_fullnode) {
tv_add2(bvh.nodes[refit_index].stroke_indices, index);
}
refit_index = bvh.nodes[refit_index].parent_index;
}
}
function bvh_delete_stroke(state, stroke) {
let node = state.bvh.nodes[stroke.bvh_node];
while (node.parent_index !== null) {
if (node.is_fullnode) {
let index_index = tv_data(node.stroke_indices).indexOf(stroke.index);
if (index_index !== -1) {
node.stroke_indices.data[index_index] = node.stroke_indices.data[node.stroke_indices.size - 1];
tv_pop(node.stroke_indices);
}
break;
}
node = state.bvh.nodes[node.parent_index];
}
}
function bvh_undelete_stroke(state, stroke) {
let node = state.bvh.nodes[stroke.bvh_node];
while (node.parent_index !== null) {
if (node.is_fullnode) {
tv_add2(node.stroke_indices, stroke.index);
break;
}
node = state.bvh.nodes[node.parent_index];
}
}
function bvh_copy_fullnode(quad, node, result_buffer) {
if (quad_fully_inside(quad, node.bbox)) {
tv_append(result_buffer, tv_data(node.stroke_indices));
return true;
}
return false;
}
function bvh_intersect_quad(state, bvh, quad, result_buffer) {
if (bvh.root === null) {
return;
}
tv_clear(bvh.traverse_stack);
tv_add(bvh.traverse_stack, bvh.root);
while (bvh.traverse_stack.size > 0) {
const node_index = tv_pop(bvh.traverse_stack);
const node = bvh.nodes[node_index];
if (!quads_intersect(node.bbox, quad)) {
continue;
}
if (node.is_fullnode) {
const fully_inside = bvh_copy_fullnode(quad, node, result_buffer);
if (fully_inside) {
continue;
}
}
if (node.is_leaf) {
if (state.events[node.stroke_index].deleted !== true) {
tv_add(result_buffer, node.stroke_index);
}
} else {
tv_add(bvh.traverse_stack, node.child1);
tv_add(bvh.traverse_stack, node.child2);
}
}
}
function bvh_clip(state, context) {
if (state.stroke_count === 0) {
return;
}
tv_ensure(context.clipped_indices, round_to_pow2(state.stroke_count, 4096))
tv_ensure(state.bvh.traverse_stack, round_to_pow2(state.stroke_count, 4096));
tv_clear(context.clipped_indices);
const screen_topleft = screen_to_canvas(state, {'x': 0, 'y': 0});
const screen_bottomright = screen_to_canvas(state, {'x': context.canvas.width, 'y': context.canvas.height});
const screen_topright = { 'x': screen_bottomright.x, 'y': screen_topleft.y };
const screen_bottomleft = { 'x': screen_topleft.x, 'y': screen_bottomright.y };
const screen = {
'x1': screen_topleft.x,
'y1': screen_topleft.y,
'x2': screen_bottomright.x,
'y2': screen_bottomright.y
};
bvh_intersect_quad(state, state.bvh, screen, context.clipped_indices);
tv_data(context.clipped_indices).sort(); // we need to draw back to front still!
}
function bvh_point(state, p) {
const bvh = state.bvh;
const stack = [];
const indices = [];
if (bvh.root === null) {
return null;
}
stack.push(bvh.root);
while (stack.length > 0) {
const node_index = stack.pop();
const node = bvh.nodes[node_index];
if (!point_in_bbox(p, node.bbox)) {
continue;
}
if (node.is_leaf) {
const stroke = state.events[node.stroke_index];
const xs = state.wasm.buffers['xs'].tv.data.subarray(stroke.coords_from, stroke.coords_to);
const ys = state.wasm.buffers['ys'].tv.data.subarray(stroke.coords_from, stroke.coords_to);
const pressures = state.wasm.buffers['pressures'].tv.data.subarray(stroke.coords_from, stroke.coords_to);
if (stroke.deleted !== true && point_in_stroke(p, xs, ys, pressures, stroke.width)) {
indices.push(node.stroke_index);
}
} else {
stack.push(node.child1);
stack.push(node.child2);
}
}
if (indices.length > 0) {
indices.sort();
return indices[indices.length - 1];
}
return null;
}
function bvh_construct_rec(state, bvh, strokes, depth) {
if (strokes.length > 1) {
// internal
let sorted_strokes;
let min_x = strokes[0].bbox.cx;
let min_y = strokes[0].bbox.cy;
let max_x = strokes[0].bbox.cx;
let max_y = strokes[0].bbox.cy;
for (let i = 0; i < strokes.length; ++i) {
const stroke = strokes[i];
const cx = stroke.bbox.cx;
const cy = stroke.bbox.cy;
if (cx < min_x) {
min_x = cx;
}
if (cy < min_y) {
min_y = cx;
}
if (cx > max_x) {
max_x = cx;
}
if (cy > max_y) {
max_y = cy;
}
}
const vertical = (max_y - min_y) > (max_x - min_x);
if (vertical) {
sorted_strokes = strokes.toSorted((a, b) => a.bbox.cy - b.bbox.cy);
} else {
sorted_strokes = strokes.toSorted((a, b) => a.bbox.cx - b.bbox.cx);
}
const node_index = bvh_make_internal(bvh);
const left_of_split_count = Math.floor(strokes.length / 2);
const child1 = bvh_construct_rec(state, bvh, sorted_strokes.slice(0, left_of_split_count), depth + 1);
const child2 = bvh_construct_rec(state, bvh, sorted_strokes.slice(left_of_split_count, sorted_strokes.length), depth + 1);
bvh.nodes[child1].parent_index = node_index;
bvh.nodes[child2].parent_index = node_index;
bvh.nodes[node_index].child1 = child1;
bvh.nodes[node_index].child2 = child2;
bvh.nodes[node_index].bbox = quad_union(bvh.nodes[child1].bbox, bvh.nodes[child2].bbox);
if (depth === config.bvh_fullnode_depth) {
const indices = tv_create(Int32Array, round_to_pow2(strokes.length, 32));
for (let i = 0; i < strokes.length; ++i) {
tv_add(indices, strokes[i].index);
}
bvh.nodes[node_index].stroke_indices = indices;
bvh.nodes[node_index].is_fullnode = true;
}
return node_index;
} else {
// leaf
const leaf_index = bvh_make_leaf(bvh, strokes[0].index, strokes[0]);
state.events[strokes[0].index].bvh_node = leaf_index;
return leaf_index;
}
}
function bvh_construct(state) {
const strokes = state.events.filter(e => e.type === EVENT.STROKE && e.deleted !== true);
if (strokes.length > 0) {
state.bvh.root = bvh_construct_rec(state, state.bvh, strokes, 0);
}
}
function bvh_get_fullnodes_debug(state, context) {
const bvh = state.bvh;
const result = [];
const stack = [];
const screen_topleft = screen_to_canvas(state, {'x': 0, 'y': 0});
const screen_bottomright = screen_to_canvas(state, {'x': context.canvas.width, 'y': context.canvas.height});
const screen_topright = { 'x': screen_bottomright.x, 'y': screen_topleft.y };
const screen_bottomleft = { 'x': screen_topleft.x, 'y': screen_bottomright.y };
const quad = {
'x1': screen_topleft.x,
'y1': screen_topleft.y,
'x2': screen_bottomright.x,
'y2': screen_bottomright.y
};
if (bvh.root === null) {
return;
}
stack.push({'depth': 0, 'node_index': bvh.root});
while (stack.length > 0) {
const entry = stack.pop();
const node = bvh.nodes[entry.node_index];
if (!quads_intersect(node.bbox, quad)) {
continue;
}
if (node.is_fullnode) {
result.push({...node.bbox});
continue;
}
if (!node.is_leaf && entry.depth < config.bvh_fullnode_depth) {
stack.push({'depth': entry.depth + 1, 'node_index': node.child1});
stack.push({'depth': entry.depth + 1, 'node_index': node.child2});
}
}
return result;
}

593
client/client_recv.js

@ -1,593 +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_s32(d) {
const value = d.view.getInt32(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_align(d, to) {
// TODO: non-stupid version of this
while (d.offset % to != 0) {
d.offset++;
}
}
function des_f32array(d, count) {
const result = new Float32Array(d.buffer, d.offset, count);
d.offset += 4 * count;
return result;
}
function des_u8array(d, count) {
const result = new Uint8Array(d.buffer, d.offset, count);
d.offset += count;
return result;
}
function des_event(d, state = null) {
const event = {};
event.type = des_u32(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.USER_JOINED:
case EVENT.LEAVE:
case EVENT.CLEAR:
case EVENT.LIFT: {
break;
}
case EVENT.MOVE_CURSOR: {
event.x = des_f32(d);
event.y = des_f32(d);
break;
}
case EVENT.MOVE_CANVAS: {
event.offset_x = des_s32(d);
event.offset_y = des_s32(d);
event.zoom_level = des_s32(d);
break;
}
case EVENT.ZOOM_CANVAS: {
event.zoom_level = des_s32(d);
event.zoom_cx = des_f32(d);
event.zoom_cy = 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);
event.coords = des_f32array(d, point_count * 2);
event.press = des_u8array(d, point_count);
des_align(d, 4);
// TODO: remove, this is duplicate data
event.stroke_id = stroke_id;
event.color = color;
event.width = width;
break;
}
case EVENT.IMAGE: {
event.image_id = des_u32(d);
event.x = des_f32(d);
event.y = des_f32(d);
event.width = des_u32(d);
event.height = des_u32(d);
break;
}
case EVENT.IMAGE_MOVE: {
event.image_id = des_u32(d);
event.x = des_f32(d);
event.y = des_f32(d);
break;
}
case EVENT.IMAGE_SCALE: {
event.image_id = des_u32(d);
event.corner = 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': [],
'online': false,
'cursor': {'x': 0, 'y': 0},
'strokes': [],
'current_prestroke': false,
};
}
function handle_event(state, context, event, options = {}) {
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.USER_JOINED: {
state.players[event.user_id].online = true;
draw_html(state);
break;
}
case EVENT.PREDRAW: {
const player = state.players[event.user_id];
if (!player.current_prestroke) {
geometry_start_prestroke(state, event.user_id);
}
geometry_add_prepoint(state, context, event.user_id, {'x': event.x, 'y': event.y, 'pressure': 128}, false); // TODO: add pressure to predraw events
need_draw = true;
break;
}
case EVENT.CLEAR: {
// TODO: @touch
break;
}
case EVENT.LIFT: {
// Current stroke from player ended. Handle following PREDRAWN events as next stroke
geometry_end_prestroke(state, event.user_id);
break;
}
case EVENT.LEAVE: {
if (event.user_id in state.players) {
state.players[event.user_id].online = false;
draw_html(state);
}
break;
}
case EVENT.MOVE_CURSOR: {
if (event.user_id in state.players) {
state.players[event.user_id].cursor.x = event.x;
state.players[event.user_id].cursor.y = event.y;
state.players[event.user_id].online = true;
}
// Should we syncronize this to RAF?
draw_html(state);
break;
}
case EVENT.MOVE_CANVAS: {
// Double-check just in case
// Non-triple equals in on purpose
if (event.user_id == state.following_player) {
state.canvas.offset.x = event.offset_x;
state.canvas.offset.y = event.offset_y;
const zoom_level = event.zoom_level;
const dz = (zoom_level > 0 ? config.zoom_delta : -config.zoom_delta);
const zoom = Math.pow(1.0 + dz, Math.abs(zoom_level))
state.canvas.zoom_level = zoom_level;
state.canvas.zoom = zoom;
state.canvas.target_zoom = zoom;
need_draw = true;
}
break;
}
case EVENT.ZOOM_CANVAS: {
if (event.user_id == state.following_player) {
const zoom_level = event.zoom_level;
const zoom_center = {'x': event.zoom_cx, 'y': event.zoom_cy};
const dz = (zoom_level > 0 ? config.zoom_delta : -config.zoom_delta);
const zoom = Math.pow(1.0 + dz, Math.abs(zoom_level))
state.canvas.zoom_level = zoom_level;
state.canvas.target_zoom = zoom;
state.canvas.zoom_screenp = canvas_to_screen(state, zoom_center);
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: {
const point_count = event.coords.length / 2;
if (point_count === 0) {
break;
}
let last_stroke = null;
for (let i = state.events.length - 1; i >= 0; --i) {
if (state.events[i].type === EVENT.STROKE) {
last_stroke = state.events[i];
break;
}
}
const index_difference = state.events.length - (last_stroke === null ? -1 : last_stroke.index);
wasm_ensure_by(state, index_difference, event.coords.length);
const pressures = state.wasm.buffers['pressures'];
const xs = state.wasm.buffers['xs'];
const ys = state.wasm.buffers['ys'];
event.coords_from = xs.tv.size;
event.coords_to = xs.tv.size + point_count;
for (let i = 0; i < index_difference - 1; ++i) {
// Create empty records for all non-stroke events that happened since the last stroke
tv_add(state.wasm.buffers['coords_from'].tv, xs.tv.size);
state.wasm.buffers['coords_from'].used += 4; // 4 bytes, not 4 ints
}
// Create actual records for this stroke
tv_add(state.wasm.buffers['coords_from'].tv, xs.tv.size + point_count);
state.wasm.buffers['coords_from'].used += 4; // 4 bytes, not 4 ints
for (let i = 0; i < event.coords.length; i += 2) {
tv_add(xs.tv, event.coords[i + 0]);
tv_add(ys.tv, event.coords[i + 1]);
}
state.wasm.buffers['xs'].used += point_count * 4;
state.wasm.buffers['ys'].used += point_count * 4;
tv_append(pressures.tv, event.press);
state.wasm.buffers['pressures'].used += point_count;
delete event.coords;
delete event.press;
need_draw = true;
event.index = state.events.length;
geometry_clear_oldest_prestroke(state, context, event.user_id);
geometry_add_stroke(state, context, event, state.events.length, options.skip_bvh === true);
state.stroke_count++;
break;
}
case EVENT.UNDO: {
geometry_add_dummy_stroke(context);
need_draw = undo(state, context, event, options);
break;
}
case EVENT.IMAGE: {
const p = {'x': event.x, 'y': event.y};
geometry_add_dummy_stroke(context);
add_image(context, event.image_id, null, p, event.width, event.height);
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();
// NOTE: this will resolve when bitmap is ready, which will be much later
const bitmap = await createImageBitmap(blob);
event.width = bitmap.width;
event.height = bitmap.height;
add_image(context, event.image_id, bitmap, p, bitmap.width, bitmap.height);
// 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: {
geometry_add_dummy_stroke(context);
const image_id = event.image_id;
const image = get_image(context, image_id);
if (image) {
// if (config.debug_print) console.debug('move image', image_id, 'to', image_event.x, image_event.y);
push_image_move(image, event.x, event.y);
need_draw = true;
}
break;
}
case EVENT.IMAGE_SCALE: {
geometry_add_dummy_stroke(context);
const image_id = event.image_id;
const image = get_image(context, image_id);
if (image !== null) {
push_image_scale(image, event.corner, event.x, event.y);
need_draw = true;
}
break;
}
case EVENT.ERASER: {
geometry_add_dummy_stroke(context);
need_draw = true;
const stroke = state.events[event.stroke_id];
stroke.deleted = true;
if (!options.skip_bvh) {
bvh_delete_stroke(state, stroke);
}
break;
}
default: {
console.error('fuck');
}
}
return need_draw;
}
function handle_message(state, context, d) {
const message_type = des_u32(d);
let do_draw = false;
// if (config.debug_print) console.debug(message_type);
switch (message_type) {
case MESSAGE.JOIN:
case MESSAGE.INIT: {
console.time('init');
state.online = true;
state.server_lsn = des_u32(d);
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);
if (config.debug_print) console.debug('init in');
}
state.me = parseInt(localStorage.getItem('sessionId'));
init_player_defaults(state, state.me);
set_color_u32(state, color);
document.querySelector('#stroke-width').value = width;
fire_event(state, width_event(width));
const event_count = des_u32(d);
const user_count = des_u32(d);
const total_points = des_u32(d);
wasm_ensure_by(state, event_count, round_to_pow2(total_points * 2, 4096));
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);
const user_online = des_u8(d);
init_player_defaults(state, user_id, user_color, user_width);
state.players[user_id].online = user_online === 1;
}
des_align(d, 4);
for (let i = 0; i < event_count; ++i) {
const event = des_event(d, state);
handle_event(state, context, event, {'skip_bvh': true});
if (event.type !== EVENT.STROKE || event.coords_to - event.coords_from > 0) {
state.events.push(event);
}
}
state.sn = event_count;
bvh_construct(state);
do_draw = true;
send_ack(event_count);
sync_queue(state);
console.timeEnd('init');
update_cursor(state);
draw_html(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, state);
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);
break;
}
default: {
console.error('fuck');
return;
}
}
if (do_draw) {
schedule_draw(state, context);
}
}

436
client/client_send.js

@ -1,436 +0,0 @@
function serializer_create(size) {
const buffer = new ArrayBuffer(size);
return {
'offset': 0,
'size': size,
'buffer': buffer,
'view': new DataView(buffer),
'strview': new Uint8Array(buffer),
'need_gpu_allocate': true, // need to call glBufferData to create a GPU buffer of size serializer.size
'gpu_upload_from': 0, // need to call glBufferSubData/glTexSubImage2D for bytes in [serializer.gpu_upload_from, serializer.offset)
};
}
function ser_ensure(s, size) {
if (s.size < size) {
const new_s = serializer_create(Math.ceil(size * 2));
new_s.strview.set(s.strview);
new_s.offset = s.offset;
return new_s;
}
return s;
}
function ser_ensure_by(s, by) {
if (s.offset + by > s.size) {
const new_s = serializer_create(Math.ceil((s.size + by) * 2));
new_s.strview.set(s.strview);
new_s.offset = s.offset;
return new_s;
}
return s;
}
function ser_clear(s) {
s.offset = 0;
s.gpu_upload_from = 0;
}
function ser_u8(s, value) {
s.view.setUint8(s.offset, value);
s.offset += 1;
}
function ser_u16(s, value) {
s.view.setUint16(s.offset, value, true);
s.offset += 2;
}
function ser_f32(s, value) {
s.view.setFloat32(s.offset, value, true);
s.offset += 4;
}
function ser_u32(s, value) {
s.view.setUint32(s.offset, value, true);
s.offset += 4;
}
function ser_s32(s, value) {
s.view.setInt32(s.offset, value, true);
s.offset += 4;
}
function ser_align(s, to) {
// TODO: non-stupid version of this
while (s.offset % to != 0) {
s.offset++;
}
}
function ser_event(s, event) {
ser_u32(s, event.type);
switch (event.type) {
case EVENT.PREDRAW: {
ser_f32(s, event.x);
ser_f32(s, event.y);
break;
}
case EVENT.CLEAR:
case EVENT.LIFT: {
break;
}
case EVENT.MOVE_CURSOR: {
ser_f32(s, event.x);
ser_f32(s, event.y);
break;
}
case EVENT.MOVE_CANVAS: {
ser_u32(s, event.offset_x);
ser_u32(s, event.offset_y);
ser_s32(s, event.zoom_level);
break;
}
case EVENT.ZOOM_CANVAS: {
ser_s32(s, event.zoom_level);
ser_f32(s, event.zoom_cx);
ser_f32(s, event.zoom_cy);
break;
}
case EVENT.SET_COLOR: {
ser_u32(s, event.color);
break;
}
case EVENT.SET_WIDTH: {
ser_u16(s, event.width);
break;
}
case EVENT.STROKE: {
ser_u16(s, event.points.length);
ser_u16(s, event.width);
ser_u32(s, event.color);
if (config.debug_print) console.debug('original', event.points);
for (const point of event.points) {
ser_f32(s, point.x);
ser_f32(s, point.y);
}
for (const point of event.points) {
ser_u8(s, point.pressure);
}
ser_align(s, 4);
break;
}
case EVENT.IMAGE:
case EVENT.IMAGE_MOVE: {
const image_id = parseInt(event.image_id);
ser_u32(s, image_id);
ser_f32(s, event.x);
ser_f32(s, event.y);
ser_u32(s, event.width);
ser_u32(s, event.height);
break;
}
case EVENT.IMAGE_SCALE: {
const image_id = parseInt(event.image_id);
ser_u32(s, image_id);
ser_u32(s, event.corner); // which corner was moved
ser_f32(s, event.x); // where corner was moved to (canvas coordinates)
ser_f32(s, event.y);
break;
}
case EVENT.UNDO:
case EVENT.REDO: {
break;
}
case EVENT.ERASER: {
ser_u32(s, event.stroke_id);
break;
}
default: {
console.error('fuck');
}
}
}
function send_ack(sn) {
const s = serializer_create(4 + 4);
ser_u32(s, MESSAGE.ACK);
ser_u32(s, sn);
if (config.debug_print) console.debug(`ack ${sn} out`);
try {
if (ws) ws.send(s.buffer);
} catch(e) {
ws.close();
}
}
function send_follow(player_id) {
const s = serializer_create(4 + 4);
player_id = player_id === null ? -1 : player_id;
ser_u32(s, MESSAGE.FOLLOW);
ser_u32(s, player_id);
if (config.debug_print) console.debug(`follow ${player_id} out`);
try {
if (ws) ws.send(s.buffer);
} catch (e) {
ws.close();
}
}
function sync_queue(state) {
if (ws === null) {
if (config.debug_print) console.debug('socket has closed, stopping SYNs');
return;
}
let size = 4 + 4 + 4; // opcode + lsn + event count
let count = state.lsn - state.server_lsn;
if (count === 0) {
if (config.debug_print) console.debug('server ACKed all events, clearing queue');
state.queue.length = 0;
return;
}
for (let i = count - 1; i >= 0; --i) {
const event = state.queue[state.queue.length - 1 - i];
size += event_size(event);
}
const s = serializer_create(size);
ser_u32(s, MESSAGE.SYN);
ser_u32(s, state.lsn);
ser_u32(s, count);
for (let i = count - 1; i >= 0; --i) {
const event = state.queue[state.queue.length - 1 - i];
ser_event(s, event);
}
if (config.debug_print) console.debug(`syn ${state.lsn} out`);
try {
if (ws) ws.send(s.buffer);
} catch(e) {
ws.close();
}
setTimeout(() => sync_queue(state), config.sync_timeout);
}
function push_event(state, event) {
state.lsn += 1;
switch (event.type) {
case EVENT.STROKE: {
state.queue.push({
'type': EVENT.STROKE,
'points': event.points,
'width': event.width,
'color': event.color,
});
break;
}
case EVENT.RULER: {
event.type = EVENT.STROKE;
state.queue.push(event);
break;
}
case EVENT.ERASER:
case EVENT.IMAGE:
case EVENT.IMAGE_MOVE:
case EVENT.IMAGE_SCALE:
case EVENT.UNDO:
case EVENT.REDO: {
state.queue.push(event);
break;
}
default: {
console.error('fuck');
}
}
}
// Queue an event and initialize repated sends until ACKed
function queue_event(state, event, skip = false) {
if (!state.online) { return; }
push_event(state, event);
if (skip) {
return;
}
if (state.timers.queue_sync) {
clearTimeout(state.timers.queue_sync);
}
sync_queue(state);
}
// Fire and forget. Doesn't do anything if we are offline
function fire_event(state, event) {
if (!state.online) { return; }
const s = serializer_create(4 + event_size(event));
ser_u32(s, MESSAGE.FIRE);
ser_event(s, event);
try {
if (ws) ws.send(s.buffer);
} catch(e) {
ws.close();
}
}
function predraw_event(x, y) {
return {
'type': EVENT.PREDRAW,
'x': x,
'y': y
};
}
function lift_event() {
return {
'type': EVENT.LIFT,
};
}
function color_event(color_u32) {
return {
'type': EVENT.SET_COLOR,
'color': color_u32,
};
}
function width_event(width) {
return {
'type': EVENT.SET_WIDTH,
'width': width,
};
}
function image_event(image_id, x, y, width, height) {
return {
'type': EVENT.IMAGE,
'image_id': image_id,
'x': x,
'y': y,
'width': width,
'height': height,
};
}
function image_move_event(image_id, x, y) {
return {
'type': EVENT.IMAGE_MOVE,
'image_id': image_id,
'x': x,
'y': y,
};
}
function image_scale_event(image_id, corner, x, y) {
return {
'type': EVENT.IMAGE_SCALE,
'image_id': image_id,
'corner': corner,
'x': x,
'y': y,
};
}
function stroke_event(state) {
const stroke = geometry_prepare_stroke(state);
return {
'type': EVENT.STROKE,
'points': stroke.points,
'width': stroke.width,
'color': stroke.color,
};
}
function clear_event(state) {
return {
'type': EVENT.CLEAR
};
}
function movecursor_event(x, y) {
return {
'type': EVENT.MOVE_CURSOR,
'x': x,
'y': y,
};
}
function movecanvas_event(state) {
return {
'type': EVENT.MOVE_CANVAS,
'offset_x': state.canvas.offset.x,
'offset_y': state.canvas.offset.y,
'zoom_level': state.canvas.zoom_level,
};
}
function zoomcanvas_event(state, zoom_cx, zoom_cy) {
return {
'type': EVENT.ZOOM_CANVAS,
'zoom_level': state.canvas.zoom_level,
'zoom_cx': zoom_cx,
'zoom_cy': zoom_cy,
};
}
function undo_event(state) {
return {
'type': EVENT.UNDO,
};
}
function eraser_event(stroke_id) {
return {
'type': EVENT.ERASER,
'stroke_id': stroke_id,
}
}

37
client/config.js

@ -1,37 +0,0 @@
const config = {
ws_url: `wss://${window.location.host}/ws/`,
ping_url: `https://${window.location.host}/api/ping`,
image_url: `https://${window.location.host}/images/`,
sync_timeout: 1000,
ws_reconnect_timeout: 2000,
brush_preview_timeout: 1000,
second_finger_timeout: 500,
buffer_first_touchmoves: 5,
debug_print: false,
draw_bvh: false,
draw_fullnodes: false,
zoom_delta: 0.05,
min_zoom_level: -275,
max_zoom_level: 200,
initial_offline_timeout: 1000,
default_color: 0x00,
default_width: 8,
bytes_per_instance: 4 * 2 + 4, // axy, stroke_id
bytes_per_stroke: 2 * 3 + 2, // r, g, b, width
initial_static_bytes: 4096 * 16,
initial_dynamic_bytes: 4096,
initial_wasm_bytes: 4096,
stroke_texture_size: 1024, // means no more than 1024^2 = 1M strokes in total (this is a LOT. HMH blackboard has like 80K)
dynamic_stroke_texture_size: 128, // means no more than 128^2 = 16K dynamic strokes at once
ui_texture_size: 16,
bvh_fullnode_depth: 6,
pattern_fadeout_min: 0.3,
pattern_fadeout_max: 0.75,
min_pressure: 50,
benchmark: {
zoom_level: -18,
offset: { x: 654, y: 372 },
frames: 500,
},
};

315
client/cursor.js

@ -0,0 +1,315 @@
function on_down(e) {
const x = Math.round((e.clientX + storage.canvas.offset_x) / storage.canvas.zoom);
const y = Math.round((e.clientY + storage.canvas.offset_y) / storage.canvas.zoom);
// Scroll wheel (mouse button 3)
if (e.button === 1) {
storage.state.moving = true;
storage.state.mousedown = true;
return;
}
// Right mouse button
if (e.button === 2) {
const image_hit = image_at(x, y);
activate_image(image_hit);
e.preventDefault();
return;
}
// Left mouse button
if (e.button === 0) {
const image_hit = image_at(x, y);
if (elements.active_image !== null && image_hit !== null) {
const image_id = image_hit.getAttribute('data-image-id');
const image_position = storage.images[image_id];
storage.state.moving_image = true;
storage.moving_image_original_x = image_position.x;
storage.moving_image_original_y = image_position.y;
return;
}
if (storage.state.moving) {
storage.state.mousedown = true;
return;
}
storage.state.drawing = true;
if (storage.ctx1.lineWidth !== storage.cursor.width) {
storage.ctx1.lineWidth = storage.cursor.width;
}
storage.cursor.x = x;
storage.cursor.y = y;
if (storage.tools.active === 'pencil') {
const predraw = predraw_event(x, y);
storage.current_stroke.push(predraw);
fire_event(predraw);
} else if (storage.tools.active === 'ruler') {
storage.ruler_origin.x = x;
storage.ruler_origin.y = y;
}
}
}
function on_move(e) {
const last_x = storage.cursor.x;
const last_y = storage.cursor.y;
const x = storage.cursor.x = Math.max(Math.round((e.clientX + storage.canvas.offset_x) / storage.canvas.zoom), 0);
const y = storage.cursor.y = Math.max(Math.round((e.clientY + storage.canvas.offset_y) / storage.canvas.zoom), 0);
const old_offset_x = storage.canvas.offset_x;
const old_offset_y = storage.canvas.offset_y;
if (elements.active_image && storage.state.moving_image) {
const dx = Math.round(e.movementX / storage.canvas.zoom);
const dy = Math.round(e.movementY / storage.canvas.zoom);
const image_id = elements.active_image.getAttribute('data-image-id');
const ix = storage.images[image_id].x += dx;
const iy = storage.images[image_id].y += dy;
elements.active_image.style.transform = `translate(${ix}px, ${iy}px)`;
return;
}
if (storage.state.drawing) {
if (storage.tools.active === 'pencil') {
const width = storage.cursor.width;
storage.ctx1.beginPath();
storage.ctx1.moveTo(last_x, last_y);
storage.ctx1.lineTo(x, y);
storage.ctx1.stroke();
const predraw = predraw_event(x, y);
storage.current_stroke.push(predraw);
fire_event(predraw);
} else if (storage.tools.active === 'eraser') {
const erased = strokes_intersect_line(last_x, last_y, x, y);
storage.erased.push(...erased);
if (erased.length > 0) {
for (const other_event of storage.events) {
for (const stroke_id of erased) {
if (stroke_id === other_event.stroke_id) {
if (!other_event.deleted) {
other_event.deleted = true;
const stats = stroke_stats(other_event.points, storage.cursor.width);
redraw_region(stats.bbox);
}
}
}
}
}
} else if (storage.tools.active === 'ruler') {
const old_ruler = [
{'x': storage.ruler_origin.x, 'y': storage.ruler_origin.y},
{'x': last_x, 'y': last_y}
];
const stats = stroke_stats(old_ruler, storage.cursor.width);
const bbox = stats.bbox;
storage.ctx1.clearRect(bbox.xmin, bbox.ymin, bbox.xmax - bbox.xmin, bbox.ymax - bbox.ymin);
storage.ctx1.beginPath();
storage.ctx1.moveTo(storage.ruler_origin.x, storage.ruler_origin.y);
storage.ctx1.lineTo(x, y);
storage.ctx1.stroke();
} else {
console.error('fuck');
}
} else if (storage.state.moving && storage.state.mousedown) {
storage.canvas.offset_x -= e.movementX;
storage.canvas.offset_y -= e.movementY;
if (storage.canvas.offset_x !== old_offset_x || storage.canvas.offset_y !== old_offset_y) {
move_canvas();
}
// if (storage.canvas.offset_x > storage.canvas.max_scroll_x) storage.canvas.offset_x = storage.canvas.max_scroll_x;
// if (storage.canvas.offset_x < 0) storage.canvas.offset_x = 0;
// if (storage.canvas.offset_y > storage.canvas.max_scroll_y) storage.canvas.offset_y = storage.canvas.max_scroll_y;
// if (storage.canvas.offset_y < 0) storage.canvas.offset_y = 0;
}
e.preventDefault();
}
async function on_up(e) {
if (storage.state.moving_image && e.button === 0) {
storage.state.moving_image = false;
const image_id = elements.active_image.getAttribute('data-image-id');
const position = storage.images[image_id];
// Store delta instead of new position for easy undo
const event = image_move_event(image_id, position.x - storage.moving_image_original_x, position.y - storage.moving_image_original_y);
await queue_event(event);
storage.moving_image_original_x = null;
storage.moving_image_original_y = null;
return;
}
if (storage.state.moving && (e.button === 1 || e.button === 0)) {
storage.state.mousedown = false;
if (!storage.state.spacedown) {
storage.state.moving = false;
return;
}
}
if (storage.state.drawing && e.button === 0) {
if (storage.tools.active === 'pencil') {
const event = stroke_event();
storage.current_stroke = [];
await queue_event(event);
} else if (storage.tools.active === 'eraser') {
const events = eraser_events();
storage.erased = [];
if (events.length > 0) {
for (const event of events) {
await queue_event(event);
}
}
} else if (storage.tools.active === 'ruler') {
const event = ruler_event(storage.cursor.x, storage.cursor.y);
await queue_event(event);
} else {
console.error('fuck');
}
storage.state.drawing = false;
return;
}
}
function on_keydown(e) {
if (e.code === 'Space' && !storage.state.drawing) {
storage.state.moving = true;
storage.state.spacedown = true;
return;
}
if (e.code === 'KeyZ' && e.ctrlKey) {
undo();
return;
}
}
function on_keyup(e) {
if (e.code === 'Space' && storage.state.moving) {
storage.state.moving = false;
storage.state.spacedown = false;
}
}
function on_leave(e) {
// TODO: broken when "moving"
if (storage.state.moving) {
storage.state.moving = false;
storage.state.holding = false;
return;
}
}
function on_resize(e) {
storage.canvas.max_scroll_x = storage.canvas.width - window.innerWidth;
storage.canvas.max_scroll_y = storage.canvas.height - window.innerHeight;
}
async function on_drop(e) {
e.preventDefault();
const file = e.dataTransfer.files[0];
const bitmap = await createImageBitmap(file);
const x = storage.cursor.x - Math.round(bitmap.width / 2);
const y = storage.cursor.y - Math.round(bitmap.height / 2);
// storage.ctx0.drawImage(bitmap, x, y);
const form_data = new FormData();
form_data.append('file', file);
const resp = await fetch(`/api/image?deskId=${storage.desk_id}`, {
method: 'post',
body: form_data,
})
if (resp.ok) {
const image_id = await resp.text();
const event = image_event(image_id, x, y);
await queue_event(event);
}
return false;
}
function on_wheel(e) {
const x = Math.round((e.clientX + storage.canvas.offset_x) / storage.canvas.zoom);
const y = Math.round((e.clientY + storage.canvas.offset_y) / storage.canvas.zoom);
const dz = (e.deltaY < 0 ? 0.1 : -0.1);
storage.canvas.zoom += dz;
if (storage.canvas.zoom > storage.max_zoom) {
storage.canvas.zoom = storage.max_zoom;
return;
}
if (storage.canvas.zoom < storage.min_zoom) {
storage.canvas.zoom = storage.min_zoom;
return;
}
const zoom_offset_x = Math.round(dz * x);
const zoom_offset_y = Math.round(dz * y);
storage.canvas.offset_x += zoom_offset_x;
storage.canvas.offset_y += zoom_offset_y;
move_canvas();
}
function cancel(e) {
e.preventDefault();
return false;
}
function update_brush() {
elements.brush_preview.classList.remove('dhide');
const color = elements.brush_color.value;
const width = elements.brush_width.value;
storage.cursor.color = color;
storage.cursor.width = width;
const x = Math.round(storage.cursor.x - width / 2);
const y = Math.round(storage.cursor.y - width / 2);
elements.brush_preview.style.transform = `translate(${x}px, ${y}px)`;
elements.brush_preview.style.width = width + 'px';
elements.brush_preview.style.height = width + 'px';
elements.brush_preview.style.background = color;
if (storage.timers.brush_preview) {
clearTimeout(storage.timers.brush_preview);
}
storage.timers.brush_preview = setTimeout(() => {
elements.brush_preview.classList.add('dhide');
}, 1000);
}

416
client/default.css

@ -1,104 +1,60 @@
:root {
--dark-blue: #2f343d;
--dark-hover: #888;
--radius: 5px;
--hgap: 5px;
--gap: 10px;
--transform-amimate: transform .1s ease-out;
}
html, body { html, body {
margin: 0; margin: 0;
padding: 0; padding: 0;
width: 100%;
height: 100%;
overflow: hidden; overflow: hidden;
} touch-action: none;
body .main {
height: 100%;
}
body.offline .main {
filter: brightness(50%);
} }
.dhide { .dhide {
display: none !important; display: none !important;
} }
.vhide { .canvas {
visibility: hidden !important; position: absolute;
} top: 0;
left: 0;
.flexcol { opacity: 1;
display: flex; transition: opacity .2s;
flex-direction: column; transform-origin: top left;
pointer-events: none;
} }
canvas { #toucher {
position: fixed;
width: 100%; width: 100%;
height: 100%; height: 100%;
display: block; top: 0;
cursor: url('icons/crosshair.svg') 16 16, crosshair; left: 0;
} z-index: 5; /* above all canvases, but below tools */
cursor: crosshair;
canvas.tool-pointer {
cursor: default;
}
canvas.picker {
cursor: url('icons/picker.svg') 0 19, crosshair;
}
canvas.resize-topleft {
cursor: nwse-resize;
} }
canvas.resize-topright { .canvas.white {
cursor: nesw-resize; opacity: 0;
}
/*
canvas.movemode {
cursor: grab;
} }
canvas.movemode.moving { #canvas-images {
cursor: grabbing; z-index: 0;
} }
canvas.mousemoving { #canvas0 {
cursor: move;
}
*/
.brush-dom {
position: absolute;
pointer-events: none;
user-select: none;
top: 0;
left: 0;
}
.html-hud {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
pointer-events: none;
z-index: 1; z-index: 1;
background: #eee;
background-position: 0px 0px;
background-size: 32px 32px;
background-image: radial-gradient(rgba(0, 0, 0, 0.1) 1px, transparent 10%);
} }
.html-hud .player-cursor { #canvas1 {
position: absolute; z-index: 2;
width: 16px; opacity: 0.3;
height: 16px;
} }
.tools-wrapper { .tools-wrapper {
position: fixed; position: fixed;
bottom: 0; bottom: 0;
width: 100%; width: 100%;
height: 32px;
display: flex; display: flex;
justify-content: center; justify-content: center;
align-items: end; align-items: end;
@ -106,170 +62,41 @@ canvas.mousemoving {
pointer-events: none; pointer-events: none;
} }
.pallete-wrapper, .tools {
.top-wrapper {
position: fixed;
top: 0;
left: 0;
height: 100%;
pointer-events: none;
display: flex;
flex-direction: column;
justify-content: center;
transition: var(--transform-amimate);
}
.top-wrapper {
height: unset;
width: 100%;
flex-direction: row;
}
.player-list {
position: absolute;
right: 0;
height: 42px;
padding-left: var(--gap);
padding-right: var(--gap);
background: var(--dark-blue);
display: flex;
gap: var(--gap);
align-items: center;
border-bottom-left-radius: var(--radius);
pointer-events: all;
}
.player-list .player {
width: 24px;
height: 24px;
border-radius: var(--radius);
display: flex;
justify-content: center;
align-items: center;
border: 2px solid transparent;
box-sizing: border-box;
}
.player-list .player.following {
border-color: white;
}
.player-list .player img {
height: 12px;
width: 12px;
filter: invert(100%);
}
.pallete {
pointer-events: none;
display: grid;
flex-direction: column;
align-items: center;
border-top-right-radius: var(--radius);
border-bottom-right-radius: var(--radius);
/* border-bottom-left-radius: var(--radius);*/
padding-top: var(--gap);
padding-bottom: var(--gap);
overflow: hidden;
}
.pallete-wrapper.hidden {
transform: translateX(-125%); /* to account for a selected color, which is also offset to the right */
}
.top-wrapper.hidden {
transform: translateY(-125%);
}
.pallete .color-major {
pointer-events: all;
background: var(--dark-blue);
display: flex;
transition: var(--transform-amimate);
transform: translateX(calc(-100% + 2 * var(--gap) + 24px));
cursor: pointer;
}
.pallete .color-major.extended {
transform: translateX(0px);
border-top-right-radius: var(--radius);
border-bottom-right-radius: var(--radius);
}
.pallete .color-major.active {
transform: translateX(calc(-100% + 3 * var(--gap) + 24px));
border-top-right-radius: var(--radius);
border-bottom-right-radius: var(--radius);
}
.pallete .color-minor {
padding: var(--gap);
display: flex;
justify-content: center;
align-items: center;
}
.pallete .color-major:not(.active) .color-minor:hover {
background: var(--dark-hover);
}
.pallete .color-pane {
width: 24px;
height: 24px;
box-sizing: border-box;
border-radius: var(--radius);
}
.pallete .color .color-pane:hover {
background: var(--dark-hover);
}
.pallete .color.active:hover {
background: var(--dark-blue);
}
.tools,
.sizer {
pointer-events: all; pointer-events: all;
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
background: var(--dark-blue); background: #333;
border-radius: var(--radius); border-radius: 5px;
border-bottom-right-radius: 0; border-bottom-right-radius: 0;
border-bottom-left-radius: 0; border-bottom-left-radius: 0;
height: 42px; height: 42px;
padding-left: var(--gap); padding-left: 10px;
padding-right: var(--gap); padding-right: 10px;
}
.sizer {
border-radius: 0;
border-bottom-right-radius: var(--radius);
border-bottom-left-radius: var(--radius);
} }
.tool { .tool {
cursor: pointer; cursor: pointer;
padding-left: var(--gap); padding-left: 10px;
padding-right: var(--gap); padding-right: 10px;
height: 100%; height: 100%;
display: flex; display: flex;
align-items: center; align-items: center;
background: var(--dark-blue); background: #333;
transition: var(--transform-amimate); transition: transform .1s ease-in-out;
user-select: none; user-select: none;
} }
.tool:hover { .tool:hover {
background: var(--dark-hover); background: #888;
} }
.tool.active { .tool.active {
transform: translateY(-10px); transform: translateY(-10px);
background: var(--dark-blue); border-top-right-radius: 5px;
border-top-right-radius: var(--radius); border-top-left-radius: 5px;
border-top-left-radius: var(--radius); background: #333;
} }
.tool img { .tool img {
@ -278,163 +105,18 @@ canvas.mousemoving {
filter: invert(100%); filter: invert(100%);
} }
.sizer input[type=range] { .toolbar {
-webkit-appearance: none; visibility: hidden;
width: 200px;
background: transparent;
}
.sizer input[type=range]:focus {
outline: none;
} }
.sizer input[type=range]::-webkit-slider-thumb { .floating-image {
-webkit-appearance: none;
border: none;
background: white;
height: 20px;
width: 20px;
border-radius: 50%;
cursor: pointer;
border: 2px solid var(--dark-blue);
margin-top: -6px; /* You need to specify a margin in Chrome, but in Firefox and IE it is automatic */
}
.sizer input[type=range]::-moz-range-thumb {
border: none;
background: white;
height: 16px;
width: 16px;
border-radius: 50%;
cursor: pointer;
border: 2px solid var(--dark-blue);
}
.sizer input[type=range]::-webkit-slider-runnable-track {
width: 100%;
height: 8px;
cursor: pointer;
background: white;
border-radius: 2px;
border: none;
}
.sizer input[type=range]:focus::-webkit-slider-runnable-track {
width: 100%;
height: 8px;
cursor: pointer;
background: white;
border-radius: 2px;
border: none;
}
.sizer input[type=range]::-moz-range-track {
width: 100%;
height: 8px;
cursor: pointer;
background: white;
border-radius: 2px;
border: none;
}
.phone-extra-controls {
display: none;
cursor: pointer; /* for click events on mobile */
pointer-events: all;
position: absolute;
right: 0;
background: var(--dark-blue);
height: 42px;
justify-content: center;
align-items: center;
padding-left: var(--gap);
padding-right: var(--gap);
border-top-left-radius: var(--radius);
}
.phone-extra-controls img {
height: 24px;
width: 24px;
filter: invert(100%);
}
@media (hover: none) and (pointer: coarse) {
.phone-extra-controls {
display: flex;
}
}
.offline-toast {
position: fixed;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
z-index: 999;
background: rgba(0, 0, 0, 0.8);
display: flex;
align-items: center;
padding: 10px;
border-radius: var(--radius);
box-shadow: 0px 2px 3px 0px rgba(155, 150, 100, 0.2);
transition: transform .1s ease-in-out, opacity .1s;
font-family: sans-serif;
font-size: 14px;
color: white;
font-weight: bold;
user-select: none;
transition: transform .1s ease-in-out, opacity .1s;
pointer-events: none;
}
.offline-toast.hidden {
transform: translate(-50%, -5px);
opacity: 0;
}
body.offline * {
pointer-events: none;
}
.loader {
position: fixed;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
width: 128px;
height: 128px;
pointer-events: none;
background: rgba(0, 0, 0, 0.8);
border-radius: 10px;
transition: opacity .2s;
}
.loader.hidden {
opacity: 0;
}
.debug-window {
position: absolute; position: absolute;
min-width: 320px; user-drag: none;
top: 20px;
right: 20px;
display: flex;
flex-direction: column;
gap: 10px;
user-select: none; user-select: none;
padding: 5px;
background: white;
border: 1px solid var(--dark-blue);
} }
.picker-preview-outer { .floating-image.activated {
position: absolute; outline: 5px solid #5286ff;
top: 16px; z-index: 999999 !important;
right: 16px; cursor: grab;
border: 1px solid black;
}
.picker-preview-inner {
width: 64px;
height: 64px;
border: 1px solid white;
} }

66
client/draw.js

@ -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();
}

BIN
client/favicon2.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 671 B

BIN
client/favicon_old.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 957 B

130
client/heapify.js

@ -1,130 +0,0 @@
// translated by esbuild to js. original typescript source MIT licensed at https://github.com/luciopaiva/heapify
const ROOT_INDEX = 1;
class MinQueue {
constructor(capacity = 64, keys = [], priorities = [], KeysBackingArrayType = Uint32Array, PrioritiesBackingArrayType = Uint32Array) {
this._capacity = capacity;
this._keys = new KeysBackingArrayType(capacity + ROOT_INDEX);
this._priorities = new PrioritiesBackingArrayType(capacity + ROOT_INDEX);
this._hasPoppedElement = false;
if (keys.length !== priorities.length) {
throw new Error("Number of keys does not match number of priorities provided.");
}
if (capacity < keys.length) {
throw new Error("Capacity less than number of provided keys.");
}
for (let i = 0; i < keys.length; i++) {
this._keys[i + ROOT_INDEX] = keys[i];
this._priorities[i + ROOT_INDEX] = priorities[i];
}
this.length = keys.length;
for (let i = keys.length >>> 1; i >= ROOT_INDEX; i--) {
this.bubbleDown(i);
}
}
get capacity() {
return this._capacity;
}
clear() {
this.length = 0;
this._hasPoppedElement = false;
}
bubbleUp(index) {
const key = this._keys[index];
const priority = this._priorities[index];
while (index > ROOT_INDEX) {
const parentIndex = index >>> 1;
if (this._priorities[parentIndex] <= priority) {
break;
}
this._keys[index] = this._keys[parentIndex];
this._priorities[index] = this._priorities[parentIndex];
index = parentIndex;
}
this._keys[index] = key;
this._priorities[index] = priority;
}
bubbleDown(index) {
const key = this._keys[index];
const priority = this._priorities[index];
const halfLength = ROOT_INDEX + (this.length >>> 1);
const lastIndex = this.length + ROOT_INDEX;
while (index < halfLength) {
const left = index << 1;
let childPriority = this._priorities[left];
let childKey = this._keys[left];
let childIndex = left;
const right = left + 1;
if (right < lastIndex) {
const rightPriority = this._priorities[right];
if (rightPriority < childPriority) {
childPriority = rightPriority;
childKey = this._keys[right];
childIndex = right;
}
}
if (childPriority >= priority) {
break;
}
this._keys[index] = childKey;
this._priorities[index] = childPriority;
index = childIndex;
}
this._keys[index] = key;
this._priorities[index] = priority;
}
push(key, priority) {
if (this.length === this._capacity) {
throw new Error("Heap has reached capacity, can't push new items");
}
if (this._hasPoppedElement) {
this._keys[ROOT_INDEX] = key;
this._priorities[ROOT_INDEX] = priority;
this.length++;
this.bubbleDown(ROOT_INDEX);
this._hasPoppedElement = false;
} else {
const pos = this.length + ROOT_INDEX;
this._keys[pos] = key;
this._priorities[pos] = priority;
this.length++;
this.bubbleUp(pos);
}
}
pop() {
if (this.length === 0) {
return void 0;
}
this.removePoppedElement();
this.length--;
this._hasPoppedElement = true;
return this._keys[ROOT_INDEX];
}
peekPriority() {
this.removePoppedElement();
return this._priorities[ROOT_INDEX];
}
peek() {
this.removePoppedElement();
return this._keys[ROOT_INDEX];
}
removePoppedElement() {
if (this._hasPoppedElement) {
this._keys[ROOT_INDEX] = this._keys[this.length + ROOT_INDEX];
this._priorities[ROOT_INDEX] = this._priorities[this.length + ROOT_INDEX];
this.bubbleDown(ROOT_INDEX);
this._hasPoppedElement = false;
}
}
get size() {
return this.length;
}
dumpRawPriorities() {
this.removePoppedElement();
const result = Array(this.length - ROOT_INDEX);
for (let i = 0; i < this.length; i++) {
result[i] = this._priorities[i + ROOT_INDEX];
}
return `[${result.join(" ")}]`;
}
}

8
client/icons/cheeseburga.svg

@ -1,8 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg width="7.9173mm" height="6.8706mm" version="1.1" viewBox="0 0 7.9173 6.8706" xmlns="http://www.w3.org/2000/svg">
<g transform="translate(-130.27 -83.243)" fill="none" stroke="#000" stroke-linecap="round">
<path d="m130.77 83.743h6.9173"/>
<path d="m130.77 89.613h6.9173"/>
<path d="m130.77 86.678h6.9173"/>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 371 B

281
client/icons/crosshair.svg

@ -1,281 +0,0 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
width="32"
height="32"
viewBox="0 0 32 32"
version="1.1"
id="svg5"
inkscape:version="1.2.2 (b0a8486541, 2022-12-01)"
sodipodi:docname="crosshair.svg"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:xlink="http://www.w3.org/1999/xlink"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg">
<sodipodi:namedview
id="namedview7"
pagecolor="#5fa9cd"
bordercolor="#666666"
borderopacity="1.0"
inkscape:showpageshadow="2"
inkscape:pageopacity="0"
inkscape:pagecheckerboard="0"
inkscape:deskcolor="#d1d1d1"
inkscape:document-units="px"
showgrid="true"
inkscape:zoom="19.02887"
inkscape:cx="10.011104"
inkscape:cy="17.447173"
inkscape:window-width="2558"
inkscape:window-height="1412"
inkscape:window-x="0"
inkscape:window-y="0"
inkscape:window-maximized="0"
inkscape:current-layer="layer1">
<inkscape:grid
type="xygrid"
id="grid686" />
</sodipodi:namedview>
<defs
id="defs2">
<linearGradient
id="linearGradient1226"
inkscape:swatch="solid">
<stop
style="stop-color:#ffffff;stop-opacity:1;"
offset="0"
id="stop1224" />
</linearGradient>
<linearGradient
inkscape:collect="always"
xlink:href="#linearGradient1226"
id="linearGradient1396"
gradientUnits="userSpaceOnUse"
gradientTransform="translate(7,-19)"
x1="-16.56695"
y1="8.5"
x2="40.56695"
y2="8.5" />
<linearGradient
inkscape:collect="always"
xlink:href="#linearGradient1226"
id="linearGradient1465"
gradientUnits="userSpaceOnUse"
gradientTransform="translate(7,-36)"
x1="-16.56695"
y1="8.5"
x2="40.56695"
y2="8.5" />
<linearGradient
inkscape:collect="always"
xlink:href="#linearGradient1226"
id="linearGradient1471"
gradientUnits="userSpaceOnUse"
gradientTransform="translate(-25,-36)"
x1="-16.56695"
y1="8.5"
x2="40.56695"
y2="8.5" />
<linearGradient
inkscape:collect="always"
xlink:href="#linearGradient1226"
id="linearGradient1473"
gradientUnits="userSpaceOnUse"
gradientTransform="translate(-25,-19)"
x1="-16.56695"
y1="8.5"
x2="40.56695"
y2="8.5" />
<filter
style="color-interpolation-filters:sRGB;"
inkscape:label="Drop Shadow"
id="filter2232"
x="-1.1"
y="-0.275"
width="3.2"
height="1.55">
<feFlood
flood-opacity="0.27451"
flood-color="rgb(0,0,0)"
result="flood"
id="feFlood2222" />
<feComposite
in="flood"
in2="SourceGraphic"
operator="in"
result="composite1"
id="feComposite2224" />
<feGaussianBlur
in="composite1"
stdDeviation="0.5"
result="blur"
id="feGaussianBlur2226" />
<feOffset
dx="0"
dy="0"
result="offset"
id="feOffset2228" />
<feComposite
in="SourceGraphic"
in2="offset"
operator="over"
result="composite2"
id="feComposite2230" />
</filter>
<filter
style="color-interpolation-filters:sRGB;"
inkscape:label="Drop Shadow"
id="filter2244"
x="-1.1"
y="-0.275"
width="3.2"
height="1.55">
<feFlood
flood-opacity="0.27451"
flood-color="rgb(0,0,0)"
result="flood"
id="feFlood2234" />
<feComposite
in="flood"
in2="SourceGraphic"
operator="in"
result="composite1"
id="feComposite2236" />
<feGaussianBlur
in="composite1"
stdDeviation="0.5"
result="blur"
id="feGaussianBlur2238" />
<feOffset
dx="0"
dy="0"
result="offset"
id="feOffset2240" />
<feComposite
in="SourceGraphic"
in2="offset"
operator="over"
result="composite2"
id="feComposite2242" />
</filter>
<filter
style="color-interpolation-filters:sRGB;"
inkscape:label="Drop Shadow"
id="filter2256"
x="-1.0999969"
y="-0.275"
width="3.1999937"
height="1.55">
<feFlood
flood-opacity="0.27451"
flood-color="rgb(0,0,0)"
result="flood"
id="feFlood2246" />
<feComposite
in="flood"
in2="SourceGraphic"
operator="in"
result="composite1"
id="feComposite2248" />
<feGaussianBlur
in="composite1"
stdDeviation="0.5"
result="blur"
id="feGaussianBlur2250" />
<feOffset
dx="0"
dy="0"
result="offset"
id="feOffset2252" />
<feComposite
in="SourceGraphic"
in2="offset"
operator="over"
result="composite2"
id="feComposite2254" />
</filter>
<filter
style="color-interpolation-filters:sRGB;"
inkscape:label="Drop Shadow"
id="filter2268"
x="-1.1"
y="-0.275"
width="3.2"
height="1.55">
<feFlood
flood-opacity="0.27451"
flood-color="rgb(0,0,0)"
result="flood"
id="feFlood2258" />
<feComposite
in="flood"
in2="SourceGraphic"
operator="in"
result="composite1"
id="feComposite2260" />
<feGaussianBlur
in="composite1"
stdDeviation="0.5"
result="blur"
id="feGaussianBlur2262" />
<feOffset
dx="0"
dy="0"
result="offset"
id="feOffset2264" />
<feComposite
in="SourceGraphic"
in2="offset"
operator="over"
result="composite2"
id="feComposite2266" />
</filter>
</defs>
<g
inkscape:label="Layer 1"
inkscape:groupmode="layer"
id="layer1">
<rect
style="fill:#000000;fill-opacity:1;stroke:url(#linearGradient1396);stroke-width:2;stroke-linecap:round;stroke-linejoin:round;stroke-dasharray:none;paint-order:stroke fill markers;filter:url(#filter2232)"
id="rect1390"
width="2"
height="8"
x="15"
y="-12"
rx="0"
ry="0"
transform="rotate(90)" />
<rect
style="fill:#000000;fill-opacity:1;stroke:url(#linearGradient1465);stroke-width:2;stroke-linecap:round;stroke-linejoin:round;stroke-dasharray:none;paint-order:stroke fill markers;filter:url(#filter2268)"
id="rect1463"
width="2"
height="8"
x="15"
y="-28"
rx="0"
ry="0"
transform="rotate(90)" />
<rect
style="fill:#000000;fill-opacity:1;stroke:url(#linearGradient1473);stroke-width:2;stroke-linecap:round;stroke-linejoin:round;stroke-dasharray:none;paint-order:stroke fill markers;filter:url(#filter2244)"
id="rect1467"
width="2"
height="8"
x="-17"
y="-12"
rx="0"
ry="0"
transform="scale(-1)" />
<rect
style="fill:#000000;fill-opacity:1;stroke:url(#linearGradient1471);stroke-width:2;stroke-linecap:round;stroke-linejoin:round;stroke-dasharray:none;paint-order:stroke fill markers;filter:url(#filter2256)"
id="rect1469"
width="2.0000057"
height="8"
x="-17"
y="-28"
rx="0"
ry="0"
transform="scale(-1)" />
</g>
</svg>

Before

Width:  |  Height:  |  Size: 7.6 KiB

26
client/icons/cursor.svg

@ -1,26 +0,0 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
width="16"
height="16"
viewBox="0 0 16 16"
version="1.1"
id="svg5"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg">
<g id="layer1">
<circle
style="fill:none;stroke:#ffffff;stroke-width:1"
id="circle1000"
cy="128.53627"
cx="135.63414"
r="1.5875" />
<circle
style="fill:none;stroke:#000000;stroke-width:0.25"
id="path236"
cy="128.53627"
cx="135.63414"
r="1.5875" />
</g>
</svg>

Before

Width:  |  Height:  |  Size: 624 B

54
client/icons/draw.svg

@ -0,0 +1,54 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
width="29.716999mm"
height="30.635mm"
viewBox="0 0 29.716999 30.635"
version="1.1"
id="svg5"
inkscape:version="1.2.2 (b0a8486541, 2022-12-01)"
sodipodi:docname="draw.svg"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg">
<sodipodi:namedview
id="namedview7"
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1.0"
inkscape:showpageshadow="2"
inkscape:pageopacity="0.0"
inkscape:pagecheckerboard="0"
inkscape:deskcolor="#d1d1d1"
inkscape:document-units="mm"
showgrid="false"
inkscape:zoom="6.7277216"
inkscape:cx="45.334813"
inkscape:cy="49.273739"
inkscape:window-width="2558"
inkscape:window-height="1413"
inkscape:window-x="0"
inkscape:window-y="0"
inkscape:window-maximized="1"
inkscape:current-layer="layer1" />
<defs
id="defs2" />
<g
inkscape:label="Layer 1"
inkscape:groupmode="layer"
id="layer1"
transform="translate(-74.769935,-147.21149)">
<path
style="fill:#000000;fill-opacity:1;stroke:#000000;stroke-width:1;stroke-linecap:round;stroke-linejoin:round;stroke-dasharray:none;stroke-opacity:1"
d="m 95.762931,151.36667 -1.459957,1.45995 -4.171293,-4.17129 -7.480518,7.48052 1.501665,1.50166 6.020569,-6.02056 2.669628,2.66962 -3.164437,3.16444 5.325352,5.32535 6.08434,-6.08434 z"
id="path1057"
sodipodi:nodetypes="ccccccccccc" />
<path
style="fill:#000000;fill-opacity:1;stroke:#000000;stroke-width:1;stroke-linecap:round;stroke-linejoin:round;stroke-dasharray:none;stroke-opacity:1"
d="m 87.28337,159.84623 5.401522,5.40152 -10.91227,11.04308 H 76.3711 v -5.53233 z"
id="path1785"
sodipodi:nodetypes="cccccc" />
</g>
</svg>

After

Width:  |  Height:  |  Size: 2.0 KiB

77
client/icons/erase.svg

@ -1,7 +1,74 @@
<?xml version="1.0" encoding="UTF-8"?> <?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg width="18.311mm" height="13.564mm" version="1.1" viewBox="0 0 18.311 13.564" xmlns="http://www.w3.org/2000/svg"> <!-- Created with Inkscape (http://www.inkscape.org/) -->
<g transform="translate(-71.864 -97.21)" stroke="#000" stroke-linecap="round">
<path d="m80.965 98.272c-2.6801 2.6801-5.3603 5.3603-8.0404 8.0404-0.30998 0.30998-0.31669 0.8191-0.015 1.1371l2.1333 2.2489a1.847 1.847 23.256 0 0 1.34 0.57587h3.418a1.9091 1.9091 157.42 0 0 1.3535-0.56275l5.6101-5.64c0.30916-0.3108 0.30849-0.81404-0.0015-1.124-1.5585-1.5585-3.117-3.117-4.6756-4.6756-0.30998-0.30998-0.81255-0.30998-1.1225 0z" stroke-linejoin="round"/> <svg
<path d="m85.793 110.02h3.6314" fill="none" stroke-width="1.5"/> width="18.310799mm"
height="13.563546mm"
viewBox="0 0 18.310798 13.563546"
version="1.1"
id="svg4722"
sodipodi:docname="erase.svg"
inkscape:version="1.2.2 (b0a8486541, 2022-12-01)"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg">
<sodipodi:namedview
id="namedview4724"
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1.0"
inkscape:showpageshadow="2"
inkscape:pageopacity="0.0"
inkscape:pagecheckerboard="0"
inkscape:deskcolor="#d1d1d1"
inkscape:document-units="mm"
showgrid="false"
inkscape:zoom="9.5144352"
inkscape:cx="25.697794"
inkscape:cy="26.223312"
inkscape:window-width="2558"
inkscape:window-height="1413"
inkscape:window-x="0"
inkscape:window-y="0"
inkscape:window-maximized="1"
inkscape:current-layer="layer1" />
<defs
id="defs4719">
<inkscape:path-effect
effect="fillet_chamfer"
id="path-effect26362"
is_visible="true"
lpeversion="1"
nodesatellites_param="F,0,0,1,0,0.79375,0,1 @ F,0,0,1,0,0.79375,0,1 @ F,0,0,1,0,0.79375,0,1 @ F,0,0,1,0,0.79375,0,1 @ F,0,0,1,0,0.79375,0,1"
unit="px"
method="auto"
mode="F"
radius="3"
chamfer_steps="1"
flexible="false"
use_knot_distance="true"
apply_no_radius="true"
apply_with_radius="true"
only_selected="false"
hide_knots="false" />
</defs>
<g
inkscape:label="Layer 1"
inkscape:groupmode="layer"
id="layer1"
transform="translate(-71.863816,-97.210284)">
<path
style="fill:#000000;fill-opacity:1;stroke:#000000;stroke-width:1;stroke-linecap:round;stroke-linejoin:round;stroke-dasharray:none;stroke-opacity:1"
d="m 80.965463,98.271529 c -2.680135,2.680131 -5.360269,5.360271 -8.040401,8.040401 -0.309979,0.30998 -0.316694,0.8191 -0.015,1.13714 l 2.133265,2.24889 a 1.8470002,1.8470002 23.255659 0 0 1.340018,0.57587 h 3.418044 a 1.909119,1.909119 157.42404 0 0 1.353526,-0.56275 l 5.610134,-5.63997 c 0.309156,-0.3108 0.308489,-0.81404 -0.0015,-1.12402 -1.55852,-1.55852 -3.117043,-3.11704 -4.675566,-4.675561 -0.309979,-0.309979 -0.812554,-0.309979 -1.122532,0 z"
id="path5932"
sodipodi:nodetypes="cccccc"
inkscape:path-effect="#path-effect26362"
inkscape:original-d="m 81.526729,97.710263 c -3.054312,3.054307 -6.108624,6.108627 -9.162933,9.162937 l 3.225801,3.40063 h 5.005544 l 6.729686,-6.76547 c -1.932697,-1.9327 -3.865398,-3.865399 -5.798098,-5.798097 z" />
<path
style="fill:none;stroke:#000000;stroke-width:1.5;stroke-linecap:round;stroke-linejoin:miter;stroke-dasharray:none;stroke-opacity:1"
d="m 85.793244,110.02383 h 3.63137"
id="path6432"
sodipodi:nodetypes="cc" />
</g> </g>
</svg> </svg>

Before

Width:  |  Height:  |  Size: 691 B

After

Width:  |  Height:  |  Size: 3.0 KiB

52
client/icons/favicon.svg

@ -1,7 +1,49 @@
<?xml version="1.0" encoding="UTF-8"?> <?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg width="10mm" height="10mm" version="1.1" viewBox="0 0 10 10" xmlns="http://www.w3.org/2000/svg"> <!-- Created with Inkscape (http://www.inkscape.org/) -->
<g transform="translate(-102.36 -119.41)" stroke-linecap="round" stroke-linejoin="round">
<rect x="102.36" y="119.41" width="10" height="10" ry="1.5851" fill="#fff" style="paint-order:stroke fill markers"/> <svg
<path d="m103.62 121.96s1.6506-1.3139 2.1706-0.75183c0.70073 0.75735-1.9619 2.1034-1.2369 2.8375 1.544 1.5634 4.5064-4.0972 6.0752-2.5586 1.5903 1.5596-3.9105 4.5061-2.4732 6.2076 0.64599 0.76473 2.9364-0.62957 2.9364-0.62957" fill="none" stroke="#000" stroke-width=".87212"/> width="10mm"
height="10mm"
viewBox="0 0 9.9999997 10"
version="1.1"
id="svg31118"
inkscape:version="1.2.2 (b0a8486541, 2022-12-01)"
sodipodi:docname="favicon.svg"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg">
<sodipodi:namedview
id="namedview31120"
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1.0"
inkscape:showpageshadow="2"
inkscape:pageopacity="0.0"
inkscape:pagecheckerboard="0"
inkscape:deskcolor="#d1d1d1"
inkscape:document-units="mm"
showgrid="false"
inkscape:zoom="19.02887"
inkscape:cx="21.730139"
inkscape:cy="19.62807"
inkscape:window-width="2558"
inkscape:window-height="1413"
inkscape:window-x="0"
inkscape:window-y="0"
inkscape:window-maximized="1"
inkscape:current-layer="layer1" />
<defs
id="defs31115" />
<g
inkscape:label="Layer 1"
inkscape:groupmode="layer"
id="layer1"
transform="translate(-102.35789,-119.40706)">
<path
style="fill:none;stroke:#000000;stroke-width:1;stroke-linecap:round;stroke-linejoin:round;stroke-dasharray:none;stroke-opacity:1"
d="m 103.07394,121.49803 c 0,0 1.8926,-1.50652 2.48887,-0.86207 0.80348,0.8684 -2.24958,2.41185 -1.41823,3.25361 1.77046,1.79265 5.16721,-4.69796 6.96607,-2.9338 1.82345,1.78827 -4.48394,5.16682 -2.83582,7.11786 0.74072,0.87686 3.36701,-0.72189 3.36701,-0.72189"
id="path33421"
sodipodi:nodetypes="caaaac" />
</g> </g>
</svg> </svg>

Before

Width:  |  Height:  |  Size: 643 B

After

Width:  |  Height:  |  Size: 1.7 KiB

7
client/icons/pen.svg

@ -1,7 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg width="29.717mm" height="30.635mm" version="1.1" viewBox="0 0 29.717 30.635" xmlns="http://www.w3.org/2000/svg">
<g transform="translate(-74.77 -147.21)" stroke="#000" stroke-linecap="round" stroke-linejoin="round">
<path d="m95.763 151.37-1.46 1.46-4.1713-4.1713-7.4805 7.4805 1.5017 1.5017 6.0206-6.0206 2.6696 2.6696-3.1644 3.1644 5.3254 5.3254 6.0843-6.0843z"/>
<path d="m87.283 159.85 5.4015 5.4015-10.912 11.043h-5.4015v-5.5323z"/>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 500 B

2
client/icons/perfect-bullet.svg

@ -1,2 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg width="85" height="40" version="1.1" viewBox="0 0 85 40" xml:space="preserve" xmlns="http://www.w3.org/2000/svg"><g transform="translate(-15,-10)" fill="#5286ff"><path d="m15 10v40h10v-15h10v15h40v-40h-40v15h-10v-15z"/><path d="m80 10v40h5v-5h5v-5h5v-5h5v-10h-5v-5h-5v-5h-5v-5z"/></g></svg>

Before

Width:  |  Height:  |  Size: 335 B

105
client/icons/picker.svg

@ -1,105 +0,0 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
width="20"
height="20"
viewBox="0 0 20 20"
version="1.1"
id="svg5"
inkscape:version="1.2.2 (b0a8486541, 2022-12-01)"
sodipodi:docname="picker.svg"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg">
<sodipodi:namedview
id="namedview7"
pagecolor="#41a7d4"
bordercolor="#666666"
borderopacity="1.0"
inkscape:showpageshadow="2"
inkscape:pageopacity="0"
inkscape:pagecheckerboard="0"
inkscape:deskcolor="#d1d1d1"
inkscape:document-units="mm"
showgrid="true"
inkscape:zoom="32"
inkscape:cx="7.765625"
inkscape:cy="9.0625"
inkscape:window-width="2558"
inkscape:window-height="1412"
inkscape:window-x="0"
inkscape:window-y="0"
inkscape:window-maximized="0"
inkscape:current-layer="layer1">
<inkscape:grid
type="xygrid"
id="grid686"
originx="-0.050020602"
originy="0.069140606" />
</sodipodi:namedview>
<defs
id="defs2" />
<g
inkscape:label="Layer 1"
inkscape:groupmode="layer"
id="layer1"
transform="translate(-0.0500206,0.06914061)">
<g
id="g2699"
style="stroke:#ffffff;stroke-width:3.64724409;stroke-dasharray:none;stroke-opacity:1"
transform="translate(-0.2273805,0.5521655)">
<path
id="path2684"
style="stroke:#ffffff;stroke-width:3.64724409;stroke-linecap:round;stroke-linejoin:round;stroke-dasharray:none;stroke-opacity:1;paint-order:stroke fill markers"
d="m 17.60468,5.2109617 c -0.867205,0.8672044 -2.273222,0.8672038 -3.140427,-1e-6 -0.867206,-0.8672056 -0.867206,-2.2732231 -2e-6,-3.1404278 0.867205,-0.8672047 2.273222,-0.867204 3.140428,1.7e-6 0.867205,0.8672048 0.867206,2.2732222 10e-7,3.1404271 z"
sodipodi:nodetypes="sssss" />
<path
id="path2686"
style="stroke:#ffffff;stroke-width:3.64724409;stroke-linecap:round;stroke-linejoin:round;stroke-dasharray:none;stroke-opacity:1;paint-order:stroke fill markers"
d="m 14.408181,2.2057099 3.062213,3.0622128 -3.09249,3.09249 -3.062213,-3.0622128 z"
sodipodi:nodetypes="ccccc" />
<path
id="path2688"
style="fill:#ffffff;fill-opacity:1;stroke:#ffffff;stroke-width:3.64724409;stroke-linecap:round;stroke-linejoin:round;stroke-dasharray:none;stroke-opacity:1;paint-order:stroke fill markers"
d="M 10.736761,5.9572145 13.718,8.9384545 6.4778464,16.178608 H 4.7742805 l -1.2776741,1.277674 -1.277674,-1.277674 1.2776743,-1.277675 -2e-7,-1.703565 z"
sodipodi:nodetypes="ccccccccc" />
<path
style="fill:#000000;fill-opacity:1;stroke:#ffffff;stroke-width:3.64724409;stroke-linecap:round;stroke-linejoin:round;stroke-dasharray:none;stroke-opacity:1"
d="m 4.7703397,13.904873 v 1 h 1.0000004 l 3.0000002,-3 -1.0000003,-1 z"
id="path2690"
sodipodi:nodetypes="cccccc" />
<path
id="path2692"
style="stroke:#ffffff;stroke-width:3.64724409;stroke-linecap:round;stroke-linejoin:round;stroke-dasharray:none;stroke-opacity:1;paint-order:stroke fill markers"
d="m 10.71679,4.0190773 4.939346,4.939346 c 0.34205,0.3420501 0.471794,0.7630421 0.290907,0.9439294 l -0.163256,0.1632553 c -0.180887,0.180888 -0.601879,0.05114 -0.94393,-0.2909063 L 9.9005113,4.8353552 C 9.5584616,4.4933056 9.4287171,4.0723131 9.6096043,3.8914257 L 9.7728599,3.7281701 C 9.9537473,3.5472831 10.37474,3.6770276 10.71679,4.0190773 Z"
sodipodi:nodetypes="sssssssss" />
</g>
<path
id="path1222"
style="stroke:#000000;stroke-width:1.48129;stroke-linecap:round;stroke-linejoin:round;stroke-dasharray:none;stroke-opacity:1;paint-order:stroke fill markers"
d="m 17.3773,5.7631272 c -0.867205,0.8672044 -2.273222,0.8672038 -3.140427,-10e-7 -0.867206,-0.8672056 -0.867206,-2.2732231 -2e-6,-3.1404278 0.867205,-0.8672047 2.273222,-0.867204 3.140428,1.7e-6 0.867205,0.8672048 0.867206,2.2732222 1e-6,3.1404271 z"
sodipodi:nodetypes="sssss" />
<path
id="rect1224"
style="stroke:#000000;stroke-width:1.5919;stroke-linecap:round;stroke-linejoin:round;stroke-dasharray:none;stroke-opacity:1;paint-order:stroke fill markers"
d="m 14.180801,2.7578754 3.062213,3.0622128 -3.09249,3.09249 -3.062213,-3.0622128 z"
sodipodi:nodetypes="ccccc" />
<path
id="rect3408"
style="fill:#ffffff;fill-opacity:1;stroke:#000000;stroke-width:1.70457;stroke-linecap:round;stroke-linejoin:round;stroke-dasharray:none;paint-order:stroke fill markers"
d="M 10.509381,6.50938 13.49062,9.49062 6.2504659,16.730773 H 4.5469 l -1.2776741,1.277674 -1.277674,-1.277674 1.2776743,-1.277675 -2e-7,-1.703565 z"
sodipodi:nodetypes="ccccccccc" />
<path
style="fill:#000000;fill-opacity:1;stroke:#000000;stroke-width:0.42656;stroke-linecap:round;stroke-linejoin:round;stroke-dasharray:none;stroke-opacity:1"
d="m 4.5429592,14.457038 v 1 h 1.0000004 l 3.0000002,-3 -1.0000003,-1 z"
id="path10295"
sodipodi:nodetypes="cccccc" />
<path
id="rect2678"
style="stroke:#000000;stroke-width:1.19462;stroke-linecap:round;stroke-linejoin:round;stroke-dasharray:none;stroke-opacity:1;paint-order:stroke fill markers"
d="m 10.489409,4.5712428 4.939347,4.939346 c 0.34205,0.3420501 0.471794,0.7630422 0.290907,0.9439292 l -0.163256,0.163255 c -0.180887,0.180888 -0.601879,0.05114 -0.94393,-0.290906 L 9.6731308,5.3875207 C 9.3310811,5.0454711 9.2013366,4.6244786 9.3822238,4.4435912 L 9.5454794,4.2803356 c 0.1808874,-0.180887 0.6018796,-0.051142 0.9439296,0.2909072 z"
sodipodi:nodetypes="sssssssss" />
</g>
</svg>

Before

Width:  |  Height:  |  Size: 5.8 KiB

54
client/icons/player-cursor.svg

@ -1,54 +0,0 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
width="23"
height="27"
viewBox="0 0 23 27"
version="1.1"
id="svg5"
sodipodi:docname="player-cursor.svg"
inkscape:version="1.2.2 (b0a8486541, 2022-12-01)"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg">
<sodipodi:namedview
id="namedview7"
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1.0"
inkscape:showpageshadow="2"
inkscape:pageopacity="0.0"
inkscape:pagecheckerboard="0"
inkscape:deskcolor="#d1d1d1"
inkscape:document-units="px"
showgrid="true"
inkscape:zoom="26.910887"
inkscape:cx="10.962106"
inkscape:cy="13.488965"
inkscape:window-width="2558"
inkscape:window-height="1412"
inkscape:window-x="0"
inkscape:window-y="0"
inkscape:window-maximized="0"
inkscape:current-layer="layer1">
<inkscape:grid
type="xygrid"
id="grid256"
originx="-4.5000348"
originy="-4.5000845" />
</sodipodi:namedview>
<defs
id="defs2" />
<g
inkscape:label="Layer 1"
inkscape:groupmode="layer"
id="layer1">
<path
style="fill:#ff0000;fill-opacity:1;stroke:#ffffff;stroke-width:2;stroke-linecap:round;stroke-linejoin:round;stroke-dasharray:none;stroke-opacity:1"
d="m 1.4999652,25.999915 9.9999998,-24.9999995 10,24.9999995 c -9.905078,-3.040471 -10,-3 -19.9999998,0 z"
id="path371"
sodipodi:nodetypes="cccc" />
</g>
</svg>

Before

Width:  |  Height:  |  Size: 1.7 KiB

57
client/icons/player.svg

@ -1,57 +0,0 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
width="16"
height="16"
viewBox="0 0 16 16"
version="1.1"
id="svg5"
sodipodi:docname="player.svg"
inkscape:version="1.2.2 (b0a8486541, 2022-12-01)"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg">
<sodipodi:namedview
id="namedview7"
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1.0"
inkscape:showpageshadow="2"
inkscape:pageopacity="0.0"
inkscape:pagecheckerboard="0"
inkscape:deskcolor="#d1d1d1"
inkscape:document-units="px"
showgrid="true"
inkscape:zoom="53.821773"
inkscape:cx="10.070274"
inkscape:cy="8.3237689"
inkscape:window-width="2558"
inkscape:window-height="1412"
inkscape:window-x="0"
inkscape:window-y="0"
inkscape:window-maximized="0"
inkscape:current-layer="layer1">
<inkscape:grid
type="xygrid"
id="grid132" />
</sodipodi:namedview>
<defs
id="defs2" />
<g
inkscape:label="Layer 1"
inkscape:groupmode="layer"
id="layer1">
<path
id="path186"
style="fill:#000000;fill-opacity:1;stroke:none;stroke-width:2;stroke-linecap:round;stroke-linejoin:round;paint-order:stroke fill markers"
d="M 11.03125 8.609375 A 4 4 0 0 1 8 10 A 4 4 0 0 1 4.9746094 8.6152344 A 8 8 0 0 0 0 16 L 8 16 L 16 16 A 8 8 0 0 0 11.03125 8.609375 z " />
<circle
style="fill:#000000;fill-opacity:1;stroke:none;stroke-width:2;stroke-linecap:round;stroke-linejoin:round;stroke-dasharray:none;stroke-opacity:1;paint-order:stroke fill markers"
id="circle1073"
cx="8"
cy="4"
r="4" />
</g>
</svg>

Before

Width:  |  Height:  |  Size: 1.9 KiB

52
client/icons/pointer.svg

@ -1,52 +0,0 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
width="32"
height="32"
viewBox="0 0 8.4666665 8.4666666"
version="1.1"
id="svg5"
sodipodi:docname="pointer.svg"
inkscape:version="1.2.2 (b0a8486541, 2022-12-01)"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg">
<sodipodi:namedview
id="namedview7"
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1.0"
inkscape:showpageshadow="2"
inkscape:pageopacity="0.0"
inkscape:pagecheckerboard="0"
inkscape:deskcolor="#d1d1d1"
inkscape:document-units="px"
showgrid="true"
inkscape:zoom="13.455443"
inkscape:cx="4.2733635"
inkscape:cy="26.086097"
inkscape:window-width="2558"
inkscape:window-height="1412"
inkscape:window-x="0"
inkscape:window-y="0"
inkscape:window-maximized="0"
inkscape:current-layer="layer1">
<inkscape:grid
type="xygrid"
id="grid1775" />
</sodipodi:namedview>
<defs
id="defs2" />
<g
inkscape:label="Layer 1"
inkscape:groupmode="layer"
id="layer1">
<path
style="fill:#000000;fill-opacity:1;stroke:#000000;stroke-width:0.264583px;stroke-linecap:round;stroke-linejoin:round;stroke-opacity:1"
d="M 2.5420359,7.0728549 2.0552699,0.9380846 7.1247537,4.4270217 5.0270833,5.0270833 6.0854165,6.8601704 5.1688732,7.3893369 4.1105397,5.5562497 Z"
id="path1887"
sodipodi:nodetypes="cccccccc" />
</g>
</svg>

Before

Width:  |  Height:  |  Size: 1.7 KiB

54
client/icons/redo.svg

@ -0,0 +1,54 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
width="7.3109999mm"
height="7.8540001mm"
viewBox="0 0 7.3109999 7.8540001"
version="1.1"
id="svg14027"
sodipodi:docname="redo.svg"
inkscape:version="1.2.2 (b0a8486541, 2022-12-01)"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg">
<sodipodi:namedview
id="namedview14029"
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1.0"
inkscape:showpageshadow="2"
inkscape:pageopacity="0.0"
inkscape:pagecheckerboard="0"
inkscape:deskcolor="#d1d1d1"
inkscape:document-units="mm"
showgrid="false"
inkscape:zoom="26.910887"
inkscape:cx="11.129325"
inkscape:cy="18.7099"
inkscape:window-width="2558"
inkscape:window-height="1413"
inkscape:window-x="0"
inkscape:window-y="0"
inkscape:window-maximized="1"
inkscape:current-layer="layer1" />
<defs
id="defs14024" />
<g
inkscape:label="Layer 1"
inkscape:groupmode="layer"
id="layer1"
transform="translate(-119.35516,-62.667286)">
<path
style="fill:none;stroke:#000000;stroke-width:1;stroke-linecap:round;stroke-linejoin:round;stroke-dasharray:none;stroke-opacity:1"
d="m 125.71554,68.856558 h -2.53401 c -2.08565,0 -2.77743,-0.237506 -2.77743,-1.919928 0,-1.682421 0.59993,-1.919928 2.77743,-1.919928 h 2.53401"
id="path14513"
sodipodi:nodetypes="cszsc" />
<path
style="fill:none;stroke:#000000;stroke-width:1;stroke-linecap:round;stroke-linejoin:round;stroke-dasharray:none;stroke-opacity:1"
d="m 124.57539,63.936869 1.14015,1.079833 -1.13791,1.077584"
id="path14515"
sodipodi:nodetypes="ccc" />
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.9 KiB

51
client/icons/ruler.svg

@ -1,6 +1,49 @@
<?xml version="1.0" encoding="UTF-8"?> <?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg width="27.692mm" height="27.692mm" version="1.1" viewBox="0 0 27.692 27.692" xmlns="http://www.w3.org/2000/svg"> <!-- Created with Inkscape (http://www.inkscape.org/) -->
<g transform="translate(-71.56 -125.16)">
<path d="m90.428 127.33-16.699 16.7 6.6554 6.6554 3.2929-3.2929-2.909-2.909 1.4142-1.4142 2.909 2.909 2.762-2.762-2.909-2.909 1.414-1.414 2.909 2.909 2.76-2.76-2.9105-2.9105 1.414-1.414 2.9105 2.9105 3.6424-3.6424z" stroke="#000" stroke-linecap="round" stroke-linejoin="round"/> <svg
width="27.691999mm"
height="27.691999mm"
viewBox="0 0 27.691999 27.691999"
version="1.1"
id="svg10490"
sodipodi:docname="ruler.svg"
inkscape:version="1.2.2 (b0a8486541, 2022-12-01)"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg">
<sodipodi:namedview
id="namedview10492"
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1.0"
inkscape:showpageshadow="2"
inkscape:pageopacity="0.0"
inkscape:pagecheckerboard="0"
inkscape:deskcolor="#d1d1d1"
inkscape:document-units="mm"
showgrid="false"
inkscape:zoom="9.5144352"
inkscape:cx="50.922623"
inkscape:cy="49.976692"
inkscape:window-width="2558"
inkscape:window-height="1413"
inkscape:window-x="0"
inkscape:window-y="0"
inkscape:window-maximized="1"
inkscape:current-layer="layer1" />
<defs
id="defs10487" />
<g
inkscape:label="Layer 1"
inkscape:groupmode="layer"
id="layer1"
transform="translate(-71.559908,-125.1583)">
<path
id="path10611"
style="fill:#000000;fill-opacity:1;stroke:#000000;stroke-width:1;stroke-linecap:round;stroke-linejoin:round;stroke-dasharray:none;stroke-opacity:1"
d="m 90.427939,127.32683 -16.699494,16.6995 6.65543,6.65543 2.08721,-2.08721 1.205669,-1.20567 -2.90901,-2.90901 1.414211,-1.41421 2.90901,2.90901 1.555266,-1.55527 1.206692,-1.20669 -2.90901,-2.90901 1.414008,-1.41401 2.90901,2.90901 1.554079,-1.55408 1.205965,-1.20596 -2.91047,-2.91048 1.414007,-1.414 2.910471,2.91047 1.555178,-1.55518 2.087209,-2.08721 z"
sodipodi:nodetypes="ccccccccccccccccccccc" />
</g> </g>
</svg> </svg>

Before

Width:  |  Height:  |  Size: 494 B

After

Width:  |  Height:  |  Size: 1.9 KiB

57
client/icons/undo.svg

@ -1,7 +1,54 @@
<?xml version="1.0" encoding="UTF-8"?> <?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg width="7.311mm" height="7.854mm" version="1.1" viewBox="0 0 7.311 7.854" xmlns="http://www.w3.org/2000/svg"> <!-- Created with Inkscape (http://www.inkscape.org/) -->
<g transform="translate(-119.36 -62.667)" fill="none" stroke="#000" stroke-linecap="round" stroke-linejoin="round">
<path d="m120.4 68.857h2.534c2.0856 0 2.7774-0.23751 2.7774-1.9199 0-1.6824-0.59993-1.9199-2.7774-1.9199h-2.534"/> <svg
<path d="m121.54 63.937-1.1402 1.0798 1.1379 1.0776"/> width="7.3109999mm"
height="7.8540001mm"
viewBox="0 0 7.3109999 7.8540001"
version="1.1"
id="svg14027"
sodipodi:docname="undo.svg"
inkscape:version="1.2.2 (b0a8486541, 2022-12-01)"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg">
<sodipodi:namedview
id="namedview14029"
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1.0"
inkscape:showpageshadow="2"
inkscape:pageopacity="0.0"
inkscape:pagecheckerboard="0"
inkscape:deskcolor="#d1d1d1"
inkscape:document-units="mm"
showgrid="false"
inkscape:zoom="26.910887"
inkscape:cx="11.129325"
inkscape:cy="18.7099"
inkscape:window-width="2558"
inkscape:window-height="1413"
inkscape:window-x="0"
inkscape:window-y="0"
inkscape:window-maximized="1"
inkscape:current-layer="layer1" />
<defs
id="defs14024" />
<g
inkscape:label="Layer 1"
inkscape:groupmode="layer"
id="layer1"
transform="translate(-119.35516,-62.667286)">
<path
style="fill:none;stroke:#000000;stroke-width:1;stroke-linecap:round;stroke-linejoin:round;stroke-dasharray:none;stroke-opacity:1"
d="m 120.4041,68.856558 h 2.53401 c 2.08565,0 2.77743,-0.237506 2.77743,-1.919928 0,-1.682421 -0.59993,-1.919928 -2.77743,-1.919928 h -2.53401"
id="path14513"
sodipodi:nodetypes="cszsc" />
<path
style="fill:none;stroke:#000000;stroke-width:1;stroke-linecap:round;stroke-linejoin:round;stroke-dasharray:none;stroke-opacity:1"
d="m 121.54425,63.936869 -1.14015,1.079833 1.13791,1.077584"
id="path14515"
sodipodi:nodetypes="ccc" />
</g> </g>
</svg> </svg>

Before

Width:  |  Height:  |  Size: 457 B

After

Width:  |  Height:  |  Size: 1.9 KiB

253
client/index.html

@ -3,255 +3,42 @@
<head> <head>
<meta charset="utf-8"> <meta charset="utf-8">
<title>Desk</title> <title>Desk</title>
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no"> <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
<link rel="shortcut icon" href="icons/favicon.svg" id="favicon"> <link rel="shortcut icon" href="icons/favicon.svg" id="favicon">
<link rel="stylesheet" type="text/css" href="default.css"> <link rel="stylesheet" type="text/css" href="default.css?v=6">
<link rel="stylesheet" type="text/css" href="touch.css?v=4">
<!-- <link rel="preload" href="icons/picker.svg" as="image" type="image/svg+xml" /> --> <script type="text/javascript" src="index.js?v=10"></script>
<script type="text/javascript" src="cursor.js?v=5"></script>
<script type="text/javascript" src="aux.js"></script> <script type="text/javascript" src="touch.js?v=20"></script>
<script type="text/javascript" src="heapify.js"></script> <script type="text/javascript" src="websocket.js?v=6"></script>
<script type="text/javascript" src="bvh.js"></script> <script type="text/javascript" src="send.js?v=5"></script>
<script type="text/javascript" src="math.js"></script> <script type="text/javascript" src="recv.js?v=5"></script>
<script type="text/javascript" src="tools.js"></script> <script type="text/javascript" src="math.js?v=5"></script>
<script type="text/javascript" src="speed.js"></script> <script type="text/javascript" src="draw.js?v=5"></script>
<script type="text/javascript" src="webgl_geometry.js"></script> <script type="text/javascript" src="tools.js?v=6"></script>
<script type="text/javascript" src="webgl_shaders.js"></script>
<script type="text/javascript" src="webgl_listeners.js"></script>
<script type="text/javascript" src="webgl_draw.js"></script>
<script type="text/javascript" src="undo.js"></script>
<script type="text/javascript" src="config.js"></script>
<script type="text/javascript" src="index.js"></script>
<script type="text/javascript" src="client_send.js"></script>
<script type="text/javascript" src="client_recv.js"></script>
<script type="text/javascript" src="websocket.js"></script>
</head> </head>
<body> <body>
<div class="main"> <div class="toolbar">
<canvas id="c"></canvas> <input type="color" id="brush-color">
<div class="html-hud"></div> <input type="number" min="1" id="brush-width">
<div class="brush-dom"></div>
<div class="debug-window dhide">
<div id="debug-stats" class="flexcol"></div>
<div class="debug-timings flexcol">
<div class="cpu"></div>
<div class="gpu"></div>
</div>
<label><input type="checkbox" id="debug-red">Simple shader</label>
<label><input type="checkbox" id="do-snap">Snap to grid</label>
<label><input type="checkbox" id="debug-print">Debug print</label>
<label><input type="checkbox" id="draw-bvh">Show BVH</label>
<button id="debug-begin-benchmark" title="Do not forget to enable recording in your browser!">Benchmark</button>
</div>
<div class="picker-preview-outer dhide">
<div class="picker-preview-inner">
</div>
</div>
<div class="top-wrapper">
<div class="topleft"></div>
<div class="sizer">
<input type="range" class="slider" id="stroke-width" min="0.01" step="0.01" max="64">
</div>
<div class="player-list vhide"></div>
</div>
<div class="pallete-wrapper">
<!-- Open Color is MIT licensed: https://github.com/yeun/open-color -->
<div class="pallete">
<div class="color-major" style="border-top-right-radius: var(--radius);">
<div class="color-minor" data-color="f1f3f5"><div class="color-pane" style="background: #f1f3f5;"></div></div>
<div class="color-minor" data-color="e9ecef"><div class="color-pane" style="background: #e9ecef;"></div></div>
<div class="color-minor" data-color="dee2e6"><div class="color-pane" style="background: #dee2e6;"></div></div>
<div class="color-minor" data-color="ced4da"><div class="color-pane" style="background: #ced4da;"></div></div>
<div class="color-minor" data-color="adb5bd"><div class="color-pane" style="background: #adb5bd;"></div></div>
<div class="color-minor" data-color="868e96"><div class="color-pane" style="background: #868e96;"></div></div>
<div class="color-minor" data-color="495057"><div class="color-pane" style="background: #495057;"></div></div>
<div class="color-minor" data-color="343a40"><div class="color-pane" style="background: #343a40;"></div></div>
<div class="color-minor" data-color="212529"><div class="color-pane" style="background: #212529;"></div></div>
<div class="color-minor" data-color="000000"><div class="color-pane" style="background: #000000;"></div></div>
</div>
<div class="color-major">
<div class="color-minor" data-color="fff5f5"><div class="color-pane" style="background: #fff5f5;"></div></div>
<div class="color-minor" data-color="ffe3e3"><div class="color-pane" style="background: #ffe3e3;"></div></div>
<div class="color-minor" data-color="ffc9c9"><div class="color-pane" style="background: #ffc9c9;"></div></div>
<div class="color-minor" data-color="ffa8a8"><div class="color-pane" style="background: #ffa8a8;"></div></div>
<div class="color-minor" data-color="ff8787"><div class="color-pane" style="background: #ff8787;"></div></div>
<div class="color-minor" data-color="ff6b6b"><div class="color-pane" style="background: #ff6b6b;"></div></div>
<div class="color-minor" data-color="fa5252"><div class="color-pane" style="background: #fa5252;"></div></div>
<div class="color-minor" data-color="f03e3e"><div class="color-pane" style="background: #f03e3e;"></div></div>
<div class="color-minor" data-color="e03131"><div class="color-pane" style="background: #e03131;"></div></div>
<div class="color-minor" data-color="c92a2a"><div class="color-pane" style="background: #c92a2a;"></div></div>
</div>
<div class="color-major">
<div class="color-minor" data-color="fff0f6"><div class="color-pane" style="background: #fff0f6;"></div></div>
<div class="color-minor" data-color="ffdeeb"><div class="color-pane" style="background: #ffdeeb;"></div></div>
<div class="color-minor" data-color="fcc2d7"><div class="color-pane" style="background: #fcc2d7;"></div></div>
<div class="color-minor" data-color="faa2c1"><div class="color-pane" style="background: #faa2c1;"></div></div>
<div class="color-minor" data-color="f783ac"><div class="color-pane" style="background: #f783ac;"></div></div>
<div class="color-minor" data-color="f06595"><div class="color-pane" style="background: #f06595;"></div></div>
<div class="color-minor" data-color="e64980"><div class="color-pane" style="background: #e64980;"></div></div>
<div class="color-minor" data-color="d6336c"><div class="color-pane" style="background: #d6336c;"></div></div>
<div class="color-minor" data-color="c2255c"><div class="color-pane" style="background: #c2255c;"></div></div>
<div class="color-minor" data-color="a61e4d"><div class="color-pane" style="background: #a61e4d;"></div></div>
</div>
<div class="color-major">
<div class="color-minor" data-color="f8f0fc"><div class="color-pane" style="background: #f8f0fc;"></div></div>
<div class="color-minor" data-color="f3d9fa"><div class="color-pane" style="background: #f3d9fa;"></div></div>
<div class="color-minor" data-color="eebefa"><div class="color-pane" style="background: #eebefa;"></div></div>
<div class="color-minor" data-color="e599f7"><div class="color-pane" style="background: #e599f7;"></div></div>
<div class="color-minor" data-color="da77f2"><div class="color-pane" style="background: #da77f2;"></div></div>
<div class="color-minor" data-color="cc5de8"><div class="color-pane" style="background: #cc5de8;"></div></div>
<div class="color-minor" data-color="be4bdb"><div class="color-pane" style="background: #be4bdb;"></div></div>
<div class="color-minor" data-color="ae3ec9"><div class="color-pane" style="background: #ae3ec9;"></div></div>
<div class="color-minor" data-color="9c36b5"><div class="color-pane" style="background: #9c36b5;"></div></div>
<div class="color-minor" data-color="862e9c"><div class="color-pane" style="background: #862e9c;"></div></div>
</div>
<div class="color-major">
<div class="color-minor" data-color="f3f0ff"><div class="color-pane" style="background: #f3f0ff;"></div></div>
<div class="color-minor" data-color="e5dbff"><div class="color-pane" style="background: #e5dbff;"></div></div>
<div class="color-minor" data-color="d0bfff"><div class="color-pane" style="background: #d0bfff;"></div></div>
<div class="color-minor" data-color="b197fc"><div class="color-pane" style="background: #b197fc;"></div></div>
<div class="color-minor" data-color="9775fa"><div class="color-pane" style="background: #9775fa;"></div></div>
<div class="color-minor" data-color="845ef7"><div class="color-pane" style="background: #845ef7;"></div></div>
<div class="color-minor" data-color="7950f2"><div class="color-pane" style="background: #7950f2;"></div></div>
<div class="color-minor" data-color="7048e8"><div class="color-pane" style="background: #7048e8;"></div></div>
<div class="color-minor" data-color="6741d9"><div class="color-pane" style="background: #6741d9;"></div></div>
<div class="color-minor" data-color="5f3dc4"><div class="color-pane" style="background: #5f3dc4;"></div></div>
</div>
<div class="color-major">
<div class="color-minor" data-color="edf2ff"><div class="color-pane" style="background: #edf2ff;"></div></div>
<div class="color-minor" data-color="dbe4ff"><div class="color-pane" style="background: #dbe4ff;"></div></div>
<div class="color-minor" data-color="bac8ff"><div class="color-pane" style="background: #bac8ff;"></div></div>
<div class="color-minor" data-color="91a7ff"><div class="color-pane" style="background: #91a7ff;"></div></div>
<div class="color-minor" data-color="748ffc"><div class="color-pane" style="background: #748ffc;"></div></div>
<div class="color-minor" data-color="5c7cfa"><div class="color-pane" style="background: #5c7cfa;"></div></div>
<div class="color-minor" data-color="4c6ef5"><div class="color-pane" style="background: #4c6ef5;"></div></div>
<div class="color-minor" data-color="4263eb"><div class="color-pane" style="background: #4263eb;"></div></div>
<div class="color-minor" data-color="3b5bdb"><div class="color-pane" style="background: #3b5bdb;"></div></div>
<div class="color-minor" data-color="364fc7"><div class="color-pane" style="background: #364fc7;"></div></div>
</div>
<div class="color-major">
<div class="color-minor" data-color="e7f5ff"><div class="color-pane" style="background: #e7f5ff;"></div></div>
<div class="color-minor" data-color="d0ebff"><div class="color-pane" style="background: #d0ebff;"></div></div>
<div class="color-minor" data-color="a5d8ff"><div class="color-pane" style="background: #a5d8ff;"></div></div>
<div class="color-minor" data-color="74c0fc"><div class="color-pane" style="background: #74c0fc;"></div></div>
<div class="color-minor" data-color="4dabf7"><div class="color-pane" style="background: #4dabf7;"></div></div>
<div class="color-minor" data-color="339af0"><div class="color-pane" style="background: #339af0;"></div></div>
<div class="color-minor" data-color="228be6"><div class="color-pane" style="background: #228be6;"></div></div>
<div class="color-minor" data-color="1c7ed6"><div class="color-pane" style="background: #1c7ed6;"></div></div>
<div class="color-minor" data-color="1971c2"><div class="color-pane" style="background: #1971c2;"></div></div>
<div class="color-minor" data-color="1864ab"><div class="color-pane" style="background: #1864ab;"></div></div>
</div>
<div class="color-major">
<div class="color-minor" data-color="e3fafc"><div class="color-pane" style="background: #e3fafc;"></div></div>
<div class="color-minor" data-color="c5f6fa"><div class="color-pane" style="background: #c5f6fa;"></div></div>
<div class="color-minor" data-color="99e9f2"><div class="color-pane" style="background: #99e9f2;"></div></div>
<div class="color-minor" data-color="66d9e8"><div class="color-pane" style="background: #66d9e8;"></div></div>
<div class="color-minor" data-color="3bc9db"><div class="color-pane" style="background: #3bc9db;"></div></div>
<div class="color-minor" data-color="22b8cf"><div class="color-pane" style="background: #22b8cf;"></div></div>
<div class="color-minor" data-color="15aabf"><div class="color-pane" style="background: #15aabf;"></div></div>
<div class="color-minor" data-color="1098ad"><div class="color-pane" style="background: #1098ad;"></div></div>
<div class="color-minor" data-color="0c8599"><div class="color-pane" style="background: #0c8599;"></div></div>
<div class="color-minor" data-color="0b7285"><div class="color-pane" style="background: #0b7285;"></div></div>
</div>
<div class="color-major">
<div class="color-minor" data-color="e6fcf5"><div class="color-pane" style="background: #e6fcf5;"></div></div>
<div class="color-minor" data-color="c3fae8"><div class="color-pane" style="background: #c3fae8;"></div></div>
<div class="color-minor" data-color="96f2d7"><div class="color-pane" style="background: #96f2d7;"></div></div>
<div class="color-minor" data-color="63e6be"><div class="color-pane" style="background: #63e6be;"></div></div>
<div class="color-minor" data-color="38d9a9"><div class="color-pane" style="background: #38d9a9;"></div></div>
<div class="color-minor" data-color="20c997"><div class="color-pane" style="background: #20c997;"></div></div>
<div class="color-minor" data-color="12b886"><div class="color-pane" style="background: #12b886;"></div></div>
<div class="color-minor" data-color="0ca678"><div class="color-pane" style="background: #0ca678;"></div></div>
<div class="color-minor" data-color="099268"><div class="color-pane" style="background: #099268;"></div></div>
<div class="color-minor" data-color="087f5b"><div class="color-pane" style="background: #087f5b;"></div></div>
</div>
<div class="color-major">
<div class="color-minor" data-color="ebfbee"><div class="color-pane" style="background: #ebfbee;"></div></div>
<div class="color-minor" data-color="d3f9d8"><div class="color-pane" style="background: #d3f9d8;"></div></div>
<div class="color-minor" data-color="b2f2bb"><div class="color-pane" style="background: #b2f2bb;"></div></div>
<div class="color-minor" data-color="8ce99a"><div class="color-pane" style="background: #8ce99a;"></div></div>
<div class="color-minor" data-color="69db7c"><div class="color-pane" style="background: #69db7c;"></div></div>
<div class="color-minor" data-color="51cf66"><div class="color-pane" style="background: #51cf66;"></div></div>
<div class="color-minor" data-color="40c057"><div class="color-pane" style="background: #40c057;"></div></div>
<div class="color-minor" data-color="37b24d"><div class="color-pane" style="background: #37b24d;"></div></div>
<div class="color-minor" data-color="2f9e44"><div class="color-pane" style="background: #2f9e44;"></div></div>
<div class="color-minor" data-color="2b8a3e"><div class="color-pane" style="background: #2b8a3e;"></div></div>
</div>
<div class="color-major">
<div class="color-minor" data-color="f4fce3"><div class="color-pane" style="background: #f4fce3;"></div></div>
<div class="color-minor" data-color="e9fac8"><div class="color-pane" style="background: #e9fac8;"></div></div>
<div class="color-minor" data-color="d8f5a2"><div class="color-pane" style="background: #d8f5a2;"></div></div>
<div class="color-minor" data-color="c0eb75"><div class="color-pane" style="background: #c0eb75;"></div></div>
<div class="color-minor" data-color="a9e34b"><div class="color-pane" style="background: #a9e34b;"></div></div>
<div class="color-minor" data-color="94d82d"><div class="color-pane" style="background: #94d82d;"></div></div>
<div class="color-minor" data-color="82c91e"><div class="color-pane" style="background: #82c91e;"></div></div>
<div class="color-minor" data-color="74b816"><div class="color-pane" style="background: #74b816;"></div></div>
<div class="color-minor" data-color="66a80f"><div class="color-pane" style="background: #66a80f;"></div></div>
<div class="color-minor" data-color="5c940d"><div class="color-pane" style="background: #5c940d;"></div></div>
</div>
<div class="color-major">
<div class="color-minor" data-color="fff9db"><div class="color-pane" style="background: #fff9db;"></div></div>
<div class="color-minor" data-color="fff3bf"><div class="color-pane" style="background: #fff3bf;"></div></div>
<div class="color-minor" data-color="ffec99"><div class="color-pane" style="background: #ffec99;"></div></div>
<div class="color-minor" data-color="ffe066"><div class="color-pane" style="background: #ffe066;"></div></div>
<div class="color-minor" data-color="ffd43b"><div class="color-pane" style="background: #ffd43b;"></div></div>
<div class="color-minor" data-color="fcc419"><div class="color-pane" style="background: #fcc419;"></div></div>
<div class="color-minor" data-color="fab005"><div class="color-pane" style="background: #fab005;"></div></div>
<div class="color-minor" data-color="f59f00"><div class="color-pane" style="background: #f59f00;"></div></div>
<div class="color-minor" data-color="f08c00"><div class="color-pane" style="background: #f08c00;"></div></div>
<div class="color-minor" data-color="e67700"><div class="color-pane" style="background: #e67700;"></div></div>
</div>
<div class="color-major" style="border-bottom-right-radius: var(--radius);">
<div class="color-minor" data-color="fff4e6"><div class="color-pane" style="background: #fff4e6;"></div></div>
<div class="color-minor" data-color="ffe8cc"><div class="color-pane" style="background: #ffe8cc;"></div></div>
<div class="color-minor" data-color="ffd8a8"><div class="color-pane" style="background: #ffd8a8;"></div></div>
<div class="color-minor" data-color="ffc078"><div class="color-pane" style="background: #ffc078;"></div></div>
<div class="color-minor" data-color="ffa94d"><div class="color-pane" style="background: #ffa94d;"></div></div>
<div class="color-minor" data-color="ff922b"><div class="color-pane" style="background: #ff922b;"></div></div>
<div class="color-minor" data-color="fd7e14"><div class="color-pane" style="background: #fd7e14;"></div></div>
<div class="color-minor" data-color="f76707"><div class="color-pane" style="background: #f76707;"></div></div>
<div class="color-minor" data-color="e8590c"><div class="color-pane" style="background: #e8590c;"></div></div>
<div class="color-minor" data-color="d9480f"><div class="color-pane" style="background: #d9480f;"></div></div>
</div>
</div>
</div> </div>
<div class="tools-wrapper"> <div class="tools-wrapper">
<div class="tools"> <div class="tools">
<div class="tool" data-tool="pointer"><img draggable="false" src="icons/pointer.svg"></div> <div class="tool" data-tool="pencil"><img draggable="false" src="icons/draw.svg"></div>
<div class="tool active" data-tool="pencil"><img draggable="false" src="icons/pen.svg"></div>
<div class="tool" data-tool="ruler"><img draggable="false" src="icons/ruler.svg"></div> <div class="tool" data-tool="ruler"><img draggable="false" src="icons/ruler.svg"></div>
<div class="tool" data-tool="eraser"><img draggable="false" src="icons/erase.svg"></div> <div class="tool" data-tool="eraser"><img draggable="false" src="icons/erase.svg"></div>
<div class="tool" data-tool="undo"><img draggable="false" src="icons/undo.svg"></div> <div class="tool" data-tool="undo"><img draggable="false" src="icons/undo.svg"></div>
<!-- <div class="tool" data-tool="redo"><img draggable="false" src="icons/redo.svg"></div> --> <!-- <div class="tool" data-tool="redo"><img draggable="false" src="icons/redo.svg"></div> -->
</div> </div>
<div class="phone-extra-controls">
<img draggable="false" src="icons/cheeseburga.svg">
</div>
</div>
</div> </div>
<div class="offline-toast hidden"> <div id="brush-preview" class="dhide"></div>
Whiteboard offline
</div>
<div class="loader"> <canvas class="canvas white" id="canvas0"></canvas>
<svg width="128" height="128" xmlns="http://www.w3.org/2000/svg"> <canvas class="canvas" id="canvas1"></canvas>
<!-- <circle cx="64" cy="64" r="32" fill="none" stroke="black" opacity="0.5" stroke-width="3" stroke-linecap="round"/> --> <div class="canvas" id="canvas-images"></div>
<path id="spinner-path" fill="none" stroke="#aaaaaa" stroke-width="3" stroke-linecap="round"/>
</svg>
</div>
<div id="toucher"></div>
</body> </body>
</html> </html>

464
client/index.js

@ -1,28 +1,16 @@
let ws = null;
let ls = window.localStorage;
document.addEventListener('DOMContentLoaded', main); document.addEventListener('DOMContentLoaded', main);
const EVENT = Object.freeze({ const EVENT = Object.freeze({
PREDRAW: 10, PREDRAW: 10,
SET_COLOR: 11,
SET_WIDTH: 12,
CLEAR: 13, // clear predraw events from me (because I started a pan instead of drawing)
MOVE_CURSOR: 14,
LIFT: 15,
LEAVE: 16,
MOVE_CANVAS: 17,
USER_JOINED: 18,
ZOOM_CANVAS: 19,
STROKE: 20, STROKE: 20,
RULER: 21, // gets re-written with EVENT.STROKE before sending to server RULER: 21, /* gets re-written with EVENT.STROKE before sending to server */
UNDO: 30, UNDO: 30,
REDO: 31, REDO: 31,
IMAGE: 40, IMAGE: 40,
IMAGE_MOVE: 41, IMAGE_MOVE: 41,
IMAGE_SCALE: 42,
ERASER: 50, ERASER: 50,
}); });
@ -33,236 +21,326 @@ const MESSAGE = Object.freeze({
FULL: 103, FULL: 103,
FIRE: 104, FIRE: 104,
JOIN: 105, JOIN: 105,
FOLLOW: 106,
}); });
// Source: const config = {
// https://stackoverflow.com/a/18473154 ws_url: 'ws://192.168.100.2/ws/',
function polarToCartesian(centerX, centerY, radius, angleInDegrees) { image_url: 'http://192.168.100.2/images/',
var angleInRadians = (angleInDegrees-90) * Math.PI / 180.0; sync_timeout: 1000,
ws_reconnect_timeout: 2000,
return { second_finger_timeout: 500,
x: centerX + (radius * Math.cos(angleInRadians)), buffer_first_touchmoves: 5,
y: centerY + (radius * Math.sin(angleInRadians)) debug_print: false,
}; };
}
const storage = {
function describeArc(x, y, radius, startAngle, endAngle) { 'state': {
var start = polarToCartesian(x, y, radius, endAngle); 'drawing': false,
var end = polarToCartesian(x, y, radius, startAngle); 'moving': false,
'moving_image': false,
'mousedown': false,
'spacedown': false,
},
var largeArcFlag = (Math.abs(endAngle - startAngle) % 360) <= 180 ? "0" : "1"; 'moving_image_original_x': null,
'moving_image_original_y': null,
var d = [ 'touch': {
"M", start.x, start.y, 'moves': 0,
"A", radius, radius, 0, largeArcFlag, 0, end.x, end.y 'drawing': false,
].join(" "); 'moving': false,
'waiting_for_second_finger': false,
'position': { 'x': null, 'y': null },
'screen_position': { 'x': null, 'y': null },
'finger_distance': null,
'buffered': [],
'ids': [],
},
return d; 'tools': {
} 'active': null,
'active_element': null,
},
let iii = 0; 'ruler_origin': {},
let a_angel = 0; 'erased': [],
let b_angel = 180; 'predraw': {},
let speed_a = 2; 'timers': {},
let speed_b = 6; 'me': {},
let b_fast = true;
function start_spinner(state) {
const str = describeArc(64, 64, 32, a_angel, b_angel);
4
a_angel += speed_a;
b_angel += speed_b;
const diff = b_angel - a_angel;
if (diff > 320) {
speed_a = 6;
speed_b = 2;
} else if (diff < 40) {
speed_a = 2;
speed_b = 6;
}
// if ((speed_a === 1) && Math.abs(a_angel - b_angel) % 360 < 90) { '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)
// speed_a = 3; 'server_lsn': 0, // what SERVER said LSN is (used to decide how much stuff to SYN)
// speed_b = 1; 'lsn': 0, // what actual LSN is (can't just use length of local queue because it gets cleared)
// } else if (Math.abs(a_angel - b_angel) % 360 > 180) { 'queue': [], // to server
// speed_a = 1; 'events': [], // from server
// speed_b = 3; 'current_stroke': [],
// }
document.querySelector('#spinner-path').setAttribute('d', str); 'desk_id': 123,
if (!state.online) { 'max_zoom': 4,
window.requestAnimationFrame(() => start_spinner(state)); 'min_zoom': 0.2,
} else {
document.querySelector('.loader').classList.add('hidden');
}
}
async function main() { 'images': {},
const state = {
'online': false,
'me': null,
'canvas': { 'canvas': {
'offset': { 'x': 0, 'y': 0 }, 'zoom': 1,
'zoom_level': 0, 'width': 1500,
'zoom': 1.0, 'height': 4000,
'target_zoom': 1.0, 'offset_x': 0,
'zoom_screenp': {'x': 0, 'y': 0}, 'offset_y': 0,
}, },
'cursor': { 'cursor': {
'width': 8,
'color': 'rgb(0, 0, 0)',
'x': 0, 'x': 0,
'y': 0, 'y': 0,
}, }
};
const elements = {
'cursor': null,
'canvas0': null,
'canvas1': null,
'active_image': null,
};
'sn': 0, function event_size(event) {
'lsn': 0, let size = 1 + 1; // type + padding
'server_lsn': 0,
'touch': { switch (event.type) {
'moves': 0, case EVENT.PREDRAW: {
'drawing': false, size += 2 * 2;
'moving': false, break;
'erasing': false, }
'waiting_for_second_finger': false,
'first_finger_position': null,
'second_finger_position': null,
'buffered': [],
'ids': [],
},
'moving': false, case EVENT.STROKE: {
'drawing': false, size += 4 + 2 + 2 + 4 + event.points.length * 2 * 2; // u32 stroke id + u16 (count) + u16 (width) + u32 (color + count * (u16, u16) points
'erasing': false, break;
'spacedown': false, }
'colorpicking': false,
'zooming': false,
'zoomdown': false,
'imagemoving': false,
'imagescaling': false,
'linedrawing': false,
'active_image': null, case EVENT.UNDO:
'scaling_corner': null, case EVENT.REDO: {
'ruler_origin': null, break;
'image_actually_moved': false, }
'current_strokes': {},
'rdp_mask': new Uint8Array(1024),
'rdp_traverse_stack': new Uint32Array(4096),
'queue': [],
'events': [],
'stroke_count': 0,
'starting_index': 0,
'total_points': 0,
'bvh': {
'nodes': [],
'root': null,
'pqueue': new MinQueue(1024),
'traverse_stack': tv_create(Uint32Array, 1024),
},
'tools': { case EVENT.IMAGE:
'active': null, case EVENT.IMAGE_MOVE: {
'active_element': null, size += 4 + 2 + 2; // file id + x + y
}, break;
}
'colors': { case EVENT.ERASER: {
'active_element': null, size += 4; // stroke id
'extended_element': null, break;
}, }
'timers': { default: {
'hide_preview': null, console.error('fuck');
'offline_toast': null, }
'raf': false, }
},
'players': {}, return size;
}
'debug': { function move_canvas() {
'red': false, elements.canvas0.style.transform = `translate(${-storage.canvas.offset_x}px, ${-storage.canvas.offset_y}px) scale(${storage.canvas.zoom})`;
'render_from': 0, elements.canvas1.style.transform = `translate(${-storage.canvas.offset_x}px, ${-storage.canvas.offset_y}px) scale(${storage.canvas.zoom})`;
'render_to': 0, 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;
}
'rdp_cache': {}, 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;
}
'stats': {}, elements.canvas1.classList.add('disabled');
'following_player': null,
'color_picked': null, if (elements.active_image) {
if (elements.active_image === item) {
return;
}
'wasm': {}, elements.active_image.classList.remove('activated');
}
elements.active_image = item;
'background_pattern': 'dots', item.classList.add('activated');
}
'erase_candidates': tv_create(Uint32Array, 4096), function predraw_event(x, y) {
return {
'type': EVENT.PREDRAW,
'x': x,
'y': y
};
}
'snap': null, 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));
const context = { return {
'canvas': null, 'type': EVENT.RULER,
'gl': null, 'points': points,
'width': storage.cursor.width,
'color': color_to_u32(storage.cursor.color),
};
}
'programs': {}, function undo_event() {
'buffers': {}, return { 'type': EVENT.UNDO };
'locations': {}, }
'textures': {},
'images': [],
'dynamic_serializer': serializer_create(config.initial_dynamic_bytes), function redo_event() {
'dynamic_index_serializer': serializer_create(config.initial_dynamic_bytes), return { 'type': EVENT.REDO };
}
'clipped_indices': tv_create(Uint32Array, 4096), function image_event(image_id, x, y) {
return {
'type': EVENT.IMAGE,
'image_id': image_id,
'x': x,
'y': y,
}
}
'instance_data_points': tv_create(Float32Array, 4096), function image_move_event(image_id, x, y) {
'instance_data_ids': tv_create(Uint32Array, 4096), return {
'instance_data_pressures': tv_create(Uint8Array, 4096), 'type': EVENT.IMAGE_MOVE,
'image_id': image_id,
'x': x,
'y': y,
}
}
'dynamic_instance_points': tv_create(Float32Array, 4096), function eraser_events() {
'dynamic_instance_pressure': tv_create(Uint8Array, 4096), const result = [];
'dynamic_instance_ids': tv_create(Uint32Array, 4096),
'stroke_data': serializer_create(config.initial_static_bytes), for (const stroke_id of storage.erased) {
'dynamic_stroke_data': serializer_create(config.initial_static_bytes), result.push({
'type': EVENT.ERASER,
'stroke_id': stroke_id,
});
}
'dynamic_stroke_count': 0, return result;
'dynamic_segment_count': 0, }
'bgcolor': {'r': 1.0, 'g': 1.0, 'b': 1.0}, // 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;
}
}
'gpu_timer_ext': null, return null;
'last_frame_ts': 0, }
'last_frame_dt': 0,
};
load_player_cursor_template(state); function queue_undo() {
start_spinner(state); const event = undo_event();
queue_event(event);
}
function main() {
const url = new URL(window.location.href); const url = new URL(window.location.href);
const parts = url.pathname.split('/'); const parts = url.pathname.split('/');
state.desk_id = parts.length > 0 ? parts[parts.length - 1] : 0; 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;
await init_wasm(state); storage.ctx1.lineJoin = storage.ctx1.lineCap = storage.ctx0.lineJoin = storage.ctx0.lineCap = 'round';
storage.ctx1.lineWidth = storage.ctx0.lineWidth = storage.cursor.width;
init_webgl(state, context); elements.toucher.addEventListener('mousedown', on_down)
init_listeners(state, context); elements.toucher.addEventListener('mousemove', on_move)
init_tools(state); 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);
ws_connect(state, context, true); elements.toucher.addEventListener('touchstart', on_touchstart);
elements.toucher.addEventListener('touchmove', on_touchmove);
elements.toucher.addEventListener('touchend', on_touchend);
elements.toucher.addEventListener('touchcancel', on_touchend);
schedule_draw(state, context); elements.brush_color.addEventListener('input', update_brush);
elements.brush_width.addEventListener('input', update_brush);
state.timers.offline_toast = setTimeout(() => ui_offline(), config.initial_offline_timeout); elements.canvas0.addEventListener('dragover', on_move);
elements.canvas0.addEventListener('drop', on_drop);
elements.canvas0.addEventListener('mouseleave', on_leave);
} }

36
client/index.log

@ -1,36 +0,0 @@
This is pdfTeX, Version 3.141592653-2.6-1.40.24 (TeX Live 2022/Debian) (preloaded format=pdflatex 2023.4.13) 27 APR 2023 21:39
entering extended mode
restricted \write18 enabled.
%&-line parsing enabled.
**/code/desk2/client/index.js
(/code/desk2/client/index.js
LaTeX2e <2022-11-01> patch level 1
L3 programming layer <2023-01-16>
! LaTeX Error: Missing \begin{document}.
See the LaTeX manual or LaTeX Companion for explanation.
Type H <return> for immediate help.
...
l.1 d
ocument.addEventListener('DOMContentLoaded', main);
?
! Emergency stop.
...
l.1 d
ocument.addEventListener('DOMContentLoaded', main);
You're in trouble here. Try typing <return> to proceed.
If that doesn't work, type X <return> to quit.
Here is how much of TeX's memory you used:
18 strings out of 476091
566 string characters out of 5794081
1849330 words of memory out of 5000000
20499 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
13i,0n,12p,112b,14s stack positions out of 10000i,1000n,20000p,200000b,200000s
! ==> Fatal error occurred, no output PDF file produced!

47
client/lod_worker.js

@ -1,47 +0,0 @@
let thread_id = null;
let exports = null;
async function init(tid, memory, heap_base) {
thread_id = tid;
const result = await WebAssembly.instantiateStreaming(fetch('wasm/lod.wasm'), {
env: { 'memory': memory }
});
exports = result.instance.exports;
exports.set_sp(heap_base - thread_id * 16 * 4096); // 64K stack
postMessage({ 'type': 'init_done' });
}
function work(indices_base, indices_count, zoom, offsets) {
try {
exports.do_lod(
indices_base, indices_count, zoom,
offsets['coords_from'],
offsets['xs'],
offsets['ys'],
offsets['pressures'],
offsets['result_buffers'] + thread_id * 4,
offsets['result_counts'] + thread_id * 4,
);
} catch (e) {
console.error('WASM:', e);
}
postMessage({ 'type': 'lod_done' });
}
onmessage = (e) => {
const d = e.data;
if (d.type === 'init') {
init(d.thread_id, d.memory, d.heap_base);
} else if (d.type === 'lod') {
work(d.indices_base, d.indices_count, d.zoom, d.offsets);
} else {
console.error('unexpected worker command:', d.type);
}
}

481
client/math.js

@ -1,102 +1,5 @@
function round_to_pow2(value, multiple) { function rdp_find_max(points, start, end) {
return (value + multiple - 1) & -multiple; const EPS = 0.5;
}
function screen_to_canvas(state, p) {
// should be called with coordinates obtained from MouseEvent.clientX/clientY * window.devicePixelRatio
const xc = (p.x - state.canvas.offset.x) / state.canvas.zoom;
const yc = (p.y - state.canvas.offset.y) / state.canvas.zoom;
return {'x': xc, 'y': yc};
}
function canvas_to_screen(state, p) {
const xs = (p.x * state.canvas.zoom + state.canvas.offset.x) / window.devicePixelRatio;
const ys = (p.y * state.canvas.zoom + state.canvas.offset.y) / window.devicePixelRatio;
return {'x': xs, 'y': ys};
}
function process_rdp_indices_r(state, zoom, mask, stroke, start, end) {
// Looks like the recursive implementation spends most of its time in the function call overhead
// Let's try to use an explicit stack instead to give the js engine more room to play with
// Update: it's not faster. But it gives more sensible source-line samples in chrome profiler, so I'll leave it
let result = 0;
const stack = [];
stack.push({'start': start, 'end': end});
while (stack.length > 0) {
const region = stack.pop();
const max = rdp_find_max(state, zoom, stroke.coords_from, region.start, region.end);
if (max !== -1) {
mask[max] = 1;
result += 1;
stack.push({'start': region.start, 'end': max});
stack.push({'start': max, 'end': region.end});
}
}
return result;
}
function process_rdp_indices(state, zoom, stroke) {
const point_count = stroke.coords_to - stroke.coords_from;
if (state.rdp_mask.length < point_count) {
state.rdp_mask = new Uint8Array(point_count);
}
state.rdp_mask.fill(0, 0, point_count);
const mask = state.rdp_mask;
const npoints = 2 + process_rdp_indices_r(state, zoom, mask, stroke, 0, point_count - 1); // 2 is for the first and last vertex, which do not get included by the recursive functions, but should always be there at any lod level
mask[0] = 1;
mask[point_count - 1] = 1;
return npoints;
}
function exponential_smoothing(points, last, up_to) {
const alpha = 0.5;
let pr = 0;
let start = points.length - up_to;
if (start < 0) {
start = 0;
}
for (let i = start; i < points.length; ++i) {
const p = points[i];
pr = alpha * p.pressure + (1 - alpha) * pr;
}
pr = alpha * last.pressure + (1 - alpha) * pr;
return pr;
}
function process_stroke(state, zoom, stroke) {
// Try caching the highest zoom level that only returns the endpoints
if (zoom <= stroke.turns_into_straight_line_zoom) {
return 2;
}
const npoints = process_rdp_indices(state, zoom, stroke, true);
if (npoints === 2 && zoom > stroke.turns_into_straight_line_zoom) {
stroke.turns_into_straight_line_zoom = zoom;
}
return npoints;
}
function rdp_find_max2(zoom, points, start, end) {
const EPS = 0.125 / zoom;
let result = -1; let result = -1;
let max_dist = 0; let max_dist = 0;
@ -111,7 +14,7 @@ function rdp_find_max2(zoom, points, start, end) {
const sin_theta = dy / dist_ab; const sin_theta = dy / dist_ab;
const cos_theta = dx / dist_ab; const cos_theta = dx / dist_ab;
for (let i = start + 1; i < end; ++i) { for (let i = start; i < end; ++i) {
const p = points[i]; const p = points[i];
const ox = p.x - a.x; const ox = p.x - a.x;
@ -123,7 +26,7 @@ function rdp_find_max2(zoom, points, start, end) {
const x = rx + a.x; const x = rx + a.x;
const y = ry + a.y; const y = ry + a.y;
const dist = Math.abs(y - a.y) + Math.abs(a.pressure - p.pressure) / 255 + Math.abs(b.pressure - p.pressure) / 255; const dist = Math.abs(y - a.y);
if (dist > EPS && dist > max_dist) { if (dist > EPS && dist > max_dist) {
result = i; result = i;
@ -134,76 +37,125 @@ function rdp_find_max2(zoom, points, start, end) {
return result; return result;
} }
function process_rdp_r2(zoom, points, start, end) { function process_rdp_r(points, start, end) {
let result = []; let result = [];
const max = rdp_find_max2(zoom, points, start, end); const max = rdp_find_max(points, start, end);
if (max !== -1) { if (max !== -1) {
const before = process_rdp_r2(zoom, points, start, max); const before = process_rdp_r(points, start, max);
const after = process_rdp_r2(zoom, points, max, end); const after = process_rdp_r(points, max, end);
result = [...before, points[max], ...after]; result = [...before, points[max], ...after];
} }
return result; return result;
} }
function process_rdp2(zoom, points) { function process_rdp(points) {
const result = []; const result = process_rdp_r(points, 0, points.length - 1);
const stack = []; result.unshift(points[0]);
result.push(points[points.length - 1]);
return result;
}
stack.push({ function process_ewmv(points, round = false) {
'type': 0, const result = [];
'start': 0, const alpha = 0.4;
'end': points.length - 1,
});
result.push(points[0]); result.push(points[0]);
while (stack.length > 0) { for (let i = 1; i < points.length; ++i) {
const entry = stack.pop(); const p = points[i];
const x = Math.round(alpha * p.x + (1 - alpha) * result[result.length - 1].x);
const y = Math.round(alpha * p.y + (1 - alpha) * result[result.length - 1].y);
result.push({'x': x, 'y': y});
}
if (entry.type === 0) { return result;
const max = rdp_find_max2(zoom, points, entry.start, entry.end); }
if (max !== -1) { function process_stroke(points) {
stack.push({ const result0 = process_ewmv(points);
'type': 0, const result1 = process_rdp(result0, true);
'start': max, return result1;
'end': entry.end }
});
function stroke_stats(points, width) {
stack.push({ if (points.length === 0) {
'type': 1, const bbox = {
'index': max, 'xmin': 0,
}); 'ymin': 0,
'xmax': 0,
stack.push({ 'ymax': 0
'type': 0, };
'start': entry.start,
'end': max, return {
}); 'bbox': bbox,
'length': 0,
};
} }
} else {
result.push(points[entry.index]); let length = 0;
let xmin = points[0].x, ymin = points[0].y;
let xmax = xmin, ymax = ymin;
for (let i = 0; i < points.length; ++i) {
const point = points[i];
if (point.x < xmin) xmin = point.x;
if (point.y < ymin) ymin = point.y;
if (point.x > xmax) xmax = point.x;
if (point.y > ymax) ymax = point.y;
if (i > 0) {
const last = points[i - 1];
const dx = point.x - last.x;
const dy = point.y - last.y;
length += Math.sqrt(dx * dx + dy * dy);
} }
} }
result.push(points[points.length - 1]); xmin -= width;
ymin -= width;
xmax += width * 2;
ymax += width * 2;
return result; const bbox = {
'xmin': Math.floor(xmin),
'ymin': Math.floor(ymin),
'xmax': Math.ceil(xmax),
'ymax': Math.ceil(ymax)
};
return {
'bbox': bbox,
'length': length,
};
} }
// TODO: unify with regular process stroke function rectangles_intersect(a, b) {
function process_stroke2(zoom, points) { const result = (
const result = process_rdp2(zoom, points); a.xmin <= b.xmax
&& a.xmax >= b.xmin
&& a.ymin <= b.ymax
&& a.ymax >= b.ymin
);
return result; return result;
} }
function stroke_intersects_region(points, bbox) {
if (points.length === 0) {
return false;
}
const stats = stroke_stats(points, storage.cursor.width);
return rectangles_intersect(stats.bbox, bbox);
}
function color_to_u32(color_str) { function color_to_u32(color_str) {
const r = parseInt(color_str.substring(0, 2), 16); const r = parseInt(color_str.substring(1, 3), 16);
const g = parseInt(color_str.substring(2, 4), 16); const g = parseInt(color_str.substring(3, 5), 16);
const b = parseInt(color_str.substring(4, 6), 16); const b = parseInt(color_str.substring(5, 7), 16);
return (r << 16) | (g << 8) | b; return (r << 16) | (g << 8) | b;
} }
@ -224,22 +176,6 @@ function color_from_u32(color_u32) {
return '#' + r_str + g_str + b_str; return '#' + r_str + g_str + b_str;
} }
function color_from_rgbdict(color_dict) {
const r = Math.floor(color_dict.r * 255);
const g = Math.floor(color_dict.g * 255);
const b = Math.floor(color_dict.b * 255);
let r_str = r.toString(16);
let g_str = g.toString(16);
let b_str = b.toString(16);
if (r <= 0xF) r_str = '0' + r_str;
if (g <= 0xF) g_str = '0' + g_str;
if (b <= 0xF) b_str = '0' + b_str;
return '#' + r_str + g_str + b_str;
}
function ccw(A, B, C) { function ccw(A, B, C) {
return (C.y - A.y) * (B.x - A.x) > (B.y - A.y) * (C.x - A.x); return (C.y - A.y) * (B.x - A.x) > (B.y - A.y) * (C.x - A.x);
} }
@ -249,220 +185,41 @@ function segments_intersect(A, B, C, D) {
return ccw(A, C, D) != ccw(B, C, D) && ccw(A, B, C) !== ccw(A, B, D); return ccw(A, C, D) != ccw(B, C, D) && ccw(A, B, C) !== ccw(A, B, D);
} }
function dist_v2(a, b) { function strokes_intersect_line(x1, y1, x2, y2) {
const dx = a.x - b.x; const result = [];
const dy = a.y - b.y;
return Math.sqrt(dx * dx + dy * dy);
}
function mid_v2(a, b) {
return {
'x': (a.x + b.x) / 2.0,
'y': (a.y + b.y) / 2.0,
};
}
function point_in_quad(p, quad_topleft, quad_bottomright) {
if ((quad_topleft.x <= p.x && p.x < quad_bottomright.x) && (quad_topleft.y <= p.y && p.y < quad_bottomright.y)) {
return true;
}
return false;
}
function point_in_bbox(p, bbox) { for (const event of storage.events) {
if (bbox.x1 <= p.x && p.x < bbox.x2 && bbox.y1 <= p.y && p.y < bbox.y2) { if (event.type === EVENT.STROKE && !event.deleted) {
return true; if (event.points.length < 2) {
continue;
} }
return false; for (let i = 0; i < event.points.length - 1; ++i) {
} const sx1 = event.points[i].x;
const sy1 = event.points[i].y;
function clamp(v, a, b) { const sx2 = event.points[i + 1].x;
return (v < a ? a : (v > b ? b : v)); const sy2 = event.points[i + 1].y;
}
function dot(a, b) { const A = {'x': x1, 'y': y1};
return a.x * b.x + a.y * b.y; const B = {'x': x2, 'y': y2};
}
function mix(a, b, t) { const C = {'x': sx1, 'y': sy1};
return a * t + b * (1 - t); const D = {'x': sx2, 'y': sy2};
}
function point_in_stroke(p, xs, ys, pressures, width) { if (segments_intersect(A, B, C, D)) {
for (let i = 0; i < xs.length - 1; ++i) { result.push(event.stroke_id);
const ax = xs[i + 0]; break;
const bx = xs[i + 1];
const ay = ys[i + 0];
const by = ys[i + 1];
const at = pressures[i + 0] / 255 * width;
const bt = pressures[i + 1] / 255 * width;
const pa = {
'x': p.x - ax,
'y': p.y - ay,
};
const ba = {
'x': bx - ax,
'y': by - ay,
};
const h = clamp(dot(pa, ba) / dot(ba, ba), 0.0, 1.0);
const thickness = mix(at, bt, h);
const v = {
'x': p.x - (ax + ba.x * h),
'y': p.y - (ay + ba.y * h),
};
const dist = Math.sqrt(dot(v, v)) - thickness;
if (dist <= 0) {
return true;
} }
} }
return false;
}
function segment_interesects_quad(a, b, quad_topleft, quad_bottomright, quad_topright, quad_bottomleft) {
if (point_in_quad(a, quad_topleft, quad_bottomright)) {
return true;
} }
if (point_in_quad(b, quad_topleft, quad_bottomright)) {
return true;
}
if (segments_intersect(a, b, quad_topleft, quad_topright)) return true;
if (segments_intersect(a, b, quad_topright, quad_bottomright)) return true;
if (segments_intersect(a, b, quad_bottomright, quad_bottomleft)) return true;
if (segments_intersect(a, b, quad_bottomleft, quad_topleft)) return true;
return false;
}
function stroke_bbox(state, stroke) {
const radius = stroke.width; // do not divide by 2 to account for max possible pressure
const xs = state.wasm.buffers['xs'].tv.data;
const ys = state.wasm.buffers['ys'].tv.data;
let min_x = xs[stroke.coords_from] - radius;
let max_x = xs[stroke.coords_from] + radius;
let min_y = ys[stroke.coords_from] - radius;
let max_y = ys[stroke.coords_from] + radius;
for (let i = stroke.coords_from + 1; i < stroke.coords_to; ++i) {
const px = xs[i];
const py = ys[i];
min_x = Math.min(min_x, px - radius);
min_y = Math.min(min_y, py - radius);
max_x = Math.max(max_x, px + radius);
max_y = Math.max(max_y, py + radius);
} }
return {'x1': min_x, 'y1': min_y, 'x2': max_x, 'y2': max_y, 'cx': (max_x + min_x) / 2, 'cy': (max_y + min_y) / 2}; return result;
}
function quads_intersect(a, b) {
if (a.x1 < b.x2 && a.x2 > b.x1 && a.y2 > b.y1 && a.y1 < b.y2) {
return true;
}
return false;
}
function quad_fully_inside(outer, inner) {
if (outer.x1 < inner.x1 && outer.x2 > inner.x2 && outer.y1 < inner.y1 && outer.y2 > inner.y2) {
return true;
}
return false;
}
function quad_union(a, b) {
return {
'x1': Math.min(a.x1, b.x1),
'y1': Math.min(a.y1, b.y1),
'x2': Math.max(a.x2, b.x2),
'y2': Math.max(a.y2, b.y2),
};
}
function box_area(box) {
return (box.x2 - box.x1) * (box.y2 - box.y1);
}
// https://stackoverflow.com/a/47593316
function mulberry32(seed) {
let t = seed + 0x6D2B79F5;
t = Math.imul(t ^ t >>> 15, t | 1);
t ^= t + Math.imul(t ^ t >>> 7, t | 61);
return ((t ^ t >>> 14) >>> 0) / 4294967296;
}
function random_bright_color_from_seed(seed) {
const h = Math.round(mulberry32(seed) * 360);
const s = 25;
const l = 50;
return `hsl(${h}deg ${s}% ${l}%)`;
}
function dot(a, b) {
return a.x * b.x + a.y * b.y;
}
function clamp(x, a, b) {
return x < a ? a : (x > b ? b : x);
}
function length(a) {
return Math.sqrt(dot(a, a));
}
function circle_intersects_capsule(ax, ay, bx, by, p1, p2, cx, cy, r) {
// Basically the SDF computation
const pa = { 'x': cx - ax, 'y': cy - ay };
const ba = { 'x': bx - ax, 'y': by - ay };
const h = clamp(dot(pa, ba) / dot(ba, ba), 0, 1);
const in1 = length({ 'x': cx - (ax + ba.x * h), 'y': cy - (ay + ba.y * h) });
const in2 = (1 - h) * p1 + h * p2;
const dist = in1 - in2;
return dist <= r;
} }
function stroke_intersects_capsule(state, stroke, a, b, radius) { function dist_v2(a, b) {
const xs = state.wasm.buffers['xs'].tv.data; const dx = a.x - b.x;
const ys = state.wasm.buffers['ys'].tv.data; const dy = a.y - b.y;
const pressures = state.wasm.buffers['pressures'].tv.data; return Math.sqrt(dx * dx + dy * dy);
for (let i = stroke.coords_from; i < stroke.coords_to - 1; ++i) {
const x1 = xs[i + 0];
const y1 = ys[i + 0];
const x2 = xs[i + 1];
const y2 = ys[i + 1];
const p1 = pressures[i + 0];
const p2 = pressures[i + 1];
// Test if p1 or p2 overlap the capsule
if (circle_intersects_capsule(x1, y1, x2, y2, p1 * stroke.width / 255, p2 * stroke.width / 255, a.x, a.y, radius)) {
return true;
}
if (circle_intersects_capsule(x1, y1, x2, y2, p1 * stroke.width / 255, p2 * stroke.width / 255, b.x, b.y, radius)) {
return true;
}
// Lame test for the quad part, only test for line-line intersection
// TODO: actually test for rotated quad vs circle/rotated quad overlap
if (segments_intersect(a, b, {'x': x1, 'y': y1}, {'x': x2, 'y': y2})) {
return true;
}
}
return false;
} }

345
client/recv.js

@ -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;
}
}
}

201
client/send.js

@ -0,0 +1,201 @@
function serializer_create(size) {
const buffer = new ArrayBuffer(size);
return {
'offset': 0,
'size': size,
'buffer': buffer,
'view': new DataView(buffer),
'strview': new Uint8Array(buffer),
};
}
function ser_u8(s, value) {
s.view.setUint8(s.offset, value);
s.offset += 1;
}
function ser_u16(s, value) {
s.view.setUint16(s.offset, value, true);
s.offset += 2;
}
function ser_u32(s, value) {
s.view.setUint32(s.offset, value, true);
s.offset += 4;
}
function ser_event(s, event) {
ser_u8(s, event.type);
ser_u8(s, 0); // padding for 16bit alignment
switch (event.type) {
case EVENT.PREDRAW: {
ser_u16(s, event.x);
ser_u16(s, event.y);
break;
}
case EVENT.STROKE: {
ser_u16(s, event.points.length);
ser_u16(s, event.width);
ser_u32(s, event.color);
if (config.debug_print) console.debug('original', event.points);
for (const point of event.points) {
ser_u16(s, point.x);
ser_u16(s, point.y);
}
break;
}
case EVENT.IMAGE:
case EVENT.IMAGE_MOVE: {
const image_id = parseInt(event.image_id);
ser_u32(s, image_id);
ser_u16(s, event.x);
ser_u16(s, event.y);
break;
}
case EVENT.UNDO:
case EVENT.REDO: {
break;
}
case EVENT.ERASER: {
ser_u32(s, event.stroke_id);
break;
}
default: {
console.error('fuck');
}
}
}
async function send_ack(sn) {
const s = serializer_create(1 + 4);
ser_u8(s, MESSAGE.ACK);
ser_u32(s, sn);
if (config.debug_print) console.debug(`ack ${sn} out`);
try {
if (ws) await ws.send(s.buffer);
} catch(e) {
ws.close();
}
}
async function sync_queue() {
if (ws === null) {
if (config.debug_print) console.debug('socket has closed, stopping SYNs');
return;
}
let size = 1 + 1 + 4 + 4; // opcode + lsn + event count
let count = storage.lsn - storage.server_lsn;
if (count === 0) {
if (config.debug_print) console.debug('server ACKed all events, clearing queue');
storage.queue.length = 0;
return;
}
for (let i = count - 1; i >= 0; --i) {
const event = storage.queue[storage.queue.length - 1 - i];
size += event_size(event);
}
const s = serializer_create(size);
ser_u8(s, MESSAGE.SYN);
ser_u8(s, 0); // padding for 16bit alignment
ser_u32(s, storage.lsn);
ser_u32(s, count);
for (let i = count - 1; i >= 0; --i) {
const event = storage.queue[storage.queue.length - 1 - i];
ser_event(s, event);
}
if (config.debug_print) console.debug(`syn ${storage.lsn} out`);
try {
if (ws) await ws.send(s.buffer);
} catch(e) {
ws.close();
}
setTimeout(sync_queue, config.sync_timeout);
}
function push_event(event) {
storage.lsn += 1;
switch (event.type) {
case EVENT.STROKE: {
const points = process_stroke(event.points);
storage.queue.push({
'type': EVENT.STROKE,
'points': points,
'width': event.width,
'color': event.color,
});
break;
}
case EVENT.RULER: {
event.type = EVENT.STROKE;
storage.queue.push(event);
break;
}
case EVENT.ERASER:
case EVENT.IMAGE:
case EVENT.IMAGE_MOVE:
case EVENT.UNDO:
case EVENT.REDO: {
storage.queue.push(event);
break;
}
default: {
console.error('fuck');
}
}
}
// Queue an event and initialize repated sends until ACKed
function queue_event(event, skip = false) {
push_event(event);
if (skip) {
return;
}
if (storage.timers.queue_sync) {
clearTimeout(storage.timers.queue_sync);
}
sync_queue();
}
// Fire and forget. Doesn't do anything if we are offline
async function fire_event(event) {
const s = serializer_create(1 + event_size(event));
ser_u8(s, MESSAGE.FIRE);
ser_event(s, event);
try {
if (ws) await ws.send(s.buffer);
} catch(e) {
ws.close();
}
}

236
client/speed.js

@ -1,236 +0,0 @@
function worker_message(worker, message) {
return new Promise((resolve) => {
worker.onmessage = (e) => resolve(e.data);
worker.postMessage(message);
});
}
function workers_messages(workers, messages) {
const promises = [];
for (let i = 0; i < workers.length; ++i) {
promises.push(worker_message(workers[i], messages[i]));
}
return Promise.all(promises);
}
function workers_thread_message(workers, message, thread_field=null) {
const messages = [];
for (let i = 0; i < workers.length; ++i) {
if (thread_field !== null) {
const m = structuredClone(message);
m[thread_field] = i;
messages.push(m);
} else {
messages.push(message);
}
}
return workers_messages(workers, messages);
}
async function init_wasm(state) {
const memory = new WebAssembly.Memory({
initial: 16384, // F U
maximum: 16384, // 1GiB
shared: true,
});
// "Master thread" to do maintance on (static allocations, merging results etc)
const master_wasm = await WebAssembly.instantiateStreaming(fetch('wasm/lod.wasm'), {
env: { 'memory': memory }
});
const nworkers = navigator.hardwareConcurrency;
state.wasm.exports = master_wasm.instance.exports;
state.wasm.heap_base = state.wasm.exports.alloc_static(0);
state.wasm.workers = [];
state.wasm.memory = memory;
for (let i = 0; i < nworkers; ++i) {
const w = new Worker('lod_worker.js');
state.wasm.workers.push(w);
}
await workers_thread_message(state.wasm.workers, {
'type': 'init',
'heap_base': state.wasm.heap_base,
'memory': memory,
}, 'thread_id');
const initial = config.initial_wasm_bytes;
state.wasm.buffers = {
'xs': {
'used': 0,
'cap': initial
},
'ys': {
'used': 0,
'cap': initial
},
'coords_from': {
'used': 0,
'cap': initial
},
'pressures': {
'used': 0,
'cap': initial
},
};
state.wasm.buffers['xs'].offset = state.wasm.exports.alloc_static(initial);
state.wasm.buffers['ys'].offset = state.wasm.exports.alloc_static(initial);
state.wasm.buffers['pressures'].offset = state.wasm.exports.alloc_static(initial);
state.wasm.buffers['coords_from'].offset = state.wasm.exports.alloc_static(initial);
const mem = state.wasm.memory.buffer;
state.wasm.buffers['xs'].tv = tv_create_on(Float32Array, initial / 4,
mem, state.wasm.buffers['xs'].offset);
state.wasm.buffers['ys'].tv = tv_create_on(Float32Array, initial / 4,
mem, state.wasm.buffers['ys'].offset);
state.wasm.buffers['pressures'].tv = tv_create_on(Uint8Array, initial,
mem, state.wasm.buffers['pressures'].offset);
state.wasm.buffers['coords_from'].tv = tv_create_on(Uint32Array, initial / 4,
mem, state.wasm.buffers['coords_from'].offset);
tv_add(state.wasm.buffers['coords_from'].tv, 0);
state.wasm.buffers['coords_from'].used = 4;
}
function wasm_ensure_by(state, nstrokes, ncoords) {
const buffers = state.wasm.buffers;
const old_ys_offset = buffers['ys'].offset;
const old_coords_from_offset = buffers['coords_from'].offset;
const old_pressures_offset = buffers['pressures'].offset;
let realloc = false;
let coords_bytes = buffers['xs'].cap;
let stroke_bytes = buffers['coords_from'].cap;
if (buffers['xs'].used + ncoords * 4 > buffers['xs'].cap) {
coords_bytes = round_to_pow2(buffers['xs'].cap + ncoords * 4, 4096 * 16); // 1 wasm page (although it doesn't matter here)
realloc = true;
}
if (buffers['coords_from'].used + nstrokes * 4 > buffers['coords_from'].cap) {
stroke_bytes = round_to_pow2(buffers['coords_from'].cap + nstrokes * 4, 4096 * 16);
realloc = true;
}
if (realloc) {
if (config.debug_print) console.debug('WASM static data re-layout');
state.wasm.exports.free_static();
const mem = state.wasm.memory.buffer;
const memv = new Uint8Array(mem);
buffers['xs'].offset = state.wasm.exports.alloc_static(coords_bytes);
buffers['ys'].offset = state.wasm.exports.alloc_static(coords_bytes);
buffers['pressures'].offset = state.wasm.exports.alloc_static(coords_bytes);
buffers['coords_from'].offset = state.wasm.exports.alloc_static(stroke_bytes);
buffers['xs'].tv = tv_create_on(Float32Array, coords_bytes / 4, mem, buffers['xs'].offset);
buffers['ys'].tv = tv_create_on(Float32Array, coords_bytes / 4, mem, buffers['ys'].offset);
buffers['pressures'].tv = tv_create_on(Uint8Array, coords_bytes, mem, buffers['pressures'].offset);
buffers['coords_from'].tv = tv_create_on(Uint32Array, stroke_bytes / 4, mem, buffers['coords_from'].offset);
// TODO: this should have been automatic maybe?
buffers['xs'].tv.size = buffers['xs'].used / 4;
buffers['ys'].tv.size = buffers['ys'].used / 4;
buffers['pressures'].tv.size = buffers['pressures'].used;
buffers['coords_from'].tv.size = buffers['coords_from'].used / 4;
buffers['xs'].cap = buffers['ys'].cap = buffers['pressures'].cap = coords_bytes;
buffers['coords_from'].cap = stroke_bytes;
const tmp = new Uint8Array(Math.max(coords_bytes, stroke_bytes));
// Copy from back to front (otherwise we will overwrite)
tmp.set(new Uint8Array(mem, old_coords_from_offset, buffers['coords_from'].used));
memv.set(new Uint8Array(tmp.buffer, 0, buffers['coords_from'].used), buffers['coords_from'].offset);
tmp.set(new Uint8Array(mem, old_pressures_offset, buffers['pressures'].used));
memv.set(new Uint8Array(tmp.buffer, 0, buffers['pressures'].used), buffers['pressures'].offset);
tmp.set(new Uint8Array(mem, old_ys_offset, buffers['ys'].used));
memv.set(new Uint8Array(tmp.buffer, 0, buffers['ys'].used), buffers['ys'].offset);
}
}
async function do_lod(state, context) {
state.wasm.exports.free_dynamic();
const buffers = state.wasm.buffers;
const result_buffers = state.wasm.exports.alloc_dynamic(state.wasm.workers.length * 4);
const result_counts = state.wasm.exports.alloc_dynamic(state.wasm.workers.length * 4);
const clipped_indices = state.wasm.exports.alloc_dynamic(context.clipped_indices.size * 4);
const mem = new Uint8Array(state.wasm.memory.buffer);
// Dynamic input data that should (by design) never be too big
mem.set(tv_bytes(context.clipped_indices), clipped_indices);
// NOTE: this static partitioning scheme turned out to be "good enough" (i.e., trying
// to allocate approximately the same amount of points per job wasn't any faster)
const indices_per_thread = Math.floor(context.clipped_indices.size / state.wasm.workers.length);
const offsets = {
'coords_from': buffers['coords_from'].offset,
'xs': buffers['xs'].offset,
'ys': buffers['ys'].offset,
'pressures': buffers['pressures'].offset,
'result_buffers': result_buffers,
'result_counts': result_counts,
};
const jobs = [];
for (let i = 0; i < state.wasm.workers.length; ++i) {
let count = indices_per_thread;
if (i === state.wasm.workers.length - 1) {
count += context.clipped_indices.size % state.wasm.workers.length;
}
jobs.push({
'type': 'lod',
'indices_base': clipped_indices + i * 4 * indices_per_thread,
'indices_count': count,
'zoom': state.canvas.zoom,
'offsets': offsets
});
}
await workers_messages(state.wasm.workers, jobs);
const result_offset = state.wasm.exports.merge_results(
result_counts,
result_buffers,
state.wasm.workers.length
);
const segment_count = new Int32Array(state.wasm.memory.buffer, result_counts, 1)[0]; // by convention
// Use results without copying from WASM memory
const wasm_points = new Float32Array(state.wasm.memory.buffer,
result_offset, segment_count * 2);
const wasm_ids = new Uint32Array(state.wasm.memory.buffer,
result_offset + segment_count * 2 * 4, segment_count);
const wasm_pressures = new Uint8Array(state.wasm.memory.buffer,
result_offset + segment_count * 2 * 4 + segment_count * 4, segment_count);
context.instance_data_points.data = wasm_points;
context.instance_data_points.size = segment_count * 2;
context.instance_data_ids.data = wasm_ids;
context.instance_data_ids.size = segment_count;
context.instance_data_pressures.data = wasm_pressures;
context.instance_data_pressures.size = segment_count;
return segment_count;
}

175
client/tools.js

@ -1,166 +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') {
queue_event(state, undo_event(state));
return;
}
if (state.tools.active_element) {
state.tools.active_element.classList.remove('active');
}
const old_class = 'tool-' + state.tools.active;
const new_class = 'tool-' + tool;
document.querySelector('canvas').classList.remove(old_class);
state.tools.active = tool;
state.tools.active_element = item;
state.tools.active_element.classList.add('active');
document.querySelector('canvas').classList.add(new_class);
if (tool === 'pencil' || tool === 'eraser' || tool === 'ruler') {
update_cursor(state);
document.querySelector('.brush-dom').classList.remove('dhide');
} else {
document.querySelector('.brush-dom').classList.add('dhide');
}
}
function select_color(state, item, color_u32) {
if (state.colors.active_element) {
state.colors.active_element.classList.remove('active');
}
if (state.colors.extended_element) {
state.colors.extended_element.classList.remove('extended');
state.colors.extended_element = null;
} }
const last_minor = item.lastElementChild; storage.tools.active = tool;
const color_css = color_from_u32(color_u32); storage.tools.active_element = document.querySelector(`.tool[data-tool="${tool}"]`);
storage.tools.active_element.classList.add('active');
last_minor.setAttribute('data-color', color_css.substring(1));
last_minor.querySelector('.color-pane').style.background = color_css;
state.colors.active_element = item;
item.classList.add('active');
}
function extend_major_color(state, item) {
if (state.colors.active_element) {
state.colors.active_element.classList.remove('active');
}
if (state.colors.extended_element) {
state.colors.extended_element.classList.remove('extended');
}
const last_minor = item.lastElementChild;
// Restore last pane color in case it was overwritten by active color
last_minor.querySelector('.color-pane').style.background = '#' + item.getAttribute('data-last-color');
last_minor.setAttribute('data-color', item.getAttribute('data-last-color'));
state.colors.extended_element = item;
item.classList.add('extended');
} }
function set_color_u32(state, color_u32) { function tools_init() {
if (color_u32 === state.players[state.me].color) { const pencil = document.querySelector('.tool[data-tool="pencil"]');
return; const ruler = document.querySelector('.tool[data-tool="ruler"]');
} const eraser = document.querySelector('.tool[data-tool="eraser"]');
const undo = document.querySelector('.tool[data-tool="undo"]');
const color_css = color_from_u32(color_u32).substring(1);
const color_minor = document.querySelector(`.color-minor[data-color="${color_css}"]`);
if (!color_minor) {
set_color_u32(state, 0);
return;
}
const major_color = color_minor.parentElement;
select_color(state, major_color, color_u32);
state.players[state.me].color = color_u32
update_cursor(state);
fire_event(state, color_event(color_u32));
}
function switch_stroke_width(e, state) {
if (!state.online) return;
const value = parseInt(e.target.value);
state.players[state.me].width = value;
update_cursor(state);
}
function broadcast_stroke_width(e, state) {
const value = e.target.value;
fire_event(state, width_event(value));
}
function init_tools(state) {
const tools = document.querySelectorAll('.tools .tool');
const color_groups = document.querySelectorAll('.pallete .color-major');
tools.forEach((item) => { item.addEventListener('click', () => switch_tool(state, item)); });
color_groups.forEach((item) => {
item.setAttribute('data-last-color', item.lastElementChild.getAttribute('data-color'));
let longtouch_timer = null;
item.addEventListener('touchstart', (e) => {
longtouch_timer = setTimeout(() => {
extend_major_color(state, item);
}, 500);
});
item.addEventListener('touchmove', (e) => {
if (longtouch_timer) {
clearTimeout(longtouch_timer);
}
longtouch_timer = null;
});
item.addEventListener('touchend', (e) => {
if (longtouch_timer) {
clearTimeout(longtouch_timer);
}
longtouch_timer = null;
});
item.addEventListener('click', (e) => {
if (e.ctrlKey) {
extend_major_color(state, item);
return;
}
let color_element = e.target;
let target = e.target;
while (!target.classList.contains('color-minor')) {
target = target.parentElement;
}
const color_str = target.getAttribute('data-color');
const color_u32 = color_to_u32(color_str);
set_color_u32(state, color_u32);
})
});
switch_tool(state, document.querySelector('.tool[data-tool="pencil"]'));
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');
} }

359
client/touch.js

@ -0,0 +1,359 @@
function on_touchstart(e) {
e.preventDefault();
if (storage.touch.drawing) {
return;
}
// First finger(s) down?
if (storage.touch.ids.length === 0) {
// We only handle 1 and 2
if (e.changedTouches.length > 2) {
return;
}
storage.touch.ids.length = 0;
for (const touch of e.changedTouches) {
storage.touch.ids.push(touch.identifier);
}
if (e.changedTouches.length === 1) {
const touch = e.changedTouches[0];
const x = Math.round((touch.clientX + storage.canvas.offset_x) / storage.canvas.zoom);
const y = Math.round((touch.clientY + storage.canvas.offset_y) / storage.canvas.zoom);
storage.touch.position.x = x;
storage.touch.position.y = y;
// We give a bit of time to add a second finger
storage.touch.waiting_for_second_finger = true;
storage.touch.moves = 0;
storage.touch.buffered.length = 0;
storage.ruler_origin.x = x;
storage.ruler_origin.y = y;
setTimeout(() => {
storage.touch.waiting_for_second_finger = false;
}, config.second_finger_timeout);
}
return;
}
// There are touches already
if (storage.touch.waiting_for_second_finger) {
if (e.changedTouches.length === 1) {
const changed_touch = e.changedTouches[0];
storage.touch.screen_position.x = changed_touch.clientX;
storage.touch.screen_position.y = changed_touch.clientY;
storage.touch.ids.push(e.changedTouches[0].identifier);
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 x = touch.clientX;
const y = touch.clientY;
if (touch.identifier === storage.touch.ids[0]) {
first_finger_position = {'x': x, 'y': y};
}
if (touch.identifier === storage.touch.ids[1]) {
second_finger_position = {'x': x, 'y': y};
}
}
storage.touch.finger_distance = dist_v2(
first_finger_position, second_finger_position);
// console.log(storage.touch.finger_distance);
}
return;
}
}
function on_touchmove(e) {
if (storage.touch.ids.length === 1 && !storage.touch.moving) {
storage.touch.moves += 1;
if (storage.touch.moves > config.buffer_first_touchmoves) {
storage.touch.waiting_for_second_finger = false; // Immediately start drawing on move
storage.touch.drawing = true;
if (storage.ctx1.lineWidth !== storage.cursor.width) {
storage.ctx1.lineWidth = storage.cursor.width;
}
} else {
let drawing_touch = null;
for (const touch of e.changedTouches) {
if (touch.identifier === storage.touch.ids[0]) {
drawing_touch = touch;
break;
}
}
if (!drawing_touch) {
return;
}
const last_x = storage.touch.position.x;
const last_y = storage.touch.position.y;
const x = Math.max(Math.round((drawing_touch.clientX + storage.canvas.offset_x) / storage.canvas.zoom), 0);
const y = Math.max(Math.round((drawing_touch.clientY + storage.canvas.offset_y) / storage.canvas.zoom), 0);
storage.touch.buffered.push({
'last_x': last_x,
'last_y': last_y,
'x': x,
'y': y,
});
storage.touch.position.x = x;
storage.touch.position.y = y;
}
}
if (storage.touch.drawing) {
let drawing_touch = null;
for (const touch of e.changedTouches) {
if (touch.identifier === storage.touch.ids[0]) {
drawing_touch = touch;
break;
}
}
if (!drawing_touch) {
return;
}
const last_x = storage.touch.position.x;
const last_y = storage.touch.position.y;
const x = storage.touch.position.x = Math.max(Math.round((drawing_touch.clientX + storage.canvas.offset_x) / storage.canvas.zoom), 0);
const y = storage.touch.position.y = Math.max(Math.round((drawing_touch.clientY + storage.canvas.offset_y) / storage.canvas.zoom), 0);
if (storage.tools.active === 'pencil') {
if (storage.touch.buffered.length > 0) {
for (const p of storage.touch.buffered) {
storage.ctx1.beginPath();
storage.ctx1.moveTo(p.last_x, p.last_y);
storage.ctx1.lineTo(p.x, p.y);
storage.ctx1.stroke();
const predraw = predraw_event(p.x, p.y);
storage.current_stroke.push(predraw);
fire_event(predraw);
}
storage.touch.buffered.length = 0;
}
storage.ctx1.beginPath();
storage.ctx1.moveTo(last_x, last_y);
storage.ctx1.lineTo(x, y);
storage.ctx1.stroke();
const predraw = predraw_event(x, y);
storage.current_stroke.push(predraw);
fire_event(predraw);
storage.touch.position.x = x;
storage.touch.position.y = y;
return;
} else if (storage.tools.active === 'eraser') {
const erase_step = (last_x, last_y, x, y) => {
const erased = strokes_intersect_line(last_x, last_y, x, y);
storage.erased.push(...erased);
if (erased.length > 0) {
for (const other_event of storage.events) {
for (const stroke_id of erased) {
if (stroke_id === other_event.stroke_id) {
if (!other_event.deleted) {
other_event.deleted = true;
const stats = stroke_stats(other_event.points, storage.cursor.width);
redraw_region(stats.bbox);
}
}
}
}
}
};
if (storage.touch.buffered.length > 0) {
for (const p of storage.touch.buffered) {
erase_step(p.last_x, p.last_y, p.x, p.y);
}
storage.touch.buffered.length = 0;
}
erase_step(last_x, last_y, x, y);
} else if (storage.tools.active === 'ruler') {
const old_ruler = [
{'x': storage.ruler_origin.x, 'y': storage.ruler_origin.y},
{'x': last_x, 'y': last_y}
];
const stats = stroke_stats(old_ruler, storage.cursor.width);
const bbox = stats.bbox;
storage.ctx1.clearRect(bbox.xmin, bbox.ymin, bbox.xmax - bbox.xmin, bbox.ymax - bbox.ymin);
storage.ctx1.beginPath();
storage.ctx1.moveTo(storage.ruler_origin.x, storage.ruler_origin.y);
storage.ctx1.lineTo(x, y);
storage.ctx1.stroke();
} else {
console.error('fuck');
}
}
if (storage.touch.ids.length === 2) {
storage.touch.moving = true;
let first_finger_position_screen = null;
let second_finger_position_screen = null;
let first_finger_position_canvas = null;
let second_finger_position_canvas = 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 x = touch.clientX;
const y = touch.clientY;
const xc = Math.max(Math.round((touch.clientX + storage.canvas.offset_x) / storage.canvas.zoom), 0);
const yc = Math.max(Math.round((touch.clientY + storage.canvas.offset_y) / storage.canvas.zoom), 0);
if (touch.identifier === storage.touch.ids[0]) {
first_finger_position_screen = {'x': x, 'y': y};
first_finger_position_canvas = {'x': xc, 'y': yc};
}
if (touch.identifier === storage.touch.ids[1]) {
second_finger_position_screen = {'x': x, 'y': y};
second_finger_position_canvas = {'x': xc, 'y': yc};
}
}
const new_finger_distance = dist_v2(
first_finger_position_screen, second_finger_position_screen);
const zoom_center = {
'x': (first_finger_position_canvas.x + second_finger_position_canvas.x) / 2.0,
'y': (first_finger_position_canvas.y + second_finger_position_canvas.y) / 2.0
};
for (const touch of e.changedTouches) {
// The second finger to be down is considered the "main" one
// Movement of the second finger is ignored
if (touch.identifier === storage.touch.ids[1]) {
const x = Math.round(touch.clientX);
const y = Math.round(touch.clientY);
const dx = x - storage.touch.screen_position.x;
const dy = y - storage.touch.screen_position.y;
const old_zoom = storage.canvas.zoom;
const old_offset_x = storage.canvas.offset_x;
const old_offset_y = storage.canvas.offset_y;
storage.canvas.offset_x -= dx;
storage.canvas.offset_y -= dy;
// console.log(new_finger_distance, storage.touch.finger_distance);
const scale_by = new_finger_distance / storage.touch.finger_distance;
const dz = storage.canvas.zoom * (scale_by - 1.0);
const zoom_offset_y = Math.round(dz * zoom_center.y);
const zoom_offset_x = Math.round(dz * zoom_center.x);
if (storage.min_zoom <= storage.canvas.zoom * scale_by && storage.canvas.zoom * scale_by <= storage.max_zoom) {
storage.canvas.zoom *= scale_by;
storage.canvas.offset_x += zoom_offset_x;
storage.canvas.offset_y += zoom_offset_y;
}
storage.touch.finger_distance = new_finger_distance;
if (storage.canvas.offset_x !== old_offset_x || storage.canvas.offset_y !== old_offset_y || old_zoom !== storage.canvas.zoom) {
move_canvas();
}
storage.touch.screen_position.x = x;
storage.touch.screen_position.y = y;
break;
}
}
return;
}
}
async function on_touchend(e) {
for (const touch of e.changedTouches) {
if (storage.touch.drawing) {
if (storage.touch.ids[0] == touch.identifier) {
storage.touch.drawing = false;
if (storage.tools.active === 'pencil') {
const event = stroke_event();
storage.current_stroke = [];
await queue_event(event);
} else if (storage.tools.active === 'eraser') {
const events = eraser_events();
storage.erased = [];
if (events.length > 0) {
for (const event of events) {
await queue_event(event);
}
}
} else if (storage.tools.active === 'ruler') {
const event = ruler_event(storage.touch.position.x, storage.touch.position.y);
await queue_event(event);
} else {
console.error('fuck');
}
}
}
const index = storage.touch.ids.indexOf(touch.identifier);
if (index !== -1) {
storage.touch.ids.splice(index, 1);
}
if (storage.touch.moving && storage.touch.ids.length === 0) {
// Only allow drawing again when ALL fingers have been lifted
storage.touch.moving = false;
}
}
if (storage.touch.ids.length === 0) {
waiting_for_second_finger = false;
}
}

132
client/undo.js

@ -1,132 +0,0 @@
function undo(state, context, event, options) {
let need_draw = false;
// Remove effect of latest own event, in a way that is recoverable
// Iterate back to front to find the _latest_ event
for (let i = state.events.length - 1; i >=0; --i) {
const other_event = state.events[i];
let skipped = false;
// Users can only undo their own, undeleted (not already undone) events
if (other_event.user_id === event.user_id && !other_event.deleted) {
// All "persistent" events (those that are pushed using SYN messages) should be handled here
// "Transient" events are by design droppable, and should not be undone, nor saved in state.events at all
switch (other_event.type) {
case EVENT.STROKE: {
other_event.deleted = true;
if (other_event.bvh_node && !options.skip_bvh) {
bvh_delete_stroke(state, other_event);
}
need_draw = true;
break;
}
case EVENT.UNDO: {
// do not undo an undo, we are not Notepad
skipped = true;
break;
}
case EVENT.IMAGE: {
other_event.deleted = true;
const image = get_image(context, other_event.image_id);
if (image !== null) {
image.deleted = true;
}
need_draw = true;
break;
}
case EVENT.IMAGE_MOVE: {
other_event.deleted = true;
const image = get_image(context, other_event.image_id);
if (image !== null) {
pop_image_transform(image);
need_draw = true;
} else {
console.warning('Undo image move for a non-existent image');
}
break;
}
case EVENT.IMAGE_SCALE: {
other_event.deleted = true;
const image = get_image(context, other_event.image_id);
if (image !== null) {
pop_image_transform(image);
need_draw = true;
} else {
console.warning('Undo image scale for a non-existent image');
}
break;
}
case EVENT.ERASER: {
other_event.deleted = true;
const stroke = state.events[other_event.stroke_id];
stroke.deleted = false;
if (!options.skip_bvh) {
bvh_undelete_stroke(state, stroke);
}
need_draw = true;
break;
}
default: {
console.error('cant undo event type', other_event.type);
break;
}
}
if (!skipped) {
break;
}
}
}
return need_draw;
}
function redo() {
console.log('TODO');
}
function push_image_move(image, x, y) {
if (image.transform_head < image.transform_history.length) {
image.transform_history[image.transform_head] = image.at.x;
image.transform_history[image.transform_head + 1] = image.at.y;
image.transform_history[image.transform_head + 2] = image.width;
image.transform_history[image.transform_head + 3] = image.height;
} else {
image.transform_history.push(image.at.x, image.at.y, image.width, image.height);
}
image.at.x = x;
image.at.y = y;
image.transform_head += 4;
}
function push_image_scale(image, corner, x, y) {
if (image.transform_head < image.transform_history.length) {
image.transform_history[image.transform_head] = image.at.x;
image.transform_history[image.transform_head + 1] = image.at.y;
image.transform_history[image.transform_head + 2] = image.width;
image.transform_history[image.transform_head + 3] = image.height;
} else {
image.transform_history.push(image.at.x, image.at.y, image.width, image.height);
}
scale_image(image, corner, {'x': x, 'y': y});
image.transform_head += 4;
}
function pop_image_transform(image, corner, x, y) {
image.transform_head -= 4;
image.at.x = image.transform_history[image.transform_head - 4];
image.at.y = image.transform_history[image.transform_head - 3];
image.width = image.transform_history[image.transform_head - 2];
image.height = image.transform_history[image.transform_head - 1];
}

1
client/wasm/compile_command

@ -1 +0,0 @@
clang -Oz --target=wasm32 -nostdlib -msimd128 -mbulk-memory -matomics -Wl,--no-entry,--import-memory,--shared-memory,--export-all,--max-memory=$((1024 * 1024 * 1024)) -z stack-size=$((1024 * 1024)) lod.c -o lod.wasm

351
client/wasm/lod.c

@ -1,351 +0,0 @@
#include <wasm_simd128.h>
extern char __heap_base;
static int allocated_static;
static int allocated_dynamic;
void
set_sp(char *sp)
{
__asm__ __volatile__(
".globaltype __stack_pointer, i32\n"
"local.get %0\n"
"global.set __stack_pointer\n"
: : "r"(sp)
);
}
void
free_static(void)
{
allocated_static = 0;
}
void
free_dynamic(void)
{
allocated_dynamic = 0;
}
void *
alloc_static(int size)
{
// This IS NOT thread-safe
void *result = &__heap_base + allocated_static;
allocated_static += size;
return(result);
}
static int
round_to_pow2(int value, int multiple)
{
return((value + multiple - 1) & -multiple);
}
void *
alloc_dynamic(int size)
{
// Very ad-van-ced thread-safe allocator
// CAN be called from multiple threads
size = round_to_pow2(size, 4);
int original_allocated_dynamic = __atomic_fetch_add(&allocated_dynamic, size, __ATOMIC_SEQ_CST);
void *result = &__heap_base + allocated_static + original_allocated_dynamic;
return(result);
}
static int
rdp_find_max(float *xs, float *ys, unsigned char *pressures, float zoom, int coords_from,
int segment_start, int segment_end)
{
int result = -1;
if (segment_start == segment_end) {
return(result);
}
float EPS = 0.125f / zoom * 255.0f;
float max_dist = 0.0f;
float ax = xs[coords_from + segment_start];
float ay = ys[coords_from + segment_start];
float bx = xs[coords_from + segment_end];
float by = ys[coords_from + segment_end];
unsigned char ap = pressures[coords_from / 2 + segment_start];
unsigned char bp = pressures[coords_from / 2 + segment_end];
float dx = bx - ax;
float dy = by - ay;
float dist_ab = __builtin_sqrtf(dx * dx + dy * dy);
float dir_nx = dy / dist_ab * 255.0f;
float dir_ny = -dx / dist_ab * 255.0f;
#if 0
for (int i = segment_start + 1; i < segment_end; ++i) {
float px = xs[coords_from + i];
float py = ys[coords_from + i];
unsigned char pp = pressures[coords_from + i];
float apx = px - ax;
float apy = py - ay;
float dist = __builtin_fabsf(apx * dir_nx + apy * dir_ny)
+ __builtin_abs(pp - ap) + __builtin_abs(pp - bp);
if (dist > EPS && dist > max_dist) {
result = i;
max_dist = dist;
}
}
#else
v128_t ax_x4 = wasm_f32x4_splat(ax);
v128_t ay_x4 = wasm_f32x4_splat(ay);
v128_t ap_x4 = wasm_f32x4_splat(ap);
v128_t bp_x4 = wasm_f32x4_splat(bp);
v128_t dir_nx_x4 = wasm_f32x4_splat(dir_nx);
v128_t dir_ny_x4 = wasm_f32x4_splat(dir_ny);
v128_t index_x4 = wasm_u32x4_make(segment_start + 1, segment_start + 2, segment_start + 3, segment_start + 4);
v128_t four_x4 = wasm_u32x4_const_splat(4);
v128_t max_dist_x4 = wasm_f32x4_splat(EPS);
v128_t max_index_x4 = wasm_u32x4_const_splat(-1);
for (int i = segment_start + 1; i < segment_end - 3; i += 4) {
v128_t px_x4 = wasm_v128_load(xs + coords_from + i);
v128_t py_x4 = wasm_v128_load(ys + coords_from + i);
v128_t pp_x4 = wasm_f32x4_make(
pressures[coords_from / 2 + i + 0],
pressures[coords_from / 2 + i + 1],
pressures[coords_from / 2 + i + 2],
pressures[coords_from / 2 + i + 3]
);
v128_t apx_x4 = wasm_f32x4_sub(px_x4, ax_x4);
v128_t apy_x4 = wasm_f32x4_sub(py_x4, ay_x4);
v128_t dist_x4 = wasm_f32x4_add(
wasm_f32x4_add(
wasm_f32x4_abs(wasm_f32x4_sub(pp_x4, ap_x4)),
wasm_f32x4_abs(wasm_f32x4_sub(pp_x4, bp_x4))
),
wasm_f32x4_abs(
wasm_f32x4_add(
wasm_f32x4_mul(apx_x4, dir_nx_x4),
wasm_f32x4_mul(apy_x4, dir_ny_x4)
)
)
);
v128_t mask = wasm_f32x4_gt(dist_x4, max_dist_x4);
max_index_x4 = wasm_v128_bitselect(index_x4, max_index_x4, mask);
max_dist_x4 = wasm_v128_bitselect(dist_x4, max_dist_x4, mask);
index_x4 = wasm_i32x4_add(index_x4, four_x4);
}
int indices[4];
float values[4];
wasm_v128_store(indices, max_index_x4);
wasm_v128_store(values, max_dist_x4);
for (int i = 0; i < 4; ++i) {
if (indices[i] != -1) {
if (values[i] > max_dist) {
result = indices[i];
max_dist = values[i];
}
}
}
if (max_dist == EPS) {
max_dist = 0.0f;
result = -1;
}
int remainder = (segment_end - segment_start - 1) % 4;
for (int i = segment_end - remainder; i < segment_end; ++i) {
float px = xs[coords_from + i];
float py = ys[coords_from + i];
unsigned char pp = pressures[coords_from + i];
float apx = px - ax;
float apy = py - ay;
float dist = __builtin_fabsf(apx * dir_nx + apy * dir_ny)
+ __builtin_abs(pp - ap) + __builtin_abs(pp - bp);
if (dist > EPS && dist > max_dist) {
result = i;
max_dist = dist;
}
}
#endif
return(result);
}
void
do_lod(int *clipped_indices, int clipped_count, float zoom,
int *stroke_coords_from,
float *xs,
float *ys,
unsigned char *pressures,
char **result_buffer,
int *result_count)
{
if (clipped_count == 0) {
result_count[0] = 0;
return;
}
int first_stroke = clipped_indices[0];
int last_stroke = clipped_indices[clipped_count - 1];
int total_points = 0;
for (int i = 0; i < clipped_count; ++i) {
int stroke_index = clipped_indices[i];
total_points += stroke_coords_from[stroke_index + 1] - stroke_coords_from[stroke_index];
}
int *segments_from = alloc_dynamic((clipped_count + 1) * 4);
int *segments = alloc_dynamic(total_points * 4); // TODO: this is a very conservative estimate, we can lower memory usage if we get this tighter
int segments_head = 0;
int stack[4096]; // TODO: what's a reasonable max size for this?
int max_stack_size = 0;
for (int i = 0; i < clipped_count; ++i) {
int stroke_index = clipped_indices[i];
// TODO: convert to a proper CSR, save half the memory
int coords_from = stroke_coords_from[stroke_index];
int coords_to = stroke_coords_from[stroke_index + 1];
int point_count = coords_to - coords_from;
// Basic CSR crap
segments_from[i] = segments_head;
int segment_count = 2;
int stack_head = 0;
segments[segments_head++] = 0;
stack[stack_head++] = 0;
stack[stack_head++] = 0;
stack[stack_head++] = point_count - 1;
while (stack_head > 0) {
if (stack_head > max_stack_size) { max_stack_size = stack_head; }
int end = stack[--stack_head];
int start = stack[--stack_head];
int type = stack[--stack_head];
if (type == 1) {
segments[segments_head++] = start;
} else {
int max = rdp_find_max(xs, ys, pressures, zoom, coords_from, start, end);
if (max != -1) {
segment_count += 1;
stack[stack_head++] = 0;
stack[stack_head++] = max;
stack[stack_head++] = end;
stack[stack_head++] = 1;
stack[stack_head++] = max;
stack[stack_head++] = -1;
stack[stack_head++] = 0;
stack[stack_head++] = start;
stack[stack_head++] = max;
}
}
}
segments[segments_head++] = point_count - 1;
}
segments_from[clipped_count] = segments_head;
// Write actual coordinates (points) and stroke ids
// Do this in one allocation so that they're not interleaved between threads
char *output = alloc_dynamic(segments_head * (3 * 4 + 1));
float *points = (float *) output;
int *ids = (int *) (output + segments_head * 4 * 2);
unsigned char *pressures_res = (unsigned char *) (output + segments_head * 4 * 3);
int phead = 0;
int ihead = 0;
for (int i = 0; i < clipped_count; ++i) {
int stroke_index = clipped_indices[i];
// TODO: convert to a proper CSR, save half the memory
int base_stroke = stroke_coords_from[stroke_index];
int from = segments_from[i];
int to = segments_from[i + 1];
for (int j = from; j < to; ++j) {
int point_index = segments[j];
float x = xs[base_stroke + point_index];
float y = ys[base_stroke + point_index];
points[phead++] = x;
points[phead++] = y;
pressures_res[ihead] = pressures[base_stroke + point_index];
if (j != to - 1) {
ids[ihead++] = stroke_index;
} else {
ids[ihead++] = stroke_index | (1 << 31);
}
}
}
result_buffer[0] = output;
result_count[0] = segments_head;
}
// NOT thread-safe, only call from one thread
char *
merge_results(int *segment_counts, char **buffers, int nthreads)
{
int total_segments = 0;
for (int i = 0; i < nthreads; ++i) {
total_segments += segment_counts[i];
}
char *merged = alloc_dynamic(total_segments * (3 * 4 + 1));
float *points = (float *) merged;
int *ids = (int *) (merged + total_segments * 4 * 2);
unsigned char *pressures = (unsigned char *) (merged + total_segments * 4 * 3);
for (int i = 0; i < nthreads; ++i) {
int segments = segment_counts[i];
if (segments > 0) {
__builtin_memcpy(points, buffers[i], segments * 4 * 2);
__builtin_memcpy(ids, buffers[i] + segments * 4 * 2, segments * 4);
__builtin_memcpy(pressures, buffers[i] + segments * 4 * 3, segments);
points += segments * 2;
ids += segments;
pressures += segments;
}
}
segment_counts[0] = total_segments;
return(merged);
}

BIN
client/wasm/lod.wasm

Binary file not shown.

613
client/webgl_draw.js

@ -1,613 +0,0 @@
function schedule_draw(state, context, animate = false) {
if (!state.timers.raf) {
window.requestAnimationFrame(async (ts) => {
await draw(state, context, animate, ts);
});
state.timers.raf = true;
}
}
function upload_if_needed(gl, buffer_kind, serializer) {
if (serializer.need_gpu_allocate) {
if (config.debug_print) console.debug('gpu allocate');
gl.bufferData(buffer_kind, serializer.size, gl.DYNAMIC_DRAW);
serializer.need_gpu_allocate = false;
serializer.gpu_upload_from = 0;
}
if (serializer.gpu_upload_from < serializer.offset) {
if (config.debug_print) console.debug('gpu upload');
const upload_offset = serializer.gpu_upload_from;
const upload_size = serializer.offset - upload_offset;
gl.bufferSubData(buffer_kind, upload_offset, new Uint8Array(serializer.buffer, upload_offset, upload_size));
serializer.gpu_upload_from = serializer.offset;
}
}
function upload_square_rgba16ui_texture(gl, serializer, texture_size) {
const bpp = 2 * 4;
const data_size = serializer.offset - serializer.gpu_upload_from;
let data_pixels = data_size / bpp; // data_size % bpp is expected to always be zero here
const pixels_already_uploaded = serializer.gpu_upload_from / bpp;
let rows_uploaded = Math.floor(pixels_already_uploaded / texture_size);
const rows_remainder = pixels_already_uploaded % texture_size;
// Upload first non-whole row (if last upload was not a whole number of rows)
if (rows_remainder > 0) {
const row_upload_to_full = texture_size - rows_remainder;
const first_upload = Math.min(row_upload_to_full, data_pixels);
if (first_upload > 0) {
gl.texSubImage2D(gl.TEXTURE_2D, 0, rows_remainder, rows_uploaded, first_upload, 1, gl.RGBA_INTEGER, gl.UNSIGNED_SHORT, new Uint16Array(serializer.buffer, serializer.gpu_upload_from, first_upload * 4));
data_pixels -= first_upload;
serializer.gpu_upload_from += first_upload;
rows_uploaded += 1;
}
}
const rows = Math.ceil(data_pixels / texture_size);
const last_row = data_pixels % texture_size;
const whole_upload = (rows - 1) * texture_size;
// Upload whole rows
if (rows > 1) {
gl.texSubImage2D(gl.TEXTURE_2D, 0, 0, rows_uploaded, texture_size, rows - 1, gl.RGBA_INTEGER, gl.UNSIGNED_SHORT, new Uint16Array(serializer.buffer, serializer.gpu_upload_from, whole_upload * 4));
rows_uploaded += rows - 1;
}
// Upload last row
if (last_row > 0) {
const last_row_upload = last_row * bpp;
gl.texSubImage2D(gl.TEXTURE_2D, 0, 0, rows_uploaded, last_row, 1, gl.RGBA_INTEGER, gl.UNSIGNED_SHORT, new Uint16Array(serializer.buffer, whole_upload, last_row_upload * 4));
}
serializer.gpu_upload_from = serializer.offset;
}
function draw_html(state) {
// HUD-like things. Player cursors, screens
for (const player_id in state.players) {
if (player_id === state.me) continue;
const player = state.players[player_id];
let player_cursor_element = document.querySelector(`.player-cursor[data-player-id="${player_id}"]`);
if (player_cursor_element === null && player.online) {
player_cursor_element = insert_player_cursor(state, player_id);
}
if (!player.online && player_cursor_element !== null) {
player_cursor_element.remove();
const player_list_item = document.querySelector(`.player-list .player[data-player-id="${player_id}"]`);
if (player_list_item) player_list_item.remove();
if (document.querySelector('.player-list').childElementCount === 0) {
document.querySelector('.player-list').classList.add('vhide');
}
}
if (player_cursor_element && player.online) {
const screenp = canvas_to_screen(state, player.cursor);
player_cursor_element.style.transform = `translate(${Math.round(screenp.x)}px, ${Math.round(screenp.y)}px) rotate(-30deg)`;
}
}
}
async function draw(state, context, animate, ts) {
const dt = ts - context.last_frame_ts;
const cpu_before = performance.now();
context.last_frame_ts = ts;
const gl = context.gl;
const width = window.innerWidth;
const height = window.innerHeight;
bvh_clip(state, context);
const segment_count = await geometry_write_instances(state, context);
const dynamic_segment_count = context.dynamic_segment_count;
const dynamic_stroke_count = context.dynamic_stroke_count;
let query = null;
if (context.gpu_timer_ext !== null) {
query = gl.createQuery();
gl.beginQuery(context.gpu_timer_ext.TIME_ELAPSED_EXT, query);
}
// Only clear once we have the data, this might not always be on the same frame?
gl.viewport(0, 0, context.canvas.width, context.canvas.height);
gl.clearColor(context.bgcolor.r, context.bgcolor.g, context.bgcolor.b, 1);
gl.clearDepth(0.0);
gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT);
const locations = context.locations;
const buffers = context.buffers;
const programs = context.programs;
const textures = context.textures;
// Draw the background pattern
if (state.background_pattern === 'dots') {
const pr = programs['dots'];
gl.useProgram(pr.program);
gl.bindBuffer(gl.ARRAY_BUFFER, buffers['b_instance_dot']);
gl.enableVertexAttribArray(pr.locations['a_center']);
gl.vertexAttribPointer(pr.locations['a_center'], 2, gl.FLOAT, false, 2 * 4, 0);
gl.vertexAttribDivisor(pr.locations['a_center'], 1);
gl.uniform2f(pr.locations['u_res'], context.canvas.width, context.canvas.height);
gl.uniform2f(pr.locations['u_scale'], state.canvas.zoom, state.canvas.zoom);
gl.uniform2f(pr.locations['u_translation'], state.canvas.offset.x, state.canvas.offset.y);
const zoom = state.canvas.zoom;
const zoom_log2 = Math.log2(zoom);
const zoom_previous = Math.pow(2, Math.floor(zoom_log2));
const zoom_next = Math.pow(2, Math.ceil(zoom_log2));
// Previous level
{
const one_dot = new Float32Array(geometry_gen_quad(0, 0, 1 / zoom_previous));
const dot_instances = new Float32Array(geometry_gen_fullscreen_grid(state, context, 32 / zoom_previous, 32 / zoom_previous));
const t = Math.min(1.0, 1.0 - (zoom / zoom_previous) / 2.0);
gl.uniform1f(pr.locations['u_fadeout'], t);
gl.bindBuffer(gl.ARRAY_BUFFER, buffers['b_instance_dot']);
gl.bufferData(gl.ARRAY_BUFFER, dot_instances, gl.STREAM_DRAW);
gl.drawArraysInstanced(gl.TRIANGLES, 0, 6, dot_instances.length / 2);
}
// Next level
if (zoom_previous != zoom_next) {
const dot_instances = new Float32Array(geometry_gen_fullscreen_grid(state, context, 32 / zoom_next, 32 / zoom_next));
const t = Math.min(1.0, 1.0 - (zoom_next / zoom) / 2.0);
gl.uniform1f(pr.locations['u_fadeout'], t);
gl.bindBuffer(gl.ARRAY_BUFFER, buffers['b_instance_dot']);
gl.bufferData(gl.ARRAY_BUFFER, dot_instances, gl.STREAM_DRAW);
gl.drawArraysInstanced(gl.TRIANGLES, 0, 6, dot_instances.length / 2);
}
} else if (state.background_pattern === 'grid') {
const pr = programs['grid'];
const zoom = state.canvas.zoom;
let zoom_log8 = Math.log(zoom) / Math.log(8);
//if (zoom_log2 === Math.floor(zoom_log2)) {
// zoom_log2 -= 0.001;
//}
const zoom_previous = Math.pow(8, Math.floor(zoom_log8));
let zoom_next = Math.pow(8, Math.ceil(zoom_log8));
if (zoom_next === zoom_previous) {
zoom_next = zoom_previous * 8;
}
gl.useProgram(pr.program);
gl.bindBuffer(gl.ARRAY_BUFFER, buffers['b_instance_grid']);
gl.enableVertexAttribArray(pr.locations['a_data']);
gl.vertexAttribPointer(pr.locations['a_data'], 2, gl.FLOAT, false, 2 * 4, 0);
gl.vertexAttribDivisor(pr.locations['a_data'], 1);
gl.uniform2f(pr.locations['u_res'], context.canvas.width, context.canvas.height);
gl.uniform2f(pr.locations['u_scale'], state.canvas.zoom, state.canvas.zoom);
gl.uniform2f(pr.locations['u_translation'], state.canvas.offset.x, state.canvas.offset.y);
gl.uniform1f(pr.locations['u_fadeout'], 1.0);
// Previous level (major lines)
{
const grid_instances = new Float32Array(geometry_gen_fullscreen_grid_1d(state, context, 32 / zoom_previous, 32 / zoom_previous));
let t = (zoom / zoom_previous - 1) / -7 + 1;
t = 0.25;
gl.uniform1f(pr.locations['u_fadeout'], t);
gl.bindBuffer(gl.ARRAY_BUFFER, buffers['b_instance_grid']);
gl.bufferData(gl.ARRAY_BUFFER, grid_instances, gl.STREAM_DRAW);
gl.drawArraysInstanced(gl.TRIANGLES, 0, 6, grid_instances.length / 2);
}
// Next level (minor lines)
{
const grid_instances = new Float32Array(geometry_gen_fullscreen_grid_1d(state, context, 32 / zoom_next, 32 / zoom_next));
let t = (zoom_next / zoom - 1) / 7;
t = Math.min(0.1, -t + 1); // slight fade-in
gl.uniform1f(pr.locations['u_fadeout'], t);
gl.bindBuffer(gl.ARRAY_BUFFER, buffers['b_instance_grid']);
gl.bufferData(gl.ARRAY_BUFFER, grid_instances, gl.STREAM_DRAW);
gl.drawArraysInstanced(gl.TRIANGLES, 0, 6, grid_instances.length / 2);
}
}
// Images
{
const pr = programs['image'];
gl.clear(gl.DEPTH_BUFFER_BIT); // draw images above the background pattern
gl.useProgram(pr.program);
let offset = 0;
const quads = geometry_image_quads(state, context);
gl.bindBuffer(gl.ARRAY_BUFFER, buffers['b_images']);
gl.bufferData(gl.ARRAY_BUFFER, quads, gl.STATIC_DRAW);
gl.vertexAttribDivisor(pr.locations['a_pos'], 0);
gl.enableVertexAttribArray(pr.locations['a_pos']);
gl.vertexAttribPointer(pr.locations['a_pos'], 2, gl.FLOAT, false, 2 * 4, 0);
for (const entry of context.images) {
if (!entry.deleted) {
gl.uniform2f(pr.locations['u_res'], context.canvas.width, context.canvas.height);
gl.uniform2f(pr.locations['u_scale'], state.canvas.zoom, state.canvas.zoom);
gl.uniform2f(pr.locations['u_translation'], state.canvas.offset.x, state.canvas.offset.y);
gl.uniform1i(pr.locations['u_texture'], 0); // Only 1 active texture for each drawcall
gl.uniform1i(pr.locations['u_solid'], 0);
gl.bindTexture(gl.TEXTURE_2D, entry.texture);
gl.drawArrays(gl.TRIANGLES, offset, 6);
// Highlight active image
if (entry.key === state.active_image) {
gl.uniform1i(pr.locations['u_solid'], 1);
gl.uniform4f(pr.locations['u_color'], 0.133 * 0.5, 0.545 * 0.5, 0.902 * 0.5, 0.5);
gl.drawArrays(gl.TRIANGLES, offset, 6);
}
}
offset += 6;
}
}
// TODO: what do we do with this
const circle_lod = Math.ceil(Math.min(7, 1 + Math.sqrt(state.canvas.zoom)));
const circle_data = geometry_good_circle_and_dummy(circle_lod);
// "Static" data upload
if (segment_count > 0) {
const pr = programs['main'];
gl.clear(gl.DEPTH_BUFFER_BIT); // draw strokes above the images
gl.useProgram(pr.program);
const total_static_size = context.instance_data_points.size * 4 +
context.instance_data_ids.size * 4 +
round_to_pow2(context.instance_data_pressures.size, 4) +
circle_data.points.size * 4;
gl.bindBuffer(gl.ARRAY_BUFFER, buffers['b_strokes_static']);
gl.bufferData(gl.ARRAY_BUFFER, total_static_size, gl.STREAM_DRAW);
gl.bufferSubData(gl.ARRAY_BUFFER, 0, tv_data(context.instance_data_points));
gl.bufferSubData(gl.ARRAY_BUFFER, context.instance_data_points.size * 4, tv_data(context.instance_data_ids));
gl.bufferSubData(gl.ARRAY_BUFFER, context.instance_data_points.size * 4 + context.instance_data_ids.size * 4,
tv_data(context.instance_data_pressures));
gl.bufferSubData(gl.ARRAY_BUFFER, context.instance_data_points.size * 4 + context.instance_data_ids.size * 4 + round_to_pow2(context.instance_data_pressures.size, 4), tv_data(circle_data.points));
gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, buffers['i_strokes_static']);
gl.bufferData(gl.ELEMENT_ARRAY_BUFFER, tv_data(circle_data.indices), gl.STREAM_DRAW);
gl.bindTexture(gl.TEXTURE_2D, textures['stroke_data']);
upload_square_rgba16ui_texture(gl, context.stroke_data, config.stroke_texture_size);
gl.uniform2f(pr.locations['u_res'], context.canvas.width, context.canvas.height);
gl.uniform2f(pr.locations['u_scale'], state.canvas.zoom, state.canvas.zoom);
gl.uniform2f(pr.locations['u_translation'], state.canvas.offset.x, state.canvas.offset.y);
gl.uniform1i(pr.locations['u_stroke_count'], state.events.length);
gl.uniform1i(pr.locations['u_debug_mode'], state.debug.red);
gl.uniform1i(pr.locations['u_stroke_data'], 0);
gl.uniform1i(pr.locations['u_stroke_texture_size'], config.stroke_texture_size);
gl.uniform1i(pr.locations['u_circle_points'], circle_data.points.size / 2 - 4);
gl.enableVertexAttribArray(pr.locations['a_pos']);
gl.enableVertexAttribArray(pr.locations['a_a']);
gl.enableVertexAttribArray(pr.locations['a_b']);
gl.enableVertexAttribArray(pr.locations['a_stroke_id']);
gl.enableVertexAttribArray(pr.locations['a_pressure']);
// Circle meshes (shared for all instances)
gl.vertexAttribPointer(pr.locations['a_pos'], 2, gl.FLOAT, false, 2 * 4, context.instance_data_points.size * 4 + context.instance_data_ids.size * 4 + round_to_pow2(context.instance_data_pressures.size, 4));
// Points (a, b) and stroke ids are stored in separate cpu buffers so that points can be reused (look at stride and offset values)
gl.vertexAttribPointer(pr.locations['a_a'], 2, gl.FLOAT, false, 2 * 4, 0);
gl.vertexAttribPointer(pr.locations['a_b'], 2, gl.FLOAT, false, 2 * 4, 2 * 4);
gl.vertexAttribIPointer(pr.locations['a_stroke_id'], 1, gl.INT, 4, context.instance_data_points.size * 4);
gl.vertexAttribPointer(pr.locations['a_pressure'], 2, gl.UNSIGNED_BYTE, true, 1, context.instance_data_points.size * 4 + context.instance_data_ids.size * 4);
gl.vertexAttribDivisor(pr.locations['a_pos'], 0);
gl.vertexAttribDivisor(pr.locations['a_a'], 1);
gl.vertexAttribDivisor(pr.locations['a_b'], 1);
gl.vertexAttribDivisor(pr.locations['a_stroke_id'], 1);
gl.vertexAttribDivisor(pr.locations['a_pressure'], 1);
// Static draw (everything already bound)
gl.drawElementsInstanced(gl.TRIANGLES, circle_data.indices.size, gl.UNSIGNED_INT, 0, segment_count);
// I don't really know why I need to do this, but it
// makes background patter drawcall work properly
gl.vertexAttribDivisor(pr.locations['a_pos'], 0);
gl.vertexAttribDivisor(pr.locations['a_a'], 0);
gl.vertexAttribDivisor(pr.locations['a_b'], 0);
gl.vertexAttribDivisor(pr.locations['a_stroke_id'], 0);
gl.vertexAttribDivisor(pr.locations['a_pressure'], 0);
}
// Dynamic draw (strokes currently being drawn)
if (false && dynamic_segment_count > 0) {
const pr = programs['main']; // same as static
// Dynamic strokes should be drawn above static strokes
gl.clear(gl.DEPTH_BUFFER_BIT);
gl.useProgram(pr.program);
gl.uniform1i(pr.locations['u_stroke_count'], dynamic_stroke_count);
gl.uniform1i(pr.locations['u_stroke_data'], 0);
gl.uniform1i(pr.locations['u_stroke_texture_size'], config.dynamic_stroke_texture_size);
gl.bindBuffer(gl.ARRAY_BUFFER, buffers['b_strokes_dynamic']);
// Dynamic data upload
const total_dynamic_size =
context.dynamic_instance_points.size * 4 + context.dynamic_instance_ids.size * 4 +
context.dynamic_instance_pressure.size;
gl.bufferData(gl.ARRAY_BUFFER, total_dynamic_size, gl.STREAM_DRAW);
gl.bufferSubData(gl.ARRAY_BUFFER, 0, tv_data(context.dynamic_instance_points));
gl.bufferSubData(gl.ARRAY_BUFFER, context.dynamic_instance_points.size * 4, tv_data(context.dynamic_instance_ids));
gl.bufferSubData(gl.ARRAY_BUFFER, context.dynamic_instance_points.size * 4 + context.dynamic_instance_ids.size * 4,
tv_data(context.dynamic_instance_pressure));
gl.bindTexture(gl.TEXTURE_2D, textures['dynamic_stroke_data']);
upload_square_rgba16ui_texture(gl, context.dynamic_stroke_data, config.dynamic_stroke_texture_size);
gl.uniform2f(pr.locations['u_res'], context.canvas.width, context.canvas.height);
gl.uniform2f(pr.locations['u_scale'], state.canvas.zoom, state.canvas.zoom);
gl.uniform2f(pr.locations['u_translation'], state.canvas.offset.x, state.canvas.offset.y);
gl.uniform1i(pr.locations['u_stroke_count'], context.dynamic_stroke_count);
gl.uniform1i(pr.locations['u_debug_mode'], state.debug.red);
gl.uniform1i(pr.locations['u_stroke_data'], 0);
gl.uniform1i(pr.locations['u_stroke_texture_size'], config.dynamic_stroke_texture_size);
gl.enableVertexAttribArray(pr.locations['a_a']);
gl.enableVertexAttribArray(pr.locations['a_b']);
gl.enableVertexAttribArray(pr.locations['a_stroke_id']);
gl.enableVertexAttribArray(pr.locations['a_pressure']);
// Points (a, b) and stroke ids are stored in separate cpu buffers so that points can be reused (look at stride and offset values)
if (context.dynamic_instance_ids.size > 1) {
gl.vertexAttribPointer(pr.locations['a_a'], 2, gl.FLOAT, false, 2 * 4, 0);
gl.vertexAttribPointer(pr.locations['a_b'], 2, gl.FLOAT, false, 2 * 4, 2 * 4);
} else {
// A special case where there is no second point. Reuse the first point and handle the zero length segment in the shader
gl.vertexAttribPointer(pr.locations['a_a'], 2, gl.FLOAT, false, 2 * 4, 0);
gl.vertexAttribPointer(pr.locations['a_b'], 2, gl.FLOAT, false, 2 * 4, 0);
}
gl.vertexAttribIPointer(pr.locations['a_stroke_id'], 1, gl.INT, 4, context.dynamic_instance_points.size * 4);
gl.vertexAttribPointer(pr.locations['a_pressure'], 2, gl.UNSIGNED_BYTE, true, 1, context.dynamic_instance_points.size * 4 + context.dynamic_instance_ids.size * 4);
gl.vertexAttribDivisor(pr.locations['a_a'], 1);
gl.vertexAttribDivisor(pr.locations['a_b'], 1);
gl.vertexAttribDivisor(pr.locations['a_stroke_id'], 1);
gl.vertexAttribDivisor(pr.locations['a_pressure'], 1);
gl.drawArraysInstanced(gl.TRIANGLES, 0, 32 * 3 + 6 + 32 * 3, dynamic_segment_count);
gl.vertexAttribDivisor(pr.locations['a_a'], 0);
gl.vertexAttribDivisor(pr.locations['a_b'], 0);
gl.vertexAttribDivisor(pr.locations['a_stroke_id'], 0);
gl.vertexAttribDivisor(pr.locations['a_pressure'], 0);
}
// HUD: resize handles, etc
if (state.active_image !== null) {
const pr = programs['main']; // same as static
const handles = geometry_generate_handles(state, context, state.active_image);
const ui_segments = 7 * 4 - 1; // each square = 4, each line = 1, square->line = 1, line->square = 1
gl.bindBuffer(gl.ARRAY_BUFFER, buffers['b_hud']);
gl.bufferData(gl.ARRAY_BUFFER, handles.points.byteLength + handles.ids.byteLength + handles.pressures.byteLength, gl.STREAM_DRAW);
gl.bufferSubData(gl.ARRAY_BUFFER, 0, handles.points);
gl.bufferSubData(gl.ARRAY_BUFFER, handles.points.byteLength, handles.ids);
gl.bufferSubData(gl.ARRAY_BUFFER, handles.points.byteLength + handles.ids.byteLength, handles.pressures);
gl.bindTexture(gl.TEXTURE_2D, textures['ui']);
upload_square_rgba16ui_texture(gl, handles.stroke_data, config.ui_texture_size);
gl.uniform2f(pr.locations['u_res'], context.canvas.width, context.canvas.height);
gl.uniform2f(pr.locations['u_scale'], state.canvas.zoom, state.canvas.zoom);
gl.uniform2f(pr.locations['u_translation'], state.canvas.offset.x, state.canvas.offset.y);
gl.uniform1i(pr.locations['u_stroke_count'], 8);
gl.uniform1i(pr.locations['u_debug_mode'], 0);
gl.uniform1i(pr.locations['u_stroke_data'], 0);
gl.uniform1i(pr.locations['u_stroke_texture_size'], config.ui_texture_size);
gl.enableVertexAttribArray(pr.locations['a_a']);
gl.enableVertexAttribArray(pr.locations['a_b']);
gl.enableVertexAttribArray(pr.locations['a_stroke_id']);
gl.enableVertexAttribArray(pr.locations['a_pressure']);
gl.vertexAttribPointer(pr.locations['a_a'], 2, gl.FLOAT, false, 2 * 4, 0);
gl.vertexAttribPointer(pr.locations['a_b'], 2, gl.FLOAT, false, 2 * 4, 2 * 4);
gl.vertexAttribIPointer(pr.locations['a_stroke_id'], 1, gl.INT, 4, handles.points.byteLength);
gl.vertexAttribPointer(pr.locations['a_pressure'], 2, gl.UNSIGNED_BYTE, true, 1, handles.points.byteLength + handles.ids.byteLength);
gl.vertexAttribDivisor(pr.locations['a_a'], 1);
gl.vertexAttribDivisor(pr.locations['a_b'], 1);
gl.vertexAttribDivisor(pr.locations['a_stroke_id'], 1);
gl.vertexAttribDivisor(pr.locations['a_pressure'], 1);
gl.drawArraysInstanced(gl.TRIANGLES, 0, 32 * 3 + 6 + 32 * 3, ui_segments);
// I don't really know why I need to do this, but it
// makes background patter drawcall work properly
gl.vertexAttribDivisor(pr.locations['a_a'], 0);
gl.vertexAttribDivisor(pr.locations['a_b'], 0);
gl.vertexAttribDivisor(pr.locations['a_stroke_id'], 0);
gl.vertexAttribDivisor(pr.locations['a_pressure'], 0);
}
if (config.draw_bvh) {
const pr = programs['iquad'];
const bboxes = tv_create(Float32Array, context.clipped_indices.size * 4);
// Debug BVH viz
for (let i = 0; i < context.clipped_indices.size; ++i) {
const stroke_id = context.clipped_indices.data[i];
const stroke = state.events[stroke_id];
tv_add(bboxes, stroke.bbox.x1);
tv_add(bboxes, stroke.bbox.y1);
tv_add(bboxes, stroke.bbox.x2);
tv_add(bboxes, stroke.bbox.y2);
}
const quad_count = bboxes.size / 4;
gl.useProgram(pr.program);
gl.bindBuffer(gl.ARRAY_BUFFER, buffers['b_iquads']);
gl.bufferData(gl.ARRAY_BUFFER, tv_data(bboxes), gl.STREAM_DRAW);
gl.uniform2f(pr.locations['u_res'], context.canvas.width, context.canvas.height);
gl.uniform2f(pr.locations['u_scale'], state.canvas.zoom, state.canvas.zoom);
gl.uniform2f(pr.locations['u_translation'], state.canvas.offset.x, state.canvas.offset.y);
gl.enableVertexAttribArray(pr.locations['a_topleft']);
gl.enableVertexAttribArray(pr.locations['a_bottomright']);
gl.vertexAttribPointer(pr.locations['a_topleft'], 2, gl.FLOAT, false, 4 * 4, 0);
gl.vertexAttribPointer(pr.locations['a_bottomright'], 2, gl.FLOAT, false, 4 * 4, 2 * 4);
gl.vertexAttribDivisor(pr.locations['a_topleft'], 1);
gl.vertexAttribDivisor(pr.locations['a_bottomright'], 1);
gl.drawArraysInstanced(gl.TRIANGLES, 0, 6, quad_count);
gl.vertexAttribDivisor(pr.locations['a_topleft'], 0);
gl.vertexAttribDivisor(pr.locations['a_bottomright'], 0);
}
if (config.draw_fullnodes) {
const quads = bvh_get_fullnodes_debug(state, context);
const pr = programs['iquad'];
const bboxes = tv_create(Float32Array, quads.length * 4);
for (let i = 0; i < quads.length; ++i) {
const bbox = quads[i];
tv_add(bboxes, bbox.x1);
tv_add(bboxes, bbox.y1);
tv_add(bboxes, bbox.x2);
tv_add(bboxes, bbox.y2);
}
const quad_count = bboxes.size / 4;
gl.useProgram(pr.program);
gl.bindBuffer(gl.ARRAY_BUFFER, buffers['b_iquads']);
gl.bufferData(gl.ARRAY_BUFFER, tv_data(bboxes), gl.STREAM_DRAW);
gl.uniform2f(pr.locations['u_res'], context.canvas.width, context.canvas.height);
gl.uniform2f(pr.locations['u_scale'], state.canvas.zoom, state.canvas.zoom);
gl.uniform2f(pr.locations['u_translation'], state.canvas.offset.x, state.canvas.offset.y);
gl.enableVertexAttribArray(pr.locations['a_topleft']);
gl.enableVertexAttribArray(pr.locations['a_bottomright']);
gl.vertexAttribPointer(pr.locations['a_topleft'], 2, gl.FLOAT, false, 4 * 4, 0);
gl.vertexAttribPointer(pr.locations['a_bottomright'], 2, gl.FLOAT, false, 4 * 4, 2 * 4);
gl.vertexAttribDivisor(pr.locations['a_topleft'], 1);
gl.vertexAttribDivisor(pr.locations['a_bottomright'], 1);
gl.drawArraysInstanced(gl.TRIANGLES, 0, 6, quad_count);
gl.vertexAttribDivisor(pr.locations['a_topleft'], 0);
gl.vertexAttribDivisor(pr.locations['a_bottomright'], 0);
}
document.getElementById('debug-stats').innerHTML = `
<span>Strokes onscreen: ${context.clipped_indices.size}</span>
<span>Segments onscreen: ${segment_count}</span>
<span>Canvas offset: (${Math.round(state.canvas.offset.x * 100) / 100}, ${Math.round(state.canvas.offset.y * 100) / 100})</span>
<span>Canvas zoom level: ${state.canvas.zoom_level}</span>
<span>Canvas zoom: ${Math.round(state.canvas.zoom * 100) / 100}</span>`;
if (context.gpu_timer_ext) {
gl.endQuery(context.gpu_timer_ext.TIME_ELAPSED_EXT);
const next_tick = () => {
if (query) {
// At some point in the future, after returning control to the browser
const available = gl.getQueryParameter(query, gl.QUERY_RESULT_AVAILABLE);
const disjoint = gl.getParameter(context.gpu_timer_ext.GPU_DISJOINT_EXT);
if (available && !disjoint) {
// See how much time the rendering of the object took in nanoseconds.
const timeElapsed = gl.getQueryParameter(query, gl.QUERY_RESULT);
//console.debug(timeElapsed / 1000000);
document.querySelector('.debug-timings .gpu').innerHTML = 'Last GPU Frametime: ' + Math.round(timeElapsed / 10000) / 100 + 'ms';
}
if (available || disjoint) {
// Clean up the query object.
gl.deleteQuery(query);
// Don't re-enter this polling loop.
query = null;
} else if (!available) {
setTimeout(next_tick, 0);
}
}
}
setTimeout(next_tick, 0);
}
const cpu_after = performance.now();
state.timers.raf = false;
document.querySelector('.debug-timings .cpu').innerHTML = 'Last CPU Frametime: ' + Math.round((cpu_after - cpu_before) * 100) / 100 + 'ms';
if (state.debug.benchmark_mode) {
const redraw = state.debug.on_benchmark();
if (redraw) {
schedule_draw(state, context);
}
}
if (state.canvas.target_zoom != state.canvas.zoom) {
update_canvas_zoom(state, state.canvas.zoom, state.canvas.target_zoom, animate ? dt : context.last_frame_dt);
schedule_draw(state, context, true);
}
context.last_frame_dt = dt;
}
function update_canvas_zoom(state, current, target, dt) {
const rate = Math.min(1.0, dt / 16.66 * 0.3);
if (Math.abs(1.0 - current / target) > 0.01) {
state.canvas.zoom = current + (target - current) * rate;
} else {
state.canvas.zoom = target;
}
// https://gist.github.com/aolo2/a373363419bd5a9283977ab9f8841f78
const zc = state.canvas.zoom_screenp;
state.canvas.offset.x = zc.x - (zc.x - state.canvas.offset.x) * state.canvas.zoom / current;
state.canvas.offset.y = zc.y - (zc.y - state.canvas.offset.y) * state.canvas.zoom / current;
update_cursor(state);
}

626
client/webgl_geometry.js

@ -1,626 +0,0 @@
function geometry_prepare_stroke(state) {
if (!state.online) {
return null;
}
const player = state.players[state.me];
const stroke = player.strokes[player.strokes.length - 1]; // MY OWN player.strokes should never be bigger than 1 element
if (stroke.points.length === 0) {
return null;
}
const points = process_stroke2(state.canvas.zoom, stroke.points);
return {
'color': stroke.color,
'width': stroke.width,
'points': points,
'user_id': state.me,
};
}
async function geometry_write_instances(state, context, callback) {
state.stats.rdp_max_count = 0;
state.stats.rdp_segments = 0;
const segment_count = await do_lod(state, context);
if (config.debug_print) console.debug('instances:', segment_count, 'rdp max:', state.stats.rdp_max_count, 'rdp segments:', state.stats.rdp_segments);
return segment_count;
}
function geometry_add_dummy_stroke(context) {
context.stroke_data = ser_ensure_by(context.stroke_data, config.bytes_per_stroke);
ser_u16(context.stroke_data, 0);
ser_u16(context.stroke_data, 0);
ser_u16(context.stroke_data, 0);
ser_u16(context.stroke_data, 0);
}
// Real stroke, add forever
function geometry_add_stroke(state, context, stroke, stroke_index, skip_bvh = false) {
if (!state.online || !stroke || stroke.coords_to - stroke.coords_from === 0 || stroke.deleted) return;
stroke.bbox = stroke_bbox(state, stroke);
stroke.area = box_area(stroke.bbox);
context.stroke_data = ser_ensure_by(context.stroke_data, config.bytes_per_stroke);
const color_u32 = stroke.color;
const r = (color_u32 >> 16) & 0xFF;
const g = (color_u32 >> 8) & 0xFF;
const b = color_u32 & 0xFF;
ser_u16(context.stroke_data, r);
ser_u16(context.stroke_data, g);
ser_u16(context.stroke_data, b);
ser_u16(context.stroke_data, stroke.width);
if (!skip_bvh) bvh_add_stroke(state, state.bvh, stroke_index, stroke);
}
function recompute_dynamic_data(state, context) {
let total_points = 0;
let total_strokes = 0;
for (const player_id in state.players) {
const player = state.players[player_id];
for (const stroke of player.strokes) {
if (!stroke.empty && stroke.points.length > 0) {
total_points += stroke.points.length;
total_strokes += 1;
}
}
}
tv_ensure(context.dynamic_instance_points, round_to_pow2(total_points * 2, 4096));
tv_ensure(context.dynamic_instance_pressure, round_to_pow2(total_points, 4096));
tv_ensure(context.dynamic_instance_ids, round_to_pow2(total_points, 4096));
tv_clear(context.dynamic_instance_points);
tv_clear(context.dynamic_instance_pressure);
tv_clear(context.dynamic_instance_ids);
context.dynamic_stroke_data = ser_ensure(context.dynamic_stroke_data, config.bytes_per_stroke * total_strokes);
ser_clear(context.dynamic_stroke_data);
let stroke_index = 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];
for (const stroke of player.strokes) {
if (!stroke.empty && stroke.points.length > 0) {
for (let i = 0; i < stroke.points.length; ++i) {
const p = stroke.points[i];
tv_add(context.dynamic_instance_points, p.x);
tv_add(context.dynamic_instance_points, p.y);
tv_add(context.dynamic_instance_pressure, p.pressure);
if (i !== stroke.points.length - 1) {
tv_add(context.dynamic_instance_ids, stroke_index);
} else {
tv_add(context.dynamic_instance_ids, stroke_index | (1 << 31));
}
}
const color_u32 = stroke.color;
const r = (color_u32 >> 16) & 0xFF;
const g = (color_u32 >> 8) & 0xFF;
const b = color_u32 & 0xFF;
ser_u16(context.dynamic_stroke_data, r);
ser_u16(context.dynamic_stroke_data, g);
ser_u16(context.dynamic_stroke_data, b);
ser_u16(context.dynamic_stroke_data, stroke.width);
stroke_index += 1; // TODO: proper player Z order
}
}
}
context.dynamic_segment_count = total_points;
context.dynamic_stroke_count = total_strokes;
}
function geometry_start_prestroke(state, player_id) {
if (!state.online) return;
const player = state.players[player_id];
player.strokes.push({
'empty': false,
'points': [],
'head': null,
'color': player.color,
'width': player.width,
});
player.current_prestroke = true;
}
function geometry_end_prestroke(state, player_id) {
if (!state.online) return;
const player = state.players[player_id];
player.current_prestroke = false;
}
function geometry_add_prepoint(state, context, player_id, point, is_pen, raw = false) {
if (!state.online) return;
const player = state.players[player_id];
const stroke = player.strokes[player.strokes.length - 1];
const points = stroke.points;
if (point.pressure < config.min_pressure) {
point.pressure = config.min_pressure;
}
if (points.length > 0 && !raw) {
// pulled from "perfect-freehand" package. MIT
// https://github.com/steveruizok/perfect-freehand/
const streamline = 0.75;
const t = 0.15 + (1 - streamline) * 0.85
const smooth_pressure = exponential_smoothing(points, point, 3);
points.push({
'x': stroke.head.x * t + point.x * (1 - t),
'y': stroke.head.y * t + point.y * (1 - t),
'pressure': is_pen ? stroke.head.pressure * t + smooth_pressure * (1 - t) : point.pressure,
});
if (is_pen) {
point.pressure = smooth_pressure;
}
} else {
points.push(point);
}
stroke.head = point;
recompute_dynamic_data(state, context);
}
// Remove prestroke from dynamic data (usually because it's now a real stroke)
function geometry_clear_oldest_prestroke(state, context, player_id) {
if (!state.online) return;
const player = state.players[player_id];
player.strokes.shift();
recompute_dynamic_data(state, context);
}
function add_image(context, image_id, bitmap, p, width, height) {
const gl = context.gl;
let entry = null;
// If bitmap not available yet - create placeholder
// Otherwise - upload actual bitmap
if (bitmap === null) {
entry = {
'texture': gl.createTexture(),
'key': image_id,
'at': {...p},
'raw_at': {...p},
'width': width,
'height': height,
'transform_history': [ p.x, p.y, width, height ],
'transform_head': 4,
};
context.images.push(entry);
} else {
entry = get_image(context, image_id);
}
gl.bindTexture(gl.TEXTURE_2D, entry.texture);
if (bitmap !== null) {
gl.texSubImage2D(gl.TEXTURE_2D, 0, 0, 0, gl.RGBA, gl.UNSIGNED_BYTE, bitmap);
gl.generateMipmap(gl.TEXTURE_2D);
} else {
gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, width, height, 0, gl.RGBA, gl.UNSIGNED_BYTE, new Uint8Array(4 * width * height));
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR_MIPMAP_LINEAR);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.NEAREST);
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);
gl.generateMipmap(gl.TEXTURE_2D);
}
}
function scale_image(image, corner, canvasp) {
let new_width, new_height;
const old_x2 = image.at.x + image.width;
const old_y2 = image.at.y + image.height;
if (corner === 0) {
image.at.x = canvasp.x;
image.at.y = canvasp.y;
new_width = old_x2 - image.at.x;
new_height = old_y2 - image.at.y;
} else if (corner === 1) {
image.at.y = canvasp.y;
new_width = canvasp.x - image.at.x;
new_height = old_y2 - image.at.y;
} else if (corner === 2) {
new_width = canvasp.x - image.at.x;
new_height = canvasp.y - image.at.y;
} else if (corner === 3) {
image.at.x = canvasp.x;
new_width = old_x2 - image.at.x;
new_height = canvasp.y - image.at.y;
}
image.width = new_width;
image.height = new_height;
}
function image_at(context, x, y) {
// Iterate back to front to pick the image at the front first
for (let i = context.images.length - 1; i >= 0; --i) {
const image = context.images[i];
if (!image.deleted) {
const at = image.at;
const w = image.width;
const h = image.height;
const in_x = (at.x <= x && x <= at.x + w) || (at.x + w <= x && x <= at.x);
const in_y = (at.y <= y && y <= at.y + h) || (at.y + h <= y && y <= at.y);
if (in_x && in_y) {
return image;
}
}
}
return null;
}
function image_corner(state, image, canvasp) {
const sp = canvas_to_screen(state, canvasp);
const at = canvas_to_screen(state, image.at);
const w = image.width * state.canvas.zoom;
const h = image.height * state.canvas.zoom;
const width = 8;
if (at.x - width <= sp.x && sp.x <= at.x + width && at.y - width <= sp.y && sp.y <= at.y + width) {
return 0;
}
if (at.x + w - width <= sp.x && sp.x <= at.x + w + width && at.y - width <= sp.y && sp.y <= at.y + width) {
return 1;
}
if (at.x + w - width <= sp.x && sp.x <= at.x + w + width && at.y + h - width <= sp.y && sp.y <= at.y + h + width) {
return 2;
}
if (at.x - width <= sp.x && sp.x <= at.x + width && at.y + h - width <= sp.y && sp.y <= at.y + h + width) {
return 3;
}
return null;
}
function geometry_gen_circle(cx, cy, r, n) {
const step = 2 * Math.PI / n;
const result = [];
for (let i = 0; i < n; ++i) {
const theta = i * step;
const next_theta = (i < n - 1 ? (i + 1) * step : 0);
const x = cx + r * Math.cos(theta);
const y = cy + r * Math.sin(theta);
const next_x = cx + r * Math.cos(next_theta);
const next_y = cy + r * Math.sin(next_theta);
result.push(cx, cy, x, y, next_x, next_y);
}
return result;
}
function geometry_gen_quad(cx, cy, r) {
const result = [
cx - r,
cy - r,
cx + r,
cy - r,
cx - r,
cy + r,
cx + r,
cy + r,
cx - r,
cy + r,
cx + r,
cy - r,
];
return result;
}
function geometry_gen_fullscreen_grid(state, context, step_x, step_y) {
const result = [];
const width = context.canvas.width;
const height = context.canvas.height;
const topleft = screen_to_canvas(state, {'x': 0, 'y': 0});
const bottomright = screen_to_canvas(state, {'x': width, 'y': height});
topleft.x = Math.floor(topleft.x / step_x) * step_x;
topleft.y = Math.ceil(topleft.y / step_y) * step_y;
bottomright.x = Math.floor(bottomright.x / step_x) * step_x;
bottomright.y = Math.ceil(bottomright.y / step_y) * step_y;
for (let y = topleft.y; y <= bottomright.y; y += step_y) {
for (let x = topleft.x; x <= bottomright.x; x += step_x) {
result.push(x, y);
}
}
return result;
}
function geometry_gen_fullscreen_grid_1d(state, context, step_x, step_y) {
const result = [];
const width = context.canvas.width;
const height = context.canvas.height;
const topleft = screen_to_canvas(state, {'x': 0, 'y': 0});
const bottomright = screen_to_canvas(state, {'x': width, 'y': height});
topleft.x = Math.floor(topleft.x / step_x) * step_x;
topleft.y = Math.floor(topleft.y / step_y) * step_y;
bottomright.x = Math.ceil(bottomright.x / step_x) * step_x;
bottomright.y = Math.ceil(bottomright.y / step_y) * step_y;
for (let x = topleft.x; x <= bottomright.x; x += step_x) {
result.push(1, x);
}
for (let y = topleft.y; y <= bottomright.y; y += step_y) {
result.push(-1, y);
}
return result;
}
function geometry_image_quads(state, context) {
const result = new Float32Array(context.images.length * 12);
for (let i = 0; i < context.images.length; ++i) {
const entry = context.images[i];
result[i * 12 + 0] = entry.at.x;
result[i * 12 + 1] = entry.at.y;
result[i * 12 + 2] = entry.at.x + entry.width;
result[i * 12 + 3] = entry.at.y;
result[i * 12 + 4] = entry.at.x;
result[i * 12 + 5] = entry.at.y + entry.height;
result[i * 12 + 6] = entry.at.x + entry.width;
result[i * 12 + 7] = entry.at.y + entry.height;
result[i * 12 + 8] = entry.at.x;
result[i * 12 + 9] = entry.at.y + entry.height;
result[i * 12 + 10] = entry.at.x + entry.width;
result[i * 12 + 11] = entry.at.y;
}
return result;
}
function geometry_generate_handles(state, context, active_image) {
let image = null;
for (const entry of context.images) {
if (entry.key === active_image) {
image = entry;
break;
}
}
const x1 = image.at.x;
const y1 = image.at.y;
const x2 = image.at.x + image.width;
const y2 = image.at.y + image.height;
const width = 4 / state.canvas.zoom;
const points = new Float32Array([
// top-left handle
x1 - width, y1 - width,
x1 + width, y1 - width,
x1 + width, y1 + width,
x1 - width, y1 + width,
x1 - width, y1 - width,
// -> top-right
x1 + width, y1,
x2 - width, y1,
// top-right handle
x2 - width, y1 - width,
x2 + width, y1 - width,
x2 + width, y1 + width,
x2 - width, y1 + width,
x2 - width, y1 - width,
// -> bottom-right
x2, y1 + width,
x2, y2 - width,
// bottom-right handle
x2 - width, y2 - width,
x2 + width, y2 - width,
x2 + width, y2 + width,
x2 - width, y2 + width,
x2 - width, y2 - width,
// -> bottom-left
x2 - width, y2,
x1 + width, y2,
// bottom-left handle
x1 - width, y2 - width,
x1 + width, y2 - width,
x1 + width, y2 + width,
x1 - width, y2 + width,
x1 - width, y2 - width,
// -> top-left
x1, y2 - width,
x1, y1 + width,
]);
const ids = new Uint32Array([
0, 0, 0, 0, 0 | (1 << 31),
1, 1 | (1 << 31),
2, 2, 2, 2, 2 | (1 << 31),
3, 3 | (1 << 31),
4, 4, 4, 4, 4 | (1 << 31),
5, 5 | (1 << 31),
6, 6, 6, 6, 6 | (1 << 31),
7, 7 | (1 << 31),
]);
const pressures = new Uint8Array([
128, 128, 128, 128, 128,
128, 128, 128,
128, 128, 128, 128, 128,
128, 128, 128,
128, 128, 128, 128, 128,
128, 128, 128,
128, 128, 128, 128, 128,
128, 128, 128,
]);
const stroke_data = serializer_create(8 * 4 * 2);
for (let i = 0; i < 8; ++i) {
ser_u16(stroke_data, 34);
ser_u16(stroke_data, 139);
ser_u16(stroke_data, 230);
ser_u16(stroke_data, 0);
}
return {
'points': points,
'ids': ids,
'pressures': pressures,
'stroke_data': stroke_data,
};
}
function geometry_line_segments_with_two_circles(circle_segments) {
const results = new Float32Array((circle_segments * 3 + 6) * 2); // triangle fan circle + two triangles, all 2D (x + y)
// Generate circle as triangle fan at 0, 0 with radius 1
// This circle will be offset/scaled in the vertex shader
let last_phi = ((circle_segments - 1) / circle_segments) * 2 * Math.PI;
for (let i = 0; i < circle_segments; ++i) {
const phi = i / circle_segments * 2 * Math.PI;
const x1 = Math.cos(phi);
const y1 = Math.sin(phi);
const x2 = Math.cos(last_phi);
const y2 = Math.sin(last_phi);
results[i * 6 + 0] = x1;
results[i * 6 + 1] = y1;
results[i * 6 + 2] = x2;
results[i * 6 + 3] = y2;
results[i * 6 + 4] = 0;
results[i * 6 + 5] = 0;
last_phi = phi;
}
return results;
}
function geometry_good_circle_and_dummy(lod) {
const total_points = 3 * Math.pow(2, lod - 1) + 4; // 3, 6, 12, 24, ... + Dummy for line segment
const total_indices = 3 * (Math.pow(3, lod + 1) - 1) / 2 + 6; // 3, 3 + 9, 3 + 9 + 18, ... + Dummy for line segment
const points = tv_create(Float32Array, total_points * 2);
const indices = tv_create(Uint32Array, total_indices);
// Initital triangle, added even for lod = 0
tv_add(indices, 0);
tv_add(indices, 1);
tv_add(indices, 2);
if (lod >= 1) {
tv_add(indices, 0);
tv_add(indices, 3);
tv_add(indices, 1);
tv_add(indices, 1);
tv_add(indices, 4);
tv_add(indices, 2);
tv_add(indices, 2);
tv_add(indices, 5);
tv_add(indices, 0);
}
let last_base = 3;
let last_offset = 0;
for (let i = 0; i < lod; ++i) {
// generate 3 * Math.pow(2, i) points on a circle
const npoints = 3 * Math.pow(2, i);
const base = indices.size;
for (let j = 0; j < npoints; ++j) {
// use every second point (except level 0, where all points are used)
if (i === 0 || (j % 2 === 1)) {
const phi = j / npoints * Math.PI * 2;
const x = Math.sin(phi);
const y = Math.cos(phi);
tv_add(points, x);
tv_add(points, y);
if (i > 1) {
tv_add(indices, indices.data[last_base + last_offset++]);
tv_add(indices, points.size / 2 - 1); // the middle of the trianle is always the newly added point
tv_add(indices, indices.data[last_base + last_offset]);
if (j % 4 == 3) {
last_offset++;
}
}
}
}
if (i > 1) {
last_base = base;
last_offset = 0;
}
}
// 4 dummy points (8 indices) for the line segment
const dummy_base = points.size / 2;
points.size += 8;
tv_add(indices, dummy_base + 0);
tv_add(indices, dummy_base + 1);
tv_add(indices, dummy_base + 2);
tv_add(indices, dummy_base + 3);
tv_add(indices, dummy_base + 2);
tv_add(indices, dummy_base + 1);
return {
'points': points,
'indices': indices
};
}

839
client/webgl_listeners.js

@ -1,839 +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('pointerdown', (e) => mousedown(e, state, context));
context.canvas.addEventListener('pointermove', (e) => mousemove(e, state, context));
context.canvas.addEventListener('pointerup', (e) => mouseup(e, state, context));
context.canvas.addEventListener('pointerleave', (e) => mouseup(e, state, context));
context.canvas.addEventListener('pointerleave', (e) => mouseleave(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));
debug_panel_init(state, context);
}
function debug_panel_init(state, context) {
document.getElementById('debug-red').checked = state.debug.red;
document.getElementById('do-snap').checked = state.snap !== null;
document.getElementById('debug-print').checked = config.debug_print;
document.getElementById('draw-bvh').checked = config.draw_bvh;
document.getElementById('debug-red').addEventListener('change', (e) => {
state.debug.red = e.target.checked;
schedule_draw(state, context);
});
document.getElementById('do-snap').addEventListener('change', (e) => {
state.snap = e.target.checked ? 'grid' : null;
});
document.getElementById('debug-print').addEventListener('change', (e) => {
config.debug_print = e.target.checked;
});
document.getElementById('draw-bvh').addEventListener('change', (e) => {
config.draw_bvh = e.target.checked;
schedule_draw(state, context);
});
document.getElementById('debug-begin-benchmark').addEventListener('click', (e) => {
state.canvas.zoom_level = config.benchmark.zoom_level;
state.canvas.offset.x = config.benchmark.offset.x;
state.canvas.offset.y = config.benchmark.offset.y;
const dz = (state.canvas.zoom_level > 0 ? config.zoom_delta : -config.zoom_delta);
state.canvas.target_zoom = Math.pow(1.0 + dz, Math.abs(state.canvas.zoom_level))
state.canvas.zoom = state.canvas.target_zoom;
state.debug.benchmark_mode = true;
const origin_x = state.canvas.offset.x;
const origin_y = state.canvas.offset.y;
const original_button_text = e.target.innerText;
let frame = 0;
state.debug.on_benchmark = () => {
if (frame >= config.benchmark.frames) {
state.debug.benchmark_mode = false;
e.target.disabled = false;
e.target.innerText = original_button_text;
return false;
}
state.canvas.offset.x = origin_x + Math.round(100 * Math.cos(frame / 360));
state.canvas.offset.y = origin_y + Math.round(100 * Math.sin(frame / 360));
frame += 1;
return true;
}
e.target.disabled = true;
e.target.innerText = 'Benchmark in progress...';
schedule_draw(state, context);
});
}
function cancel(e) {
e.preventDefault();
return false;
}
function zenmode() {
document.querySelector('.pallete-wrapper').classList.toggle('hidden');
document.querySelector('.top-wrapper').classList.toggle('hidden');
}
function enter_picker_mode(state, context) {
if (state.tools.active === 'pencil') { // or other drawing tools
document.querySelector('canvas').classList.add('picker');
document.querySelector('.picker-preview-outer').classList.remove('dhide');
document.querySelector('.brush-dom').classList.add('dhide');
state.colorpicking = true;
const canvasp = screen_to_canvas(state, state.cursor);
update_color_picker_color(state, context, canvasp);
}
}
function exit_picker_mode(state) {
if (state.colorpicking) {
document.querySelector('canvas').classList.remove('picker');
document.querySelector('.picker-preview-outer').classList.add('dhide');
document.querySelector('.brush-dom').classList.remove('dhide');
state.colorpicking = false;
}
}
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 (config.debug_print) {
console.debug('keydown', e.code);
}
const doing_things = (state.moving || state.drawing || state.erasing || state.colorpicking || state.imagemoving || state.imagescaling || state.linedrawing);
if (e.code === 'Space' && !state.drawing) {
state.spacedown = true;
context.canvas.classList.add('movemode');
} else if (e.code === 'Tab') {
e.preventDefault();
zenmode();
} else if (e.code === 'ControlLeft' || e.paddingcode === 'ControlRight') {
enter_picker_mode(state, context);
} else if (e.code === 'Slash') {
document.querySelector('.debug-window').classList.toggle('dhide');
e.preventDefault();
} else if (e.code === 'KeyZ') {
if (e.ctrlKey) {
queue_event(state, undo_event(state));
} else {
state.zoomdown = true;
}
} else if (e.code === 'KeyS') {
if (!doing_things) {
switch_tool(state, document.querySelector('.tool[data-tool="pointer"]'));
}
} else if (e.code === 'KeyD') {
if (!doing_things) {
switch_tool(state, document.querySelector('.tool[data-tool="pencil"]'));
}
} else if (e.code === 'KeyE') {
if (!doing_things) {
switch_tool(state, document.querySelector('.tool[data-tool="eraser"]'));
}
} else if (e.code === 'KeyR') {
if (!doing_things) {
switch_tool(state, document.querySelector('.tool[data-tool="ruler"]'));
}
} else if (e.code === 'Esc') {
cancel_everything(state, context);
}
}
function keyup(e, state, context) {
if (config.debug_print) {
console.debug('keydown', e.code);
}
if (e.code === 'Space' && state.spacedown) {
state.spacedown = false;
state.moving = false;
context.canvas.classList.remove('movemode');
} else if (e.code === 'ControlLeft' || e.code === 'ControlRight') {
exit_picker_mode(state);exit_picker_mode
} else if (e.code === 'KeyZ') {
state.zoomdown = false;
}
}
function mousedown(e, state, context) {
const screenp = {'x': window.devicePixelRatio * e.clientX, 'y': window.devicePixelRatio * e.clientY};
const canvasp = screen_to_canvas(state, screenp);
const raw_canvasp = {...canvasp};
if (state.snap === 'grid') {
const step = grid_snap_step(state);
canvasp.x = Math.round(canvasp.x / step) * step;
canvasp.y = Math.round(canvasp.y / step) * step;
}
if (e.button !== 0 && e.button !== 1) {
return;
}
if (state.zoomdown) {
state.zooming = true;
state.canvas.zoom_screenp = screenp;
return;
}
if (state.colorpicking) {
const color_u32 = color_to_u32(state.color_picked.substring(1));
state.players[state.me].color = color_u32;
update_cursor(state);
fire_event(state, color_event(color_u32));
return;
}
if (state.spacedown || e.button === 1) {
state.moving = true;
context.canvas.classList.add('moving');
if (e.button === 1) {
context.canvas.classList.add('mousemoving');
}
return;
}
if (state.tools.active === 'pencil') {
canvasp.pressure = Math.ceil(e.pressure * 255);
geometry_start_prestroke(state, state.me);
geometry_add_prepoint(state, context, state.me, canvasp, e.pointerType === "pen");
state.drawing = true;
state.active_image = null;
schedule_draw(state, context);
} else if (state.tools.active === 'ruler') {
state.linedrawing = true;
state.ruler_origin = canvasp;
geometry_start_prestroke(state, state.me);
} else if (state.tools.active === 'eraser') {
state.erasing = true;
} else if (state.tools.active === 'pointer') {
state.imagescaling = false;
state.imagemoving = false;
if (state.active_image !== null) {
// Check for resize first, because it supports
// clicking slightly outside of the image
const image = get_image(context, state.active_image);
const corner = image_corner(state, image, raw_canvasp);
if (corner !== null) {
// Resize
state.imagescaling = true;
state.scaling_corner = corner;
document.querySelector('canvas').classList.remove('resize-topleft');
document.querySelector('canvas').classList.remove('resize-topright');
if (corner === 0 || corner === 2) {
document.querySelector('canvas').classList.add('resize-topleft');
} else if (corner === 1 || corner === 3) {
document.querySelector('canvas').classList.add('resize-topright');
}
}
}
// Only do picking logic if we haven't started imagescaling already
if (!state.imagescaling) {
const image = image_at(context, raw_canvasp.x, raw_canvasp.y);
if (image !== null) {
state.active_image = image.key;
// Allow immediately moving
state.imagemoving = true;
state.image_actually_moved = false;
image.raw_at.x = image.at.x;
image.raw_at.y = image.at.y;
} else {
state.active_image = null;
}
}
schedule_draw(state, context);
}
}
function update_color_picker_color(state, context, canvasp) {
const stroke_index = bvh_point(state, canvasp);
let color_under_cursor = color_from_rgbdict(context.bgcolor);
if (stroke_index != null) {
color_under_cursor = color_from_u32(state.events[stroke_index].color);
}
document.querySelector('.picker-preview-inner').style.background = color_under_cursor;
state.color_picked = color_under_cursor;
}
function mousemove(e, state, context) {
e.preventDefault();
let do_draw = false;
const screenp = {'x': window.devicePixelRatio * e.clientX, 'y': window.devicePixelRatio * e.clientY};
const canvasp = screen_to_canvas(state, screenp);
const raw_canvasp = {...canvasp};
if (state.snap === 'grid') {
const step = grid_snap_step(state);
canvasp.x = Math.round(canvasp.x / step) * step;
canvasp.y = Math.round(canvasp.y / step) * step;
}
if (state.tools.active === 'pointer') {
if (state.active_image !== null) {
const image = get_image(context, state.active_image);
const corner = image_corner(state, image, raw_canvasp);
if (state.scaling_corner === null) {
document.querySelector('canvas').classList.remove('resize-topleft');
document.querySelector('canvas').classList.remove('resize-topright');
if (corner === 0 || corner === 2) {
document.querySelector('canvas').classList.add('resize-topleft');
} else if (corner === 1 || corner === 3) {
document.querySelector('canvas').classList.add('resize-topright');
}
}
}
}
if (state.me in state.players) {
const me = state.players[state.me];
const width = Math.max(me.width * state.canvas.zoom, 2.0);
const radius = Math.round(width / 2);
const brush_screen = canvas_to_screen(state, canvasp);
const brush_x = brush_screen.x - radius - 2;
const brush_y = brush_screen.y - radius - 2;
document.querySelector('.brush-dom').style.transform = `translate(${brush_x}px, ${brush_y}px)`;
}
if (state.me in state.players && dist_v2(state.players[state.me].cursor, canvasp) > 5) {
state.players[state.me].cursor = canvasp;
fire_event(state, movecursor_event(canvasp.x, canvasp.y));
}
if (state.colorpicking) {
update_color_picker_color(state, context, canvasp);
}
if (state.zooming) {
const zooming_in = e.movementY > 0;
const zooming_out = e.movementY < 0;
let zoom_level = null;
if (zooming_in) {
zoom_level = state.canvas.zoom_level + 1
} else if (zooming_out) {
zoom_level = state.canvas.zoom_level - 1;
} else {
return;
}
if (zoom_level < config.min_zoom_level || zoom_level > config.max_zoom_level) {
return;
}
const dz = (zoom_level > 0 ? config.zoom_delta : -config.zoom_delta);
state.canvas.zoom_level = zoom_level;
state.canvas.target_zoom = Math.pow(1.0 + dz, Math.abs(zoom_level))
do_draw = true;
}
if (state.moving) {
state.canvas.offset.x += e.movementX;
state.canvas.offset.y += e.movementY;
// If we are moving our canvas, we don't need to follow anymore
if (state.following_player !== null) {
toggle_follow_player(state, state.following_player);
}
fire_event(state, movecanvas_event(state));
draw_html(state, context);
do_draw = true;
}
if (state.imagescaling) {
const image = get_image(context, state.active_image);
scale_image(image, state.scaling_corner, canvasp);
do_draw = true;
}
if (state.imagemoving) {
const image = get_image(context, state.active_image);
if (image !== null) {
const dx = e.movementX / state.canvas.zoom;
const dy = e.movementY / state.canvas.zoom;
image.raw_at.x += dx;
image.raw_at.y += dy;
if (state.snap === 'grid') {
const step = grid_snap_step(state);
image.at.x = Math.round(image.raw_at.x / step) * step;
image.at.y = Math.round(image.raw_at.y / step) * step;
} else if (state.snap === null) {
image.at.x = image.raw_at.x;
image.at.y = image.raw_at.y;
}
state.image_actually_moved = true;
do_draw = true;
}
}
if (state.drawing) {
canvasp.pressure = Math.ceil(e.pressure * 255);
geometry_add_prepoint(state, context, state.me, canvasp, e.pointerType === "pen");
fire_event(state, predraw_event(canvasp.x, canvasp.y));
do_draw = true;
}
if (state.erasing) {
const me = state.players[state.me];
const radius = Math.round(me.width / 2);
const last_canvasp = screen_to_canvas(state, state.cursor);
const cursor_bbox = {
'x1': Math.min(canvasp.x, last_canvasp.x) - radius,
'y1': Math.min(canvasp.y, last_canvasp.y) - radius,
'x2': Math.max(canvasp.x, last_canvasp.x) + radius,
'y2': Math.max(canvasp.y, last_canvasp.y) + radius,
};
tv_ensure(state.erase_candidates, round_to_pow2(state.stroke_count, 4096));
tv_clear(state.erase_candidates);
// Rough pass, not all of these might actually need to be erased
bvh_intersect_quad(state, state.bvh, cursor_bbox, state.erase_candidates);
// Fine pass, actually run expensive capsule vs capsule intersection tests
for (let i = 0; i < state.erase_candidates.size; ++i) {
const stroke_id = state.erase_candidates.data[i];
const stroke = state.events[stroke_id];
if (!stroke.deleted && stroke_intersects_capsule(state, stroke, last_canvasp, canvasp, radius)) {
stroke.deleted = true;
bvh_delete_stroke(state, stroke);
queue_event(state, eraser_event(stroke_id));
do_draw = true;
}
}
}
if (state.linedrawing) {
const p1 = {'x': state.ruler_origin.x, 'y': state.ruler_origin.y, 'pressure': 128};
const p2 = {'x': canvasp.x, 'y': canvasp.y, 'pressure': 128};
if (state.online) {
const me = state.players[state.me];
const prestroke = me.strokes[me.strokes.length - 1]; // TODO: might as well be me.strokes[0] ?
prestroke.points.length = 2;
prestroke.points[0] = p1;
prestroke.points[1] = p2;
recompute_dynamic_data(state, context);
do_draw = true;
}
}
if (do_draw) {
schedule_draw(state, context);
}
state.cursor = screenp;
return false;
}
function mouseup(e, state, context) {
const screenp = {'x': window.devicePixelRatio * e.clientX, 'y': window.devicePixelRatio * e.clientY};
const canvasp = screen_to_canvas(state, screenp);
const raw_canvasp = {...canvasp};
if (state.snap === 'grid') {
const step = grid_snap_step(state);
canvasp.x = Math.round(canvasp.x / step) * step;
canvasp.y = Math.round(canvasp.y / step) * step;
}
if (e.button !== 0 && e.button !== 1) {
return;
}
if (state.zooming) {
state.zooming = false;
return;
}
if (state.imagemoving) {
state.imagemoving = false;
if (state.image_actually_moved) {
state.image_actually_moved = false;
const image = get_image(context, state.active_image);
image.raw_at.x = image.at.x;
image.raw_at.y = image.at.y;
queue_event(state, image_move_event(state.active_image, image.at.x, image.at.y));
schedule_draw(state, context);
}
return;
}
if (state.imagescaling) {
queue_event(state, image_scale_event(state.active_image, state.scaling_corner, canvasp.x, canvasp.y));
state.imagescaling = false;
state.scaling_corner = null;
return;
}
if (state.moving || e.button === 1) {
state.moving = false;
context.canvas.classList.remove('moving');
if (e.button === 1) {
context.canvas.classList.remove('mousemoving');
}
return;
}
if (state.drawing) {
const stroke = geometry_prepare_stroke(state);
if (stroke) {
// TODO: be able to add a baked stroke locally
queue_event(state, stroke_event(state));
schedule_draw(state, context);
}
fire_event(state, lift_event());
state.drawing = false;
return;
}
if (state.erasing) {
state.erasing = false;
return;
}
if (state.linedrawing) {
state.linedrawing = false;
queue_event(state, stroke_event(state));
schedule_draw(state, context);
return;
}
}
function mouseleave(e, state, context) {
if (state.moving) {
state.moving = false;
context.canvas.classList.remove('movemode');
}
//exit_picker_mode(state);
// something else?
}
function update_cursor(state) {
if (!(state.me in state.players)) {
// we not ready yet
return;
}
const me = state.players[state.me];
const width = Math.max(me.width * state.canvas.zoom, 2.0);
const radius = Math.round(width / 2);
let svg;
if (state.tools.active === 'pencil' || state.tools.active === 'ruler') {
const current_color = color_from_u32(me.color);
const stroke = (me.color === 0xFFFFFF ? 'black' : 'white');
svg = `<svg style="display: block" xmlns="http://www.w3.org/2000/svg" width="${width + 4}" height="${width + 4}">
<circle cx="${radius + 2}" cy="${radius + 2}" r="${radius}" stroke="${stroke}" fill="none" stroke-width="3"/>
<circle cx="${radius + 2}" cy="${radius + 2}" r="${radius}" stroke="none" fill="${current_color}" stroke-width="1"/>
</svg>`.replaceAll('\n', ' ');
} else if (state.tools.active === 'eraser') {
const current_color = '#ffffff';
const stroke = '#000000';
svg = `<svg style="display: block" xmlns="http://www.w3.org/2000/svg" width="${width + 4}" height="${width + 4}">
<circle cx="${radius + 2}" cy="${radius + 2}" r="${radius}" stroke="${stroke}" fill="none" stroke-width="3"/>
<circle cx="${radius + 2}" cy="${radius + 2}" r="${radius}" stroke="none" fill="${current_color}" stroke-width="1"/>
</svg>`.replaceAll('\n', ' ');
}
document.querySelector('.brush-dom').innerHTML = svg;
const brush_x = state.cursor.x - width / 2 - 2;
const brush_y = state.cursor.y - width / 2 - 2;
document.querySelector('.brush-dom').style.transform = `translate(${Math.round(brush_x)}px, ${Math.round(brush_y)}px)`;
}
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 zooming_in = e.deltaY < 0;
const zoom_level = zooming_in ? state.canvas.zoom_level + 2 : state.canvas.zoom_level - 2;
if (zoom_level < config.min_zoom_level || zoom_level > config.max_zoom_level) {
return;
}
const dz = (zoom_level > 0 ? config.zoom_delta : -config.zoom_delta);
state.canvas.zoom_level = zoom_level;
state.canvas.target_zoom = Math.pow(1.0 + dz, Math.abs(zoom_level))
state.canvas.zoom_screenp = screenp;
// If we are moving our canvas, we don't need to follow anymore
if (state.following_player !== null) {
toggle_follow_player(state, state.following_player);
}
fire_event(state, zoomcanvas_event(state, canvasp.x, canvasp.y));
schedule_draw(state, context);
}
function start_move(e, state, context) {
// two touch identifiers are expected to be pushed into state.touch.ids at this point
// TODO: @touch, remove preview
fire_event(state, clear_event(state)); // Tell others to hide predraws of this stroke
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;
}
}
}
function touchstart(e, state, context) {
e.preventDefault();
// 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);
state.touch.drawing = true;
setTimeout(() => {
state.touch.waiting_for_second_finger = false;
}, config.second_finger_timeout);
} else {
state.touch.ids.push(e.changedTouches[0].identifier);
state.touch.ids.push(e.changedTouches[1].identifier);
start_move(e, state, context);
}
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);
start_move(e, state, context);
}
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;
}
state.touch.moves += 1;
if (state.touch.moves > config.buffer_first_touchmoves) {
// At this point touch with second finger will NOT start a pan
state.touch.waiting_for_second_finger = false;
}
canvasp.pressure = 128; // TODO: check out touch devices' e.pressure
// TODO: fix when doing @touch
//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;
}
// If we are moving our canvas, we don't need to follow anymore
if (state.following_player !== null) {
toggle_follow_player(state, state.following_player);
}
state.touch.first_finger_position = first_finger_position;
state.touch.second_finger_position = second_finger_position;
fire_event(state, movecanvas_event(state));
draw_html(state, context);
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 stroke = geometry_prepare_stroke(state);
if (stroke) {
queue_event(state, stroke_event(state));
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);
return false;
}
function cancel_everything(state, context) {
}

460
client/webgl_shaders.js

@ -1,460 +0,0 @@
const sdf_vs_src = `#version 300 es
in vec2 a_pos; // for the joins/caps these are relative positions. for line segments these are dummy values
in vec2 a_a; // point from
in vec2 a_b; // point to
in int a_stroke_id;
in vec2 a_pressure;
uniform vec2 u_scale;
uniform vec2 u_res;
uniform vec2 u_translation;
uniform int u_stroke_count;
uniform int u_stroke_texture_size;
uniform highp usampler2D u_stroke_data;
uniform int u_circle_points;
out vec3 v_color;
flat out vec2 v_thickness;
void main() {
vec2 screen02;
int vertex_index = gl_VertexID;
int stroke_index = a_stroke_id;
if (a_stroke_id >> 31 != 0) {
stroke_index = a_stroke_id & 0x7FFFFFFF;
}
int stroke_data_y = stroke_index / u_stroke_texture_size;
int stroke_data_x = stroke_index % u_stroke_texture_size;
uvec4 stroke_data = texelFetch(u_stroke_data, ivec2(stroke_data_x, stroke_data_y), 0);
float radius = float(stroke_data.w);
vec2 pos;
if (vertex_index < u_circle_points) {
pos = a_a + a_pos * radius * a_pressure.x;
} else {
int vertex_index_line = vertex_index - u_circle_points;
vec2 line_dir = normalize(a_b - a_a);
vec2 up_dir = vec2(line_dir.y, -line_dir.x);
// connecting line
if (vertex_index_line == 0) {
// top left
pos = a_a + up_dir * radius * a_pressure.x;
} else if (vertex_index_line == 1 || vertex_index_line == 5) {
// top right
pos = a_b + up_dir * radius * a_pressure.y;
} else if (vertex_index_line == 2 || vertex_index_line == 4) {
// bottom left
pos = a_a - up_dir * radius * a_pressure.x;
} else {
// bottom right
pos = a_b - up_dir * radius * a_pressure.y;
}
}
screen02 = (pos.xy * u_scale + u_translation) / u_res * 2.0;
screen02.y = 2.0 - screen02.y;
v_color = vec3(stroke_data.xyz) / 255.0;
if (a_stroke_id >> 31 != 0 && (vertex_index >= u_circle_points)) {
screen02 += vec2(100.0); // shift offscreen
}
gl_Position = vec4(screen02 - 1.0, (float(stroke_index) / float(u_stroke_count)) * 2.0 - 1.0, 1.0);
}
`;
const sdf_fs_src = `#version 300 es
precision highp float;
uniform int u_debug_mode;
in vec3 v_color;
layout(location = 0) out vec4 FragColor;
void main() {
if (u_debug_mode == 0) {
float alpha = 0.75;
FragColor = vec4(v_color * alpha, alpha);
} else {
FragColor = vec4(0.2, 0.0, 0.0, 0.2);
}
}
`;
const tquad_vs_src = `#version 300 es
in vec2 a_pos;
uniform vec2 u_scale;
uniform vec2 u_res;
uniform vec2 u_translation;
out vec2 v_texcoord;
void main() {
vec2 screen01 = (a_pos * u_scale + u_translation) / u_res;
vec2 screen02 = screen01 * 2.0;
int vertex_index = gl_VertexID % 6;
if (vertex_index == 0) {
v_texcoord = vec2(0.0, 0.0);
} else if (vertex_index == 1 || vertex_index == 5) {
v_texcoord = vec2(1.0, 0.0);
} else if (vertex_index == 2 || vertex_index == 4) {
v_texcoord = vec2(0.0, 1.0);
} else {
v_texcoord = vec2(1.0, 1.0);
}
screen02.y = 2.0 - screen02.y;
vec2 screen11 = screen02 - 1.0;
gl_Position = vec4(screen11, 0, 1);
}
`;
const tquad_fs_src = `#version 300 es
precision highp float;
in vec2 v_texcoord;
uniform sampler2D u_texture;
uniform int u_solid;
uniform vec4 u_color;
layout(location = 0) out vec4 FragColor;
void main() {
if (u_solid == 0) {
FragColor = texture(u_texture, v_texcoord);
} else {
FragColor = u_color;
}
}
`;
const grid_vs_src = `#version 300 es
in vec2 a_data; // per-instance
out float v_fadeout;
uniform vec2 u_scale;
uniform vec2 u_res;
uniform vec2 u_translation;
uniform float u_fadeout;
void main() {
vec2 origin;
vec2 minor_offset;
vec2 major_offset;
vec2 pixel = 2.0 / u_res;
if (a_data.x > 0.0) {
// Vertical, treat Y as X
float x = a_data.y;
origin = vec2(x, 0.0);
minor_offset = pixel * vec2(1.0, 0.0);
major_offset = vec2(0.0, 2.0);
} else {
// Horizontal, treat Y as Y
float y = a_data.y;
origin = vec2(0.0, y);
minor_offset = pixel * vec2(0.0, 1.0);
major_offset = vec2(2.0, 0.0);
}
vec2 v = (origin * u_scale + u_translation) / u_res * 2.0;
vec2 pos;
if (a_data.x > 0.0) {
v.y = 0.0;
} else {
v.x = 0.0;
}
if (gl_VertexID % 6 == 0) {
pos = v;
} else if (gl_VertexID % 6 == 1 || gl_VertexID % 6 == 5) {
pos = v + (a_data.x > 0.0 ? minor_offset : major_offset);
//pos = v + minor_offset;
} else if (gl_VertexID % 6 == 2 || gl_VertexID % 6 == 4) {
pos = v + (a_data.x > 0.0 ? major_offset : minor_offset);
//pos = v + major_offset;
} else if (gl_VertexID % 6 == 3) {
pos = v + major_offset + minor_offset;
//pos = v + major_offset + minor_offset;
}
vec2 screen02 = pos;
screen02.y = 2.0 - screen02.y;
v_fadeout = u_fadeout;
gl_Position = vec4(screen02 - 1.0, 0.0, 1.0);
}
`;
const dots_vs_src = `#version 300 es
in vec2 a_center; // per-instance
out float v_fadeout;
uniform vec2 u_scale;
uniform vec2 u_res;
uniform vec2 u_translation;
uniform float u_fadeout;
void main() {
vec2 v = (a_center * u_scale + u_translation) / u_res * 2.0;
vec2 pos;
vec2 pixel = 2.0 / u_res;
if (gl_VertexID % 6 == 0) {
pos = v + pixel * vec2(-1.0);
} else if (gl_VertexID % 6 == 1) {
pos = v + pixel * vec2(1.0, -1.0);
} else if (gl_VertexID % 6 == 2) {
pos = v + pixel * vec2(-1.0, 1.0);
} else if (gl_VertexID % 6 == 3) {
pos = v + pixel * vec2(1.0);
} else if (gl_VertexID % 6 == 4) {
pos = v + pixel * vec2(-1.0, 1.0);
} else if (gl_VertexID % 6 == 5) {
pos = v + pixel * vec2(1.0, -1.0);
}
vec2 screen02 = pos;
screen02.y = 2.0 - screen02.y;
v_fadeout = u_fadeout;
gl_Position = vec4(screen02 - 1.0, 0.0, 1.0);
}
`;
const dots_fs_src = `#version 300 es
precision highp float;
in float v_fadeout;
layout(location = 0) out vec4 FragColor;
void main() {
vec3 color = vec3(0.5);
FragColor = vec4(color * v_fadeout, v_fadeout);
}
`;
//
const iquad_vs_src = `#version 300 es
in vec2 a_topleft; // per-instance
in vec2 a_bottomright; // per-instance
uniform vec2 u_scale;
uniform vec2 u_res;
uniform vec2 u_translation;
out vec3 v_color;
void main() {
vec2 pos;
int vertex_index = gl_VertexID % 6;
if (vertex_index == 0) {
// top left
pos = a_topleft;
} else if (vertex_index == 1 || vertex_index == 5) {
// top right
pos = vec2(a_bottomright.x, a_topleft.y);
} else if (vertex_index == 2 || vertex_index == 4) {
// bottom left
pos = vec2(a_topleft.x, a_bottomright.y);
} else {
// bottom right
pos = a_bottomright;
}
v_color = vec3(
float(int(a_topleft.x) * 908125 % 255) / 255.0,
float(int(a_topleft.y) * 257722 % 255) / 255.0,
float(int(a_bottomright.y) * 826586 % 255) / 255.0
);
vec2 screen01 = (pos * u_scale + u_translation) / u_res;
vec2 screen02 = screen01 * 2.0;
screen02.y = 2.0 - screen02.y;
vec2 screen11 = screen02 - 1.0;
gl_Position = vec4(screen11, 0.0, 1.0);
}
`;
const iquad_fs_src = `#version 300 es
precision highp float;
layout(location = 0) out vec4 FragColor;
in vec3 v_color;
void main() {
FragColor = vec4(v_color, 0.5);
}
`;
function init_webgl(state, context) {
context.canvas = document.querySelector('#c');
context.gl = context.canvas.getContext('webgl2', {
'preserveDrawingBuffer': true,
'desynchronized': true,
'antialias': true,
});
const gl = context.gl;
gl.enable(gl.BLEND);
gl.blendFunc(gl.ONE, gl.ONE_MINUS_SRC_ALPHA);
gl.enable(gl.DEPTH_TEST);
gl.depthFunc(gl.NOTEQUAL);
context.gpu_timer_ext = gl.getExtension('EXT_disjoint_timer_query_webgl2');
if (context.gpu_timer_ext === null) {
context.gpu_timer_ext = gl.getExtension('EXT_disjoint_timer_query');
}
const quad_vs = create_shader(gl, gl.VERTEX_SHADER, tquad_vs_src);
const quad_fs = create_shader(gl, gl.FRAGMENT_SHADER, tquad_fs_src);
const sdf_vs = create_shader(gl, gl.VERTEX_SHADER, sdf_vs_src);
const sdf_fs = create_shader(gl, gl.FRAGMENT_SHADER, sdf_fs_src);
const dots_vs = create_shader(gl, gl.VERTEX_SHADER, dots_vs_src);
const dots_fs = create_shader(gl, gl.FRAGMENT_SHADER, dots_fs_src);
const grid_vs = create_shader(gl, gl.VERTEX_SHADER, grid_vs_src);
const iquad_vs = create_shader(gl, gl.VERTEX_SHADER, iquad_vs_src);
const iquad_fs = create_shader(gl, gl.FRAGMENT_SHADER, iquad_fs_src);
context.programs = {
'image': create_program(gl, quad_vs, quad_fs),
'main': create_program(gl, sdf_vs, sdf_fs),
'dots': create_program(gl, dots_vs, dots_fs),
'grid': create_program(gl, grid_vs, dots_fs),
'iquad': create_program(gl, iquad_vs, iquad_fs),
};
context.buffers = {
'b_images': gl.createBuffer(),
'b_strokes_static': gl.createBuffer(),
'i_strokes_static': gl.createBuffer(),
'b_strokes_dynamic': gl.createBuffer(),
'b_instance_dot': gl.createBuffer(),
'b_instance_grid': gl.createBuffer(),
'b_dot': gl.createBuffer(),
'b_hud': gl.createBuffer(),
'b_iquads': gl.createBuffer(),
};
context.textures = {
'stroke_data': gl.createTexture(),
'dynamic_stroke_data': gl.createTexture(),
'ui': gl.createTexture(),
};
gl.bindTexture(gl.TEXTURE_2D, context.textures['stroke_data']);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.NEAREST);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.NEAREST);
gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA16UI, config.stroke_texture_size, config.stroke_texture_size, 0, gl.RGBA_INTEGER, gl.UNSIGNED_SHORT, new Uint16Array(config.stroke_texture_size * config.stroke_texture_size * 4)); // fill the whole texture once with zeroes to kill a warning about a partial upload
gl.bindTexture(gl.TEXTURE_2D, context.textures['dynamic_stroke_data']);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.NEAREST);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.NEAREST);
gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA16UI, config.dynamic_stroke_texture_size, config.dynamic_stroke_texture_size, 0, gl.RGBA_INTEGER, gl.UNSIGNED_SHORT, new Uint16Array(config.dynamic_stroke_texture_size * config.dynamic_stroke_texture_size * 4)); // fill the whole texture once with zeroes to kill a warning about a partial upload
gl.bindTexture(gl.TEXTURE_2D, context.textures['ui']);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.NEAREST);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.NEAREST);
gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA16UI, config.ui_texture_size, config.ui_texture_size, 0, gl.RGBA_INTEGER, gl.UNSIGNED_SHORT, new Uint16Array(config.ui_texture_size * config.ui_texture_size * 4)); // fill the whole texture once with zeroes to kill a warning about a partial upload
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)) {
// src: tiny-sdf
// https://github.com/mapbox/tiny-sdf
const wrapper = {program};
const num_attrs = gl.getProgramParameter(program, gl.ACTIVE_ATTRIBUTES);
const num_uniforms = gl.getProgramParameter(program, gl.ACTIVE_UNIFORMS);
wrapper.locations = {};
for (let i = 0; i < num_attrs; i++) {
const attribute = gl.getActiveAttrib(program, i);
wrapper.locations[attribute.name] = gl.getAttribLocation(program, attribute.name);
}
for (let i = 0; i < num_uniforms; i++) {
const uniform = gl.getActiveUniform(program, i);
wrapper.locations[uniform.name] = gl.getUniformLocation(program, uniform.name);
}
return wrapper;
}
console.error('link:', gl.getProgramInfoLog(program));
gl.deleteProgram(program);
}

50
client/websocket.js

@ -6,41 +6,24 @@
// //
// Details best described here: https://github.com/kee-org/KeeFox/issues/189 // Details best described here: https://github.com/kee-org/KeeFox/issues/189
async function ws_connect(state, context, first_connect = false) { function ws_connect(first_connect = false) {
const session_id = localStorage.getItem('sessionId') || '0'; const session_id = ls.getItem('sessionId') || '0';
const desk_id = state.desk_id; const desk_id = storage.desk_id;
try {
const resp = await fetch(config.ping_url);
if (resp.ok) {
const text = await resp.text();
if (text === 'pong') {
ws = new WebSocket(`${config.ws_url}?deskId=${desk_id}&sessionId=${session_id}`); ws = new WebSocket(`${config.ws_url}?deskId=${desk_id}&sessionId=${session_id}`);
ws.addEventListener('open', () => on_open(state)); ws.addEventListener('open', on_open);
ws.addEventListener('message', (e) => on_message(state, context, e)); ws.addEventListener('message', on_message);
ws.addEventListener('error', () => on_error(state, context)); ws.addEventListener('error', on_error);
ws.addEventListener('close', () => on_close(state, context)); ws.addEventListener('close', on_close);
return;
}
}
} catch (e) {
console.log('Could not ping the server:', e);
}
state.timers.offline_toast = setTimeout(() => ws_connect(state, context, first_connect), config.ws_reconnect_timeout);
} }
function on_open(state) { function on_open() {
clearTimeout(state.timers.offline_toast); clearTimeout(storage.timers.ws_reconnect);
ui_online();
if (config.debug_print) console.debug('open') if (config.debug_print) console.debug('open')
} }
async function on_message(state, context, event) { async function on_message(event) {
const data = event.data; const data = event.data;
let message_data = null; let message_data = null;
@ -48,7 +31,7 @@ async function on_message(state, context, event) {
message_data = await data.arrayBuffer(); message_data = await data.arrayBuffer();
const view = new DataView(message_data); const view = new DataView(message_data);
const d = deserializer_create(message_data, view); const d = deserializer_create(message_data, view);
handle_message(state, context, d); await handle_message(d);
} else { } else {
/* For all my Safari < 14 bros out there */ /* For all my Safari < 14 bros out there */
const reader = new FileReader(); const reader = new FileReader();
@ -56,20 +39,19 @@ async function on_message(state, context, event) {
message_data = e.target.result; message_data = e.target.result;
const view = new DataView(message_data); const view = new DataView(message_data);
const d = deserializer_create(message_data, view); const d = deserializer_create(message_data, view);
handle_message(state, context, d); await handle_message(d);
}; };
reader.readAsArrayBuffer(data); reader.readAsArrayBuffer(data);
} }
} }
function on_close(state, context) { function on_close() {
state.timers.offline_toast = setTimeout(() => ui_offline(), config.initial_offline_timeout);
ws = null; ws = null;
if (config.debug_print) console.debug('close'); if (config.debug_print) console.debug('close');
ws_connect(state, context, false); storage.timers.ws_reconnect = setTimeout(ws_connect, config.ws_reconnect_timeout);
} }
function on_error(state, context) { function on_error() {
ws.close(state, context); ws.close();
} }

2
server/config.js

@ -2,6 +2,4 @@ export const HOST = '127.0.0.1';
export const PORT = 3003; export const PORT = 3003;
export const DATADIR = 'data'; export const DATADIR = 'data';
export const SYNC_TIMEOUT = 1000; export const SYNC_TIMEOUT = 1000;
export const SYNC_MAX_ATTEMPTS = 3;
export const IMAGEDIR = 'images'; export const IMAGEDIR = 'images';
export const DEBUG_PRINT = true;

BIN
server/data-local.sqlite

Binary file not shown.

101
server/deserializer.js

@ -26,117 +26,40 @@ export function u32(d) {
return value; return value;
} }
export function s32(d) { function u16array(d, count) {
const value = d.view.getInt32(d.offset, true); const array = new Uint16Array(d.view.buffer, d.offset, count);
d.offset += 4; d.offset += count * 2;
return value;
}
export function f32(d) {
const value = d.view.getFloat32(d.offset, true);
d.offset += 4;
return value;
}
function f32array(d, count) {
const array = new Float32Array(d.view.buffer, d.offset, count);
d.offset += count * 4;
return array;
}
function u8array(d, count) {
const array = new Uint8Array(d.view.buffer, d.offset, count);
d.offset += count;
return array; return array;
} }
export function align(d, to) {
while (d.offset % to !== 0) {
d.offset++;
}
}
export function event(d) { export function event(d) {
const event = {}; const event = {};
event.type = u32(d); event.type = u8(d);
u8(d); // padding
switch (event.type) { switch (event.type) {
case EVENT.PREDRAW: { case EVENT.PREDRAW: {
event.x = f32(d); event.x = u16(d);
event.y = f32(d); event.y = u16(d);
break;
}
case EVENT.MOVE_CURSOR: {
event.x = f32(d);
event.y = f32(d);
break;
}
case EVENT.MOVE_CANVAS: {
event.offset_x = u32(d);
event.offset_y = u32(d);
event.zoom_level = s32(d);
break;
}
case EVENT.ZOOM_CANVAS: {
event.zoom_level = s32(d);
event.zoom_cx = f32(d);
event.zoom_cy = f32(d);
break;
}
case EVENT.CLEAR:
case EVENT.LIFT: {
break;
}
case EVENT.SET_COLOR: {
event.color = u32(d);
break;
}
case EVENT.SET_WIDTH: {
event.width = u16(d);
break; break;
} }
case EVENT.STROKE: { case EVENT.STROKE: {
// point_count + width align to 4 bytes :D
const point_count = u16(d); const point_count = u16(d);
const width = u16(d); const width = u16(d);
const color = u32(d); const color = u32(d);
event.width = width; event.width = width;
event.color = color; event.color = color;
event.points = f32array(d, point_count * 2); event.points = u16array(d, point_count * 2);
event.pressures = u8array(d, point_count);
align(d, 4);
break;
}
case EVENT.IMAGE: {
event.image_id = u32(d);
event.x = f32(d);
event.y = f32(d);
event.width = u32(d);
event.height = u32(d);
break; break;
} }
case EVENT.IMAGE:
case EVENT.IMAGE_MOVE: { case EVENT.IMAGE_MOVE: {
event.image_id = u32(d); event.image_id = u32(d);
event.x = f32(d); event.x = u16(d);
event.y = f32(d); event.y = u16(d);
break;
}
case EVENT.IMAGE_SCALE: {
event.image_id = u32(d);
event.corner = u32(d);
event.x = f32(d);
event.y = f32(d);
break; break;
} }
@ -151,7 +74,7 @@ export function event(d) {
} }
default: { default: {
console.error('fuck', event.type); console.error('fuck');
console.trace(); console.trace();
process.exit(1); process.exit(1);
} }

18
server/enums.js

@ -6,23 +6,11 @@ export const SESSION = Object.freeze({
export const EVENT = Object.freeze({ export const EVENT = Object.freeze({
PREDRAW: 10, PREDRAW: 10,
SET_COLOR: 11,
SET_WIDTH: 12,
CLEAR: 13,
MOVE_CURSOR: 14,
LIFT: 15,
LEAVE: 16,
MOVE_CANVAS: 17,
USER_JOINED: 18,
ZOOM_CANVAS: 19,
STROKE: 20, STROKE: 20,
UNDO: 30, UNDO: 30,
REDO: 31, REDO: 31,
IMAGE: 40, IMAGE: 40,
IMAGE_MOVE: 41, IMAGE_MOVE: 41,
IMAGE_SCALE: 42,
ERASER: 50, ERASER: 50,
}); });
@ -33,5 +21,9 @@ export const MESSAGE = Object.freeze({
FULL: 103, FULL: 103,
FIRE: 104, FIRE: 104,
JOIN: 105, JOIN: 105,
FOLLOW: 106, });
export const SNS = Object.freeze({
DESK: 1,
SESSION: 2,
}); });

10
server/http.js

@ -7,11 +7,13 @@ export async function route(req) {
if (url.pathname === '/api/image') { if (url.pathname === '/api/image') {
const desk_id = url.searchParams.get('deskId') || '0'; const desk_id = url.searchParams.get('deskId') || '0';
const formdata = await req.formData(); const formData = await req.formData();
const file = formdata.get('file'); const file = formData.get('file');
const image_id = math.crypto_random32(); const image_id = math.fast_random32();
Bun.write(config.IMAGEDIR + '/' + image_id, file); await Bun.write(config.IMAGEDIR + '/' + image_id, file);
storage.put_image(image_id, desk_id);
return new Response(image_id); return new Response(image_id);
} else if (url.pathname === '/api/ping') { } else if (url.pathname === '/api/ping') {

4
server/math.js

@ -9,6 +9,6 @@ export function crypto_random32() {
return dataview.getUint32(0); return dataview.getUint32(0);
} }
export function round_to_pow2(value, multiple) { export function fast_random32() {
return (value + multiple - 1) & -multiple; return Math.floor(Math.random() * 4294967296);
} }

85
server/milton.js

@ -1,85 +0,0 @@
import * as storage from './storage.js'
import * as math from './math.js'
import { SESSION, MESSAGE, EVENT } from './enums';
let first_point_x = null;
let first_point_y = null;
function parse_and_insert_stroke(desk_id, line) {
const words = line.split(' ');
const width = parseInt(words.shift());
const data = new Float32Array(words.map(i => parseFloat(i)));
const points = new Float32Array(data.length / 3 * 2);
const pressures = new Uint8Array(data.length / 3);
if (first_point_x === null) {
first_point_x = points[0];
first_point_y = points[1];
}
for (let i = 0; i < data.length; i += 3) {
points[i / 3 * 2 + 0] = data[i + 0];
points[i / 3 * 2 + 1] = data[i + 1];
pressures[i / 3 + 0] = Math.floor(data[i + 2] * 255);
}
const stroke_res = storage.queries.insert_stroke.get({
'$width': width,
'$color': 0,
'$points': points,
'$pressures': pressures,
});
storage.queries.insert_event.run({
'$type': EVENT.STROKE,
'$desk_id': desk_id,
'$session_id': 0,
'$stroke_id': stroke_res.id,
'$image_id': 0,
'$corner': 0,
'$x': 0,
'$y': 0,
'$width': 0,
'$height': 0,
});
}
async function import_milton_file_to_sqlite(fullpath) {
storage.startup();
const desk_id = 9881; // math.fast_random32();
console.log(`Importing ${fullpath} into desk ${desk_id}`);
storage.queries.insert_desk.run({
'$id': desk_id,
'$title': `Desk ${desk_id}`
});
const input_file = Bun.file(fullpath);
const input_text = await input_file.text();
const input_lines = input_text.split('\n');
for (let i = 0; i < input_lines.length; ++i) {
console.log(`Importing ${i}/${input_lines.length}`);
parse_and_insert_stroke(desk_id, input_lines[i]);
}
console.log(`Finished importing desk ${desk_id}`);
}
async function set_dimentions_to_images(fullpath) {
const images = [
//
];
storage.startup();
for (const image of images) {
storage.db.run(`UPDATE events SET width = ${image.w}, height = ${image.h} WHERE image_id = ${image.t};`);
}
}
set_dimentions_to_images();

159
server/recv.js

@ -2,10 +2,9 @@ import * as des from './deserializer';
import * as send from './send'; import * as send from './send';
import * as math from './math'; import * as math from './math';
import * as storage from './storage'; import * as storage from './storage';
import * as config from './config';
import { SESSION, MESSAGE, EVENT } from './enums'; import { SESSION, MESSAGE, EVENT } from './enums';
import { sessions, desks, queries } from './storage'; import { sessions, desks } from './storage';
// Session ACKed events up to SN // Session ACKed events up to SN
function recv_ack(d, session) { function recv_ack(d, session) {
@ -13,28 +12,53 @@ function recv_ack(d, session) {
session.state = SESSION.READY; session.state = SESSION.READY;
session.sn = sn; session.sn = sn;
session.sync_attempts = 0;
if (config.DEBUG_PRINT) console.log(`ack ${sn} in`); console.log(`ack ${sn} in`);
}
function handle_event(session, event) {
switch (event.type) {
case EVENT.STROKE: {
event.stroke_id = math.fast_random32();
storage.put_stroke(event.stroke_id, session.desk_id, event.points, event.width, event.color);
storage.put_event(event);
break;
}
case EVENT.ERASER:
case EVENT.IMAGE:
case EVENT.IMAGE_MOVE:
case EVENT.UNDO: {
storage.put_event(event);
break;
}
default: {
console.error('fuck');
console.trace();
process.exit(1);
}
}
} }
async function recv_syn(d, session) { async function recv_syn(d, session) {
const padding = des.u8(d);
const lsn = des.u32(d); const lsn = des.u32(d);
const count = des.u32(d); const count = des.u32(d);
if (config.DEBUG_PRINT) console.log(`syn ${lsn} in, total size = ${d.size}`); console.log(`syn ${lsn} in, total size = ${d.size}`);
const we_expect = lsn - session.lsn; const we_expect = lsn - session.lsn;
const first = count - we_expect; const first = count - we_expect;
const events = []; const events = [];
if (config.DEBUG_PRINT) console.log(`we expect ${we_expect}, count ${count}`); console.log(`we expect ${we_expect}, count ${count}`);
for (let i = 0; i < count; ++i) { for (let i = 0; i < count; ++i) {
const event = des.event(d); const event = des.event(d);
if (i >= first) { if (i >= first) {
event.desk_id = session.desk_id; event.desk_id = session.desk_id;
event.user_id = session.id; event.user_id = session.user_id;
handle_event(session, event); handle_event(session, event);
events.push(event); events.push(event);
} }
@ -44,15 +68,8 @@ async function recv_syn(d, session) {
desks[session.desk_id].events.push(...events); desks[session.desk_id].events.push(...events);
session.lsn = lsn; session.lsn = lsn;
storage.queries.update_desk_sn.run({ storage.save_desk_sn(session.desk_id, desks[session.desk_id].sn);
'$id': session.desk_id, storage.save_session_lsn(session.id, lsn);
'$sn': desks[session.desk_id].sn
});
storage.queries.update_session_lsn.run({
'$id': session.id,
'$lsn': lsn
});
send.send_ack(session.ws, lsn); send.send_ack(session.ws, lsn);
send.sync_desk(session.desk_id); send.sync_desk(session.desk_id);
@ -61,116 +78,33 @@ async function recv_syn(d, session) {
function recv_fire(d, session) { function recv_fire(d, session) {
const event = des.event(d); const event = des.event(d);
event.user_id = session.id; for (const sid in sessions) {
const other = sessions[sid];
switch (event.type) {
case EVENT.SET_COLOR: {
session.color = event.color;
storage.queries.update_session_color.run({
'$id': session.id,
'$color': event.color
});
break;
}
case EVENT.SET_WIDTH: {
session.width = event.width;
storage.queries.update_session_width.run({ if (other.id === session.id) {
'$id': session.id, continue;
'$width': event.width
});
break;
} }
}
send.fire_event(session, event);
}
function recv_follow(d, session) {
const user_id = des.u32(d);
if (config.DEBUG_PRINT) console.log(`follow ${user_id} in`); if (other.state !== SESSION.READY) {
continue;
if (user_id === 4294967295) {
// unfollow
session.follow = null;
} else {
// follow
session.follow = user_id;
} }
}
function handle_event(session, event) { if (other.desk_id != session.desk_id) {
switch (event.type) { continue;
case EVENT.STROKE: {
const stroke_result = storage.queries.insert_stroke.get({
'$width': event.width,
'$color': event.color,
'$points': event.points,
'$pressures': event.pressures,
});
event.stroke_id = stroke_result.id;
storage.queries.insert_event.run({
'$type': event.type,
'$desk_id': session.desk_id,
'$session_id': session.id,
'$stroke_id': event.stroke_id,
'$image_id': 0,
'$corner': 0,
'$x': 0,
'$y': 0,
'$width': 0,
'$height': 0,
});
desks[session.desk_id].total_points += event.points.length;
break;
} }
case EVENT.ERASER: send.send_fire(other.ws, session.user_id, event);
case EVENT.IMAGE:
case EVENT.IMAGE_MOVE:
case EVENT.IMAGE_SCALE:
case EVENT.UNDO: {
storage.queries.insert_event.run({
'$type': event.type,
'$desk_id': session.desk_id,
'$session_id': session.id,
'$stroke_id': event.stroke_id || 0,
'$image_id': event.image_id || 0,
'$corner': event.corner || 0,
'$x': event.x || 0,
'$y': event.y || 0,
'$width': event.width || 0,
'$height': event.height || 0,
});
break;
}
default: {
console.error('fuck');
console.trace();
process.exit(1);
}
} }
} }
export function handle_message(ws, d) { export async function handle_message(ws, d) {
if (!(ws.data.session_id in sessions)) { if (!(ws.data.session_id in sessions)) {
return; return;
} }
const session = sessions[ws.data.session_id]; const session = sessions[ws.data.session_id];
const desk_id = session.desk_id; const desk_id = session.desk_id;
const message_type = des.u32(d); const message_type = des.u8(d);
switch (message_type) { switch (message_type) {
case MESSAGE.FIRE: { case MESSAGE.FIRE: {
@ -188,11 +122,6 @@ export function handle_message(ws, d) {
break; break;
} }
case MESSAGE.FOLLOW: {
recv_follow(d, session);
break;
}
default: { default: {
console.error('fuck'); console.error('fuck');
console.trace(); console.trace();

156
server/send.js

@ -7,61 +7,23 @@ import { MESSAGE, SESSION, EVENT } from './enums';
import { sessions, desks } from './storage'; import { sessions, desks } from './storage';
function event_size(event) { function event_size(event) {
let size = 4 + 4; // type + user_id let size = 1 + 4; // type + user_id
switch (event.type) { switch (event.type) {
case EVENT.PREDRAW: case EVENT.PREDRAW: {
case EVENT.MOVE_CURSOR: { size += 2 * 2;
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; break;
} }
case EVENT.STROKE: { case EVENT.STROKE: {
size += 4 + 2 + 2 + 4; // stroke id + point count + width + color size += 4 + 2 + 2 + 4; // stroke id + point count + width + color
size += event.points.byteLength; size += event.points.byteLength;
size += math.round_to_pow2(event.pressures.byteLength, 4);
break;
}
case EVENT.IMAGE: {
size += 4 + 4 + 4 + 4 + 4; // file_id + x + y + width + height
break; break;
} }
case EVENT.IMAGE:
case EVENT.IMAGE_MOVE: { case EVENT.IMAGE_MOVE: {
size += 4 + 4 + 4; // file id + x + y size += 4 + 2 + 2; // file id + x + y
break;
}
case EVENT.IMAGE_SCALE: {
size += 4 + 4 + 4 + 4; // file_id + corner + x + y
break; break;
} }
@ -76,7 +38,6 @@ function event_size(event) {
} }
default: { default: {
console.error(event.desk_id);
console.error('fuck'); console.error('fuck');
console.trace(); console.trace();
process.exit(1); process.exit(1);
@ -87,28 +48,30 @@ function event_size(event) {
} }
function create_session(ws, desk_id) { function create_session(ws, desk_id) {
const user = {
id: math.crypto_random32(),
login: 'unnamed',
};
const session = { const session = {
id: math.crypto_random32(), id: math.crypto_random32(),
user_id: user.id,
desk_id: desk_id, desk_id: desk_id,
state: SESSION.OPENED, state: SESSION.OPENED,
sn: 0, sn: 0,
lsn: 0, lsn: 0,
ws: ws, ws: ws,
color: 0x00,
width: 8,
}; };
storage.queries.insert_session.run({ storage.create_user(user);
'$id': session.id, storage.create_session(session);
'$desk_id': desk_id
});
sessions[session.id] = session; sessions[session.id] = session;
return session; return session;
} }
export function send_init(ws) { export async function send_init(ws) {
if (!ws) { if (!ws) {
return; return;
} }
@ -118,12 +81,11 @@ export function send_init(ws) {
const desk = desks[desk_id]; const desk = desks[desk_id];
let opcode = MESSAGE.INIT; let opcode = MESSAGE.INIT;
let size = 4 + 4 + 4 + 4 + 4 + 4; // opcode + user_id + lsn + event count + stroke count + user count + total_point_count let size = 1 + 4 + 4 + 4 + 4; // opcode + user_id + lsn + event count + stroke count
let session = null; let session = null;
if (session_id in sessions && sessions[session_id].desk_id == desk_id) { if (session_id in sessions && sessions[session_id].desk_id == desk_id) {
session = sessions[session_id]; session = sessions[session_id];
size += 4 + 2; // color + width
} else { } else {
size += 4; // session id size += 4; // session id
opcode = MESSAGE.JOIN; opcode = MESSAGE.JOIN;
@ -136,17 +98,7 @@ export function send_init(ws) {
session.sn = 0; // Always re-send everything on reconnect session.sn = 0; // Always re-send everything on reconnect
session.state = SESSION.OPENED; session.state = SESSION.OPENED;
if (config.DEBUG_PRINT) console.log(`session ${session.id} opened`); console.log(`session ${session.id} opened`);
let user_count = 0;
for (const sid in sessions) {
const other_session = sessions[sid];
if (other_session.id !== session_id && other_session.desk_id === desk_id) {
++user_count;
size += 4 + 4 + 2 + 1; // user id + color + width + online
}
}
for (const event of desk.events) { for (const event of desk.events) {
size += event_size(event); size += event_size(event);
@ -154,38 +106,21 @@ export function send_init(ws) {
const s = ser.create(size); const s = ser.create(size);
ser.u32(s, opcode); ser.u8(s, opcode);
ser.u32(s, session.user_id);
ser.u32(s, session.lsn); ser.u32(s, session.lsn);
if (opcode === MESSAGE.JOIN) { if (opcode === MESSAGE.JOIN) {
ser.u32(s, session.id); ser.u32(s, session.id);
} else {
ser.u32(s, session.color);
ser.u16(s, session.width);
} }
ser.u32(s, desk.events.length); ser.u32(s, desk.events.length);
ser.u32(s, user_count);
ser.u32(s, desk.total_points);
for (const sid in sessions) {
const other_session = sessions[sid];
if (other_session.id !== session_id && other_session.desk_id === desk_id) {
// console.log(other_session.id, other_session.color, other_session.width);
ser.u32(s, other_session.id);
ser.u32(s, other_session.color);
ser.u16(s, other_session.width);
ser.u8(s, other_session.state === SESSION.READY);
}
}
ser.align(s, 4);
for (const event of desk.events) { for (const event of desk.events) {
ser.event(s, event); ser.event(s, event);
} }
ws.send(s.buffer); await ws.send(s.buffer);
} }
export function send_ack(ws, lsn) { export function send_ack(ws, lsn) {
@ -193,56 +128,32 @@ export function send_ack(ws, lsn) {
return; return;
} }
const size = 4 + 4; // opcode + lsn const size = 1 + 4; // opcode + lsn
const s = ser.create(size); const s = ser.create(size);
ser.u32(s, MESSAGE.ACK); ser.u8(s, MESSAGE.ACK);
ser.u32(s, lsn); ser.u32(s, lsn);
if (config.DEBUG_PRINT) console.log(`ack ${lsn} out`); console.log(`ack ${lsn} out`);
ws.send(s.buffer); ws.send(s.buffer);
} }
function send_fire(ws, event) { export function send_fire(ws, user_id, event) {
if (!ws) { if (!ws) {
return; return;
} }
const s = ser.create(4 + 4 + event_size(event)); const s = ser.create(1 + 4 + event_size(event));
ser.u32(s, MESSAGE.FIRE); ser.u8(s, MESSAGE.FIRE);
ser.u32(s, user_id);
ser.event(s, event); ser.event(s, event);
ws.send(s.buffer); ws.send(s.buffer);
} }
export function fire_event(from_session, event) { async function sync_session(session_id) {
for (const sid in sessions) {
const other = sessions[sid];
if (other.id === from_session.id) {
continue;
}
if (other.state !== SESSION.READY) {
continue;
}
if (other.desk_id != from_session.desk_id) {
continue;
}
if (event.type === EVENT.MOVE_CANVAS && other.follow !== from_session.id) {
// Do not spam canvas move events to those who don't follow us
continue;
}
send_fire(other.ws, event);
}
}
function sync_session(session_id) {
if (!(session_id in sessions)) { if (!(session_id in sessions)) {
return; return;
} }
@ -258,11 +169,11 @@ function sync_session(session_id) {
return; return;
} }
let size = 4 + 4 + 4; // opcode + sn + event count let size = 1 + 4 + 4; // opcode + sn + event count
let count = desk.sn - session.sn; let count = desk.sn - session.sn;
if (count === 0) { if (count === 0) {
if (config.DEBUG_PRINT) console.log('client ACKed all events'); console.log('client ACKed all events');
return; return;
} }
@ -273,7 +184,7 @@ function sync_session(session_id) {
const s = ser.create(size); const s = ser.create(size);
ser.u32(s, MESSAGE.SYN); ser.u8(s, MESSAGE.SYN);
ser.u32(s, desk.sn); ser.u32(s, desk.sn);
ser.u32(s, count); ser.u32(s, count);
@ -282,14 +193,11 @@ function sync_session(session_id) {
ser.event(s, event); ser.event(s, event);
} }
if (config.DEBUG_PRINT) console.log(`syn ${desk.sn} out`); console.debug(`syn ${desk.sn} out`);
session.ws.send(s.buffer); await session.ws.send(s.buffer);
if (session.sync_attempts < config.SYNC_MAX_ATTEMPTS) {
session.sync_attempts += 1;
session.sync_timer = setTimeout(() => sync_session(session_id), config.SYNC_TIMEOUT); session.sync_timer = setTimeout(() => sync_session(session_id), config.SYNC_TIMEOUT);
}
} }
export function sync_desk(desk_id) { export function sync_desk(desk_id) {

87
server/serializer.js

@ -21,115 +21,42 @@ export function u16(s, value) {
s.offset += 2; s.offset += 2;
} }
export function f32(s, value) {
s.view.setFloat32(s.offset, value, true);
s.offset += 4;
}
export function u32(s, value) { export function u32(s, value) {
s.view.setUint32(s.offset, value, true); s.view.setUint32(s.offset, value, true);
s.offset += 4; s.offset += 4;
} }
export function s32(s, value) {
s.view.setInt32(s.offset, value, true);
s.offset += 4;
}
export function bytes(s, bytes) { export function bytes(s, bytes) {
s.strview.set(new Uint8Array(bytes.buffer, bytes.byteOffset, bytes.byteLength), s.offset); s.strview.set(new Uint8Array(bytes.buffer, bytes.byteOffset, bytes.byteLength), s.offset);
s.offset += bytes.byteLength; s.offset += bytes.byteLength;
} }
export function align(s, to) {
// TODO: non-stupid version of this
while (s.offset % to != 0) {
s.offset++;
}
}
export function event(s, event) { export function event(s, event) {
u32(s, event.type); // for alignment reasons u8(s, event.type);
u32(s, event.user_id); u32(s, event.user_id);
switch (event.type) { switch (event.type) {
case EVENT.PREDRAW: { case EVENT.PREDRAW: {
f32(s, event.x); u16(s, event.x);
f32(s, event.y); u16(s, event.y);
break;
}
case EVENT.MOVE_CURSOR: {
f32(s, event.x);
f32(s, event.y);
break;
}
case EVENT.MOVE_CANVAS: {
u32(s, event.offset_x);
u32(s, event.offset_y);
s32(s, event.zoom_level);
break;
}
case EVENT.ZOOM_CANVAS: {
s32(s, event.zoom_level);
f32(s, event.zoom_cx);
f32(s, event.zoom_cy);
break;
}
case EVENT.USER_JOINED:
case EVENT.LEAVE:
case EVENT.CLEAR:
case EVENT.LIFT: {
break;
}
case EVENT.SET_COLOR: {
u32(s, event.color);
break;
}
case EVENT.SET_WIDTH: {
u16(s, event.width);
break; break;
} }
case EVENT.STROKE: { case EVENT.STROKE: {
const points_bytes = event.points; const points_bytes = event.points;
const pressures_bytes = event.pressures;
u32(s, event.stroke_id); u32(s, event.stroke_id);
u16(s, points_bytes.byteLength / 2 / 4); // each point is 2 * f32 u16(s, points_bytes.byteLength / 2 / 2); // each point is 2 u16s
u16(s, event.width); u16(s, event.width);
u32(s, event.color); u32(s, event.color);
bytes(s, points_bytes); bytes(s, points_bytes);
bytes(s, pressures_bytes);
align(s, 4);
break;
}
case EVENT.IMAGE: {
u32(s, event.image_id);
f32(s, event.x);
f32(s, event.y);
u32(s, event.width);
u32(s, event.height);
break; break;
} }
case EVENT.IMAGE:
case EVENT.IMAGE_MOVE: { case EVENT.IMAGE_MOVE: {
u32(s, event.image_id); u32(s, event.image_id);
f32(s, event.x); u16(s, event.x);
f32(s, event.y); u16(s, event.y);
break;
}
case EVENT.IMAGE_SCALE: {
u32(s, event.image_id);
u32(s, event.corner);
f32(s, event.x);
f32(s, event.y);
break; break;
} }

16
server/server.js

@ -1,11 +1,13 @@
import * as config from './config'; import * as config from './config';
import * as storage from './storage'; import * as storage from './storage';
import * as http_server from './http'; import * as http_server from './http';
import * as math from './math';
import * as ser from './serializer';
import * as des from './deserializer'; import * as des from './deserializer';
import * as send from './send'; import * as send from './send';
import * as recv from './recv'; import * as recv from './recv';
import { MESSAGE, EVENT, SESSION } from './enums'; import { MESSAGE, EVENT, SESSION, SNS } from './enums';
import { sessions, desks } from './storage'; import { sessions, desks } from './storage';
export function startup() { export function startup() {
@ -27,11 +29,7 @@ export function startup() {
events: [], events: [],
}; };
storage.queries.insert_desk.run({ storage.create_desk(desk_id);
'$id': desk_id,
'$title': `Desk ${desk_id}`
});
desks[desk_id] = desk; desks[desk_id] = desk;
} }
@ -51,20 +49,16 @@ export function startup() {
websocket: { websocket: {
open(ws) { open(ws) {
send.send_init(ws); send.send_init(ws);
const userjoin_event = {'type': EVENT.USER_JOINED, 'user_id': ws.data.session_id};
send.fire_event(sessions[ws.data.session_id], userjoin_event);
}, },
async message(ws, u8array) { async message(ws, u8array) {
const dataview = new DataView(u8array.buffer); const dataview = new DataView(u8array.buffer);
const d = des.create(dataview); const d = des.create(dataview);
recv.handle_message(ws, d); await recv.handle_message(ws, d);
}, },
close(ws, code, message) { close(ws, code, message) {
if (ws.data.session_id in sessions) { if (ws.data.session_id in sessions) {
const leave_event = {'type': EVENT.LEAVE, 'user_id': ws.data.session_id};
send.fire_event(sessions[ws.data.session_id], leave_event);
console.log(`session ${ws.data.session_id} closed`); console.log(`session ${ws.data.session_id} closed`);
sessions[ws.data.session_id].state = SESSION.CLOSED; sessions[ws.data.session_id].state = SESSION.CLOSED;
sessions[ws.data.session_id].ws = null; sessions[ws.data.session_id].ws = null;

180
server/storage.js

@ -3,12 +3,11 @@ import * as sqlite from 'bun:sqlite';
import { EVENT, SESSION } from './enums'; import { EVENT, SESSION } from './enums';
// In-memory views
export const sessions = {}; export const sessions = {};
export const desks = {}; export const desks = {};
export const queries = {}; let db = null;
export let db = null; const queries = {};
export function startup() { export function startup() {
const path = `${config.DATADIR}/db.sqlite`; const path = `${config.DATADIR}/db.sqlite`;
@ -21,12 +20,31 @@ export function startup() {
title TEXT title TEXT
);`).run(); );`).run();
db.query(`CREATE TABLE IF NOT EXISTS users (
id INTEGER PRIMARY KEY,
login TEXT
);`).run();
db.query(`CREATE TABLE IF NOT EXISTS sessions ( db.query(`CREATE TABLE IF NOT EXISTS sessions (
id INTEGER PRIMARY KEY, id INTEGER PRIMARY KEY,
user_id INTEGER,
desk_id INTEGER, desk_id INTEGER,
lsn INTEGER, lsn INTEGER,
color INTEGER,
width INTEGER, FOREIGN KEY (user_id)
REFERENCES users (id)
ON DELETE CASCADE
ON UPDATE NO ACTION,
FOREIGN KEY (desk_id)
REFERENCES desks (id)
ON DELETE CASCADE
ON UPDATE NO ACTION
);`).run();
db.query(`CREATE TABLE IF NOT EXISTS images (
id INTEGER PRIMARY KEY,
desk_id INTEGER,
FOREIGN KEY (desk_id) FOREIGN KEY (desk_id)
REFERENCES desks (id) REFERENCES desks (id)
@ -36,108 +54,176 @@ export function startup() {
db.query(`CREATE TABLE IF NOT EXISTS strokes ( db.query(`CREATE TABLE IF NOT EXISTS strokes (
id INTEGER PRIMARY KEY, id INTEGER PRIMARY KEY,
desk_id INTEGER,
points BLOB,
width INTEGER, width INTEGER,
color INTEGER, color INTEGER,
points BLOB,
pressures BLOB FOREIGN KEY (desk_id)
REFERENCES desks (id)
ON DELETE CASCADE
ON UPDATE NO ACTION
);`).run(); );`).run();
db.query(`CREATE TABLE IF NOT EXISTS events ( db.query(`CREATE TABLE IF NOT EXISTS events (
id INTEGER PRIMARY KEY, id INTEGER PRIMARY KEY,
type INTEGER, type INTEGER,
desk_id INTEGER, desk_id INTEGER,
session_id INTEGER, user_id INTEGER,
stroke_id INTEGER, stroke_id INTEGER,
image_id INTEGER, image_id INTEGER,
corner INTEGER,
x INTEGER, x INTEGER,
y INTEGER, y INTEGER,
width INTEGER,
height INTEGER,
FOREIGN KEY (desk_id) FOREIGN KEY (desk_id)
REFERENCES desks (id) REFERENCES desks (id)
ON DELETE CASCADE ON DELETE CASCADE
ON UPDATE NO ACTION ON UPDATE NO ACTION
FOREIGN KEY (session_id) FOREIGN KEY (user_id)
REFERENCES sessions (id) REFERENCES users (id)
ON DELETE NO ACTION ON DELETE CASCADE
ON UPDATE NO ACTION ON UPDATE NO ACTION
FOREIGN KEY (stroke_id) FOREIGN KEY (stroke_id)
REFERENCES strokes (id) REFERENCES strokes (id)
ON DELETE CASCADE ON DELETE CASCADE
ON UPDATE NO ACTION ON UPDATE NO ACTION
FOREIGN KEY (image_id)
REFERENCES images (id)
ON DELETE CASCADE
ON UPDATE NO ACTION
);`).run(); );`).run();
// INSERT db.query(`CREATE INDEX IF NOT EXISTS idx_events_desk_id
queries.insert_desk = db.query('INSERT INTO desks (id, title, sn) VALUES ($id, $title, 0) RETURNING id'); ON events (desk_id);
queries.insert_stroke = db.query('INSERT INTO strokes (width, color, points, pressures) VALUES ($width, $color, $points, $pressures) RETURNING id'); `).run();
queries.insert_session = db.query('INSERT INTO sessions (id, desk_id, lsn) VALUES ($id, $desk_id, 0) RETURNING id');
queries.insert_event = db.query('INSERT INTO events (type, desk_id, session_id, stroke_id, image_id, corner, x, y, width, height) VALUES ($type, $desk_id, $session_id, $stroke_id, $image_id, $corner, $x, $y, $width, $height) RETURNING id');
// UPDATE db.query(`CREATE INDEX IF NOT EXISTS idx_strokes_desk_id
queries.update_desk_sn = db.query('UPDATE desks SET sn = $sn WHERE id = $id'); ON strokes (desk_id);
queries.update_session_lsn = db.query('UPDATE sessions SET lsn = $lsn WHERE id = $id'); `).run();
queries.update_session_color = db.query('UPDATE sessions SET color = $color WHERE id = $id');
queries.update_session_width = db.query('UPDATE sessions SET width = $width WHERE id = $id');
const res1 = db.query('SELECT COUNT(id) as count FROM desks').get(); const res1 = db.query('SELECT COUNT(id) as count FROM desks').get();
const res2 = db.query('SELECT COUNT(id) as count FROM events').get(); const res2 = db.query('SELECT COUNT(id) as count FROM events').get();
const res3 = db.query('SELECT COUNT(id) as count FROM strokes').get(); const res3 = db.query('SELECT COUNT(id) as count FROM strokes').get();
const res4 = db.query('SELECT COUNT(id) as count FROM sessions').get(); const res4 = db.query('SELECT COUNT(id) as count FROM users').get();
const res5 = db.query('SELECT COUNT(id) as count FROM sessions').get();
const res6 = db.query('SELECT COUNT(id) as count FROM images').get();
queries.desks = db.query('SELECT id, sn FROM desks');
queries.events = db.query('SELECT * FROM events');
queries.sessions = db.query('SELECT id, lsn, user_id, desk_id FROM sessions');
queries.strokes = db.query('SELECT * FROM strokes');
queries.empty_desk = db.query('INSERT INTO desks (id, title, sn) VALUES (?1, ?2, 0)');
queries.desk_strokes = db.query('SELECT id, points FROM strokes WHERE desk_id = ?1');
queries.put_desk_stroke = db.query('INSERT INTO strokes (id, desk_id, points, width, color) VALUES (?1, ?2, ?3, ?4, ?5)');
queries.clear_desk_events = db.query('DELETE FROM events WHERE desk_id = ?1');
queries.set_desk_sn = db.query('UPDATE desks SET sn = ?1 WHERE id = ?2');
queries.save_session_lsn = db.query('UPDATE sessions SET lsn = ?1 WHERE id = ?2');
queries.create_session = db.query('INSERT INTO sessions (id, lsn, user_id, desk_id) VALUES (?1, 0, ?2, ?3)');
queries.create_user = db.query('INSERT INTO users (id, login) VALUES (?1, ?2)');
queries.put_event = db.query('INSERT INTO events (type, desk_id, user_id, stroke_id, image_id, x, y) VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7)');
queries.put_image = db.query('INSERT INTO images (id, desk_id) VALUES (?1, ?2)');
console.log(`Storing data in ${path}`); console.log(`Storing data in ${path}`);
console.log(`Entity count at startup: console.log(`Entity count at startup:
${res1.count} desks ${res1.count} desks
${res2.count} events ${res2.count} events
${res3.count} strokes ${res3.count} strokes
${res4.count} sessions ${res4.count} users
`); ${res5.count} sessions
${res6.count} images`
);
// Init in-memory view: merge strokes into events, set all sessions to closed const stored_desks = get_desks();
const stored_desks = db.query('SELECT * FROM desks').all(); const stored_events = get_events();
const stored_events = db.query('SELECT * FROM events').all(); const stored_strokes = get_strokes();
const stored_strokes = db.query('SELECT * FROM strokes').all(); const stored_sessions = get_sessions();
const stored_sessions = db.query('SELECT * FROM sessions').all();
const stroke_dict = {}; const stroke_dict = {};
for (const stroke of stored_strokes) {
stroke.points = new Uint16Array(stroke.points.buffer);
stroke_dict[stroke.id] = stroke;
}
for (const desk of stored_desks) { for (const desk of stored_desks) {
desks[desk.id] = desk; desks[desk.id] = desk;
desks[desk.id].events = []; desks[desk.id].events = [];
desks[desk.id].total_points = 0;
}
for (const stroke of stored_strokes) {
stroke.points = new Float32Array(stroke.points.buffer);
stroke_dict[stroke.id] = stroke;
} }
for (const event of stored_events) { for (const event of stored_events) {
if (event.type === EVENT.STROKE) { if (event.type === EVENT.STROKE) {
const stroke = stroke_dict[event.stroke_id]; const stroke = stroke_dict[event.stroke_id];
event.points = stroke.points; event.points = stroke.points;
event.pressures = stroke.pressures;
event.color = stroke.color; event.color = stroke.color;
event.width = stroke.width; event.width = stroke.width;
desks[event.desk_id].total_points += stroke.points.length / 2;
} }
desks[event.desk_id].events.push(event);
}
for (const desk of stored_desks) { desks[event.desk_id].events.push(event);
desk.sn = desk.events.length;
} }
for (const session of stored_sessions) { for (const session of stored_sessions) {
session.state = SESSION.CLOSED; session.state = SESSION.CLOSED;
session.ws = null; session.ws = null;
session.sync_attempts = 0;
session.follow = null;
sessions[session.id] = session; sessions[session.id] = session;
} }
} }
export function get_strokes() {
return queries.strokes.all();
}
export function get_sessions() {
return queries.sessions.all();
}
export function get_desks() {
return queries.desks.all();
}
export function get_events() {
return queries.events.all();
}
export function get_desk_strokes(desk_id) {
return queries.desk_strokes.all(desk_id);
}
export function put_event(event) {
return queries.put_event.get(event.type, event.desk_id || 0, event.user_id || 0, event.stroke_id || 0, event.image_id || 0, event.x || 0, event.y || 0);
}
export function put_stroke(stroke_id, desk_id, points, width, color) {
return queries.put_desk_stroke.get(stroke_id, desk_id, new Uint8Array(points.buffer, points.byteOffset, points.byteLength), width, color);
}
export function clear_events(desk_id) {
return queries.clear_desk_events.get(desk_id);
}
export function create_desk(desk_id, title = 'untitled') {
return queries.empty_desk.get(desk_id, title);
}
export function save_desk_sn(desk_id, sn) {
return queries.set_desk_sn.get(sn, desk_id);
}
export function create_session(session) {
return queries.create_session.get(session.id, session.user_id, session.desk_id);
}
export function create_user(user) {
return queries.create_user.get(user.id, user.login);
}
export function save_session_lsn(session_id, lsn) {
return queries.save_session_lsn.get(lsn, session_id);
}
export function put_image(image_id, desk_id) {
return queries.put_image.get(image_id, desk_id);
}
Loading…
Cancel
Save