Compare commits

..

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

  1. 6
      .gitignore
  2. 10
      Caddyfile
  3. 24
      LICENSE
  4. 116
      README.txt
  5. 100
      client/aux.js
  6. 427
      client/bvh.js
  7. 393
      client/client_recv.js
  8. 195
      client/client_send.js
  9. 52
      client/config.js
  10. 327
      client/cursor.js
  11. 214
      client/default.css
  12. 130
      client/heapify.js
  13. 281
      client/icons/crosshair.svg
  14. 52
      client/icons/cursor.svg
  15. 0
      client/icons/draw.svg
  16. 2
      client/icons/perfect-bullet.svg
  17. 105
      client/icons/picker.svg
  18. 54
      client/icons/player-cursor.svg
  19. 57
      client/icons/player.svg
  20. 52
      client/icons/pointer.svg
  21. 243
      client/index.html
  22. 194
      client/index.js
  23. 36
      client/index.log
  24. 49
      client/lod_worker.js
  25. 662
      client/math.js
  26. 9
      client/offline.html
  27. 264
      client/random_helpers.js
  28. 278
      client/speed.js
  29. 138
      client/tools.js
  30. 5
      client/touch.css
  31. 359
      client/touch.js
  32. 132
      client/undo.js
  33. 1
      client/wasm/compile_command
  34. 419
      client/wasm/lod.c
  35. BIN
      client/wasm/lod.wasm
  36. 692
      client/webgl_draw.js
  37. 921
      client/webgl_geometry.js
  38. 1003
      client/webgl_listeners.js
  39. 421
      client/webgl_shaders.js
  40. 8
      client/websocket.js
  41. 3
      server/config.js
  42. BIN
      server/data-local.sqlite
  43. 62
      server/deserializer.js
  44. 13
      server/enums.js
  45. 4
      server/http.js
  46. 6
      server/math.js
  47. 85
      server/milton.js
  48. 57
      server/recv.js
  49. 105
      server/send.js
  50. 62
      server/serializer.js
  51. 8
      server/server.js
  52. 40
      server/storage.js
  53. 21
      server/texput.log

6
.gitignore vendored

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

10
Caddyfile

@ -1,10 +1,4 @@ @@ -1,10 +1,4 @@
localhost {
header {
Cross-Origin-Opener-Policy same-origin
Cross-Origin-Embedder-Policy require-corp
Cross-Origin-Resource-Policy same-origin
}
desk.local {
redir /ws /ws/
redir /desk /desk/
@ -22,7 +16,7 @@ localhost { @@ -22,7 +16,7 @@ localhost {
}
handle_path /desk/* {
root * /code/desk2/client
root * /code/desk2/client
try_files {path} /index.html
file_server
}

24
LICENSE

@ -1,24 +0,0 @@ @@ -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>

116
README.txt

@ -1,116 +0,0 @@ @@ -1,116 +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)
+ Frame-independent lerp where applicable
- Dedup images on server, so we don't download the same image too many times
- No lag while downloading images
+ 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
- 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?)
Shiplist:
- Fix mess
- Redo
- User-friendly config
+ min 1px wide lines with transparency fade
- offline-only version
- select and manupulate curves with selection tool
- deduplicate pictures
- No lag while downloading images
- Show previous color in color picker preview
- Color picker should work for ruler
- Color picker precision and pick order seems off
+ Stuck in color picker mode when mouse leaves screen

100
client/aux.js

@ -0,0 +1,100 @@ @@ -0,0 +1,100 @@
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);
await queue_event(state, event);
}
}
function event_size(event) {
let size = 1 + 3; // type + padding
switch (event.type) {
case EVENT.PREDRAW: {
size += 4 * 2;
break;
}
case EVENT.SET_COLOR: {
size += 4;
break;
}
case EVENT.SET_WIDTH: {
size += 2;
break;
}
case EVENT.STROKE: {
size += 4 + 2 + 2 + 4 + event.points.length * 4 * 2; // u32 stroke id + u16 (count) + u16 (width) + u32 (color + count * (f32, f32) points
break;
}
case EVENT.UNDO:
case EVENT.REDO: {
break;
}
case EVENT.IMAGE:
case EVENT.IMAGE_MOVE: {
size += 4 + 4 + 4; // file id + 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;
}
}
}

427
client/bvh.js

@ -1,427 +0,0 @@ @@ -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].sort();
} else {
sorted_strokes = [...strokes].sort();
}
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;
}

393
client/client_recv.js

@ -26,41 +26,28 @@ function des_u32(d) { @@ -26,41 +26,28 @@ function des_u32(d) {
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;
}
const result = [];
for (let i = 0; i < count; ++i) {
const item = d.view.getFloat32(d.offset, true);
d.offset += 4;
result.push(item);
}
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) {
function des_event(d) {
const event = {};
event.type = des_u32(d);
event.type = des_u8(d);
event.user_id = des_u32(d);
switch (event.type) {
@ -70,33 +57,6 @@ function des_event(d, state = null) { @@ -70,33 +57,6 @@ function des_event(d, state = null) {
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;
@ -112,15 +72,16 @@ function des_event(d, state = null) { @@ -112,15 +72,16 @@ function des_event(d, state = null) {
const point_count = des_u16(d);
const width = des_u16(d);
const color = des_u32(d);
const coords = des_f32array(d, point_count * 2);
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.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;
@ -128,15 +89,7 @@ function des_event(d, state = null) { @@ -128,15 +89,7 @@ function des_event(d, state = null) {
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:
case EVENT.IMAGE_MOVE: {
event.image_id = des_u32(d);
event.x = des_f32(d);
@ -144,14 +97,6 @@ function des_event(d, state = null) { @@ -144,14 +97,6 @@ function des_event(d, state = null) {
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;
@ -175,7 +120,7 @@ function bitmap_bbox(event) { @@ -175,7 +120,7 @@ function bitmap_bbox(event) {
'xmin': event.x,
'xmax': event.x + event.bitmap.width,
'ymin': event.y,
'ymax': event.y + event.bitmap.height,
'ymax': event.y + event.bitmap.height
};
return bbox;
@ -186,14 +131,10 @@ function init_player_defaults(state, player_id, color = config.default_color, wi @@ -186,14 +131,10 @@ function init_player_defaults(state, player_id, color = config.default_color, wi
'color': color,
'width': width,
'points': [],
'online': false,
'cursor': {'x': 0, 'y': 0},
'strokes': [],
'current_prestroke': false,
};
}
function handle_event(state, context, event, options = {}) {
function handle_event(state, context, event) {
if (config.debug_print) console.debug(`event type ${event.type} from user ${event.user_id}`);
let need_draw = false;
@ -203,96 +144,12 @@ function handle_event(state, context, event, options = {}) { @@ -203,96 +144,12 @@ function handle_event(state, context, event, options = {}) {
}
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
geometry_add_point(state, context, event.user_id, {'x': event.x, 'y': event.y});
need_draw = true;
break;
}
case EVENT.CLEAR: {
// Clear oldest preview from player. This event is sent when stroke is cancelled
geometry_clear_oldest_prestroke(state, context, event.user_id);
need_draw = true;
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;
@ -304,92 +161,72 @@ function handle_event(state, context, event, options = {}) { @@ -304,92 +161,72 @@ function handle_event(state, context, event, options = {}) {
}
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]);
if (event.user_id != state.me) {
geometry_clear_player(state, context, event.user_id);
need_draw = true;
}
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++;
geometry_add_stroke(state, context, event);
break;
}
case EVENT.UNDO: {
geometry_add_dummy_stroke(state, context);
need_draw = undo(state, context, event, options);
need_draw = true;
console.error('todo');
// for (let i = state.events.length - 1; i >=0; --i) {
// const other_event = state.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, state.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, state.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 = state.images[other_event.image_id].x -= other_event.x;
// const iy = state.images[other_event.image_id].y -= other_event.y;
// item.style.transform = `translate(${ix}px, ${iy}px)`;
// break;
// }
// }
// }
break;
}
case EVENT.IMAGE: {
const p = {'x': event.x, 'y': event.y};
geometry_add_dummy_stroke(state, 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);
const p = {'x': event.x, 'y': event.y};
event.width = bitmap.width;
event.height = bitmap.height;
add_image(context, event.image_id, bitmap, p, bitmap.width, bitmap.height);
add_image(context, event.image_id, bitmap, p);
// 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
@ -403,40 +240,41 @@ function handle_event(state, context, event, options = {}) { @@ -403,40 +240,41 @@ function handle_event(state, context, event, options = {}) {
}
case EVENT.IMAGE_MOVE: {
geometry_add_dummy_stroke(state, 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(state, 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;
// Already moved due to local prediction
if (event.user_id !== state.me) {
const image_id = event.image_id;
const image_event = find_image(state, image_id);
if (image_event) {
// if (config.debug_print) console.debug('move image', image_id, 'to', image_event.x, image_event.y);
image_event.x = event.x;
image_event.y = event.y;
move_image(context, image_event);
need_draw = true;
}
}
break;
}
case EVENT.ERASER: {
geometry_add_dummy_stroke(state, context);
need_draw = true;
const stroke = state.events[event.stroke_id];
stroke.deleted = true;
if (!options.skip_bvh) {
bvh_delete_stroke(state, stroke);
}
console.error('todo');
// if (event.deleted) {
// break;
// }
// for (const other_event of state.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, state.cursor.width);
// redraw_region(stats.bbox);
// }
// break;
// }
// }
break;
}
@ -448,8 +286,8 @@ function handle_event(state, context, event, options = {}) { @@ -448,8 +286,8 @@ function handle_event(state, context, event, options = {}) {
return need_draw;
}
function handle_message(state, context, d) {
const message_type = des_u32(d);
async function handle_message(state, context, d) {
const message_type = des_u8(d);
let do_draw = false;
// if (config.debug_print) console.debug(message_type);
@ -457,10 +295,8 @@ function handle_message(state, context, d) { @@ -457,10 +295,8 @@ function handle_message(state, context, d) {
switch (message_type) {
case MESSAGE.JOIN:
case MESSAGE.INIT: {
console.time('init');
state.online = true;
state.server_lsn = des_u32(d);
state.online = true;
if (state.server_lsn > state.lsn) {
// Server knows something that we don't
@ -476,23 +312,20 @@ function handle_message(state, context, d) { @@ -476,23 +312,20 @@ function handle_message(state, context, d) {
} else {
color = des_u32(d);
width = des_u16(d);
state.me = parseInt(localStorage.getItem('sessionId'));
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);
const color_code = color_from_u32(color).substring(1);
switch_color(state, document.querySelector(`.color[data-color="${color_code}"]`));
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`);
@ -502,37 +335,21 @@ function handle_message(state, context, d) { @@ -502,37 +335,21 @@ function handle_message(state, context, d) {
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);
}
const event = des_event(d);
handle_event(state, context, event);
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;
}
@ -568,7 +385,7 @@ function handle_message(state, context, d) { @@ -568,7 +385,7 @@ function handle_message(state, context, d) {
if (config.debug_print) console.debug(`syn ${sn} in`);
for (let i = 0; i < count; ++i) {
const event = des_event(d, state);
const event = des_event(d);
if (i >= first) {
const need_draw = handle_event(state, context, event);
do_draw = do_draw || need_draw;
@ -578,7 +395,7 @@ function handle_message(state, context, d) { @@ -578,7 +395,7 @@ function handle_message(state, context, d) {
state.sn = sn;
send_ack(sn);
send_ack(sn); // await?
break;
}

195
client/client_send.js

@ -6,43 +6,9 @@ function serializer_create(size) { @@ -6,43 +6,9 @@ function serializer_create(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;
@ -63,20 +29,14 @@ function ser_u32(s, value) { @@ -63,20 +29,14 @@ function ser_u32(s, value) {
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);
ser_u8(s, event.type);
switch (event.type) {
case EVENT.PREDRAW: {
@ -85,31 +45,6 @@ function ser_event(s, event) { @@ -85,31 +45,6 @@ function ser_event(s, event) {
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;
@ -127,17 +62,13 @@ function ser_event(s, event) { @@ -127,17 +62,13 @@ function ser_event(s, event) {
if (config.debug_print) console.debug('original', event.points);
ser_align(s, 4);
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;
}
@ -147,17 +78,6 @@ function ser_event(s, event) { @@ -147,17 +78,6 @@ function ser_event(s, event) {
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;
}
@ -177,45 +97,28 @@ function ser_event(s, event) { @@ -177,45 +97,28 @@ function ser_event(s, event) {
}
}
function send_ack(sn) {
const s = serializer_create(4 + 4);
async function send_ack(sn) {
const s = serializer_create(1 + 4);
ser_u32(s, MESSAGE.ACK);
ser_u8(s, MESSAGE.ACK);
ser_u32(s, sn);
if (config.debug_print) console.debug(`ack ${sn} out`);
try {
if (ws) ws.send(s.buffer);
if (ws) await 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) {
async 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 size = 1 + 3 + 4 + 4; // opcode + lsn + event count
let count = state.lsn - state.server_lsn;
if (count === 0) {
@ -231,7 +134,7 @@ function sync_queue(state) { @@ -231,7 +134,7 @@ function sync_queue(state) {
const s = serializer_create(size);
ser_u32(s, MESSAGE.SYN);
ser_u8(s, MESSAGE.SYN);
ser_u32(s, state.lsn);
ser_u32(s, count);
@ -243,12 +146,12 @@ function sync_queue(state) { @@ -243,12 +146,12 @@ function sync_queue(state) {
if (config.debug_print) console.debug(`syn ${state.lsn} out`);
try {
if (ws) ws.send(s.buffer);
if (ws) await ws.send(s.buffer);
} catch(e) {
ws.close();
}
state.timers.queue_sync = setTimeout(() => sync_queue(state), config.sync_timeout);
setTimeout(() => sync_queue(state), config.sync_timeout);
}
function push_event(state, event) {
@ -275,7 +178,6 @@ function push_event(state, event) { @@ -275,7 +178,6 @@ function push_event(state, event) {
case EVENT.ERASER:
case EVENT.IMAGE:
case EVENT.IMAGE_MOVE:
case EVENT.IMAGE_SCALE:
case EVENT.UNDO:
case EVENT.REDO: {
state.queue.push(event);
@ -306,16 +208,16 @@ function queue_event(state, event, skip = false) { @@ -306,16 +208,16 @@ function queue_event(state, event, skip = false) {
}
// Fire and forget. Doesn't do anything if we are offline
function fire_event(state, event) {
async function fire_event(state, event) {
if (!state.online) { return; }
const s = serializer_create(4 + event_size(event));
const s = serializer_create(1 + event_size(event));
ser_u32(s, MESSAGE.FIRE);
ser_u8(s, MESSAGE.FIRE);
ser_event(s, event);
try {
if (ws) ws.send(s.buffer);
if (ws) await ws.send(s.buffer);
} catch(e) {
ws.close();
}
@ -329,12 +231,6 @@ function predraw_event(x, y) { @@ -329,12 +231,6 @@ function predraw_event(x, y) {
};
}
function lift_event() {
return {
'type': EVENT.LIFT,
};
}
function color_event(color_u32) {
return {
'type': EVENT.SET_COLOR,
@ -349,14 +245,12 @@ function width_event(width) { @@ -349,14 +245,12 @@ function width_event(width) {
};
}
function image_event(image_id, x, y, width, height) {
function image_event(image_id, x, y) {
return {
'type': EVENT.IMAGE,
'image_id': image_id,
'x': x,
'y': y,
'width': width,
'height': height,
};
}
@ -369,16 +263,6 @@ function image_move_event(image_id, x, y) { @@ -369,16 +263,6 @@ function image_move_event(image_id, x, 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);
@ -389,48 +273,3 @@ function stroke_event(state) { @@ -389,48 +273,3 @@ function stroke_event(state) {
'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,
}
}

52
client/config.js

@ -1,52 +0,0 @@ @@ -1,52 +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,
// animation_decay: 16,
animation_decay: 10,
vertical_zoom_speed_multiplier: 3,
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)
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,
},
debug_force_lod: null,
demetri_ms: 40,
p: 'avg',
avg_window: 10,
// WR is the weight ratio of the most recent point vs the oldest point used
// to fit, linearly interpolated for the rest; 1.0 means even weighting of all
// data, 5.0 means fitting the most recent point matters 5x in the optimization
wr: 1.0,
/*
* points of interest (desk/zoomlevel/x/y)
* 1/32/-2075/1020
*/
};

327
client/cursor.js

@ -0,0 +1,327 @@ @@ -0,0 +1,327 @@
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) {
const width = window.innerWidth;
const height = window.innerHeight;
elements.canvas0.width = elements.canvas1.width = width;
elements.canvas0.height = elements.canvas1.height = height;
storage.ctx1.lineJoin = storage.ctx1.lineCap = storage.ctx0.lineJoin = storage.ctx0.lineCap = 'round';
storage.ctx1.lineWidth = storage.ctx0.lineWidth = storage.cursor.width;
redraw_region({'xmin': 0, 'xmax': width, 'ymin': 0, 'ymax': width});
// 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) {
return;
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);
}

