Compare commits

..

147 Commits

Author SHA1 Message Date
Aleksey Olokhtonov d6f2fc5548 Remove subframe logic, instead use ffmpeg to blur frames afterwards 2 weeks ago
Aleksey Olokhtonov 0384ac6246 Render button! 2 weeks ago
Aleksey Olokhtonov db277039df Make vertial zooming faster and also framerate-independent. 2 weeks ago
Aleksey Olokhtonov 268a55b71b Use framerate-independent lerp. Very Freya-esque in that sense. 2 weeks ago
Aleksey Olokhtonov 26ec2216ce Unstuck from picker mode on mousemove with no ctrl. 2 weeks ago
Aleksey Olokhtonov dc1553dc1e Increase WASM mem limit to 256MB. Rename aux.js to not get adblocked. 2 weeks ago
Aleksey Olokhtonov cf4b1880d2 Actually perfect smoothing. Amazing, good, Demetri.y 3 months ago
Aleksey Olokhtonov 61f93e0ef0 Add option to use "demetri smoothing". Small fixes here and there 3 months ago
A.Olokhtonov 967c7554dd Use e.pressure from touch screen 1 year ago
A.Olokhtonov 86193fe26d Reset player.current_prestroke if we deleted last prestroke of theirs 1 year ago
A.Olokhtonov a57b814982 Panning and zooming with touch also works (more or less) 1 year ago
A.Olokhtonov 7f9d04d620 Drawing with one finger kinda works again 1 year ago
A.Olokhtonov a32088991a Update readme 1 year ago
A.Olokhtonov 2a94d4625c Temprorarily undo busted partial texture uploads 1 year ago
A.Olokhtonov 666d0393d8 Resurrect image handles 1 year ago
A.Olokhtonov 8ffc5b99b5 Fix z range which sometimes caused the last stroke to have Z equal to 1 year ago
A.Olokhtonov da755e4ec2 Fix first stroke not drawing because it has Z=0 1 year ago
A.Olokhtonov b961504b54 Separate stroke rendering into function. Reuse said function for static 1 year ago
A.Olokhtonov 5bb8b0a7ee Further tune LOD levels to optimize triangle counts. Removed LOD level 7 1 year ago
A.Olokhtonov d618913111 Fix uninitialized return when no strokes are submitted to a thread 1 year ago
A.Olokhtonov 500fd71d3e Batching is working! (only in singethreaded mode though) 1 year ago
A.Olokhtonov 777772530f Batching is close to working. Not quite though 1 year ago
A.Olokhtonov ce824a8e31 Pass stroke widths to wasm (not actually writing any values right now, 1 year ago
A.Olokhtonov 55f390e55b Support different LOD level per batch of instances. Mostly OpenGL 1 year ago
A.Olokhtonov 8a4e87f4c6 Batching of the main instanced drawcall 1 year ago
A.Olokhtonov d2adce838b A simple (and bad) LOD determination scheme 1 year ago
A.Olokhtonov 07bdbb585b Generate the circles using indexed geometry 1 year ago
Aleksey Olokhtonov dc824c12c9 Reuse a single circle geometry. One circle per segment. Still needs last circle 1 year ago
A.Olokhtonov 78ba044527 Use straight up geometry for everything, finally obtaining TRANSPARENCY! 1 year ago
A.Olokhtonov dc235c7365 Implement partial uploads to texture 1 year ago
A.Olokhtonov 0c3259d00f Tweak initial BVH construction to split along the longest axis, instead 1 year ago
Aleksey Olokhtonov a2f574214c Fix warning when dynamic draw has only a single point. Fragment shader fix is untested. 1 year ago
Aleksey Olokhtonov 0c21579694 Boudning boxes debug draw. Fix missing mipmap warning. Fix dynamic stroke not drawing on empty canvas 1 year ago
A.Olokhtonov 4e09c47190 Sudden shader refactoring (automatically extract locations a-la 1 year ago
A.Olokhtonov fea2874004 Merge scale and move local history, fixing undo for image move/scale 1 year ago
A.Olokhtonov 8bea9593da The big unAWAITing! Also found the image scale undo bug, but haven't 1 year ago
A.Olokhtonov 29ec265632 Keep multiple current strokes ("prestrokes") per player in a queue 1 year ago
A.Olokhtonov bdc3bdc9a1 Undo for all available actions 1 year ago
A.Olokhtonov 5d45eb3255 Undo eraser 1 year ago
A.Olokhtonov 49620ff8fb Fix image selection order, fix image upload typo. Bind hotkeys to switch between tools 1 year ago
A.Olokhtonov ea409f76c4 Basic snapping to grid is kinda working (needs more testing). Deleted obsolete shaders and debug options 2 years ago
A.Olokhtonov 8b4b4e09f7 Basic line drawing is back IN 2 years ago
A.Olokhtonov b11c46354f Add line-line intersection test to the eraser to kinda make it work for faster movement 2 years ago
A.Olokhtonov ab1659f77f Fix missing dummy stroke for undo event. Fix typo in listener 2 years ago
A.Olokhtonov 803b7f80cb Eraser has gone on-line! 2 years ago
A.Olokhtonov 0ffac004c0 Eraser works again! Offline though 2 years ago
A.Olokhtonov 21aecb7d08 Make image move and image scale work in multiplayer. Add width and height to image event and fix late-arriving bitmaps breaking things 2 years ago
A.Olokhtonov 45bac3395a Clean up image move/scale logic a little bit. Remove obsolete cursor.js file 2 years ago
A.Olokhtonov 9d6f333778 Resize images (local only) 2 years ago
A.Olokhtonov 254268c309 Move images (local only) 2 years ago
A.Olokhtonov c1583cb8fb Resize handles for images 2 years ago
A.Olokhtonov 66e84c1a50 More betterer active image highlight 2 years ago
A.Olokhtonov acb9aaa83d Fix misallocation of pressures tv_array 2 years ago
A.Olokhtonov 4a6d1d6640 Fix dummy offsets not getting inserted if first event is not a stroke 2 years ago
A.Olokhtonov c6f8bc4b5a Fix first stroke preview not showing 2 years ago
A.Olokhtonov 5871833cd1 (Probably) fix the nasty WASM mis-allocations 2 years ago
A.Olokhtonov 8a15093147 The images are in 2 years ago
A.Olokhtonov 903660adbc Marginally better grid level transitions 2 years ago
Aleksey Olokhtonov 5aa29b18c0 Fix follow mode by separating canvas move and zoom events. Touch devices are TODO! 2 years ago
Aleksey Olokhtonov 01560389af Fix predrawn curves having zero width 2 years ago
Aleksey Olokhtonov 90f0beb4f9 Undo is back in! 2 years ago
Aleksey Olokhtonov e2ba3bb1c2 Make zoom animation use delta time 2 years ago
A.Olokhtonov bf2eace6fe Grid background pattern 2 years ago
A.Olokhtonov eafe860990 Zoom without the mousewheel by holding down Z 2 years ago
A.Olokhtonov eb66ffbcad Significantly improve stroke smoothing and pressure handling. "Fix" issue with underallocation of WASM memory by doubling the size 2 years ago
A.Olokhtonov 30559b0381 The dots are now zoomable 2 years ago
A.Olokhtonov c13e5a6848 Smooth zooming 2 years ago
A.Olokhtonov ee42e400c4 Dots pattern with fancy fadeout 2 years ago
A.Olokhtonov 807a1d0c03 More betterer picker icon. Hide dom brush when in picker mode 2 years ago
A.Olokhtonov 07bb47b6dc Dynamic cursor that shows size and color of the brush. Background pattern scaffolding 2 years ago
A.Olokhtonov 5552dc11a3 Make cursor reflect the size of the brush. Doesn't work when the brush size is > max allowed cursor size though 2 years ago
A.Olokhtonov e4a10ebf30 Make color picker actually change the active color 2 years ago
A.Olokhtonov 68c892ba5f Color picker: picks colors! They don't get used though 2 years ago
A.Olokhtonov 48e9dd9a3e Remove stupid try catch from webgl_draw 2 years ago
A.Olokhtonov 221e2a5aed Fix bvh fullnodes returning full buffer instead of tv_data. Skip handling of empty strokes 2 years ago
A.Olokhtonov c3f66d966c Determinitstic zooming 2 years ago
A.Olokhtonov acdbf73a26 Add new strokes to fullnodes. Strokes appear one too late though 2 years ago
A.Olokhtonov 6d9791d3e0 Cache full stroke lists in BVH levels (massive speedup!). Remove "line_threshold" array, since it has very little effect. 2 years ago
A.Olokhtonov f6573e7bb9 Correcty complute total point count. Only call glClear once workers have finished LOD. Only allow next draw() call after we finished rendering frame 2 years ago
A.Olokhtonov c5ad40f739 Update README 2 years ago
A.Olokhtonov 4f84509b16 Multithreading works! Kinda sorta (slows down in chrome, out of bounds accesses on phone) 2 years ago
A.Olokhtonov 2c01d315d6 A small test WASM module to demonstrate how to use shared memory and run multiple threads in WASM without Emscripten 2 years ago
A.Olokhtonov e949860279 Change extracts of pressure byutes to a f32x4_make. Change andnot masking to a bitselect. Rename readme.md to readme.txt to hopefully fix gitea formatting 2 years ago
A.Olokhtonov 37ff1e55bc Oops, actually enable SIMD 2 years ago
A.Olokhtonov 84a5859541 SIMD version of rdp_find_max. 16->12ms cpu frametime firefox, 16->9ms chrome 2 years ago
A.Olokhtonov 6f78c0ae21 AoS -> SoA for point coordinates 2 years ago
A.Olokhtonov 58f2a60295 Fix wrong wasm allocation, remove unused js LOD code, take radius of stroke into account when doing LOD, reduce EPS for LOD 2 years ago
A.Olokhtonov 40dd278f21 Improved shader (thanks BYP!) 2 years ago
A.Olokhtonov 6f19e6c954 Uneven capsules (and a very naive shader) to draw getter variable-width lines 2 years ago
aolo2 dfecad2c99 Fix strokes being added to storage even when skipped by SN logic 2 years ago
aolo2 704c833c16 First working draft of pressure hanlding 2 years ago
aolo2 71f1016a40 Store WASM-processed data in WASM memory 2 years ago
A.Olokhtonov 8abe88373f Skip copying out segments and segments_from. Use wasm_points and wasm_ids as is 2 years ago
A.Olokhtonov 3bf812df39 Move point writes to WASM, already a lot faster!!!! 2 years ago
A.Olokhtonov c9d1706b6e Fix final (???) WASM issue 2 years ago
A.Olokhtonov 1960bdebe9 LOD generation has been wassembled! (a little borked for now though) 2 years ago
A.Olokhtonov cf11f6c701 Successfull sum function in wasm :D 2 years ago
A.Olokhtonov 28fec7f306 Redraw HTML on canvas move, first draft of wasm LOD core 2 years ago
A.Olokhtonov 4b5b8db053 Update README 2 years ago
A.Olokhtonov ab7545918b Fix state.me not being set when joining new desk 2 years ago
A.Olokhtonov 7555de10bf Long touch to open color picker details 2 years ago
A.Olokhtonov 08ba8a7687 Fix GC pauses! 2 years ago
A.Olokhtonov cc800c1ffe Fancy-shmancy Open Color (TM) color pallete 2 years ago
A.Olokhtonov 8bbcfb616f JOIN event, send online info for users in init message 2 years ago
A.Olokhtonov 99bd99a465 Follow and unfollow players like in Figma 2 years ago
A.Olokhtonov ee1d1471f4 Pretty(er) player cursor: bright colors and an actual icon 2 years ago
A.Olokhtonov d8a5cd3fca Draw player pointers as colored squares with html 2 years ago
A.Olokhtonov 1f983f3389 Fix multiplayer, add mouse wheel panning 2 years ago
A.Olokhtonov c893a73ec5 You can draw once again! 2 years ago
A.Olokhtonov 847fb70381 Don't repeat points segment points. Introduce "tv" (typedvector) 2 years ago
A.Olokhtonov 9bbab32369 Try to speed up the LOD calculation with a cache (kinda succesfull, but not really) 2 years ago
A.Olokhtonov a60c3d1948 Recompute LODs and instance data on demand - ??? - it works??? 2 years ago
A.Olokhtonov 2d88b57dca Fix off-by-factor-of-2 in the vertex shader 2 years ago
A.Olokhtonov 248501e50f Remove recursive allocations from process_rdp 2 years ago
A.Olokhtonov 31a0b0113a Prototype: separate per-quad data using instanced rendeding and per-stroke data using a texture 2 years ago
A.Olokhtonov 1438b0ad73 LODs work! Need to fix that memory usage though 2 years ago
A.Olokhtonov 60fc48b1e7 Fix dynamic draw to use indexed, and thus the same shader as main 2 years ago
A.Olokhtonov fdca3e6b07 Fix epsilon calculation for RDP 2 years ago
A.Olokhtonov 7b53c7215e Fix predraw artifacts by making the lines too fat at far zoomout 2 years ago
A.Olokhtonov 72eedf9b48 Fix dynamic data 2 years ago
A.Olokhtonov 24222f9565 Fix wrong sort order for indices 2 years ago
A.Olokhtonov cca3b4cd5d Fix the depth prepass artefacts 2 years ago
A.Olokhtonov a991cf3247 BVH construction and modification 2 years ago
aolo2 1691bd07ae Milton blackboard import 2 years ago
A.Olokhtonov 6be8ba7823 Add readme 2 years ago
A.Olokhtonov 7752e41bf5 Separate CPU and GPU timings in debug window 2 years ago
A.Olokhtonov 1bc6f2c3fe No clipping at zoom < 0.3. Really fast 2 years ago
A.Olokhtonov 316d0fedc9 Use correct rendering order.. 2 years ago
A.Olokhtonov e86ffd6508 Depth prepass with simplified opaque shapes 2 years ago
A.Olokhtonov 7e3b6156c0 Render stroke_id to a small texture 2 years ago
A.Olokhtonov 0d8667aa3a Use TypedArray for culling. This is up to 10 times faster! 2 years ago
A.Olokhtonov e41997563f Faster clipping 2 years ago
A.Olokhtonov 46cd48fecd Cull segments, switch to indexed drawing (only for static data for now) 2 years ago
A.Olokhtonov be50af68d4 Delete garbage file 2 years ago
A.Olokhtonov 72deea0b03 Added LICENSE (unlicense) 2 years ago
A.Olokhtonov 9a8854dc90 Handle touchstart with 2 e.changedTouches. This fixes panning sometimes not working on iPhone 2 years ago
A.Olokhtonov 1803dedee2 Black-on-white cursor for ultimate readability 2 years ago
A.Olokhtonov 14faef4146 Better draw->move cancellation. Instead of not registering short strokes, cancel them for all users if we registered a second finger during the time window. 2 years ago
A.Olokhtonov c5928dd5bd Do not upload whole static buffer, instead use glBufferSubData. This commit fixes lag (and skipped iinputs) when other people are drawing 2 years ago
A.Olokhtonov eefbb8ef75 Do not upload buffers each frame (DUH!), re-introduce dynamic data, set headers in caddy to enable precise timings 2 years ago
A.Olokhtonov 605500028a Bump version 2 years ago
A.Olokhtonov c1fd46da53 We have achieved good AA! 2 years ago
A.Olokhtonov 01db70cab0 Good speed (quad per line segment). Need to figure out rounding 2 years ago
A.Olokhtonov e3ae4cf6cd Upload whole point buffer, and align its size to 8K 2 years ago
A.Olokhtonov 46587068e6 Limit max points per quad, fix wrong rounding of texture size 2 years ago
A.Olokhtonov f72cbf5941 SDF bull fixed boxes + passed stroke width to shader 2 years ago
A.Olokhtonov 69feb482a2 sdf bullshit (almost working) 2 years ago
  1. 6
      .gitignore
  2. 8
      Caddyfile
  3. 24
      LICENSE
  4. 116
      README.txt
  5. 100
      client/aux.js
  6. 427
      client/bvh.js
  7. 379
      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/pen.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. 241
      client/index.html
  22. 188
      client/index.js
  23. 36
      client/index.log
  24. 49
      client/lod_worker.js
  25. 648
      client/math.js
  26. 9
      client/offline.html
  27. 264
      client/random_helpers.js
  28. 278
      client/speed.js
  29. 136
      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. 682
      client/webgl_draw.js
  37. 905
      client/webgl_geometry.js
  38. 923
      client/webgl_listeners.js
  39. 413
      client/webgl_shaders.js
  40. 6
      client/websocket.js
  41. 1
      server/config.js
  42. BIN
      server/data-local.sqlite
  43. 60
      server/deserializer.js
  44. 11
      server/enums.js
  45. 2
      server/http.js
  46. 4
      server/math.js
  47. 85
      server/milton.js
  48. 53
      server/recv.js
  49. 95
      server/send.js
  50. 62
      server/serializer.js
  51. 6
      server/server.js
  52. 36
      server/storage.js
  53. 21
      server/texput.log

6
.gitignore vendored

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

8
Caddyfile

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

24
LICENSE

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

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

@ -1,100 +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);
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

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

379
client/client_recv.js

@ -26,28 +26,41 @@ function des_u32(d) {
return value; return value;
} }
function des_s32(d) {
const value = d.view.getInt32(d.offset, true);
d.offset += 4;
return value;
}
function des_f32(d) { function des_f32(d) {
const value = d.view.getFloat32(d.offset, true); const value = d.view.getFloat32(d.offset, true);
d.offset += 4; d.offset += 4;
return value; return value;
} }
function des_f32array(d, count) { function des_align(d, to) {
const result = []; // TODO: non-stupid version of this
while (d.offset % to != 0) {
for (let i = 0; i < count; ++i) { d.offset++;
const item = d.view.getFloat32(d.offset, true);
d.offset += 4;
result.push(item);
} }
}
function des_f32array(d, count) {
const result = new Float32Array(d.buffer, d.offset, count);
d.offset += 4 * count;
return result; return result;
} }
function des_event(d) { function des_u8array(d, count) {
const result = new Uint8Array(d.buffer, d.offset, count);
d.offset += count;
return result;
}
function des_event(d, state = null) {
const event = {}; const event = {};
event.type = des_u8(d); event.type = des_u32(d);
event.user_id = des_u32(d); event.user_id = des_u32(d);
switch (event.type) { switch (event.type) {
@ -57,6 +70,33 @@ function des_event(d) {
break; 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: { case EVENT.SET_COLOR: {
event.color = des_u32(d); event.color = des_u32(d);
break; break;
@ -72,16 +112,15 @@ function des_event(d) {
const point_count = des_u16(d); const point_count = des_u16(d);
const width = des_u16(d); const width = des_u16(d);
const color = des_u32(d); const color = des_u32(d);
const coords = des_f32array(d, point_count * 2);
event.stroke_id = stroke_id; event.coords = des_f32array(d, point_count * 2);
event.points = []; event.press = des_u8array(d, point_count);
for (let i = 0; i < point_count; ++i) { des_align(d, 4);
const x = coords[2 * i + 0];
const y = coords[2 * i + 1]; // TODO: remove, this is duplicate data
event.points.push({'x': x, 'y': y});
} event.stroke_id = stroke_id;
event.color = color; event.color = color;
event.width = width; event.width = width;
@ -89,7 +128,15 @@ function des_event(d) {
break; break;
} }
case EVENT.IMAGE: case EVENT.IMAGE: {
event.image_id = des_u32(d);
event.x = des_f32(d);
event.y = des_f32(d);
event.width = des_u32(d);
event.height = des_u32(d);
break;
}
case EVENT.IMAGE_MOVE: { case EVENT.IMAGE_MOVE: {
event.image_id = des_u32(d); event.image_id = des_u32(d);
event.x = des_f32(d); event.x = des_f32(d);
@ -97,6 +144,14 @@ function des_event(d) {
break; 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.UNDO:
case EVENT.REDO: { case EVENT.REDO: {
break; break;
@ -120,7 +175,7 @@ function bitmap_bbox(event) {
'xmin': event.x, 'xmin': event.x,
'xmax': event.x + event.bitmap.width, 'xmax': event.x + event.bitmap.width,
'ymin': event.y, 'ymin': event.y,
'ymax': event.y + event.bitmap.height 'ymax': event.y + event.bitmap.height,
}; };
return bbox; return bbox;
@ -131,10 +186,14 @@ function init_player_defaults(state, player_id, color = config.default_color, wi
'color': color, 'color': color,
'width': width, 'width': width,
'points': [], 'points': [],
'online': false,
'cursor': {'x': 0, 'y': 0},
'strokes': [],
'current_prestroke': false,
}; };
} }
function handle_event(state, context, event) { function handle_event(state, context, event, options = {}) {
if (config.debug_print) console.debug(`event type ${event.type} from user ${event.user_id}`); if (config.debug_print) console.debug(`event type ${event.type} from user ${event.user_id}`);
let need_draw = false; let need_draw = false;
@ -144,9 +203,93 @@ function handle_event(state, context, event) {
} }
switch (event.type) { switch (event.type) {
case EVENT.USER_JOINED: {
state.players[event.user_id].online = true;
draw_html(state);
break;
}
case EVENT.PREDRAW: { case EVENT.PREDRAW: {
geometry_add_point(state, context, event.user_id, {'x': event.x, 'y': event.y}); const player = state.players[event.user_id];
if (!player.current_prestroke) {
geometry_start_prestroke(state, event.user_id);
}
geometry_add_prepoint(state, context, event.user_id, {'x': event.x, 'y': event.y, 'pressure': 128}, false); // TODO: add pressure to predraw events
need_draw = true;
break;
}
case EVENT.CLEAR: {
// 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; need_draw = true;
}
break; break;
} }
@ -161,72 +304,92 @@ function handle_event(state, context, event) {
} }
case EVENT.STROKE: { case EVENT.STROKE: {
if (event.user_id != state.me) { const point_count = event.coords.length / 2;
geometry_clear_player(state, context, event.user_id);
need_draw = true; if (point_count === 0) {
break;
} }
geometry_add_stroke(state, context, event); 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; break;
} }
}
const index_difference = state.events.length - (last_stroke === null ? -1 : last_stroke.index);
wasm_ensure_by(state, index_difference, event.coords.length);
const pressures = state.wasm.buffers['pressures'];
const xs = state.wasm.buffers['xs'];
const ys = state.wasm.buffers['ys'];
event.coords_from = xs.tv.size;
event.coords_to = xs.tv.size + point_count;
for (let i = 0; i < index_difference - 1; ++i) {
// Create empty records for all non-stroke events that happened since the last stroke
tv_add(state.wasm.buffers['coords_from'].tv, xs.tv.size);
state.wasm.buffers['coords_from'].used += 4; // 4 bytes, not 4 ints
}
// Create actual records for this stroke
tv_add(state.wasm.buffers['coords_from'].tv, xs.tv.size + point_count);
state.wasm.buffers['coords_from'].used += 4; // 4 bytes, not 4 ints
for (let i = 0; i < event.coords.length; i += 2) {
tv_add(xs.tv, event.coords[i + 0]);
tv_add(ys.tv, event.coords[i + 1]);
}
state.wasm.buffers['xs'].used += point_count * 4;
state.wasm.buffers['ys'].used += point_count * 4;
tv_append(pressures.tv, event.press);
state.wasm.buffers['pressures'].used += point_count;
delete event.coords;
delete event.press;
case EVENT.UNDO: {
need_draw = true; need_draw = true;
console.error('todo');
// for (let i = state.events.length - 1; i >=0; --i) { event.index = state.events.length;
// const other_event = state.events[i];
geometry_clear_oldest_prestroke(state, context, event.user_id);
// // Users can only undo their own, undeleted (not already undone) events geometry_add_stroke(state, context, event, state.events.length, options.skip_bvh === true);
// if (other_event.user_id === event.user_id && !other_event.deleted) {
// if (other_event.type === EVENT.STROKE) { state.stroke_count++;
// 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; break;
} }
case EVENT.UNDO: {
geometry_add_dummy_stroke(state, context);
need_draw = undo(state, context, event, options);
break;
}
case EVENT.IMAGE: { 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 { try {
(async () => { (async () => {
const url = config.image_url + event.image_id; const url = config.image_url + event.image_id;
const r = await fetch(config.image_url + event.image_id); const r = await fetch(config.image_url + event.image_id);
const blob = await r.blob(); const blob = await r.blob();
// NOTE: this will resolve when bitmap is ready, which will be much later
const bitmap = await createImageBitmap(blob); const bitmap = await createImageBitmap(blob);
const p = {'x': event.x, 'y': event.y};
event.width = bitmap.width; event.width = bitmap.width;
event.height = bitmap.height; event.height = bitmap.height;
add_image(context, event.image_id, bitmap, p); add_image(context, event.image_id, bitmap, p, bitmap.width, bitmap.height);
// God knows when this will actually complete (it loads the image from the server) // 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 // so do not set need_draw. Instead just schedule the draw ourselves when done
@ -240,41 +403,40 @@ function handle_event(state, context, event) {
} }
case EVENT.IMAGE_MOVE: { case EVENT.IMAGE_MOVE: {
// Already moved due to local prediction geometry_add_dummy_stroke(state, context);
if (event.user_id !== state.me) {
const image_id = event.image_id; const image_id = event.image_id;
const image_event = find_image(state, image_id); const image = get_image(context, image_id);
if (image_event) { if (image) {
// if (config.debug_print) console.debug('move image', image_id, 'to', image_event.x, image_event.y); // if (config.debug_print) console.debug('move image', image_id, 'to', image_event.x, image_event.y);
image_event.x = event.x; push_image_move(image, event.x, event.y);
image_event.y = event.y;
move_image(context, image_event);
need_draw = true; 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;
} }
break; break;
} }
case EVENT.ERASER: { case EVENT.ERASER: {
geometry_add_dummy_stroke(state, context);
need_draw = true; need_draw = true;
console.error('todo'); const stroke = state.events[event.stroke_id];
// if (event.deleted) { stroke.deleted = true;
// break; if (!options.skip_bvh) {
// } bvh_delete_stroke(state, stroke);
}
// 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; break;
} }
@ -286,8 +448,8 @@ function handle_event(state, context, event) {
return need_draw; return need_draw;
} }
async function handle_message(state, context, d) { function handle_message(state, context, d) {
const message_type = des_u8(d); const message_type = des_u32(d);
let do_draw = false; let do_draw = false;
// if (config.debug_print) console.debug(message_type); // if (config.debug_print) console.debug(message_type);
@ -295,8 +457,10 @@ async function handle_message(state, context, d) {
switch (message_type) { switch (message_type) {
case MESSAGE.JOIN: case MESSAGE.JOIN:
case MESSAGE.INIT: { case MESSAGE.INIT: {
state.server_lsn = des_u32(d); console.time('init');
state.online = true; state.online = true;
state.server_lsn = des_u32(d);
if (state.server_lsn > state.lsn) { if (state.server_lsn > state.lsn) {
// Server knows something that we don't // Server knows something that we don't
@ -312,20 +476,23 @@ async function handle_message(state, context, d) {
} else { } else {
color = des_u32(d); color = des_u32(d);
width = des_u16(d); width = des_u16(d);
state.me = parseInt(localStorage.getItem('sessionId'));
if (config.debug_print) console.debug('init in'); if (config.debug_print) console.debug('init in');
} }
state.me = parseInt(localStorage.getItem('sessionId'));
init_player_defaults(state, state.me); init_player_defaults(state, state.me);
const color_code = color_from_u32(color).substring(1); set_color_u32(state, color);
switch_color(state, document.querySelector(`.color[data-color="${color_code}"]`));
document.querySelector('#stroke-width').value = width; document.querySelector('#stroke-width').value = width;
fire_event(state, width_event(width)); fire_event(state, width_event(width));
const event_count = des_u32(d); const event_count = des_u32(d);
const user_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`); if (config.debug_print) console.debug(`${event_count} events in init`);
@ -335,21 +502,37 @@ async function handle_message(state, context, d) {
const user_id = des_u32(d); const user_id = des_u32(d);
const user_color = des_u32(d); const user_color = des_u32(d);
const user_width = des_u16(d); const user_width = des_u16(d);
const user_online = des_u8(d);
init_player_defaults(state, user_id, user_color, user_width); 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) { for (let i = 0; i < event_count; ++i) {
const event = des_event(d); const event = des_event(d, state);
handle_event(state, context, event); handle_event(state, context, event, {'skip_bvh': true});
if (event.type !== EVENT.STROKE || event.coords_to - event.coords_from > 0) {
state.events.push(event); state.events.push(event);
} }
}
state.sn = event_count;
bvh_construct(state);
do_draw = true; do_draw = true;
send_ack(event_count); send_ack(event_count);
sync_queue(state); sync_queue(state);
console.timeEnd('init');
update_cursor(state);
draw_html(state);
break; break;
} }
@ -385,7 +568,7 @@ async function handle_message(state, context, d) {
if (config.debug_print) console.debug(`syn ${sn} in`); if (config.debug_print) console.debug(`syn ${sn} in`);
for (let i = 0; i < count; ++i) { for (let i = 0; i < count; ++i) {
const event = des_event(d); const event = des_event(d, state);
if (i >= first) { if (i >= first) {
const need_draw = handle_event(state, context, event); const need_draw = handle_event(state, context, event);
do_draw = do_draw || need_draw; do_draw = do_draw || need_draw;
@ -395,7 +578,7 @@ async function handle_message(state, context, d) {
state.sn = sn; state.sn = sn;
send_ack(sn); // await? send_ack(sn);
break; break;
} }

195
client/client_send.js

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

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

@ -1,327 +0,0 @@
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 @@
--radius: 5px; --radius: 5px;
--hgap: 5px; --hgap: 5px;
--gap: 10px; --gap: 10px;
--transform-amimate: transform .1s ease-in-out; --transform-amimate: transform .1s ease-out;
} }
html, body { html, body {
@ -27,13 +27,38 @@ body.offline .main {
display: none !important; display: none !important;
} }
.vhide {
visibility: hidden !important;
}
.flexcol {
display: flex;
flex-direction: column;
}
canvas { canvas {
width: 100%; width: 100%;
height: 100%; height: 100%;
display: block; display: block;
cursor: url('icons/cursor.svg') 7 7, crosshair; cursor: url('icons/crosshair.svg') 16 16, crosshair;
}
canvas.tool-pointer {
cursor: default;
} }
canvas.picker {
cursor: url('icons/picker.svg') 0 19, crosshair;
}
canvas.resize-topleft {
cursor: nwse-resize;
}
canvas.resize-topright {
cursor: nesw-resize;
}
/*
canvas.movemode { canvas.movemode {
cursor: grab; cursor: grab;
} }
@ -42,6 +67,34 @@ canvas.movemode.moving {
cursor: grabbing; 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 { .tools-wrapper {
position: fixed; position: fixed;
bottom: 0; bottom: 0;
@ -54,7 +107,7 @@ canvas.movemode.moving {
} }
.pallete-wrapper, .pallete-wrapper,
.sizer-wrapper { .top-wrapper {
position: fixed; position: fixed;
top: 0; top: 0;
left: 0; left: 0;
@ -66,52 +119,98 @@ canvas.movemode.moving {
transition: var(--transform-amimate); transition: var(--transform-amimate);
} }
.sizer-wrapper { .top-wrapper {
height: unset; height: unset;
width: 100%; width: 100%;
flex-direction: row; flex-direction: row;
} }
.pallete { .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; pointer-events: all;
}
.player-list .player {
width: 24px;
height: 24px;
border-radius: var(--radius);
display: flex;
justify-content: center;
align-items: center;
border: 2px solid transparent;
box-sizing: border-box;
}
.player-list .player.following {
border-color: white;
}
.player-list .player img {
height: 12px;
width: 12px;
filter: invert(100%);
}
.pallete {
pointer-events: none;
display: grid; display: grid;
flex-direction: column; flex-direction: column;
align-items: center; align-items: center;
background: var(--dark-blue);
border-top-right-radius: var(--radius); border-top-right-radius: var(--radius);
border-bottom-right-radius: var(--radius); border-bottom-right-radius: var(--radius);
/* border-bottom-left-radius: var(--radius);*/ /* border-bottom-left-radius: var(--radius);*/
padding-top: var(--gap); padding-top: var(--gap);
padding-bottom: var(--gap); padding-bottom: var(--gap);
overflow: hidden;
} }
.pallete-wrapper.hidden { .pallete-wrapper.hidden {
transform: translateX(-125%); /* to account for a selected color, which is also offset to the right */ transform: translateX(-125%); /* to account for a selected color, which is also offset to the right */
} }
.sizer-wrapper.hidden { .top-wrapper.hidden {
transform: translateY(-125%); transform: translateY(-125%);
} }
.pallete .color { .pallete .color-major {
padding: var(--gap); pointer-events: all;
cursor: pointer;
background: var(--dark-blue); background: var(--dark-blue);
display: flex;
transition: var(--transform-amimate); transition: var(--transform-amimate);
transform: translateX(calc(-100% + 2 * var(--gap) + 24px));
cursor: pointer;
} }
.pallete .color:hover { .pallete .color-major.extended {
background: var(--dark-hover); transform: translateX(0px);
border-top-right-radius: var(--radius);
border-bottom-right-radius: var(--radius);
} }
.pallete .color.active { .pallete .color-major.active {
transform: translateX(10px); transform: translateX(calc(-100% + 3 * var(--gap) + 24px));
border-top-right-radius: var(--radius); border-top-right-radius: var(--radius);
border-bottom-right-radius: var(--radius); border-bottom-right-radius: var(--radius);
} }
.pallete .color.active:hover { .pallete .color-minor {
background: var(--dark-blue); padding: var(--gap);
display: flex;
justify-content: center;
align-items: center;
}
.pallete .color-major:not(.active) .color-minor:hover {
background: var(--dark-hover);
} }
.pallete .color-pane { .pallete .color-pane {
@ -120,6 +219,14 @@ canvas.movemode.moving {
box-sizing: border-box; box-sizing: border-box;
border-radius: var(--radius); border-radius: var(--radius);
} }
.pallete .color .color-pane:hover {
background: var(--dark-hover);
}
.pallete .color.active:hover {
background: var(--dark-blue);
}
.tools, .tools,
.sizer { .sizer {
@ -160,9 +267,9 @@ canvas.movemode.moving {
.tool.active { .tool.active {
transform: translateY(-10px); transform: translateY(-10px);
background: var(--dark-blue);
border-top-right-radius: var(--radius); border-top-right-radius: var(--radius);
border-top-left-radius: var(--radius); border-top-left-radius: var(--radius);
background: var(--dark-blue);
} }
.tool img { .tool img {
@ -171,17 +278,17 @@ canvas.movemode.moving {
filter: invert(100%); filter: invert(100%);
} }
input[type=range] { .sizer input[type=range] {
-webkit-appearance: none; -webkit-appearance: none;
width: 200px; width: 200px;
background: transparent; background: transparent;
} }
input[type=range]:focus { .sizer input[type=range]:focus {
outline: none; outline: none;
} }
input[type=range]::-webkit-slider-thumb { .sizer input[type=range]::-webkit-slider-thumb {
-webkit-appearance: none; -webkit-appearance: none;
border: none; border: none;
background: white; background: white;
@ -193,7 +300,7 @@ input[type=range]::-webkit-slider-thumb {
margin-top: -6px; /* You need to specify a margin in Chrome, but in Firefox and IE it is automatic */ margin-top: -6px; /* You need to specify a margin in Chrome, but in Firefox and IE it is automatic */
} }
input[type=range]::-moz-range-thumb { .sizer input[type=range]::-moz-range-thumb {
border: none; border: none;
background: white; background: white;
height: 16px; height: 16px;
@ -203,7 +310,7 @@ input[type=range]::-moz-range-thumb {
border: 2px solid var(--dark-blue); border: 2px solid var(--dark-blue);
} }
input[type=range]::-webkit-slider-runnable-track { .sizer input[type=range]::-webkit-slider-runnable-track {
width: 100%; width: 100%;
height: 8px; height: 8px;
cursor: pointer; cursor: pointer;
@ -212,7 +319,7 @@ input[type=range]::-webkit-slider-runnable-track {
border: none; border: none;
} }
input[type=range]:focus::-webkit-slider-runnable-track { .sizer input[type=range]:focus::-webkit-slider-runnable-track {
width: 100%; width: 100%;
height: 8px; height: 8px;
cursor: pointer; cursor: pointer;
@ -221,7 +328,7 @@ input[type=range]:focus::-webkit-slider-runnable-track {
border: none; border: none;
} }
input[type=range]::-moz-range-track { .sizer input[type=range]::-moz-range-track {
width: 100%; width: 100%;
height: 8px; height: 8px;
cursor: pointer; cursor: pointer;
@ -252,17 +359,17 @@ input[type=range]::-moz-range-track {
} }
@media (hover: none) and (pointer: coarse) { @media (hover: none) and (pointer: coarse) {
html, body {
touch-action: none;
}
.phone-extra-controls { .phone-extra-controls {
display: flex; display: flex;
} }
}
#stroke-preview { .brush-dom {
position: absolute; display: none;
border-radius: 50%; }
left: 50%;
top: 96px;
transform: translate(-50%, -50%);
} }
.offline-toast { .offline-toast {
@ -278,6 +385,7 @@ input[type=range]::-moz-range-track {
border-radius: var(--radius); border-radius: var(--radius);
box-shadow: 0px 2px 3px 0px rgba(155, 150, 100, 0.2); box-shadow: 0px 2px 3px 0px rgba(155, 150, 100, 0.2);
transition: transform .1s ease-in-out, opacity .1s; transition: transform .1s ease-in-out, opacity .1s;
font-family: sans-serif;
font-size: 14px; font-size: 14px;
color: white; color: white;
font-weight: bold; font-weight: bold;
@ -294,3 +402,47 @@ input[type=range]::-moz-range-track {
body.offline * { body.offline * {
pointer-events: none; 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

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

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

After

Width:  |  Height:  |  Size: 7.6 KiB

52
client/icons/cursor.svg

@ -2,49 +2,25 @@
<!-- Created with Inkscape (http://www.inkscape.org/) --> <!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg <svg
width="3.7139902mm" width="16"
height="3.7139902mm" height="16"
viewBox="0 0 3.7139902 3.7139902" viewBox="0 0 16 16"
version="1.1" version="1.1"
id="svg5" 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="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg"> xmlns:svg="http://www.w3.org/2000/svg">
<sodipodi:namedview <g id="layer1">
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 <circle
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" style="fill:none;stroke:#ffffff;stroke-width:1"
id="circle1000"
cy="128.53627"
cx="135.63414"
r="1.5875" />
<circle
style="fill:none;stroke:#000000;stroke-width:0.25"
id="path236" id="path236"
cy="128.2766" cy="128.53627"
cx="135.37447" cx="135.63414"
r="1.6" /> r="1.5875" />
</g> </g>
</svg> </svg>

Before

Width:  |  Height:  |  Size: 1.5 KiB

After

Width:  |  Height:  |  Size: 624 B

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

Before

Width:  |  Height:  |  Size: 500 B

After

Width:  |  Height:  |  Size: 500 B

2
client/icons/perfect-bullet.svg

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

After

Width:  |  Height:  |  Size: 335 B

105
client/icons/picker.svg

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

After

Width:  |  Height:  |  Size: 5.8 KiB

54
client/icons/player-cursor.svg

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

After

Width:  |  Height:  |  Size: 1.7 KiB

57
client/icons/player.svg

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

After

Width:  |  Height:  |  Size: 1.9 KiB

52
client/icons/pointer.svg

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

After

Width:  |  Height:  |  Size: 1.7 KiB

241
client/index.html

@ -7,48 +7,229 @@
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no"> <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
<link rel="shortcut icon" href="icons/favicon.svg" id="favicon"> <link rel="shortcut icon" href="icons/favicon.svg" id="favicon">
<link rel="stylesheet" type="text/css" href="default.css?v=28"> <link rel="stylesheet" type="text/css" href="default.css">
<script type="text/javascript" src="aux.js?v=28"></script> <!-- <link rel="preload" href="icons/picker.svg" as="image" type="image/svg+xml" /> -->
<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="random_helpers.js"></script>
<script type="text/javascript" src="webgl_geometry.js?v=28"></script> <script type="text/javascript" src="heapify.js"></script>
<script type="text/javascript" src="webgl_shaders.js?v=28"></script> <script type="text/javascript" src="bvh.js"></script>
<script type="text/javascript" src="webgl_listeners.js?v=28"></script> <script type="text/javascript" src="math.js"></script>
<script type="text/javascript" src="webgl_draw.js?v=28"></script> <script type="text/javascript" src="tools.js"></script>
<script type="text/javascript" src="index.js?v=28"></script> <script type="text/javascript" src="speed.js"></script>
<script type="text/javascript" src="webgl_geometry.js"></script>
<script type="text/javascript" src="client_send.js?v=28"></script> <script type="text/javascript" src="webgl_shaders.js"></script>
<script type="text/javascript" src="client_recv.js?v=28"></script> <script type="text/javascript" src="webgl_listeners.js"></script>
<script type="text/javascript" src="websocket.js?v=28"></script> <script type="text/javascript" src="webgl_draw.js"></script>
<script type="text/javascript" src="undo.js"></script>
<script type="text/javascript" src="config.js"></script>
<script type="text/javascript" src="index.js"></script>
<script type="text/javascript" src="client_send.js"></script>
<script type="text/javascript" src="client_recv.js"></script>
<script type="text/javascript" src="websocket.js"></script>
</head> </head>
<body> <body>
<div class="main"> <div class="main">
<canvas id="c"></canvas> <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="sizer-wrapper"> <div class="top-wrapper">
<div class="topleft"></div>
<div class="sizer"> <div class="sizer">
<input type="range" class="slider" id="stroke-width" min="1" max="64"> <input type="range" class="slider" id="stroke-width" min="0.01" step="0.01" max="64">
</div> </div>
<div id="stroke-preview" class="dhide"></div> <div class="player-list vhide"></div>
</div> </div>
<div class="pallete-wrapper"> <div class="pallete-wrapper">
<!-- Open Color is MIT licensed: https://github.com/yeun/open-color -->
<div class="pallete"> <div class="pallete">
<div class="color" data-color="000000"><div class="color-pane" style="background: #000000;"></div></div> <div class="color-major" style="border-top-right-radius: var(--radius);">
<div class="color" data-color="ffffff"><div class="color-pane" style="background: #ffffff;"></div></div> <div class="color-minor" data-color="f1f3f5"><div class="color-pane" style="background: #f1f3f5;"></div></div>
<div class="color" data-color="d65c5c"><div class="color-pane" style="background: #d65c5c;"></div></div> <div class="color-minor" data-color="e9ecef"><div class="color-pane" style="background: #e9ecef;"></div></div>
<div class="color" data-color="d6835c"><div class="color-pane" style="background: #d6835c;"></div></div> <div class="color-minor" data-color="dee2e6"><div class="color-pane" style="background: #dee2e6;"></div></div>
<div class="color" data-color="72d65c"><div class="color-pane" style="background: #72d65c;"></div></div> <div class="color-minor" data-color="ced4da"><div class="color-pane" style="background: #ced4da;"></div></div>
<div class="color" data-color="5cd6ce"><div class="color-pane" style="background: #5cd6ce;"></div></div> <div class="color-minor" data-color="adb5bd"><div class="color-pane" style="background: #adb5bd;"></div></div>
<div class="color" data-color="5c89d6"><div class="color-pane" style="background: #5c89d6;"></div></div> <div class="color-minor" data-color="868e96"><div class="color-pane" style="background: #868e96;"></div></div>
<div class="color" data-color="6e5cd6"><div class="color-pane" style="background: #6e5cd6;"></div></div> <div class="color-minor" data-color="495057"><div class="color-pane" style="background: #495057;"></div></div>
<div class="color-minor" data-color="343a40"><div class="color-pane" style="background: #343a40;"></div></div>
<div class="color-minor" data-color="212529"><div class="color-pane" style="background: #212529;"></div></div>
<div class="color-minor" data-color="000000"><div class="color-pane" style="background: #000000;"></div></div>
</div>
<div class="color-major">
<div class="color-minor" data-color="fff5f5"><div class="color-pane" style="background: #fff5f5;"></div></div>
<div class="color-minor" data-color="ffe3e3"><div class="color-pane" style="background: #ffe3e3;"></div></div>
<div class="color-minor" data-color="ffc9c9"><div class="color-pane" style="background: #ffc9c9;"></div></div>
<div class="color-minor" data-color="ffa8a8"><div class="color-pane" style="background: #ffa8a8;"></div></div>
<div class="color-minor" data-color="ff8787"><div class="color-pane" style="background: #ff8787;"></div></div>
<div class="color-minor" data-color="ff6b6b"><div class="color-pane" style="background: #ff6b6b;"></div></div>
<div class="color-minor" data-color="fa5252"><div class="color-pane" style="background: #fa5252;"></div></div>
<div class="color-minor" data-color="f03e3e"><div class="color-pane" style="background: #f03e3e;"></div></div>
<div class="color-minor" data-color="e03131"><div class="color-pane" style="background: #e03131;"></div></div>
<div class="color-minor" data-color="c92a2a"><div class="color-pane" style="background: #c92a2a;"></div></div>
</div>
<div class="color-major">
<div class="color-minor" data-color="fff0f6"><div class="color-pane" style="background: #fff0f6;"></div></div>
<div class="color-minor" data-color="ffdeeb"><div class="color-pane" style="background: #ffdeeb;"></div></div>
<div class="color-minor" data-color="fcc2d7"><div class="color-pane" style="background: #fcc2d7;"></div></div>
<div class="color-minor" data-color="faa2c1"><div class="color-pane" style="background: #faa2c1;"></div></div>
<div class="color-minor" data-color="f783ac"><div class="color-pane" style="background: #f783ac;"></div></div>
<div class="color-minor" data-color="f06595"><div class="color-pane" style="background: #f06595;"></div></div>
<div class="color-minor" data-color="e64980"><div class="color-pane" style="background: #e64980;"></div></div>
<div class="color-minor" data-color="d6336c"><div class="color-pane" style="background: #d6336c;"></div></div>
<div class="color-minor" data-color="c2255c"><div class="color-pane" style="background: #c2255c;"></div></div>
<div class="color-minor" data-color="a61e4d"><div class="color-pane" style="background: #a61e4d;"></div></div>
</div>
<div class="color-major">
<div class="color-minor" data-color="f8f0fc"><div class="color-pane" style="background: #f8f0fc;"></div></div>
<div class="color-minor" data-color="f3d9fa"><div class="color-pane" style="background: #f3d9fa;"></div></div>
<div class="color-minor" data-color="eebefa"><div class="color-pane" style="background: #eebefa;"></div></div>
<div class="color-minor" data-color="e599f7"><div class="color-pane" style="background: #e599f7;"></div></div>
<div class="color-minor" data-color="da77f2"><div class="color-pane" style="background: #da77f2;"></div></div>
<div class="color-minor" data-color="cc5de8"><div class="color-pane" style="background: #cc5de8;"></div></div>
<div class="color-minor" data-color="be4bdb"><div class="color-pane" style="background: #be4bdb;"></div></div>
<div class="color-minor" data-color="ae3ec9"><div class="color-pane" style="background: #ae3ec9;"></div></div>
<div class="color-minor" data-color="9c36b5"><div class="color-pane" style="background: #9c36b5;"></div></div>
<div class="color-minor" data-color="862e9c"><div class="color-pane" style="background: #862e9c;"></div></div>
</div>
<div class="color-major">
<div class="color-minor" data-color="f3f0ff"><div class="color-pane" style="background: #f3f0ff;"></div></div>
<div class="color-minor" data-color="e5dbff"><div class="color-pane" style="background: #e5dbff;"></div></div>
<div class="color-minor" data-color="d0bfff"><div class="color-pane" style="background: #d0bfff;"></div></div>
<div class="color-minor" data-color="b197fc"><div class="color-pane" style="background: #b197fc;"></div></div>
<div class="color-minor" data-color="9775fa"><div class="color-pane" style="background: #9775fa;"></div></div>
<div class="color-minor" data-color="845ef7"><div class="color-pane" style="background: #845ef7;"></div></div>
<div class="color-minor" data-color="7950f2"><div class="color-pane" style="background: #7950f2;"></div></div>
<div class="color-minor" data-color="7048e8"><div class="color-pane" style="background: #7048e8;"></div></div>
<div class="color-minor" data-color="6741d9"><div class="color-pane" style="background: #6741d9;"></div></div>
<div class="color-minor" data-color="5f3dc4"><div class="color-pane" style="background: #5f3dc4;"></div></div>
</div>
<div class="color-major">
<div class="color-minor" data-color="edf2ff"><div class="color-pane" style="background: #edf2ff;"></div></div>
<div class="color-minor" data-color="dbe4ff"><div class="color-pane" style="background: #dbe4ff;"></div></div>
<div class="color-minor" data-color="bac8ff"><div class="color-pane" style="background: #bac8ff;"></div></div>
<div class="color-minor" data-color="91a7ff"><div class="color-pane" style="background: #91a7ff;"></div></div>
<div class="color-minor" data-color="748ffc"><div class="color-pane" style="background: #748ffc;"></div></div>
<div class="color-minor" data-color="5c7cfa"><div class="color-pane" style="background: #5c7cfa;"></div></div>
<div class="color-minor" data-color="4c6ef5"><div class="color-pane" style="background: #4c6ef5;"></div></div>
<div class="color-minor" data-color="4263eb"><div class="color-pane" style="background: #4263eb;"></div></div>
<div class="color-minor" data-color="3b5bdb"><div class="color-pane" style="background: #3b5bdb;"></div></div>
<div class="color-minor" data-color="364fc7"><div class="color-pane" style="background: #364fc7;"></div></div>
</div>
<div class="color-major">
<div class="color-minor" data-color="e7f5ff"><div class="color-pane" style="background: #e7f5ff;"></div></div>
<div class="color-minor" data-color="d0ebff"><div class="color-pane" style="background: #d0ebff;"></div></div>
<div class="color-minor" data-color="a5d8ff"><div class="color-pane" style="background: #a5d8ff;"></div></div>
<div class="color-minor" data-color="74c0fc"><div class="color-pane" style="background: #74c0fc;"></div></div>
<div class="color-minor" data-color="4dabf7"><div class="color-pane" style="background: #4dabf7;"></div></div>
<div class="color-minor" data-color="339af0"><div class="color-pane" style="background: #339af0;"></div></div>
<div class="color-minor" data-color="228be6"><div class="color-pane" style="background: #228be6;"></div></div>
<div class="color-minor" data-color="1c7ed6"><div class="color-pane" style="background: #1c7ed6;"></div></div>
<div class="color-minor" data-color="1971c2"><div class="color-pane" style="background: #1971c2;"></div></div>
<div class="color-minor" data-color="1864ab"><div class="color-pane" style="background: #1864ab;"></div></div>
</div>
<div class="color-major">
<div class="color-minor" data-color="e3fafc"><div class="color-pane" style="background: #e3fafc;"></div></div>
<div class="color-minor" data-color="c5f6fa"><div class="color-pane" style="background: #c5f6fa;"></div></div>
<div class="color-minor" data-color="99e9f2"><div class="color-pane" style="background: #99e9f2;"></div></div>
<div class="color-minor" data-color="66d9e8"><div class="color-pane" style="background: #66d9e8;"></div></div>
<div class="color-minor" data-color="3bc9db"><div class="color-pane" style="background: #3bc9db;"></div></div>
<div class="color-minor" data-color="22b8cf"><div class="color-pane" style="background: #22b8cf;"></div></div>
<div class="color-minor" data-color="15aabf"><div class="color-pane" style="background: #15aabf;"></div></div>
<div class="color-minor" data-color="1098ad"><div class="color-pane" style="background: #1098ad;"></div></div>
<div class="color-minor" data-color="0c8599"><div class="color-pane" style="background: #0c8599;"></div></div>
<div class="color-minor" data-color="0b7285"><div class="color-pane" style="background: #0b7285;"></div></div>
</div>
<div class="color-major">
<div class="color-minor" data-color="e6fcf5"><div class="color-pane" style="background: #e6fcf5;"></div></div>
<div class="color-minor" data-color="c3fae8"><div class="color-pane" style="background: #c3fae8;"></div></div>
<div class="color-minor" data-color="96f2d7"><div class="color-pane" style="background: #96f2d7;"></div></div>
<div class="color-minor" data-color="63e6be"><div class="color-pane" style="background: #63e6be;"></div></div>
<div class="color-minor" data-color="38d9a9"><div class="color-pane" style="background: #38d9a9;"></div></div>
<div class="color-minor" data-color="20c997"><div class="color-pane" style="background: #20c997;"></div></div>
<div class="color-minor" data-color="12b886"><div class="color-pane" style="background: #12b886;"></div></div>
<div class="color-minor" data-color="0ca678"><div class="color-pane" style="background: #0ca678;"></div></div>
<div class="color-minor" data-color="099268"><div class="color-pane" style="background: #099268;"></div></div>
<div class="color-minor" data-color="087f5b"><div class="color-pane" style="background: #087f5b;"></div></div>
</div>
<div class="color-major">
<div class="color-minor" data-color="ebfbee"><div class="color-pane" style="background: #ebfbee;"></div></div>
<div class="color-minor" data-color="d3f9d8"><div class="color-pane" style="background: #d3f9d8;"></div></div>
<div class="color-minor" data-color="b2f2bb"><div class="color-pane" style="background: #b2f2bb;"></div></div>
<div class="color-minor" data-color="8ce99a"><div class="color-pane" style="background: #8ce99a;"></div></div>
<div class="color-minor" data-color="69db7c"><div class="color-pane" style="background: #69db7c;"></div></div>
<div class="color-minor" data-color="51cf66"><div class="color-pane" style="background: #51cf66;"></div></div>
<div class="color-minor" data-color="40c057"><div class="color-pane" style="background: #40c057;"></div></div>
<div class="color-minor" data-color="37b24d"><div class="color-pane" style="background: #37b24d;"></div></div>
<div class="color-minor" data-color="2f9e44"><div class="color-pane" style="background: #2f9e44;"></div></div>
<div class="color-minor" data-color="2b8a3e"><div class="color-pane" style="background: #2b8a3e;"></div></div>
</div>
<div class="color-major">
<div class="color-minor" data-color="f4fce3"><div class="color-pane" style="background: #f4fce3;"></div></div>
<div class="color-minor" data-color="e9fac8"><div class="color-pane" style="background: #e9fac8;"></div></div>
<div class="color-minor" data-color="d8f5a2"><div class="color-pane" style="background: #d8f5a2;"></div></div>
<div class="color-minor" data-color="c0eb75"><div class="color-pane" style="background: #c0eb75;"></div></div>
<div class="color-minor" data-color="a9e34b"><div class="color-pane" style="background: #a9e34b;"></div></div>
<div class="color-minor" data-color="94d82d"><div class="color-pane" style="background: #94d82d;"></div></div>
<div class="color-minor" data-color="82c91e"><div class="color-pane" style="background: #82c91e;"></div></div>
<div class="color-minor" data-color="74b816"><div class="color-pane" style="background: #74b816;"></div></div>
<div class="color-minor" data-color="66a80f"><div class="color-pane" style="background: #66a80f;"></div></div>
<div class="color-minor" data-color="5c940d"><div class="color-pane" style="background: #5c940d;"></div></div>
</div>
<div class="color-major">
<div class="color-minor" data-color="fff9db"><div class="color-pane" style="background: #fff9db;"></div></div>
<div class="color-minor" data-color="fff3bf"><div class="color-pane" style="background: #fff3bf;"></div></div>
<div class="color-minor" data-color="ffec99"><div class="color-pane" style="background: #ffec99;"></div></div>
<div class="color-minor" data-color="ffe066"><div class="color-pane" style="background: #ffe066;"></div></div>
<div class="color-minor" data-color="ffd43b"><div class="color-pane" style="background: #ffd43b;"></div></div>
<div class="color-minor" data-color="fcc419"><div class="color-pane" style="background: #fcc419;"></div></div>
<div class="color-minor" data-color="fab005"><div class="color-pane" style="background: #fab005;"></div></div>
<div class="color-minor" data-color="f59f00"><div class="color-pane" style="background: #f59f00;"></div></div>
<div class="color-minor" data-color="f08c00"><div class="color-pane" style="background: #f08c00;"></div></div>
<div class="color-minor" data-color="e67700"><div class="color-pane" style="background: #e67700;"></div></div>
</div>
<div class="color-major" style="border-bottom-right-radius: var(--radius);">
<div class="color-minor" data-color="fff4e6"><div class="color-pane" style="background: #fff4e6;"></div></div>
<div class="color-minor" data-color="ffe8cc"><div class="color-pane" style="background: #ffe8cc;"></div></div>
<div class="color-minor" data-color="ffd8a8"><div class="color-pane" style="background: #ffd8a8;"></div></div>
<div class="color-minor" data-color="ffc078"><div class="color-pane" style="background: #ffc078;"></div></div>
<div class="color-minor" data-color="ffa94d"><div class="color-pane" style="background: #ffa94d;"></div></div>
<div class="color-minor" data-color="ff922b"><div class="color-pane" style="background: #ff922b;"></div></div>
<div class="color-minor" data-color="fd7e14"><div class="color-pane" style="background: #fd7e14;"></div></div>
<div class="color-minor" data-color="f76707"><div class="color-pane" style="background: #f76707;"></div></div>
<div class="color-minor" data-color="e8590c"><div class="color-pane" style="background: #e8590c;"></div></div>
<div class="color-minor" data-color="d9480f"><div class="color-pane" style="background: #d9480f;"></div></div>
</div>
</div> </div>
</div> </div>
<div class="tools-wrapper"> <div class="tools-wrapper">
<div class="tools"> <div class="tools">
<div class="tool active" data-tool="pencil"><img draggable="false" src="icons/draw.svg"></div> <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" data-tool="ruler"><img draggable="false" src="icons/ruler.svg"></div> <div class="tool" data-tool="ruler"><img draggable="false" src="icons/ruler.svg"></div>
<div class="tool" data-tool="eraser"><img draggable="false" src="icons/erase.svg"></div> <div class="tool" data-tool="eraser"><img draggable="false" src="icons/erase.svg"></div>
<div class="tool" data-tool="undo"><img draggable="false" src="icons/undo.svg"></div> <div class="tool" data-tool="undo"><img draggable="false" src="icons/undo.svg"></div>
@ -64,5 +245,13 @@
<div class="offline-toast hidden"> <div class="offline-toast hidden">
Whiteboard offline Whiteboard offline
</div> </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> </body>
</html> </html>

188
client/index.js

@ -1,37 +1,27 @@
document.addEventListener('DOMContentLoaded', main); 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({ const EVENT = Object.freeze({
PREDRAW: 10, PREDRAW: 10,
SET_COLOR: 11, SET_COLOR: 11,
SET_WIDTH: 12, SET_WIDTH: 12,
CLEAR: 13, // clear predraw events from me (because I started a pan instead of drawing)
MOVE_CURSOR: 14,
LIFT: 15,
LEAVE: 16,
MOVE_CANVAS: 17,
USER_JOINED: 18,
ZOOM_CANVAS: 19,
STROKE: 20, STROKE: 20,
RULER: 21, /* gets re-written with EVENT.STROKE before sending to server */ RULER: 21, // gets re-written with EVENT.STROKE before sending to server
UNDO: 30, UNDO: 30,
REDO: 31, REDO: 31,
IMAGE: 40, IMAGE: 40,
IMAGE_MOVE: 41, IMAGE_MOVE: 41,
IMAGE_SCALE: 42,
ERASER: 50, ERASER: 50,
}); });
@ -43,16 +33,85 @@ const MESSAGE = Object.freeze({
FULL: 103, FULL: 103,
FIRE: 104, FIRE: 104,
JOIN: 105, JOIN: 105,
FOLLOW: 106,
}); });
function main() { // 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() {
const state = { const state = {
'online': false, 'online': false,
'me': null, 'me': null,
'canvas': { 'canvas': {
'offset': { 'x': 0, 'y': 0 }, 'offset': { 'x': 0, 'y': 0 },
'zoom_level': 0,
'zoom': 1.0, 'zoom': 1.0,
'target_zoom': 1.0,
'zoom_screenp': {'x': 0, 'y': 0},
}, },
'cursor': { 'cursor': {
@ -65,26 +124,47 @@ function main() {
'server_lsn': 0, 'server_lsn': 0,
'touch': { 'touch': {
'moves': 0,
'drawing': false, 'drawing': false,
'moving': false, 'moving': false,
'erasing': false,
'waiting_for_second_finger': false, 'waiting_for_second_finger': false,
'first_finger_position': null,
'second_finger_position': null,
'buffered': [], 'buffered': [],
'ids': [], 'events': [],
}, },
'moving': false, 'moving': false,
'drawing': false, 'drawing': false,
'erasing': false,
'spacedown': false, 'spacedown': false,
'colorpicking': false,
'zooming': false,
'zoomdown': false,
'imagemoving': false,
'imagescaling': false,
'linedrawing': false,
'moving_image': null, 'active_image': null,
'scaling_corner': null,
'ruler_origin': null,
'image_actually_moved': false,
'current_strokes': {}, 'current_strokes': {},
'rdp_mask': new Uint8Array(1024),
'rdp_traverse_stack': new Uint32Array(4096),
'queue': [], 'queue': [],
'events': [], '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': { 'tools': {
'active': null, 'active': null,
@ -93,6 +173,7 @@ function main() {
'colors': { 'colors': {
'active_element': null, 'active_element': null,
'extended_element': null,
}, },
'timers': { 'timers': {
@ -102,6 +183,27 @@ function main() {
}, },
'players': {}, '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 = { const context = {
@ -112,27 +214,51 @@ function main() {
'buffers': {}, 'buffers': {},
'locations': {}, 'locations': {},
'textures': {}, 'textures': {},
'images': [],
'quad_positions': [], 'dynamic_serializer': serializer_create(config.initial_dynamic_bytes),
'quad_texcoords': [], 'dynamic_index_serializer': serializer_create(config.initial_dynamic_bytes),
'static_stroke_serializer': serializer_create(config.initial_static_bytes), 'clipped_indices': tv_create(Uint32Array, 4096),
'dynamic_stroke_serializer': serializer_create(config.initial_static_bytes),
'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,
'bgcolor': {'r': 1.0, 'g': 1.0, 'b': 1.0}, 'bgcolor': {'r': 1.0, 'g': 1.0, 'b': 1.0},
'active_image': null, 'gpu_timer_ext': null,
'last_frame_ts': 0,
'last_frame_dt': 0,
}; };
load_player_cursor_template(state);
start_spinner(state);
const url = new URL(window.location.href); const url = new URL(window.location.href);
const parts = url.pathname.split('/'); const parts = url.pathname.split('/');
state.desk_id = parts.length > 0 ? parts[parts.length - 1] : 0; state.desk_id = parts.length > 0 ? parts[parts.length - 1] : 0;
await init_wasm(state);
init_webgl(state, context); init_webgl(state, context);
init_listeners(state, context); init_listeners(state, context);
init_tools(state); init_tools(state);
ws_connect(state, context, true); ws_connect(state, context, true);
schedule_draw(state, context); schedule_draw(state, context);

36
client/index.log

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

@ -0,0 +1,49 @@
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);
}
}

648
client/math.js

@ -1,3 +1,7 @@
function round_to_pow2(value, multiple) {
return (value + multiple - 1) & -multiple;
}
function screen_to_canvas(state, p) { function screen_to_canvas(state, p) {
// should be called with coordinates obtained from MouseEvent.clientX/clientY * window.devicePixelRatio // should be called with coordinates obtained from MouseEvent.clientX/clientY * window.devicePixelRatio
const xc = (p.x - state.canvas.offset.x) / state.canvas.zoom; const xc = (p.x - state.canvas.offset.x) / state.canvas.zoom;
@ -6,14 +10,93 @@ function screen_to_canvas(state, p) {
return {'x': xc, 'y': yc}; return {'x': xc, 'y': yc};
} }
function point_right_of_line(a, b, p) { function canvas_to_screen(state, p) {
// a bit of cross-product tomfoolery (we check sign of z of the crossproduct) const xs = (p.x * state.canvas.zoom + state.canvas.offset.x) / window.devicePixelRatio;
return ((b.x - a.x) * (a.y - p.y) - (a.y - b.y) * (p.x - a.x)) <= 0; const ys = (p.y * state.canvas.zoom + state.canvas.offset.y) / window.devicePixelRatio;
return {'x': xs, 'y': ys};
} }
function rdp_find_max(state, points, start, end) { function process_rdp_indices_r(state, zoom, mask, stroke, start, end) {
const EPS = 0.5 / state.canvas.zoom; // Looks like the recursive implementation spends most of its time in the function call overhead
// const EPS = 10.0; // Let's try to use an explicit stack instead to give the js engine more room to play with
// Update: it's not faster. But it gives more sensible source-line samples in chrome profiler, so I'll leave it
let result = 0;
const stack = [];
stack.push({'start': start, 'end': end});
while (stack.length > 0) {
const region = stack.pop();
const max = rdp_find_max(state, zoom, stroke.coords_from, region.start, region.end);
if (max !== -1) {
mask[max] = 1;
result += 1;
stack.push({'start': region.start, 'end': max});
stack.push({'start': max, 'end': region.end});
}
}
return result;
}
function process_rdp_indices(state, zoom, stroke) {
const point_count = stroke.coords_to - stroke.coords_from;
if (state.rdp_mask.length < point_count) {
state.rdp_mask = new Uint8Array(point_count);
}
state.rdp_mask.fill(0, 0, point_count);
const mask = state.rdp_mask;
const npoints = 2 + process_rdp_indices_r(state, zoom, mask, stroke, 0, point_count - 1); // 2 is for the first and last vertex, which do not get included by the recursive functions, but should always be there at any lod level
mask[0] = 1;
mask[point_count - 1] = 1;
return npoints;
}
function exponential_smoothing(points, last, up_to) {
const alpha = 0.5;
let pr = 0;
let start = points.length - up_to;
if (start < 0) {
start = 0;
}
for (let i = start; i < points.length; ++i) {
const p = points[i];
pr = alpha * p.pressure + (1 - alpha) * pr;
}
pr = alpha * last.pressure + (1 - alpha) * pr;
return pr;
}
function process_stroke(state, zoom, stroke) {
// Try caching the highest zoom level that only returns the endpoints
if (zoom <= stroke.turns_into_straight_line_zoom) {
return 2;
}
const npoints = process_rdp_indices(state, zoom, stroke, true);
if (npoints === 2 && zoom > stroke.turns_into_straight_line_zoom) {
stroke.turns_into_straight_line_zoom = zoom;
}
return npoints;
}
function rdp_find_max2(zoom, points, start, end) {
const EPS = 0.125 / zoom;
let result = -1; let result = -1;
let max_dist = 0; let max_dist = 0;
@ -28,7 +111,7 @@ function rdp_find_max(state, points, start, end) {
const sin_theta = dy / dist_ab; const sin_theta = dy / dist_ab;
const cos_theta = dx / dist_ab; const cos_theta = dx / dist_ab;
for (let i = start; i < end; ++i) { for (let i = start + 1; i < end; ++i) {
const p = points[i]; const p = points[i];
const ox = p.x - a.x; const ox = p.x - a.x;
@ -40,7 +123,7 @@ function rdp_find_max(state, points, start, end) {
const x = rx + a.x; const x = rx + a.x;
const y = ry + a.y; const y = ry + a.y;
const dist = Math.abs(y - a.y); const dist = Math.abs(y - a.y) + Math.abs(a.pressure - p.pressure) / 255 + Math.abs(b.pressure - p.pressure) / 255;
if (dist > EPS && dist > max_dist) { if (dist > EPS && dist > max_dist) {
result = i; result = i;
@ -51,119 +134,71 @@ function rdp_find_max(state, points, start, end) {
return result; return result;
} }
function process_rdp_r(state, points, start, end) { function process_rdp_r2(zoom, points, start, end) {
let result = []; let result = [];
const max = rdp_find_max(state, points, start, end); const max = rdp_find_max2(zoom, points, start, end);
if (max !== -1) { if (max !== -1) {
const before = process_rdp_r(state, points, start, max); const before = process_rdp_r2(zoom, points, start, max);
const after = process_rdp_r(state, points, max, end); const after = process_rdp_r2(zoom, points, max, end);
result = [...before, points[max], ...after]; result = [...before, points[max], ...after];
} }
return result; return result;
} }
function process_rdp(state, points) { function process_rdp2(zoom, points) {
const result = process_rdp_r(state, points, 0, points.length - 1);
result.unshift(points[0]);
result.push(points[points.length - 1]);
return result;
}
function process_ewmv(points, round = false) {
const result = []; const result = [];
const alpha = 0.5; const stack = [];
result.push(points[0]); stack.push({
'type': 0,
'start': 0,
'end': points.length - 1,
});
for (let i = 1; i < points.length; ++i) { result.push(points[0]);
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) { while (stack.length > 0) {
// const result0 = process_ewmv(points); const entry = stack.pop();
const result1 = process_rdp(state, points, true);
return result1;
}
function stroke_stats(points, width) { if (entry.type === 0) {
if (points.length === 0) { const max = rdp_find_max2(zoom, points, entry.start, entry.end);
const bbox = {
'xmin': 0,
'ymin': 0,
'xmax': 0,
'ymax': 0
};
return { if (max !== -1) {
'bbox': bbox, stack.push({
'length': 0, '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]);
}
} }
let length = 0; result.push(points[points.length - 1]);
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);
}
}
xmin -= width;
ymin -= width;
xmax += width * 2;
ymax += width * 2;
const bbox = {
'xmin': Math.floor(xmin),
'ymin': Math.floor(ymin),
'xmax': Math.ceil(xmax),
'ymax': Math.ceil(ymax)
};
return {
'bbox': bbox,
'length': length,
};
}
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; return result;
} }
function stroke_intersects_region(points, bbox) { // TODO: unify with regular process stroke
if (points.length === 0) { function process_stroke2(zoom, points) {
return false; //const result = smooth_curve(points);
} const result = process_rdp2(zoom, points);
return result;
const stats = stroke_stats(points, storage.cursor.width);
return rectangles_intersect(stats.bbox, bbox);
} }
function color_to_u32(color_str) { function color_to_u32(color_str) {
@ -190,6 +225,22 @@ function color_from_u32(color_u32) {
return '#' + r_str + g_str + b_str; return '#' + r_str + g_str + b_str;
} }
function color_from_rgbdict(color_dict) {
const r = Math.floor(color_dict.r * 255);
const g = Math.floor(color_dict.g * 255);
const b = Math.floor(color_dict.b * 255);
let r_str = r.toString(16);
let g_str = g.toString(16);
let b_str = b.toString(16);
if (r <= 0xF) r_str = '0' + r_str;
if (g <= 0xF) g_str = '0' + g_str;
if (b <= 0xF) b_str = '0' + b_str;
return '#' + r_str + g_str + b_str;
}
function ccw(A, B, C) { function ccw(A, B, C) {
return (C.y - A.y) * (B.x - A.x) > (B.y - A.y) * (C.x - A.x); return (C.y - A.y) * (B.x - A.x) > (B.y - A.y) * (C.x - A.x);
} }
@ -199,74 +250,385 @@ function segments_intersect(A, B, C, D) {
return ccw(A, C, D) != ccw(B, C, D) && ccw(A, B, C) !== ccw(A, B, D); return ccw(A, C, D) != ccw(B, C, D) && ccw(A, B, C) !== ccw(A, B, D);
} }
function strokes_intersect_line(x1, y1, x2, y2) { function dist_v2(a, b) {
const result = []; 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;
}
for (const event of storage.events) { function point_in_bbox(p, bbox) {
if (event.type === EVENT.STROKE && !event.deleted) { if (bbox.x1 <= p.x && p.x < bbox.x2 && bbox.y1 <= p.y && p.y < bbox.y2) {
if (event.points.length < 2) { return true;
continue;
} }
for (let i = 0; i < event.points.length - 1; ++i) { return false;
const sx1 = event.points[i].x; }
const sy1 = event.points[i].y;
const sx2 = event.points[i + 1].x; function clamp(v, a, b) {
const sy2 = event.points[i + 1].y; return (v < a ? a : (v > b ? b : v));
}
const A = {'x': x1, 'y': y1}; function dot(a, b) {
const B = {'x': x2, 'y': y2}; return a.x * b.x + a.y * b.y;
}
const C = {'x': sx1, 'y': sy1}; function dotn(a, b) {
const D = {'x': sx2, 'y': sy2}; let r = 0;
if (segments_intersect(A, B, C, D)) { for (let i = 0; i < a.length; ++i) {
result.push(event.stroke_id); r += a[i] * b[i];
break;
} }
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 mix(a, b, t) {
return a * t + b * (1 - t);
}
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,
};
const ba = {
'x': bx - ax,
'y': by - ay,
};
const h = clamp(dot(pa, ba) / dot(ba, ba), 0.0, 1.0);
const thickness = mix(at, bt, h);
const v = {
'x': p.x - (ax + ba.x * h),
'y': p.y - (ay + ba.y * h),
};
const dist = Math.sqrt(dot(v, v)) - thickness;
if (dist <= 0) {
return true;
} }
} }
return result; return false;
} }
function dist_v2(a, b) { function segment_interesects_quad(a, b, quad_topleft, quad_bottomright, quad_topright, quad_bottomleft) {
const dx = a.x - b.x; if (point_in_quad(a, quad_topleft, quad_bottomright)) {
const dy = a.y - b.y; return true;
return Math.sqrt(dx * dx + dy * dy); }
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 mid_v2(a, b) { 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;
}
function quad_fully_inside(outer, inner) {
if (outer.x1 < inner.x1 && outer.x2 > inner.x2 && outer.y1 < inner.y1 && outer.y2 > inner.y2) {
return true;
}
return false;
}
function quad_union(a, b) {
return { return {
'x': (a.x + b.x) / 2.0, 'x1': Math.min(a.x1, b.x1),
'y': (a.y + b.y) / 2.0, 'y1': Math.min(a.y1, b.y1),
'x2': Math.max(a.x2, b.x2),
'y2': Math.max(a.y2, b.y2),
}; };
} }
function perpendicular(ax, ay, bx, by, width) { function box_area(box) {
// Place points at (stroke_width / 2) distance from the line return (box.x2 - box.x1) * (box.y2 - box.y1);
const dirx = bx - ax; }
const diry = by - ay;
// 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;
}
let pdirx = diry; function random_bright_color_from_seed(seed) {
let pdiry = -dirx; const h = Math.round(mulberry32(seed) * 360);
const s = 25;
const l = 50;
const pdir_norm = Math.sqrt(pdirx * pdirx + pdiry * pdiry); return `hsl(${h}deg ${s}% ${l}%)`;
}
pdirx /= pdir_norm; function dot(a, b) {
pdiry /= pdir_norm; return a.x * b.x + a.y * b.y;
}
return { function clamp(x, a, b) {
'p1': { return x < a ? a : (x > b ? b : x);
'x': ax + pdirx * width / 2, }
'y': ay + pdiry * width / 2,
}, 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;
}
'p2': { function stroke_intersects_capsule(state, stroke, a, b, radius) {
'x': ax - pdirx * width / 2, const xs = state.wasm.buffers['xs'].tv.data;
'y': ay - pdiry * width / 2, 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);
// 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];
}
function estimate_next_point1(ts, xs, ys, dt=0) {
const N = ts.length;
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 = [];
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);
}
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

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

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

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

136
client/tools.js

@ -2,6 +2,7 @@ function switch_tool(state, item) {
const tool = item.getAttribute('data-tool'); const tool = item.getAttribute('data-tool');
if (tool === 'undo') { if (tool === 'undo') {
queue_event(state, undo_event(state));
return; return;
} }
@ -9,55 +10,93 @@ function switch_tool(state, item) {
state.tools.active_element.classList.remove('active'); 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 = tool;
state.tools.active_element = item; state.tools.active_element = item;
state.tools.active_element.classList.add('active'); state.tools.active_element.classList.add('active');
}
function switch_color(state, item) { document.querySelector('canvas').classList.add(new_class);
const color = item.getAttribute('data-color');
if (tool === 'pencil' || tool === 'eraser' || tool === 'ruler') {
update_cursor(state);
document.querySelector('.brush-dom').classList.remove('dhide');
} else {
document.querySelector('.brush-dom').classList.add('dhide');
}
}
function select_color(state, item, color_u32) {
if (state.colors.active_element) { if (state.colors.active_element) {
state.colors.active_element.classList.remove('active'); state.colors.active_element.classList.remove('active');
} }
if (state.me in state.players) { if (state.colors.extended_element) {
const color_u32 = color_to_u32(color); state.colors.extended_element.classList.remove('extended');
state.players[state.me].color = color_u32 state.colors.extended_element = null;
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; state.colors.active_element = item;
state.colors.active_element.classList.add('active'); item.classList.add('active');
} }
function show_stroke_preview(state, size) { function extend_major_color(state, item) {
const preview = document.querySelector('#stroke-preview'); 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;
preview.style.width = size * state.canvas.zoom + 'px'; // Restore last pane color in case it was overwritten by active color
preview.style.height = size * state.canvas.zoom + 'px'; last_minor.querySelector('.color-pane').style.background = '#' + item.getAttribute('data-last-color');
preview.style.background = color_from_u32(state.players[state.me].color); last_minor.setAttribute('data-color', item.getAttribute('data-last-color'));
preview.classList.remove('dhide'); state.colors.extended_element = item;
item.classList.add('extended');
} }
function hide_stroke_preview() { function set_color_u32(state, color_u32) {
document.querySelector('#stroke-preview').classList.add('dhide'); 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 switch_stroke_width(e, state) { function switch_stroke_width(e, state) {
if (!state.online) return; if (!state.online) return;
const value = e.target.value; const value = parseInt(e.target.value);
state.players[state.me].width = value; state.players[state.me].width = value;
show_stroke_preview(state, value); update_cursor(state);
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) { function broadcast_stroke_width(e, state) {
@ -67,14 +106,55 @@ function broadcast_stroke_width(e, state) {
function init_tools(state) { function init_tools(state) {
const tools = document.querySelectorAll('.tools .tool'); const tools = document.querySelectorAll('.tools .tool');
const colors = document.querySelectorAll('.pallete .color'); const color_groups = document.querySelectorAll('.pallete .color-major');
tools.forEach((item) => { item.addEventListener('click', () => switch_tool(state, item)); }); tools.forEach((item) => { item.addEventListener('click', () => switch_tool(state, item)); });
colors.forEach((item) => { item.addEventListener('click', () => switch_color(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);
})
});
// TODO: from localstorage
switch_tool(state, document.querySelector('.tool[data-tool="pencil"]')); switch_tool(state, document.querySelector('.tool[data-tool="pencil"]'));
switch_color(state, document.querySelector('.color[data-color="000000"]'));
const slider = document.querySelector('#stroke-width'); const slider = document.querySelector('#stroke-width');

5
client/touch.css

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

359
client/touch.js

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

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

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

@ -0,0 +1,419 @@
#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.

682
client/webgl_draw.js

@ -1,92 +1,664 @@
function schedule_draw(state, context) { function schedule_draw(state, context, animate = false) {
if (!state.timers.raf) { if (!state.timers.raf) {
window.requestAnimationFrame(() => draw(state, context)); window.requestAnimationFrame(async (ts) => {
await draw(state, context, animate, ts);
});
state.timers.raf = true; state.timers.raf = true;
} }
} }
function draw(state, context) { function upload_if_needed(gl, buffer_kind, serializer) {
state.timers.raf = false; 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;
const gl = context.gl; const gl = context.gl;
const width = window.innerWidth; const width = window.innerWidth;
const height = window.innerHeight; const height = window.innerHeight;
let locations; bvh_clip(state, context);
let buffers;
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.viewport(0, 0, context.canvas.width, context.canvas.height);
gl.clearColor(context.bgcolor.r, context.bgcolor.g, context.bgcolor.b, 1); gl.clearColor(context.bgcolor.r, context.bgcolor.g, context.bgcolor.b, 1);
gl.clear(gl.COLOR_BUFFER_BIT); gl.clearDepth(0.0);
gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT);
const locations = context.locations;
const buffers = context.buffers;
const programs = context.programs;
const textures = context.textures;
// Draw the background pattern
if (state.background_pattern === 'dots') {
const pr = programs['dots'];
gl.useProgram(pr.program);
gl.bindBuffer(gl.ARRAY_BUFFER, buffers['b_instance_dot']);
gl.enableVertexAttribArray(pr.locations['a_center']);
gl.vertexAttribPointer(pr.locations['a_center'], 2, gl.FLOAT, false, 2 * 4, 0);
gl.vertexAttribDivisor(pr.locations['a_center'], 1);
gl.uniform2f(pr.locations['u_res'], context.canvas.width, context.canvas.height);
gl.uniform2f(pr.locations['u_scale'], state.canvas.zoom, state.canvas.zoom);
gl.uniform2f(pr.locations['u_translation'], state.canvas.offset.x, state.canvas.offset.y);
const zoom = state.canvas.zoom;
const zoom_log2 = Math.log2(zoom);
const zoom_previous = Math.pow(2, Math.floor(zoom_log2));
const zoom_next = Math.pow(2, Math.ceil(zoom_log2));
// Previous level
{
const one_dot = new Float32Array(geometry_gen_quad(0, 0, 1 / zoom_previous));
const dot_instances = new Float32Array(geometry_gen_fullscreen_grid(state, context, 32 / zoom_previous, 32 / zoom_previous));
const t = Math.min(1.0, 1.0 - (zoom / zoom_previous) / 2.0);
gl.uniform1f(pr.locations['u_fadeout'], t);
gl.bindBuffer(gl.ARRAY_BUFFER, buffers['b_instance_dot']);
gl.bufferData(gl.ARRAY_BUFFER, dot_instances, gl.STREAM_DRAW);
gl.drawArraysInstanced(gl.TRIANGLES, 0, 6, dot_instances.length / 2);
}
// Next level
if (zoom_previous != zoom_next) {
const dot_instances = new Float32Array(geometry_gen_fullscreen_grid(state, context, 32 / zoom_next, 32 / zoom_next));
const t = Math.min(1.0, 1.0 - (zoom_next / zoom) / 2.0);
gl.uniform1f(pr.locations['u_fadeout'], t);
gl.bindBuffer(gl.ARRAY_BUFFER, buffers['b_instance_dot']);
gl.bufferData(gl.ARRAY_BUFFER, dot_instances, gl.STREAM_DRAW);
gl.drawArraysInstanced(gl.TRIANGLES, 0, 6, dot_instances.length / 2);
}
} else if (state.background_pattern === 'grid') {
const pr = programs['grid'];
const zoom = state.canvas.zoom;
let zoom_log8 = Math.log(zoom) / Math.log(8);
//if (zoom_log2 === Math.floor(zoom_log2)) {
// zoom_log2 -= 0.001;
//}
const zoom_previous = Math.pow(8, Math.floor(zoom_log8));
let zoom_next = Math.pow(8, Math.ceil(zoom_log8));
if (zoom_next === zoom_previous) {
zoom_next = zoom_previous * 8;
}
gl.useProgram(pr.program);
gl.bindBuffer(gl.ARRAY_BUFFER, buffers['b_instance_grid']);
gl.enableVertexAttribArray(pr.locations['a_data']);
gl.vertexAttribPointer(pr.locations['a_data'], 2, gl.FLOAT, false, 2 * 4, 0);
gl.vertexAttribDivisor(pr.locations['a_data'], 1);
gl.uniform2f(pr.locations['u_res'], context.canvas.width, context.canvas.height);
gl.uniform2f(pr.locations['u_scale'], state.canvas.zoom, state.canvas.zoom);
gl.uniform2f(pr.locations['u_translation'], state.canvas.offset.x, state.canvas.offset.y);
gl.uniform1f(pr.locations['u_fadeout'], 1.0);
// Previous level (major lines)
{
const grid_instances = new Float32Array(geometry_gen_fullscreen_grid_1d(state, context, 32 / zoom_previous, 32 / zoom_previous));
let t = (zoom / zoom_previous - 1) / -7 + 1;
t = 0.25;
gl.uniform1f(pr.locations['u_fadeout'], t);
gl.bindBuffer(gl.ARRAY_BUFFER, buffers['b_instance_grid']);
gl.bufferData(gl.ARRAY_BUFFER, grid_instances, gl.STREAM_DRAW);
gl.drawArraysInstanced(gl.TRIANGLES, 0, 6, grid_instances.length / 2);
}
// Next level (minor lines)
{
const grid_instances = new Float32Array(geometry_gen_fullscreen_grid_1d(state, context, 32 / zoom_next, 32 / zoom_next));
let t = (zoom_next / zoom - 1) / 7;
t = Math.min(0.1, -t + 1); // slight fade-in
gl.uniform1f(pr.locations['u_fadeout'], t);
gl.bindBuffer(gl.ARRAY_BUFFER, buffers['b_instance_grid']);
gl.bufferData(gl.ARRAY_BUFFER, grid_instances, gl.STREAM_DRAW);
gl.drawArraysInstanced(gl.TRIANGLES, 0, 6, grid_instances.length / 2);
}
}
// Images // Images
locations = context.locations['quad']; {
buffers = context.buffers['quad']; 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);
gl.useProgram(context.programs['quad']); 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);
gl.enableVertexAttribArray(locations['a_pos']); draw_strokes(state, context.canvas.width, context.canvas.height, programs, gl, lod_levels, ui_segments,
gl.enableVertexAttribArray(locations['a_texcoord']); 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,
);
}
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);
}
gl.uniform2f(locations['u_res'], context.canvas.width, context.canvas.height); const quad_count = bboxes.size / 4;
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_pos']); gl.useProgram(pr.program);
gl.vertexAttribPointer(locations['a_pos'], 2, gl.FLOAT, false, 0, 0);
gl.bufferData(gl.ARRAY_BUFFER, context.quad_positions_f32, gl.STATIC_DRAW);
gl.bindBuffer(gl.ARRAY_BUFFER, buffers['b_texcoord']); gl.bindBuffer(gl.ARRAY_BUFFER, buffers['b_iquads']);
gl.vertexAttribPointer(locations['a_texcoord'], 2, gl.FLOAT, false, 0, 0); gl.bufferData(gl.ARRAY_BUFFER, tv_data(bboxes), gl.STREAM_DRAW);
gl.bufferData(gl.ARRAY_BUFFER, context.quad_texcoords_f32, gl.STATIC_DRAW);
const count = Object.keys(context.textures).length; gl.uniform2f(pr.locations['u_res'], context.canvas.width, context.canvas.height);
let active_image_index = -1; 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(locations['u_outline'], 0); gl.enableVertexAttribArray(pr.locations['a_topleft']);
gl.enableVertexAttribArray(pr.locations['a_bottomright']);
for (let key = 0; key < count; ++key) { gl.vertexAttribPointer(pr.locations['a_topleft'], 2, gl.FLOAT, false, 4 * 4, 0);
if (context.textures[key].image_id === context.active_image) { gl.vertexAttribPointer(pr.locations['a_bottomright'], 2, gl.FLOAT, false, 4 * 4, 2 * 4);
active_image_index = key;
continue; 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); if (config.draw_fullnodes) {
gl.drawArrays(gl.TRIANGLES, key * 6, 6); 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);
} }
if (active_image_index !== -1) { const quad_count = bboxes.size / 4;
gl.uniform1i(locations['u_outline'], 1);
gl.bindTexture(gl.TEXTURE_2D, context.textures[active_image_index].texture); gl.useProgram(pr.program);
gl.drawArrays(gl.TRIANGLES, active_image_index * 6, 6);
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);
} }
// Strokes document.getElementById('debug-stats').innerHTML = `
locations = context.locations['stroke']; <span>Strokes onscreen: ${context.clipped_indices.size}</span>
buffers = context.buffers['stroke']; <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>`;
gl.useProgram(context.programs['stroke']); if (context.gpu_timer_ext) {
gl.endQuery(context.gpu_timer_ext.TIME_ELAPSED_EXT);
gl.enableVertexAttribArray(locations['a_type']); const next_tick = () => {
gl.enableVertexAttribArray(locations['a_pos']); if (query) {
gl.enableVertexAttribArray(locations['a_texcoord']); // At some point in the future, after returning control to the browser
gl.enableVertexAttribArray(locations['a_color']); const available = gl.getQueryParameter(query, gl.QUERY_RESULT_AVAILABLE);
const disjoint = gl.getParameter(context.gpu_timer_ext.GPU_DISJOINT_EXT);
gl.uniform2f(locations['u_res'], context.canvas.width, context.canvas.height); if (available && !disjoint) {
gl.uniform2f(locations['u_scale'], state.canvas.zoom, state.canvas.zoom); // See how much time the rendering of the object took in nanoseconds.
gl.uniform2f(locations['u_translation'], state.canvas.offset.x, state.canvas.offset.y); 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';
}
gl.bindBuffer(gl.ARRAY_BUFFER, buffers['b_packed']); 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);
}
}
gl.vertexAttribPointer(locations['a_pos'], 2, gl.FLOAT, false, config.bytes_per_point, 0); if (state.canvas.target_zoom != state.canvas.zoom) {
gl.vertexAttribPointer(locations['a_texcoord'], 2, gl.FLOAT, false, config.bytes_per_point, 8); update_canvas_zoom(state, state.canvas.zoom, state.canvas.target_zoom, animate ? dt : 0);
gl.vertexAttribPointer(locations['a_color'], 3, gl.UNSIGNED_BYTE, true, config.bytes_per_point, 16); if (!external_draw) {
gl.vertexAttribPointer(locations['a_type'], 1, gl.UNSIGNED_BYTE, false, config.bytes_per_point, 19); schedule_draw(state, context, true);
}
}
}
// 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;
if (state.zoomdown) {
decay *= config.vertical_zoom_speed_multiplier; // to make it feel more responsive at fast speed
}
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.bufferData(gl.ARRAY_BUFFER, context.static_stroke_serializer.buffer, gl.STATIC_DRAW); // https://gist.github.com/aolo2/a373363419bd5a9283977ab9f8841f78
gl.drawArrays(gl.TRIANGLES, 0, context.static_stroke_serializer.offset / config.bytes_per_point); 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.dynamic_stroke_serializer.buffer, gl.STATIC_DRAW); update_cursor(state);
gl.drawArrays(gl.TRIANGLES, 0, context.dynamic_stroke_serializer.offset / config.bytes_per_point);
} }

905
client/webgl_geometry.js

@ -1,244 +1,829 @@
function push_point(s, x, y, u, v, r, g, b, type) { function geometry_prepare_stroke(state) {
ser_f32(s, x); if (!state.online) {
ser_f32(s, y); return null;
ser_f32(s, u); }
ser_f32(s, v);
// ser_u8(s, Math.floor(Math.random() * 255)); const player = state.players[state.me];
// ser_u8(s, Math.floor(Math.random() * 255)); const stroke = player.strokes[player.strokes.length - 1]; // MY OWN player.strokes should never be bigger than 1 element
// ser_u8(s, Math.floor(Math.random() * 255));
ser_u8(s, r); if (stroke.points.length === 0) {
ser_u8(s, g); return null;
ser_u8(s, b); }
ser_u8(s, type);
} const points = process_stroke2(state.canvas.zoom, stroke.points);
function push_circle(s, cx, cy, radius, r, g, b) { return {
push_point(s, cx - radius, cy - radius, 0, 0, r, g, b, 1); 'color': stroke.color,
push_point(s, cx - radius, cy + radius, 0, 1, r, g, b, 1); 'width': stroke.width,
push_point(s, cx + radius, cy - radius, 1, 0, r, g, b, 1); 'points': points,
'user_id': state.me,
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);
} async function geometry_write_instances(state, context, callback) {
state.stats.rdp_max_count = 0;
function push_quad(s, p1x, p1y, p2x, p2y, p3x, p3y, p4x, p4y, r, g, b) { state.stats.rdp_segments = 0;
push_point(s, p1x, p1y, 0, 0, r, g, b, 0);
push_point(s, p2x, p2y, 0, 1, r, g, b, 0); const segment_count = await do_lod(state, context);
push_point(s, p3x, p3y, 1, 0, r, g, b, 0);
if (config.debug_print) console.debug('instances:', segment_count, 'rdp max:', state.stats.rdp_max_count, 'rdp segments:', state.stats.rdp_segments);
push_point(s, p4x, p4y, 1, 1, r, g, b, 0);
push_point(s, p3x, p3y, 1, 0, r, g, b, 0); return segment_count;
push_point(s, p2x, p2y, 0, 1, r, g, b, 0); }
}
function geometry_add_dummy_stroke(state, context) {
function push_stroke(s, stroke) { context.stroke_data = ser_ensure_by(context.stroke_data, config.bytes_per_stroke);
const stroke_width = stroke.width; ser_u16(context.stroke_data, 0);
const points = stroke.points; 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);
const color_u32 = stroke.color; 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 r = (color_u32 >> 16) & 0xFF;
const g = (color_u32 >> 8) & 0xFF; const g = (color_u32 >> 8) & 0xFF;
const b = color_u32 & 0xFF; const b = color_u32 & 0xFF;
if (points.length === 0) { ser_u16(context.dynamic_stroke_data, r);
return; 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;
} }
if (points.length === 1) { batch_size += stroke.points.length;
push_circle(s, points[0].x, points[0].y, stroke_width / 2, r, g, b); last_lod = lod;
return;
stroke_index += 1; // TODO: proper player Z order
}
}
} }
for (let i = 0; i < points.length - 1; ++i) { if (batch_size > 0) {
const px = points[i].x; tv_add(context.dynamic_instance_batches, batch_size);
const py = points[i].y; tv_add(context.dynamic_instance_batches, last_lod);
}
const nextpx = points[i + 1].x; context.dynamic_segment_count = total_points;
const nextpy = points[i + 1].y; context.dynamic_stroke_count = total_strokes;
const d1x = nextpx - px; let batch_base = 0;
const d1y = nextpy - py;
// Perpendicular to (d1x, d1y), points to the LEFT for (let i = 0; i < context.dynamic_instance_batches.size; i += 2) {
let perp1x = -d1y; const nbatches = context.dynamic_instance_batches.data[i * 2 + 0];
let perp1y = d1x; context.dynamic_instance_batches.data[i * 2 + 0] = batch_base;
batch_base += nbatches;
}
}
const perpnorm1 = Math.sqrt(perp1x * perp1x + perp1y * perp1y); 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;
}
perp1x /= perpnorm1; return lod;
perp1y /= perpnorm1; }
function geometry_start_prestroke(state, player_id) {
if (!state.online) return;
const s1x = px + perp1x * stroke_width / 2; const player = state.players[player_id];
const s1y = py + perp1y * stroke_width / 2;
const s2x = px - perp1x * stroke_width / 2;
const s2y = py - perp1y * stroke_width / 2;
const s3x = nextpx + perp1x * stroke_width / 2; player.strokes.push({
const s3y = nextpy + perp1y * stroke_width / 2; 'empty': false,
const s4x = nextpx - perp1x * stroke_width / 2; 'points': [],
const s4y = nextpy - perp1y * stroke_width / 2; 'raw_points': [],
'head': null,
'color': player.color,
'width': player.width,
});
push_quad(s, s2x, s2y, s1x, s1y, s4x, s4y, s3x, s3y, r, g, b); player.current_prestroke = true;
push_circle(s, px, py, stroke_width / 2, r, g, b); }
function geometry_end_prestroke(state, player_id) {
if (!state.online) return;
const player = state.players[player_id];
player.current_prestroke = false;
}
function geometry_add_prepoint(state, context, player_id, point, is_pen, raw = false) {
if (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);
} }
}
function geometry_add_prepoint_demetri(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];
stroke.raw_points.push(point);
stroke.points = smooth_curve(stroke.raw_points);
const lastp = points[points.length - 1]; if (point.pressure < config.min_pressure) {
point.pressure = config.min_pressure;
}
push_circle(s, lastp.x, lastp.y, stroke_width / 2, r, g, b); recompute_dynamic_data(state, context);
} }
function geometry_prepare_stroke(state) { function geometry_add_prepoint_raw(state, context, player_id, point, is_pen, raw = false) {
if (!state.online) { if (!state.online) return;
return null;
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;
} }
return { points.push(point);
'color': state.players[state.me].color,
'width': state.players[state.me].width, recompute_dynamic_data(state, context);
'points': process_stroke(state, state.players[state.me].points),
'user_id': state.me,
};
} }
function geometry_add_stroke(state, context, stroke) { function average_screen_speed(state, points, last, range=10) {
if (!state.online || !stroke) return; let screen_last = canvas_to_screen(state, points[points.length - 1]);
let sum_speed = 0;
let n = 0;
const bytes_left = context.static_stroke_serializer.size - context.static_stroke_serializer.offset; for (let i = points.length - 1; i >= 0 && i >= points.length - range; --i) {
const bytes_needed = (stroke.points.length * 12 + 6) * config.bytes_per_point; 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);
if (bytes_left < bytes_needed) { sum_speed += screen_speed;
const old_view = context.static_stroke_serializer.strview; n++;
const old_offset = context.static_stroke_serializer.offset;
const new_size = Math.ceil((context.static_stroke_serializer.size + bytes_needed) * 1.62); screen_last = screen_this;
}
context.static_stroke_serializer = serializer_create(new_size); if (n === 0) {
context.static_stroke_serializer.strview.set(old_view); return 0;
context.static_stroke_serializer.offset = old_offset;
} }
push_stroke(context.static_stroke_serializer, stroke); return sum_speed / n;
} }
function recompute_dynamic_data(state, context) { function geometry_add_prepoint_new(state, context, player_id, point, is_pen, raw = false) {
let bytes_needed = 0; if (!state.online) return;
for (const player_id in state.players) {
const player = state.players[player_id]; const player = state.players[player_id];
if (player.points.length > 0) { const stroke = player.strokes[player.strokes.length - 1];
bytes_needed += (player.points.length * 12 + 6) * config.bytes_per_point; const points = stroke.points;
} const raw_points = stroke.raw_points;
if (point.pressure < config.min_pressure) {
point.pressure = config.min_pressure;
} }
if (bytes_needed > context.dynamic_stroke_serializer.size) { let avg_window = 0;
context.dynamic_stroke_serializer = serializer_create(Math.ceil(bytes_needed * 1.62));
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;
if (screen_speed <= bot) {
avg_window = max_avg_window;
} else if (screen_speed >= top) {
avg_window = 0;
} else { } else {
context.dynamic_stroke_serializer.offset = 0; const onezero = 1.0 - (screen_speed - bot) / (top - bot);
avg_window = Math.floor(onezero * max_avg_window);
}
} }
for (const player_id in state.players) { avg_window = Math.min(avg_window, raw_points.length);
// player has the same data as their current stroke: points, color, width
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 player = state.players[player_id];
if (player.points.length > 0) { const stroke = player.strokes[player.strokes.length - 1];
push_stroke(context.dynamic_stroke_serializer, player); const points = stroke.points;
if (point.pressure < config.min_pressure) {
point.pressure = config.min_pressure;
} }
if (points.length > 0 && !raw) {
// pulled from "perfect-freehand" package. MIT
// https://github.com/steveruizok/perfect-freehand/
const streamline = 0.75;
const t = 0.15 + (1 - streamline) * 0.85
const smooth_pressure = exponential_smoothing(points, point, 3);
points.push({
'x': stroke.head.x * t + point.x * (1 - t),
'y': stroke.head.y * t + point.y * (1 - t),
'pressure': is_pen ? stroke.head.pressure * t + smooth_pressure * (1 - t) : point.pressure,
});
if (is_pen) {
point.pressure = smooth_pressure;
} }
} else {
points.push(point);
}
stroke.head = point;
recompute_dynamic_data(state, context);
} }
function geometry_add_point(state, context, player_id, point) { // Remove prestroke from dynamic data (usually because it's now a real stroke)
function geometry_clear_oldest_prestroke(state, context, player_id) {
if (!state.online) return; if (!state.online) return;
state.players[player_id].points.push(point);
const player = state.players[player_id];
player.strokes.shift();
if (player.strokes.length === 0) {
player.current_prestroke = false;
}
recompute_dynamic_data(state, context); recompute_dynamic_data(state, context);
} }
function geometry_clear_player(state, context, player_id) { function geometry_clear_newest_prestroke(state, context, player_id) {
if (!state.online) return; if (!state.online) return;
state.players[player_id].points.length = 0;
const player = state.players[player_id];
player.strokes.pop();
if (player.strokes.length === 0) {
player.current_prestroke = false;
}
recompute_dynamic_data(state, context); recompute_dynamic_data(state, context);
} }
function add_image(context, image_id, bitmap, p) { function add_image(context, image_id, bitmap, p, width, height) {
const x = p.x;
const y = p.y;
const gl = context.gl; const gl = context.gl;
const id = Object.keys(context.textures).length; let entry = null;
context.textures[id] = { // If bitmap not available yet - create placeholder
// Otherwise - upload actual bitmap
if (bitmap === null) {
entry = {
'texture': gl.createTexture(), 'texture': gl.createTexture(),
'image_id': image_id 'key': image_id,
'at': {...p},
'raw_at': {...p},
'width': width,
'height': height,
'transform_history': [ p.x, p.y, width, height ],
'transform_head': 4,
}; };
gl.bindTexture(gl.TEXTURE_2D, context.textures[id].texture); context.images.push(entry);
gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA,gl.UNSIGNED_BYTE, bitmap); } else {
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR); 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_S, gl.CLAMP_TO_EDGE);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE); gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);
gl.generateMipmap(gl.TEXTURE_2D);
}
}
context.quad_positions.push(...[ function scale_image(image, corner, canvasp) {
x, y, let new_width, new_height;
x, y + bitmap.height,
x + bitmap.width, y + bitmap.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;
}
x + bitmap.width, y, image.width = new_width;
x, y, image.height = new_height;
x + bitmap.width, y + bitmap.height, }
]);
context.quad_texcoords.push(...[ function image_at(context, x, y) {
0, 0, // Iterate back to front to pick the image at the front first
0, 1, for (let i = context.images.length - 1; i >= 0; --i) {
1, 1, const image = context.images[i];
1, 0, if (!image.deleted) {
0, 0, const at = image.at;
1, 1, 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;
}
context.quad_positions_f32 = new Float32Array(context.quad_positions); function geometry_gen_circle(cx, cy, r, n) {
context.quad_texcoords_f32 = new Float32Array(context.quad_texcoords); const step = 2 * Math.PI / n;
const result = [];
for (let i = 0; i < n; ++i) {
const theta = i * step;
const next_theta = (i < n - 1 ? (i + 1) * step : 0);
const x = cx + r * Math.cos(theta);
const y = cy + r * Math.sin(theta);
const next_x = cx + r * Math.cos(next_theta);
const next_y = cy + r * Math.sin(next_theta);
result.push(cx, cy, x, y, next_x, next_y);
}
return result;
}
function geometry_gen_quad(cx, cy, r) {
const result = [
cx - r,
cy - r,
cx + r,
cy - r,
cx - r,
cy + r,
cx + r,
cy + r,
cx - r,
cy + r,
cx + r,
cy - r,
];
return result;
} }
function move_image(context, image_event) { function geometry_gen_fullscreen_grid(state, context, step_x, step_y) {
const x = image_event.x; const result = [];
const y = image_event.y; 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});
const count = Object.keys(context.textures).length; topleft.x = Math.floor(topleft.x / step_x) * step_x;
topleft.y = Math.ceil(topleft.y / step_y) * step_y;
for (let id = 0; id < count; ++id) { bottomright.x = Math.floor(bottomright.x / step_x) * step_x;
const image = context.textures[id]; bottomright.y = Math.ceil(bottomright.y / step_y) * step_y;
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;
context.quad_positions[id * 12 + 6] = x + image_event.width; for (let y = topleft.y; y <= bottomright.y; y += step_y) {
context.quad_positions[id * 12 + 7] = y; for (let x = topleft.x; x <= bottomright.x; x += step_x) {
context.quad_positions[id * 12 + 8] = x; result.push(x, y);
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;
return result;
}
context.quad_positions_f32 = new Float32Array(context.quad_positions); function geometry_gen_fullscreen_grid_1d(state, context, step_x, step_y) {
const result = [];
const width = context.canvas.width;
const height = context.canvas.height;
const topleft = screen_to_canvas(state, {'x': 0, 'y': 0});
const bottomright = screen_to_canvas(state, {'x': width, 'y': height});
topleft.x = Math.floor(topleft.x / step_x) * step_x;
topleft.y = Math.floor(topleft.y / step_y) * step_y;
bottomright.x = Math.ceil(bottomright.x / step_x) * step_x;
bottomright.y = Math.ceil(bottomright.y / step_y) * step_y;
for (let x = topleft.x; x <= bottomright.x; x += step_x) {
result.push(1, x);
}
for (let y = topleft.y; y <= bottomright.y; y += step_y) {
result.push(-1, y);
}
return result;
}
function geometry_image_quads(state, context) {
const result = new Float32Array(context.images.length * 12);
for (let i = 0; i < context.images.length; ++i) {
const entry = context.images[i];
result[i * 12 + 0] = entry.at.x;
result[i * 12 + 1] = entry.at.y;
result[i * 12 + 2] = entry.at.x + entry.width;
result[i * 12 + 3] = entry.at.y;
result[i * 12 + 4] = entry.at.x;
result[i * 12 + 5] = entry.at.y + entry.height;
result[i * 12 + 6] = entry.at.x + entry.width;
result[i * 12 + 7] = entry.at.y + entry.height;
result[i * 12 + 8] = entry.at.x;
result[i * 12 + 9] = entry.at.y + entry.height;
result[i * 12 + 10] = entry.at.x + entry.width;
result[i * 12 + 11] = entry.at.y;
}
return result;
}
function geometry_generate_handles(state, context, active_image) {
let image = null;
for (const entry of context.images) {
if (entry.key === active_image) {
image = entry;
break; 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 image_at(state, x, y) { function geometry_line_segments_with_two_circles(circle_segments) {
for (let i = state.events.length - 1; i >= 0; --i) { const results = new Float32Array((circle_segments * 3 + 6) * 2); // triangle fan circle + two triangles, all 2D (x + y)
const event = state.events[i];
if (event.type === EVENT.IMAGE && !event.deleted) { // Generate circle as triangle fan at 0, 0 with radius 1
if ('height' in event && 'width' in event) { // This circle will be offset/scaled in the vertex shader
if (event.x <= x && x <= event.x + event.width && event.y <= y && y <= event.y + event.height) { let last_phi = ((circle_segments - 1) / circle_segments) * 2 * Math.PI;
return event; 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++;
} }
} }
} }
} }
return null; 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
};
} }

923
client/webgl_listeners.js

File diff suppressed because it is too large Load Diff

413
client/webgl_shaders.js

@ -1,140 +1,400 @@
const stroke_vs_src = ` const sdf_vs_src = `#version 300 es
attribute float a_type; in vec2 a_pos; // for the joins/caps these are relative positions. for line segments these are dummy values
attribute vec2 a_pos; in vec2 a_a; // point from
attribute vec2 a_texcoord; in vec2 a_b; // point to
attribute vec3 a_color;
in int a_stroke_id;
in vec2 a_pressure;
uniform vec2 u_scale; uniform vec2 u_scale;
uniform vec2 u_res; uniform vec2 u_res;
uniform vec2 u_translation; 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;
varying vec3 v_color; out float v_scalefade;
varying vec2 v_texcoord;
varying float v_type;
void main() { void main() {
vec2 screen01 = (a_pos * u_scale + u_translation) / u_res; vec2 screen02;
vec2 screen02 = screen01 * 2.0;
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;
}
}
screen02 = (pos.xy * u_scale + u_translation) / u_res * 2.0;
screen02.y = 2.0 - screen02.y; screen02.y = 2.0 - screen02.y;
v_color = a_color; v_color = vec3(stroke_data.xyz) / 255.0;
v_texcoord = a_texcoord;
v_type = a_type; if (a_stroke_id >> 31 != 0 && (vertex_index >= u_circle_points)) {
screen02 += vec2(100.0); // shift offscreen
}
gl_Position = vec4(screen02 - 1.0, 0, 1); gl_Position = vec4(screen02 - 1.0, (float(stroke_index + 1) / float(u_stroke_count + 1)) * 2.0 - 1.0, 1.0);
} }
`; `;
const stroke_fs_src = ` const sdf_fs_src = `#version 300 es
#extension GL_OES_standard_derivatives : enable precision highp float;
precision mediump float; uniform int u_debug_mode;
uniform vec3 u_debug_color;
uniform float u_opacity_multipliter;
varying vec3 v_color; in vec3 v_color;
varying vec2 v_texcoord; in float v_scalefade;
varying float v_type;
void main() { layout(location = 0) out vec4 FragColor;
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); 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);
}
} }
`; `;
const tquad_vs_src = ` const tquad_vs_src = `#version 300 es
attribute vec2 a_pos; in vec2 a_pos;
attribute vec2 a_texcoord;
uniform vec2 u_scale; uniform vec2 u_scale;
uniform vec2 u_res; uniform vec2 u_res;
uniform vec2 u_translation; uniform vec2 u_translation;
varying vec2 v_texcoord; out vec2 v_texcoord;
void main() { void main() {
vec2 screen01 = (a_pos * u_scale + u_translation) / u_res; vec2 screen01 = (a_pos * u_scale + u_translation) / u_res;
vec2 screen02 = screen01 * 2.0; 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; screen02.y = 2.0 - screen02.y;
vec2 screen11 = screen02 - 1.0; vec2 screen11 = screen02 - 1.0;
v_texcoord = a_texcoord;
gl_Position = vec4(screen11, 0, 1); gl_Position = vec4(screen11, 0, 1);
} }
`; `;
const tquad_fs_src = ` const tquad_fs_src = `#version 300 es
precision mediump float; precision highp float;
varying vec2 v_texcoord; in vec2 v_texcoord;
uniform sampler2D u_texture; uniform sampler2D u_texture;
uniform bool u_outline; uniform int u_solid;
uniform vec4 u_color;
layout(location = 0) out vec4 FragColor;
void main() {
if (u_solid == 0) {
FragColor = texture(u_texture, v_texcoord);
} else {
FragColor = u_color;
}
}
`;
const grid_vs_src = `#version 300 es
in vec2 a_data; // per-instance
out float v_fadeout;
uniform vec2 u_scale;
uniform vec2 u_res;
uniform vec2 u_translation;
uniform float u_fadeout;
void main() { void main() {
if (!u_outline) { vec2 origin;
gl_FragColor = texture2D(u_texture, v_texcoord); vec2 minor_offset;
vec2 major_offset;
vec2 pixel = 2.0 / u_res;
if (a_data.x > 0.0) {
// Vertical, treat Y as X
float x = a_data.y;
origin = vec2(x, 0.0);
minor_offset = pixel * vec2(1.0, 0.0);
major_offset = vec2(0.0, 2.0);
} else { } else {
gl_FragColor = mix(texture2D(u_texture, v_texcoord), vec4(0.7, 0.7, 0.95, 1), 0.5); // Horizontal, treat Y as Y
float y = a_data.y;
origin = vec2(0.0, y);
minor_offset = pixel * vec2(0.0, 1.0);
major_offset = vec2(2.0, 0.0);
} }
vec2 v = (origin * u_scale + u_translation) / u_res * 2.0;
vec2 pos;
if (a_data.x > 0.0) {
v.y = 0.0;
} else {
v.x = 0.0;
}
if (gl_VertexID % 6 == 0) {
pos = v;
} else if (gl_VertexID % 6 == 1 || gl_VertexID % 6 == 5) {
pos = v + (a_data.x > 0.0 ? minor_offset : major_offset);
//pos = v + minor_offset;
} else if (gl_VertexID % 6 == 2 || gl_VertexID % 6 == 4) {
pos = v + (a_data.x > 0.0 ? major_offset : minor_offset);
//pos = v + major_offset;
} else if (gl_VertexID % 6 == 3) {
pos = v + major_offset + minor_offset;
//pos = v + major_offset + minor_offset;
}
vec2 screen02 = pos;
screen02.y = 2.0 - screen02.y;
v_fadeout = u_fadeout;
gl_Position = vec4(screen02 - 1.0, 0.0, 1.0);
}
`;
const dots_vs_src = `#version 300 es
in vec2 a_center; // per-instance
out float v_fadeout;
uniform vec2 u_scale;
uniform vec2 u_res;
uniform vec2 u_translation;
uniform float u_fadeout;
void main() {
vec2 v = (a_center * u_scale + u_translation) / u_res * 2.0;
vec2 pos;
vec2 pixel = 2.0 / u_res;
if (gl_VertexID % 6 == 0) {
pos = v + pixel * vec2(-1.0);
} else if (gl_VertexID % 6 == 1) {
pos = v + pixel * vec2(1.0, -1.0);
} else if (gl_VertexID % 6 == 2) {
pos = v + pixel * vec2(-1.0, 1.0);
} else if (gl_VertexID % 6 == 3) {
pos = v + pixel * vec2(1.0);
} else if (gl_VertexID % 6 == 4) {
pos = v + pixel * vec2(-1.0, 1.0);
} else if (gl_VertexID % 6 == 5) {
pos = v + pixel * vec2(1.0, -1.0);
}
vec2 screen02 = pos;
screen02.y = 2.0 - screen02.y;
v_fadeout = u_fadeout;
gl_Position = vec4(screen02 - 1.0, 0.0, 1.0);
}
`;
const dots_fs_src = `#version 300 es
precision highp float;
in float v_fadeout;
layout(location = 0) out vec4 FragColor;
void main() {
vec3 color = vec3(0.5);
FragColor = vec4(color * v_fadeout, v_fadeout);
}
`;
//
const iquad_vs_src = `#version 300 es
in vec2 a_topleft; // per-instance
in vec2 a_bottomright; // per-instance
uniform vec2 u_scale;
uniform vec2 u_res;
uniform vec2 u_translation;
out vec3 v_color;
void main() {
vec2 pos;
int vertex_index = gl_VertexID % 6;
if (vertex_index == 0) {
// top left
pos = a_topleft;
} else if (vertex_index == 1 || vertex_index == 5) {
// top right
pos = vec2(a_bottomright.x, a_topleft.y);
} else if (vertex_index == 2 || vertex_index == 4) {
// bottom left
pos = vec2(a_topleft.x, a_bottomright.y);
} else {
// bottom right
pos = a_bottomright;
}
v_color = vec3(
float(int(a_topleft.x) * 908125 % 255) / 255.0,
float(int(a_topleft.y) * 257722 % 255) / 255.0,
float(int(a_bottomright.y) * 826586 % 255) / 255.0
);
vec2 screen01 = (pos * u_scale + u_translation) / u_res;
vec2 screen02 = screen01 * 2.0;
screen02.y = 2.0 - screen02.y;
vec2 screen11 = screen02 - 1.0;
gl_Position = vec4(screen11, 0.0, 1.0);
}
`;
const iquad_fs_src = `#version 300 es
precision highp float;
layout(location = 0) out vec4 FragColor;
in vec3 v_color;
void main() {
FragColor = vec4(v_color, 0.5);
} }
`; `;
function init_webgl(state, context) { function init_webgl(state, context) {
context.canvas = document.querySelector('#c'); context.canvas = document.querySelector('#c');
context.gl = context.canvas.getContext('webgl', { context.gl = context.canvas.getContext('webgl2', {
'preserveDrawingBuffer': true, 'preserveDrawingBuffer': true,
'desynchronized': true, 'desynchronized': true,
'antialias': false, 'antialias': true,
}); });
const gl = context.gl; const gl = context.gl;
gl.enable(gl.BLEND); gl.enable(gl.BLEND);
gl.blendFunc(gl.ONE, gl.ONE_MINUS_SRC_ALPHA); gl.blendFunc(gl.ONE, gl.ONE_MINUS_SRC_ALPHA);
gl.getExtension('OES_standard_derivatives');
const stroke_vs = create_shader(gl, gl.VERTEX_SHADER, stroke_vs_src); gl.enable(gl.DEPTH_TEST);
const stroke_fs = create_shader(gl, gl.FRAGMENT_SHADER, stroke_fs_src); gl.depthFunc(gl.NOTEQUAL);
context.gpu_timer_ext = gl.getExtension('EXT_disjoint_timer_query_webgl2');
if (context.gpu_timer_ext === null) {
context.gpu_timer_ext = gl.getExtension('EXT_disjoint_timer_query');
}
const quad_vs = create_shader(gl, gl.VERTEX_SHADER, tquad_vs_src); const quad_vs = create_shader(gl, gl.VERTEX_SHADER, tquad_vs_src);
const quad_fs = create_shader(gl, gl.FRAGMENT_SHADER, tquad_fs_src); const quad_fs = create_shader(gl, gl.FRAGMENT_SHADER, tquad_fs_src);
context.programs['stroke'] = create_program(gl, stroke_vs, stroke_fs); const sdf_vs = create_shader(gl, gl.VERTEX_SHADER, sdf_vs_src);
context.programs['quad'] = create_program(gl, quad_vs, quad_fs); const sdf_fs = create_shader(gl, gl.FRAGMENT_SHADER, sdf_fs_src);
context.locations['stroke'] = { const dots_vs = create_shader(gl, gl.VERTEX_SHADER, dots_vs_src);
'a_type': gl.getAttribLocation(context.programs['stroke'], 'a_type'), const dots_fs = create_shader(gl, gl.FRAGMENT_SHADER, dots_fs_src);
'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'),
'u_res': gl.getUniformLocation(context.programs['stroke'], 'u_res'), const grid_vs = create_shader(gl, gl.VERTEX_SHADER, grid_vs_src);
'u_scale': gl.getUniformLocation(context.programs['stroke'], 'u_scale'),
'u_translation': gl.getUniformLocation(context.programs['stroke'], 'u_translation'),
};
context.locations['quad'] = { const iquad_vs = create_shader(gl, gl.VERTEX_SHADER, iquad_vs_src);
'a_pos': gl.getAttribLocation(context.programs['quad'], 'a_pos'), const iquad_fs = create_shader(gl, gl.FRAGMENT_SHADER, iquad_fs_src);
'a_texcoord': gl.getAttribLocation(context.programs['quad'], 'a_texcoord'),
'u_res': gl.getUniformLocation(context.programs['quad'], 'u_res'), context.programs = {
'u_scale': gl.getUniformLocation(context.programs['quad'], 'u_scale'), 'image': create_program(gl, quad_vs, quad_fs),
'u_translation': gl.getUniformLocation(context.programs['quad'], 'u_translation'), 'main': create_program(gl, sdf_vs, sdf_fs),
'u_outline': gl.getUniformLocation(context.programs['quad'], 'u_outline'), 'dots': create_program(gl, dots_vs, dots_fs),
'u_texture': gl.getUniformLocation(context.programs['quad'], 'u_texture'), 'grid': create_program(gl, grid_vs, dots_fs),
'iquad': create_program(gl, iquad_vs, iquad_fs),
}; };
context.buffers['stroke'] = { context.buffers = {
'b_packed': context.gl.createBuffer(), '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.buffers['quad'] = { context.textures = {
'b_pos': context.gl.createBuffer(), 'stroke_data': gl.createTexture(),
'b_texcoord': context.gl.createBuffer(), 'dynamic_stroke_data': gl.createTexture(),
'ui': gl.createTexture(),
}; };
gl.bindTexture(gl.TEXTURE_2D, context.textures['stroke_data']);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.NEAREST);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.NEAREST);
gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA16UI, config.stroke_texture_size, config.stroke_texture_size, 0, gl.RGBA_INTEGER, gl.UNSIGNED_SHORT, new Uint16Array(config.stroke_texture_size * config.stroke_texture_size * 4)); // fill the whole texture once with zeroes to kill a warning about a partial upload
gl.bindTexture(gl.TEXTURE_2D, context.textures['dynamic_stroke_data']);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.NEAREST);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.NEAREST);
gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA16UI, config.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));
const resize_canvas = (entries) => { const resize_canvas = (entries) => {
// https://www.khronos.org/webgl/wiki/HandlingHighDPI // https://www.khronos.org/webgl/wiki/HandlingHighDPI
const entry = entries[0]; const entry = entries[0];
@ -184,9 +444,26 @@ function create_program(gl, vs, fs) {
gl.linkProgram(program); gl.linkProgram(program);
if (gl.getProgramParameter(program, gl.LINK_STATUS)) { if (gl.getProgramParameter(program, gl.LINK_STATUS)) {
return program; // src: tiny-sdf
// https://github.com/mapbox/tiny-sdf
const wrapper = {program};
const num_attrs = gl.getProgramParameter(program, gl.ACTIVE_ATTRIBUTES);
const num_uniforms = gl.getProgramParameter(program, gl.ACTIVE_UNIFORMS);
wrapper.locations = {};
for (let i = 0; i < num_attrs; i++) {
const attribute = gl.getActiveAttrib(program, i);
wrapper.locations[attribute.name] = gl.getAttribLocation(program, attribute.name);
} }
for (let i = 0; i < num_uniforms; i++) {
const uniform = gl.getActiveUniform(program, i);
wrapper.locations[uniform.name] = gl.getUniformLocation(program, uniform.name);
}
return wrapper;
}
console.error('link:', gl.getProgramInfoLog(program)); console.error('link:', gl.getProgramInfoLog(program));
gl.deleteProgram(program); gl.deleteProgram(program);

6
client/websocket.js

@ -28,7 +28,7 @@ async function ws_connect(state, context, first_connect = false) {
} }
} }
} catch (e) { } 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); 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) {
message_data = await data.arrayBuffer(); message_data = await data.arrayBuffer();
const view = new DataView(message_data); const view = new DataView(message_data);
const d = deserializer_create(message_data, view); const d = deserializer_create(message_data, view);
await handle_message(state, context, d); handle_message(state, context, d);
} else { } else {
/* For all my Safari < 14 bros out there */ /* For all my Safari < 14 bros out there */
const reader = new FileReader(); const reader = new FileReader();
@ -56,7 +56,7 @@ async function on_message(state, context, event) {
message_data = e.target.result; message_data = e.target.result;
const view = new DataView(message_data); const view = new DataView(message_data);
const d = deserializer_create(message_data, view); const d = deserializer_create(message_data, view);
await handle_message(state, context, d); handle_message(state, context, d);
}; };
reader.readAsArrayBuffer(data); reader.readAsArrayBuffer(data);

1
server/config.js

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

BIN
server/data-local.sqlite

Binary file not shown.

60
server/deserializer.js

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

11
server/enums.js

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

2
server/http.js

@ -9,7 +9,7 @@ export async function route(req) {
const desk_id = url.searchParams.get('deskId') || '0'; const desk_id = url.searchParams.get('deskId') || '0';
const formdata = await req.formData(); const formdata = await req.formData();
const file = formdata.get('file'); const file = formdata.get('file');
const image_id = math.fast_random32(); const image_id = math.crypto_random32();
Bun.write(config.IMAGEDIR + '/' + image_id, file); Bun.write(config.IMAGEDIR + '/' + image_id, file);

4
server/math.js

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

85
server/milton.js

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

53
server/recv.js

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

95
server/send.js

@ -7,14 +7,32 @@ import { MESSAGE, SESSION, EVENT } from './enums';
import { sessions, desks } from './storage'; import { sessions, desks } from './storage';
function event_size(event) { function event_size(event) {
let size = 1 + 4; // type + user_id let size = 4 + 4; // type + user_id
switch (event.type) { switch (event.type) {
case EVENT.PREDRAW: { case EVENT.PREDRAW:
case EVENT.MOVE_CURSOR: {
size += 4 * 2; size += 4 * 2;
break; 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: { case EVENT.SET_COLOR: {
size += 4; size += 4;
break; break;
@ -28,15 +46,25 @@ function event_size(event) {
case EVENT.STROKE: { case EVENT.STROKE: {
size += 4 + 2 + 2 + 4; // stroke id + point count + width + color size += 4 + 2 + 2 + 4; // stroke id + point count + width + color
size += event.points.byteLength; size += event.points.byteLength;
size += math.round_to_pow2(event.pressures.byteLength, 4);
break;
}
case EVENT.IMAGE: {
size += 4 + 4 + 4 + 4 + 4; // file_id + x + y + width + height
break; break;
} }
case EVENT.IMAGE:
case EVENT.IMAGE_MOVE: { case EVENT.IMAGE_MOVE: {
size += 4 + 4 + 4; // file id + x + y size += 4 + 4 + 4; // file id + x + y
break; break;
} }
case EVENT.IMAGE_SCALE: {
size += 4 + 4 + 4 + 4; // file_id + corner + x + y
break;
}
case EVENT.UNDO: case EVENT.UNDO:
case EVENT.REDO: { case EVENT.REDO: {
break; break;
@ -48,6 +76,7 @@ function event_size(event) {
} }
default: { default: {
console.error(event.desk_id);
console.error('fuck'); console.error('fuck');
console.trace(); console.trace();
process.exit(1); process.exit(1);
@ -79,7 +108,7 @@ function create_session(ws, desk_id) {
return session; return session;
} }
export async function send_init(ws) { export function send_init(ws) {
if (!ws) { if (!ws) {
return; return;
} }
@ -89,7 +118,7 @@ export async function send_init(ws) {
const desk = desks[desk_id]; const desk = desks[desk_id];
let opcode = MESSAGE.INIT; let opcode = MESSAGE.INIT;
let size = 1 + 4 + 4 + 4 + 4; // opcode + user_id + lsn + event count + stroke count + user count let size = 4 + 4 + 4 + 4 + 4 + 4; // opcode + user_id + lsn + event count + stroke count + user count + total_point_count
let session = null; let session = null;
if (session_id in sessions && sessions[session_id].desk_id == desk_id) { if (session_id in sessions && sessions[session_id].desk_id == desk_id) {
@ -115,7 +144,7 @@ export async function send_init(ws) {
const other_session = sessions[sid]; const other_session = sessions[sid];
if (other_session.id !== session_id && other_session.desk_id === desk_id) { if (other_session.id !== session_id && other_session.desk_id === desk_id) {
++user_count; ++user_count;
size += 4 + 4 + 2; // user id + color + width size += 4 + 4 + 2 + 1; // user id + color + width + online
} }
} }
@ -125,7 +154,7 @@ export async function send_init(ws) {
const s = ser.create(size); const s = ser.create(size);
ser.u8(s, opcode); ser.u32(s, opcode);
ser.u32(s, session.lsn); ser.u32(s, session.lsn);
if (opcode === MESSAGE.JOIN) { if (opcode === MESSAGE.JOIN) {
@ -137,6 +166,7 @@ export async function send_init(ws) {
ser.u32(s, desk.events.length); ser.u32(s, desk.events.length);
ser.u32(s, user_count); ser.u32(s, user_count);
ser.u32(s, desk.total_points);
for (const sid in sessions) { for (const sid in sessions) {
const other_session = sessions[sid]; const other_session = sessions[sid];
@ -145,14 +175,17 @@ export async function send_init(ws) {
ser.u32(s, other_session.id); ser.u32(s, other_session.id);
ser.u32(s, other_session.color); ser.u32(s, other_session.color);
ser.u16(s, other_session.width); ser.u16(s, other_session.width);
ser.u8(s, other_session.state === SESSION.READY);
} }
} }
ser.align(s, 4);
for (const event of desk.events) { for (const event of desk.events) {
ser.event(s, event); ser.event(s, event);
} }
await ws.send(s.buffer); ws.send(s.buffer);
} }
export function send_ack(ws, lsn) { export function send_ack(ws, lsn) {
@ -160,10 +193,10 @@ export function send_ack(ws, lsn) {
return; return;
} }
const size = 1 + 4; // opcode + lsn const size = 4 + 4; // opcode + lsn
const s = ser.create(size); const s = ser.create(size);
ser.u8(s, MESSAGE.ACK); ser.u32(s, MESSAGE.ACK);
ser.u32(s, lsn); ser.u32(s, lsn);
if (config.DEBUG_PRINT) console.log(`ack ${lsn} out`); if (config.DEBUG_PRINT) console.log(`ack ${lsn} out`);
@ -171,20 +204,45 @@ export function send_ack(ws, lsn) {
ws.send(s.buffer); ws.send(s.buffer);
} }
export function send_fire(ws, event) { function send_fire(ws, event) {
if (!ws) { if (!ws) {
return; return;
} }
const s = ser.create(1 + 4 + event_size(event)); const s = ser.create(4 + 4 + event_size(event));
ser.u8(s, MESSAGE.FIRE); ser.u32(s, MESSAGE.FIRE);
ser.event(s, event); ser.event(s, event);
ws.send(s.buffer); ws.send(s.buffer);
} }
async function sync_session(session_id) { 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) {
if (!(session_id in sessions)) { if (!(session_id in sessions)) {
return; return;
} }
@ -200,7 +258,7 @@ async function sync_session(session_id) {
return; return;
} }
let size = 1 + 4 + 4; // opcode + sn + event count let size = 4 + 4 + 4; // opcode + sn + event count
let count = desk.sn - session.sn; let count = desk.sn - session.sn;
if (count === 0) { if (count === 0) {
@ -215,7 +273,7 @@ async function sync_session(session_id) {
const s = ser.create(size); const s = ser.create(size);
ser.u8(s, MESSAGE.SYN); ser.u32(s, MESSAGE.SYN);
ser.u32(s, desk.sn); ser.u32(s, desk.sn);
ser.u32(s, count); ser.u32(s, count);
@ -226,9 +284,12 @@ async function sync_session(session_id) {
if (config.DEBUG_PRINT) console.log(`syn ${desk.sn} out`); if (config.DEBUG_PRINT) console.log(`syn ${desk.sn} out`);
await session.ws.send(s.buffer); session.ws.send(s.buffer);
if (session.sync_attempts < config.SYNC_MAX_ATTEMPTS) {
session.sync_attempts += 1;
session.sync_timer = setTimeout(() => sync_session(session_id), config.SYNC_TIMEOUT); session.sync_timer = setTimeout(() => sync_session(session_id), config.SYNC_TIMEOUT);
}
} }
export function sync_desk(desk_id) { export function sync_desk(desk_id) {

62
server/serializer.js

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

6
server/server.js

@ -51,16 +51,20 @@ export function startup() {
websocket: { websocket: {
open(ws) { open(ws) {
send.send_init(ws); send.send_init(ws);
const userjoin_event = {'type': EVENT.USER_JOINED, 'user_id': ws.data.session_id};
send.fire_event(sessions[ws.data.session_id], userjoin_event);
}, },
async message(ws, u8array) { async message(ws, u8array) {
const dataview = new DataView(u8array.buffer); const dataview = new DataView(u8array.buffer);
const d = des.create(dataview); const d = des.create(dataview);
await recv.handle_message(ws, d); recv.handle_message(ws, d);
}, },
close(ws, code, message) { close(ws, code, message) {
if (ws.data.session_id in sessions) { if (ws.data.session_id in sessions) {
const leave_event = {'type': EVENT.LEAVE, 'user_id': ws.data.session_id};
send.fire_event(sessions[ws.data.session_id], leave_event);
console.log(`session ${ws.data.session_id} closed`); console.log(`session ${ws.data.session_id} closed`);
sessions[ws.data.session_id].state = SESSION.CLOSED; sessions[ws.data.session_id].state = SESSION.CLOSED;
sessions[ws.data.session_id].ws = null; sessions[ws.data.session_id].ws = null;

36
server/storage.js

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

21
server/texput.log

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