Compare commits
147 Commits
Author | SHA1 | Date |
---|---|---|
Aleksey Olokhtonov | dc824c12c9 | 4 days ago |
A.Olokhtonov | 78ba044527 | 1 week ago |
A.Olokhtonov | dc235c7365 | 3 weeks ago |
A.Olokhtonov | 0c3259d00f | 3 weeks ago |
Aleksey Olokhtonov | a2f574214c | 4 weeks ago |
Aleksey Olokhtonov | 0c21579694 | 4 weeks ago |
A.Olokhtonov | 4e09c47190 | 2 months ago |
A.Olokhtonov | fea2874004 | 2 months ago |
A.Olokhtonov | 8bea9593da | 2 months ago |
A.Olokhtonov | 29ec265632 | 2 months ago |
A.Olokhtonov | bdc3bdc9a1 | 2 months ago |
A.Olokhtonov | 5d45eb3255 | 2 months ago |
A.Olokhtonov | 49620ff8fb | 2 months ago |
A.Olokhtonov | ea409f76c4 | 5 months ago |
A.Olokhtonov | 8b4b4e09f7 | 5 months ago |
A.Olokhtonov | b11c46354f | 5 months ago |
A.Olokhtonov | ab1659f77f | 5 months ago |
A.Olokhtonov | 803b7f80cb | 5 months ago |
A.Olokhtonov | 0ffac004c0 | 5 months ago |
A.Olokhtonov | 21aecb7d08 | 5 months ago |
A.Olokhtonov | 45bac3395a | 5 months ago |
A.Olokhtonov | 9d6f333778 | 5 months ago |
A.Olokhtonov | 254268c309 | 5 months ago |
A.Olokhtonov | c1583cb8fb | 5 months ago |
A.Olokhtonov | 66e84c1a50 | 5 months ago |
A.Olokhtonov | acb9aaa83d | 6 months ago |
A.Olokhtonov | 4a6d1d6640 | 6 months ago |
A.Olokhtonov | c6f8bc4b5a | 6 months ago |
A.Olokhtonov | 5871833cd1 | 6 months ago |
A.Olokhtonov | 8a15093147 | 6 months ago |
A.Olokhtonov | 903660adbc | 6 months ago |
Aleksey Olokhtonov | 5aa29b18c0 | 6 months ago |
Aleksey Olokhtonov | 01560389af | 6 months ago |
Aleksey Olokhtonov | 90f0beb4f9 | 6 months ago |
Aleksey Olokhtonov | e2ba3bb1c2 | 6 months ago |
A.Olokhtonov | bf2eace6fe | 6 months ago |
A.Olokhtonov | eafe860990 | 6 months ago |
A.Olokhtonov | eb66ffbcad | 6 months ago |
A.Olokhtonov | 30559b0381 | 6 months ago |
A.Olokhtonov | c13e5a6848 | 6 months ago |
A.Olokhtonov | ee42e400c4 | 6 months ago |
A.Olokhtonov | 807a1d0c03 | 6 months ago |
A.Olokhtonov | 07bb47b6dc | 6 months ago |
A.Olokhtonov | 5552dc11a3 | 7 months ago |
A.Olokhtonov | e4a10ebf30 | 7 months ago |
A.Olokhtonov | 68c892ba5f | 7 months ago |
A.Olokhtonov | 48e9dd9a3e | 7 months ago |
A.Olokhtonov | 221e2a5aed | 7 months ago |
A.Olokhtonov | c3f66d966c | 7 months ago |
A.Olokhtonov | acdbf73a26 | 8 months ago |
A.Olokhtonov | 6d9791d3e0 | 8 months ago |
A.Olokhtonov | f6573e7bb9 | 8 months ago |
A.Olokhtonov | c5ad40f739 | 8 months ago |
A.Olokhtonov | 4f84509b16 | 8 months ago |
A.Olokhtonov | 2c01d315d6 | 8 months ago |
A.Olokhtonov | e949860279 | 8 months ago |
A.Olokhtonov | 37ff1e55bc | 8 months ago |
A.Olokhtonov | 84a5859541 | 8 months ago |
A.Olokhtonov | 6f78c0ae21 | 8 months ago |
A.Olokhtonov | 58f2a60295 | 8 months ago |
A.Olokhtonov | 40dd278f21 | 10 months ago |
A.Olokhtonov | 6f19e6c954 | 10 months ago |
aolo2 | dfecad2c99 | 10 months ago |
aolo2 | 704c833c16 | 10 months ago |
aolo2 | 71f1016a40 | 10 months ago |
A.Olokhtonov | 8abe88373f | 10 months ago |
A.Olokhtonov | 3bf812df39 | 10 months ago |
A.Olokhtonov | c9d1706b6e | 10 months ago |
A.Olokhtonov | 1960bdebe9 | 10 months ago |
A.Olokhtonov | cf11f6c701 | 10 months ago |
A.Olokhtonov | 28fec7f306 | 10 months ago |
A.Olokhtonov | 4b5b8db053 | 10 months ago |
A.Olokhtonov | ab7545918b | 10 months ago |
A.Olokhtonov | 7555de10bf | 10 months ago |
A.Olokhtonov | 08ba8a7687 | 10 months ago |
A.Olokhtonov | cc800c1ffe | 10 months ago |
A.Olokhtonov | 8bbcfb616f | 10 months ago |
A.Olokhtonov | 99bd99a465 | 10 months ago |
A.Olokhtonov | ee1d1471f4 | 10 months ago |
A.Olokhtonov | d8a5cd3fca | 10 months ago |
A.Olokhtonov | 1f983f3389 | 10 months ago |
A.Olokhtonov | c893a73ec5 | 10 months ago |
A.Olokhtonov | 847fb70381 | 10 months ago |
A.Olokhtonov | 9bbab32369 | 11 months ago |
A.Olokhtonov | a60c3d1948 | 11 months ago |
A.Olokhtonov | 2d88b57dca | 11 months ago |
A.Olokhtonov | 248501e50f | 11 months ago |
A.Olokhtonov | 31a0b0113a | 11 months ago |
A.Olokhtonov | 1438b0ad73 | 11 months ago |
A.Olokhtonov | 60fc48b1e7 | 11 months ago |
A.Olokhtonov | fdca3e6b07 | 11 months ago |
A.Olokhtonov | 7b53c7215e | 11 months ago |
A.Olokhtonov | 72eedf9b48 | 11 months ago |
A.Olokhtonov | 24222f9565 | 12 months ago |
A.Olokhtonov | cca3b4cd5d | 12 months ago |
A.Olokhtonov | a991cf3247 | 12 months ago |
aolo2 | 1691bd07ae | 12 months ago |
A.Olokhtonov | 6be8ba7823 | 12 months ago |
A.Olokhtonov | 7752e41bf5 | 12 months ago |
A.Olokhtonov | 1bc6f2c3fe | 12 months ago |
A.Olokhtonov | 316d0fedc9 | 1 year ago |
A.Olokhtonov | e86ffd6508 | 1 year ago |
A.Olokhtonov | 7e3b6156c0 | 1 year ago |
A.Olokhtonov | 0d8667aa3a | 1 year ago |
A.Olokhtonov | e41997563f | 1 year ago |
A.Olokhtonov | 46cd48fecd | 1 year ago |
A.Olokhtonov | be50af68d4 | 1 year ago |
A.Olokhtonov | 72deea0b03 | 1 year ago |
A.Olokhtonov | 9a8854dc90 | 1 year ago |
A.Olokhtonov | 1803dedee2 | 1 year ago |
A.Olokhtonov | 14faef4146 | 1 year ago |
A.Olokhtonov | c5928dd5bd | 1 year ago |
A.Olokhtonov | eefbb8ef75 | 1 year ago |
A.Olokhtonov | 605500028a | 1 year ago |
A.Olokhtonov | c1fd46da53 | 1 year ago |
A.Olokhtonov | 01db70cab0 | 1 year ago |
A.Olokhtonov | e3ae4cf6cd | 1 year ago |
A.Olokhtonov | 46587068e6 | 1 year ago |
A.Olokhtonov | f72cbf5941 | 1 year ago |
A.Olokhtonov | 69feb482a2 | 1 year ago |
A.Olokhtonov | ad9cded350 | 2 years ago |
A.Olokhtonov | 6f446b7d13 | 2 years ago |
A.Olokhtonov | 8b3f28337e | 2 years ago |
A.Olokhtonov | 8557c5d47e | 2 years ago |
A.Olokhtonov | 0271e38dbc | 2 years ago |
A.Olokhtonov | 31b18e69a0 | 2 years ago |
A.Olokhtonov | 3b8232e196 | 2 years ago |
A.Olokhtonov | 1edcc6725b | 2 years ago |
A.Olokhtonov | ac0d8f7605 | 2 years ago |
A.Olokhtonov | 33898ab27a | 2 years ago |
A.Olokhtonov | dec07b4edc | 2 years ago |
A.Olokhtonov | cb783db614 | 2 years ago |
A.Olokhtonov | 165d9235ce | 2 years ago |
A.Olokhtonov | 7c2ba5ff72 | 2 years ago |
A.Olokhtonov | 110afe123f | 2 years ago |
A.Olokhtonov | 45c3af9c67 | 2 years ago |
A.Olokhtonov | bf273a9424 | 2 years ago |
A.Olokhtonov | 59cb197e58 | 2 years ago |
A.Olokhtonov | 343008c0af | 2 years ago |
A.Olokhtonov | f24e8d386b | 2 years ago |
A.Olokhtonov | 29f697dceb | 2 years ago |
A.Olokhtonov | 04c11e23f3 | 2 years ago |
A.Olokhtonov | 5c0d9e1537 | 2 years ago |
A.Olokhtonov | 5593536485 | 2 years ago |
A.Olokhtonov | ab152b2d0a | 2 years ago |
A.Olokhtonov | e056d6c698 | 2 years ago |
A.Olokhtonov | 7011cc86be | 2 years ago |
@ -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 |
||||||
|
@ -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> |
@ -0,0 +1,104 @@ |
|||||||
|
Release: |
||||||
|
* Engine |
||||||
|
+ Benchmark harness |
||||||
|
+ Reuse points, pack "nodraw" in high bit of stroke id (probably have at least one more bit, so up to 4 flag configurations) |
||||||
|
+ Draw dynamic data (strokes in progress) |
||||||
|
+ Webassembly for core LOD generation |
||||||
|
+ Webassembly for final buffers |
||||||
|
+ Do not copy memory to wasm, instead use wasm memory to store data in the first place |
||||||
|
+ SIMD for LOD? |
||||||
|
+ Multithreading for LOD |
||||||
|
+ Textured quads (pictures, code already written in older version |
||||||
|
+ Resize and move pictures (draw handles) |
||||||
|
- Z-prepass fringe bug (also, when do we enable the prepass?) |
||||||
|
- Frame-independent lerp where applicable |
||||||
|
+ Bugs |
||||||
|
+ GC stalls!!! |
||||||
|
+ Stroke previews get connected when drawn without panning on touch devices |
||||||
|
+ Redraw HTML (cursors) on local canvas moves |
||||||
|
+ New strokes dissapear on the HMH desk |
||||||
|
+ Undo history of moving and scaling images seems messed up |
||||||
|
- Nothing get's drawn if we enable snapping and draw a curve where first and last point match |
||||||
|
- Weird clipping on HMH desk full zoomout after running "benchmark" |
||||||
|
- Stuck in color picker mode when mouse leaves screen |
||||||
|
- Debug |
||||||
|
* Debug view for BVH |
||||||
|
- Restore ability to limit event range |
||||||
|
* Listeners/events/multiplayer |
||||||
|
+ Fix multiplayer LUL |
||||||
|
+ Fix blinking own stroke inbetween SYN->server and SYN->client |
||||||
|
+ Drag with mouse button 3 |
||||||
|
+ Investigate skipped inputs on mobile (panning, zooming) [Events were not actually getting skipped. The stroke previews were just not being drawn] |
||||||
|
+ Smooth zoom |
||||||
|
+ Infinite background pattern |
||||||
|
+ Be able to have multiple "current" strokes per player. In case of bad internet this can happen! |
||||||
|
- Do NOT use session id as player id LUL |
||||||
|
- Save events to indexeddb (as some kind of a blob), restore on reconnect and page reload |
||||||
|
- Handle out of space |
||||||
|
- Local prediction for tools! |
||||||
|
- Immediately commit a stroke to the canvas, change order if earlier strokes arrive |
||||||
|
- Show my own image immediately, show placeholders while images are loading (add bitmap size to event) |
||||||
|
- undo immediately, this one can not arrive out of order, because noone else is going to undo MY actions |
||||||
|
* Missing features I do not consider bonus |
||||||
|
+ Player pointers |
||||||
|
+ Pretty player pointers |
||||||
|
+ Player list |
||||||
|
+ Follow player |
||||||
|
+ Color picker (or at the very least an Open Color color pallete) |
||||||
|
+ EYE DROPPER! |
||||||
|
+ Dynamic svg cursor to represent the brush |
||||||
|
+ Eraser |
||||||
|
* Line drawing |
||||||
|
+ Live preview |
||||||
|
~ Alignment (horizontal, vertical, diagonal, etc) [kinda gets covered by the snapping? question mark?] |
||||||
|
+ Undo |
||||||
|
+ Undo for eraser |
||||||
|
+ Undo for images (add, move, scale) |
||||||
|
- Redo |
||||||
|
+ Snapping to grid |
||||||
|
- Snapping to other points? |
||||||
|
- Color picker should work for ruler |
||||||
|
- Show previous color in color picker preview |
||||||
|
- Stick picker preview to cursor |
||||||
|
* Polish |
||||||
|
+ Use typedvector where appropriate |
||||||
|
- Show what's happening while the desk is loading (downloading, processing, uploading to gpu) |
||||||
|
- Settings panel for config values (including the setting for "offline mode") |
||||||
|
- Set up VAOs |
||||||
|
- We are calling "geometry_prepare_stroke" twice for some reason |
||||||
|
- Replace "geometry_add_dummy_stroke" with something not [so] cursed |
||||||
|
- Automatically extract locations from shaders (see nitka project for code examples) |
||||||
|
- Presentation / "marketing" |
||||||
|
- Title (InfiNotes? MegaDesk?) |
||||||
|
- Icon |
||||||
|
- Product page (github readme, demo videos) |
||||||
|
|
||||||
|
Bonus: |
||||||
|
* Handle pressure |
||||||
|
+ Add pressure data to quads |
||||||
|
+ Account for pressure in quad/bbox calc |
||||||
|
+ Adjust curve simplification to include pressure info |
||||||
|
+ Migrate old non-pressure desks |
||||||
|
- Check out e.pressure on touch devices |
||||||
|
- Send pressure in PREDRAW event |
||||||
|
- Stroke smoothing |
||||||
|
https://github.com/xournalpp/xournalpp/issues/2320 |
||||||
|
https://www.digital-epigraphy.com/tutorials/the-most-useful-new-features-of-photoshop-cc-using-brush-stroke-smoothing-for-digital-inking |
||||||
|
https://stackoverflow.com/questions/20618804/how-to-smooth-a-curve-for-a-dataset |
||||||
|
- Curve modification |
||||||
|
- Select curves (with a lasso?) |
||||||
|
- Move whole curve |
||||||
|
- Move single point |
||||||
|
- Move multiple points |
||||||
|
* Customizable background |
||||||
|
+ Dots pattern |
||||||
|
+ Grid pattern |
||||||
|
- Menu option |
||||||
|
- Offline mode |
||||||
|
- Only one user |
||||||
|
- No server, everything applied immediately |
||||||
|
- Allow export to file |
||||||
|
- Save to browser storage (probaby indexed db + recent events in localstorage) |
||||||
|
|
||||||
|
Bonus-bonus: |
||||||
|
- Actually infinite canvas (replace floats with something, some kind of fixed point scheme? chunks? multilevel scheme?) |
@ -0,0 +1,268 @@ |
|||||||
|
function ui_offline() { |
||||||
|
document.body.classList.add('offline'); |
||||||
|
document.querySelector('.offline-toast').classList.remove('hidden'); |
||||||
|
} |
||||||
|
|
||||||
|
function ui_online() { |
||||||
|
document.body.classList.remove('offline'); |
||||||
|
document.querySelector('.offline-toast').classList.add('hidden'); |
||||||
|
} |
||||||
|
|
||||||
|
async function insert_image(state, context, file) { |
||||||
|
const bitmap = await createImageBitmap(file); |
||||||
|
|
||||||
|
const p = { 'x': state.cursor.x, 'y': state.cursor.y }; |
||||||
|
const canvasp = screen_to_canvas(state, p); |
||||||
|
|
||||||
|
canvasp.x -= bitmap.width / 2; |
||||||
|
canvasp.y -= bitmap.height / 2; |
||||||
|
|
||||||
|
const form_data = new FormData(); |
||||||
|
form_data.append('file', file); |
||||||
|
|
||||||
|
const resp = await fetch(`/api/image?deskId=${state.desk_id}`, { |
||||||
|
method: 'post', |
||||||
|
body: form_data, |
||||||
|
}) |
||||||
|
|
||||||
|
if (resp.ok) { |
||||||
|
const image_id = await resp.text(); |
||||||
|
const event = image_event(image_id, canvasp.x, canvasp.y, bitmap.width, bitmap.height); |
||||||
|
queue_event(state, event); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
function event_size(event) { |
||||||
|
let size = 4; // type
|
||||||
|
|
||||||
|
switch (event.type) { |
||||||
|
case EVENT.PREDRAW: |
||||||
|
case EVENT.MOVE_CURSOR: { |
||||||
|
size += 4 * 2; |
||||||
|
break; |
||||||
|
} |
||||||
|
|
||||||
|
case EVENT.MOVE_CANVAS: { |
||||||
|
size += 4 * 2 + 4; |
||||||
|
break; |
||||||
|
} |
||||||
|
|
||||||
|
case EVENT.ZOOM_CANVAS: { |
||||||
|
size += 4 + 4 * 2; |
||||||
|
break; |
||||||
|
} |
||||||
|
|
||||||
|
case EVENT.USER_JOINED: |
||||||
|
case EVENT.LEAVE: |
||||||
|
case EVENT.CLEAR: |
||||||
|
case EVENT.LIFT: { |
||||||
|
break; |
||||||
|
} |
||||||
|
|
||||||
|
case EVENT.SET_COLOR: { |
||||||
|
size += 4; |
||||||
|
break; |
||||||
|
} |
||||||
|
|
||||||
|
case EVENT.SET_WIDTH: { |
||||||
|
size += 2; |
||||||
|
break; |
||||||
|
} |
||||||
|
|
||||||
|
case EVENT.STROKE: { |
||||||
|
// u32 stroke id + u16 (count) + u16 (width) + u32 (color) + count * (f32, f32) points + count (u8) pressures
|
||||||
|
size += 4 + 2 + 2 + 4 + event.points.length * 4 * 2 + round_to_pow2(event.points.length, 4); |
||||||
|
break; |
||||||
|
} |
||||||
|
|
||||||
|
case EVENT.UNDO: |
||||||
|
case EVENT.REDO: { |
||||||
|
break; |
||||||
|
} |
||||||
|
|
||||||
|
case EVENT.IMAGE: |
||||||
|
case EVENT.IMAGE_MOVE: { |
||||||
|
size += 4 + 4 + 4 + 4 + 4; // file id + x + y + width + height
|
||||||
|
break; |
||||||
|
} |
||||||
|
|
||||||
|
case EVENT.IMAGE_SCALE: { |
||||||
|
size += 4 + 4 + 4 + 4; // file_id + corner + x + y
|
||||||
|
break; |
||||||
|
} |
||||||
|
|
||||||
|
case EVENT.ERASER: { |
||||||
|
size += 4; // stroke id
|
||||||
|
break; |
||||||
|
} |
||||||
|
|
||||||
|
default: { |
||||||
|
console.error('fuck'); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
return size; |
||||||
|
} |
||||||
|
|
||||||
|
function find_touch(touchlist, id) { |
||||||
|
for (const touch of touchlist) { |
||||||
|
if (touch.identifier === id) { |
||||||
|
return touch; |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
return null; |
||||||
|
} |
||||||
|
|
||||||
|
function find_image(state, image_id) { |
||||||
|
for (let i = state.events.length - 1; i >= 0; --i) { |
||||||
|
const event = state.events[i]; |
||||||
|
if (event.type === EVENT.IMAGE && !event.deleted && event.image_id === image_id) { |
||||||
|
return event; |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
// TODO: move these to a file? TypedVector
|
||||||
|
function tv_create(class_name, capacity) { |
||||||
|
return { |
||||||
|
'class_name': class_name, |
||||||
|
'data': new class_name(capacity), |
||||||
|
'capacity': capacity, |
||||||
|
'size': 0, |
||||||
|
}; |
||||||
|
} |
||||||
|
|
||||||
|
function tv_create_on(class_name, capacity, buffer, offset) { |
||||||
|
return { |
||||||
|
'class_name': class_name, |
||||||
|
'data': new class_name(buffer, offset, capacity), |
||||||
|
'capacity': capacity, |
||||||
|
'size': 0, |
||||||
|
}; |
||||||
|
} |
||||||
|
|
||||||
|
function tv_data(tv) { |
||||||
|
return tv.data.subarray(0, tv.size); |
||||||
|
} |
||||||
|
|
||||||
|
function tv_bytes(tv) { |
||||||
|
return new Uint8Array(tv.data.buffer, 0, tv.size * tv.data.BYTES_PER_ELEMENT); |
||||||
|
} |
||||||
|
|
||||||
|
function tv_ensure(tv, capacity) { |
||||||
|
if (tv.capacity < capacity) { |
||||||
|
const new_data = new tv.class_name(capacity); |
||||||
|
new_data.set(tv_data(tv)); |
||||||
|
tv.capacity = capacity; |
||||||
|
tv.data = new_data; |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
function tv_ensure_by(tv, by) { |
||||||
|
tv_ensure(tv, round_to_pow2(tv.size + by, 4096)); |
||||||
|
} |
||||||
|
|
||||||
|
function tv_add(tv, item) { |
||||||
|
tv.data[tv.size++] = item; |
||||||
|
} |
||||||
|
|
||||||
|
function tv_add2(tv, item) { |
||||||
|
tv_ensure_by(tv, 1); |
||||||
|
tv_add(tv, item); |
||||||
|
} |
||||||
|
|
||||||
|
function tv_pop(tv) { |
||||||
|
const result = tv.data[tv.size - 1]; |
||||||
|
tv.size--; |
||||||
|
return result; |
||||||
|
} |
||||||
|
|
||||||
|
function tv_append(tv, typedarray) { |
||||||
|
tv.data.set(typedarray, tv.size); |
||||||
|
tv.size += typedarray.length; |
||||||
|
} |
||||||
|
|
||||||
|
function tv_clear(tv) { |
||||||
|
tv.size = 0; |
||||||
|
} |
||||||
|
|
||||||
|
function HTML(html) { |
||||||
|
const template = document.createElement('template'); |
||||||
|
template.innerHTML = html.trim(); |
||||||
|
return template.content.firstChild; |
||||||
|
} |
||||||
|
|
||||||
|
function toggle_follow_player(state, player_id) { |
||||||
|
document.querySelectorAll('.player-list .player').forEach(p => p.classList.remove('following')); |
||||||
|
|
||||||
|
if (state.following_player === null) { |
||||||
|
state.following_player = player_id; |
||||||
|
} else { |
||||||
|
if (player_id === state.following_player) { |
||||||
|
state.following_player = null; |
||||||
|
} else { |
||||||
|
state.following_player = player_id; |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
const player_element = document.querySelector(`.player-list .player[data-player-id="${state.following_player}"]`); |
||||||
|
if (player_element) player_element.classList.add('following'); |
||||||
|
|
||||||
|
send_follow(state.following_player); |
||||||
|
} |
||||||
|
|
||||||
|
function insert_player_cursor(state, player_id) { |
||||||
|
const color = random_bright_color_from_seed(parseInt(player_id)); |
||||||
|
const path_copy = state.cursor_path.cloneNode(); |
||||||
|
|
||||||
|
path_copy.style.fill = color; |
||||||
|
|
||||||
|
const cursor = HTML(`<svg viewBox="0 0 28 28" xmlns="http://www.w3.org/2000/svg" class="player-cursor" data-player-id="${player_id}">${path_copy.outerHTML}</svg>`); |
||||||
|
const player = HTML(`<div class="player" data-player-id="${player_id}"><img src="icons/player.svg"></div>`); |
||||||
|
|
||||||
|
player.style.background = color; |
||||||
|
|
||||||
|
player.addEventListener('click', () => { |
||||||
|
toggle_follow_player(state, player_id); |
||||||
|
}); |
||||||
|
|
||||||
|
document.querySelector('.html-hud').appendChild(cursor); |
||||||
|
document.querySelector('.player-list').appendChild(player); |
||||||
|
|
||||||
|
document.querySelector('.player-list').classList.remove('vhide'); |
||||||
|
|
||||||
|
return cursor; |
||||||
|
} |
||||||
|
|
||||||
|
async function load_player_cursor_template(state) { |
||||||
|
const resp = await fetch('icons/player-cursor.svg'); |
||||||
|
const text = await resp.text(); |
||||||
|
const parser = new DOMParser(); |
||||||
|
const parsed_xml = parser.parseFromString(text, 'image/svg+xml'); |
||||||
|
const path = parsed_xml.querySelector('path'); |
||||||
|
|
||||||
|
state.cursor_path = path; |
||||||
|
} |
||||||
|
|
||||||
|
function get_image(context, key) { |
||||||
|
for (const entry of context.images) { |
||||||
|
if (entry.key === key) { |
||||||
|
return entry; |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
return null; |
||||||
|
} |
||||||
|
|
||||||
|
function grid_snap_step(state) { |
||||||
|
const zoom_log2 = Math.log2(state.canvas.zoom); |
||||||
|
const zoom_previous = Math.pow(2, Math.floor(zoom_log2)); |
||||||
|
const zoom_next = Math.pow(2, Math.ceil(zoom_log2)); |
||||||
|
|
||||||
|
if (Math.abs(state.canvas.zoom - zoom_previous) < Math.abs(state.canvas.zoom - zoom_next)) { |
||||||
|
return 32 / zoom_previous; |
||||||
|
} else { |
||||||
|
return 32 / zoom_next; |
||||||
|
} |
||||||
|
} |
@ -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.toSorted((a, b) => a.bbox.cy - b.bbox.cy); |
||||||
|
} else { |
||||||
|
sorted_strokes = strokes.toSorted((a, b) => a.bbox.cx - b.bbox.cx); |
||||||
|
} |
||||||
|
|
||||||
|
const node_index = bvh_make_internal(bvh); |
||||||
|
const left_of_split_count = Math.floor(strokes.length / 2); |
||||||
|
|
||||||
|
const child1 = bvh_construct_rec(state, bvh, sorted_strokes.slice(0, left_of_split_count), depth + 1); |
||||||
|
const child2 = bvh_construct_rec(state, bvh, sorted_strokes.slice(left_of_split_count, sorted_strokes.length), depth + 1); |
||||||
|
|
||||||
|
bvh.nodes[child1].parent_index = node_index; |
||||||
|
bvh.nodes[child2].parent_index = node_index; |
||||||
|
|
||||||
|
bvh.nodes[node_index].child1 = child1; |
||||||
|
bvh.nodes[node_index].child2 = child2; |
||||||
|
bvh.nodes[node_index].bbox = quad_union(bvh.nodes[child1].bbox, bvh.nodes[child2].bbox); |
||||||
|
|
||||||
|
if (depth === config.bvh_fullnode_depth) { |
||||||
|
const indices = tv_create(Int32Array, round_to_pow2(strokes.length, 32)); |
||||||
|
|
||||||
|
for (let i = 0; i < strokes.length; ++i) { |
||||||
|
tv_add(indices, strokes[i].index); |
||||||
|
} |
||||||
|
|
||||||
|
bvh.nodes[node_index].stroke_indices = indices; |
||||||
|
bvh.nodes[node_index].is_fullnode = true; |
||||||
|
} |
||||||
|
|
||||||
|
return node_index; |
||||||
|
} else { |
||||||
|
// leaf
|
||||||
|
const leaf_index = bvh_make_leaf(bvh, strokes[0].index, strokes[0]); |
||||||
|
state.events[strokes[0].index].bvh_node = leaf_index; |
||||||
|
return leaf_index; |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
function bvh_construct(state) { |
||||||
|
const strokes = state.events.filter(e => e.type === EVENT.STROKE && e.deleted !== true); |
||||||
|
if (strokes.length > 0) { |
||||||
|
state.bvh.root = bvh_construct_rec(state, state.bvh, strokes, 0); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
function bvh_get_fullnodes_debug(state, context) { |
||||||
|
const bvh = state.bvh; |
||||||
|
const result = []; |
||||||
|
const stack = []; |
||||||
|
|
||||||
|
const screen_topleft = screen_to_canvas(state, {'x': 0, 'y': 0}); |
||||||
|
const screen_bottomright = screen_to_canvas(state, {'x': context.canvas.width, 'y': context.canvas.height}); |
||||||
|
const screen_topright = { 'x': screen_bottomright.x, 'y': screen_topleft.y }; |
||||||
|
const screen_bottomleft = { 'x': screen_topleft.x, 'y': screen_bottomright.y }; |
||||||
|
|
||||||
|
const quad = { |
||||||
|
'x1': screen_topleft.x, |
||||||
|
'y1': screen_topleft.y, |
||||||
|
'x2': screen_bottomright.x, |
||||||
|
'y2': screen_bottomright.y |
||||||
|
}; |
||||||
|
|
||||||
|
if (bvh.root === null) { |
||||||
|
return; |
||||||
|
} |
||||||
|
|
||||||
|
stack.push({'depth': 0, 'node_index': bvh.root}); |
||||||
|
|
||||||
|
while (stack.length > 0) { |
||||||
|
const entry = stack.pop(); |
||||||
|
const node = bvh.nodes[entry.node_index]; |
||||||
|
|
||||||
|
if (!quads_intersect(node.bbox, quad)) { |
||||||
|
continue; |
||||||
|
} |
||||||
|
|
||||||
|
if (node.is_fullnode) { |
||||||
|
result.push({...node.bbox}); |
||||||
|
continue; |
||||||
|
} |
||||||
|
|
||||||
|
if (!node.is_leaf && entry.depth < config.bvh_fullnode_depth) { |
||||||
|
stack.push({'depth': entry.depth + 1, 'node_index': node.child1}); |
||||||
|
stack.push({'depth': entry.depth + 1, 'node_index': node.child2}); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
return result; |
||||||
|
} |
||||||
|
|
@ -0,0 +1,593 @@ |
|||||||
|
function deserializer_create(buffer, dataview) { |
||||||
|
return { |
||||||
|
'offset': 0, |
||||||
|
'size': buffer.byteLength, |
||||||
|
'buffer': buffer, |
||||||
|
'view': dataview, |
||||||
|
'strview': new Uint8Array(buffer), |
||||||
|
}; |
||||||
|
} |
||||||
|
|
||||||
|
function des_u8(d) { |
||||||
|
const value = d.view.getUint8(d.offset); |
||||||
|
d.offset += 1; |
||||||
|
return value; |
||||||
|
} |
||||||
|
|
||||||
|
function des_u16(d) { |
||||||
|
const value = d.view.getUint16(d.offset, true); |
||||||
|
d.offset += 2; |
||||||
|
return value; |
||||||
|
} |
||||||
|
|
||||||
|
function des_u32(d) { |
||||||
|
const value = d.view.getUint32(d.offset, true); |
||||||
|
d.offset += 4; |
||||||
|
return value; |
||||||
|
} |
||||||
|
|
||||||
|
function des_s32(d) { |
||||||
|
const value = d.view.getInt32(d.offset, true); |
||||||
|
d.offset += 4; |
||||||
|
return value; |
||||||
|
} |
||||||
|
|
||||||
|
function des_f32(d) { |
||||||
|
const value = d.view.getFloat32(d.offset, true); |
||||||
|
d.offset += 4; |
||||||
|
return value; |
||||||
|
} |
||||||
|
|
||||||
|
function des_align(d, to) { |
||||||
|
// TODO: non-stupid version of this
|
||||||
|
while (d.offset % to != 0) { |
||||||
|
d.offset++; |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
function des_f32array(d, count) { |
||||||
|
const result = new Float32Array(d.buffer, d.offset, count); |
||||||
|
d.offset += 4 * count; |
||||||
|
return result; |
||||||
|
} |
||||||
|
|
||||||
|
function des_u8array(d, count) { |
||||||
|
const result = new Uint8Array(d.buffer, d.offset, count); |
||||||
|
d.offset += count; |
||||||
|
return result; |
||||||
|
} |
||||||
|
|
||||||
|
function des_event(d, state = null) { |
||||||
|
const event = {}; |
||||||
|
|
||||||
|
event.type = des_u32(d); |
||||||
|
event.user_id = des_u32(d); |
||||||
|
|
||||||
|
switch (event.type) { |
||||||
|
case EVENT.PREDRAW: { |
||||||
|
event.x = des_f32(d); |
||||||
|
event.y = des_f32(d); |
||||||
|
break; |
||||||
|
} |
||||||
|
|
||||||
|
case EVENT.USER_JOINED: |
||||||
|
case EVENT.LEAVE: |
||||||
|
case EVENT.CLEAR: |
||||||
|
case EVENT.LIFT: { |
||||||
|
break; |
||||||
|
} |
||||||
|
|
||||||
|
case EVENT.MOVE_CURSOR: { |
||||||
|
event.x = des_f32(d); |
||||||
|
event.y = des_f32(d); |
||||||
|
break; |
||||||
|
} |
||||||
|
|
||||||
|
case EVENT.MOVE_CANVAS: { |
||||||
|
event.offset_x = des_s32(d); |
||||||
|
event.offset_y = des_s32(d); |
||||||
|
event.zoom_level = des_s32(d); |
||||||
|
break; |
||||||
|
} |
||||||
|
|
||||||
|
case EVENT.ZOOM_CANVAS: { |
||||||
|
event.zoom_level = des_s32(d); |
||||||
|
event.zoom_cx = des_f32(d); |
||||||
|
event.zoom_cy = des_f32(d); |
||||||
|
break; |
||||||
|
} |
||||||
|
|
||||||
|
case EVENT.SET_COLOR: { |
||||||
|
event.color = des_u32(d); |
||||||
|
break; |
||||||
|
} |
||||||
|
|
||||||
|
case EVENT.SET_WIDTH: { |
||||||
|
event.width = des_u16(d); |
||||||
|
break; |
||||||
|
} |
||||||
|
|
||||||
|
case EVENT.STROKE: { |
||||||
|
const stroke_id = des_u32(d); |
||||||
|
const point_count = des_u16(d); |
||||||
|
const width = des_u16(d); |
||||||
|
const color = des_u32(d); |
||||||
|
|
||||||
|
event.coords = des_f32array(d, point_count * 2); |
||||||
|
event.press = des_u8array(d, point_count); |
||||||
|
|
||||||
|
des_align(d, 4); |
||||||
|
|
||||||
|
// TODO: remove, this is duplicate data
|
||||||
|
|
||||||
|
event.stroke_id = stroke_id; |
||||||
|
|
||||||
|
event.color = color; |
||||||
|
event.width = width; |
||||||
|
|
||||||
|
break; |
||||||
|
} |
||||||
|
|
||||||
|
case EVENT.IMAGE: { |
||||||
|
event.image_id = des_u32(d); |
||||||
|
event.x = des_f32(d); |
||||||
|
event.y = des_f32(d); |
||||||
|
event.width = des_u32(d); |
||||||
|
event.height = des_u32(d); |
||||||
|
break; |
||||||
|
} |
||||||
|
|
||||||
|
case EVENT.IMAGE_MOVE: { |
||||||
|
event.image_id = des_u32(d); |
||||||
|
event.x = des_f32(d); |
||||||
|
event.y = des_f32(d); |
||||||
|
break; |
||||||
|
} |
||||||
|
|
||||||
|
case EVENT.IMAGE_SCALE: { |
||||||
|
event.image_id = des_u32(d); |
||||||
|
event.corner = des_u32(d); |
||||||
|
event.x = des_f32(d); |
||||||
|
event.y = des_f32(d); |
||||||
|
break; |
||||||
|
} |
||||||
|
|
||||||
|
case EVENT.UNDO: |
||||||
|
case EVENT.REDO: { |
||||||
|
break; |
||||||
|
} |
||||||
|
|
||||||
|
case EVENT.ERASER: { |
||||||
|
event.stroke_id = des_u32(d); |
||||||
|
break; |
||||||
|
} |
||||||
|
|
||||||
|
default: { |
||||||
|
console.error('fuck'); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
return event; |
||||||
|
} |
||||||
|
|
||||||
|
function bitmap_bbox(event) { |
||||||
|
const bbox = { |
||||||
|
'xmin': event.x, |
||||||
|
'xmax': event.x + event.bitmap.width, |
||||||
|
'ymin': event.y, |
||||||
|
'ymax': event.y + event.bitmap.height, |
||||||
|
}; |
||||||
|
|
||||||
|
return bbox; |
||||||
|
} |
||||||
|
|
||||||
|
function init_player_defaults(state, player_id, color = config.default_color, width = config.default_width) { |
||||||
|
state.players[player_id] = { |
||||||
|
'color': color, |
||||||
|
'width': width, |
||||||
|
'points': [], |
||||||
|
'online': false, |
||||||
|
'cursor': {'x': 0, 'y': 0}, |
||||||
|
'strokes': [], |
||||||
|
'current_prestroke': false, |
||||||
|
}; |
||||||
|
} |
||||||
|
|
||||||
|
function handle_event(state, context, event, options = {}) { |
||||||
|
if (config.debug_print) console.debug(`event type ${event.type} from user ${event.user_id}`); |
||||||
|
|
||||||
|
let need_draw = false; |
||||||
|
|
||||||
|
if (!(event.user_id in state.players)) { |
||||||
|
init_player_defaults(state, event.user_id); |
||||||
|
} |
||||||
|
|
||||||
|
switch (event.type) { |
||||||
|
case EVENT.USER_JOINED: { |
||||||
|
state.players[event.user_id].online = true; |
||||||
|
draw_html(state); |
||||||
|
break; |
||||||
|
} |
||||||
|
|
||||||
|
case EVENT.PREDRAW: { |
||||||
|
const player = state.players[event.user_id]; |
||||||
|
|
||||||
|
if (!player.current_prestroke) { |
||||||
|
geometry_start_prestroke(state, event.user_id); |
||||||
|
} |
||||||
|
|
||||||
|
geometry_add_prepoint(state, context, event.user_id, {'x': event.x, 'y': event.y, 'pressure': 128}, false); // TODO: add pressure to predraw events
|
||||||
|
need_draw = true; |
||||||
|
break; |
||||||
|
} |
||||||
|
|
||||||
|
case EVENT.CLEAR: { |
||||||
|
// TODO: @touch
|
||||||
|
break; |
||||||
|
} |
||||||
|
|
||||||
|
case EVENT.LIFT: { |
||||||
|
// Current stroke from player ended. Handle following PREDRAWN events as next stroke
|
||||||
|
geometry_end_prestroke(state, event.user_id); |
||||||
|
break; |
||||||
|
} |
||||||
|
|
||||||
|
case EVENT.LEAVE: { |
||||||
|
if (event.user_id in state.players) { |
||||||
|
state.players[event.user_id].online = false; |
||||||
|
draw_html(state); |
||||||
|
} |
||||||
|
break; |
||||||
|
} |
||||||
|
|
||||||
|
case EVENT.MOVE_CURSOR: { |
||||||
|
if (event.user_id in state.players) { |
||||||
|
state.players[event.user_id].cursor.x = event.x; |
||||||
|
state.players[event.user_id].cursor.y = event.y; |
||||||
|
state.players[event.user_id].online = true; |
||||||
|
} |
||||||
|
|
||||||
|
// Should we syncronize this to RAF?
|
||||||
|
draw_html(state); |
||||||
|
|
||||||
|
break; |
||||||
|
} |
||||||
|
|
||||||
|
case EVENT.MOVE_CANVAS: { |
||||||
|
// Double-check just in case
|
||||||
|
// Non-triple equals in on purpose
|
||||||
|
if (event.user_id == state.following_player) { |
||||||
|
state.canvas.offset.x = event.offset_x; |
||||||
|
state.canvas.offset.y = event.offset_y; |
||||||
|
|
||||||
|
const zoom_level = event.zoom_level; |
||||||
|
const dz = (zoom_level > 0 ? config.zoom_delta : -config.zoom_delta); |
||||||
|
const zoom = Math.pow(1.0 + dz, Math.abs(zoom_level)) |
||||||
|
|
||||||
|
state.canvas.zoom_level = zoom_level; |
||||||
|
state.canvas.zoom = zoom; |
||||||
|
state.canvas.target_zoom = zoom; |
||||||
|
|
||||||
|
need_draw = true; |
||||||
|
} |
||||||
|
|
||||||
|
break; |
||||||
|
} |
||||||
|
|
||||||
|
case EVENT.ZOOM_CANVAS: { |
||||||
|
if (event.user_id == state.following_player) { |
||||||
|
const zoom_level = event.zoom_level; |
||||||
|
const zoom_center = {'x': event.zoom_cx, 'y': event.zoom_cy}; |
||||||
|
const dz = (zoom_level > 0 ? config.zoom_delta : -config.zoom_delta); |
||||||
|
const zoom = Math.pow(1.0 + dz, Math.abs(zoom_level)) |
||||||
|
|
||||||
|
state.canvas.zoom_level = zoom_level; |
||||||
|
state.canvas.target_zoom = zoom; |
||||||
|
state.canvas.zoom_screenp = canvas_to_screen(state, zoom_center); |
||||||
|
|
||||||
|
need_draw = true; |
||||||
|
} |
||||||
|
|
||||||
|
break; |
||||||
|
} |
||||||
|
|
||||||
|
case EVENT.SET_COLOR: { |
||||||
|
state.players[event.user_id].color = event.color; |
||||||
|
break; |
||||||
|
} |
||||||
|
|
||||||
|
case EVENT.SET_WIDTH: { |
||||||
|
state.players[event.user_id].width = event.width; |
||||||
|
break; |
||||||
|
} |
||||||
|
|
||||||
|
case EVENT.STROKE: { |
||||||
|
const point_count = event.coords.length / 2; |
||||||
|
|
||||||
|
if (point_count === 0) { |
||||||
|
break; |
||||||
|
} |
||||||
|
|
||||||
|
let last_stroke = null; |
||||||
|
|
||||||
|
for (let i = state.events.length - 1; i >= 0; --i) { |
||||||
|
if (state.events[i].type === EVENT.STROKE) { |
||||||
|
last_stroke = state.events[i]; |
||||||
|
break; |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
const index_difference = state.events.length - (last_stroke === null ? -1 : last_stroke.index); |
||||||
|
wasm_ensure_by(state, index_difference, event.coords.length); |
||||||
|
|
||||||
|
const pressures = state.wasm.buffers['pressures']; |
||||||
|
const xs = state.wasm.buffers['xs']; |
||||||
|
const ys = state.wasm.buffers['ys']; |
||||||
|
|
||||||
|
event.coords_from = xs.tv.size; |
||||||
|
event.coords_to = xs.tv.size + point_count; |
||||||
|
|
||||||
|
for (let i = 0; i < index_difference - 1; ++i) { |
||||||
|
// Create empty records for all non-stroke events that happened since the last stroke
|
||||||
|
tv_add(state.wasm.buffers['coords_from'].tv, xs.tv.size); |
||||||
|
state.wasm.buffers['coords_from'].used += 4; // 4 bytes, not 4 ints
|
||||||
|
} |
||||||
|
|
||||||
|
// Create actual records for this stroke
|
||||||
|
tv_add(state.wasm.buffers['coords_from'].tv, xs.tv.size + point_count); |
||||||
|
state.wasm.buffers['coords_from'].used += 4; // 4 bytes, not 4 ints
|
||||||
|
|
||||||
|
for (let i = 0; i < event.coords.length; i += 2) { |
||||||
|
tv_add(xs.tv, event.coords[i + 0]); |
||||||
|
tv_add(ys.tv, event.coords[i + 1]); |
||||||
|
} |
||||||
|
|
||||||
|
state.wasm.buffers['xs'].used += point_count * 4; |
||||||
|
state.wasm.buffers['ys'].used += point_count * 4; |
||||||
|
|
||||||
|
tv_append(pressures.tv, event.press); |
||||||
|
state.wasm.buffers['pressures'].used += point_count; |
||||||
|
|
||||||
|
delete event.coords; |
||||||
|
delete event.press; |
||||||
|
|
||||||
|
need_draw = true; |
||||||
|
|
||||||
|
event.index = state.events.length; |
||||||
|
|
||||||
|
geometry_clear_oldest_prestroke(state, context, event.user_id); |
||||||
|
geometry_add_stroke(state, context, event, state.events.length, options.skip_bvh === true); |
||||||
|
|
||||||
|
state.stroke_count++; |
||||||
|
|
||||||
|
break; |
||||||
|
} |
||||||
|
|
||||||
|
case EVENT.UNDO: { |
||||||
|
geometry_add_dummy_stroke(context); |
||||||
|
need_draw = undo(state, context, event, options); |
||||||
|
break; |
||||||
|
} |
||||||
|
|
||||||
|
case EVENT.IMAGE: { |
||||||
|
const p = {'x': event.x, 'y': event.y}; |
||||||
|
|
||||||
|
geometry_add_dummy_stroke(context); |
||||||
|
add_image(context, event.image_id, null, p, event.width, event.height); |
||||||
|
|
||||||
|
try { |
||||||
|
(async () => { |
||||||
|
const url = config.image_url + event.image_id; |
||||||
|
const r = await fetch(config.image_url + event.image_id); |
||||||
|
const blob = await r.blob(); |
||||||
|
|
||||||
|
// NOTE: this will resolve when bitmap is ready, which will be much later
|
||||||
|
const bitmap = await createImageBitmap(blob); |
||||||
|
|
||||||
|
event.width = bitmap.width; |
||||||
|
event.height = bitmap.height; |
||||||
|
|
||||||
|
add_image(context, event.image_id, bitmap, p, bitmap.width, bitmap.height); |
||||||
|
|
||||||
|
// God knows when this will actually complete (it loads the image from the server)
|
||||||
|
// so do not set need_draw. Instead just schedule the draw ourselves when done
|
||||||
|
schedule_draw(state, context); |
||||||
|
})(); |
||||||
|
} catch (e) { |
||||||
|
console.log('Could not load image bitmap:', e); |
||||||
|
} |
||||||
|
|
||||||
|
break; |
||||||
|
} |
||||||
|
|
||||||
|
case EVENT.IMAGE_MOVE: { |
||||||
|
geometry_add_dummy_stroke(context); |
||||||
|
const image_id = event.image_id; |
||||||
|
const image = get_image(context, image_id); |
||||||
|
|
||||||
|
if (image) { |
||||||
|
// if (config.debug_print) console.debug('move image', image_id, 'to', image_event.x, image_event.y);
|
||||||
|
push_image_move(image, event.x, event.y); |
||||||
|
need_draw = true; |
||||||
|
} |
||||||
|
|
||||||
|
break; |
||||||
|
} |
||||||
|
|
||||||
|
case EVENT.IMAGE_SCALE: { |
||||||
|
geometry_add_dummy_stroke(context); |
||||||
|
const image_id = event.image_id; |
||||||
|
const image = get_image(context, image_id); |
||||||
|
|
||||||
|
if (image !== null) { |
||||||
|
push_image_scale(image, event.corner, event.x, event.y); |
||||||
|
need_draw = true; |
||||||
|
} |
||||||
|
|
||||||
|
break; |
||||||
|
} |
||||||
|
|
||||||
|
case EVENT.ERASER: { |
||||||
|
geometry_add_dummy_stroke(context); |
||||||
|
need_draw = true; |
||||||
|
const stroke = state.events[event.stroke_id]; |
||||||
|
stroke.deleted = true; |
||||||
|
if (!options.skip_bvh) { |
||||||
|
bvh_delete_stroke(state, stroke); |
||||||
|
} |
||||||
|
break; |
||||||
|
} |
||||||
|
|
||||||
|
default: { |
||||||
|
console.error('fuck'); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
return need_draw; |
||||||
|
} |
||||||
|
|
||||||
|
function handle_message(state, context, d) { |
||||||
|
const message_type = des_u32(d); |
||||||
|
let do_draw = false; |
||||||
|
|
||||||
|
// if (config.debug_print) console.debug(message_type);
|
||||||
|
|
||||||
|
switch (message_type) { |
||||||
|
case MESSAGE.JOIN: |
||||||
|
case MESSAGE.INIT: { |
||||||
|
console.time('init'); |
||||||
|
|
||||||
|
state.online = true; |
||||||
|
state.server_lsn = des_u32(d); |
||||||
|
|
||||||
|
if (state.server_lsn > state.lsn) { |
||||||
|
// Server knows something that we don't
|
||||||
|
state.lsn = state.server_lsn; |
||||||
|
} |
||||||
|
|
||||||
|
let color = config.default_color; |
||||||
|
let width = config.default_width; |
||||||
|
|
||||||
|
if (message_type === MESSAGE.JOIN) { |
||||||
|
localStorage.setItem('sessionId', des_u32(d)); |
||||||
|
if (config.debug_print) console.debug('join in'); |
||||||
|
} else { |
||||||
|
color = des_u32(d); |
||||||
|
width = des_u16(d); |
||||||
|
if (config.debug_print) console.debug('init in'); |
||||||
|
} |
||||||
|
|
||||||
|
state.me = parseInt(localStorage.getItem('sessionId')); |
||||||
|
|
||||||
|
init_player_defaults(state, state.me); |
||||||
|
|
||||||
|
set_color_u32(state, color); |
||||||
|
|
||||||
|
document.querySelector('#stroke-width').value = width; |
||||||
|
fire_event(state, width_event(width)); |
||||||
|
|
||||||
|
const event_count = des_u32(d); |
||||||
|
const user_count = des_u32(d); |
||||||
|
const total_points = des_u32(d); |
||||||
|
|
||||||
|
wasm_ensure_by(state, event_count, round_to_pow2(total_points * 2, 4096)); |
||||||
|
|
||||||
|
if (config.debug_print) console.debug(`${event_count} events in init`); |
||||||
|
|
||||||
|
state.events.length = 0; |
||||||
|
|
||||||
|
for (let i = 0; i < user_count; ++i) { |
||||||
|
const user_id = des_u32(d); |
||||||
|
const user_color = des_u32(d); |
||||||
|
const user_width = des_u16(d); |
||||||
|
const user_online = des_u8(d); |
||||||
|
|
||||||
|
init_player_defaults(state, user_id, user_color, user_width); |
||||||
|
state.players[user_id].online = user_online === 1; |
||||||
|
} |
||||||
|
|
||||||
|
des_align(d, 4); |
||||||
|
|
||||||
|
for (let i = 0; i < event_count; ++i) { |
||||||
|
const event = des_event(d, state); |
||||||
|
handle_event(state, context, event, {'skip_bvh': true}); |
||||||
|
|
||||||
|
if (event.type !== EVENT.STROKE || event.coords_to - event.coords_from > 0) { |
||||||
|
state.events.push(event); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
state.sn = event_count; |
||||||
|
|
||||||
|
bvh_construct(state); |
||||||
|
|
||||||
|
do_draw = true; |
||||||
|
|
||||||
|
send_ack(event_count); |
||||||
|
sync_queue(state); |
||||||
|
|
||||||
|
console.timeEnd('init'); |
||||||
|
|
||||||
|
update_cursor(state); |
||||||
|
draw_html(state); |
||||||
|
|
||||||
|
break; |
||||||
|
} |
||||||
|
|
||||||
|
case MESSAGE.FIRE: { |
||||||
|
const event = des_event(d); |
||||||
|
const need_draw = handle_event(state, context, event); |
||||||
|
|
||||||
|
do_draw = do_draw || need_draw; |
||||||
|
|
||||||
|
break; |
||||||
|
} |
||||||
|
|
||||||
|
case MESSAGE.ACK: { |
||||||
|
const lsn = des_u32(d); |
||||||
|
|
||||||
|
if (config.debug_print) console.debug(`ack ${lsn} in`); |
||||||
|
|
||||||
|
if (lsn > state.server_lsn) { |
||||||
|
// ACKs may arrive out of order
|
||||||
|
state.server_lsn = lsn; |
||||||
|
} |
||||||
|
|
||||||
|
break; |
||||||
|
} |
||||||
|
|
||||||
|
case MESSAGE.SYN: { |
||||||
|
const sn = des_u32(d); |
||||||
|
const count = des_u32(d); |
||||||
|
|
||||||
|
const we_expect = sn - state.sn; |
||||||
|
const first = count - we_expect; |
||||||
|
|
||||||
|
if (config.debug_print) console.debug(`syn ${sn} in`); |
||||||
|
|
||||||
|
for (let i = 0; i < count; ++i) { |
||||||
|
const event = des_event(d, state); |
||||||
|
if (i >= first) { |
||||||
|
const need_draw = handle_event(state, context, event); |
||||||
|
do_draw = do_draw || need_draw; |
||||||
|
state.events.push(event); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
state.sn = sn; |
||||||
|
|
||||||
|
send_ack(sn); |
||||||
|
|
||||||
|
break; |
||||||
|
} |
||||||
|
|
||||||
|
default: { |
||||||
|
console.error('fuck'); |
||||||
|
return; |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
if (do_draw) { |
||||||
|
schedule_draw(state, context); |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,436 @@ |
|||||||
|
function serializer_create(size) { |
||||||
|
const buffer = new ArrayBuffer(size); |
||||||
|
return { |
||||||
|
'offset': 0, |
||||||
|
'size': size, |
||||||
|
'buffer': buffer, |
||||||
|
'view': new DataView(buffer), |
||||||
|
'strview': new Uint8Array(buffer), |
||||||
|
|
||||||
|
'need_gpu_allocate': true, // need to call glBufferData to create a GPU buffer of size serializer.size
|
||||||
|
'gpu_upload_from': 0, // need to call glBufferSubData/glTexSubImage2D for bytes in [serializer.gpu_upload_from, serializer.offset)
|
||||||
|
}; |
||||||
|
} |
||||||
|
|
||||||
|
function ser_ensure(s, size) { |
||||||
|
if (s.size < size) { |
||||||
|
const new_s = serializer_create(Math.ceil(size * 2)); |
||||||
|
|
||||||
|
new_s.strview.set(s.strview); |
||||||
|
new_s.offset = s.offset; |
||||||
|
|
||||||
|
return new_s; |
||||||
|
} |
||||||
|
|
||||||
|
return s; |
||||||
|
} |
||||||
|
|
||||||
|
function ser_ensure_by(s, by) { |
||||||
|
if (s.offset + by > s.size) { |
||||||
|
const new_s = serializer_create(Math.ceil((s.size + by) * 2)); |
||||||
|
|
||||||
|
new_s.strview.set(s.strview); |
||||||
|
new_s.offset = s.offset; |
||||||
|
|
||||||
|
return new_s; |
||||||
|
} |
||||||
|
|
||||||
|
return s; |
||||||
|
} |
||||||
|
|
||||||
|
function ser_clear(s) { |
||||||
|
s.offset = 0; |
||||||
|
s.gpu_upload_from = 0; |
||||||
|
} |
||||||
|
|
||||||
|
function ser_u8(s, value) { |
||||||
|
s.view.setUint8(s.offset, value); |
||||||
|
s.offset += 1; |
||||||
|
} |
||||||
|
|
||||||
|
function ser_u16(s, value) { |
||||||
|
s.view.setUint16(s.offset, value, true); |
||||||
|
s.offset += 2; |
||||||
|
} |
||||||
|
|
||||||
|
function ser_f32(s, value) { |
||||||
|
s.view.setFloat32(s.offset, value, true); |
||||||
|
s.offset += 4; |
||||||
|
} |
||||||
|
|
||||||
|
function ser_u32(s, value) { |
||||||
|
s.view.setUint32(s.offset, value, true); |
||||||
|
s.offset += 4; |
||||||
|
} |
||||||
|
|
||||||
|
function ser_s32(s, value) { |
||||||
|
s.view.setInt32(s.offset, value, true); |
||||||
|
s.offset += 4; |
||||||
|
} |
||||||
|
|
||||||
|
function ser_align(s, to) { |
||||||
|
// TODO: non-stupid version of this
|
||||||
|
while (s.offset % to != 0) { |
||||||
|
s.offset++; |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
function ser_event(s, event) { |
||||||
|
ser_u32(s, event.type); |
||||||
|
|
||||||
|
switch (event.type) { |
||||||
|
case EVENT.PREDRAW: { |
||||||
|
ser_f32(s, event.x); |
||||||
|
ser_f32(s, event.y); |
||||||
|
break; |
||||||
|
} |
||||||
|
|
||||||
|
case EVENT.CLEAR: |
||||||
|
case EVENT.LIFT: { |
||||||
|
break; |
||||||
|
} |
||||||
|
|
||||||
|
case EVENT.MOVE_CURSOR: { |
||||||
|
ser_f32(s, event.x); |
||||||
|
ser_f32(s, event.y); |
||||||
|
break; |
||||||
|
} |
||||||
|
|
||||||
|
case EVENT.MOVE_CANVAS: { |
||||||
|
ser_u32(s, event.offset_x); |
||||||
|
ser_u32(s, event.offset_y); |
||||||
|
ser_s32(s, event.zoom_level); |
||||||
|
break; |
||||||
|
} |
||||||
|
|
||||||
|
case EVENT.ZOOM_CANVAS: { |
||||||
|
ser_s32(s, event.zoom_level); |
||||||
|
ser_f32(s, event.zoom_cx); |
||||||
|
ser_f32(s, event.zoom_cy); |
||||||
|
break; |
||||||
|
} |
||||||
|
|
||||||
|
case EVENT.SET_COLOR: { |
||||||
|
ser_u32(s, event.color); |
||||||
|
break; |
||||||
|
} |
||||||
|
|
||||||
|
case EVENT.SET_WIDTH: { |
||||||
|
ser_u16(s, event.width); |
||||||
|
break; |
||||||
|
} |
||||||
|
|
||||||
|
case EVENT.STROKE: { |
||||||
|
ser_u16(s, event.points.length); |
||||||
|
ser_u16(s, event.width); |
||||||
|
ser_u32(s, event.color); |
||||||
|
|
||||||
|
if (config.debug_print) console.debug('original', event.points); |
||||||
|
|
||||||
|
for (const point of event.points) { |
||||||
|
ser_f32(s, point.x); |
||||||
|
ser_f32(s, point.y); |
||||||
|
} |
||||||
|
|
||||||
|
for (const point of event.points) { |
||||||
|
ser_u8(s, point.pressure); |
||||||
|
} |
||||||
|
|
||||||
|
ser_align(s, 4); |
||||||
|
|
||||||
|
break; |
||||||
|
} |
||||||
|
|
||||||
|
case EVENT.IMAGE: |
||||||
|
case EVENT.IMAGE_MOVE: { |
||||||
|
const image_id = parseInt(event.image_id); |
||||||
|
ser_u32(s, image_id); |
||||||
|
ser_f32(s, event.x); |
||||||
|
ser_f32(s, event.y); |
||||||
|
ser_u32(s, event.width); |
||||||
|
ser_u32(s, event.height); |
||||||
|
break; |
||||||
|
} |
||||||
|
|
||||||
|
case EVENT.IMAGE_SCALE: { |
||||||
|
const image_id = parseInt(event.image_id); |
||||||
|
ser_u32(s, image_id); |
||||||
|
ser_u32(s, event.corner); // which corner was moved
|
||||||
|
ser_f32(s, event.x); // where corner was moved to (canvas coordinates)
|
||||||
|
ser_f32(s, event.y); |
||||||
|
break; |
||||||
|
} |
||||||
|
|
||||||
|
case EVENT.UNDO: |
||||||
|
case EVENT.REDO: { |
||||||
|
break; |
||||||
|
} |
||||||
|
|
||||||
|
case EVENT.ERASER: { |
||||||
|
ser_u32(s, event.stroke_id); |
||||||
|
break; |
||||||
|
} |
||||||
|
|
||||||
|
default: { |
||||||
|
console.error('fuck'); |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
function send_ack(sn) { |
||||||
|
const s = serializer_create(4 + 4); |
||||||
|
|
||||||
|
ser_u32(s, MESSAGE.ACK); |
||||||
|
ser_u32(s, sn); |
||||||
|
|
||||||
|
if (config.debug_print) console.debug(`ack ${sn} out`); |
||||||
|
|
||||||
|
try { |
||||||
|
if (ws) ws.send(s.buffer); |
||||||
|
} catch(e) { |
||||||
|
ws.close(); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
function send_follow(player_id) { |
||||||
|
const s = serializer_create(4 + 4); |
||||||
|
|
||||||
|
player_id = player_id === null ? -1 : player_id; |
||||||
|
|
||||||
|
ser_u32(s, MESSAGE.FOLLOW); |
||||||
|
ser_u32(s, player_id); |
||||||
|
|
||||||
|
if (config.debug_print) console.debug(`follow ${player_id} out`); |
||||||
|
|
||||||
|
try { |
||||||
|
if (ws) ws.send(s.buffer); |
||||||
|
} catch (e) { |
||||||
|
ws.close(); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
function sync_queue(state) { |
||||||
|
if (ws === null) { |
||||||
|
if (config.debug_print) console.debug('socket has closed, stopping SYNs'); |
||||||
|
return; |
||||||
|
} |
||||||
|
|
||||||
|
let size = 4 + 4 + 4; // opcode + lsn + event count
|
||||||
|
let count = state.lsn - state.server_lsn; |
||||||
|
|
||||||
|
if (count === 0) { |
||||||
|
if (config.debug_print) console.debug('server ACKed all events, clearing queue'); |
||||||
|
state.queue.length = 0; |
||||||
|
return; |
||||||
|
} |
||||||
|
|
||||||
|
for (let i = count - 1; i >= 0; --i) { |
||||||
|
const event = state.queue[state.queue.length - 1 - i]; |
||||||
|
size += event_size(event); |
||||||
|
} |
||||||
|
|
||||||
|
const s = serializer_create(size); |
||||||
|
|
||||||
|
ser_u32(s, MESSAGE.SYN); |
||||||
|
ser_u32(s, state.lsn); |
||||||
|
ser_u32(s, count); |
||||||
|
|
||||||
|
for (let i = count - 1; i >= 0; --i) { |
||||||
|
const event = state.queue[state.queue.length - 1 - i]; |
||||||
|
ser_event(s, event); |
||||||
|
} |
||||||
|
|
||||||
|
if (config.debug_print) console.debug(`syn ${state.lsn} out`); |
||||||
|
|
||||||
|
try { |
||||||
|
if (ws) ws.send(s.buffer); |
||||||
|
} catch(e) { |
||||||
|
ws.close(); |
||||||
|
} |
||||||
|
|
||||||
|
setTimeout(() => sync_queue(state), config.sync_timeout); |
||||||
|
} |
||||||
|
|
||||||
|
function push_event(state, event) { |
||||||
|
state.lsn += 1; |
||||||
|
|
||||||
|
switch (event.type) { |
||||||
|
case EVENT.STROKE: { |
||||||
|
state.queue.push({ |
||||||
|
'type': EVENT.STROKE, |
||||||
|
'points': event.points, |
||||||
|
'width': event.width, |
||||||
|
'color': event.color, |
||||||
|
}); |
||||||
|
|
||||||
|
break; |
||||||
|
} |
||||||
|
|
||||||
|
case EVENT.RULER: { |
||||||
|
event.type = EVENT.STROKE; |
||||||
|
state.queue.push(event); |
||||||
|
break; |
||||||
|
} |
||||||
|
|
||||||
|
case EVENT.ERASER: |
||||||
|
case EVENT.IMAGE: |
||||||
|
case EVENT.IMAGE_MOVE: |
||||||
|
case EVENT.IMAGE_SCALE: |
||||||
|
case EVENT.UNDO: |
||||||
|
case EVENT.REDO: { |
||||||
|
state.queue.push(event); |
||||||
|
break; |
||||||
|
} |
||||||
|
|
||||||
|
default: { |
||||||
|
console.error('fuck'); |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
// Queue an event and initialize repated sends until ACKed
|
||||||
|
function queue_event(state, event, skip = false) { |
||||||
|
if (!state.online) { return; } |
||||||
|
|
||||||
|
push_event(state, event); |
||||||
|
|
||||||
|
if (skip) { |
||||||
|
return; |
||||||
|
} |
||||||
|
|
||||||
|
if (state.timers.queue_sync) { |
||||||
|
clearTimeout(state.timers.queue_sync); |
||||||
|
} |
||||||
|
|
||||||
|
sync_queue(state); |
||||||
|
} |
||||||
|
|
||||||
|
// Fire and forget. Doesn't do anything if we are offline
|
||||||
|
function fire_event(state, event) { |
||||||
|
if (!state.online) { return; } |
||||||
|
|
||||||
|
const s = serializer_create(4 + event_size(event)); |
||||||
|
|
||||||
|
ser_u32(s, MESSAGE.FIRE); |
||||||
|
ser_event(s, event); |
||||||
|
|
||||||
|
try { |
||||||
|
if (ws) ws.send(s.buffer); |
||||||
|
} catch(e) { |
||||||
|
ws.close(); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
function predraw_event(x, y) { |
||||||
|
return { |
||||||
|
'type': EVENT.PREDRAW, |
||||||
|
'x': x, |
||||||
|
'y': y |
||||||
|
}; |
||||||
|
} |
||||||
|
|
||||||
|
function lift_event() { |
||||||
|
return { |
||||||
|
'type': EVENT.LIFT, |
||||||
|
}; |
||||||
|
} |
||||||
|
|
||||||
|
function color_event(color_u32) { |
||||||
|
return { |
||||||
|
'type': EVENT.SET_COLOR, |
||||||
|
'color': color_u32, |
||||||
|
}; |
||||||
|
} |
||||||
|
|
||||||
|
function width_event(width) { |
||||||
|
return { |
||||||
|
'type': EVENT.SET_WIDTH, |
||||||
|
'width': width, |
||||||
|
}; |
||||||
|
} |
||||||
|
|
||||||
|
function image_event(image_id, x, y, width, height) { |
||||||
|
return { |
||||||
|
'type': EVENT.IMAGE, |
||||||
|
'image_id': image_id, |
||||||
|
'x': x, |
||||||
|
'y': y, |
||||||
|
'width': width, |
||||||
|
'height': height, |
||||||
|
}; |
||||||
|
} |
||||||
|
|
||||||
|
function image_move_event(image_id, x, y) { |
||||||
|
return { |
||||||
|
'type': EVENT.IMAGE_MOVE, |
||||||
|
'image_id': image_id, |
||||||
|
'x': x, |
||||||
|
'y': y, |
||||||
|
}; |
||||||
|
} |
||||||
|
|
||||||
|
function image_scale_event(image_id, corner, x, y) { |
||||||
|
return { |
||||||
|
'type': EVENT.IMAGE_SCALE, |
||||||
|
'image_id': image_id, |
||||||
|
'corner': corner, |
||||||
|
'x': x, |
||||||
|
'y': y, |
||||||
|
}; |
||||||
|
} |
||||||
|
|
||||||
|
function stroke_event(state) { |
||||||
|
const stroke = geometry_prepare_stroke(state); |
||||||
|
|
||||||
|
return { |
||||||
|
'type': EVENT.STROKE, |
||||||
|
'points': stroke.points, |
||||||
|
'width': stroke.width, |
||||||
|
'color': stroke.color, |
||||||
|
}; |
||||||
|
} |
||||||
|
|
||||||
|
function clear_event(state) { |
||||||
|
return { |
||||||
|
'type': EVENT.CLEAR |
||||||
|
}; |
||||||
|
} |
||||||
|
|
||||||
|
function movecursor_event(x, y) { |
||||||
|
return { |
||||||
|
'type': EVENT.MOVE_CURSOR, |
||||||
|
'x': x, |
||||||
|
'y': y, |
||||||
|
}; |
||||||
|
} |
||||||
|
|
||||||
|
function movecanvas_event(state) { |
||||||
|
return { |
||||||
|
'type': EVENT.MOVE_CANVAS, |
||||||
|
'offset_x': state.canvas.offset.x, |
||||||
|
'offset_y': state.canvas.offset.y, |
||||||
|
'zoom_level': state.canvas.zoom_level, |
||||||
|
}; |
||||||
|
} |
||||||
|
|
||||||
|
function zoomcanvas_event(state, zoom_cx, zoom_cy) { |
||||||
|
return { |
||||||
|
'type': EVENT.ZOOM_CANVAS, |
||||||
|
'zoom_level': state.canvas.zoom_level, |
||||||
|
'zoom_cx': zoom_cx, |
||||||
|
'zoom_cy': zoom_cy, |
||||||
|
}; |
||||||
|
} |
||||||
|
|
||||||
|
function undo_event(state) { |
||||||
|
return { |
||||||
|
'type': EVENT.UNDO, |
||||||
|
}; |
||||||
|
} |
||||||
|
|
||||||
|
function eraser_event(stroke_id) { |
||||||
|
return { |
||||||
|
'type': EVENT.ERASER, |
||||||
|
'stroke_id': stroke_id, |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,37 @@ |
|||||||
|
const config = { |
||||||
|
ws_url: `wss://${window.location.host}/ws/`, |
||||||
|
ping_url: `https://${window.location.host}/api/ping`, |
||||||
|
image_url: `https://${window.location.host}/images/`, |
||||||
|
sync_timeout: 1000, |
||||||
|
ws_reconnect_timeout: 2000, |
||||||
|
brush_preview_timeout: 1000, |
||||||
|
second_finger_timeout: 500, |
||||||
|
buffer_first_touchmoves: 5, |
||||||
|
debug_print: false, |
||||||
|
draw_bvh: false, |
||||||
|
draw_fullnodes: false, |
||||||
|
zoom_delta: 0.05, |
||||||
|
min_zoom_level: -275, |
||||||
|
max_zoom_level: 200, |
||||||
|
initial_offline_timeout: 1000, |
||||||
|
default_color: 0x00, |
||||||
|
default_width: 8, |
||||||
|
bytes_per_instance: 4 * 2 + 4, // axy, stroke_id
|
||||||
|
bytes_per_stroke: 2 * 3 + 2, // r, g, b, width
|
||||||
|
initial_static_bytes: 4096 * 16, |
||||||
|
initial_dynamic_bytes: 4096, |
||||||
|
initial_wasm_bytes: 4096, |
||||||
|
stroke_texture_size: 1024, // means no more than 1024^2 = 1M strokes in total (this is a LOT. HMH blackboard has like 80K)
|
||||||
|
dynamic_stroke_texture_size: 128, // means no more than 128^2 = 16K dynamic strokes at once
|
||||||
|
ui_texture_size: 16, |
||||||
|
bvh_fullnode_depth: 6, |
||||||
|
pattern_fadeout_min: 0.3, |
||||||
|
pattern_fadeout_max: 0.75, |
||||||
|
min_pressure: 50, |
||||||
|
benchmark: { |
||||||
|
zoom_level: -18, |
||||||
|
offset: { x: 654, y: 372 }, |
||||||
|
frames: 500, |
||||||
|
}, |
||||||
|
}; |
||||||
|
|
@ -1,315 +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) { |
|
||||||
storage.canvas.max_scroll_x = storage.canvas.width - window.innerWidth; |
|
||||||
storage.canvas.max_scroll_y = storage.canvas.height - window.innerHeight; |
|
||||||
} |
|
||||||
|
|
||||||
async function on_drop(e) { |
|
||||||
e.preventDefault(); |
|
||||||
const file = e.dataTransfer.files[0]; |
|
||||||
const bitmap = await createImageBitmap(file); |
|
||||||
|
|
||||||
const x = storage.cursor.x - Math.round(bitmap.width / 2); |
|
||||||
const y = storage.cursor.y - Math.round(bitmap.height / 2); |
|
||||||
|
|
||||||
// storage.ctx0.drawImage(bitmap, x, y);
|
|
||||||
|
|
||||||
const form_data = new FormData(); |
|
||||||
form_data.append('file', file); |
|
||||||
|
|
||||||
const resp = await fetch(`/api/image?deskId=${storage.desk_id}`, { |
|
||||||
method: 'post', |
|
||||||
body: form_data, |
|
||||||
}) |
|
||||||
|
|
||||||
if (resp.ok) { |
|
||||||
const image_id = await resp.text(); |
|
||||||
const event = image_event(image_id, x, y); |
|
||||||
await queue_event(event); |
|
||||||
} |
|
||||||
|
|
||||||
return false; |
|
||||||
} |
|
||||||
|
|
||||||
function on_wheel(e) { |
|
||||||
const x = Math.round((e.clientX + storage.canvas.offset_x) / storage.canvas.zoom); |
|
||||||
const y = Math.round((e.clientY + storage.canvas.offset_y) / storage.canvas.zoom); |
|
||||||
|
|
||||||
const dz = (e.deltaY < 0 ? 0.1 : -0.1); |
|
||||||
|
|
||||||
storage.canvas.zoom += dz; |
|
||||||
|
|
||||||
if (storage.canvas.zoom > storage.max_zoom) { |
|
||||||
storage.canvas.zoom = storage.max_zoom; |
|
||||||
return; |
|
||||||
} |
|
||||||
|
|
||||||
if (storage.canvas.zoom < storage.min_zoom) { |
|
||||||
storage.canvas.zoom = storage.min_zoom; |
|
||||||
return; |
|
||||||
} |
|
||||||
|
|
||||||
const zoom_offset_x = Math.round(dz * x); |
|
||||||
const zoom_offset_y = Math.round(dz * y); |
|
||||||
|
|
||||||
storage.canvas.offset_x += zoom_offset_x; |
|
||||||
storage.canvas.offset_y += zoom_offset_y; |
|
||||||
|
|
||||||
move_canvas(); |
|
||||||
} |
|
||||||
|
|
||||||
function cancel(e) { |
|
||||||
e.preventDefault(); |
|
||||||
return false; |
|
||||||
} |
|
||||||
|
|
||||||
function update_brush() { |
|
||||||
elements.brush_preview.classList.remove('dhide'); |
|
||||||
|
|
||||||
const color = elements.brush_color.value; |
|
||||||
const width = elements.brush_width.value; |
|
||||||
|
|
||||||
storage.cursor.color = color; |
|
||||||
storage.cursor.width = width; |
|
||||||
|
|
||||||
const x = Math.round(storage.cursor.x - width / 2); |
|
||||||
const y = Math.round(storage.cursor.y - width / 2); |
|
||||||
|
|
||||||
elements.brush_preview.style.transform = `translate(${x}px, ${y}px)`; |
|
||||||
elements.brush_preview.style.width = width + 'px'; |
|
||||||
elements.brush_preview.style.height = width + 'px'; |
|
||||||
elements.brush_preview.style.background = color; |
|
||||||
|
|
||||||
if (storage.timers.brush_preview) { |
|
||||||
clearTimeout(storage.timers.brush_preview); |
|
||||||
} |
|
||||||
|
|
||||||
storage.timers.brush_preview = setTimeout(() => { |
|
||||||
elements.brush_preview.classList.add('dhide'); |
|
||||||
}, 1000); |
|
||||||
} |
|
@ -1,66 +0,0 @@ |
|||||||
function draw_stroke(stroke) { |
|
||||||
const points = stroke.points; |
|
||||||
|
|
||||||
if (points.length === 0) { |
|
||||||
return; |
|
||||||
} |
|
||||||
|
|
||||||
// if (config.debug_print) console.debug(points)
|
|
||||||
|
|
||||||
storage.ctx0.beginPath(); |
|
||||||
storage.ctx0.moveTo(points[0].x, points[0].y); |
|
||||||
storage.ctx0.strokeStyle = color_from_u32(stroke.color); |
|
||||||
storage.ctx0.lineWidth = stroke.width; |
|
||||||
|
|
||||||
for (let i = 1; i < points.length; ++i) { |
|
||||||
const p = points[i]; |
|
||||||
storage.ctx0.lineTo(p.x, p.y); |
|
||||||
} |
|
||||||
|
|
||||||
storage.ctx0.stroke(); |
|
||||||
} |
|
||||||
|
|
||||||
function redraw_predraw() { |
|
||||||
storage.ctx1.clearRect(0, 0, storage.ctx1.canvas.width, storage.ctx1.canvas.height); |
|
||||||
} |
|
||||||
|
|
||||||
function predraw_user(user_id, event) { |
|
||||||
if (!(user_id in storage.predraw)) { |
|
||||||
storage.predraw[user_id] = []; |
|
||||||
} |
|
||||||
|
|
||||||
storage.ctx1.beginPath(); |
|
||||||
if (storage.predraw[user_id].length > 0) { |
|
||||||
const last = storage.predraw[user_id][storage.predraw[user_id].length - 1]; |
|
||||||
storage.ctx1.moveTo(last.x, last.y); |
|
||||||
storage.ctx1.lineTo(event.x, event.y); |
|
||||||
} else { |
|
||||||
storage.ctx1.moveTo(event.x, event.y); |
|
||||||
} |
|
||||||
storage.ctx1.stroke(); |
|
||||||
|
|
||||||
storage.predraw[user_id].push({ 'x': event.x, 'y': event.y }); |
|
||||||
} |
|
||||||
|
|
||||||
function redraw_region(bbox) { |
|
||||||
if (bbox.xmin === bbox.xmax || bbox.ymin === bbox.ymax) { |
|
||||||
return; |
|
||||||
} |
|
||||||
|
|
||||||
storage.ctx0.save(); |
|
||||||
storage.ctx0.clearRect(bbox.xmin, bbox.ymin, bbox.xmax - bbox.xmin, bbox.ymax - bbox.ymin); |
|
||||||
|
|
||||||
storage.ctx0.beginPath(); |
|
||||||
storage.ctx0.rect(bbox.xmin, bbox.ymin, bbox.xmax - bbox.xmin, bbox.ymax - bbox.ymin); |
|
||||||
storage.ctx0.clip(); |
|
||||||
|
|
||||||
for (const event of storage.events) { |
|
||||||
if (event.type === EVENT.STROKE && !event.deleted) { |
|
||||||
if (stroke_intersects_region(event.points, bbox)) { |
|
||||||
draw_stroke(event); |
|
||||||
} |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
storage.ctx0.restore(); |
|
||||||
} |
|
Before Width: | Height: | Size: 671 B |
Before Width: | Height: | Size: 957 B |
@ -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(" ")}]`; |
||||||
|
} |
||||||
|
} |
Before Width: | Height: | Size: 3.0 KiB After Width: | Height: | Size: 691 B |
Before Width: | Height: | Size: 1.7 KiB After Width: | Height: | Size: 643 B |
Before Width: | Height: | Size: 1.9 KiB After Width: | Height: | Size: 494 B |
Before Width: | Height: | Size: 1.9 KiB After Width: | Height: | Size: 457 B |
@ -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! |
@ -0,0 +1,47 @@ |
|||||||
|
let thread_id = null; |
||||||
|
let exports = null; |
||||||
|
|
||||||
|
async function init(tid, memory, heap_base) { |
||||||
|
thread_id = tid; |
||||||
|
|
||||||
|
const result = await WebAssembly.instantiateStreaming(fetch('wasm/lod.wasm'), { |
||||||
|
env: { 'memory': memory } |
||||||
|
}); |
||||||
|
|
||||||
|
exports = result.instance.exports; |
||||||
|
exports.set_sp(heap_base - thread_id * 16 * 4096); // 64K stack
|
||||||
|
|
||||||
|
postMessage({ 'type': 'init_done' }); |
||||||
|
} |
||||||
|
|
||||||
|
function work(indices_base, indices_count, zoom, offsets) { |
||||||
|
try { |
||||||
|
exports.do_lod( |
||||||
|
indices_base, indices_count, zoom, |
||||||
|
offsets['coords_from'], |
||||||
|
offsets['xs'], |
||||||
|
offsets['ys'], |
||||||
|
offsets['pressures'], |
||||||
|
offsets['result_buffers'] + thread_id * 4, |
||||||
|
offsets['result_counts'] + thread_id * 4, |
||||||
|
); |
||||||
|
} catch (e) { |
||||||
|
console.error('WASM:', e); |
||||||
|
} |
||||||
|
|
||||||
|
postMessage({ 'type': 'lod_done' }); |
||||||
|
} |
||||||
|
|
||||||
|
onmessage = (e) => { |
||||||
|
const d = e.data; |
||||||
|
|
||||||
|
if (d.type === 'init') { |
||||||
|
init(d.thread_id, d.memory, d.heap_base); |
||||||
|
} else if (d.type === 'lod') { |
||||||
|
work(d.indices_base, d.indices_count, d.zoom, d.offsets); |
||||||
|
} else { |
||||||
|
console.error('unexpected worker command:', d.type); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
|
@ -1,345 +0,0 @@ |
|||||||
function deserializer_create(buffer, dataview) { |
|
||||||
return { |
|
||||||
'offset': 0, |
|
||||||
'size': buffer.byteLength, |
|
||||||
'buffer': buffer, |
|
||||||
'view': dataview, |
|
||||||
'strview': new Uint8Array(buffer), |
|
||||||
}; |
|
||||||
} |
|
||||||
|
|
||||||
function des_u8(d) { |
|
||||||
const value = d.view.getUint8(d.offset); |
|
||||||
d.offset += 1; |
|
||||||
return value; |
|
||||||
} |
|
||||||
|
|
||||||
function des_u16(d) { |
|
||||||
const value = d.view.getUint16(d.offset, true); |
|
||||||
d.offset += 2; |
|
||||||
return value; |
|
||||||
} |
|
||||||
|
|
||||||
function des_s16(d) { |
|
||||||
const value = d.view.getInt16(d.offset, true); |
|
||||||
d.offset += 2; |
|
||||||
return value; |
|
||||||
} |
|
||||||
|
|
||||||
function des_u32(d) { |
|
||||||
const value = d.view.getUint32(d.offset, true); |
|
||||||
d.offset += 4; |
|
||||||
return value; |
|
||||||
} |
|
||||||
|
|
||||||
function des_u16array(d, count) { |
|
||||||
const result = []; |
|
||||||
|
|
||||||
for (let i = 0; i < count; ++i) { |
|
||||||
const item = d.view.getUint16(d.offset, true); |
|
||||||
d.offset += 2; |
|
||||||
result.push(item); |
|
||||||
} |
|
||||||
|
|
||||||
return result; |
|
||||||
} |
|
||||||
|
|
||||||
function des_event(d) { |
|
||||||
const event = {}; |
|
||||||
|
|
||||||
event.type = des_u8(d); |
|
||||||
event.user_id = des_u32(d); |
|
||||||
|
|
||||||
switch (event.type) { |
|
||||||
case EVENT.PREDRAW: { |
|
||||||
event.x = des_u16(d); |
|
||||||
event.y = des_u16(d); |
|
||||||
break; |
|
||||||
} |
|
||||||
|
|
||||||
case EVENT.STROKE: { |
|
||||||
const stroke_id = des_u32(d); |
|
||||||
const point_count = des_u16(d); |
|
||||||
const width = des_u16(d); |
|
||||||
const color = des_u32(d); |
|
||||||
const coords = des_u16array(d, point_count * 2); |
|
||||||
|
|
||||||
event.stroke_id = stroke_id; |
|
||||||
event.points = []; |
|
||||||
|
|
||||||
for (let i = 0; i < point_count; ++i) { |
|
||||||
const x = coords[2 * i + 0]; |
|
||||||
const y = coords[2 * i + 1]; |
|
||||||
event.points.push({'x': x, 'y': y}); |
|
||||||
} |
|
||||||
|
|
||||||
event.color = color; |
|
||||||
event.width = width; |
|
||||||
|
|
||||||
break; |
|
||||||
} |
|
||||||
|
|
||||||
case EVENT.IMAGE: |
|
||||||
case EVENT.IMAGE_MOVE: { |
|
||||||
event.image_id = des_u32(d); |
|
||||||
event.x = des_s16(d); // stored as u16, but actually is s16
|
|
||||||
event.y = des_s16(d); // stored as u16, but actually is s16
|
|
||||||
break; |
|
||||||
} |
|
||||||
|
|
||||||
case EVENT.UNDO: |
|
||||||
case EVENT.REDO: { |
|
||||||
break; |
|
||||||
} |
|
||||||
|
|
||||||
case EVENT.ERASER: { |
|
||||||
event.stroke_id = des_u32(d); |
|
||||||
break; |
|
||||||
} |
|
||||||
|
|
||||||
default: { |
|
||||||
console.error('fuck'); |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
return event; |
|
||||||
} |
|
||||||
|
|
||||||
function bitmap_bbox(event) { |
|
||||||
const x = (event.x <= storage.canvas.width ? event.x : event.x - 65536); |
|
||||||
const y = (event.y <= storage.canvas.height ? event.y : event.y - 65536); |
|
||||||
|
|
||||||
const bbox = { |
|
||||||
'xmin': x, |
|
||||||
'xmax': x + event.bitmap.width, |
|
||||||
'ymin': y, |
|
||||||
'ymax': y + event.bitmap.height |
|
||||||
}; |
|
||||||
|
|
||||||
return bbox; |
|
||||||
} |
|
||||||
|
|
||||||
async function handle_event(event) { |
|
||||||
if (config.debug_print) console.debug(`event type ${event.type} from user ${event.user_id}`); |
|
||||||
|
|
||||||
// TODO(@speed): do not handle locally predicted events
|
|
||||||
|
|
||||||
switch (event.type) { |
|
||||||
case EVENT.STROKE: { |
|
||||||
if (event.user_id in storage.predraw || event.user_id === storage.me.id) { |
|
||||||
storage.predraw[event.user_id] = []; |
|
||||||
redraw_predraw(); |
|
||||||
} |
|
||||||
|
|
||||||
draw_stroke(event); |
|
||||||
|
|
||||||
break; |
|
||||||
} |
|
||||||
|
|
||||||
case EVENT.UNDO: { |
|
||||||
for (let i = storage.events.length - 1; i >=0; --i) { |
|
||||||
const other_event = storage.events[i]; |
|
||||||
|
|
||||||
// Users can only undo their own, undeleted (not already undone) events
|
|
||||||
if (other_event.user_id === event.user_id && !other_event.deleted) { |
|
||||||
if (other_event.type === EVENT.STROKE) { |
|
||||||
other_event.deleted = true; |
|
||||||
const stats = stroke_stats(other_event.points, storage.cursor.width); |
|
||||||
redraw_region(stats.bbox); |
|
||||||
break; |
|
||||||
} else if (other_event.type === EVENT.IMAGE) { |
|
||||||
other_event.deleted = true; |
|
||||||
const item = document.querySelector(`img[data-image-id="${other_event.image_id}"]`); |
|
||||||
if (item) item.remove(); |
|
||||||
break; |
|
||||||
} else if (other_event.type === EVENT.ERASER) { |
|
||||||
other_event.deleted = true; |
|
||||||
const erased = find_stroke_backwards(other_event.stroke_id); |
|
||||||
if (erased) { |
|
||||||
erased.deleted = false; |
|
||||||
const stats = stroke_stats(erased.points, storage.cursor.width); |
|
||||||
redraw_region(stats.bbox); |
|
||||||
} |
|
||||||
break; |
|
||||||
} else if (other_event.type === EVENT.IMAGE_MOVE) { |
|
||||||
const item = document.querySelector(`img[data-image-id="${other_event.image_id}"]`); |
|
||||||
|
|
||||||
const ix = storage.images[other_event.image_id].x -= other_event.x; |
|
||||||
const iy = storage.images[other_event.image_id].y -= other_event.y; |
|
||||||
|
|
||||||
item.style.transform = `translate(${ix}px, ${iy}px)`; |
|
||||||
|
|
||||||
break; |
|
||||||
} |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
break; |
|
||||||
} |
|
||||||
|
|
||||||
case EVENT.IMAGE: { |
|
||||||
const url = config.image_url + event.image_id; |
|
||||||
const item = document.createElement('img'); |
|
||||||
|
|
||||||
item.classList.add('floating-image'); |
|
||||||
item.style['z-index'] = storage.events.length; |
|
||||||
item.setAttribute('data-image-id', event.image_id); |
|
||||||
item.setAttribute('src', url); |
|
||||||
item.style.transform = `translate(${event.x}px, ${event.y}px)`; |
|
||||||
|
|
||||||
elements.images.appendChild(item); |
|
||||||
storage.images[event.image_id] = { |
|
||||||
'x': event.x, 'y': event.y |
|
||||||
}; |
|
||||||
// const r = await fetch(config.image_url + event.image_id);
|
|
||||||
// const blob = await r.blob();
|
|
||||||
// const bitmap = await createImageBitmap(blob);
|
|
||||||
// event.bitmap = bitmap;
|
|
||||||
// const bbox = bitmap_bbox(event);
|
|
||||||
// storage.ctx0.drawImage(bitmap, bbox.xmin, bbox.ymin);
|
|
||||||
|
|
||||||
break; |
|
||||||
} |
|
||||||
|
|
||||||
case EVENT.IMAGE_MOVE: { |
|
||||||
// Already moved due to local prediction
|
|
||||||
if (event.user_id !== storage.me.id) { |
|
||||||
const image_id = event.image_id; |
|
||||||
const item = document.querySelector(`.floating-image[data-image-id="${image_id}"]`); |
|
||||||
|
|
||||||
const ix = storage.images[event.image_id].x += event.x; |
|
||||||
const iy = storage.images[event.image_id].y += event.y; |
|
||||||
|
|
||||||
if (item) { |
|
||||||
item.style.transform = `translate(${ix}px, ${iy}px)`; |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
break; |
|
||||||
} |
|
||||||
|
|
||||||
case EVENT.ERASER: { |
|
||||||
if (event.deleted) { |
|
||||||
break; |
|
||||||
} |
|
||||||
|
|
||||||
for (const other_event of storage.events) { |
|
||||||
if (other_event.type === EVENT.STROKE && other_event.stroke_id === event.stroke_id) { |
|
||||||
// Might already be deleted because of local prediction
|
|
||||||
if (!other_event.deleted) { |
|
||||||
other_event.deleted = true; |
|
||||||
const stats = stroke_stats(other_event.points, storage.cursor.width); |
|
||||||
redraw_region(stats.bbox); |
|
||||||
} |
|
||||||
break; |
|
||||||
} |
|
||||||
} |
|
||||||
break; |
|
||||||
} |
|
||||||
|
|
||||||
default: { |
|
||||||
console.error('fuck'); |
|
||||||
} |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
async function handle_message(d) { |
|
||||||
const message_type = des_u8(d); |
|
||||||
|
|
||||||
if (config.debug_print) console.debug(message_type); |
|
||||||
|
|
||||||
switch (message_type) { |
|
||||||
case MESSAGE.JOIN: |
|
||||||
case MESSAGE.INIT: { |
|
||||||
elements.canvas0.classList.add('white'); |
|
||||||
|
|
||||||
storage.me.id = des_u32(d); |
|
||||||
storage.server_lsn = des_u32(d); |
|
||||||
|
|
||||||
if (storage.server_lsn > storage.lsn) { |
|
||||||
// Server knows something that we don't
|
|
||||||
storage.lsn = storage.server_lsn; |
|
||||||
} |
|
||||||
|
|
||||||
if (message_type === MESSAGE.JOIN) { |
|
||||||
ls.setItem('sessionId', des_u32(d)); |
|
||||||
if (config.debug_print) console.debug('join in'); |
|
||||||
} else { |
|
||||||
if (config.debug_print) console.debug('init in'); |
|
||||||
} |
|
||||||
|
|
||||||
const event_count = des_u32(d); |
|
||||||
|
|
||||||
if (config.debug_print) console.debug(`${event_count} events in init`); |
|
||||||
|
|
||||||
storage.ctx0.clearRect(0, 0, storage.ctx0.canvas.width, storage.ctx0.canvas.height); |
|
||||||
elements.images.innerHTML = ''; |
|
||||||
storage.events.length = 0; |
|
||||||
|
|
||||||
for (let i = 0; i < event_count; ++i) { |
|
||||||
const event = des_event(d); |
|
||||||
await handle_event(event); |
|
||||||
storage.events.push(event); |
|
||||||
} |
|
||||||
|
|
||||||
elements.canvas0.classList.remove('white'); |
|
||||||
|
|
||||||
send_ack(event_count); |
|
||||||
|
|
||||||
sync_queue(); |
|
||||||
|
|
||||||
break; |
|
||||||
} |
|
||||||
|
|
||||||
case MESSAGE.FIRE: { |
|
||||||
const user_id = des_u32(d); |
|
||||||
const predraw_event = des_event(d); |
|
||||||
|
|
||||||
predraw_user(user_id, predraw_event); |
|
||||||
|
|
||||||
break; |
|
||||||
} |
|
||||||
|
|
||||||
case MESSAGE.ACK: { |
|
||||||
const lsn = des_u32(d); |
|
||||||
|
|
||||||
if (config.debug_print) console.debug(`ack ${lsn} in`); |
|
||||||
|
|
||||||
if (lsn > storage.server_lsn) { |
|
||||||
// ACKs may arrive out of order
|
|
||||||
storage.server_lsn = lsn; |
|
||||||
} |
|
||||||
|
|
||||||
break; |
|
||||||
} |
|
||||||
|
|
||||||
case MESSAGE.SYN: { |
|
||||||
const sn = des_u32(d); |
|
||||||
const count = des_u32(d); |
|
||||||
|
|
||||||
const we_expect = sn - storage.sn; |
|
||||||
const first = count - we_expect; |
|
||||||
|
|
||||||
if (config.debug_print) console.debug(`syn ${sn} in`); |
|
||||||
|
|
||||||
for (let i = 0; i < count; ++i) { |
|
||||||
const event = des_event(d); |
|
||||||
if (i >= first) { |
|
||||||
handle_event(event); |
|
||||||
storage.events.push(event); |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
storage.sn = sn; |
|
||||||
|
|
||||||
await send_ack(sn); |
|
||||||
|
|
||||||
break; |
|
||||||
} |
|
||||||
|
|
||||||
default: { |
|
||||||
console.error('fuck'); |
|
||||||
return; |
|
||||||
} |
|
||||||
} |
|
||||||
} |
|
@ -1,201 +0,0 @@ |
|||||||
function serializer_create(size) { |
|
||||||
const buffer = new ArrayBuffer(size); |
|
||||||
return { |
|
||||||
'offset': 0, |
|
||||||
'size': size, |
|
||||||
'buffer': buffer, |
|
||||||
'view': new DataView(buffer), |
|
||||||
'strview': new Uint8Array(buffer), |
|
||||||
}; |
|
||||||
} |
|
||||||
|
|
||||||
function ser_u8(s, value) { |
|
||||||
s.view.setUint8(s.offset, value); |
|
||||||
s.offset += 1; |
|
||||||
} |
|
||||||
|
|
||||||
function ser_u16(s, value) { |
|
||||||
s.view.setUint16(s.offset, value, true); |
|
||||||
s.offset += 2; |
|
||||||
} |
|
||||||
|
|
||||||
function ser_u32(s, value) { |
|
||||||
s.view.setUint32(s.offset, value, true); |
|
||||||
s.offset += 4; |
|
||||||
} |
|
||||||
|
|
||||||
function ser_event(s, event) { |
|
||||||
ser_u8(s, event.type); |
|
||||||
ser_u8(s, 0); // padding for 16bit alignment
|
|
||||||
|
|
||||||
switch (event.type) { |
|
||||||
case EVENT.PREDRAW: { |
|
||||||
ser_u16(s, event.x); |
|
||||||
ser_u16(s, event.y); |
|
||||||
break; |
|
||||||
} |
|
||||||
|
|
||||||
case EVENT.STROKE: { |
|
||||||
ser_u16(s, event.points.length); |
|
||||||
ser_u16(s, event.width); |
|
||||||
ser_u32(s, event.color); |
|
||||||
|
|
||||||
if (config.debug_print) console.debug('original', event.points); |
|
||||||
|
|
||||||
for (const point of event.points) { |
|
||||||
ser_u16(s, point.x); |
|
||||||
ser_u16(s, point.y); |
|
||||||
} |
|
||||||
|
|
||||||
break; |
|
||||||
} |
|
||||||
|
|
||||||
case EVENT.IMAGE: |
|
||||||
case EVENT.IMAGE_MOVE: { |
|
||||||
const image_id = parseInt(event.image_id); |
|
||||||
ser_u32(s, image_id); |
|
||||||
ser_u16(s, event.x); |
|
||||||
ser_u16(s, event.y); |
|
||||||
break; |
|
||||||
} |
|
||||||
|
|
||||||
case EVENT.UNDO: |
|
||||||
case EVENT.REDO: { |
|
||||||
break; |
|
||||||
} |
|
||||||
|
|
||||||
case EVENT.ERASER: { |
|
||||||
ser_u32(s, event.stroke_id); |
|
||||||
break; |
|
||||||
} |
|
||||||
|
|
||||||
default: { |
|
||||||
console.error('fuck'); |
|
||||||
} |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
async function send_ack(sn) { |
|
||||||
const s = serializer_create(1 + 4); |
|
||||||
|
|
||||||
ser_u8(s, MESSAGE.ACK); |
|
||||||
ser_u32(s, sn); |
|
||||||
|
|
||||||
if (config.debug_print) console.debug(`ack ${sn} out`); |
|
||||||
|
|
||||||
try { |
|
||||||
if (ws) await ws.send(s.buffer); |
|
||||||
} catch(e) { |
|
||||||
ws.close(); |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
async function sync_queue() { |
|
||||||
if (ws === null) { |
|
||||||
if (config.debug_print) console.debug('socket has closed, stopping SYNs'); |
|
||||||
return; |
|
||||||
} |
|
||||||
|
|
||||||
let size = 1 + 1 + 4 + 4; // opcode + lsn + event count
|
|
||||||
let count = storage.lsn - storage.server_lsn; |
|
||||||
|
|
||||||
if (count === 0) { |
|
||||||
if (config.debug_print) console.debug('server ACKed all events, clearing queue'); |
|
||||||
storage.queue.length = 0; |
|
||||||
return; |
|
||||||
} |
|
||||||
|
|
||||||
for (let i = count - 1; i >= 0; --i) { |
|
||||||
const event = storage.queue[storage.queue.length - 1 - i]; |
|
||||||
size += event_size(event); |
|
||||||
} |
|
||||||
|
|
||||||
const s = serializer_create(size); |
|
||||||
|
|
||||||
ser_u8(s, MESSAGE.SYN); |
|
||||||
ser_u8(s, 0); // padding for 16bit alignment
|
|
||||||
ser_u32(s, storage.lsn); |
|
||||||
ser_u32(s, count); |
|
||||||
|
|
||||||
for (let i = count - 1; i >= 0; --i) { |
|
||||||
const event = storage.queue[storage.queue.length - 1 - i]; |
|
||||||
ser_event(s, event); |
|
||||||
} |
|
||||||
|
|
||||||
if (config.debug_print) console.debug(`syn ${storage.lsn} out`); |
|
||||||
|
|
||||||
try { |
|
||||||
if (ws) await ws.send(s.buffer); |
|
||||||
} catch(e) { |
|
||||||
ws.close(); |
|
||||||
} |
|
||||||
|
|
||||||
setTimeout(sync_queue, config.sync_timeout); |
|
||||||
} |
|
||||||
|
|
||||||
function push_event(event) { |
|
||||||
storage.lsn += 1; |
|
||||||
|
|
||||||
switch (event.type) { |
|
||||||
case EVENT.STROKE: { |
|
||||||
const points = process_stroke(event.points); |
|
||||||
|
|
||||||
storage.queue.push({ |
|
||||||
'type': EVENT.STROKE, |
|
||||||
'points': points, |
|
||||||
'width': event.width, |
|
||||||
'color': event.color, |
|
||||||
}); |
|
||||||
|
|
||||||
break; |
|
||||||
} |
|
||||||
|
|
||||||
case EVENT.RULER: { |
|
||||||
event.type = EVENT.STROKE; |
|
||||||
storage.queue.push(event); |
|
||||||
break; |
|
||||||
} |
|
||||||
|
|
||||||
case EVENT.ERASER: |
|
||||||
case EVENT.IMAGE: |
|
||||||
case EVENT.IMAGE_MOVE: |
|
||||||
case EVENT.UNDO: |
|
||||||
case EVENT.REDO: { |
|
||||||
storage.queue.push(event); |
|
||||||
break; |
|
||||||
} |
|
||||||
|
|
||||||
default: { |
|
||||||
console.error('fuck'); |
|
||||||
} |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
// Queue an event and initialize repated sends until ACKed
|
|
||||||
function queue_event(event, skip = false) { |
|
||||||
push_event(event); |
|
||||||
|
|
||||||
if (skip) { |
|
||||||
return; |
|
||||||
} |
|
||||||
|
|
||||||
if (storage.timers.queue_sync) { |
|
||||||
clearTimeout(storage.timers.queue_sync); |
|
||||||
} |
|
||||||
|
|
||||||
sync_queue(); |
|
||||||
} |
|
||||||
|
|
||||||
// Fire and forget. Doesn't do anything if we are offline
|
|
||||||
async function fire_event(event) { |
|
||||||
const s = serializer_create(1 + event_size(event)); |
|
||||||
|
|
||||||
ser_u8(s, MESSAGE.FIRE); |
|
||||||
ser_event(s, event); |
|
||||||
|
|
||||||
try { |
|
||||||
if (ws) await ws.send(s.buffer); |
|
||||||
} catch(e) { |
|
||||||
ws.close(); |
|
||||||
} |
|
||||||
} |
|
@ -0,0 +1,236 @@ |
|||||||
|
function worker_message(worker, message) { |
||||||
|
return new Promise((resolve) => { |
||||||
|
worker.onmessage = (e) => resolve(e.data); |
||||||
|
worker.postMessage(message); |
||||||
|
}); |
||||||
|
} |
||||||
|
|
||||||
|
function workers_messages(workers, messages) { |
||||||
|
const promises = []; |
||||||
|
|
||||||
|
for (let i = 0; i < workers.length; ++i) { |
||||||
|
promises.push(worker_message(workers[i], messages[i])); |
||||||
|
} |
||||||
|
|
||||||
|
return Promise.all(promises); |
||||||
|
} |
||||||
|
|
||||||
|
function workers_thread_message(workers, message, thread_field=null) { |
||||||
|
const messages = []; |
||||||
|
|
||||||
|
for (let i = 0; i < workers.length; ++i) { |
||||||
|
if (thread_field !== null) { |
||||||
|
const m = structuredClone(message); |
||||||
|
m[thread_field] = i; |
||||||
|
messages.push(m); |
||||||
|
} else { |
||||||
|
messages.push(message); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
return workers_messages(workers, messages); |
||||||
|
} |
||||||
|
|
||||||
|
async function init_wasm(state) { |
||||||
|
const memory = new WebAssembly.Memory({ |
||||||
|
initial: 16384, // F U
|
||||||
|
maximum: 16384, // 1GiB
|
||||||
|
shared: true, |
||||||
|
}); |
||||||
|
|
||||||
|
// "Master thread" to do maintance on (static allocations, merging results etc)
|
||||||
|
const master_wasm = await WebAssembly.instantiateStreaming(fetch('wasm/lod.wasm'), { |
||||||
|
env: { 'memory': memory } |
||||||
|
}); |
||||||
|
|
||||||
|
const nworkers = navigator.hardwareConcurrency; |
||||||
|
|
||||||
|
state.wasm.exports = master_wasm.instance.exports; |
||||||
|
state.wasm.heap_base = state.wasm.exports.alloc_static(0); |
||||||
|
state.wasm.workers = []; |
||||||
|
state.wasm.memory = memory; |
||||||
|
|
||||||
|
for (let i = 0; i < nworkers; ++i) { |
||||||
|
const w = new Worker('lod_worker.js'); |
||||||
|
state.wasm.workers.push(w); |
||||||
|
} |
||||||
|
|
||||||
|
await workers_thread_message(state.wasm.workers, { |
||||||
|
'type': 'init', |
||||||
|
'heap_base': state.wasm.heap_base, |
||||||
|
'memory': memory, |
||||||
|
}, 'thread_id'); |
||||||
|
|
||||||
|
const initial = config.initial_wasm_bytes; |
||||||
|
|
||||||
|
state.wasm.buffers = { |
||||||
|
'xs': { |
||||||
|
'used': 0, |
||||||
|
'cap': initial |
||||||
|
}, |
||||||
|
'ys': { |
||||||
|
'used': 0, |
||||||
|
'cap': initial |
||||||
|
}, |
||||||
|
'coords_from': { |
||||||
|
'used': 0, |
||||||
|
'cap': initial |
||||||
|
}, |
||||||
|
'pressures': { |
||||||
|
'used': 0, |
||||||
|
'cap': initial |
||||||
|
}, |
||||||
|
}; |
||||||
|
|
||||||
|
state.wasm.buffers['xs'].offset = state.wasm.exports.alloc_static(initial); |
||||||
|
state.wasm.buffers['ys'].offset = state.wasm.exports.alloc_static(initial); |
||||||
|
state.wasm.buffers['pressures'].offset = state.wasm.exports.alloc_static(initial); |
||||||
|
state.wasm.buffers['coords_from'].offset = state.wasm.exports.alloc_static(initial); |
||||||
|
|
||||||
|
const mem = state.wasm.memory.buffer; |
||||||
|
|
||||||
|
state.wasm.buffers['xs'].tv = tv_create_on(Float32Array, initial / 4, |
||||||
|
mem, state.wasm.buffers['xs'].offset); |
||||||
|
state.wasm.buffers['ys'].tv = tv_create_on(Float32Array, initial / 4, |
||||||
|
mem, state.wasm.buffers['ys'].offset); |
||||||
|
state.wasm.buffers['pressures'].tv = tv_create_on(Uint8Array, initial, |
||||||
|
mem, state.wasm.buffers['pressures'].offset); |
||||||
|
state.wasm.buffers['coords_from'].tv = tv_create_on(Uint32Array, initial / 4, |
||||||
|
mem, state.wasm.buffers['coords_from'].offset); |
||||||
|
|
||||||
|
tv_add(state.wasm.buffers['coords_from'].tv, 0); |
||||||
|
state.wasm.buffers['coords_from'].used = 4; |
||||||
|
} |
||||||
|
|
||||||
|
function wasm_ensure_by(state, nstrokes, ncoords) { |
||||||
|
const buffers = state.wasm.buffers; |
||||||
|
|
||||||
|
const old_ys_offset = buffers['ys'].offset; |
||||||
|
const old_coords_from_offset = buffers['coords_from'].offset; |
||||||
|
const old_pressures_offset = buffers['pressures'].offset; |
||||||
|
|
||||||
|
let realloc = false; |
||||||
|
let coords_bytes = buffers['xs'].cap; |
||||||
|
let stroke_bytes = buffers['coords_from'].cap; |
||||||
|
|
||||||
|
if (buffers['xs'].used + ncoords * 4 > buffers['xs'].cap) { |
||||||
|
coords_bytes = round_to_pow2(buffers['xs'].cap + ncoords * 4, 4096 * 16); // 1 wasm page (although it doesn't matter here)
|
||||||
|
realloc = true; |
||||||
|
} |
||||||
|
|
||||||
|
if (buffers['coords_from'].used + nstrokes * 4 > buffers['coords_from'].cap) { |
||||||
|
stroke_bytes = round_to_pow2(buffers['coords_from'].cap + nstrokes * 4, 4096 * 16); |
||||||
|
realloc = true; |
||||||
|
} |
||||||
|
|
||||||
|
if (realloc) { |
||||||
|
if (config.debug_print) console.debug('WASM static data re-layout'); |
||||||
|
|
||||||
|
state.wasm.exports.free_static(); |
||||||
|
|
||||||
|
const mem = state.wasm.memory.buffer; |
||||||
|
const memv = new Uint8Array(mem); |
||||||
|
|
||||||
|
buffers['xs'].offset = state.wasm.exports.alloc_static(coords_bytes); |
||||||
|
buffers['ys'].offset = state.wasm.exports.alloc_static(coords_bytes); |
||||||
|
buffers['pressures'].offset = state.wasm.exports.alloc_static(coords_bytes); |
||||||
|
buffers['coords_from'].offset = state.wasm.exports.alloc_static(stroke_bytes); |
||||||
|
|
||||||
|
buffers['xs'].tv = tv_create_on(Float32Array, coords_bytes / 4, mem, buffers['xs'].offset); |
||||||
|
buffers['ys'].tv = tv_create_on(Float32Array, coords_bytes / 4, mem, buffers['ys'].offset); |
||||||
|
buffers['pressures'].tv = tv_create_on(Uint8Array, coords_bytes, mem, buffers['pressures'].offset); |
||||||
|
buffers['coords_from'].tv = tv_create_on(Uint32Array, stroke_bytes / 4, mem, buffers['coords_from'].offset); |
||||||
|
|
||||||
|
// TODO: this should have been automatic maybe?
|
||||||
|
buffers['xs'].tv.size = buffers['xs'].used / 4; |
||||||
|
buffers['ys'].tv.size = buffers['ys'].used / 4; |
||||||
|
buffers['pressures'].tv.size = buffers['pressures'].used; |
||||||
|
buffers['coords_from'].tv.size = buffers['coords_from'].used / 4; |
||||||
|
buffers['xs'].cap = buffers['ys'].cap = buffers['pressures'].cap = coords_bytes; |
||||||
|
buffers['coords_from'].cap = stroke_bytes; |
||||||
|
|
||||||
|
const tmp = new Uint8Array(Math.max(coords_bytes, stroke_bytes)); |
||||||
|
|
||||||
|
// Copy from back to front (otherwise we will overwrite)
|
||||||
|
tmp.set(new Uint8Array(mem, old_coords_from_offset, buffers['coords_from'].used)); |
||||||
|
memv.set(new Uint8Array(tmp.buffer, 0, buffers['coords_from'].used), buffers['coords_from'].offset); |
||||||
|
|
||||||
|
tmp.set(new Uint8Array(mem, old_pressures_offset, buffers['pressures'].used)); |
||||||
|
memv.set(new Uint8Array(tmp.buffer, 0, buffers['pressures'].used), buffers['pressures'].offset); |
||||||
|
|
||||||
|
tmp.set(new Uint8Array(mem, old_ys_offset, buffers['ys'].used)); |
||||||
|
memv.set(new Uint8Array(tmp.buffer, 0, buffers['ys'].used), buffers['ys'].offset); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
async function do_lod(state, context) { |
||||||
|
state.wasm.exports.free_dynamic(); |
||||||
|
|
||||||
|
const buffers = state.wasm.buffers; |
||||||
|
const result_buffers = state.wasm.exports.alloc_dynamic(state.wasm.workers.length * 4); |
||||||
|
const result_counts = state.wasm.exports.alloc_dynamic(state.wasm.workers.length * 4); |
||||||
|
const clipped_indices = state.wasm.exports.alloc_dynamic(context.clipped_indices.size * 4); |
||||||
|
const mem = new Uint8Array(state.wasm.memory.buffer); |
||||||
|
|
||||||
|
// Dynamic input data that should (by design) never be too big
|
||||||
|
mem.set(tv_bytes(context.clipped_indices), clipped_indices); |
||||||
|
|
||||||
|
// NOTE: this static partitioning scheme turned out to be "good enough" (i.e., trying
|
||||||
|
// to allocate approximately the same amount of points per job wasn't any faster)
|
||||||
|
const indices_per_thread = Math.floor(context.clipped_indices.size / state.wasm.workers.length); |
||||||
|
const offsets = { |
||||||
|
'coords_from': buffers['coords_from'].offset, |
||||||
|
'xs': buffers['xs'].offset, |
||||||
|
'ys': buffers['ys'].offset, |
||||||
|
'pressures': buffers['pressures'].offset, |
||||||
|
'result_buffers': result_buffers, |
||||||
|
'result_counts': result_counts, |
||||||
|
}; |
||||||
|
|
||||||
|
const jobs = []; |
||||||
|
|
||||||
|
for (let i = 0; i < state.wasm.workers.length; ++i) { |
||||||
|
let count = indices_per_thread; |
||||||
|
|
||||||
|
if (i === state.wasm.workers.length - 1) { |
||||||
|
count += context.clipped_indices.size % state.wasm.workers.length; |
||||||
|
} |
||||||
|
|
||||||
|
jobs.push({ |
||||||
|
'type': 'lod', |
||||||
|
'indices_base': clipped_indices + i * 4 * indices_per_thread, |
||||||
|
'indices_count': count, |
||||||
|
'zoom': state.canvas.zoom, |
||||||
|
'offsets': offsets |
||||||
|
}); |
||||||
|
} |
||||||
|
|
||||||
|
await workers_messages(state.wasm.workers, jobs); |
||||||
|
|
||||||
|
const result_offset = state.wasm.exports.merge_results( |
||||||
|
result_counts, |
||||||
|
result_buffers, |
||||||
|
state.wasm.workers.length |
||||||
|
); |
||||||
|
|
||||||
|
const segment_count = new Int32Array(state.wasm.memory.buffer, result_counts, 1)[0]; // by convention
|
||||||
|
|
||||||
|
// Use results without copying from WASM memory
|
||||||
|
const wasm_points = new Float32Array(state.wasm.memory.buffer, |
||||||
|
result_offset, segment_count * 2); |
||||||
|
const wasm_ids = new Uint32Array(state.wasm.memory.buffer, |
||||||
|
result_offset + segment_count * 2 * 4, segment_count); |
||||||
|
const wasm_pressures = new Uint8Array(state.wasm.memory.buffer, |
||||||
|
result_offset + segment_count * 2 * 4 + segment_count * 4, segment_count); |
||||||
|
|
||||||
|
context.instance_data_points.data = wasm_points; |
||||||
|
context.instance_data_points.size = segment_count * 2; |
||||||
|
|
||||||
|
context.instance_data_ids.data = wasm_ids; |
||||||
|
context.instance_data_ids.size = segment_count; |
||||||
|
|
||||||
|
context.instance_data_pressures.data = wasm_pressures; |
||||||
|
context.instance_data_pressures.size = segment_count; |
||||||
|
|
||||||
|
return segment_count; |
||||||
|
} |
@ -1,23 +1,166 @@ |
|||||||
function tools_switch(tool) { |
function switch_tool(state, item) { |
||||||
if (storage.tools.active_element) { |
const tool = item.getAttribute('data-tool'); |
||||||
storage.tools.active_element.classList.remove('active'); |
|
||||||
|
if (tool === 'undo') { |
||||||
|
queue_event(state, undo_event(state)); |
||||||
|
return; |
||||||
|
} |
||||||
|
|
||||||
|
if (state.tools.active_element) { |
||||||
|
state.tools.active_element.classList.remove('active'); |
||||||
|
} |
||||||
|
|
||||||
|
const old_class = 'tool-' + state.tools.active; |
||||||
|
const new_class = 'tool-' + tool; |
||||||
|
|
||||||
|
document.querySelector('canvas').classList.remove(old_class); |
||||||
|
|
||||||
|
state.tools.active = tool; |
||||||
|
state.tools.active_element = item; |
||||||
|
state.tools.active_element.classList.add('active'); |
||||||
|
|
||||||
|
document.querySelector('canvas').classList.add(new_class); |
||||||
|
|
||||||
|
if (tool === 'pencil' || tool === 'eraser' || tool === 'ruler') { |
||||||
|
update_cursor(state); |
||||||
|
document.querySelector('.brush-dom').classList.remove('dhide'); |
||||||
|
} else { |
||||||
|
document.querySelector('.brush-dom').classList.add('dhide'); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
function select_color(state, item, color_u32) { |
||||||
|
if (state.colors.active_element) { |
||||||
|
state.colors.active_element.classList.remove('active'); |
||||||
|
} |
||||||
|
|
||||||
|
if (state.colors.extended_element) { |
||||||
|
state.colors.extended_element.classList.remove('extended'); |
||||||
|
state.colors.extended_element = null; |
||||||
|
} |
||||||
|
|
||||||
|
const last_minor = item.lastElementChild; |
||||||
|
const color_css = color_from_u32(color_u32); |
||||||
|
|
||||||
|
|
||||||
|
last_minor.setAttribute('data-color', color_css.substring(1)); |
||||||
|
last_minor.querySelector('.color-pane').style.background = color_css; |
||||||
|
|
||||||
|
state.colors.active_element = item; |
||||||
|
item.classList.add('active'); |
||||||
|
} |
||||||
|
|
||||||
|
function extend_major_color(state, item) { |
||||||
|
if (state.colors.active_element) { |
||||||
|
state.colors.active_element.classList.remove('active'); |
||||||
|
} |
||||||
|
|
||||||
|
if (state.colors.extended_element) { |
||||||
|
state.colors.extended_element.classList.remove('extended'); |
||||||
|
} |
||||||
|
|
||||||
|
const last_minor = item.lastElementChild; |
||||||
|
|
||||||
|
// Restore last pane color in case it was overwritten by active color
|
||||||
|
last_minor.querySelector('.color-pane').style.background = '#' + item.getAttribute('data-last-color'); |
||||||
|
last_minor.setAttribute('data-color', item.getAttribute('data-last-color')); |
||||||
|
|
||||||
|
state.colors.extended_element = item; |
||||||
|
item.classList.add('extended'); |
||||||
|
} |
||||||
|
|
||||||
|
function set_color_u32(state, color_u32) { |
||||||
|
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; |
||||||
} |
} |
||||||
|
|
||||||
storage.tools.active = tool; |
const major_color = color_minor.parentElement; |
||||||
storage.tools.active_element = document.querySelector(`.tool[data-tool="${tool}"]`); |
select_color(state, major_color, color_u32); |
||||||
storage.tools.active_element.classList.add('active'); |
|
||||||
|
state.players[state.me].color = color_u32 |
||||||
|
update_cursor(state); |
||||||
|
fire_event(state, color_event(color_u32)); |
||||||
|
} |
||||||
|
|
||||||
|
function switch_stroke_width(e, state) { |
||||||
|
if (!state.online) return; |
||||||
|
|
||||||
|
const value = parseInt(e.target.value); |
||||||
|
|
||||||
|
state.players[state.me].width = value; |
||||||
|
update_cursor(state); |
||||||
|
} |
||||||
|
|
||||||
|
function broadcast_stroke_width(e, state) { |
||||||
|
const value = e.target.value; |
||||||
|
fire_event(state, width_event(value)); |
||||||
} |
} |
||||||
|
|
||||||
function tools_init() { |
function init_tools(state) { |
||||||
const pencil = document.querySelector('.tool[data-tool="pencil"]'); |
const tools = document.querySelectorAll('.tools .tool'); |
||||||
const ruler = document.querySelector('.tool[data-tool="ruler"]'); |
const color_groups = document.querySelectorAll('.pallete .color-major'); |
||||||
const eraser = document.querySelector('.tool[data-tool="eraser"]'); |
|
||||||
const undo = document.querySelector('.tool[data-tool="undo"]'); |
|
||||||
|
|
||||||
pencil.addEventListener('click', () => tools_switch('pencil')); |
tools.forEach((item) => { item.addEventListener('click', () => switch_tool(state, item)); }); |
||||||
ruler.addEventListener('click', () => tools_switch('ruler')); |
color_groups.forEach((item) => { |
||||||
eraser.addEventListener('click', () => tools_switch('eraser')); |
item.setAttribute('data-last-color', item.lastElementChild.getAttribute('data-color')); |
||||||
undo.addEventListener('click', queue_undo); |
|
||||||
|
let longtouch_timer = null; |
||||||
|
|
||||||
tools_switch('pencil'); |
item.addEventListener('touchstart', (e) => { |
||||||
} |
longtouch_timer = setTimeout(() => { |
||||||
|
extend_major_color(state, item); |
||||||
|
}, 500); |
||||||
|
}); |
||||||
|
|
||||||
|
item.addEventListener('touchmove', (e) => { |
||||||
|
if (longtouch_timer) { |
||||||
|
clearTimeout(longtouch_timer); |
||||||
|
} |
||||||
|
longtouch_timer = null; |
||||||
|
}); |
||||||
|
|
||||||
|
item.addEventListener('touchend', (e) => { |
||||||
|
if (longtouch_timer) { |
||||||
|
clearTimeout(longtouch_timer); |
||||||
|
} |
||||||
|
longtouch_timer = null; |
||||||
|
}); |
||||||
|
|
||||||
|
item.addEventListener('click', (e) => { |
||||||
|
if (e.ctrlKey) { |
||||||
|
extend_major_color(state, item); |
||||||
|
return; |
||||||
|
} |
||||||
|
|
||||||
|
let color_element = e.target; |
||||||
|
let target = e.target; |
||||||
|
|
||||||
|
while (!target.classList.contains('color-minor')) { |
||||||
|
target = target.parentElement; |
||||||
|
} |
||||||
|
|
||||||
|
const color_str = target.getAttribute('data-color'); |
||||||
|
const color_u32 = color_to_u32(color_str); |
||||||
|
|
||||||
|
set_color_u32(state, color_u32); |
||||||
|
}) |
||||||
|
}); |
||||||
|
|
||||||
|
switch_tool(state, document.querySelector('.tool[data-tool="pencil"]')); |
||||||
|
|
||||||
|
const slider = document.querySelector('#stroke-width'); |
||||||
|
|
||||||
|
// slider.value = state.players[state.me].width;
|
||||||
|
slider.addEventListener('input', (e) => switch_stroke_width(e, state)); |
||||||
|
slider.addEventListener('change', (e) => broadcast_stroke_width(e, state)); |
||||||
|
|
||||||
|
document.querySelector('.phone-extra-controls').addEventListener('click', zenmode); |
||||||
|
} |
||||||
|
@ -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; |
|
||||||
} |
|
||||||
} |
|
@ -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]; |
||||||
|
} |
@ -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 |
@ -0,0 +1,351 @@ |
|||||||
|
#include <wasm_simd128.h> |
||||||
|
|
||||||
|
extern char __heap_base; |
||||||
|
|
||||||
|
static int allocated_static; |
||||||
|
static int allocated_dynamic; |
||||||
|
|
||||||
|
void |
||||||
|
set_sp(char *sp) |
||||||
|
{ |
||||||
|
__asm__ __volatile__( |
||||||
|
".globaltype __stack_pointer, i32\n" |
||||||
|
"local.get %0\n" |
||||||
|
"global.set __stack_pointer\n" |
||||||
|
: : "r"(sp) |
||||||
|
); |
||||||
|
} |
||||||
|
|
||||||
|
void |
||||||
|
free_static(void) |
||||||
|
{ |
||||||
|
allocated_static = 0; |
||||||
|
} |
||||||
|
|
||||||
|
void |
||||||
|
free_dynamic(void) |
||||||
|
{ |
||||||
|
allocated_dynamic = 0; |
||||||
|
} |
||||||
|
|
||||||
|
void * |
||||||
|
alloc_static(int size) |
||||||
|
{ |
||||||
|
// This IS NOT thread-safe
|
||||||
|
void *result = &__heap_base + allocated_static; |
||||||
|
allocated_static += size; |
||||||
|
return(result); |
||||||
|
} |
||||||
|
|
||||||
|
static int |
||||||
|
round_to_pow2(int value, int multiple) |
||||||
|
{ |
||||||
|
return((value + multiple - 1) & -multiple); |
||||||
|
} |
||||||
|
|
||||||
|
void * |
||||||
|
alloc_dynamic(int size) |
||||||
|
{ |
||||||
|
// Very ad-van-ced thread-safe allocator
|
||||||
|
// CAN be called from multiple threads
|
||||||
|
size = round_to_pow2(size, 4); |
||||||
|
int original_allocated_dynamic = __atomic_fetch_add(&allocated_dynamic, size, __ATOMIC_SEQ_CST); |
||||||
|
void *result = &__heap_base + allocated_static + original_allocated_dynamic; |
||||||
|
return(result); |
||||||
|
} |
||||||
|
|
||||||
|
static int |
||||||
|
rdp_find_max(float *xs, float *ys, unsigned char *pressures, float zoom, int coords_from, |
||||||
|
int segment_start, int segment_end) |
||||||
|
{ |
||||||
|
int result = -1; |
||||||
|
|
||||||
|
if (segment_start == segment_end) { |
||||||
|
return(result); |
||||||
|
} |
||||||
|
|
||||||
|
float EPS = 0.125f / zoom * 255.0f; |
||||||
|
float max_dist = 0.0f; |
||||||
|
|
||||||
|
float ax = xs[coords_from + segment_start]; |
||||||
|
float ay = ys[coords_from + segment_start]; |
||||||
|
float bx = xs[coords_from + segment_end]; |
||||||
|
float by = ys[coords_from + segment_end]; |
||||||
|
|
||||||
|
unsigned char ap = pressures[coords_from / 2 + segment_start]; |
||||||
|
unsigned char bp = pressures[coords_from / 2 + segment_end]; |
||||||
|
|
||||||
|
float dx = bx - ax; |
||||||
|
float dy = by - ay; |
||||||
|
|
||||||
|
float dist_ab = __builtin_sqrtf(dx * dx + dy * dy); |
||||||
|
float dir_nx = dy / dist_ab * 255.0f; |
||||||
|
float dir_ny = -dx / dist_ab * 255.0f; |
||||||
|
|
||||||
|
#if 0 |
||||||
|
for (int i = segment_start + 1; i < segment_end; ++i) { |
||||||
|
float px = xs[coords_from + i]; |
||||||
|
float py = ys[coords_from + i]; |
||||||
|
|
||||||
|
unsigned char pp = pressures[coords_from + i]; |
||||||
|
|
||||||
|
float apx = px - ax; |
||||||
|
float apy = py - ay; |
||||||
|
|
||||||
|
float dist = __builtin_fabsf(apx * dir_nx + apy * dir_ny) |
||||||
|
+ __builtin_abs(pp - ap) + __builtin_abs(pp - bp); |
||||||
|
|
||||||
|
if (dist > EPS && dist > max_dist) { |
||||||
|
result = i; |
||||||
|
max_dist = dist; |
||||||
|
} |
||||||
|
} |
||||||
|
#else |
||||||
|
v128_t ax_x4 = wasm_f32x4_splat(ax); |
||||||
|
v128_t ay_x4 = wasm_f32x4_splat(ay); |
||||||
|
v128_t ap_x4 = wasm_f32x4_splat(ap); |
||||||
|
v128_t bp_x4 = wasm_f32x4_splat(bp); |
||||||
|
v128_t dir_nx_x4 = wasm_f32x4_splat(dir_nx); |
||||||
|
v128_t dir_ny_x4 = wasm_f32x4_splat(dir_ny); |
||||||
|
|
||||||
|
v128_t index_x4 = wasm_u32x4_make(segment_start + 1, segment_start + 2, segment_start + 3, segment_start + 4); |
||||||
|
v128_t four_x4 = wasm_u32x4_const_splat(4); |
||||||
|
v128_t max_dist_x4 = wasm_f32x4_splat(EPS); |
||||||
|
v128_t max_index_x4 = wasm_u32x4_const_splat(-1); |
||||||
|
|
||||||
|
for (int i = segment_start + 1; i < segment_end - 3; i += 4) { |
||||||
|
v128_t px_x4 = wasm_v128_load(xs + coords_from + i); |
||||||
|
v128_t py_x4 = wasm_v128_load(ys + coords_from + i); |
||||||
|
|
||||||
|
v128_t pp_x4 = wasm_f32x4_make( |
||||||
|
pressures[coords_from / 2 + i + 0], |
||||||
|
pressures[coords_from / 2 + i + 1], |
||||||
|
pressures[coords_from / 2 + i + 2], |
||||||
|
pressures[coords_from / 2 + i + 3] |
||||||
|
); |
||||||
|
|
||||||
|
v128_t apx_x4 = wasm_f32x4_sub(px_x4, ax_x4); |
||||||
|
v128_t apy_x4 = wasm_f32x4_sub(py_x4, ay_x4); |
||||||
|
|
||||||
|
v128_t dist_x4 = wasm_f32x4_add( |
||||||
|
wasm_f32x4_add( |
||||||
|
wasm_f32x4_abs(wasm_f32x4_sub(pp_x4, ap_x4)), |
||||||
|
wasm_f32x4_abs(wasm_f32x4_sub(pp_x4, bp_x4)) |
||||||
|
), |
||||||
|
wasm_f32x4_abs( |
||||||
|
wasm_f32x4_add( |
||||||
|
wasm_f32x4_mul(apx_x4, dir_nx_x4), |
||||||
|
wasm_f32x4_mul(apy_x4, dir_ny_x4) |
||||||
|
) |
||||||
|
) |
||||||
|
); |
||||||
|
|
||||||
|
v128_t mask = wasm_f32x4_gt(dist_x4, max_dist_x4); |
||||||
|
|
||||||
|
max_index_x4 = wasm_v128_bitselect(index_x4, max_index_x4, mask); |
||||||
|
max_dist_x4 = wasm_v128_bitselect(dist_x4, max_dist_x4, mask); |
||||||
|
|
||||||
|
index_x4 = wasm_i32x4_add(index_x4, four_x4); |
||||||
|
} |
||||||
|
|
||||||
|
int indices[4]; |
||||||
|
float values[4]; |
||||||
|
|
||||||
|
wasm_v128_store(indices, max_index_x4); |
||||||
|
wasm_v128_store(values, max_dist_x4); |
||||||
|
|
||||||
|
for (int i = 0; i < 4; ++i) { |
||||||
|
if (indices[i] != -1) { |
||||||
|
if (values[i] > max_dist) { |
||||||
|
result = indices[i]; |
||||||
|
max_dist = values[i]; |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
if (max_dist == EPS) { |
||||||
|
max_dist = 0.0f; |
||||||
|
result = -1; |
||||||
|
} |
||||||
|
|
||||||
|
int remainder = (segment_end - segment_start - 1) % 4; |
||||||
|
|
||||||
|
for (int i = segment_end - remainder; i < segment_end; ++i) { |
||||||
|
float px = xs[coords_from + i]; |
||||||
|
float py = ys[coords_from + i]; |
||||||
|
|
||||||
|
unsigned char pp = pressures[coords_from + i]; |
||||||
|
|
||||||
|
float apx = px - ax; |
||||||
|
float apy = py - ay; |
||||||
|
|
||||||
|
float dist = __builtin_fabsf(apx * dir_nx + apy * dir_ny) |
||||||
|
+ __builtin_abs(pp - ap) + __builtin_abs(pp - bp); |
||||||
|
|
||||||
|
if (dist > EPS && dist > max_dist) { |
||||||
|
result = i; |
||||||
|
max_dist = dist; |
||||||
|
} |
||||||
|
} |
||||||
|
#endif |
||||||
|
|
||||||
|
return(result); |
||||||
|
} |
||||||
|
|
||||||
|
void |
||||||
|
do_lod(int *clipped_indices, int clipped_count, float zoom, |
||||||
|
int *stroke_coords_from, |
||||||
|
float *xs, |
||||||
|
float *ys, |
||||||
|
unsigned char *pressures, |
||||||
|
char **result_buffer, |
||||||
|
int *result_count) |
||||||
|
{ |
||||||
|
if (clipped_count == 0) { |
||||||
|
result_count[0] = 0; |
||||||
|
return; |
||||||
|
} |
||||||
|
|
||||||
|
int first_stroke = clipped_indices[0]; |
||||||
|
int last_stroke = clipped_indices[clipped_count - 1]; |
||||||
|
int total_points = 0; |
||||||
|
|
||||||
|
for (int i = 0; i < clipped_count; ++i) { |
||||||
|
int stroke_index = clipped_indices[i]; |
||||||
|
total_points += stroke_coords_from[stroke_index + 1] - stroke_coords_from[stroke_index]; |
||||||
|
} |
||||||
|
|
||||||
|
int *segments_from = alloc_dynamic((clipped_count + 1) * 4); |
||||||
|
int *segments = alloc_dynamic(total_points * 4); // TODO: this is a very conservative estimate, we can lower memory usage if we get this tighter
|
||||||
|
|
||||||
|
int segments_head = 0; |
||||||
|
int stack[4096]; // TODO: what's a reasonable max size for this?
|
||||||
|
int max_stack_size = 0; |
||||||
|
|
||||||
|
for (int i = 0; i < clipped_count; ++i) { |
||||||
|
int stroke_index = clipped_indices[i]; |
||||||
|
|
||||||
|
// TODO: convert to a proper CSR, save half the memory
|
||||||
|
int coords_from = stroke_coords_from[stroke_index]; |
||||||
|
int coords_to = stroke_coords_from[stroke_index + 1]; |
||||||
|
|
||||||
|
int point_count = coords_to - coords_from; |
||||||
|
|
||||||
|
// Basic CSR crap
|
||||||
|
segments_from[i] = segments_head; |
||||||
|
|
||||||
|
int segment_count = 2; |
||||||
|
int stack_head = 0; |
||||||
|
|
||||||
|
segments[segments_head++] = 0; |
||||||
|
|
||||||
|
stack[stack_head++] = 0; |
||||||
|
stack[stack_head++] = 0; |
||||||
|
stack[stack_head++] = point_count - 1; |
||||||
|
|
||||||
|
while (stack_head > 0) { |
||||||
|
if (stack_head > max_stack_size) { max_stack_size = stack_head; } |
||||||
|
int end = stack[--stack_head]; |
||||||
|
int start = stack[--stack_head]; |
||||||
|
int type = stack[--stack_head]; |
||||||
|
|
||||||
|
if (type == 1) { |
||||||
|
segments[segments_head++] = start; |
||||||
|
} else { |
||||||
|
int max = rdp_find_max(xs, ys, pressures, zoom, coords_from, start, end); |
||||||
|
if (max != -1) { |
||||||
|
segment_count += 1; |
||||||
|
|
||||||
|
stack[stack_head++] = 0; |
||||||
|
stack[stack_head++] = max; |
||||||
|
stack[stack_head++] = end; |
||||||
|
|
||||||
|
stack[stack_head++] = 1; |
||||||
|
stack[stack_head++] = max; |
||||||
|
stack[stack_head++] = -1; |
||||||
|
|
||||||
|
stack[stack_head++] = 0; |
||||||
|
stack[stack_head++] = start; |
||||||
|
stack[stack_head++] = max; |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
segments[segments_head++] = point_count - 1; |
||||||
|
} |
||||||
|
|
||||||
|
segments_from[clipped_count] = segments_head; |
||||||
|
|
||||||
|
// Write actual coordinates (points) and stroke ids
|
||||||
|
// Do this in one allocation so that they're not interleaved between threads
|
||||||
|
char *output = alloc_dynamic(segments_head * (3 * 4 + 1)); |
||||||
|
float *points = (float *) output; |
||||||
|
int *ids = (int *) (output + segments_head * 4 * 2); |
||||||
|
unsigned char *pressures_res = (unsigned char *) (output + segments_head * 4 * 3); |
||||||
|
|
||||||
|
int phead = 0; |
||||||
|
int ihead = 0; |
||||||
|
|
||||||
|
for (int i = 0; i < clipped_count; ++i) { |
||||||
|
int stroke_index = clipped_indices[i]; |
||||||
|
|
||||||
|
// TODO: convert to a proper CSR, save half the memory
|
||||||
|
int base_stroke = stroke_coords_from[stroke_index]; |
||||||
|
int from = segments_from[i]; |
||||||
|
int to = segments_from[i + 1]; |
||||||
|
|
||||||
|
for (int j = from; j < to; ++j) { |
||||||
|
int point_index = segments[j]; |
||||||
|
float x = xs[base_stroke + point_index]; |
||||||
|
float y = ys[base_stroke + point_index]; |
||||||
|
|
||||||
|
points[phead++] = x; |
||||||
|
points[phead++] = y; |
||||||
|
|
||||||
|
pressures_res[ihead] = pressures[base_stroke + point_index]; |
||||||
|
|
||||||
|
if (j != to - 1) { |
||||||
|
ids[ihead++] = stroke_index; |
||||||
|
} else { |
||||||
|
ids[ihead++] = stroke_index | (1 << 31); |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
result_buffer[0] = output; |
||||||
|
result_count[0] = segments_head; |
||||||
|
} |
||||||
|
|
||||||
|
// NOT thread-safe, only call from one thread
|
||||||
|
char * |
||||||
|
merge_results(int *segment_counts, char **buffers, int nthreads) |
||||||
|
{ |
||||||
|
int total_segments = 0; |
||||||
|
|
||||||
|
for (int i = 0; i < nthreads; ++i) { |
||||||
|
total_segments += segment_counts[i]; |
||||||
|
} |
||||||
|
|
||||||
|
char *merged = alloc_dynamic(total_segments * (3 * 4 + 1)); |
||||||
|
|
||||||
|
float *points = (float *) merged; |
||||||
|
int *ids = (int *) (merged + total_segments * 4 * 2); |
||||||
|
unsigned char *pressures = (unsigned char *) (merged + total_segments * 4 * 3); |
||||||
|
|
||||||
|
for (int i = 0; i < nthreads; ++i) { |
||||||
|
int segments = segment_counts[i]; |
||||||
|
if (segments > 0) { |
||||||
|
__builtin_memcpy(points, buffers[i], segments * 4 * 2); |
||||||
|
__builtin_memcpy(ids, buffers[i] + segments * 4 * 2, segments * 4); |
||||||
|
__builtin_memcpy(pressures, buffers[i] + segments * 4 * 3, segments); |
||||||
|
|
||||||
|
points += segments * 2; |
||||||
|
ids += segments; |
||||||
|
pressures += segments; |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
segment_counts[0] = total_segments; |
||||||
|
|
||||||
|
return(merged); |
||||||
|
} |
@ -0,0 +1,615 @@ |
|||||||
|
function schedule_draw(state, context, animate = false) { |
||||||
|
if (!state.timers.raf) { |
||||||
|
window.requestAnimationFrame(async (ts) => { |
||||||
|
await draw(state, context, animate, ts); |
||||||
|
}); |
||||||
|
state.timers.raf = true; |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
function upload_if_needed(gl, buffer_kind, serializer) { |
||||||
|
if (serializer.need_gpu_allocate) { |
||||||
|
if (config.debug_print) console.debug('gpu allocate'); |
||||||
|
gl.bufferData(buffer_kind, serializer.size, gl.DYNAMIC_DRAW); |
||||||
|
serializer.need_gpu_allocate = false; |
||||||
|
serializer.gpu_upload_from = 0; |
||||||
|
} |
||||||
|
|
||||||
|
if (serializer.gpu_upload_from < serializer.offset) { |
||||||
|
if (config.debug_print) console.debug('gpu upload'); |
||||||
|
const upload_offset = serializer.gpu_upload_from; |
||||||
|
const upload_size = serializer.offset - upload_offset; |
||||||
|
gl.bufferSubData(buffer_kind, upload_offset, new Uint8Array(serializer.buffer, upload_offset, upload_size)); |
||||||
|
serializer.gpu_upload_from = serializer.offset; |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
function upload_square_rgba16ui_texture(gl, serializer, texture_size) { |
||||||
|
const bpp = 2 * 4; |
||||||
|
const data_size = serializer.offset - serializer.gpu_upload_from; |
||||||
|
|
||||||
|
let data_pixels = data_size / bpp; // data_size % bpp is expected to always be zero here
|
||||||
|
|
||||||
|
const pixels_already_uploaded = serializer.gpu_upload_from / bpp; |
||||||
|
let rows_uploaded = Math.floor(pixels_already_uploaded / texture_size); |
||||||
|
const rows_remainder = pixels_already_uploaded % texture_size; |
||||||
|
|
||||||
|
// Upload first non-whole row (if last upload was not a whole number of rows)
|
||||||
|
if (rows_remainder > 0) { |
||||||
|
const row_upload_to_full = texture_size - rows_remainder; |
||||||
|
const first_upload = Math.min(row_upload_to_full, data_pixels); |
||||||
|
|
||||||
|
if (first_upload > 0) { |
||||||
|
gl.texSubImage2D(gl.TEXTURE_2D, 0, rows_remainder, rows_uploaded, first_upload, 1, gl.RGBA_INTEGER, gl.UNSIGNED_SHORT, new Uint16Array(serializer.buffer, serializer.gpu_upload_from, first_upload * 4)); |
||||||
|
|
||||||
|
data_pixels -= first_upload; |
||||||
|
serializer.gpu_upload_from += first_upload; |
||||||
|
rows_uploaded += 1; |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
const rows = Math.ceil(data_pixels / texture_size); |
||||||
|
const last_row = data_pixels % texture_size; |
||||||
|
const whole_upload = (rows - 1) * texture_size; |
||||||
|
|
||||||
|
// Upload whole rows
|
||||||
|
if (rows > 1) { |
||||||
|
gl.texSubImage2D(gl.TEXTURE_2D, 0, 0, rows_uploaded, texture_size, rows - 1, gl.RGBA_INTEGER, gl.UNSIGNED_SHORT, new Uint16Array(serializer.buffer, serializer.gpu_upload_from, whole_upload * 4)); |
||||||
|
rows_uploaded += rows - 1; |
||||||
|
} |
||||||
|
|
||||||
|
// Upload last row
|
||||||
|
if (last_row > 0) { |
||||||
|
const last_row_upload = last_row * bpp; |
||||||
|
gl.texSubImage2D(gl.TEXTURE_2D, 0, 0, rows_uploaded, last_row, 1, gl.RGBA_INTEGER, gl.UNSIGNED_SHORT, new Uint16Array(serializer.buffer, whole_upload, last_row_upload * 4)); |
||||||
|
} |
||||||
|
|
||||||
|
serializer.gpu_upload_from = serializer.offset; |
||||||
|
} |
||||||
|
|
||||||
|
function draw_html(state) { |
||||||
|
// HUD-like things. Player cursors, screens
|
||||||
|
for (const player_id in state.players) { |
||||||
|
if (player_id === state.me) continue; |
||||||
|
|
||||||
|
const player = state.players[player_id]; |
||||||
|
let player_cursor_element = document.querySelector(`.player-cursor[data-player-id="${player_id}"]`); |
||||||
|
|
||||||
|
if (player_cursor_element === null && player.online) { |
||||||
|
player_cursor_element = insert_player_cursor(state, player_id); |
||||||
|
} |
||||||
|
|
||||||
|
if (!player.online && player_cursor_element !== null) { |
||||||
|
player_cursor_element.remove(); |
||||||
|
const player_list_item = document.querySelector(`.player-list .player[data-player-id="${player_id}"]`); |
||||||
|
if (player_list_item) player_list_item.remove(); |
||||||
|
if (document.querySelector('.player-list').childElementCount === 0) { |
||||||
|
document.querySelector('.player-list').classList.add('vhide'); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
if (player_cursor_element && player.online) { |
||||||
|
const screenp = canvas_to_screen(state, player.cursor); |
||||||
|
player_cursor_element.style.transform = `translate(${Math.round(screenp.x)}px, ${Math.round(screenp.y)}px) rotate(-30deg)`; |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
|
||||||
|
async function draw(state, context, animate, ts) { |
||||||
|
const dt = ts - context.last_frame_ts; |
||||||
|
const cpu_before = performance.now(); |
||||||
|
|
||||||
|
context.last_frame_ts = ts; |
||||||
|
|
||||||
|
const gl = context.gl; |
||||||
|
const width = window.innerWidth; |
||||||
|
const height = window.innerHeight; |
||||||
|
|
||||||
|
bvh_clip(state, context); |
||||||
|
|
||||||
|
const segment_count = await geometry_write_instances(state, context); |
||||||
|
const dynamic_segment_count = context.dynamic_segment_count; |
||||||
|
const dynamic_stroke_count = context.dynamic_stroke_count; |
||||||
|
|
||||||
|
let query = null; |
||||||
|
|
||||||
|
if (context.gpu_timer_ext !== null) { |
||||||
|
query = gl.createQuery(); |
||||||
|
gl.beginQuery(context.gpu_timer_ext.TIME_ELAPSED_EXT, query); |
||||||
|
} |
||||||
|
|
||||||
|
// Only clear once we have the data, this might not always be on the same frame?
|
||||||
|
gl.viewport(0, 0, context.canvas.width, context.canvas.height); |
||||||
|
gl.clearColor(context.bgcolor.r, context.bgcolor.g, context.bgcolor.b, 1); |
||||||
|
gl.clearDepth(0.0); |
||||||
|
gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT); |
||||||
|
|
||||||
|
const locations = context.locations; |
||||||
|
const buffers = context.buffers; |
||||||
|
const programs = context.programs; |
||||||
|
const textures = context.textures; |
||||||
|
|
||||||
|
// Draw the background pattern
|
||||||
|
if (state.background_pattern === 'dots') { |
||||||
|
const pr = programs['dots']; |
||||||
|
gl.useProgram(pr.program); |
||||||
|
|
||||||
|
gl.bindBuffer(gl.ARRAY_BUFFER, buffers['b_instance_dot']); |
||||||
|
gl.enableVertexAttribArray(pr.locations['a_center']); |
||||||
|
gl.vertexAttribPointer(pr.locations['a_center'], 2, gl.FLOAT, false, 2 * 4, 0); |
||||||
|
gl.vertexAttribDivisor(pr.locations['a_center'], 1); |
||||||
|
|
||||||
|
gl.uniform2f(pr.locations['u_res'], context.canvas.width, context.canvas.height); |
||||||
|
gl.uniform2f(pr.locations['u_scale'], state.canvas.zoom, state.canvas.zoom); |
||||||
|
gl.uniform2f(pr.locations['u_translation'], state.canvas.offset.x, state.canvas.offset.y); |
||||||
|
|
||||||
|
const zoom = state.canvas.zoom; |
||||||
|
const zoom_log2 = Math.log2(zoom); |
||||||
|
const zoom_previous = Math.pow(2, Math.floor(zoom_log2)); |
||||||
|
const zoom_next = Math.pow(2, Math.ceil(zoom_log2)); |
||||||
|
|
||||||
|
// Previous level
|
||||||
|
{ |
||||||
|
const one_dot = new Float32Array(geometry_gen_quad(0, 0, 1 / zoom_previous)); |
||||||
|
const dot_instances = new Float32Array(geometry_gen_fullscreen_grid(state, context, 32 / zoom_previous, 32 / zoom_previous)); |
||||||
|
const t = Math.min(1.0, 1.0 - (zoom / zoom_previous) / 2.0); |
||||||
|
|
||||||
|
gl.uniform1f(pr.locations['u_fadeout'], t); |
||||||
|
|
||||||
|
gl.bindBuffer(gl.ARRAY_BUFFER, buffers['b_instance_dot']); |
||||||
|
gl.bufferData(gl.ARRAY_BUFFER, dot_instances, gl.STREAM_DRAW); |
||||||
|
|
||||||
|
gl.drawArraysInstanced(gl.TRIANGLES, 0, 6, dot_instances.length / 2); |
||||||
|
} |
||||||
|
|
||||||
|
// Next level
|
||||||
|
if (zoom_previous != zoom_next) { |
||||||
|
const dot_instances = new Float32Array(geometry_gen_fullscreen_grid(state, context, 32 / zoom_next, 32 / zoom_next)); |
||||||
|
const t = Math.min(1.0, 1.0 - (zoom_next / zoom) / 2.0); |
||||||
|
|
||||||
|
gl.uniform1f(pr.locations['u_fadeout'], t); |
||||||
|
|
||||||
|
gl.bindBuffer(gl.ARRAY_BUFFER, buffers['b_instance_dot']); |
||||||
|
gl.bufferData(gl.ARRAY_BUFFER, dot_instances, gl.STREAM_DRAW); |
||||||
|
|
||||||
|
gl.drawArraysInstanced(gl.TRIANGLES, 0, 6, dot_instances.length / 2); |
||||||
|
} |
||||||
|
} else if (state.background_pattern === 'grid') { |
||||||
|
const pr = programs['grid']; |
||||||
|
const zoom = state.canvas.zoom; |
||||||
|
|
||||||
|
let zoom_log8 = Math.log(zoom) / Math.log(8); |
||||||
|
//if (zoom_log2 === Math.floor(zoom_log2)) {
|
||||||
|
// zoom_log2 -= 0.001;
|
||||||
|
//}
|
||||||
|
|
||||||
|
const zoom_previous = Math.pow(8, Math.floor(zoom_log8)); |
||||||
|
let zoom_next = Math.pow(8, Math.ceil(zoom_log8)); |
||||||
|
|
||||||
|
if (zoom_next === zoom_previous) { |
||||||
|
zoom_next = zoom_previous * 8; |
||||||
|
} |
||||||
|
|
||||||
|
gl.useProgram(pr.program); |
||||||
|
|
||||||
|
gl.bindBuffer(gl.ARRAY_BUFFER, buffers['b_instance_grid']); |
||||||
|
gl.enableVertexAttribArray(pr.locations['a_data']); |
||||||
|
gl.vertexAttribPointer(pr.locations['a_data'], 2, gl.FLOAT, false, 2 * 4, 0); |
||||||
|
gl.vertexAttribDivisor(pr.locations['a_data'], 1); |
||||||
|
|
||||||
|
gl.uniform2f(pr.locations['u_res'], context.canvas.width, context.canvas.height); |
||||||
|
gl.uniform2f(pr.locations['u_scale'], state.canvas.zoom, state.canvas.zoom); |
||||||
|
gl.uniform2f(pr.locations['u_translation'], state.canvas.offset.x, state.canvas.offset.y); |
||||||
|
gl.uniform1f(pr.locations['u_fadeout'], 1.0); |
||||||
|
|
||||||
|
// Previous level (major lines)
|
||||||
|
{ |
||||||
|
const grid_instances = new Float32Array(geometry_gen_fullscreen_grid_1d(state, context, 32 / zoom_previous, 32 / zoom_previous)); |
||||||
|
let t = (zoom / zoom_previous - 1) / -7 + 1; |
||||||
|
t = 0.25; |
||||||
|
|
||||||
|
gl.uniform1f(pr.locations['u_fadeout'], t); |
||||||
|
|
||||||
|
gl.bindBuffer(gl.ARRAY_BUFFER, buffers['b_instance_grid']); |
||||||
|
gl.bufferData(gl.ARRAY_BUFFER, grid_instances, gl.STREAM_DRAW); |
||||||
|
|
||||||
|
gl.drawArraysInstanced(gl.TRIANGLES, 0, 6, grid_instances.length / 2); |
||||||
|
} |
||||||
|
|
||||||
|
// Next level (minor lines)
|
||||||
|
{ |
||||||
|
const grid_instances = new Float32Array(geometry_gen_fullscreen_grid_1d(state, context, 32 / zoom_next, 32 / zoom_next)); |
||||||
|
let t = (zoom_next / zoom - 1) / 7; |
||||||
|
t = Math.min(0.1, -t + 1); // slight fade-in
|
||||||
|
|
||||||
|
gl.uniform1f(pr.locations['u_fadeout'], t); |
||||||
|
|
||||||
|
gl.bindBuffer(gl.ARRAY_BUFFER, buffers['b_instance_grid']); |
||||||
|
gl.bufferData(gl.ARRAY_BUFFER, grid_instances, gl.STREAM_DRAW); |
||||||
|
|
||||||
|
gl.drawArraysInstanced(gl.TRIANGLES, 0, 6, grid_instances.length / 2); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
// Images
|
||||||
|
{ |
||||||
|
const pr = programs['image']; |
||||||
|
|
||||||
|
gl.clear(gl.DEPTH_BUFFER_BIT); // draw images above the background pattern
|
||||||
|
gl.useProgram(pr.program); |
||||||
|
|
||||||
|
let offset = 0; |
||||||
|
|
||||||
|
const quads = geometry_image_quads(state, context); |
||||||
|
|
||||||
|
gl.bindBuffer(gl.ARRAY_BUFFER, buffers['b_images']); |
||||||
|
gl.bufferData(gl.ARRAY_BUFFER, quads, gl.STATIC_DRAW); |
||||||
|
gl.vertexAttribDivisor(pr.locations['a_pos'], 0); |
||||||
|
|
||||||
|
gl.enableVertexAttribArray(pr.locations['a_pos']); |
||||||
|
gl.vertexAttribPointer(pr.locations['a_pos'], 2, gl.FLOAT, false, 2 * 4, 0); |
||||||
|
|
||||||
|
for (const entry of context.images) { |
||||||
|
if (!entry.deleted) { |
||||||
|
gl.uniform2f(pr.locations['u_res'], context.canvas.width, context.canvas.height); |
||||||
|
gl.uniform2f(pr.locations['u_scale'], state.canvas.zoom, state.canvas.zoom); |
||||||
|
gl.uniform2f(pr.locations['u_translation'], state.canvas.offset.x, state.canvas.offset.y); |
||||||
|
gl.uniform1i(pr.locations['u_texture'], 0); // Only 1 active texture for each drawcall
|
||||||
|
gl.uniform1i(pr.locations['u_solid'], 0); |
||||||
|
|
||||||
|
gl.bindTexture(gl.TEXTURE_2D, entry.texture); |
||||||
|
gl.drawArrays(gl.TRIANGLES, offset, 6); |
||||||
|
|
||||||
|
// Highlight active image
|
||||||
|
if (entry.key === state.active_image) { |
||||||
|
gl.uniform1i(pr.locations['u_solid'], 1); |
||||||
|
gl.uniform4f(pr.locations['u_color'], 0.133 * 0.5, 0.545 * 0.5, 0.902 * 0.5, 0.5); |
||||||
|
gl.drawArrays(gl.TRIANGLES, offset, 6); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
offset += 6; |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
const circle_segments = 32; |
||||||
|
const circle_points = circle_segments * 3; |
||||||
|
const circle_data = geometry_line_segments_with_two_circles(circle_segments); |
||||||
|
|
||||||
|
// "Static" data upload
|
||||||
|
if (segment_count > 0) { |
||||||
|
const pr = programs['main']; |
||||||
|
|
||||||
|
gl.clear(gl.DEPTH_BUFFER_BIT); // draw strokes above the images
|
||||||
|
gl.useProgram(pr.program); |
||||||
|
|
||||||
|
const total_static_size = context.instance_data_points.size * 4 + |
||||||
|
context.instance_data_ids.size * 4 + |
||||||
|
round_to_pow2(context.instance_data_pressures.size, 4) + |
||||||
|
circle_data.length * 4; |
||||||
|
|
||||||
|
gl.bindBuffer(gl.ARRAY_BUFFER, buffers['b_strokes_static']); |
||||||
|
gl.bufferData(gl.ARRAY_BUFFER, total_static_size, gl.DYNAMIC_DRAW); |
||||||
|
gl.bufferSubData(gl.ARRAY_BUFFER, 0, tv_data(context.instance_data_points)); |
||||||
|
gl.bufferSubData(gl.ARRAY_BUFFER, context.instance_data_points.size * 4, tv_data(context.instance_data_ids)); |
||||||
|
gl.bufferSubData(gl.ARRAY_BUFFER, context.instance_data_points.size * 4 + context.instance_data_ids.size * 4, |
||||||
|
tv_data(context.instance_data_pressures)); |
||||||
|
gl.bufferSubData(gl.ARRAY_BUFFER, context.instance_data_points.size * 4 + context.instance_data_ids.size * 4 + round_to_pow2(context.instance_data_pressures.size, 4), |
||||||
|
circle_data); |
||||||
|
gl.bindTexture(gl.TEXTURE_2D, textures['stroke_data']); |
||||||
|
upload_square_rgba16ui_texture(gl, context.stroke_data, config.stroke_texture_size); |
||||||
|
|
||||||
|
gl.uniform2f(pr.locations['u_res'], context.canvas.width, context.canvas.height); |
||||||
|
gl.uniform2f(pr.locations['u_scale'], state.canvas.zoom, state.canvas.zoom); |
||||||
|
gl.uniform2f(pr.locations['u_translation'], state.canvas.offset.x, state.canvas.offset.y); |
||||||
|
gl.uniform1i(pr.locations['u_stroke_count'], state.events.length); |
||||||
|
gl.uniform1i(pr.locations['u_debug_mode'], state.debug.red); |
||||||
|
gl.uniform1i(pr.locations['u_stroke_data'], 0); |
||||||
|
gl.uniform1i(pr.locations['u_stroke_texture_size'], config.stroke_texture_size); |
||||||
|
gl.uniform1f(pr.locations['u_fixed_pixel_width'], 0); |
||||||
|
gl.uniform1i(pr.locations['u_circle_points'], circle_points); |
||||||
|
|
||||||
|
gl.enableVertexAttribArray(pr.locations['a_pos']); |
||||||
|
gl.enableVertexAttribArray(pr.locations['a_a']); |
||||||
|
gl.enableVertexAttribArray(pr.locations['a_b']); |
||||||
|
gl.enableVertexAttribArray(pr.locations['a_stroke_id']); |
||||||
|
gl.enableVertexAttribArray(pr.locations['a_pressure']); |
||||||
|
|
||||||
|
// Circle meshes (shared for all instances)
|
||||||
|
gl.vertexAttribPointer(pr.locations['a_pos'], 2, gl.FLOAT, false, 2 * 4, context.instance_data_points.size * 4 + context.instance_data_ids.size * 4 + round_to_pow2(context.instance_data_pressures.size, 4)); |
||||||
|
|
||||||
|
// Points (a, b) and stroke ids are stored in separate cpu buffers so that points can be reused (look at stride and offset values)
|
||||||
|
gl.vertexAttribPointer(pr.locations['a_a'], 2, gl.FLOAT, false, 2 * 4, 0); |
||||||
|
gl.vertexAttribPointer(pr.locations['a_b'], 2, gl.FLOAT, false, 2 * 4, 2 * 4); |
||||||
|
gl.vertexAttribIPointer(pr.locations['a_stroke_id'], 1, gl.INT, 4, context.instance_data_points.size * 4); |
||||||
|
gl.vertexAttribPointer(pr.locations['a_pressure'], 2, gl.UNSIGNED_BYTE, true, 1, context.instance_data_points.size * 4 + context.instance_data_ids.size * 4); |
||||||
|
|
||||||
|
gl.vertexAttribDivisor(pr.locations['a_pos'], 0); |
||||||
|
gl.vertexAttribDivisor(pr.locations['a_a'], 1); |
||||||
|
gl.vertexAttribDivisor(pr.locations['a_b'], 1); |
||||||
|
gl.vertexAttribDivisor(pr.locations['a_stroke_id'], 1); |
||||||
|
gl.vertexAttribDivisor(pr.locations['a_pressure'], 1); |
||||||
|
|
||||||
|
// Static draw (everything already bound)
|
||||||
|
gl.drawArraysInstanced(gl.TRIANGLES, 0, circle_points + 6, segment_count); |
||||||
|
|
||||||
|
// I don't really know why I need to do this, but it
|
||||||
|
// makes background patter drawcall work properly
|
||||||
|
gl.vertexAttribDivisor(pr.locations['a_pos'], 0); |
||||||
|
gl.vertexAttribDivisor(pr.locations['a_a'], 0); |
||||||
|
gl.vertexAttribDivisor(pr.locations['a_b'], 0); |
||||||
|
gl.vertexAttribDivisor(pr.locations['a_stroke_id'], 0); |
||||||
|
gl.vertexAttribDivisor(pr.locations['a_pressure'], 0); |
||||||
|
} |
||||||
|
|
||||||
|
|
||||||
|
// Dynamic draw (strokes currently being drawn)
|
||||||
|
if (false && dynamic_segment_count > 0) { |
||||||
|
const pr = programs['main']; // same as static
|
||||||
|
|
||||||
|
// Dynamic strokes should be drawn above static strokes
|
||||||
|
gl.clear(gl.DEPTH_BUFFER_BIT); |
||||||
|
gl.useProgram(pr.program); |
||||||
|
|
||||||
|
gl.uniform1i(pr.locations['u_stroke_count'], dynamic_stroke_count); |
||||||
|
gl.uniform1i(pr.locations['u_stroke_data'], 0); |
||||||
|
gl.uniform1i(pr.locations['u_stroke_texture_size'], config.dynamic_stroke_texture_size); |
||||||
|
|
||||||
|
gl.bindBuffer(gl.ARRAY_BUFFER, buffers['b_strokes_dynamic']); |
||||||
|
|
||||||
|
// Dynamic data upload
|
||||||
|
const total_dynamic_size = |
||||||
|
context.dynamic_instance_points.size * 4 + context.dynamic_instance_ids.size * 4 + |
||||||
|
context.dynamic_instance_pressure.size; |
||||||
|
|
||||||
|
gl.bufferData(gl.ARRAY_BUFFER, total_dynamic_size, gl.STREAM_DRAW); |
||||||
|
gl.bufferSubData(gl.ARRAY_BUFFER, 0, tv_data(context.dynamic_instance_points)); |
||||||
|
gl.bufferSubData(gl.ARRAY_BUFFER, context.dynamic_instance_points.size * 4, tv_data(context.dynamic_instance_ids)); |
||||||
|
gl.bufferSubData(gl.ARRAY_BUFFER, context.dynamic_instance_points.size * 4 + context.dynamic_instance_ids.size * 4, |
||||||
|
tv_data(context.dynamic_instance_pressure)); |
||||||
|
gl.bindTexture(gl.TEXTURE_2D, textures['dynamic_stroke_data']); |
||||||
|
upload_square_rgba16ui_texture(gl, context.dynamic_stroke_data, config.dynamic_stroke_texture_size); |
||||||
|
|
||||||
|
gl.uniform2f(pr.locations['u_res'], context.canvas.width, context.canvas.height); |
||||||
|
gl.uniform2f(pr.locations['u_scale'], state.canvas.zoom, state.canvas.zoom); |
||||||
|
gl.uniform2f(pr.locations['u_translation'], state.canvas.offset.x, state.canvas.offset.y); |
||||||
|
|
||||||
|
gl.uniform1i(pr.locations['u_stroke_count'], context.dynamic_stroke_count); |
||||||
|
gl.uniform1i(pr.locations['u_debug_mode'], state.debug.red); |
||||||
|
gl.uniform1i(pr.locations['u_stroke_data'], 0); |
||||||
|
gl.uniform1i(pr.locations['u_stroke_texture_size'], config.dynamic_stroke_texture_size); |
||||||
|
gl.uniform1f(pr.locations['u_fixed_pixel_width'], 0); |
||||||
|
|
||||||
|
gl.enableVertexAttribArray(pr.locations['a_a']); |
||||||
|
gl.enableVertexAttribArray(pr.locations['a_b']); |
||||||
|
gl.enableVertexAttribArray(pr.locations['a_stroke_id']); |
||||||
|
gl.enableVertexAttribArray(pr.locations['a_pressure']); |
||||||
|
|
||||||
|
// Points (a, b) and stroke ids are stored in separate cpu buffers so that points can be reused (look at stride and offset values)
|
||||||
|
if (context.dynamic_instance_ids.size > 1) { |
||||||
|
gl.vertexAttribPointer(pr.locations['a_a'], 2, gl.FLOAT, false, 2 * 4, 0); |
||||||
|
gl.vertexAttribPointer(pr.locations['a_b'], 2, gl.FLOAT, false, 2 * 4, 2 * 4); |
||||||
|
} else { |
||||||
|
// A special case where there is no second point. Reuse the first point and handle the zero length segment in the shader
|
||||||
|
gl.vertexAttribPointer(pr.locations['a_a'], 2, gl.FLOAT, false, 2 * 4, 0); |
||||||
|
gl.vertexAttribPointer(pr.locations['a_b'], 2, gl.FLOAT, false, 2 * 4, 0); |
||||||
|
} |
||||||
|
|
||||||
|
gl.vertexAttribIPointer(pr.locations['a_stroke_id'], 1, gl.INT, 4, context.dynamic_instance_points.size * 4); |
||||||
|
gl.vertexAttribPointer(pr.locations['a_pressure'], 2, gl.UNSIGNED_BYTE, true, 1, context.dynamic_instance_points.size * 4 + context.dynamic_instance_ids.size * 4); |
||||||
|
|
||||||
|
gl.vertexAttribDivisor(pr.locations['a_a'], 1); |
||||||
|
gl.vertexAttribDivisor(pr.locations['a_b'], 1); |
||||||
|
gl.vertexAttribDivisor(pr.locations['a_stroke_id'], 1); |
||||||
|
gl.vertexAttribDivisor(pr.locations['a_pressure'], 1); |
||||||
|
|
||||||
|
gl.drawArraysInstanced(gl.TRIANGLES, 0, 32 * 3 + 6 + 32 * 3, dynamic_segment_count); |
||||||
|
|
||||||
|
gl.vertexAttribDivisor(pr.locations['a_a'], 0); |
||||||
|
gl.vertexAttribDivisor(pr.locations['a_b'], 0); |
||||||
|
gl.vertexAttribDivisor(pr.locations['a_stroke_id'], 0); |
||||||
|
gl.vertexAttribDivisor(pr.locations['a_pressure'], 0); |
||||||
|
} |
||||||
|
|
||||||
|
// HUD: resize handles, etc
|
||||||
|
if (state.active_image !== null) { |
||||||
|
const pr = programs['main']; // same as static
|
||||||
|
const handles = geometry_generate_handles(state, context, state.active_image); |
||||||
|
const ui_segments = 7 * 4 - 1; // each square = 4, each line = 1, square->line = 1, line->square = 1
|
||||||
|
|
||||||
|
gl.bindBuffer(gl.ARRAY_BUFFER, buffers['b_hud']); |
||||||
|
gl.bufferData(gl.ARRAY_BUFFER, handles.points.byteLength + handles.ids.byteLength + handles.pressures.byteLength, gl.STREAM_DRAW); |
||||||
|
gl.bufferSubData(gl.ARRAY_BUFFER, 0, handles.points); |
||||||
|
gl.bufferSubData(gl.ARRAY_BUFFER, handles.points.byteLength, handles.ids); |
||||||
|
gl.bufferSubData(gl.ARRAY_BUFFER, handles.points.byteLength + handles.ids.byteLength, handles.pressures); |
||||||
|
|
||||||
|
gl.bindTexture(gl.TEXTURE_2D, textures['ui']); |
||||||
|
upload_square_rgba16ui_texture(gl, handles.stroke_data, config.ui_texture_size); |
||||||
|
|
||||||
|
gl.uniform2f(pr.locations['u_res'], context.canvas.width, context.canvas.height); |
||||||
|
gl.uniform2f(pr.locations['u_scale'], state.canvas.zoom, state.canvas.zoom); |
||||||
|
gl.uniform2f(pr.locations['u_translation'], state.canvas.offset.x, state.canvas.offset.y); |
||||||
|
gl.uniform1i(pr.locations['u_stroke_count'], 8); |
||||||
|
gl.uniform1i(pr.locations['u_debug_mode'], 0); |
||||||
|
gl.uniform1i(pr.locations['u_stroke_data'], 0); |
||||||
|
gl.uniform1i(pr.locations['u_stroke_texture_size'], config.ui_texture_size); |
||||||
|
gl.uniform1f(pr.locations['u_fixed_pixel_width'], 2); |
||||||
|
|
||||||
|
gl.enableVertexAttribArray(pr.locations['a_a']); |
||||||
|
gl.enableVertexAttribArray(pr.locations['a_b']); |
||||||
|
gl.enableVertexAttribArray(pr.locations['a_stroke_id']); |
||||||
|
gl.enableVertexAttribArray(pr.locations['a_pressure']); |
||||||
|
|
||||||
|
gl.vertexAttribPointer(pr.locations['a_a'], 2, gl.FLOAT, false, 2 * 4, 0); |
||||||
|
gl.vertexAttribPointer(pr.locations['a_b'], 2, gl.FLOAT, false, 2 * 4, 2 * 4); |
||||||
|
gl.vertexAttribIPointer(pr.locations['a_stroke_id'], 1, gl.INT, 4, handles.points.byteLength); |
||||||
|
gl.vertexAttribPointer(pr.locations['a_pressure'], 2, gl.UNSIGNED_BYTE, true, 1, handles.points.byteLength + handles.ids.byteLength); |
||||||
|
|
||||||
|
gl.vertexAttribDivisor(pr.locations['a_a'], 1); |
||||||
|
gl.vertexAttribDivisor(pr.locations['a_b'], 1); |
||||||
|
gl.vertexAttribDivisor(pr.locations['a_stroke_id'], 1); |
||||||
|
gl.vertexAttribDivisor(pr.locations['a_pressure'], 1); |
||||||
|
|
||||||
|
gl.drawArraysInstanced(gl.TRIANGLES, 0, 32 * 3 + 6 + 32 * 3, ui_segments); |
||||||
|
|
||||||
|
// I don't really know why I need to do this, but it
|
||||||
|
// makes background patter drawcall work properly
|
||||||
|
gl.vertexAttribDivisor(pr.locations['a_a'], 0); |
||||||
|
gl.vertexAttribDivisor(pr.locations['a_b'], 0); |
||||||
|
gl.vertexAttribDivisor(pr.locations['a_stroke_id'], 0); |
||||||
|
gl.vertexAttribDivisor(pr.locations['a_pressure'], 0); |
||||||
|
} |
||||||
|
|
||||||
|
if (config.draw_bvh) { |
||||||
|
const pr = programs['iquad']; |
||||||
|
const bboxes = tv_create(Float32Array, context.clipped_indices.size * 4); |
||||||
|
// Debug BVH viz
|
||||||
|
for (let i = 0; i < context.clipped_indices.size; ++i) { |
||||||
|
const stroke_id = context.clipped_indices.data[i]; |
||||||
|
const stroke = state.events[stroke_id]; |
||||||
|
tv_add(bboxes, stroke.bbox.x1); |
||||||
|
tv_add(bboxes, stroke.bbox.y1); |
||||||
|
tv_add(bboxes, stroke.bbox.x2); |
||||||
|
tv_add(bboxes, stroke.bbox.y2); |
||||||
|
} |
||||||
|
|
||||||
|
const quad_count = bboxes.size / 4; |
||||||
|
|
||||||
|
gl.useProgram(pr.program); |
||||||
|
|
||||||
|
gl.bindBuffer(gl.ARRAY_BUFFER, buffers['b_iquads']); |
||||||
|
gl.bufferData(gl.ARRAY_BUFFER, tv_data(bboxes), gl.STREAM_DRAW); |
||||||
|
|
||||||
|
gl.uniform2f(pr.locations['u_res'], context.canvas.width, context.canvas.height); |
||||||
|
gl.uniform2f(pr.locations['u_scale'], state.canvas.zoom, state.canvas.zoom); |
||||||
|
gl.uniform2f(pr.locations['u_translation'], state.canvas.offset.x, state.canvas.offset.y); |
||||||
|
|
||||||
|
gl.enableVertexAttribArray(pr.locations['a_topleft']); |
||||||
|
gl.enableVertexAttribArray(pr.locations['a_bottomright']); |
||||||
|
|
||||||
|
gl.vertexAttribPointer(pr.locations['a_topleft'], 2, gl.FLOAT, false, 4 * 4, 0); |
||||||
|
gl.vertexAttribPointer(pr.locations['a_bottomright'], 2, gl.FLOAT, false, 4 * 4, 2 * 4); |
||||||
|
|
||||||
|
gl.vertexAttribDivisor(pr.locations['a_topleft'], 1); |
||||||
|
gl.vertexAttribDivisor(pr.locations['a_bottomright'], 1); |
||||||
|
|
||||||
|
gl.drawArraysInstanced(gl.TRIANGLES, 0, 6, quad_count); |
||||||
|
|
||||||
|
gl.vertexAttribDivisor(pr.locations['a_topleft'], 0); |
||||||
|
gl.vertexAttribDivisor(pr.locations['a_bottomright'], 0); |
||||||
|
} |
||||||
|
|
||||||
|
if (config.draw_fullnodes) { |
||||||
|
const quads = bvh_get_fullnodes_debug(state, context); |
||||||
|
const pr = programs['iquad']; |
||||||
|
const bboxes = tv_create(Float32Array, quads.length * 4); |
||||||
|
|
||||||
|
for (let i = 0; i < quads.length; ++i) { |
||||||
|
const bbox = quads[i]; |
||||||
|
tv_add(bboxes, bbox.x1); |
||||||
|
tv_add(bboxes, bbox.y1); |
||||||
|
tv_add(bboxes, bbox.x2); |
||||||
|
tv_add(bboxes, bbox.y2); |
||||||
|
} |
||||||
|
|
||||||
|
const quad_count = bboxes.size / 4; |
||||||
|
|
||||||
|
gl.useProgram(pr.program); |
||||||
|
|
||||||
|
gl.bindBuffer(gl.ARRAY_BUFFER, buffers['b_iquads']); |
||||||
|
gl.bufferData(gl.ARRAY_BUFFER, tv_data(bboxes), gl.STREAM_DRAW); |
||||||
|
|
||||||
|
gl.uniform2f(pr.locations['u_res'], context.canvas.width, context.canvas.height); |
||||||
|
gl.uniform2f(pr.locations['u_scale'], state.canvas.zoom, state.canvas.zoom); |
||||||
|
gl.uniform2f(pr.locations['u_translation'], state.canvas.offset.x, state.canvas.offset.y); |
||||||
|
|
||||||
|
gl.enableVertexAttribArray(pr.locations['a_topleft']); |
||||||
|
gl.enableVertexAttribArray(pr.locations['a_bottomright']); |
||||||
|
|
||||||
|
gl.vertexAttribPointer(pr.locations['a_topleft'], 2, gl.FLOAT, false, 4 * 4, 0); |
||||||
|
gl.vertexAttribPointer(pr.locations['a_bottomright'], 2, gl.FLOAT, false, 4 * 4, 2 * 4); |
||||||
|
|
||||||
|
gl.vertexAttribDivisor(pr.locations['a_topleft'], 1); |
||||||
|
gl.vertexAttribDivisor(pr.locations['a_bottomright'], 1); |
||||||
|
|
||||||
|
gl.drawArraysInstanced(gl.TRIANGLES, 0, 6, quad_count); |
||||||
|
|
||||||
|
gl.vertexAttribDivisor(pr.locations['a_topleft'], 0); |
||||||
|
gl.vertexAttribDivisor(pr.locations['a_bottomright'], 0); |
||||||
|
} |
||||||
|
|
||||||
|
document.getElementById('debug-stats').innerHTML = ` |
||||||
|
<span>Strokes onscreen: ${context.clipped_indices.size}</span> |
||||||
|
<span>Segments onscreen: ${segment_count}</span> |
||||||
|
<span>Canvas offset: (${Math.round(state.canvas.offset.x * 100) / 100}, ${Math.round(state.canvas.offset.y * 100) / 100})</span> |
||||||
|
<span>Canvas zoom level: ${state.canvas.zoom_level}</span> |
||||||
|
<span>Canvas zoom: ${Math.round(state.canvas.zoom * 100) / 100}</span>`; |
||||||
|
|
||||||
|
if (context.gpu_timer_ext) { |
||||||
|
gl.endQuery(context.gpu_timer_ext.TIME_ELAPSED_EXT); |
||||||
|
|
||||||
|
const next_tick = () => { |
||||||
|
if (query) { |
||||||
|
// At some point in the future, after returning control to the browser
|
||||||
|
const available = gl.getQueryParameter(query, gl.QUERY_RESULT_AVAILABLE); |
||||||
|
const disjoint = gl.getParameter(context.gpu_timer_ext.GPU_DISJOINT_EXT); |
||||||
|
|
||||||
|
if (available && !disjoint) { |
||||||
|
// See how much time the rendering of the object took in nanoseconds.
|
||||||
|
const timeElapsed = gl.getQueryParameter(query, gl.QUERY_RESULT); |
||||||
|
//console.debug(timeElapsed / 1000000);
|
||||||
|
document.querySelector('.debug-timings .gpu').innerHTML = 'Last GPU Frametime: ' + Math.round(timeElapsed / 10000) / 100 + 'ms'; |
||||||
|
} |
||||||
|
|
||||||
|
if (available || disjoint) { |
||||||
|
// Clean up the query object.
|
||||||
|
gl.deleteQuery(query); |
||||||
|
// Don't re-enter this polling loop.
|
||||||
|
query = null; |
||||||
|
} else if (!available) { |
||||||
|
setTimeout(next_tick, 0); |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
setTimeout(next_tick, 0); |
||||||
|
} |
||||||
|
|
||||||
|
const cpu_after = performance.now(); |
||||||
|
|
||||||
|
state.timers.raf = false; |
||||||
|
|
||||||
|
document.querySelector('.debug-timings .cpu').innerHTML = 'Last CPU Frametime: ' + Math.round((cpu_after - cpu_before) * 100) / 100 + 'ms'; |
||||||
|
|
||||||
|
if (state.debug.benchmark_mode) { |
||||||
|
const redraw = state.debug.on_benchmark(); |
||||||
|
if (redraw) { |
||||||
|
schedule_draw(state, context); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
if (state.canvas.target_zoom != state.canvas.zoom) { |
||||||
|
update_canvas_zoom(state, state.canvas.zoom, state.canvas.target_zoom, animate ? dt : context.last_frame_dt); |
||||||
|
schedule_draw(state, context, true); |
||||||
|
} |
||||||
|
|
||||||
|
context.last_frame_dt = dt; |
||||||
|
} |
||||||
|
|
||||||
|
function update_canvas_zoom(state, current, target, dt) { |
||||||
|
const rate = Math.min(1.0, dt / 16.66 * 0.3); |
||||||
|
|
||||||
|
if (Math.abs(1.0 - current / target) > 0.01) { |
||||||
|
state.canvas.zoom = current + (target - current) * rate; |
||||||
|
} else { |
||||||
|
state.canvas.zoom = target; |
||||||
|
} |
||||||
|
|
||||||
|
// https://gist.github.com/aolo2/a373363419bd5a9283977ab9f8841f78
|
||||||
|
const zc = state.canvas.zoom_screenp; |
||||||
|
state.canvas.offset.x = zc.x - (zc.x - state.canvas.offset.x) * state.canvas.zoom / current; |
||||||
|
state.canvas.offset.y = zc.y - (zc.y - state.canvas.offset.y) * state.canvas.zoom / current; |
||||||
|
|
||||||
|
update_cursor(state); |
||||||
|
} |
@ -0,0 +1,549 @@ |
|||||||
|
function geometry_prepare_stroke(state) { |
||||||
|
if (!state.online) { |
||||||
|
return null; |
||||||
|
} |
||||||
|
|
||||||
|
const player = state.players[state.me]; |
||||||
|
const stroke = player.strokes[player.strokes.length - 1]; // MY OWN player.strokes should never be bigger than 1 element
|
||||||
|
|
||||||
|
if (stroke.points.length === 0) { |
||||||
|
return null; |
||||||
|
} |
||||||
|
|
||||||
|
const points = process_stroke2(state.canvas.zoom, stroke.points); |
||||||
|
|
||||||
|
return { |
||||||
|
'color': stroke.color, |
||||||
|
'width': stroke.width, |
||||||
|
'points': points, |
||||||
|
'user_id': state.me, |
||||||
|
}; |
||||||
|
} |
||||||
|
|
||||||
|
async function geometry_write_instances(state, context, callback) { |
||||||
|
state.stats.rdp_max_count = 0; |
||||||
|
state.stats.rdp_segments = 0; |
||||||
|
|
||||||
|
const segment_count = await do_lod(state, context); |
||||||
|
|
||||||
|
if (config.debug_print) console.debug('instances:', segment_count, 'rdp max:', state.stats.rdp_max_count, 'rdp segments:', state.stats.rdp_segments); |
||||||
|
|
||||||
|
return segment_count; |
||||||
|
} |
||||||
|
|
||||||
|
function geometry_add_dummy_stroke(context) { |
||||||
|
context.stroke_data = ser_ensure_by(context.stroke_data, config.bytes_per_stroke); |
||||||
|
ser_u16(context.stroke_data, 0); |
||||||
|
ser_u16(context.stroke_data, 0); |
||||||
|
ser_u16(context.stroke_data, 0); |
||||||
|
ser_u16(context.stroke_data, 0); |
||||||
|
} |
||||||
|
|
||||||
|
// Real stroke, add forever
|
||||||
|
function geometry_add_stroke(state, context, stroke, stroke_index, skip_bvh = false) { |
||||||
|
if (!state.online || !stroke || stroke.coords_to - stroke.coords_from === 0 || stroke.deleted) return; |
||||||
|
|
||||||
|
stroke.bbox = stroke_bbox(state, stroke); |
||||||
|
stroke.area = box_area(stroke.bbox); |
||||||
|
|
||||||
|
context.stroke_data = ser_ensure_by(context.stroke_data, config.bytes_per_stroke); |
||||||
|
|
||||||
|
const color_u32 = stroke.color; |
||||||
|
const r = (color_u32 >> 16) & 0xFF; |
||||||
|
const g = (color_u32 >> 8) & 0xFF; |
||||||
|
const b = color_u32 & 0xFF; |
||||||
|
|
||||||
|
ser_u16(context.stroke_data, r); |
||||||
|
ser_u16(context.stroke_data, g); |
||||||
|
ser_u16(context.stroke_data, b); |
||||||
|
ser_u16(context.stroke_data, stroke.width); |
||||||
|
|
||||||
|
if (!skip_bvh) bvh_add_stroke(state, state.bvh, stroke_index, stroke); |
||||||
|
} |
||||||
|
|
||||||
|
function recompute_dynamic_data(state, context) { |
||||||
|
let total_points = 0; |
||||||
|
let total_strokes = 0; |
||||||
|
|
||||||
|
for (const player_id in state.players) { |
||||||
|
const player = state.players[player_id]; |
||||||
|
for (const stroke of player.strokes) { |
||||||
|
if (!stroke.empty && stroke.points.length > 0) { |
||||||
|
total_points += stroke.points.length; |
||||||
|
total_strokes += 1; |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
tv_ensure(context.dynamic_instance_points, round_to_pow2(total_points * 2, 4096)); |
||||||
|
tv_ensure(context.dynamic_instance_pressure, round_to_pow2(total_points, 4096)); |
||||||
|
tv_ensure(context.dynamic_instance_ids, round_to_pow2(total_points, 4096)); |
||||||
|
|
||||||
|
tv_clear(context.dynamic_instance_points); |
||||||
|
tv_clear(context.dynamic_instance_pressure); |
||||||
|
tv_clear(context.dynamic_instance_ids); |
||||||
|
|
||||||
|
context.dynamic_stroke_data = ser_ensure(context.dynamic_stroke_data, config.bytes_per_stroke * total_strokes); |
||||||
|
ser_clear(context.dynamic_stroke_data); |
||||||
|
|
||||||
|
let stroke_index = 0; |
||||||
|
|
||||||
|
for (const player_id in state.players) { |
||||||
|
// player has the same data as their current stroke: points, color, width
|
||||||
|
const player = state.players[player_id]; |
||||||
|
|
||||||
|
for (const stroke of player.strokes) { |
||||||
|
if (!stroke.empty && stroke.points.length > 0) { |
||||||
|
for (let i = 0; i < stroke.points.length; ++i) { |
||||||
|
const p = stroke.points[i]; |
||||||
|
|
||||||
|
tv_add(context.dynamic_instance_points, p.x); |
||||||
|
tv_add(context.dynamic_instance_points, p.y); |
||||||
|
tv_add(context.dynamic_instance_pressure, p.pressure); |
||||||
|
|
||||||
|
if (i !== stroke.points.length - 1) { |
||||||
|
tv_add(context.dynamic_instance_ids, stroke_index); |
||||||
|
} else { |
||||||
|
tv_add(context.dynamic_instance_ids, stroke_index | (1 << 31)); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
const color_u32 = stroke.color; |
||||||
|
const r = (color_u32 >> 16) & 0xFF; |
||||||
|
const g = (color_u32 >> 8) & 0xFF; |
||||||
|
const b = color_u32 & 0xFF; |
||||||
|
|
||||||
|
ser_u16(context.dynamic_stroke_data, r); |
||||||
|
ser_u16(context.dynamic_stroke_data, g); |
||||||
|
ser_u16(context.dynamic_stroke_data, b); |
||||||
|
ser_u16(context.dynamic_stroke_data, stroke.width); |
||||||
|
|
||||||
|
stroke_index += 1; // TODO: proper player Z order
|
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
context.dynamic_segment_count = total_points; |
||||||
|
context.dynamic_stroke_count = total_strokes; |
||||||
|
} |
||||||
|
|
||||||
|
function geometry_start_prestroke(state, player_id) { |
||||||
|
if (!state.online) return; |
||||||
|
|
||||||
|
const player = state.players[player_id]; |
||||||
|
|
||||||
|
player.strokes.push({ |
||||||
|
'empty': false, |
||||||
|
'points': [], |
||||||
|
'head': null, |
||||||
|
'color': player.color, |
||||||
|
'width': player.width, |
||||||
|
}); |
||||||
|
|
||||||
|
player.current_prestroke = true; |
||||||
|
} |
||||||
|
|
||||||
|
function geometry_end_prestroke(state, player_id) { |
||||||
|
if (!state.online) return; |
||||||
|
const player = state.players[player_id]; |
||||||
|
player.current_prestroke = false; |
||||||
|
} |
||||||
|
|
||||||
|
function geometry_add_prepoint(state, context, player_id, point, is_pen, raw = false) { |
||||||
|
if (!state.online) return; |
||||||
|
|
||||||
|
const player = state.players[player_id]; |
||||||
|
const stroke = player.strokes[player.strokes.length - 1]; |
||||||
|
const points = stroke.points; |
||||||
|
|
||||||
|
if (point.pressure < config.min_pressure) { |
||||||
|
point.pressure = config.min_pressure; |
||||||
|
} |
||||||
|
|
||||||
|
if (points.length > 0 && !raw) { |
||||||
|
// pulled from "perfect-freehand" package. MIT
|
||||||
|
// https://github.com/steveruizok/perfect-freehand/
|
||||||
|
const streamline = 0.75; |
||||||
|
const t = 0.15 + (1 - streamline) * 0.85 |
||||||
|
const smooth_pressure = exponential_smoothing(points, point, 3); |
||||||
|
|
||||||
|
points.push({ |
||||||
|
'x': stroke.head.x * t + point.x * (1 - t), |
||||||
|
'y': stroke.head.y * t + point.y * (1 - t), |
||||||
|
'pressure': is_pen ? stroke.head.pressure * t + smooth_pressure * (1 - t) : point.pressure, |
||||||
|
}); |
||||||
|
|
||||||
|
if (is_pen) { |
||||||
|
point.pressure = smooth_pressure; |
||||||
|
} |
||||||
|
} else { |
||||||
|
points.push(point); |
||||||
|
} |
||||||
|
|
||||||
|
stroke.head = point; |
||||||
|
|
||||||
|
recompute_dynamic_data(state, context); |
||||||
|
} |
||||||
|
|
||||||
|
// Remove prestroke from dynamic data (usually because it's now a real stroke)
|
||||||
|
function geometry_clear_oldest_prestroke(state, context, player_id) { |
||||||
|
if (!state.online) return; |
||||||
|
|
||||||
|
const player = state.players[player_id]; |
||||||
|
player.strokes.shift(); |
||||||
|
|
||||||
|
recompute_dynamic_data(state, context); |
||||||
|
} |
||||||
|
|
||||||
|
function add_image(context, image_id, bitmap, p, width, height) { |
||||||
|
const gl = context.gl; |
||||||
|
let entry = null; |
||||||
|
|
||||||
|
// If bitmap not available yet - create placeholder
|
||||||
|
// Otherwise - upload actual bitmap
|
||||||
|
if (bitmap === null) { |
||||||
|
entry = { |
||||||
|
'texture': gl.createTexture(), |
||||||
|
'key': image_id, |
||||||
|
'at': {...p}, |
||||||
|
'raw_at': {...p}, |
||||||
|
'width': width, |
||||||
|
'height': height, |
||||||
|
'transform_history': [ p.x, p.y, width, height ], |
||||||
|
'transform_head': 4, |
||||||
|
}; |
||||||
|
|
||||||
|
context.images.push(entry); |
||||||
|
} else { |
||||||
|
entry = get_image(context, image_id); |
||||||
|
} |
||||||
|
|
||||||
|
gl.bindTexture(gl.TEXTURE_2D, entry.texture); |
||||||
|
|
||||||
|
if (bitmap !== null) { |
||||||
|
gl.texSubImage2D(gl.TEXTURE_2D, 0, 0, 0, gl.RGBA, gl.UNSIGNED_BYTE, bitmap); |
||||||
|
gl.generateMipmap(gl.TEXTURE_2D); |
||||||
|
} else { |
||||||
|
gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, width, height, 0, gl.RGBA, gl.UNSIGNED_BYTE, new Uint8Array(4 * width * height)); |
||||||
|
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR_MIPMAP_LINEAR); |
||||||
|
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.NEAREST); |
||||||
|
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE); |
||||||
|
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE); |
||||||
|
gl.generateMipmap(gl.TEXTURE_2D); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
function scale_image(image, corner, canvasp) { |
||||||
|
let new_width, new_height; |
||||||
|
|
||||||
|
const old_x2 = image.at.x + image.width; |
||||||
|
const old_y2 = image.at.y + image.height; |
||||||
|
|
||||||
|
if (corner === 0) { |
||||||
|
image.at.x = canvasp.x; |
||||||
|
image.at.y = canvasp.y; |
||||||
|
new_width = old_x2 - image.at.x; |
||||||
|
new_height = old_y2 - image.at.y; |
||||||
|
} else if (corner === 1) { |
||||||
|
image.at.y = canvasp.y; |
||||||
|
new_width = canvasp.x - image.at.x; |
||||||
|
new_height = old_y2 - image.at.y; |
||||||
|
} else if (corner === 2) { |
||||||
|
new_width = canvasp.x - image.at.x; |
||||||
|
new_height = canvasp.y - image.at.y; |
||||||
|
} else if (corner === 3) { |
||||||
|
image.at.x = canvasp.x; |
||||||
|
new_width = old_x2 - image.at.x; |
||||||
|
new_height = canvasp.y - image.at.y; |
||||||
|
} |
||||||
|
|
||||||
|
image.width = new_width; |
||||||
|
image.height = new_height; |
||||||
|
} |
||||||
|
|
||||||
|
function image_at(context, x, y) { |
||||||
|
// Iterate back to front to pick the image at the front first
|
||||||
|
for (let i = context.images.length - 1; i >= 0; --i) { |
||||||
|
const image = context.images[i]; |
||||||
|
if (!image.deleted) { |
||||||
|
const at = image.at; |
||||||
|
const w = image.width; |
||||||
|
const h = image.height; |
||||||
|
|
||||||
|
const in_x = (at.x <= x && x <= at.x + w) || (at.x + w <= x && x <= at.x); |
||||||
|
const in_y = (at.y <= y && y <= at.y + h) || (at.y + h <= y && y <= at.y); |
||||||
|
|
||||||
|
if (in_x && in_y) { |
||||||
|
return image; |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
return null; |
||||||
|
} |
||||||
|
|
||||||
|
function image_corner(state, image, canvasp) { |
||||||
|
const sp = canvas_to_screen(state, canvasp); |
||||||
|
const at = canvas_to_screen(state, image.at); |
||||||
|
const w = image.width * state.canvas.zoom; |
||||||
|
const h = image.height * state.canvas.zoom; |
||||||
|
|
||||||
|
const width = 8; |
||||||
|
|
||||||
|
if (at.x - width <= sp.x && sp.x <= at.x + width && at.y - width <= sp.y && sp.y <= at.y + width) { |
||||||
|
return 0; |
||||||
|
} |
||||||
|
|
||||||
|
if (at.x + w - width <= sp.x && sp.x <= at.x + w + width && at.y - width <= sp.y && sp.y <= at.y + width) { |
||||||
|
return 1; |
||||||
|
} |
||||||
|
|
||||||
|
if (at.x + w - width <= sp.x && sp.x <= at.x + w + width && at.y + h - width <= sp.y && sp.y <= at.y + h + width) { |
||||||
|
return 2; |
||||||
|
} |
||||||
|
|
||||||
|
if (at.x - width <= sp.x && sp.x <= at.x + width && at.y + h - width <= sp.y && sp.y <= at.y + h + width) { |
||||||
|
return 3; |
||||||
|
} |
||||||
|
|
||||||
|
return null; |
||||||
|
} |
||||||
|
|
||||||
|
function geometry_gen_circle(cx, cy, r, n) { |
||||||
|
const step = 2 * Math.PI / n; |
||||||
|
const result = []; |
||||||
|
|
||||||
|
for (let i = 0; i < n; ++i) { |
||||||
|
const theta = i * step; |
||||||
|
const next_theta = (i < n - 1 ? (i + 1) * step : 0); |
||||||
|
const x = cx + r * Math.cos(theta); |
||||||
|
const y = cy + r * Math.sin(theta); |
||||||
|
const next_x = cx + r * Math.cos(next_theta); |
||||||
|
const next_y = cy + r * Math.sin(next_theta); |
||||||
|
result.push(cx, cy, x, y, next_x, next_y); |
||||||
|
} |
||||||
|
|
||||||
|
return result; |
||||||
|
} |
||||||
|
|
||||||
|
function geometry_gen_quad(cx, cy, r) { |
||||||
|
const result = [ |
||||||
|
cx - r, |
||||||
|
cy - r, |
||||||
|
cx + r, |
||||||
|
cy - r, |
||||||
|
cx - r, |
||||||
|
cy + r, |
||||||
|
cx + r, |
||||||
|
cy + r, |
||||||
|
cx - r, |
||||||
|
cy + r, |
||||||
|
cx + r, |
||||||
|
cy - r, |
||||||
|
]; |
||||||
|
|
||||||
|
return result; |
||||||
|
} |
||||||
|
|
||||||
|
function geometry_gen_fullscreen_grid(state, context, step_x, step_y) { |
||||||
|
const result = []; |
||||||
|
const width = context.canvas.width; |
||||||
|
const height = context.canvas.height; |
||||||
|
const topleft = screen_to_canvas(state, {'x': 0, 'y': 0}); |
||||||
|
const bottomright = screen_to_canvas(state, {'x': width, 'y': height}); |
||||||
|
|
||||||
|
topleft.x = Math.floor(topleft.x / step_x) * step_x; |
||||||
|
topleft.y = Math.ceil(topleft.y / step_y) * step_y; |
||||||
|
|
||||||
|
bottomright.x = Math.floor(bottomright.x / step_x) * step_x; |
||||||
|
bottomright.y = Math.ceil(bottomright.y / step_y) * step_y; |
||||||
|
|
||||||
|
for (let y = topleft.y; y <= bottomright.y; y += step_y) { |
||||||
|
for (let x = topleft.x; x <= bottomright.x; x += step_x) { |
||||||
|
result.push(x, y); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
return result; |
||||||
|
} |
||||||
|
|
||||||
|
function geometry_gen_fullscreen_grid_1d(state, context, step_x, step_y) { |
||||||
|
const result = []; |
||||||
|
const width = context.canvas.width; |
||||||
|
const height = context.canvas.height; |
||||||
|
const topleft = screen_to_canvas(state, {'x': 0, 'y': 0}); |
||||||
|
const bottomright = screen_to_canvas(state, {'x': width, 'y': height}); |
||||||
|
|
||||||
|
topleft.x = Math.floor(topleft.x / step_x) * step_x; |
||||||
|
topleft.y = Math.floor(topleft.y / step_y) * step_y; |
||||||
|
|
||||||
|
bottomright.x = Math.ceil(bottomright.x / step_x) * step_x; |
||||||
|
bottomright.y = Math.ceil(bottomright.y / step_y) * step_y; |
||||||
|
|
||||||
|
for (let x = topleft.x; x <= bottomright.x; x += step_x) { |
||||||
|
result.push(1, x); |
||||||
|
} |
||||||
|
|
||||||
|
for (let y = topleft.y; y <= bottomright.y; y += step_y) { |
||||||
|
result.push(-1, y); |
||||||
|
} |
||||||
|
|
||||||
|
return result; |
||||||
|
} |
||||||
|
|
||||||
|
function geometry_image_quads(state, context) { |
||||||
|
const result = new Float32Array(context.images.length * 12); |
||||||
|
|
||||||
|
for (let i = 0; i < context.images.length; ++i) { |
||||||
|
const entry = context.images[i]; |
||||||
|
|
||||||
|
result[i * 12 + 0] = entry.at.x; |
||||||
|
result[i * 12 + 1] = entry.at.y; |
||||||
|
|
||||||
|
result[i * 12 + 2] = entry.at.x + entry.width; |
||||||
|
result[i * 12 + 3] = entry.at.y; |
||||||
|
|
||||||
|
result[i * 12 + 4] = entry.at.x; |
||||||
|
result[i * 12 + 5] = entry.at.y + entry.height; |
||||||
|
|
||||||
|
result[i * 12 + 6] = entry.at.x + entry.width; |
||||||
|
result[i * 12 + 7] = entry.at.y + entry.height; |
||||||
|
|
||||||
|
result[i * 12 + 8] = entry.at.x; |
||||||
|
result[i * 12 + 9] = entry.at.y + entry.height; |
||||||
|
|
||||||
|
result[i * 12 + 10] = entry.at.x + entry.width; |
||||||
|
result[i * 12 + 11] = entry.at.y; |
||||||
|
} |
||||||
|
|
||||||
|
return result; |
||||||
|
} |
||||||
|
|
||||||
|
function geometry_generate_handles(state, context, active_image) { |
||||||
|
let image = null; |
||||||
|
|
||||||
|
for (const entry of context.images) { |
||||||
|
if (entry.key === active_image) { |
||||||
|
image = entry; |
||||||
|
break; |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
|
||||||
|
const x1 = image.at.x; |
||||||
|
const y1 = image.at.y; |
||||||
|
const x2 = image.at.x + image.width; |
||||||
|
const y2 = image.at.y + image.height; |
||||||
|
|
||||||
|
const width = 4 / state.canvas.zoom; |
||||||
|
|
||||||
|
const points = new Float32Array([ |
||||||
|
// top-left handle
|
||||||
|
x1 - width, y1 - width, |
||||||
|
x1 + width, y1 - width, |
||||||
|
x1 + width, y1 + width, |
||||||
|
x1 - width, y1 + width, |
||||||
|
x1 - width, y1 - width, |
||||||
|
|
||||||
|
// -> top-right
|
||||||
|
x1 + width, y1, |
||||||
|
x2 - width, y1, |
||||||
|
|
||||||
|
// top-right handle
|
||||||
|
x2 - width, y1 - width, |
||||||
|
x2 + width, y1 - width, |
||||||
|
x2 + width, y1 + width, |
||||||
|
x2 - width, y1 + width, |
||||||
|
x2 - width, y1 - width, |
||||||
|
|
||||||
|
// -> bottom-right
|
||||||
|
x2, y1 + width, |
||||||
|
x2, y2 - width, |
||||||
|
|
||||||
|
// bottom-right handle
|
||||||
|
x2 - width, y2 - width, |
||||||
|
x2 + width, y2 - width, |
||||||
|
x2 + width, y2 + width, |
||||||
|
x2 - width, y2 + width, |
||||||
|
x2 - width, y2 - width, |
||||||
|
|
||||||
|
// -> bottom-left
|
||||||
|
x2 - width, y2, |
||||||
|
x1 + width, y2, |
||||||
|
|
||||||
|
// bottom-left handle
|
||||||
|
x1 - width, y2 - width, |
||||||
|
x1 + width, y2 - width, |
||||||
|
x1 + width, y2 + width, |
||||||
|
x1 - width, y2 + width, |
||||||
|
x1 - width, y2 - width, |
||||||
|
|
||||||
|
// -> top-left
|
||||||
|
x1, y2 - width, |
||||||
|
x1, y1 + width, |
||||||
|
]); |
||||||
|
|
||||||
|
const ids = new Uint32Array([ |
||||||
|
0, 0, 0, 0, 0 | (1 << 31), |
||||||
|
1, 1 | (1 << 31), |
||||||
|
2, 2, 2, 2, 2 | (1 << 31), |
||||||
|
3, 3 | (1 << 31), |
||||||
|
4, 4, 4, 4, 4 | (1 << 31), |
||||||
|
5, 5 | (1 << 31), |
||||||
|
6, 6, 6, 6, 6 | (1 << 31), |
||||||
|
7, 7 | (1 << 31), |
||||||
|
]); |
||||||
|
|
||||||
|
const pressures = new Uint8Array([ |
||||||
|
128, 128, 128, 128, 128, |
||||||
|
128, 128, 128, |
||||||
|
128, 128, 128, 128, 128, |
||||||
|
128, 128, 128, |
||||||
|
128, 128, 128, 128, 128, |
||||||
|
128, 128, 128, |
||||||
|
128, 128, 128, 128, 128, |
||||||
|
128, 128, 128, |
||||||
|
]); |
||||||
|
|
||||||
|
const stroke_data = serializer_create(8 * 4 * 2); |
||||||
|
|
||||||
|
for (let i = 0; i < 8; ++i) { |
||||||
|
ser_u16(stroke_data, 34); |
||||||
|
ser_u16(stroke_data, 139); |
||||||
|
ser_u16(stroke_data, 230); |
||||||
|
ser_u16(stroke_data, 0); |
||||||
|
} |
||||||
|
|
||||||
|
return { |
||||||
|
'points': points, |
||||||
|
'ids': ids, |
||||||
|
'pressures': pressures, |
||||||
|
'stroke_data': stroke_data, |
||||||
|
}; |
||||||
|
} |
||||||
|
|
||||||
|
function geometry_line_segments_with_two_circles(circle_segments) { |
||||||
|
const results = new Float32Array((circle_segments * 3 + 6) * 2); // triangle fan circle + two triangles, all 2D (x + y)
|
||||||
|
|
||||||
|
// Generate circle as triangle fan at 0, 0 with radius 1
|
||||||
|
// This circle will be offset/scaled in the vertex shader
|
||||||
|
let last_phi = ((circle_segments - 1) / circle_segments) * 2 * Math.PI; |
||||||
|
for (let i = 0; i < circle_segments; ++i) { |
||||||
|
const phi = i / circle_segments * 2 * Math.PI; |
||||||
|
const x1 = Math.cos(phi); |
||||||
|
const y1 = Math.sin(phi); |
||||||
|
const x2 = Math.cos(last_phi); |
||||||
|
const y2 = Math.sin(last_phi); |
||||||
|
|
||||||
|
results[i * 6 + 0] = x1; |
||||||
|
results[i * 6 + 1] = y1; |
||||||
|
results[i * 6 + 2] = x2; |
||||||
|
results[i * 6 + 3] = y2; |
||||||
|
results[i * 6 + 4] = 0; |
||||||
|
results[i * 6 + 5] = 0; |
||||||
|
|
||||||
|
last_phi = phi; |
||||||
|
} |
||||||
|
|
||||||
|
return results; |
||||||
|
} |
@ -0,0 +1,839 @@ |
|||||||
|
function init_listeners(state, context) { |
||||||
|
window.addEventListener('keydown', (e) => keydown(e, state, context)); |
||||||
|
window.addEventListener('keyup', (e) => keyup(e, state, context)); |
||||||
|
window.addEventListener('paste', (e) => paste(e, state, context)); |
||||||
|
|
||||||
|
context.canvas.addEventListener('pointerdown', (e) => mousedown(e, state, context)); |
||||||
|
context.canvas.addEventListener('pointermove', (e) => mousemove(e, state, context)); |
||||||
|
context.canvas.addEventListener('pointerup', (e) => mouseup(e, state, context)); |
||||||
|
context.canvas.addEventListener('pointerleave', (e) => mouseup(e, state, context)); |
||||||
|
context.canvas.addEventListener('pointerleave', (e) => mouseleave(e, state, context)); |
||||||
|
context.canvas.addEventListener('contextmenu', cancel); |
||||||
|
context.canvas.addEventListener('wheel', (e) => wheel(e, state, context)); |
||||||
|
|
||||||
|
context.canvas.addEventListener('touchstart', (e) => touchstart(e, state, context)); |
||||||
|
context.canvas.addEventListener('touchmove', (e) => touchmove(e, state, context)); |
||||||
|
context.canvas.addEventListener('touchend', (e) => touchend(e, state, context)); |
||||||
|
context.canvas.addEventListener('touchcancel', (e) => touchend(e, state, context)); |
||||||
|
|
||||||
|
context.canvas.addEventListener('drop', (e) => on_drop(e, state, context)); |
||||||
|
context.canvas.addEventListener('dragover', (e) => mousemove(e, state, context)); |
||||||
|
|
||||||
|
debug_panel_init(state, context); |
||||||
|
} |
||||||
|
|
||||||
|
function debug_panel_init(state, context) { |
||||||
|
document.getElementById('debug-red').checked = state.debug.red; |
||||||
|
document.getElementById('do-snap').checked = state.snap !== null; |
||||||
|
document.getElementById('debug-print').checked = config.debug_print; |
||||||
|
document.getElementById('draw-bvh').checked = config.draw_bvh; |
||||||
|
|
||||||
|
document.getElementById('debug-red').addEventListener('change', (e) => { |
||||||
|
state.debug.red = e.target.checked; |
||||||
|
schedule_draw(state, context); |
||||||
|
}); |
||||||
|
|
||||||
|
document.getElementById('do-snap').addEventListener('change', (e) => { |
||||||
|
state.snap = e.target.checked ? 'grid' : null; |
||||||
|
}); |
||||||
|
|
||||||
|
document.getElementById('debug-print').addEventListener('change', (e) => { |
||||||
|
config.debug_print = e.target.checked; |
||||||
|
}); |
||||||
|
|
||||||
|
document.getElementById('draw-bvh').addEventListener('change', (e) => { |
||||||
|
config.draw_bvh = e.target.checked; |
||||||
|
schedule_draw(state, context); |
||||||
|
}); |
||||||
|
|
||||||
|
document.getElementById('debug-begin-benchmark').addEventListener('click', (e) => { |
||||||
|
state.canvas.zoom_level = config.benchmark.zoom_level; |
||||||
|
state.canvas.offset.x = config.benchmark.offset.x; |
||||||
|
state.canvas.offset.y = config.benchmark.offset.y; |
||||||
|
|
||||||
|
const dz = (state.canvas.zoom_level > 0 ? config.zoom_delta : -config.zoom_delta); |
||||||
|
state.canvas.target_zoom = Math.pow(1.0 + dz, Math.abs(state.canvas.zoom_level)) |
||||||
|
state.canvas.zoom = state.canvas.target_zoom; |
||||||
|
|
||||||
|
state.debug.benchmark_mode = true; |
||||||
|
|
||||||
|
const origin_x = state.canvas.offset.x; |
||||||
|
const origin_y = state.canvas.offset.y; |
||||||
|
const original_button_text = e.target.innerText; |
||||||
|
|
||||||
|
let frame = 0; |
||||||
|
|
||||||
|
state.debug.on_benchmark = () => { |
||||||
|
if (frame >= config.benchmark.frames) { |
||||||
|
state.debug.benchmark_mode = false; |
||||||
|
e.target.disabled = false; |
||||||
|
e.target.innerText = original_button_text; |
||||||
|
return false; |
||||||
|
} |
||||||
|
|
||||||
|
state.canvas.offset.x = origin_x + Math.round(100 * Math.cos(frame / 360)); |
||||||
|
state.canvas.offset.y = origin_y + Math.round(100 * Math.sin(frame / 360)); |
||||||
|
frame += 1; |
||||||
|
|
||||||
|
return true; |
||||||
|
} |
||||||
|
|
||||||
|
e.target.disabled = true; |
||||||
|
e.target.innerText = 'Benchmark in progress...'; |
||||||
|
|
||||||
|
schedule_draw(state, context); |
||||||
|
}); |
||||||
|
} |
||||||
|
|
||||||
|
function cancel(e) { |
||||||
|
e.preventDefault(); |
||||||
|
return false; |
||||||
|
} |
||||||
|
|
||||||
|
function zenmode() { |
||||||
|
document.querySelector('.pallete-wrapper').classList.toggle('hidden'); |
||||||
|
document.querySelector('.top-wrapper').classList.toggle('hidden'); |
||||||
|
} |
||||||
|
|
||||||
|
function enter_picker_mode(state, context) { |
||||||
|
if (state.tools.active === 'pencil') { // or other drawing tools
|
||||||
|
document.querySelector('canvas').classList.add('picker'); |
||||||
|
document.querySelector('.picker-preview-outer').classList.remove('dhide'); |
||||||
|
document.querySelector('.brush-dom').classList.add('dhide'); |
||||||
|
|
||||||
|
state.colorpicking = true; |
||||||
|
|
||||||
|
const canvasp = screen_to_canvas(state, state.cursor); |
||||||
|
update_color_picker_color(state, context, canvasp); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
function exit_picker_mode(state) { |
||||||
|
if (state.colorpicking) { |
||||||
|
document.querySelector('canvas').classList.remove('picker'); |
||||||
|
document.querySelector('.picker-preview-outer').classList.add('dhide'); |
||||||
|
document.querySelector('.brush-dom').classList.remove('dhide'); |
||||||
|
state.colorpicking = false; |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
async function paste(e, state, context) { |
||||||
|
const items = (e.clipboardData || e.originalEvent.clipboardData).items; |
||||||
|
for (const item of items) { |
||||||
|
if (item.kind === 'file') { |
||||||
|
const file = item.getAsFile(); |
||||||
|
await insert_image(state, context, file); |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
function keydown(e, state, context) { |
||||||
|
if (config.debug_print) { |
||||||
|
console.debug('keydown', e.code); |
||||||
|
} |
||||||
|
|
||||||
|
const doing_things = (state.moving || state.drawing || state.erasing || state.colorpicking || state.imagemoving || state.imagescaling || state.linedrawing); |
||||||
|
|
||||||
|
if (e.code === 'Space' && !state.drawing) { |
||||||
|
state.spacedown = true; |
||||||
|
context.canvas.classList.add('movemode'); |
||||||
|
} else if (e.code === 'Tab') { |
||||||
|
e.preventDefault(); |
||||||
|
zenmode(); |
||||||
|
} else if (e.code === 'ControlLeft' || e.paddingcode === 'ControlRight') { |
||||||
|
enter_picker_mode(state, context); |
||||||
|
} else if (e.code === 'Slash') { |
||||||
|
document.querySelector('.debug-window').classList.toggle('dhide'); |
||||||
|
e.preventDefault(); |
||||||
|
} else if (e.code === 'KeyZ') { |
||||||
|
if (e.ctrlKey) { |
||||||
|
queue_event(state, undo_event(state)); |
||||||
|
} else { |
||||||
|
state.zoomdown = true; |
||||||
|
} |
||||||
|
} else if (e.code === 'KeyS') { |
||||||
|
if (!doing_things) { |
||||||
|
switch_tool(state, document.querySelector('.tool[data-tool="pointer"]')); |
||||||
|
} |
||||||
|
} else if (e.code === 'KeyD') { |
||||||
|
if (!doing_things) { |
||||||
|
switch_tool(state, document.querySelector('.tool[data-tool="pencil"]')); |
||||||
|
} |
||||||
|
} else if (e.code === 'KeyE') { |
||||||
|
if (!doing_things) { |
||||||
|
switch_tool(state, document.querySelector('.tool[data-tool="eraser"]')); |
||||||
|
} |
||||||
|
} else if (e.code === 'KeyR') { |
||||||
|
if (!doing_things) { |
||||||
|
switch_tool(state, document.querySelector('.tool[data-tool="ruler"]')); |
||||||
|
} |
||||||
|
} else if (e.code === 'Esc') { |
||||||
|
cancel_everything(state, context); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
function keyup(e, state, context) { |
||||||
|
if (config.debug_print) { |
||||||
|
console.debug('keydown', e.code); |
||||||
|
} |
||||||
|
|
||||||
|
if (e.code === 'Space' && state.spacedown) { |
||||||
|
state.spacedown = false; |
||||||
|
state.moving = false; |
||||||
|
context.canvas.classList.remove('movemode'); |
||||||
|
} else if (e.code === 'ControlLeft' || e.code === 'ControlRight') { |
||||||
|
exit_picker_mode(state);exit_picker_mode |
||||||
|
} else if (e.code === 'KeyZ') { |
||||||
|
state.zoomdown = false; |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
function mousedown(e, state, context) { |
||||||
|
const screenp = {'x': window.devicePixelRatio * e.clientX, 'y': window.devicePixelRatio * e.clientY}; |
||||||
|
const canvasp = screen_to_canvas(state, screenp); |
||||||
|
const raw_canvasp = {...canvasp}; |
||||||
|
|
||||||
|
if (state.snap === 'grid') { |
||||||
|
const step = grid_snap_step(state); |
||||||
|
canvasp.x = Math.round(canvasp.x / step) * step; |
||||||
|
canvasp.y = Math.round(canvasp.y / step) * step; |
||||||
|
} |
||||||
|
|
||||||
|
if (e.button !== 0 && e.button !== 1) { |
||||||
|
return; |
||||||
|
} |
||||||
|
|
||||||
|
if (state.zoomdown) { |
||||||
|
state.zooming = true; |
||||||
|
state.canvas.zoom_screenp = screenp; |
||||||
|
return; |
||||||
|
} |
||||||
|
|
||||||
|
if (state.colorpicking) { |
||||||
|
const color_u32 = color_to_u32(state.color_picked.substring(1)); |
||||||
|
state.players[state.me].color = color_u32; |
||||||
|
update_cursor(state); |
||||||
|
fire_event(state, color_event(color_u32)); |
||||||
|
return; |
||||||
|
} |
||||||
|
|
||||||
|
if (state.spacedown || e.button === 1) { |
||||||
|
state.moving = true; |
||||||
|
context.canvas.classList.add('moving'); |
||||||
|
|
||||||
|
if (e.button === 1) { |
||||||
|
context.canvas.classList.add('mousemoving'); |
||||||
|
} |
||||||
|
|
||||||
|
return; |
||||||
|
} |
||||||
|
|
||||||
|
if (state.tools.active === 'pencil') { |
||||||
|
canvasp.pressure = Math.ceil(e.pressure * 255); |
||||||
|
geometry_start_prestroke(state, state.me); |
||||||
|
geometry_add_prepoint(state, context, state.me, canvasp, e.pointerType === "pen"); |
||||||
|
|
||||||
|
state.drawing = true; |
||||||
|
state.active_image = null; |
||||||
|
|
||||||
|
schedule_draw(state, context); |
||||||
|
} else if (state.tools.active === 'ruler') { |
||||||
|
state.linedrawing = true; |
||||||
|
state.ruler_origin = canvasp; |
||||||
|
geometry_start_prestroke(state, state.me); |
||||||
|
} else if (state.tools.active === 'eraser') { |
||||||
|
state.erasing = true; |
||||||
|
} else if (state.tools.active === 'pointer') { |
||||||
|
state.imagescaling = false; |
||||||
|
state.imagemoving = false; |
||||||
|
|
||||||
|
if (state.active_image !== null) { |
||||||
|
// Check for resize first, because it supports
|
||||||
|
// clicking slightly outside of the image
|
||||||
|
const image = get_image(context, state.active_image); |
||||||
|
const corner = image_corner(state, image, raw_canvasp); |
||||||
|
if (corner !== null) { |
||||||
|
// Resize
|
||||||
|
state.imagescaling = true; |
||||||
|
state.scaling_corner = corner; |
||||||
|
|
||||||
|
document.querySelector('canvas').classList.remove('resize-topleft'); |
||||||
|
document.querySelector('canvas').classList.remove('resize-topright'); |
||||||
|
|
||||||
|
if (corner === 0 || corner === 2) { |
||||||
|
document.querySelector('canvas').classList.add('resize-topleft'); |
||||||
|
} else if (corner === 1 || corner === 3) { |
||||||
|
document.querySelector('canvas').classList.add('resize-topright'); |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
// Only do picking logic if we haven't started imagescaling already
|
||||||
|
if (!state.imagescaling) { |
||||||
|
const image = image_at(context, raw_canvasp.x, raw_canvasp.y); |
||||||
|
if (image !== null) { |
||||||
|
state.active_image = image.key; |
||||||
|
// Allow immediately moving
|
||||||
|
state.imagemoving = true; |
||||||
|
state.image_actually_moved = false; |
||||||
|
image.raw_at.x = image.at.x; |
||||||
|
image.raw_at.y = image.at.y; |
||||||
|
} else { |
||||||
|
state.active_image = null; |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
schedule_draw(state, context); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
function update_color_picker_color(state, context, canvasp) { |
||||||
|
const stroke_index = bvh_point(state, canvasp); |
||||||
|
let color_under_cursor = color_from_rgbdict(context.bgcolor); |
||||||
|
|
||||||
|
if (stroke_index != null) { |
||||||
|
color_under_cursor = color_from_u32(state.events[stroke_index].color); |
||||||
|
} |
||||||
|
|
||||||
|
document.querySelector('.picker-preview-inner').style.background = color_under_cursor; |
||||||
|
|
||||||
|
state.color_picked = color_under_cursor; |
||||||
|
} |
||||||
|
|
||||||
|
function mousemove(e, state, context) { |
||||||
|
e.preventDefault(); |
||||||
|
|
||||||
|
let do_draw = false; |
||||||
|
|
||||||
|
const screenp = {'x': window.devicePixelRatio * e.clientX, 'y': window.devicePixelRatio * e.clientY}; |
||||||
|
const canvasp = screen_to_canvas(state, screenp); |
||||||
|
const raw_canvasp = {...canvasp}; |
||||||
|
|
||||||
|
if (state.snap === 'grid') { |
||||||
|
const step = grid_snap_step(state); |
||||||
|
canvasp.x = Math.round(canvasp.x / step) * step; |
||||||
|
canvasp.y = Math.round(canvasp.y / step) * step; |
||||||
|
} |
||||||
|
|
||||||
|
if (state.tools.active === 'pointer') { |
||||||
|
if (state.active_image !== null) { |
||||||
|
const image = get_image(context, state.active_image); |
||||||
|
const corner = image_corner(state, image, raw_canvasp); |
||||||
|
|
||||||
|
if (state.scaling_corner === null) { |
||||||
|
document.querySelector('canvas').classList.remove('resize-topleft'); |
||||||
|
document.querySelector('canvas').classList.remove('resize-topright'); |
||||||
|
|
||||||
|
if (corner === 0 || corner === 2) { |
||||||
|
document.querySelector('canvas').classList.add('resize-topleft'); |
||||||
|
} else if (corner === 1 || corner === 3) { |
||||||
|
document.querySelector('canvas').classList.add('resize-topright'); |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
if (state.me in state.players) { |
||||||
|
const me = state.players[state.me]; |
||||||
|
const width = Math.max(me.width * state.canvas.zoom, 2.0); |
||||||
|
const radius = Math.round(width / 2); |
||||||
|
const brush_screen = canvas_to_screen(state, canvasp); |
||||||
|
const brush_x = brush_screen.x - radius - 2; |
||||||
|
const brush_y = brush_screen.y - radius - 2; |
||||||
|
document.querySelector('.brush-dom').style.transform = `translate(${brush_x}px, ${brush_y}px)`; |
||||||
|
} |
||||||
|
|
||||||
|
if (state.me in state.players && dist_v2(state.players[state.me].cursor, canvasp) > 5) { |
||||||
|
state.players[state.me].cursor = canvasp; |
||||||
|
fire_event(state, movecursor_event(canvasp.x, canvasp.y)); |
||||||
|
} |
||||||
|
|
||||||
|
if (state.colorpicking) { |
||||||
|
update_color_picker_color(state, context, canvasp); |
||||||
|
} |
||||||
|
|
||||||
|
if (state.zooming) { |
||||||
|
const zooming_in = e.movementY > 0; |
||||||
|
const zooming_out = e.movementY < 0; |
||||||
|
|
||||||
|
let zoom_level = null; |
||||||
|
|
||||||
|
if (zooming_in) { |
||||||
|
zoom_level = state.canvas.zoom_level + 1 |
||||||
|
} else if (zooming_out) { |
||||||
|
zoom_level = state.canvas.zoom_level - 1; |
||||||
|
} else { |
||||||
|
return; |
||||||
|
} |
||||||
|
|
||||||
|
if (zoom_level < config.min_zoom_level || zoom_level > config.max_zoom_level) { |
||||||
|
return; |
||||||
|
} |
||||||
|
|
||||||
|
const dz = (zoom_level > 0 ? config.zoom_delta : -config.zoom_delta); |
||||||
|
state.canvas.zoom_level = zoom_level; |
||||||
|
state.canvas.target_zoom = Math.pow(1.0 + dz, Math.abs(zoom_level)) |
||||||
|
|
||||||
|
do_draw = true; |
||||||
|
} |
||||||
|
|
||||||
|
if (state.moving) { |
||||||
|
state.canvas.offset.x += e.movementX; |
||||||
|
state.canvas.offset.y += e.movementY; |
||||||
|
|
||||||
|
// If we are moving our canvas, we don't need to follow anymore
|
||||||
|
if (state.following_player !== null) { |
||||||
|
toggle_follow_player(state, state.following_player); |
||||||
|
} |
||||||
|
|
||||||
|
fire_event(state, movecanvas_event(state)); |
||||||
|
draw_html(state, context); |
||||||
|
do_draw = true; |
||||||
|
} |
||||||
|
|
||||||
|
if (state.imagescaling) { |
||||||
|
const image = get_image(context, state.active_image); |
||||||
|
scale_image(image, state.scaling_corner, canvasp); |
||||||
|
do_draw = true; |
||||||
|
} |
||||||
|
|
||||||
|
if (state.imagemoving) { |
||||||
|
const image = get_image(context, state.active_image); |
||||||
|
|
||||||
|
if (image !== null) { |
||||||
|
const dx = e.movementX / state.canvas.zoom; |
||||||
|
const dy = e.movementY / state.canvas.zoom; |
||||||
|
|
||||||
|
image.raw_at.x += dx; |
||||||
|
image.raw_at.y += dy; |
||||||
|
|
||||||
|
if (state.snap === 'grid') { |
||||||
|
const step = grid_snap_step(state); |
||||||
|
image.at.x = Math.round(image.raw_at.x / step) * step; |
||||||
|
image.at.y = Math.round(image.raw_at.y / step) * step; |
||||||
|
} else if (state.snap === null) { |
||||||
|
image.at.x = image.raw_at.x; |
||||||
|
image.at.y = image.raw_at.y; |
||||||
|
} |
||||||
|
|
||||||
|
state.image_actually_moved = true; |
||||||
|
do_draw = true; |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
if (state.drawing) { |
||||||
|
canvasp.pressure = Math.ceil(e.pressure * 255); |
||||||
|
geometry_add_prepoint(state, context, state.me, canvasp, e.pointerType === "pen"); |
||||||
|
fire_event(state, predraw_event(canvasp.x, canvasp.y)); |
||||||
|
do_draw = true; |
||||||
|
} |
||||||
|
|
||||||
|
if (state.erasing) { |
||||||
|
const me = state.players[state.me]; |
||||||
|
const radius = Math.round(me.width / 2); |
||||||
|
const last_canvasp = screen_to_canvas(state, state.cursor); |
||||||
|
|
||||||
|
const cursor_bbox = { |
||||||
|
'x1': Math.min(canvasp.x, last_canvasp.x) - radius, |
||||||
|
'y1': Math.min(canvasp.y, last_canvasp.y) - radius, |
||||||
|
'x2': Math.max(canvasp.x, last_canvasp.x) + radius, |
||||||
|
'y2': Math.max(canvasp.y, last_canvasp.y) + radius, |
||||||
|
}; |
||||||
|
|
||||||
|
tv_ensure(state.erase_candidates, round_to_pow2(state.stroke_count, 4096)); |
||||||
|
tv_clear(state.erase_candidates); |
||||||
|
|
||||||
|
// Rough pass, not all of these might actually need to be erased
|
||||||
|
bvh_intersect_quad(state, state.bvh, cursor_bbox, state.erase_candidates); |
||||||
|
|
||||||
|
// Fine pass, actually run expensive capsule vs capsule intersection tests
|
||||||
|
for (let i = 0; i < state.erase_candidates.size; ++i) { |
||||||
|
const stroke_id = state.erase_candidates.data[i]; |
||||||
|
const stroke = state.events[stroke_id]; |
||||||
|
|
||||||
|
if (!stroke.deleted && stroke_intersects_capsule(state, stroke, last_canvasp, canvasp, radius)) { |
||||||
|
stroke.deleted = true; |
||||||
|
bvh_delete_stroke(state, stroke); |
||||||
|
queue_event(state, eraser_event(stroke_id)); |
||||||
|
do_draw = true; |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
if (state.linedrawing) { |
||||||
|
const p1 = {'x': state.ruler_origin.x, 'y': state.ruler_origin.y, 'pressure': 128}; |
||||||
|
const p2 = {'x': canvasp.x, 'y': canvasp.y, 'pressure': 128}; |
||||||
|
|
||||||
|
if (state.online) { |
||||||
|
const me = state.players[state.me]; |
||||||
|
const prestroke = me.strokes[me.strokes.length - 1]; // TODO: might as well be me.strokes[0] ?
|
||||||
|
|
||||||
|
prestroke.points.length = 2; |
||||||
|
prestroke.points[0] = p1; |
||||||
|
prestroke.points[1] = p2; |
||||||
|
|
||||||
|
recompute_dynamic_data(state, context); |
||||||
|
|
||||||
|
do_draw = true; |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
if (do_draw) { |
||||||
|
schedule_draw(state, context); |
||||||
|
} |
||||||
|
|
||||||
|
state.cursor = screenp; |
||||||
|
|
||||||
|
return false; |
||||||
|
} |
||||||
|
|
||||||
|
function mouseup(e, state, context) { |
||||||
|
const screenp = {'x': window.devicePixelRatio * e.clientX, 'y': window.devicePixelRatio * e.clientY}; |
||||||
|
const canvasp = screen_to_canvas(state, screenp); |
||||||
|
const raw_canvasp = {...canvasp}; |
||||||
|
|
||||||
|
if (state.snap === 'grid') { |
||||||
|
const step = grid_snap_step(state); |
||||||
|
canvasp.x = Math.round(canvasp.x / step) * step; |
||||||
|
canvasp.y = Math.round(canvasp.y / step) * step; |
||||||
|
} |
||||||
|
|
||||||
|
if (e.button !== 0 && e.button !== 1) { |
||||||
|
return; |
||||||
|
} |
||||||
|
|
||||||
|
if (state.zooming) { |
||||||
|
state.zooming = false; |
||||||
|
return; |
||||||
|
} |
||||||
|
|
||||||
|
if (state.imagemoving) { |
||||||
|
state.imagemoving = false; |
||||||
|
if (state.image_actually_moved) { |
||||||
|
state.image_actually_moved = false; |
||||||
|
const image = get_image(context, state.active_image); |
||||||
|
image.raw_at.x = image.at.x; |
||||||
|
image.raw_at.y = image.at.y; |
||||||
|
queue_event(state, image_move_event(state.active_image, image.at.x, image.at.y)); |
||||||
|
schedule_draw(state, context); |
||||||
|
} |
||||||
|
return; |
||||||
|
} |
||||||
|
|
||||||
|
if (state.imagescaling) { |
||||||
|
queue_event(state, image_scale_event(state.active_image, state.scaling_corner, canvasp.x, canvasp.y)); |
||||||
|
state.imagescaling = false; |
||||||
|
state.scaling_corner = null; |
||||||
|
return; |
||||||
|
} |
||||||
|
|
||||||
|
if (state.moving || e.button === 1) { |
||||||
|
state.moving = false; |
||||||
|
context.canvas.classList.remove('moving'); |
||||||
|
|
||||||
|
if (e.button === 1) { |
||||||
|
context.canvas.classList.remove('mousemoving'); |
||||||
|
} |
||||||
|
|
||||||
|
return; |
||||||
|
} |
||||||
|
|
||||||
|
if (state.drawing) { |
||||||
|
const stroke = geometry_prepare_stroke(state); |
||||||
|
|
||||||
|
if (stroke) { |
||||||
|
// TODO: be able to add a baked stroke locally
|
||||||
|
queue_event(state, stroke_event(state)); |
||||||
|
schedule_draw(state, context); |
||||||
|
} |
||||||
|
|
||||||
|
fire_event(state, lift_event()); |
||||||
|
state.drawing = false; |
||||||
|
|
||||||
|
return; |
||||||
|
} |
||||||
|
|
||||||
|
if (state.erasing) { |
||||||
|
state.erasing = false; |
||||||
|
return; |
||||||
|
} |
||||||
|
|
||||||
|
if (state.linedrawing) { |
||||||
|
state.linedrawing = false; |
||||||
|
queue_event(state, stroke_event(state)); |
||||||
|
schedule_draw(state, context); |
||||||
|
return; |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
function mouseleave(e, state, context) { |
||||||
|
if (state.moving) { |
||||||
|
state.moving = false; |
||||||
|
context.canvas.classList.remove('movemode'); |
||||||
|
} |
||||||
|
|
||||||
|
//exit_picker_mode(state);
|
||||||
|
// something else?
|
||||||
|
} |
||||||
|
|
||||||
|
function update_cursor(state) { |
||||||
|
if (!(state.me in state.players)) { |
||||||
|
// we not ready yet
|
||||||
|
return; |
||||||
|
} |
||||||
|
|
||||||
|
const me = state.players[state.me]; |
||||||
|
|
||||||
|
const width = Math.max(me.width * state.canvas.zoom, 2.0); |
||||||
|
const radius = Math.round(width / 2); |
||||||
|
|
||||||
|
let svg; |
||||||
|
|
||||||
|
if (state.tools.active === 'pencil' || state.tools.active === 'ruler') { |
||||||
|
const current_color = color_from_u32(me.color); |
||||||
|
const stroke = (me.color === 0xFFFFFF ? 'black' : 'white'); |
||||||
|
|
||||||
|
svg = `<svg style="display: block" xmlns="http://www.w3.org/2000/svg" width="${width + 4}" height="${width + 4}">
|
||||||
|
<circle cx="${radius + 2}" cy="${radius + 2}" r="${radius}" stroke="${stroke}" fill="none" stroke-width="3"/> |
||||||
|
<circle cx="${radius + 2}" cy="${radius + 2}" r="${radius}" stroke="none" fill="${current_color}" stroke-width="1"/> |
||||||
|
</svg>`.replaceAll('\n', ' '); |
||||||
|
} else if (state.tools.active === 'eraser') { |
||||||
|
const current_color = '#ffffff'; |
||||||
|
const stroke = '#000000'; |
||||||
|
svg = `<svg style="display: block" xmlns="http://www.w3.org/2000/svg" width="${width + 4}" height="${width + 4}">
|
||||||
|
<circle cx="${radius + 2}" cy="${radius + 2}" r="${radius}" stroke="${stroke}" fill="none" stroke-width="3"/> |
||||||
|
<circle cx="${radius + 2}" cy="${radius + 2}" r="${radius}" stroke="none" fill="${current_color}" stroke-width="1"/> |
||||||
|
</svg>`.replaceAll('\n', ' '); |
||||||
|
} |
||||||
|
|
||||||
|
document.querySelector('.brush-dom').innerHTML = svg; |
||||||
|
|
||||||
|
const brush_x = state.cursor.x - width / 2 - 2; |
||||||
|
const brush_y = state.cursor.y - width / 2 - 2; |
||||||
|
document.querySelector('.brush-dom').style.transform = `translate(${Math.round(brush_x)}px, ${Math.round(brush_y)}px)`; |
||||||
|
} |
||||||
|
|
||||||
|
function wheel(e, state, context) { |
||||||
|
const screenp = {'x': window.devicePixelRatio * e.clientX, 'y': window.devicePixelRatio * e.clientY}; |
||||||
|
const canvasp = screen_to_canvas(state, screenp); |
||||||
|
const zooming_in = e.deltaY < 0; |
||||||
|
const zoom_level = zooming_in ? state.canvas.zoom_level + 2 : state.canvas.zoom_level - 2; |
||||||
|
|
||||||
|
if (zoom_level < config.min_zoom_level || zoom_level > config.max_zoom_level) { |
||||||
|
return; |
||||||
|
} |
||||||
|
|
||||||
|
const dz = (zoom_level > 0 ? config.zoom_delta : -config.zoom_delta); |
||||||
|
state.canvas.zoom_level = zoom_level; |
||||||
|
state.canvas.target_zoom = Math.pow(1.0 + dz, Math.abs(zoom_level)) |
||||||
|
state.canvas.zoom_screenp = screenp; |
||||||
|
|
||||||
|
// If we are moving our canvas, we don't need to follow anymore
|
||||||
|
if (state.following_player !== null) { |
||||||
|
toggle_follow_player(state, state.following_player); |
||||||
|
} |
||||||
|
|
||||||
|
fire_event(state, zoomcanvas_event(state, canvasp.x, canvasp.y)); |
||||||
|
schedule_draw(state, context); |
||||||
|
} |
||||||
|
|
||||||
|
function start_move(e, state, context) { |
||||||
|
// two touch identifiers are expected to be pushed into state.touch.ids at this point
|
||||||
|
|
||||||
|
// TODO: @touch, remove preview
|
||||||
|
fire_event(state, clear_event(state)); // Tell others to hide predraws of this stroke
|
||||||
|
|
||||||
|
for (const touch of e.touches) { |
||||||
|
const screenp = {'x': window.devicePixelRatio * touch.clientX, 'y': window.devicePixelRatio * touch.clientY}; |
||||||
|
|
||||||
|
if (touch.identifier === state.touch.ids[0]) { |
||||||
|
state.touch.first_finger_position = screenp; |
||||||
|
} else if (touch.identifier === state.touch.ids[1]) { |
||||||
|
state.touch.second_finger_position = screenp; |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
function touchstart(e, state, context) { |
||||||
|
e.preventDefault(); |
||||||
|
|
||||||
|
// First finger(s) down?
|
||||||
|
if (state.touch.ids.length === 0) { |
||||||
|
if (e.changedTouches.length === 1) { |
||||||
|
|
||||||
|
// We give a bit of time to add a second finger
|
||||||
|
state.touch.waiting_for_second_finger = true; |
||||||
|
state.touch.moves = 0; |
||||||
|
state.touch.buffered.length = 0; |
||||||
|
state.touch.ids.push(e.changedTouches[0].identifier); |
||||||
|
state.touch.drawing = true; |
||||||
|
|
||||||
|
setTimeout(() => { |
||||||
|
state.touch.waiting_for_second_finger = false; |
||||||
|
}, config.second_finger_timeout); |
||||||
|
} else { |
||||||
|
state.touch.ids.push(e.changedTouches[0].identifier); |
||||||
|
state.touch.ids.push(e.changedTouches[1].identifier); |
||||||
|
start_move(e, state, context); |
||||||
|
} |
||||||
|
|
||||||
|
return; |
||||||
|
} |
||||||
|
|
||||||
|
// There are touches already
|
||||||
|
if (state.touch.waiting_for_second_finger) { |
||||||
|
if (e.changedTouches.length === 1) { |
||||||
|
state.touch.ids.push(e.changedTouches[0].identifier); |
||||||
|
start_move(e, state, context); |
||||||
|
} |
||||||
|
|
||||||
|
return; |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
function touchmove(e, state, context) { |
||||||
|
if (state.touch.ids.length === 1) { |
||||||
|
const touch = find_touch(e.changedTouches, state.touch.ids[0]); |
||||||
|
|
||||||
|
if (!touch) { |
||||||
|
return; |
||||||
|
} |
||||||
|
|
||||||
|
const screenp = {'x': window.devicePixelRatio * touch.clientX, 'y': window.devicePixelRatio * touch.clientY}; |
||||||
|
const canvasp = screen_to_canvas(state, screenp); |
||||||
|
|
||||||
|
if (state.touch.moving) { |
||||||
|
// Can happen if we have been panning the canvas and lifted one finger,
|
||||||
|
// but not the second one
|
||||||
|
return; |
||||||
|
} |
||||||
|
|
||||||
|
state.touch.moves += 1; |
||||||
|
|
||||||
|
if (state.touch.moves > config.buffer_first_touchmoves) { |
||||||
|
// At this point touch with second finger will NOT start a pan
|
||||||
|
state.touch.waiting_for_second_finger = false; |
||||||
|
} |
||||||
|
|
||||||
|
canvasp.pressure = 128; // TODO: check out touch devices' e.pressure
|
||||||
|
// TODO: fix when doing @touch
|
||||||
|
//geometry_add_point(state, context, state.me, canvasp);
|
||||||
|
fire_event(state, predraw_event(canvasp.x, canvasp.y)); |
||||||
|
|
||||||
|
schedule_draw(state, context); |
||||||
|
|
||||||
|
return; |
||||||
|
} |
||||||
|
|
||||||
|
if (state.touch.ids.length === 2) { |
||||||
|
state.touch.moving = true; |
||||||
|
|
||||||
|
let first_finger_position = null; |
||||||
|
let second_finger_position = null; |
||||||
|
|
||||||
|
// A separate loop because touches might be in different order ? (question mark)
|
||||||
|
// IMPORTANT: e.touches, not e.changedTouches!
|
||||||
|
for (const touch of e.touches) { |
||||||
|
const screenp = {'x': window.devicePixelRatio * touch.clientX, 'y': window.devicePixelRatio * touch.clientY}; |
||||||
|
|
||||||
|
if (touch.identifier === state.touch.ids[0]) { |
||||||
|
first_finger_position = screenp; |
||||||
|
} else if (touch.identifier === state.touch.ids[1]) { |
||||||
|
second_finger_position = screenp; |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
const old_finger_midpoint = mid_v2(state.touch.first_finger_position, state.touch.second_finger_position); |
||||||
|
const new_finger_midpoint = mid_v2(first_finger_position, second_finger_position); |
||||||
|
|
||||||
|
const new_finger_midpoint_canvas = mid_v2( |
||||||
|
screen_to_canvas(state, first_finger_position), |
||||||
|
screen_to_canvas(state, second_finger_position) |
||||||
|
); |
||||||
|
|
||||||
|
const old_finger_distance = dist_v2(state.touch.first_finger_position, state.touch.second_finger_position); |
||||||
|
const new_finger_distance = dist_v2(first_finger_position, second_finger_position); |
||||||
|
|
||||||
|
const dx = new_finger_midpoint.x - old_finger_midpoint.x; |
||||||
|
const dy = new_finger_midpoint.y - old_finger_midpoint.y; |
||||||
|
|
||||||
|
const old_zoom = state.canvas.zoom; |
||||||
|
|
||||||
|
state.canvas.offset.x += dx; |
||||||
|
state.canvas.offset.y += dy; |
||||||
|
|
||||||
|
// console.log(new_finger_distance, state.touch.finger_distance);
|
||||||
|
|
||||||
|
const scale_by = new_finger_distance / old_finger_distance; |
||||||
|
const dz = state.canvas.zoom * (scale_by - 1.0); |
||||||
|
|
||||||
|
const zoom_offset_x = dz * new_finger_midpoint_canvas.x; |
||||||
|
const zoom_offset_y = dz * new_finger_midpoint_canvas.y; |
||||||
|
|
||||||
|
if (config.min_zoom <= state.canvas.zoom * scale_by && state.canvas.zoom * scale_by <= config.max_zoom) { |
||||||
|
state.canvas.zoom *= scale_by; |
||||||
|
state.canvas.offset.x -= zoom_offset_x; |
||||||
|
state.canvas.offset.y -= zoom_offset_y; |
||||||
|
} |
||||||
|
|
||||||
|
// If we are moving our canvas, we don't need to follow anymore
|
||||||
|
if (state.following_player !== null) { |
||||||
|
toggle_follow_player(state, state.following_player); |
||||||
|
} |
||||||
|
|
||||||
|
state.touch.first_finger_position = first_finger_position; |
||||||
|
state.touch.second_finger_position = second_finger_position; |
||||||
|
|
||||||
|
fire_event(state, movecanvas_event(state)); |
||||||
|
draw_html(state, context); |
||||||
|
schedule_draw(state, context); |
||||||
|
|
||||||
|
return; |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
function touchend(e, state, context) { |
||||||
|
for (const touch of e.changedTouches) { |
||||||
|
if (state.touch.drawing) { |
||||||
|
if (state.touch.ids[0] == touch.identifier) { |
||||||
|
const stroke = geometry_prepare_stroke(state); |
||||||
|
|
||||||
|
if (stroke) { |
||||||
|
queue_event(state, stroke_event(state)); |
||||||
|
schedule_draw(state, context); |
||||||
|
} |
||||||
|
|
||||||
|
state.touch.drawing = false; |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
const index = state.touch.ids.indexOf(touch.identifier); |
||||||
|
|
||||||
|
if (index !== -1) { |
||||||
|
state.touch.ids.splice(index, 1); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
if (state.touch.ids.length === 0) { |
||||||
|
// Only allow drawing again when ALL fingers have been lifted
|
||||||
|
state.touch.moving = false; |
||||||
|
waiting_for_second_finger = false; |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
async function on_drop(e, state, context) { |
||||||
|
e.preventDefault(); |
||||||
|
|
||||||
|
if (e.dataTransfer.files.length !== 1) { |
||||||
|
return; |
||||||
|
} |
||||||
|
|
||||||
|
const file = e.dataTransfer.files[0]; |
||||||
|
await insert_image(state, context, file); |
||||||
|
|
||||||
|
return false; |
||||||
|
} |
||||||
|
|
||||||
|
function cancel_everything(state, context) { |
||||||
|
|
||||||
|
} |
@ -0,0 +1,463 @@ |
|||||||
|
const sdf_vs_src = `#version 300 es
|
||||||
|
in vec2 a_pos; // for the joins/caps these are relative positions. for line segments these are dummy values
|
||||||
|
in vec2 a_a; // point from
|
||||||
|
in vec2 a_b; // point to
|
||||||
|
|
||||||
|
in int a_stroke_id; |
||||||
|
in vec2 a_pressure; |
||||||
|
|
||||||
|
uniform vec2 u_scale; |
||||||
|
uniform vec2 u_res; |
||||||
|
uniform vec2 u_translation; |
||||||
|
uniform int u_stroke_count; |
||||||
|
uniform int u_stroke_texture_size; |
||||||
|
uniform highp usampler2D u_stroke_data; |
||||||
|
uniform float u_fixed_pixel_width; |
||||||
|
uniform int u_circle_points; |
||||||
|
|
||||||
|
out vec3 v_color; |
||||||
|
|
||||||
|
flat out vec2 v_thickness; |
||||||
|
|
||||||
|
void main() { |
||||||
|
vec2 screen02; |
||||||
|
|
||||||
|
int per_segment_points = u_circle_points * 2 + 6; |
||||||
|
int vertex_index = gl_VertexID % per_segment_points; |
||||||
|
|
||||||
|
int stroke_data_y = a_stroke_id / u_stroke_texture_size; |
||||||
|
int stroke_data_x = a_stroke_id % u_stroke_texture_size; |
||||||
|
uvec4 stroke_data = texelFetch(u_stroke_data, ivec2(stroke_data_x, stroke_data_y), 0); |
||||||
|
float radius = float(stroke_data.w); |
||||||
|
|
||||||
|
if (u_fixed_pixel_width > 0.0) { |
||||||
|
radius = u_fixed_pixel_width / u_scale.x; |
||||||
|
} |
||||||
|
|
||||||
|
vec2 pos; |
||||||
|
|
||||||
|
if (vertex_index < u_circle_points) { |
||||||
|
pos = a_a + a_pos * radius * a_pressure.x * 2.0; |
||||||
|
} 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 * 2.0; |
||||||
|
} else if (vertex_index_line == 1 || vertex_index_line == 5) { |
||||||
|
// top right
|
||||||
|
pos = a_b + up_dir * radius * a_pressure.y * 2.0; |
||||||
|
} else if (vertex_index_line == 2 || vertex_index_line == 4) { |
||||||
|
// bottom left
|
||||||
|
pos = a_a - up_dir * radius * a_pressure.x * 2.0; |
||||||
|
} else { |
||||||
|
// bottom right
|
||||||
|
pos = a_b - up_dir * radius * a_pressure.y * 2.0; |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
screen02 = (pos.xy * u_scale + u_translation) / u_res * 2.0; |
||||||
|
screen02.y = 2.0 - screen02.y; |
||||||
|
|
||||||
|
v_color = vec3(stroke_data.xyz) / 255.0; |
||||||
|
|
||||||
|
/* |
||||||
|
if (a_stroke_id >> 31 != 0 && (vertex_index >= u_circle_points)) { |
||||||
|
screen02 += vec2(100.0); // shift offscreen
|
||||||
|
} |
||||||
|
*/ |
||||||
|
gl_Position = vec4(screen02 - 1.0, (float(a_stroke_id) / float(u_stroke_count)) * 2.0 - 1.0, 1.0); |
||||||
|
} |
||||||
|
`;
|
||||||
|
|
||||||
|
const sdf_fs_src = `#version 300 es
|
||||||
|
precision highp float; |
||||||
|
|
||||||
|
uniform int u_debug_mode; |
||||||
|
|
||||||
|
in vec3 v_color; |
||||||
|
|
||||||
|
layout(location = 0) out vec4 FragColor; |
||||||
|
|
||||||
|
void main() { |
||||||
|
if (u_debug_mode == 0) { |
||||||
|
float alpha = 0.5; |
||||||
|
FragColor = vec4(v_color * alpha, alpha); |
||||||
|
} else { |
||||||
|
FragColor = vec4(0.2, 0.0, 0.0, 0.2); |
||||||
|
} |
||||||
|
} |
||||||
|
`;
|
||||||
|
|
||||||
|
const tquad_vs_src = `#version 300 es
|
||||||
|
in vec2 a_pos; |
||||||
|
|
||||||
|
uniform vec2 u_scale; |
||||||
|
uniform vec2 u_res; |
||||||
|
uniform vec2 u_translation; |
||||||
|
|
||||||
|
out vec2 v_texcoord; |
||||||
|
|
||||||
|
void main() { |
||||||
|
vec2 screen01 = (a_pos * u_scale + u_translation) / u_res; |
||||||
|
vec2 screen02 = screen01 * 2.0; |
||||||
|
|
||||||
|
int vertex_index = gl_VertexID % 6; |
||||||
|
|
||||||
|
if (vertex_index == 0) { |
||||||
|
v_texcoord = vec2(0.0, 0.0); |
||||||
|
} else if (vertex_index == 1 || vertex_index == 5) { |
||||||
|
v_texcoord = vec2(1.0, 0.0); |
||||||
|
} else if (vertex_index == 2 || vertex_index == 4) { |
||||||
|
v_texcoord = vec2(0.0, 1.0); |
||||||
|
} else { |
||||||
|
v_texcoord = vec2(1.0, 1.0); |
||||||
|
} |
||||||
|
|
||||||
|
screen02.y = 2.0 - screen02.y; |
||||||
|
vec2 screen11 = screen02 - 1.0; |
||||||
|
|
||||||
|
gl_Position = vec4(screen11, 0, 1); |
||||||
|
} |
||||||
|
`;
|
||||||
|
|
||||||
|
const tquad_fs_src = `#version 300 es
|
||||||
|
precision highp float; |
||||||
|
|
||||||
|
in vec2 v_texcoord; |
||||||
|
|
||||||
|
uniform sampler2D u_texture; |
||||||
|
uniform int u_solid; |
||||||
|
uniform vec4 u_color; |
||||||
|
|
||||||
|
layout(location = 0) out vec4 FragColor; |
||||||
|
|
||||||
|
void main() { |
||||||
|
if (u_solid == 0) { |
||||||
|
FragColor = texture(u_texture, v_texcoord); |
||||||
|
} else { |
||||||
|
FragColor = u_color; |
||||||
|
} |
||||||
|
} |
||||||
|
`;
|
||||||
|
|
||||||
|
const grid_vs_src = `#version 300 es
|
||||||
|
in vec2 a_data; // per-instance
|
||||||
|
|
||||||
|
out float v_fadeout; |
||||||
|
|
||||||
|
uniform vec2 u_scale; |
||||||
|
uniform vec2 u_res; |
||||||
|
uniform vec2 u_translation; |
||||||
|
uniform float u_fadeout; |
||||||
|
|
||||||
|
void main() { |
||||||
|
vec2 origin; |
||||||
|
vec2 minor_offset; |
||||||
|
vec2 major_offset; |
||||||
|
|
||||||
|
vec2 pixel = 2.0 / u_res; |
||||||
|
|
||||||
|
if (a_data.x > 0.0) { |
||||||
|
// Vertical, treat Y as X
|
||||||
|
float x = a_data.y; |
||||||
|
origin = vec2(x, 0.0); |
||||||
|
minor_offset = pixel * vec2(1.0, 0.0); |
||||||
|
major_offset = vec2(0.0, 2.0); |
||||||
|
} else { |
||||||
|
// Horizontal, treat Y as Y
|
||||||
|
float y = a_data.y; |
||||||
|
origin = vec2(0.0, y); |
||||||
|
minor_offset = pixel * vec2(0.0, 1.0); |
||||||
|
major_offset = vec2(2.0, 0.0); |
||||||
|
} |
||||||
|
|
||||||
|
vec2 v = (origin * u_scale + u_translation) / u_res * 2.0; |
||||||
|
vec2 pos; |
||||||
|
|
||||||
|
if (a_data.x > 0.0) { |
||||||
|
v.y = 0.0; |
||||||
|
} else { |
||||||
|
v.x = 0.0; |
||||||
|
} |
||||||
|
|
||||||
|
if (gl_VertexID % 6 == 0) { |
||||||
|
pos = v; |
||||||
|
} else if (gl_VertexID % 6 == 1 || gl_VertexID % 6 == 5) { |
||||||
|
pos = v + (a_data.x > 0.0 ? minor_offset : major_offset); |
||||||
|
//pos = v + minor_offset;
|
||||||
|
} else if (gl_VertexID % 6 == 2 || gl_VertexID % 6 == 4) { |
||||||
|
pos = v + (a_data.x > 0.0 ? major_offset : minor_offset); |
||||||
|
//pos = v + major_offset;
|
||||||
|
} else if (gl_VertexID % 6 == 3) { |
||||||
|
pos = v + major_offset + minor_offset; |
||||||
|
//pos = v + major_offset + minor_offset;
|
||||||
|
} |
||||||
|
|
||||||
|
vec2 screen02 = pos; |
||||||
|
screen02.y = 2.0 - screen02.y; |
||||||
|
v_fadeout = u_fadeout; |
||||||
|
gl_Position = vec4(screen02 - 1.0, 0.0, 1.0); |
||||||
|
} |
||||||
|
`;
|
||||||
|
|
||||||
|
const dots_vs_src = `#version 300 es
|
||||||
|
in vec2 a_center; // per-instance
|
||||||
|
|
||||||
|
out float v_fadeout; |
||||||
|
|
||||||
|
uniform vec2 u_scale; |
||||||
|
uniform vec2 u_res; |
||||||
|
uniform vec2 u_translation; |
||||||
|
uniform float u_fadeout; |
||||||
|
|
||||||
|
void main() { |
||||||
|
vec2 v = (a_center * u_scale + u_translation) / u_res * 2.0; |
||||||
|
vec2 pos; |
||||||
|
vec2 pixel = 2.0 / u_res; |
||||||
|
|
||||||
|
if (gl_VertexID % 6 == 0) { |
||||||
|
pos = v + pixel * vec2(-1.0); |
||||||
|
} else if (gl_VertexID % 6 == 1) { |
||||||
|
pos = v + pixel * vec2(1.0, -1.0); |
||||||
|
} else if (gl_VertexID % 6 == 2) { |
||||||
|
pos = v + pixel * vec2(-1.0, 1.0); |
||||||
|
} else if (gl_VertexID % 6 == 3) { |
||||||
|
pos = v + pixel * vec2(1.0); |
||||||
|
} else if (gl_VertexID % 6 == 4) { |
||||||
|
pos = v + pixel * vec2(-1.0, 1.0); |
||||||
|
} else if (gl_VertexID % 6 == 5) { |
||||||
|
pos = v + pixel * vec2(1.0, -1.0); |
||||||
|
} |
||||||
|
|
||||||
|
vec2 screen02 = pos; |
||||||
|
screen02.y = 2.0 - screen02.y; |
||||||
|
v_fadeout = u_fadeout; |
||||||
|
gl_Position = vec4(screen02 - 1.0, 0.0, 1.0); |
||||||
|
} |
||||||
|
`;
|
||||||
|
|
||||||
|
const dots_fs_src = `#version 300 es
|
||||||
|
precision highp float; |
||||||
|
|
||||||
|
in float v_fadeout; |
||||||
|
|
||||||
|
layout(location = 0) out vec4 FragColor; |
||||||
|
|
||||||
|
void main() { |
||||||
|
vec3 color = vec3(0.5); |
||||||
|
FragColor = vec4(color * v_fadeout, v_fadeout); |
||||||
|
} |
||||||
|
`;
|
||||||
|
|
||||||
|
|
||||||
|
//
|
||||||
|
const iquad_vs_src = `#version 300 es
|
||||||
|
in vec2 a_topleft; // per-instance
|
||||||
|
in vec2 a_bottomright; // per-instance
|
||||||
|
|
||||||
|
uniform vec2 u_scale; |
||||||
|
uniform vec2 u_res; |
||||||
|
uniform vec2 u_translation; |
||||||
|
|
||||||
|
out vec3 v_color; |
||||||
|
|
||||||
|
void main() { |
||||||
|
vec2 pos; |
||||||
|
|
||||||
|
int vertex_index = gl_VertexID % 6; |
||||||
|
|
||||||
|
if (vertex_index == 0) { |
||||||
|
// top left
|
||||||
|
pos = a_topleft; |
||||||
|
} else if (vertex_index == 1 || vertex_index == 5) { |
||||||
|
// top right
|
||||||
|
pos = vec2(a_bottomright.x, a_topleft.y); |
||||||
|
} else if (vertex_index == 2 || vertex_index == 4) { |
||||||
|
// bottom left
|
||||||
|
pos = vec2(a_topleft.x, a_bottomright.y); |
||||||
|
} else { |
||||||
|
// bottom right
|
||||||
|
pos = a_bottomright; |
||||||
|
} |
||||||
|
|
||||||
|
v_color = vec3( |
||||||
|
float(int(a_topleft.x) * 908125 % 255) / 255.0, |
||||||
|
float(int(a_topleft.y) * 257722 % 255) / 255.0, |
||||||
|
float(int(a_bottomright.y) * 826586 % 255) / 255.0 |
||||||
|
); |
||||||
|
|
||||||
|
vec2 screen01 = (pos * u_scale + u_translation) / u_res; |
||||||
|
vec2 screen02 = screen01 * 2.0; |
||||||
|
screen02.y = 2.0 - screen02.y; |
||||||
|
vec2 screen11 = screen02 - 1.0; |
||||||
|
gl_Position = vec4(screen11, 0.0, 1.0); |
||||||
|
} |
||||||
|
`;
|
||||||
|
|
||||||
|
const iquad_fs_src = `#version 300 es
|
||||||
|
precision highp float; |
||||||
|
|
||||||
|
layout(location = 0) out vec4 FragColor; |
||||||
|
|
||||||
|
in vec3 v_color; |
||||||
|
|
||||||
|
void main() { |
||||||
|
FragColor = vec4(v_color, 0.5); |
||||||
|
} |
||||||
|
`;
|
||||||
|
|
||||||
|
function init_webgl(state, context) { |
||||||
|
context.canvas = document.querySelector('#c'); |
||||||
|
context.gl = context.canvas.getContext('webgl2', { |
||||||
|
'preserveDrawingBuffer': true, |
||||||
|
'desynchronized': true, |
||||||
|
'antialias': true, |
||||||
|
}); |
||||||
|
|
||||||
|
const gl = context.gl; |
||||||
|
|
||||||
|
gl.enable(gl.BLEND); |
||||||
|
gl.blendFunc(gl.ONE, gl.ONE_MINUS_SRC_ALPHA); |
||||||
|
|
||||||
|
/* |
||||||
|
gl.enable(gl.DEPTH_TEST); |
||||||
|
gl.depthFunc(gl.NOTEQUAL); |
||||||
|
*/ |
||||||
|
|
||||||
|
context.gpu_timer_ext = gl.getExtension('EXT_disjoint_timer_query_webgl2'); |
||||||
|
if (context.gpu_timer_ext === null) { |
||||||
|
context.gpu_timer_ext = gl.getExtension('EXT_disjoint_timer_query'); |
||||||
|
} |
||||||
|
|
||||||
|
const quad_vs = create_shader(gl, gl.VERTEX_SHADER, tquad_vs_src); |
||||||
|
const quad_fs = create_shader(gl, gl.FRAGMENT_SHADER, tquad_fs_src); |
||||||
|
|
||||||
|
const sdf_vs = create_shader(gl, gl.VERTEX_SHADER, sdf_vs_src); |
||||||
|
const sdf_fs = create_shader(gl, gl.FRAGMENT_SHADER, sdf_fs_src); |
||||||
|
|
||||||
|
const dots_vs = create_shader(gl, gl.VERTEX_SHADER, dots_vs_src); |
||||||
|
const dots_fs = create_shader(gl, gl.FRAGMENT_SHADER, dots_fs_src); |
||||||
|
|
||||||
|
const grid_vs = create_shader(gl, gl.VERTEX_SHADER, grid_vs_src); |
||||||
|
|
||||||
|
const iquad_vs = create_shader(gl, gl.VERTEX_SHADER, iquad_vs_src); |
||||||
|
const iquad_fs = create_shader(gl, gl.FRAGMENT_SHADER, iquad_fs_src); |
||||||
|
|
||||||
|
context.programs = { |
||||||
|
'image': create_program(gl, quad_vs, quad_fs), |
||||||
|
'main': create_program(gl, sdf_vs, sdf_fs), |
||||||
|
'dots': create_program(gl, dots_vs, dots_fs), |
||||||
|
'grid': create_program(gl, grid_vs, dots_fs), |
||||||
|
'iquad': create_program(gl, iquad_vs, iquad_fs), |
||||||
|
}; |
||||||
|
|
||||||
|
context.buffers = { |
||||||
|
'b_images': gl.createBuffer(), |
||||||
|
'b_strokes_static': gl.createBuffer(), |
||||||
|
'b_strokes_dynamic': gl.createBuffer(), |
||||||
|
'b_instance_dot': gl.createBuffer(), |
||||||
|
'b_instance_grid': gl.createBuffer(), |
||||||
|
'b_dot': gl.createBuffer(), |
||||||
|
'b_hud': gl.createBuffer(), |
||||||
|
'b_iquads': gl.createBuffer(), |
||||||
|
}; |
||||||
|
|
||||||
|
context.textures = { |
||||||
|
'stroke_data': gl.createTexture(), |
||||||
|
'dynamic_stroke_data': gl.createTexture(), |
||||||
|
'ui': gl.createTexture(), |
||||||
|
}; |
||||||
|
|
||||||
|
gl.bindTexture(gl.TEXTURE_2D, context.textures['stroke_data']); |
||||||
|
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.NEAREST); |
||||||
|
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.NEAREST); |
||||||
|
gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA16UI, config.stroke_texture_size, config.stroke_texture_size, 0, gl.RGBA_INTEGER, gl.UNSIGNED_SHORT, new Uint16Array(config.stroke_texture_size * config.stroke_texture_size * 4)); // fill the whole texture once with zeroes to kill a warning about a partial upload
|
||||||
|
|
||||||
|
gl.bindTexture(gl.TEXTURE_2D, context.textures['dynamic_stroke_data']); |
||||||
|
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.NEAREST); |
||||||
|
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.NEAREST); |
||||||
|
gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA16UI, config.dynamic_stroke_texture_size, config.dynamic_stroke_texture_size, 0, gl.RGBA_INTEGER, gl.UNSIGNED_SHORT, new Uint16Array(config.dynamic_stroke_texture_size * config.dynamic_stroke_texture_size * 4)); // fill the whole texture once with zeroes to kill a warning about a partial upload
|
||||||
|
|
||||||
|
gl.bindTexture(gl.TEXTURE_2D, context.textures['ui']); |
||||||
|
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.NEAREST); |
||||||
|
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.NEAREST); |
||||||
|
gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA16UI, config.ui_texture_size, config.ui_texture_size, 0, gl.RGBA_INTEGER, gl.UNSIGNED_SHORT, new Uint16Array(config.ui_texture_size * config.ui_texture_size * 4)); // fill the whole texture once with zeroes to kill a warning about a partial upload
|
||||||
|
|
||||||
|
const resize_canvas = (entries) => { |
||||||
|
// https://www.khronos.org/webgl/wiki/HandlingHighDPI
|
||||||
|
const entry = entries[0]; |
||||||
|
|
||||||
|
let width; |
||||||
|
let height; |
||||||
|
|
||||||
|
if (entry.devicePixelContentBoxSize) { |
||||||
|
width = entry.devicePixelContentBoxSize[0].inlineSize; |
||||||
|
height = entry.devicePixelContentBoxSize[0].blockSize; |
||||||
|
} else if (entry.contentBoxSize) { |
||||||
|
// fallback for Safari that will not always be correct
|
||||||
|
width = Math.round(entry.contentBoxSize[0].inlineSize * devicePixelRatio); |
||||||
|
height = Math.round(entry.contentBoxSize[0].blockSize * devicePixelRatio); |
||||||
|
} |
||||||
|
|
||||||
|
context.canvas.width = width; |
||||||
|
context.canvas.height = height; |
||||||
|
|
||||||
|
schedule_draw(state, context); |
||||||
|
} |
||||||
|
|
||||||
|
const resize_observer = new ResizeObserver(resize_canvas); |
||||||
|
resize_observer.observe(context.canvas); |
||||||
|
} |
||||||
|
|
||||||
|
function create_shader(gl, type, source) { |
||||||
|
const shader = gl.createShader(type); |
||||||
|
|
||||||
|
gl.shaderSource(shader, source); |
||||||
|
gl.compileShader(shader); |
||||||
|
|
||||||
|
if (gl.getShaderParameter(shader, gl.COMPILE_STATUS)) { |
||||||
|
return shader; |
||||||
|
} |
||||||
|
|
||||||
|
console.error(type, ':', gl.getShaderInfoLog(shader)); |
||||||
|
|
||||||
|
gl.deleteShader(shader); |
||||||
|
} |
||||||
|
|
||||||
|
function create_program(gl, vs, fs) { |
||||||
|
const program = gl.createProgram(); |
||||||
|
|
||||||
|
gl.attachShader(program, vs); |
||||||
|
gl.attachShader(program, fs); |
||||||
|
gl.linkProgram(program); |
||||||
|
|
||||||
|
if (gl.getProgramParameter(program, gl.LINK_STATUS)) { |
||||||
|
// src: tiny-sdf
|
||||||
|
// https://github.com/mapbox/tiny-sdf
|
||||||
|
const wrapper = {program}; |
||||||
|
const num_attrs = gl.getProgramParameter(program, gl.ACTIVE_ATTRIBUTES); |
||||||
|
const num_uniforms = gl.getProgramParameter(program, gl.ACTIVE_UNIFORMS); |
||||||
|
|
||||||
|
wrapper.locations = {}; |
||||||
|
|
||||||
|
for (let i = 0; i < num_attrs; i++) { |
||||||
|
const attribute = gl.getActiveAttrib(program, i); |
||||||
|
wrapper.locations[attribute.name] = gl.getAttribLocation(program, attribute.name); |
||||||
|
} |
||||||
|
|
||||||
|
for (let i = 0; i < num_uniforms; i++) { |
||||||
|
const uniform = gl.getActiveUniform(program, i); |
||||||
|
wrapper.locations[uniform.name] = gl.getUniformLocation(program, uniform.name); |
||||||
|
} |
||||||
|
|
||||||
|
return wrapper; |
||||||
|
} |
||||||
|
console.error('link:', gl.getProgramInfoLog(program)); |
||||||
|
|
||||||
|
gl.deleteProgram(program); |
||||||
|
} |
@ -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(); |