214
client/default.css

@ -4,7 +4,7 @@ @@ -4,7 +4,7 @@
--radius: 5px;
--hgap: 5px;
--gap: 10px;
--transform-amimate: transform .1s ease-out;
--transform-amimate: transform .1s ease-in-out;
}
html, body {
@ -27,38 +27,13 @@ body.offline .main { @@ -27,38 +27,13 @@ body.offline .main {
display: none !important;
}
.vhide {
visibility: hidden !important;
}
.flexcol {
display: flex;
flex-direction: column;
}
canvas {
width: 100%;
height: 100%;
display: block;
cursor: url('icons/crosshair.svg') 16 16, crosshair;
}
canvas.tool-pointer {
cursor: default;
cursor: url('icons/cursor.svg') 7 7, crosshair;
}
canvas.picker {
cursor: url('icons/picker.svg') 0 19, crosshair;
}
canvas.resize-topleft {
cursor: nwse-resize;
}
canvas.resize-topright {
cursor: nesw-resize;
}
/*
canvas.movemode {
cursor: grab;
}
@ -67,34 +42,6 @@ canvas.movemode.moving { @@ -67,34 +42,6 @@ canvas.movemode.moving {
cursor: grabbing;
}
canvas.mousemoving {
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;
}
.html-hud .player-cursor {
position: absolute;
width: 16px;
height: 16px;
}
.tools-wrapper {
position: fixed;
bottom: 0;
@ -107,7 +54,7 @@ canvas.mousemoving { @@ -107,7 +54,7 @@ canvas.mousemoving {
}
.pallete-wrapper,
.top-wrapper {
.sizer-wrapper {
position: fixed;
top: 0;
left: 0;
@ -119,98 +66,52 @@ canvas.mousemoving { @@ -119,98 +66,52 @@ canvas.mousemoving {
transition: var(--transform-amimate);
}
.top-wrapper {
.sizer-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;
pointer-events: all;
display: grid;
flex-direction: column;
align-items: center;
background: var(--dark-blue);
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 {
.sizer-wrapper.hidden {
transform: translateY(-125%);
}
.pallete .color-major {
pointer-events: all;
.pallete .color {
padding: var(--gap);
cursor: pointer;
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:hover {
background: var(--dark-hover);
}
.pallete .color-major.active {
transform: translateX(calc(-100% + 3 * var(--gap) + 24px));
.pallete .color.active {
transform: translateX(10px);
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.active:hover {
background: var(--dark-blue);
}
.pallete .color-pane {
@ -219,14 +120,6 @@ canvas.mousemoving { @@ -219,14 +120,6 @@ canvas.mousemoving {
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 {
@ -267,9 +160,9 @@ canvas.mousemoving { @@ -267,9 +160,9 @@ canvas.mousemoving {
.tool.active {
transform: translateY(-10px);
background: var(--dark-blue);
border-top-right-radius: var(--radius);
border-top-left-radius: var(--radius);
background: var(--dark-blue);
}
.tool img {
@ -278,17 +171,17 @@ canvas.mousemoving { @@ -278,17 +171,17 @@ canvas.mousemoving {
filter: invert(100%);
}
.sizer input[type=range] {
input[type=range] {
-webkit-appearance: none;
width: 200px;
background: transparent;
}
.sizer input[type=range]:focus {
input[type=range]:focus {
outline: none;
}
.sizer input[type=range]::-webkit-slider-thumb {
input[type=range]::-webkit-slider-thumb {
-webkit-appearance: none;
border: none;
background: white;
@ -300,7 +193,7 @@ canvas.mousemoving { @@ -300,7 +193,7 @@ canvas.mousemoving {
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 {
input[type=range]::-moz-range-thumb {
border: none;
background: white;
height: 16px;
@ -310,7 +203,7 @@ canvas.mousemoving { @@ -310,7 +203,7 @@ canvas.mousemoving {
border: 2px solid var(--dark-blue);
}
.sizer input[type=range]::-webkit-slider-runnable-track {
input[type=range]::-webkit-slider-runnable-track {
width: 100%;
height: 8px;
cursor: pointer;
@ -319,7 +212,7 @@ canvas.mousemoving { @@ -319,7 +212,7 @@ canvas.mousemoving {
border: none;
}
.sizer input[type=range]:focus::-webkit-slider-runnable-track {
input[type=range]:focus::-webkit-slider-runnable-track {
width: 100%;
height: 8px;
cursor: pointer;
@ -328,7 +221,7 @@ canvas.mousemoving { @@ -328,7 +221,7 @@ canvas.mousemoving {
border: none;
}
.sizer input[type=range]::-moz-range-track {
input[type=range]::-moz-range-track {
width: 100%;
height: 8px;
cursor: pointer;
@ -359,17 +252,17 @@ canvas.mousemoving { @@ -359,17 +252,17 @@ canvas.mousemoving {
}
@media (hover: none) and (pointer: coarse) {
html, body {
touch-action: none;
}
.phone-extra-controls {
display: flex;
}
}
.brush-dom {
display: none;
}
#stroke-preview {
position: absolute;
border-radius: 50%;
left: 50%;
top: 96px;
transform: translate(-50%, -50%);
}
.offline-toast {
@ -385,7 +278,6 @@ canvas.mousemoving { @@ -385,7 +278,6 @@ canvas.mousemoving {
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;
@ -402,47 +294,3 @@ canvas.mousemoving { @@ -402,47 +294,3 @@ canvas.mousemoving {
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;
min-width: 320px;
top: 20px;
right: 20px;
display: flex;
flex-direction: column;
gap: 10px;
user-select: none;
padding: 5px;
background: white;
border: 1px solid var(--dark-blue);
}
.picker-preview-outer {
position: absolute;
top: 16px;
right: 16px;
border: 1px solid black;
}
.picker-preview-inner {
width: 64px;
height: 64px;
border: 1px solid white;
}

130
client/heapify.js

@ -1,130 +0,0 @@ @@ -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(" ")}]`;
}
}

281
client/icons/crosshair.svg

@ -1,281 +0,0 @@ @@ -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

52
client/icons/cursor.svg

@ -2,25 +2,49 @@ @@ -2,25 +2,49 @@
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
width="16"
height="16"
viewBox="0 0 16 16"
width="3.7139902mm"
height="3.7139902mm"
viewBox="0 0 3.7139902 3.7139902"
version="1.1"
id="svg5"
sodipodi:docname="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">
<g id="layer1">
<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="38.057741"
inkscape:cx="6.358759"
inkscape:cy="8.5659315"
inkscape:window-width="2558"
inkscape:window-height="1412"
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(-133.51747,-126.4196)">
<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"
style="fill:none;stroke:#000000;stroke-width:0.3;stroke-linecap:round;stroke-linejoin:round;stroke-dasharray:none;stroke-opacity:1;paint-order:stroke fill markers"
id="path236"
cy="128.53627"
cx="135.63414"
r="1.5875" />
cy="128.2766"
cx="135.37447"
r="1.6" />
</g>
</svg>

Before

Width:  |  Height:  |  Size: 624 B

After

Width:  |  Height:  |  Size: 1.5 KiB

0
client/icons/pen.svg → client/icons/draw.svg

Before

Width:  |  Height:  |  Size: 500 B

After

Width:  |  Height:  |  Size: 500 B

2
client/icons/perfect-bullet.svg

@ -1,2 +0,0 @@ @@ -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 @@ @@ -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 @@ @@ -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 @@ @@ -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 @@ @@ -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

243
client/index.html

@ -7,229 +7,48 @@ @@ -7,229 +7,48 @@
<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="stylesheet" type="text/css" href="default.css">
<!-- <link rel="preload" href="icons/picker.svg" as="image" type="image/svg+xml" /> -->
<script type="text/javascript" src="random_helpers.js"></script>
<script type="text/javascript" src="heapify.js"></script>
<script type="text/javascript" src="bvh.js"></script>
<script type="text/javascript" src="math.js"></script>
<script type="text/javascript" src="tools.js"></script>
<script type="text/javascript" src="speed.js"></script>
<script type="text/javascript" src="webgl_geometry.js"></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>
<link rel="stylesheet" type="text/css" href="default.css?v=28">
<script type="text/javascript" src="aux.js?v=28"></script>
<script type="text/javascript" src="math.js?v=28"></script>
<script type="text/javascript" src="tools.js?v=28"></script>
<script type="text/javascript" src="webgl_geometry.js?v=28"></script>
<script type="text/javascript" src="webgl_shaders.js?v=28"></script>
<script type="text/javascript" src="webgl_listeners.js?v=28"></script>
<script type="text/javascript" src="webgl_draw.js?v=28"></script>
<script type="text/javascript" src="index.js?v=28"></script>
<script type="text/javascript" src="client_send.js?v=28"></script>
<script type="text/javascript" src="client_recv.js?v=28"></script>
<script type="text/javascript" src="websocket.js?v=28"></script>
</head>
<body>
<div class="main">
<canvas id="c"></canvas>
<div class="html-hud"></div>
<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-render">Render</button>
<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-wrapper">
<div class="sizer">
<input type="range" class="slider" id="stroke-width" min="0.01" step="0.01" max="64">
<input type="range" class="slider" id="stroke-width" min="1" max="64">
</div>
<div class="player-list vhide"></div>
<div id="stroke-preview" class="dhide"></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 class="color" data-color="000000"><div class="color-pane" style="background: #000000;"></div></div>
<div class="color" data-color="ffffff"><div class="color-pane" style="background: #ffffff;"></div></div>
<div class="color" data-color="d65c5c"><div class="color-pane" style="background: #d65c5c;"></div></div>
<div class="color" data-color="d6835c"><div class="color-pane" style="background: #d6835c;"></div></div>
<div class="color" data-color="72d65c"><div class="color-pane" style="background: #72d65c;"></div></div>
<div class="color" data-color="5cd6ce"><div class="color-pane" style="background: #5cd6ce;"></div></div>
<div class="color" data-color="5c89d6"><div class="color-pane" style="background: #5c89d6;"></div></div>
<div class="color" data-color="6e5cd6"><div class="color-pane" style="background: #6e5cd6;"></div></div>
</div>
</div>
<div class="tools-wrapper">
<div class="tools">
<div class="tool" data-tool="pointer"><img draggable="false" src="icons/pointer.svg"></div>
<div class="tool active" data-tool="pencil"><img draggable="false" src="icons/pen.svg"></div>
<div class="tool active" data-tool="pencil"><img draggable="false" src="icons/draw.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="undo"><img draggable="false" src="icons/undo.svg"></div>
@ -245,13 +64,5 @@ @@ -245,13 +64,5 @@
<div class="offline-toast hidden">
Whiteboard offline
</div>
<div class="loader">
<svg width="128" height="128" xmlns="http://www.w3.org/2000/svg">
<!-- <circle cx="64" cy="64" r="32" fill="none" stroke="black" opacity="0.5" stroke-width="3" stroke-linecap="round"/> -->
<path id="spinner-path" fill="none" stroke="#aaaaaa" stroke-width="3" stroke-linecap="round"/>
</svg>
</div>
</body>
</html>
</html>

194
client/index.js

@ -1,27 +1,37 @@ @@ -1,27 +1,37 @@
document.addEventListener('DOMContentLoaded', main);
const config = {
ws_url: 'ws://192.168.100.2/ws/',
ping_url: 'http://192.168.100.2/api/ping',
image_url: 'http://192.168.100.2/images/',
sync_timeout: 1000,
ws_reconnect_timeout: 2000,
brush_preview_timeout: 1000,
second_finger_timeout: 500,
buffer_first_touchmoves: 5,
debug_print: true,
min_zoom: 0.05,
max_zoom: 10.0,
initial_offline_timeout: 1000,
default_color: 0x00,
default_width: 8,
bytes_per_point: 20,
initial_static_bytes: 4096,
};
const EVENT = Object.freeze({
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,
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,
REDO: 31,
IMAGE: 40,
IMAGE_MOVE: 41,
IMAGE_SCALE: 42,
ERASER: 50,
});
@ -33,85 +43,16 @@ const MESSAGE = Object.freeze({ @@ -33,85 +43,16 @@ const MESSAGE = Object.freeze({
FULL: 103,
FIRE: 104,
JOIN: 105,
FOLLOW: 106,
});
// Source:
// https://stackoverflow.com/a/18473154
function polarToCartesian(centerX, centerY, radius, angleInDegrees) {
var angleInRadians = (angleInDegrees-90) * Math.PI / 180.0;
return {
x: centerX + (radius * Math.cos(angleInRadians)),
y: centerY + (radius * Math.sin(angleInRadians))
};
}
function describeArc(x, y, radius, startAngle, endAngle) {
var start = polarToCartesian(x, y, radius, endAngle);
var end = polarToCartesian(x, y, radius, startAngle);
var largeArcFlag = (Math.abs(endAngle - startAngle) % 360) <= 180 ? "0" : "1";
var d = [
"M", start.x, start.y,
"A", radius, radius, 0, largeArcFlag, 0, end.x, end.y
].join(" ");
return d;
}
let iii = 0;
let a_angel = 0;
let b_angel = 180;
let speed_a = 2;
let speed_b = 6;
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) {
// speed_a = 3;
// speed_b = 1;
// } else if (Math.abs(a_angel - b_angel) % 360 > 180) {
// speed_a = 1;
// speed_b = 3;
// }
document.querySelector('#spinner-path').setAttribute('d', str);
if (!state.online) {
window.requestAnimationFrame(() => start_spinner(state));
} else {
document.querySelector('.loader').classList.add('hidden');
}
}
async function main() {
function main() {
const state = {
'online': false,
'me': null,
'canvas': {
'offset': { 'x': 0, 'y': 0 },
'zoom_level': 0,
'zoom': 1.0,
'target_zoom': 1.0,
'zoom_screenp': {'x': 0, 'y': 0},
},
'cursor': {
@ -124,47 +65,26 @@ async function main() { @@ -124,47 +65,26 @@ async function main() {
'server_lsn': 0,
'touch': {
'moves': 0,
'drawing': false,
'moving': false,
'erasing': false,
'waiting_for_second_finger': false,
'first_finger_position': null,
'second_finger_position': null,
'buffered': [],
'events': [],
'ids': [],
},
'moving': false,
'drawing': false,
'erasing': false,
'spacedown': false,
'colorpicking': false,
'zooming': false,
'zoomdown': false,
'imagemoving': false,
'imagescaling': false,
'linedrawing': false,
'active_image': null,
'scaling_corner': null,
'ruler_origin': null,
'image_actually_moved': false,
'moving_image': null,
'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': {
'active': null,
@ -173,7 +93,6 @@ async function main() { @@ -173,7 +93,6 @@ async function main() {
'colors': {
'active_element': null,
'extended_element': null,
},
'timers': {
@ -183,27 +102,6 @@ async function main() { @@ -183,27 +102,6 @@ async function main() {
},
'players': {},
'debug': {
'red': false,
'render_from': 0,
'render_to': 0,
},
'rdp_cache': {},
'stats': {},
'following_player': null,
'color_picked': null,
'wasm': {},
'background_pattern': 'dots',
'erase_candidates': tv_create(Uint32Array, 4096),
'snap': null,
};
const context = {
@ -214,54 +112,30 @@ async function main() { @@ -214,54 +112,30 @@ async function main() {
'buffers': {},
'locations': {},
'textures': {},
'images': [],
'dynamic_serializer': serializer_create(config.initial_dynamic_bytes),
'dynamic_index_serializer': serializer_create(config.initial_dynamic_bytes),
'clipped_indices': tv_create(Uint32Array, 4096),
'instance_data_points': tv_create(Float32Array, 4096),
'instance_data_ids': tv_create(Uint32Array, 4096),
'instance_data_pressures': tv_create(Uint8Array, 4096),
'instance_data_batches': tv_create(Uint32Array, 4096),
'dynamic_instance_points': tv_create(Float32Array, 4096),
'dynamic_instance_pressure': tv_create(Uint8Array, 4096),
'dynamic_instance_ids': tv_create(Uint32Array, 4096),
'dynamic_instance_batches': tv_create(Uint32Array, 4096),
'stroke_data': serializer_create(config.initial_static_bytes),
'dynamic_stroke_data': serializer_create(config.initial_static_bytes),
'dynamic_stroke_count': 0,
'dynamic_segment_count': 0,
'quad_positions': [],
'quad_texcoords': [],
'static_stroke_serializer': serializer_create(config.initial_static_bytes),
'dynamic_stroke_serializer': serializer_create(config.initial_static_bytes),
'bgcolor': {'r': 1.0, 'g': 1.0, 'b': 1.0},
'gpu_timer_ext': null,
'last_frame_ts': 0,
'last_frame_dt': 0,
'active_image': null,
};
load_player_cursor_template(state);
start_spinner(state);
const url = new URL(window.location.href);
const parts = url.pathname.split('/');
state.desk_id = parts.length > 0 ? parts[parts.length - 1] : 0;
await init_wasm(state);
init_webgl(state, context);
init_listeners(state, context);
init_tools(state);
ws_connect(state, context, true);
schedule_draw(state, context);
state.timers.offline_toast = setTimeout(() => ui_offline(), config.initial_offline_timeout);
}
}

36
client/index.log

@ -1,36 +0,0 @@ @@ -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!

49
client/lod_worker.js

@ -1,49 +0,0 @@ @@ -1,49 +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['width'],
offsets['xs'],
offsets['ys'],
offsets['pressures'],
offsets['result_buffers'] + thread_id * 4,
offsets['result_counts'] + thread_id * 4,
offsets['result_batch_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);
}
}

662
client/math.js

@ -1,7 +1,3 @@ @@ -1,7 +1,3 @@
function round_to_pow2(value, multiple) {
return (value + multiple - 1) & -multiple;
}
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;
@ -10,93 +6,14 @@ function screen_to_canvas(state, p) { @@ -10,93 +6,14 @@ function screen_to_canvas(state, p) {
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 point_right_of_line(a, b, p) {
// a bit of cross-product tomfoolery (we check sign of z of the crossproduct)
return ((b.x - a.x) * (a.y - p.y) - (a.y - b.y) * (p.x - a.x)) <= 0;
}
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;
function rdp_find_max(state, points, start, end) {
const EPS = 0.5 / state.canvas.zoom;
// const EPS = 10.0;
let result = -1;
let max_dist = 0;
@ -111,7 +28,7 @@ function rdp_find_max2(zoom, points, start, end) { @@ -111,7 +28,7 @@ function rdp_find_max2(zoom, points, start, end) {
const sin_theta = dy / 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 ox = p.x - a.x;
@ -123,7 +40,7 @@ function rdp_find_max2(zoom, points, start, end) { @@ -123,7 +40,7 @@ function rdp_find_max2(zoom, points, start, end) {
const x = rx + a.x;
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) {
result = i;
@ -134,73 +51,121 @@ function rdp_find_max2(zoom, points, start, end) { @@ -134,73 +51,121 @@ function rdp_find_max2(zoom, points, start, end) {
return result;
}
function process_rdp_r2(zoom, points, start, end) {
function process_rdp_r(state, points, start, end) {
let result = [];
const max = rdp_find_max2(zoom, points, start, end);
const max = rdp_find_max(state, points, start, end);
if (max !== -1) {
const before = process_rdp_r2(zoom, points, start, max);
const after = process_rdp_r2(zoom, points, max, end);
const before = process_rdp_r(state, points, start, max);
const after = process_rdp_r(state, points, max, end);
result = [...before, points[max], ...after];
}
return result;
}
function process_rdp2(zoom, points) {
const result = [];
const stack = [];
function process_rdp(state, points) {
const result = process_rdp_r(state, points, 0, points.length - 1);
result.unshift(points[0]);
result.push(points[points.length - 1]);
return result;
}
stack.push({
'type': 0,
'start': 0,
'end': points.length - 1,
});
function process_ewmv(points, round = false) {
const result = [];
const alpha = 0.5;
result.push(points[0]);
while (stack.length > 0) {
const entry = stack.pop();
if (entry.type === 0) {
const max = rdp_find_max2(zoom, points, entry.start, entry.end);
if (max !== -1) {
stack.push({
'type': 0,
'start': max,
'end': entry.end
});
stack.push({
'type': 1,
'index': max,
});
stack.push({
'type': 0,
'start': entry.start,
'end': max,
});
}
} else {
result.push(points[entry.index]);
for (let i = 1; i < points.length; ++i) {
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});
}
return result;
}
function process_stroke(state, points) {
// const result0 = process_ewmv(points);
const result1 = process_rdp(state, points, true);
return result1;
}
function stroke_stats(points, width) {
if (points.length === 0) {
const bbox = {
'xmin': 0,
'ymin': 0,
'xmax': 0,
'ymax': 0
};
return {
'bbox': bbox,
'length': 0,
};
}
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 process_stroke2(zoom, points) {
//const result = smooth_curve(points);
const result = process_rdp2(zoom, points);
function rectangles_intersect(a, b) {
const result = (
a.xmin <= b.xmax
&& a.xmax >= b.xmin
&& a.ymin <= b.ymax
&& a.ymax >= b.ymin
);
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) {
const r = parseInt(color_str.substring(0, 2), 16);
const g = parseInt(color_str.substring(2, 4), 16);
@ -225,22 +190,6 @@ function color_from_u32(color_u32) { @@ -225,22 +190,6 @@ function color_from_u32(color_u32) {
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) {
return (C.y - A.y) * (B.x - A.x) > (B.y - A.y) * (C.x - A.x);
}
@ -250,385 +199,74 @@ function segments_intersect(A, B, C, D) { @@ -250,385 +199,74 @@ function segments_intersect(A, B, C, D) {
return ccw(A, C, D) != ccw(B, C, D) && ccw(A, B, C) !== ccw(A, B, D);
}
function dist_v2(a, b) {
const dx = a.x - b.x;
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) {
if (bbox.x1 <= p.x && p.x < bbox.x2 && bbox.y1 <= p.y && p.y < bbox.y2) {
return true;
}
return false;
}
function clamp(v, a, b) {
return (v < a ? a : (v > b ? b : v));
}
function dot(a, b) {
return a.x * b.x + a.y * b.y;
}
function dotn(a, b) {
let r = 0;
for (let i = 0; i < a.length; ++i) {
r += a[i] * b[i];
}
return r;
}
function dotn3(a, b, c) {
let r = 0;
for (let i = 0; i < a.length; ++i) {
r += a[i] * b[i] * c[i];
}
return r;
}
function strokes_intersect_line(x1, y1, x2, y2) {
const result = [];
function mix(a, b, t) {
return a * t + b * (1 - t);
}
for (const event of storage.events) {
if (event.type === EVENT.STROKE && !event.deleted) {
if (event.points.length < 2) {
continue;
}
function point_in_stroke(p, xs, ys, pressures, width) {
for (let i = 0; i < xs.length - 1; ++i) {
const ax = xs[i + 0];
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,
};
for (let i = 0; i < event.points.length - 1; ++i) {
const sx1 = event.points[i].x;
const sy1 = event.points[i].y;
const ba = {
'x': bx - ax,
'y': by - ay,
};
const sx2 = event.points[i + 1].x;
const sy2 = event.points[i + 1].y;
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 A = {'x': x1, 'y': y1};
const B = {'x': x2, 'y': y2};
const dist = Math.sqrt(dot(v, v)) - thickness;
const C = {'x': sx1, 'y': sy1};
const D = {'x': sx2, 'y': sy2};
if (dist <= 0) {
return true;
if (segments_intersect(A, B, C, D)) {
result.push(event.stroke_id);
break;
}
}
}
}
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};
}
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;
return result;
}
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 dist_v2(a, b) {
const dx = a.x - b.x;
const dy = a.y - b.y;
return Math.sqrt(dx * dx + dy * dy);
}
function quad_union(a, b) {
function mid_v2(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),
'x': (a.x + b.x) / 2.0,
'y': (a.y + b.y) / 2.0,
};
}
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) {
const xs = state.wasm.buffers['xs'].tv.data;
const ys = state.wasm.buffers['ys'].tv.data;
const pressures = state.wasm.buffers['pressures'].tv.data;
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;
}
function proj_remove_x_from_y(xs, ys, ws) {
const dxx = dotn3(xs, xs, ws);
const dxy = dotn3(xs, ys, ws);
if (dxx < 1e-6) {
return [0, ys];
}
const c = dxy / dxx;
const res = [];
for (let i = 0; i < xs.length; ++i) {
res.push(ys[i] - c * xs[i]);
}
return [c, res];
}
function estimate_next_point2(ts, xs, ys, wr=1.0) {
const N = ts.length;
if (N < 2) {
return [xs[N - 1], ys[N - 1]];
}
// inner product weight, power-law ramp over time from W0 to 1.0 (max)
const t0 = ts[0];
const t1 = ts[N - 1];
const ws = [];
// constant and quadratic basis terms
const q0 = [];
const ss = [];
for (const t of ts) {
ws.push(1 + ((wr - 1) * (t - t0) / (t1 - t0)));
q0.push(1);
ss.push(t * t);
}
// constant term
const [c0x, xs_0] = proj_remove_x_from_y(q0, xs, ws);
const [c0y, ys_0] = proj_remove_x_from_y(q0, ys, ws);
const [c0t, ts_0] = proj_remove_x_from_y(q0, ts, ws);
const [c0s, ss_0] = proj_remove_x_from_y(q0, ss, ws);
// linear term
const [c01x, xs_01] = proj_remove_x_from_y(ts_0, xs_0, ws);
const [c01y, ys_01] = proj_remove_x_from_y(ts_0, ys_0, ws);
// don't need to do ts here because it's guaranteed to go to zero
const [c01s, ss_01] = proj_remove_x_from_y(ts_0, ss_0, ws);
function perpendicular(ax, ay, bx, by, width) {
// Place points at (stroke_width / 2) distance from the line
const dirx = bx - ax;
const diry = by - ay;
// quadratic term
const [c012x, xs_012] = proj_remove_x_from_y(ss_01, xs_0, ws);
const [c012y, ys_012] = proj_remove_x_from_y(ss_01, ys_0, ws);
const reconstructed_x = c0x * 1 + c01x * ts_0[N - 1] + c012x * ss_01[N - 1];
const reconstructed_y = c0y * 1 + c01y * ts_0[N - 1] + c012y * ss_01[N - 1];
return [reconstructed_x, reconstructed_y];
}
let pdirx = diry;
let pdiry = -dirx;
function estimate_next_point1(ts, xs, ys, dt=0) {
const N = ts.length;
const pdir_norm = Math.sqrt(pdirx * pdirx + pdiry * pdiry);
if (N < 2) {
return [xs[N - 1], ys[N - 1]];
}
// mean values
let mx = 0, my = 0, mt = 0;
for (let i = 0; i < N; ++i) {
mt += ts[i];
mx += xs[i];
my += ys[i];
}
mt /= N;
mx /= N;
my /= N;
// orthogonalize against constant term
for (let i = 0; i < N; ++i) {
ts[i] -= mt;
xs[i] -= mx;
ys[i] -= my;
}
// dot products against time basis
const dtt = dotn(ts, ts);
const dtx = dotn(ts, xs);
const dty = dotn(ts, ys);
// reconstruction coefficients
const cx = dtx / dtt;
const cy = dty / dtt;
// estimated next values
const nx = cx * (ts[N - 1] + dt) + mx;
const ny = cy * (ts[N - 1] + dt) + my;
return [nx, ny];
}
function smooth_curve(points, window_ms=config.demetri_ms, dt=0) {
const result = [];
pdirx /= pdir_norm;
pdiry /= pdir_norm;
for (let i = 0; i < points.length; ++i) {
let start_i = i;
let end_i = i;
const curr_t = points[i].t;
while (start_i - 1 >= 0) {
if (curr_t - points[start_i - 1].t < window_ms) {
start_i--;
} else {
break;
}
}
const t_window = [];
const x_window = [];
const y_window = []
for (let j = start_i; j < i + 1; ++j) {
const p = points[j];
t_window.push(p.t);
x_window.push(p.x);
y_window.push(p.y);
return {
'p1': {
'x': ax + pdirx * width / 2,
'y': ay + pdiry * width / 2,
},
'p2': {
'x': ax - pdirx * width / 2,
'y': ay - pdiry * width / 2,
}
const [nx, ny] = estimate_next_point2(t_window, x_window, y_window, config.wr);
result.push({
'x': nx,
'y': ny,
'pressure': points[i].pressure,
});
}
return result;
}
};
}

9
client/offline.html

@ -1,9 +0,0 @@ @@ -1,9 +0,0 @@
<script>
async function getFile() {
// Open file picker and destructure the result the first handle
const [fileHandle] = await window.showOpenFilePicker();
const file = await fileHandle.getFile();
return file;
}
</script>

264
client/random_helpers.js

@ -1,264 +0,0 @@ @@ -1,264 +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_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_wrap(view) {
const result = tv_create_on(view.constructor, view.length, view.buffer, 0);
result.size = view.length;
return result;
}
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;
}
}

278
client/speed.js

@ -1,278 +0,0 @@ @@ -1,278 +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 = {};
for (const key in message) {
m[key] = message[key];
}
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: 4096, // F U
maximum: 4096, // 256MiB
shared: true,
});
let master_wasm;
if (WebAssembly.hasOwnProperty('instantiateStreaming')) {
// "Master thread" to do maintance on (static allocations, merging results etc)
master_wasm = await WebAssembly.instantiateStreaming(fetch('wasm/lod.wasm'), {
env: { 'memory': memory }
});
} else {
const f = await fetch('wasm/lod.wasm');
const bytes = await f.arrayBuffer();
master_wasm = await WebAssembly.instantiate(bytes, {
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
},
'width': {
'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);
state.wasm.buffers['width'].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);
state.wasm.buffers['width'].tv = tv_create_on(Uint32Array, initial / 4,
mem, state.wasm.buffers['width'].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;
const old_width_offset = buffers['width'].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['width'].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);
buffers['width'].tv = tv_create_on(Uint32Array, stroke_bytes / 4, mem, buffers['width'].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['width'].tv.size = buffers['width'].used / 4;
// TODO: this is SUS, should all the caps really be coords_bytes?
buffers['xs'].cap = buffers['ys'].cap = buffers['pressures'].cap = coords_bytes;
buffers['coords_from'].cap = buffers['width'].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_width_offset, buffers['width'].used));
memv.set(new Uint8Array(tmp.buffer, 0, buffers['width'].used), buffers['width'].offset);
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 result_batch_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,
'width': buffers['width'].offset,
'xs': buffers['xs'].offset,
'ys': buffers['ys'].offset,
'pressures': buffers['pressures'].offset,
'result_buffers': result_buffers,
'result_counts': result_counts,
'result_batch_counts': result_batch_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_batch_counts,
result_buffers,
state.wasm.workers.length
);
const segment_count = new Int32Array(state.wasm.memory.buffer, result_counts, 1)[0]; // by convention
const batch_count = new Int32Array(state.wasm.memory.buffer, result_batch_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 * 3 * 4, segment_count);
const wasm_batches = new Int32Array(state.wasm.memory.buffer,
result_offset + round_to_pow2(segment_count * (3 * 4 + 1), 4), batch_count * 2);
context.instance_data_points.data = wasm_points;
context.instance_data_points.size = segment_count * 2;
context.instance_data_points.capacity = segment_count * 2;
context.instance_data_ids.data = wasm_ids;
context.instance_data_ids.size = segment_count;
context.instance_data_ids.capacity = segment_count;
context.instance_data_pressures.data = wasm_pressures;
context.instance_data_pressures.size = segment_count;
context.instance_data_pressures.capacity = segment_count;
context.instance_data_batches.data = wasm_batches;
context.instance_data_batches.size = batch_count * 2;
context.instance_data_batches.capacity = batch_count * 2;
return segment_count;
}

138
client/tools.js

@ -2,7 +2,6 @@ function switch_tool(state, item) { @@ -2,7 +2,6 @@ function switch_tool(state, item) {
const tool = item.getAttribute('data-tool');
if (tool === 'undo') {
queue_event(state, undo_event(state));
return;
}
@ -10,93 +9,55 @@ function switch_tool(state, item) { @@ -10,93 +9,55 @@ function switch_tool(state, item) {
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) {
function switch_color(state, item) {
const color = item.getAttribute('data-color');
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;
if (state.me in state.players) {
const color_u32 = color_to_u32(color);
state.players[state.me].color = color_u32
fire_event(state, color_event(color_u32));
}
const last_minor = item.lastElementChild;
const color_css = color_from_u32(color_u32);
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');
state.colors.active_element.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;
function show_stroke_preview(state, size) {
const preview = document.querySelector('#stroke-preview');
// 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'));
preview.style.width = size * state.canvas.zoom + 'px';
preview.style.height = size * state.canvas.zoom + 'px';
preview.style.background = color_from_u32(state.players[state.me].color);
state.colors.extended_element = item;
item.classList.add('extended');
preview.classList.remove('dhide');
}
function set_color_u32(state, color_u32) {
if (color_u32 === state.players[state.me].color) {
return;
}
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 hide_stroke_preview() {
document.querySelector('#stroke-preview').classList.add('dhide');
}
function switch_stroke_width(e, state) {
if (!state.online) return;
const value = parseInt(e.target.value);
const value = e.target.value;
state.players[state.me].width = value;
update_cursor(state);
show_stroke_preview(state, value);
if (state.hide_preview) {
clearTimeout(state.hide_preview);
}
state.hide_preview = setTimeout(hide_stroke_preview, config.brush_preview_timeout);
}
function broadcast_stroke_width(e, state) {
@ -106,55 +67,14 @@ function broadcast_stroke_width(e, state) { @@ -106,55 +67,14 @@ function broadcast_stroke_width(e, state) {
function init_tools(state) {
const tools = document.querySelectorAll('.tools .tool');
const color_groups = document.querySelectorAll('.pallete .color-major');
const colors = document.querySelectorAll('.pallete .color');
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);
})
});
colors.forEach((item) => { item.addEventListener('click', () => switch_color(state, item)); });
// TODO: from localstorage
switch_tool(state, document.querySelector('.tool[data-tool="pencil"]'));
switch_color(state, document.querySelector('.color[data-color="000000"]'));
const slider = document.querySelector('#stroke-width');
@ -163,4 +83,4 @@ function init_tools(state) { @@ -163,4 +83,4 @@ function init_tools(state) {
slider.addEventListener('change', (e) => broadcast_stroke_width(e, state));
document.querySelector('.phone-extra-controls').addEventListener('click', zenmode);
}
}

5
client/touch.css

@ -0,0 +1,5 @@ @@ -0,0 +1,5 @@
@media (pointer:none), (pointer:coarse) {
.tool:hover {
background: #333;
}
}

359
client/touch.js

@ -0,0 +1,359 @@ @@ -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 @@ @@ -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 @@ @@ -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

419
client/wasm/lod.c

@ -1,419 +0,0 @@ @@ -1,419 +0,0 @@
#ifndef FORCE_SCALAR
#include <wasm_simd128.h>
#endif
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;
#ifdef FORCE_SCALAR
// Scalar version preserved for reference
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,
int *width,
float *xs,
float *ys,
unsigned char *pressures,
char **result_buffer,
int *result_count,
int *result_batch_count)
{
if (clipped_count == 0) {
result_count[0] = 0;
result_batch_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(round_to_pow2(segments_head * (3 * 4 + 1), 4) + clipped_count * 4 * 2); // max two ints per stroke for batch info (realistically, much less)
float *points = (float *) output;
int *ids = (int *) (output + segments_head * 4 * 2);
unsigned char *pressures_res = (unsigned char *) (output + segments_head * 4 * 3);
int *batches = (int *) (output + round_to_pow2(segments_head * (4 * 3 + 1), 4));
int phead = 0;
int ihead = 0;
float sqrt_zoom = __builtin_sqrtf(zoom);
int last_lod = -100;
int batch_count = 0;
int batch_size = 0;
for (int i = 0; i < clipped_count; ++i) {
int stroke_index = clipped_indices[i];
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);
}
}
int segment_count = to - from;
// Compute recommended LOD level, add to current batch or start new batch
int lod;
float perceptual_width = width[stroke_index] * zoom;
// The LOD levels have been picked manually based on "COM" (Careful Observation Method) @lod
if (perceptual_width < 1.9f) {
lod = 0;
} else if (perceptual_width < 4.56f) {
lod = 1;
} else if (perceptual_width < 6.12f) {
lod = 2;
} else if (perceptual_width < 25.08f) {
lod = 3;
} else if (perceptual_width < 122.74f) {
lod = 4;
} else if (perceptual_width < 1710.0f) {
lod = 5;
} else {
lod = 6;
}
if (batch_size > 0 && lod != last_lod) {
// Start new batch
batches[batch_count * 2 + 0] = batch_size;
batches[batch_count * 2 + 1] = last_lod;
++batch_count;
batch_size = 0;
}
batch_size += segment_count;
last_lod = lod;
}
if (batch_size > 0) {
batches[batch_count * 2 + 0] = batch_size;
batches[batch_count * 2 + 1] = last_lod;
++batch_count;
}
result_buffer[0] = output;
result_count[0] = segments_head;
result_batch_count[0] = batch_count;
}
// NOT thread-safe, only call from one thread
char *
merge_results(int *segment_counts, int *batch_counts, char **buffers, int nthreads)
{
int total_segments = 0;
int total_batches = 0;
for (int i = 0; i < nthreads; ++i) {
total_segments += segment_counts[i];
total_batches += batch_counts[i];
}
char *merged = alloc_dynamic(round_to_pow2(total_segments * (3 * 4 + 1), 4) + total_batches * 4);
float *points = (float *) merged;
int *ids = (int *) (merged + total_segments * 4 * 2);
unsigned char *pressures = (unsigned char *) (merged + total_segments * 4 * 3);
int *batches = (int *) (merged + round_to_pow2(total_segments * (3 * 4 + 1), 4));
int batch_base = 0;
int last_batch_lod = -99;
int bhead = 0;
int written_batches = 0;
for (int i = 0; i < nthreads; ++i) {
int segments = segment_counts[i];
int nbatches = batch_counts[i];
int *thread_batches = (int *) (buffers[i] + round_to_pow2(segments * (4 * 3 + 1), 4));
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);
for (int j = 0; j < nbatches * 2; j += 2) {
batches[bhead++] = written_batches;
batches[bhead++] = thread_batches[j + 1];
written_batches += thread_batches[j + 0];
}
points += segments * 2;
ids += segments;
pressures += segments;
}
}
segment_counts[0] = total_segments;
batch_counts[0] = total_batches;
return(merged);
}

BIN
client/wasm/lod.wasm

Binary file not shown.

692
client/webgl_draw.js

@ -1,664 +1,92 @@ @@ -1,664 +1,92 @@
function schedule_draw(state, context, animate = false) {
function schedule_draw(state, context) {
if (!state.timers.raf) {
window.requestAnimationFrame(async (ts) => {
await draw(state, context, animate, ts);
});
window.requestAnimationFrame(() => draw(state, context));
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) {
// TODO: only subupload what's needed
const bpp = 2 * 4;
const data_size = serializer.offset;
const data_pixels = data_size / bpp; // data_size % bpp is expected to always be zero here
const rows = Math.ceil(data_pixels / texture_size);
const last_row = data_pixels % texture_size;
const whole_upload = (rows - 1) * texture_size * bpp;
// Upload whole rows
if (rows > 1) {
gl.texSubImage2D(gl.TEXTURE_2D, 0, 0, 0, texture_size, rows - 1, gl.RGBA_INTEGER, gl.UNSIGNED_SHORT, new Uint16Array(serializer.buffer, 0, whole_upload / 2));
}
// Upload last row
if (last_row > 0) {
const last_row_upload = last_row * bpp;
gl.texSubImage2D(gl.TEXTURE_2D, 0, 0, rows - 1, last_row, 1, gl.RGBA_INTEGER, gl.UNSIGNED_SHORT, new Uint16Array(serializer.buffer, whole_upload, last_row_upload / 2));
}
}
/*
PLS FIX ME
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 / 2));
}
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)`;
}
}
}
function draw_strokes(state, width, height, programs, gl, lod_levels, segment_count,
total_lod_floats, total_lod_indices,
batches_tv,
points_tv,
ids_tv,
pressures_tv,
vbo,
ebo,
stroke_texture,
stroke_texture_size,
stroke_data,
stroke_count,
opacity_multiplier,
) {
const pr = programs['main'];
// Last pair (lod unused) to have a proper from;to
tv_add2(batches_tv, segment_count);
tv_add2(batches_tv, -1);
gl.clear(gl.DEPTH_BUFFER_BIT); // draw strokes above the images
gl.useProgram(pr.program);
const total_size = points_tv.size * 4 +
ids_tv.size * 4 +
round_to_pow2(pressures_tv.size, 4) +
total_lod_floats * 4;
gl.bindBuffer(gl.ARRAY_BUFFER, vbo);
gl.bufferData(gl.ARRAY_BUFFER, total_size, gl.STREAM_DRAW);
// Segment points, segment stroke ids, segment pressures
gl.bufferSubData(gl.ARRAY_BUFFER, 0, tv_data(points_tv));
gl.bufferSubData(gl.ARRAY_BUFFER, points_tv.size * 4, tv_data(ids_tv));
gl.bufferSubData(gl.ARRAY_BUFFER, points_tv.size * 4 + ids_tv.size * 4, tv_data(pressures_tv));
gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, ebo);
gl.bufferData(gl.ELEMENT_ARRAY_BUFFER, total_lod_indices * 4, gl.STREAM_DRAW);
// Upload all variants of LOD vertices/indices
const base_lod_points_offset = points_tv.size * 4 + ids_tv.size * 4 + round_to_pow2(pressures_tv.size, 4);
for (const level of lod_levels) {
gl.bufferSubData(gl.ARRAY_BUFFER, base_lod_points_offset + level.vertices_offset, tv_data(level.data.points));
gl.bufferSubData(gl.ELEMENT_ARRAY_BUFFER, level.indices_offset, tv_data(level.data.indices));
}
// Per-stroke data (base width, color)
gl.bindTexture(gl.TEXTURE_2D, stroke_texture);
upload_square_rgba16ui_texture(gl, stroke_data, stroke_texture_size);
gl.uniform2f(pr.locations['u_res'], width, 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'], 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.stroke_texture_size);
gl.uniform1f(pr.locations['u_opacity_multipliter'], opacity_multiplier);
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']);
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);
for (let b = 0; b < batches_tv.size - 2; b += 2) {
const batch_from = batches_tv.data[b + 0];
const batch_size = batches_tv.data[b + 2] - batch_from;
let lod_level = batches_tv.data[b + 1];
if (config.debug_force_lod !== null) {
lod_level = config.debug_force_lod;
}
const level = lod_levels[lod_level];
if (batch_size > 0) {
//stat_total_vertices += batch_size * level.data.indices.size;
gl.uniform1i(pr.locations['u_circle_points'], level.data.points.size / 2 - 4);
gl.uniform3f(pr.locations['u_debug_color'],
(lod_level * 785892 + 125127) % 8 / 7,
(lod_level * 901824 + 985835) % 8 / 7,
(lod_level * 232181 + 838533) % 8 / 7,
);
// 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, batch_from * 2 * 4);
gl.vertexAttribPointer(pr.locations['a_b'], 2, gl.FLOAT, false, 2 * 4, batch_from * 2 * 4 + 2 * 4);
gl.vertexAttribIPointer(pr.locations['a_stroke_id'], 1, gl.INT, 4, points_tv.size * 4 + batch_from * 4);
gl.vertexAttribPointer(pr.locations['a_pressure'], 2, gl.UNSIGNED_BYTE, true, 1, points_tv.size * 4 + ids_tv.size * 4 + batch_from);
gl.vertexAttribPointer(pr.locations['a_pos'], 2, gl.FLOAT, false, 2 * 4, base_lod_points_offset + level.vertices_offset, 4);
gl.drawElementsInstanced(gl.TRIANGLES, level.data.indices.size, gl.UNSIGNED_INT, level.indices_offset, batch_size);
}
}
// 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);
tv_pop(batches_tv);
tv_pop(batches_tv);
}
async function draw(state, context, animate, ts, external_draw = false) {
const dt = ts - context.last_frame_ts;
const cpu_before = performance.now();
context.last_frame_ts = ts;
function draw(state, context) {
state.timers.raf = false;
const gl = context.gl;
const width = window.innerWidth;
const height = window.innerHeight;
let locations;
let buffers;
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);
}
}
gl.clear(gl.COLOR_BUFFER_BIT);
// 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: @speed we can do this once at startup
const lod_levels = [];
let total_lod_floats = 0;
let total_lod_indices = 0;
let stat_total_vertices = 0;
for (let i = 0; i <= 6; ++i) {
const d = geometry_good_circle_and_dummy(i);
lod_levels.push({
'data': d,
'vertices_offset': total_lod_floats * 4,
'indices_offset': total_lod_indices * 4,
});
total_lod_floats += d.points.size;
total_lod_indices += d.indices.size;
}
// "Static" data upload
if (segment_count > 0) {
draw_strokes(state, context.canvas.width, context.canvas.height, programs, gl, lod_levels, segment_count,
total_lod_floats,
total_lod_indices,
context.instance_data_batches,
context.instance_data_points,
context.instance_data_ids,
context.instance_data_pressures,
buffers['b_strokes_static'],
buffers['i_strokes_static'],
textures['stroke_data'],
config.stroke_texture_size,
context.stroke_data,
state.events.length, // not really
1.0,
);
}
// Dynamic draw (strokes currently being drawn)
if (dynamic_segment_count > 0) {
// Dynamic strokes should be drawn above static strokes
gl.clear(gl.DEPTH_BUFFER_BIT);
draw_strokes(state, context.canvas.width, context.canvas.height, programs, gl, lod_levels, dynamic_segment_count,
total_lod_floats,
total_lod_indices,
context.dynamic_instance_batches,
context.dynamic_instance_points,
context.dynamic_instance_ids,
context.dynamic_instance_pressure,
buffers['b_strokes_dynamic'],
buffers['i_strokes_dynamic'],
textures['dynamic_stroke_data'],
config.stroke_texture_size,
context.dynamic_stroke_data,
context.dynamic_stroke_count,
0.5,
);
}
// HUD: resize handles, etc
if (state.active_image !== null) {
gl.clear(gl.DEPTH_BUFFER_BIT);
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
const hud_batches = tv_create(Uint32Array, 4);
tv_add(hud_batches, 0);
tv_add(hud_batches, compute_circle_lod(state.canvas.zoom * 2));
tv_add(hud_batches, ui_segments);
tv_add(hud_batches, -1);
locations = context.locations['quad'];
buffers = context.buffers['quad'];
draw_strokes(state, context.canvas.width, context.canvas.height, programs, gl, lod_levels, ui_segments,
total_lod_floats,
total_lod_indices,
hud_batches,
handles.points,
handles.ids,
handles.pressures,
buffers['b_hud'],
buffers['i_hud'],
textures['ui'],
config.ui_texture_size,
handles.stroke_data,
8,
1.0,
);
}
gl.useProgram(context.programs['quad']);
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.enableVertexAttribArray(locations['a_pos']);
gl.enableVertexAttribArray(locations['a_texcoord']);
gl.useProgram(pr.program);
gl.uniform2f(locations['u_res'], context.canvas.width, context.canvas.height);
gl.uniform2f(locations['u_scale'], state.canvas.zoom, state.canvas.zoom);
gl.uniform2f(locations['u_translation'], state.canvas.offset.x, state.canvas.offset.y);
gl.uniform1i(locations['u_texture'], 0);
gl.bindBuffer(gl.ARRAY_BUFFER, buffers['b_iquads']);
gl.bufferData(gl.ARRAY_BUFFER, tv_data(bboxes), gl.STREAM_DRAW);
gl.bindBuffer(gl.ARRAY_BUFFER, buffers['b_pos']);
gl.vertexAttribPointer(locations['a_pos'], 2, gl.FLOAT, false, 0, 0);
gl.bufferData(gl.ARRAY_BUFFER, context.quad_positions_f32, gl.STATIC_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.bindBuffer(gl.ARRAY_BUFFER, buffers['b_texcoord']);
gl.vertexAttribPointer(locations['a_texcoord'], 2, gl.FLOAT, false, 0, 0);
gl.bufferData(gl.ARRAY_BUFFER, context.quad_texcoords_f32, gl.STATIC_DRAW);
gl.enableVertexAttribArray(pr.locations['a_topleft']);
gl.enableVertexAttribArray(pr.locations['a_bottomright']);
const count = Object.keys(context.textures).length;
let active_image_index = -1;
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.uniform1i(locations['u_outline'], 0);
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);
for (let key = 0; key < count; ++key) {
if (context.textures[key].image_id === context.active_image) {
active_image_index = key;
continue;
}
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);
gl.bindTexture(gl.TEXTURE_2D, context.textures[key].texture);
gl.drawArrays(gl.TRIANGLES, key * 6, 6);
}
document.getElementById('debug-stats').innerHTML = `
<span>Strokes onscreen: ${context.clipped_indices.size}</span>
<span>Segments onscreen: ${segment_count}</span>
<span>Total vertices: ${stat_total_vertices}</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 * 10000) / 10000}</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 : 0);
if (!external_draw) {
schedule_draw(state, context, true);
}
if (active_image_index !== -1) {
gl.uniform1i(locations['u_outline'], 1);
gl.bindTexture(gl.TEXTURE_2D, context.textures[active_image_index].texture);
gl.drawArrays(gl.TRIANGLES, active_image_index * 6, 6);
}
}
// https://www.youtube.com/watch?v=LSNQuFEDOyQ
function exp_decay(a, b, decay, dt) {
return b + (a - b) * Math.exp(-decay * dt);
}
function update_canvas_zoom(state, current, target, dt) {
let decay = config.animation_decay;
// Strokes
locations = context.locations['stroke'];
buffers = context.buffers['stroke'];
gl.useProgram(context.programs['stroke']);
gl.enableVertexAttribArray(locations['a_type']);
gl.enableVertexAttribArray(locations['a_pos']);
gl.enableVertexAttribArray(locations['a_texcoord']);
gl.enableVertexAttribArray(locations['a_color']);
if (state.zoomdown) {
decay *= config.vertical_zoom_speed_multiplier; // to make it feel more responsive at fast speed
}
gl.uniform2f(locations['u_res'], context.canvas.width, context.canvas.height);
gl.uniform2f(locations['u_scale'], state.canvas.zoom, state.canvas.zoom);
gl.uniform2f(locations['u_translation'], state.canvas.offset.x, state.canvas.offset.y);
if (Math.abs(1.0 - current / target) > 0.01) {
state.canvas.zoom = exp_decay(state.canvas.zoom, target, decay, dt / 1000.0);
} else {
state.canvas.zoom = target;
}
gl.bindBuffer(gl.ARRAY_BUFFER, buffers['b_packed']);
gl.vertexAttribPointer(locations['a_pos'], 2, gl.FLOAT, false, config.bytes_per_point, 0);
gl.vertexAttribPointer(locations['a_texcoord'], 2, gl.FLOAT, false, config.bytes_per_point, 8);
gl.vertexAttribPointer(locations['a_color'], 3, gl.UNSIGNED_BYTE, true, config.bytes_per_point, 16);
gl.vertexAttribPointer(locations['a_type'], 1, gl.UNSIGNED_BYTE, false, config.bytes_per_point, 19);
// 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;
gl.bufferData(gl.ARRAY_BUFFER, context.static_stroke_serializer.buffer, gl.STATIC_DRAW);
gl.drawArrays(gl.TRIANGLES, 0, context.static_stroke_serializer.offset / config.bytes_per_point);
update_cursor(state);
}
gl.bufferData(gl.ARRAY_BUFFER, context.dynamic_stroke_serializer.buffer, gl.STATIC_DRAW);
gl.drawArrays(gl.TRIANGLES, 0, context.dynamic_stroke_serializer.offset / config.bytes_per_point);
}

921
client/webgl_geometry.js

@ -1,829 +1,244 @@ @@ -1,829 +1,244 @@
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(state, 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);
tv_add(state.wasm.buffers['width'].tv, 0);
state.wasm.buffers['width'].used += 4;
}
// 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);
function push_point(s, x, y, u, v, r, g, b, type) {
ser_f32(s, x);
ser_f32(s, y);
ser_f32(s, u);
ser_f32(s, v);
// ser_u8(s, Math.floor(Math.random() * 255));
// ser_u8(s, Math.floor(Math.random() * 255));
// ser_u8(s, Math.floor(Math.random() * 255));
ser_u8(s, r);
ser_u8(s, g);
ser_u8(s, b);
ser_u8(s, type);
}
function push_circle(s, cx, cy, radius, r, g, b) {
push_point(s, cx - radius, cy - radius, 0, 0, r, g, b, 1);
push_point(s, cx - radius, cy + radius, 0, 1, r, g, b, 1);
push_point(s, cx + radius, cy - radius, 1, 0, r, g, b, 1);
push_point(s, cx + radius, cy + radius, 1, 1, r, g, b, 1);
push_point(s, cx + radius, cy - radius, 1, 0, r, g, b, 1);
push_point(s, cx - radius, cy + radius, 0, 1, r, g, b, 1);
}
function push_quad(s, p1x, p1y, p2x, p2y, p3x, p3y, p4x, p4y, r, g, b) {
push_point(s, p1x, p1y, 0, 0, r, g, b, 0);
push_point(s, p2x, p2y, 0, 1, r, g, b, 0);
push_point(s, p3x, p3y, 1, 0, r, g, b, 0);
push_point(s, p4x, p4y, 1, 1, r, g, b, 0);
push_point(s, p3x, p3y, 1, 0, r, g, b, 0);
push_point(s, p2x, p2y, 0, 1, r, g, b, 0);
}
function push_stroke(s, stroke) {
const stroke_width = stroke.width;
const points = stroke.points;
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);
tv_add(state.wasm.buffers['width'].tv, stroke.width);
state.wasm.buffers['width'].used += 4;
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_ensure(context.dynamic_instance_batches, round_to_pow2(total_strokes * 2, 4096));
tv_clear(context.dynamic_instance_points);
tv_clear(context.dynamic_instance_pressure);
tv_clear(context.dynamic_instance_ids);
tv_clear(context.dynamic_instance_batches);
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;
let last_lod = -100;
let batch_size = 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);
const perceptual_width = stroke.width * state.canvas.zoom;
const lod = compute_circle_lod(perceptual_width);
// Copypaste from the WASM version @lod
if (batch_size > 0 && lod !== last_lod) {
tv_add(context.dynamic_instance_batches, batch_size);
tv_add(context.dynamic_instance_batches, last_lod);
batch_size = 0;
}
batch_size += stroke.points.length;
last_lod = lod;
stroke_index += 1; // TODO: proper player Z order
}
}
if (points.length === 0) {
return;
}
if (batch_size > 0) {
tv_add(context.dynamic_instance_batches, batch_size);
tv_add(context.dynamic_instance_batches, last_lod);
if (points.length === 1) {
push_circle(s, points[0].x, points[0].y, stroke_width / 2, r, g, b);
return;
}
context.dynamic_segment_count = total_points;
context.dynamic_stroke_count = total_strokes;
let batch_base = 0;
for (let i = 0; i < points.length - 1; ++i) {
const px = points[i].x;
const py = points[i].y;
for (let i = 0; i < context.dynamic_instance_batches.size; i += 2) {
const nbatches = context.dynamic_instance_batches.data[i * 2 + 0];
context.dynamic_instance_batches.data[i * 2 + 0] = batch_base;
batch_base += nbatches;
}
}
function compute_circle_lod(perceptual_width) {
let lod;
if (perceptual_width < 1.9) {
lod = 0;
} else if (perceptual_width < 4.56) {
lod = 1;
} else if (perceptual_width < 6.12) {
lod = 2;
} else if (perceptual_width < 25.08) {
lod = 3;
} else if (perceptual_width < 122.74) {
lod = 4;
} else if (perceptual_width < 1710.0) {
lod = 5;
} else {
lod = 6;
}
const nextpx = points[i + 1].x;
const nextpy = points[i + 1].y;
return lod;
}
const d1x = nextpx - px;
const d1y = nextpy - py;
function geometry_start_prestroke(state, player_id) {
if (!state.online) return;
// Perpendicular to (d1x, d1y), points to the LEFT
let perp1x = -d1y;
let perp1y = d1x;
const player = state.players[player_id];
const perpnorm1 = Math.sqrt(perp1x * perp1x + perp1y * perp1y);
player.strokes.push({
'empty': false,
'points': [],
'raw_points': [],
'head': null,
'color': player.color,
'width': player.width,
});
perp1x /= perpnorm1;
perp1y /= perpnorm1;
player.current_prestroke = true;
}
const s1x = px + perp1x * stroke_width / 2;
const s1y = py + perp1y * stroke_width / 2;
const s2x = px - perp1x * stroke_width / 2;
const s2y = py - perp1y * stroke_width / 2;
function geometry_end_prestroke(state, player_id) {
if (!state.online) return;
const player = state.players[player_id];
player.current_prestroke = false;
}
const s3x = nextpx + perp1x * stroke_width / 2;
const s3y = nextpy + perp1y * stroke_width / 2;
const s4x = nextpx - perp1x * stroke_width / 2;
const s4y = nextpy - perp1y * stroke_width / 2;
function geometry_add_prepoint(state, context, player_id, point, is_pen, raw = false) {
if (config.p === 'demetri') {
return geometry_add_prepoint_demetri(state, context, player_id, point, is_pen, raw);
} else if (config.p == 'raw') {
return geometry_add_prepoint_raw(state, context, player_id, point, is_pen, raw);
} else if (config.p == 'avg') {
return geometry_add_prepoint_new(state, context, player_id, point, is_pen, raw);
} else if (config.p == 'perf') {
return geometry_add_prepoint_old(state, context, player_id, point, is_pen, raw);
push_quad(s, s2x, s2y, s1x, s1y, s4x, s4y, s3x, s3y, r, g, b);
push_circle(s, px, py, stroke_width / 2, r, g, b);
}
}
function geometry_add_prepoint_demetri(state, context, player_id, point, is_pen, raw = false) {
if (!state.online) return;
const lastp = points[points.length - 1];
const player = state.players[player_id];
const stroke = player.strokes[player.strokes.length - 1];
stroke.raw_points.push(point);
stroke.points = smooth_curve(stroke.raw_points);
if (point.pressure < config.min_pressure) {
point.pressure = config.min_pressure;
}
recompute_dynamic_data(state, context);
push_circle(s, lastp.x, lastp.y, stroke_width / 2, r, g, b);
}
function geometry_add_prepoint_raw(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;
function geometry_prepare_stroke(state) {
if (!state.online) {
return null;
}
points.push(point);
recompute_dynamic_data(state, context);
return {
'color': state.players[state.me].color,
'width': state.players[state.me].width,
'points': process_stroke(state, state.players[state.me].points),
'user_id': state.me,
};
}
function average_screen_speed(state, points, last, range=10) {
let screen_last = canvas_to_screen(state, points[points.length - 1]);
let sum_speed = 0;
let n = 0;
function geometry_add_stroke(state, context, stroke) {
if (!state.online || !stroke) return;
for (let i = points.length - 1; i >= 0 && i >= points.length - range; --i) {
const point = points[i];
const screen_this = canvas_to_screen(state, point);
const screen_dx = Math.abs(screen_this.x - screen_last.x);
const screen_dy = Math.abs(screen_this.y - screen_last.y);
const screen_speed = Math.sqrt(screen_dx * screen_dx + screen_dy * screen_dy);
const bytes_left = context.static_stroke_serializer.size - context.static_stroke_serializer.offset;
const bytes_needed = (stroke.points.length * 12 + 6) * config.bytes_per_point;
sum_speed += screen_speed;
n++;
if (bytes_left < bytes_needed) {
const old_view = context.static_stroke_serializer.strview;
const old_offset = context.static_stroke_serializer.offset;
screen_last = screen_this;
}
const new_size = Math.ceil((context.static_stroke_serializer.size + bytes_needed) * 1.62);
if (n === 0) {
return 0;
context.static_stroke_serializer = serializer_create(new_size);
context.static_stroke_serializer.strview.set(old_view);
context.static_stroke_serializer.offset = old_offset;
}
return sum_speed / n;
push_stroke(context.static_stroke_serializer, stroke);
}
function geometry_add_prepoint_new(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;
const raw_points = stroke.raw_points;
if (point.pressure < config.min_pressure) {
point.pressure = config.min_pressure;
}
let avg_window = 0;
if (raw_points.length > 0) {
const screen_speed = average_screen_speed(state, raw_points, point);
// Empirically chosen.
// TODO: dpi scaling?
const bot = 1;
const top = 10;
const max_avg_window = config.avg_window;
function recompute_dynamic_data(state, context) {
let bytes_needed = 0;
if (screen_speed <= bot) {
avg_window = max_avg_window;
} else if (screen_speed >= top) {
avg_window = 0;
} else {
const onezero = 1.0 - (screen_speed - bot) / (top - bot);
avg_window = Math.floor(onezero * max_avg_window);
for (const player_id in state.players) {
const player = state.players[player_id];
if (player.points.length > 0) {
bytes_needed += (player.points.length * 12 + 6) * config.bytes_per_point;
}
}
avg_window = Math.min(avg_window, raw_points.length);
let xsum = 0;
let ysum = 0;
for (let i = raw_points.length - avg_window; i < raw_points.length; ++i) {
xsum += raw_points[i].x;
ysum += raw_points[i].y;
}
xsum += point.x;
ysum += point.y;
points.push({
'x': xsum / (avg_window + 1),
'y': ysum / (avg_window + 1),
'pressure': point.pressure
});
stroke.raw_points.push(point);
recompute_dynamic_data(state, context);
}
function geometry_add_prepoint_old(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 (bytes_needed > context.dynamic_stroke_serializer.size) {
context.dynamic_stroke_serializer = serializer_create(Math.ceil(bytes_needed * 1.62));
} else {
context.dynamic_stroke_serializer.offset = 0;
}
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;
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];
if (player.points.length > 0) {
push_stroke(context.dynamic_stroke_serializer, player);
}
} 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) {
function geometry_add_point(state, context, player_id, point) {
if (!state.online) return;
const player = state.players[player_id];
player.strokes.shift();
if (player.strokes.length === 0) {
player.current_prestroke = false;
}
state.players[player_id].points.push(point);
recompute_dynamic_data(state, context);
}
function geometry_clear_newest_prestroke(state, context, player_id) {
function geometry_clear_player(state, context, player_id) {
if (!state.online) return;
const player = state.players[player_id];
player.strokes.pop();
if (player.strokes.length === 0) {
player.current_prestroke = false;
}
state.players[player_id].points.length = 0;
recompute_dynamic_data(state, context);
}
function add_image(context, image_id, bitmap, p, width, height) {
function add_image(context, image_id, bitmap, p) {
const x = p.x;
const y = p.y;
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);
}
const id = Object.keys(context.textures).length;
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});
context.textures[id] = {
'texture': gl.createTexture(),
'image_id': image_id
};
topleft.x = Math.floor(topleft.x / step_x) * step_x;
topleft.y = Math.floor(topleft.y / step_y) * step_y;
gl.bindTexture(gl.TEXTURE_2D, context.textures[id].texture);
gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA,gl.UNSIGNED_BYTE, bitmap);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR);
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);
bottomright.x = Math.ceil(bottomright.x / step_x) * step_x;
bottomright.y = Math.ceil(bottomright.y / step_y) * step_y;
context.quad_positions.push(...[
x, y,
x, y + bitmap.height,
x + bitmap.width, y + bitmap.height,
for (let x = topleft.x; x <= bottomright.x; x += step_x) {
result.push(1, x);
}
x + bitmap.width, y,
x, y,
x + bitmap.width, y + bitmap.height,
]);
for (let y = topleft.y; y <= bottomright.y; y += step_y) {
result.push(-1, y);
}
context.quad_texcoords.push(...[
0, 0,
0, 1,
1, 1,
1, 0,
0, 0,
1, 1,
]);
return result;
context.quad_positions_f32 = new Float32Array(context.quad_positions);
context.quad_texcoords_f32 = new Float32Array(context.quad_texcoords);
}
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];
function move_image(context, image_event) {
const x = image_event.x;
const y = image_event.y;
result[i * 12 + 0] = entry.at.x;
result[i * 12 + 1] = entry.at.y;
const count = Object.keys(context.textures).length;
result[i * 12 + 2] = entry.at.x + entry.width;
result[i * 12 + 3] = entry.at.y;
for (let id = 0; id < count; ++id) {
const image = context.textures[id];
if (image.image_id === image_event.image_id) {
context.quad_positions[id * 12 + 0] = x;
context.quad_positions[id * 12 + 1] = y;
context.quad_positions[id * 12 + 2] = x;
context.quad_positions[id * 12 + 3] = y + image_event.height;
context.quad_positions[id * 12 + 4] = x + image_event.width;
context.quad_positions[id * 12 + 5] = y + image_event.height;
result[i * 12 + 4] = entry.at.x;
result[i * 12 + 5] = entry.at.y + entry.height;
context.quad_positions[id * 12 + 6] = x + image_event.width;
context.quad_positions[id * 12 + 7] = y;
context.quad_positions[id * 12 + 8] = x;
context.quad_positions[id * 12 + 9] = y;
context.quad_positions[id * 12 + 10] = x + image_event.width;
context.quad_positions[id * 12 + 11] = y + image_event.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;
}
context.quad_positions_f32 = new Float32Array(context.quad_positions);
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, 2);
}
return {
'points': tv_wrap(points),
'ids': tv_wrap(ids),
'pressures': tv_wrap(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) + 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++;
}
function image_at(state, x, y) {
for (let i = state.events.length - 1; i >= 0; --i) {
const event = state.events[i];
if (event.type === EVENT.IMAGE && !event.deleted) {
if ('height' in event && 'width' in event) {
if (event.x <= x && x <= event.x + event.width && event.y <= y && y <= event.y + event.height) {
return event;
}
}
}
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
};
}
return null;
}

1003
client/webgl_listeners.js

File diff suppressed because it is too large Load Diff

421
client/webgl_shaders.js

@ -1,399 +1,139 @@ @@ -1,399 +1,139 @@
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;
const stroke_vs_src = `
attribute float a_type;
attribute vec2 a_pos;
attribute vec2 a_texcoord;
attribute vec3 a_color;
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;
out float v_scalefade;
varying vec3 v_color;
varying vec2 v_texcoord;
varying float v_type;
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 canvas_pixel = 1.0 / u_scale.x; // assuming square pixels here..
float radius = max(0.5 * canvas_pixel, float(stroke_data.w));
// Fade from 1.0 to 0.0 based on sqrt distance beyond being 1 pixel wide? (compeltely ad-hoc, picked whatever looked good to me)
float scalefade = float(stroke_data.w) / canvas_pixel;
v_scalefade = min(1.0, sqrt(scalefade));
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;
}
}
vec2 screen01 = (a_pos * u_scale + u_translation) / u_res;
vec2 screen02 = screen01 * 2.0;
screen02 = (pos.xy * u_scale + u_translation) / u_res * 2.0;
screen02.y = 2.0 - screen02.y;
v_color = a_color;
v_texcoord = a_texcoord;
v_type = a_type;
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 + 1) / float(u_stroke_count + 1)) * 2.0 - 1.0, 1.0);
gl_Position = vec4(screen02 - 1.0, 0, 1);
}
`;
const sdf_fs_src = `#version 300 es
precision highp float;
uniform int u_debug_mode;
uniform vec3 u_debug_color;
uniform float u_opacity_multipliter;
const stroke_fs_src = `
#extension GL_OES_standard_derivatives : enable
in vec3 v_color;
in float v_scalefade;
precision mediump float;
layout(location = 0) out vec4 FragColor;
varying vec3 v_color;
varying vec2 v_texcoord;
varying float v_type;
void main() {
if (u_debug_mode == 0) {
float alpha = u_opacity_multipliter * v_scalefade;
FragColor = vec4(v_color * alpha, alpha);
} else {
FragColor = vec4(u_debug_color, 0.8);
}
vec2 uv = v_texcoord * 2.0 - 1.0;
float sdf = 1.0 - mix(abs(uv.y), length(uv), v_type);
float pd = fwidth(sdf);
float alpha = 1.0 - smoothstep(pd, 0.0, sdf);
gl_FragColor = vec4(v_color * alpha, alpha);
}
`;
const tquad_vs_src = `#version 300 es
in vec2 a_pos;
const tquad_vs_src = `
attribute vec2 a_pos;
attribute vec2 a_texcoord;
uniform vec2 u_scale;
uniform vec2 u_res;
uniform vec2 u_translation;
out vec2 v_texcoord;
varying 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;
v_texcoord = a_texcoord;
gl_Position = vec4(screen11, 0, 1);
}
`;
const tquad_fs_src = `#version 300 es
precision highp float;
const tquad_fs_src = `
precision mediump float;
in vec2 v_texcoord;
varying 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;
uniform bool u_outline;
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);
if (!u_outline) {
gl_FragColor = texture2D(u_texture, v_texcoord);
} 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);
gl_FragColor = mix(texture2D(u_texture, v_texcoord), vec4(0.7, 0.7, 0.95, 1), 0.5);
}
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', {
context.gl = context.canvas.getContext('webgl', {
'preserveDrawingBuffer': true,
'desynchronized': true,
'antialias': true,
'antialias': false,
});
const gl = context.gl;
gl.enable(gl.BLEND);
gl.blendFunc(gl.ONE, gl.ONE_MINUS_SRC_ALPHA);
gl.getExtension('OES_standard_derivatives');
gl.enable(gl.DEPTH_TEST);
gl.depthFunc(gl.NOTEQUAL);
const stroke_vs = create_shader(gl, gl.VERTEX_SHADER, stroke_vs_src);
const stroke_fs = create_shader(gl, gl.FRAGMENT_SHADER, stroke_fs_src);
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);
context.programs['stroke'] = create_program(gl, stroke_vs, stroke_fs);
context.programs['quad'] = create_program(gl, quad_vs, quad_fs);
const grid_vs = create_shader(gl, gl.VERTEX_SHADER, grid_vs_src);
context.locations['stroke'] = {
'a_type': gl.getAttribLocation(context.programs['stroke'], 'a_type'),
'a_pos': gl.getAttribLocation(context.programs['stroke'], 'a_pos'),
'a_texcoord': gl.getAttribLocation(context.programs['stroke'], 'a_texcoord'),
'a_color': gl.getAttribLocation(context.programs['stroke'], 'a_color'),
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),
'u_res': gl.getUniformLocation(context.programs['stroke'], 'u_res'),
'u_scale': gl.getUniformLocation(context.programs['stroke'], 'u_scale'),
'u_translation': gl.getUniformLocation(context.programs['stroke'], 'u_translation'),
};
context.buffers = {
'b_images': gl.createBuffer(),
'b_strokes_static': gl.createBuffer(),
'i_strokes_static': gl.createBuffer(),
'b_strokes_dynamic': gl.createBuffer(),
'i_strokes_dynamic': gl.createBuffer(),
'b_instance_dot': gl.createBuffer(),
'b_instance_grid': gl.createBuffer(),
'b_dot': gl.createBuffer(),
'b_hud': gl.createBuffer(),
'i_hud': gl.createBuffer(),
'b_iquads': gl.createBuffer(),
context.locations['quad'] = {
'a_pos': gl.getAttribLocation(context.programs['quad'], 'a_pos'),
'a_texcoord': gl.getAttribLocation(context.programs['quad'], 'a_texcoord'),
'u_res': gl.getUniformLocation(context.programs['quad'], 'u_res'),
'u_scale': gl.getUniformLocation(context.programs['quad'], 'u_scale'),
'u_translation': gl.getUniformLocation(context.programs['quad'], 'u_translation'),
'u_outline': gl.getUniformLocation(context.programs['quad'], 'u_outline'),
'u_texture': gl.getUniformLocation(context.programs['quad'], 'u_texture'),
};
context.textures = {
'stroke_data': gl.createTexture(),
'dynamic_stroke_data': gl.createTexture(),
'ui': gl.createTexture(),
context.buffers['stroke'] = {
'b_packed': context.gl.createBuffer(),
};
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.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));
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));
context.buffers['quad'] = {
'b_pos': context.gl.createBuffer(),
'b_texcoord': context.gl.createBuffer(),
};
const resize_canvas = (entries) => {
// https://www.khronos.org/webgl/wiki/HandlingHighDPI
@ -423,7 +163,7 @@ function init_webgl(state, context) { @@ -423,7 +163,7 @@ function init_webgl(state, context) {
function create_shader(gl, type, source) {
const shader = gl.createShader(type);
gl.shaderSource(shader, source);
gl.compileShader(shader);
@ -444,26 +184,9 @@ function create_program(gl, vs, fs) { @@ -444,26 +184,9 @@ function create_program(gl, vs, 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;
return program;
}
console.error('link:', gl.getProgramInfoLog(program));
gl.deleteProgram(program);

8
client/websocket.js

@ -28,7 +28,7 @@ async function ws_connect(state, context, first_connect = false) { @@ -28,7 +28,7 @@ async function ws_connect(state, context, first_connect = false) {
}
}
} catch (e) {
console.log('Could not ping the server:', e);
// console.log('Could not ping the server:', e);
}
state.timers.offline_toast = setTimeout(() => ws_connect(state, context, first_connect), config.ws_reconnect_timeout);
@ -48,7 +48,7 @@ async function on_message(state, context, event) { @@ -48,7 +48,7 @@ async function on_message(state, context, event) {
message_data = await data.arrayBuffer();
const view = new DataView(message_data);
const d = deserializer_create(message_data, view);
handle_message(state, context, d);
await handle_message(state, context, d);
} else {
/* For all my Safari < 14 bros out there */
const reader = new FileReader();
@ -56,7 +56,7 @@ async function on_message(state, context, event) { @@ -56,7 +56,7 @@ async function on_message(state, context, event) {
message_data = e.target.result;
const view = new DataView(message_data);
const d = deserializer_create(message_data, view);
handle_message(state, context, d);
await handle_message(state, context, d);
};
reader.readAsArrayBuffer(data);
@ -72,4 +72,4 @@ function on_close(state, context) { @@ -72,4 +72,4 @@ function on_close(state, context) {
function on_error(state, context) {
ws.close(state, context);
}
}

3
server/config.js

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

BIN
server/data-local.sqlite

Binary file not shown.

62
server/deserializer.js

@ -26,12 +26,6 @@ export function u32(d) { @@ -26,12 +26,6 @@ export function u32(d) {
return value;
}
export function s32(d) {
const value = d.view.getInt32(d.offset, true);
d.offset += 4;
return value;
}
export function f32(d) {
const value = d.view.getFloat32(d.offset, true);
d.offset += 4;
@ -44,12 +38,6 @@ function f32array(d, count) { @@ -44,12 +38,6 @@ function f32array(d, count) {
return array;
}
function u8array(d, count) {
const array = new Uint8Array(d.view.buffer, d.offset, count);
d.offset += count;
return array;
}
export function align(d, to) {
while (d.offset % to !== 0) {
d.offset++;
@ -59,7 +47,7 @@ export function align(d, to) { @@ -59,7 +47,7 @@ export function align(d, to) {
export function event(d) {
const event = {};
event.type = u32(d);
event.type = u8(d);
switch (event.type) {
case EVENT.PREDRAW: {
@ -68,31 +56,6 @@ export function event(d) { @@ -68,31 +56,6 @@ export function event(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;
@ -108,37 +71,20 @@ export function event(d) { @@ -108,37 +71,20 @@ export function event(d) {
const point_count = u16(d);
const width = u16(d);
const color = u32(d);
align(d, 4);
event.width = width;
event.color = color;
event.points = f32array(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;
}
case EVENT.IMAGE:
case EVENT.IMAGE_MOVE: {
event.image_id = u32(d);
event.x = f32(d);
event.y = f32(d);
break;
}
case EVENT.IMAGE_SCALE: {
event.image_id = u32(d);
event.corner = u32(d);
event.x = f32(d);
event.y = f32(d);
break;
}
case EVENT.UNDO:
case EVENT.REDO: {
@ -158,4 +104,4 @@ export function event(d) { @@ -158,4 +104,4 @@ export function event(d) {
}
return event;
}
}

13
server/enums.js

@ -8,21 +8,11 @@ export const EVENT = Object.freeze({ @@ -8,21 +8,11 @@ export const EVENT = Object.freeze({
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,
UNDO: 30,
REDO: 31,
IMAGE: 40,
IMAGE_MOVE: 41,
IMAGE_SCALE: 42,
ERASER: 50,
});
@ -33,5 +23,4 @@ export const MESSAGE = Object.freeze({ @@ -33,5 +23,4 @@ export const MESSAGE = Object.freeze({
FULL: 103,
FIRE: 104,
JOIN: 105,
FOLLOW: 106,
});
});

4
server/http.js

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

6
server/math.js

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

85
server/milton.js

@ -1,85 +0,0 @@ @@ -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();

57
server/recv.js

@ -13,7 +13,6 @@ function recv_ack(d, session) { @@ -13,7 +13,6 @@ function recv_ack(d, session) {
session.state = SESSION.READY;
session.sn = sn;
session.sync_attempts = 0;
if (config.DEBUG_PRINT) console.log(`ack ${sn} in`);
}
@ -39,7 +38,7 @@ async function recv_syn(d, session) { @@ -39,7 +38,7 @@ async function recv_syn(d, session) {
events.push(event);
}
}
desks[session.desk_id].sn += we_expect;
desks[session.desk_id].events.push(...events);
session.lsn = lsn;
@ -53,7 +52,7 @@ async function recv_syn(d, session) { @@ -53,7 +52,7 @@ async function recv_syn(d, session) {
'$id': session.id,
'$lsn': lsn
});
send.send_ack(session.ws, lsn);
send.sync_desk(session.desk_id);
}
@ -87,57 +86,53 @@ function recv_fire(d, session) { @@ -87,57 +86,53 @@ function recv_fire(d, session) {
}
}
send.fire_event(session, event);
}
for (const sid in sessions) {
const other = sessions[sid];
if (other.id === session.id) {
continue;
}
function recv_follow(d, session) {
const user_id = des.u32(d);
if (other.state !== SESSION.READY) {
continue;
}
if (config.DEBUG_PRINT) console.log(`follow ${user_id} in`);
if (other.desk_id != session.desk_id) {
continue;
}
if (user_id === 4294967295) {
// unfollow
session.follow = null;
} else {
// follow
session.follow = user_id;
send.send_fire(other.ws, event);
}
}
function handle_event(session, event) {
switch (event.type) {
case EVENT.STROKE: {
const stroke_result = storage.queries.insert_stroke.get({
event.stroke_id = math.fast_random32();
storage.queries.insert_stroke.run({
'$id': event.stroke_id,
'$width': event.width,
'$color': event.color,
'$points': event.points,
'$pressures': event.pressures,
'$points': event.points
});
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:
case EVENT.IMAGE:
case EVENT.IMAGE_MOVE:
case EVENT.IMAGE_SCALE:
case EVENT.UNDO: {
storage.queries.insert_event.run({
'$type': event.type,
@ -145,11 +140,8 @@ function handle_event(session, event) { @@ -145,11 +140,8 @@ function handle_event(session, event) {
'$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;
@ -163,14 +155,14 @@ function handle_event(session, event) { @@ -163,14 +155,14 @@ function handle_event(session, event) {
}
}
export function handle_message(ws, d) {
export async function handle_message(ws, d) {
if (!(ws.data.session_id in sessions)) {
return;
}
const session = sessions[ws.data.session_id];
const desk_id = session.desk_id;
const message_type = des.u32(d);
const message_type = des.u8(d);
switch (message_type) {
case MESSAGE.FIRE: {
@ -188,11 +180,6 @@ export function handle_message(ws, d) { @@ -188,11 +180,6 @@ export function handle_message(ws, d) {
break;
}
case MESSAGE.FOLLOW: {
recv_follow(d, session);
break;
}
default: {
console.error('fuck');
console.trace();

105
server/send.js

@ -7,31 +7,13 @@ import { MESSAGE, SESSION, EVENT } from './enums'; @@ -7,31 +7,13 @@ import { MESSAGE, SESSION, EVENT } from './enums';
import { sessions, desks } from './storage';
function event_size(event) {
let size = 4 + 4; // type + user_id
let size = 1 + 4; // type + user_id
switch (event.type) {
case EVENT.PREDRAW:
case EVENT.MOVE_CURSOR: {
case EVENT.PREDRAW: {
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;
@ -46,25 +28,15 @@ function event_size(event) { @@ -46,25 +28,15 @@ function event_size(event) {
case EVENT.STROKE: {
size += 4 + 2 + 2 + 4; // stroke id + point count + width + color
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;
}
case EVENT.IMAGE:
case EVENT.IMAGE_MOVE: {
size += 4 + 4 + 4; // file id + x + y
break;
}
case EVENT.IMAGE_SCALE: {
size += 4 + 4 + 4 + 4; // file_id + corner + x + y
break;
}
case EVENT.UNDO:
case EVENT.REDO: {
break;
@ -76,7 +48,6 @@ function event_size(event) { @@ -76,7 +48,6 @@ function event_size(event) {
}
default: {
console.error(event.desk_id);
console.error('fuck');
console.trace();
process.exit(1);
@ -108,7 +79,7 @@ function create_session(ws, desk_id) { @@ -108,7 +79,7 @@ function create_session(ws, desk_id) {
return session;
}
export function send_init(ws) {
export async function send_init(ws) {
if (!ws) {
return;
}
@ -118,7 +89,7 @@ export function send_init(ws) { @@ -118,7 +89,7 @@ export function send_init(ws) {
const desk = desks[desk_id];
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 + user count
let session = null;
if (session_id in sessions && sessions[session_id].desk_id == desk_id) {
@ -144,7 +115,7 @@ export function send_init(ws) { @@ -144,7 +115,7 @@ export function send_init(ws) {
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
size += 4 + 4 + 2; // user id + color + width
}
}
@ -154,7 +125,7 @@ export function send_init(ws) { @@ -154,7 +125,7 @@ export function send_init(ws) {
const s = ser.create(size);
ser.u32(s, opcode);
ser.u8(s, opcode);
ser.u32(s, session.lsn);
if (opcode === MESSAGE.JOIN) {
@ -166,7 +137,6 @@ export function send_init(ws) { @@ -166,7 +137,6 @@ export function send_init(ws) {
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];
@ -175,17 +145,14 @@ export function send_init(ws) { @@ -175,17 +145,14 @@ export function send_init(ws) {
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) {
ser.event(s, event);
}
ws.send(s.buffer);
await ws.send(s.buffer);
}
export function send_ack(ws, lsn) {
@ -193,10 +160,10 @@ export function send_ack(ws, lsn) { @@ -193,10 +160,10 @@ export function send_ack(ws, lsn) {
return;
}
const size = 4 + 4; // opcode + lsn
const size = 1 + 4; // opcode + lsn
const s = ser.create(size);
ser.u32(s, MESSAGE.ACK);
ser.u8(s, MESSAGE.ACK);
ser.u32(s, lsn);
if (config.DEBUG_PRINT) console.log(`ack ${lsn} out`);
@ -204,45 +171,20 @@ export function send_ack(ws, lsn) { @@ -204,45 +171,20 @@ export function send_ack(ws, lsn) {
ws.send(s.buffer);
}
function send_fire(ws, event) {
export function send_fire(ws, event) {
if (!ws) {
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.event(s, event);
ws.send(s.buffer);
}
export function fire_event(from_session, event) {
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) {
async function sync_session(session_id) {
if (!(session_id in sessions)) {
return;
}
@ -258,8 +200,8 @@ function sync_session(session_id) { @@ -258,8 +200,8 @@ function sync_session(session_id) {
return;
}
let size = 4 + 4 + 4; // opcode + sn + event count
let count = desk.sn - session.sn;
let size = 1 + 4 + 4; // opcode + sn + event count
let count = desk.sn - session.sn;
if (count === 0) {
if (config.DEBUG_PRINT) console.log('client ACKed all events');
@ -273,7 +215,7 @@ function sync_session(session_id) { @@ -273,7 +215,7 @@ function sync_session(session_id) {
const s = ser.create(size);
ser.u32(s, MESSAGE.SYN);
ser.u8(s, MESSAGE.SYN);
ser.u32(s, desk.sn);
ser.u32(s, count);
@ -281,15 +223,12 @@ function sync_session(session_id) { @@ -281,15 +223,12 @@ function sync_session(session_id) {
const event = desk.events[desk.events.length - 1 - i];
ser.event(s, event);
}
if (config.DEBUG_PRINT) console.log(`syn ${desk.sn} out`);
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);
}
await session.ws.send(s.buffer);
session.sync_timer = setTimeout(() => sync_session(session_id), config.SYNC_TIMEOUT);
}
export function sync_desk(desk_id) {
@ -302,4 +241,4 @@ export function sync_desk(desk_id) { @@ -302,4 +241,4 @@ export function sync_desk(desk_id) {
sync_session(sid);
}
}
}
}

62
server/serializer.js

@ -31,25 +31,13 @@ export function u32(s, value) { @@ -31,25 +31,13 @@ export function u32(s, value) {
s.offset += 4;
}
export function s32(s, value) {
s.view.setInt32(s.offset, value, true);
s.offset += 4;
}
export function bytes(s, bytes) {
s.strview.set(new Uint8Array(bytes.buffer, bytes.byteOffset, bytes.byteLength), s.offset);
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) {
u32(s, event.type); // for alignment reasons
u8(s, event.type);
u32(s, event.user_id);
switch (event.type) {
@ -59,33 +47,6 @@ export function event(s, event) { @@ -59,33 +47,6 @@ export function event(s, event) {
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;
@ -98,26 +59,15 @@ export function event(s, event) { @@ -98,26 +59,15 @@ export function event(s, event) {
case EVENT.STROKE: {
const points_bytes = event.points;
const pressures_bytes = event.pressures;
u32(s, event.stroke_id);
u16(s, points_bytes.byteLength / 2 / 4); // each point is 2 * f32
u16(s, event.width);
u32(s, event.color);
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;
}
case EVENT.IMAGE:
case EVENT.IMAGE_MOVE: {
u32(s, event.image_id);
f32(s, event.x);
@ -125,14 +75,6 @@ export function event(s, event) { @@ -125,14 +75,6 @@ export function event(s, event) {
break;
}
case EVENT.IMAGE_SCALE: {
u32(s, event.image_id);
u32(s, event.corner);
f32(s, event.x);
f32(s, event.y);
break;
}
case EVENT.UNDO:
case EVENT.REDO: {
break;

8
server/server.js

@ -51,20 +51,16 @@ export function startup() { @@ -51,20 +51,16 @@ export function startup() {
websocket: {
open(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) {
const dataview = new DataView(u8array.buffer);
const d = des.create(dataview);
recv.handle_message(ws, d);
await recv.handle_message(ws, d);
},
close(ws, code, message) {
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`);
sessions[ws.data.session_id].state = SESSION.CLOSED;
sessions[ws.data.session_id].ws = null;
@ -78,4 +74,4 @@ export function startup() { @@ -78,4 +74,4 @@ export function startup() {
});
console.log(`Running on ${config.HOST}:${config.PORT}`)
}
}

40
server/storage.js

@ -8,7 +8,7 @@ export const sessions = {}; @@ -8,7 +8,7 @@ export const sessions = {};
export const desks = {};
export const queries = {};
export let db = null;
let db = null;
export function startup() {
const path = `${config.DATADIR}/db.sqlite`;
@ -38,8 +38,7 @@ export function startup() { @@ -38,8 +38,7 @@ export function startup() {
id INTEGER PRIMARY KEY,
width INTEGER,
color INTEGER,
points BLOB,
pressures BLOB
points BLOB
);`).run();
db.query(`CREATE TABLE IF NOT EXISTS events (
@ -49,11 +48,8 @@ export function startup() { @@ -49,11 +48,8 @@ export function startup() {
session_id INTEGER,
stroke_id INTEGER,
image_id INTEGER,
corner INTEGER,
x INTEGER,
y INTEGER,
width INTEGER,
height INTEGER,
FOREIGN KEY (desk_id)
REFERENCES desks (id)
@ -72,10 +68,10 @@ export function startup() { @@ -72,10 +68,10 @@ export function startup() {
);`).run();
// INSERT
queries.insert_desk = db.query('INSERT INTO desks (id, title, sn) VALUES ($id, $title, 0) RETURNING id');
queries.insert_stroke = db.query('INSERT INTO strokes (width, color, points, pressures) VALUES ($width, $color, $points, $pressures) RETURNING id');
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');
queries.insert_desk = db.query('INSERT INTO desks (id, title, sn) VALUES ($id, $title, 0)');
queries.insert_stroke = db.query('INSERT INTO strokes (id, width, color, points) VALUES ($id, $width, $color, $points)');
queries.insert_session = db.query('INSERT INTO sessions (id, desk_id, lsn) VALUES ($id, $desk_id, 0)');
queries.insert_event = db.query('INSERT INTO events (type, desk_id, session_id, stroke_id, image_id, x, y) VALUES ($type, $desk_id, $session_id, $stroke_id, $image_id, $x, $y)');
// UPDATE
queries.update_desk_sn = db.query('UPDATE desks SET sn = $sn WHERE id = $id');
@ -104,40 +100,30 @@ export function startup() { @@ -104,40 +100,30 @@ export function startup() {
const stroke_dict = {};
for (const desk of stored_desks) {
desks[desk.id] = desk;
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 desk of stored_desks) {
desks[desk.id] = desk;
desks[desk.id].events = [];
}
for (const event of stored_events) {
if (event.type === EVENT.STROKE) {
const stroke = stroke_dict[event.stroke_id];
event.points = stroke.points;
event.pressures = stroke.pressures;
event.color = stroke.color;
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) {
desk.sn = desk.events.length;
}
for (const session of stored_sessions) {
session.state = SESSION.CLOSED;
session.ws = null;
session.sync_attempts = 0;
session.follow = null;
sessions[session.id] = session;
}
}
}

21
server/texput.log

@ -0,0 +1,21 @@ @@ -0,0 +1,21 @@
This is pdfTeX, Version 3.141592653-2.6-1.40.24 (TeX Live 2022/Debian) (preloaded format=pdflatex 2023.4.13) 16 APR 2023 21:20
entering extended mode
restricted \write18 enabled.
%&-line parsing enabled.
**
! Emergency stop.
<*>
End of file on the terminal!
Here is how much of TeX's memory you used:
3 strings out of 476091
111 string characters out of 5794081
1849330 words of memory out of 5000000
20488 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
0i,0n,0p,1b,6s stack positions out of 10000i,1000n,20000p,200000b,200000s
! ==> Fatal error occurred, no output PDF file produced!
Loading…
Cancel
Save