Compare commits
135 Commits
Author | SHA1 | Date |
---|---|---|
A.Olokhtonov | 9892ebf33a | 4 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 @@
@@ -1,3 +1,9 @@
|
||||
server/images |
||||
server/server.log |
||||
doca.txt |
||||
data/ |
||||
client/*.dot |
||||
server/points.txt |
||||
server/scripts.js |
||||
*.o |
||||
*.out |
||||
|
@ -0,0 +1,24 @@
@@ -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,83 @@
@@ -0,0 +1,83 @@
|
||||
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?) |
||||
+ 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 |
||||
- Debug |
||||
- 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 |
||||
- 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) |
||||
+ Undo |
||||
- Undo for images (add, move, scale) |
||||
- Undo for eraser |
||||
- Redo |
||||
+ Snapping to grid |
||||
- Snapping to other points? |
||||
* 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 |
||||
- 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 |
||||
- Curve modification |
||||
- Select curves (with a lasso?) |
||||
- Move whole curve |
||||
- Move single point |
||||
- Move multiple points |
||||
* Customizable background |
||||
+ Dots pattern |
||||
+ Grid pattern |
||||
- Menu option |
||||
|
||||
Bonus-bonus: |
||||
- Actually infinite canvas (replace floats with something, some kind of fixed point scheme? chunks? multilevel scheme?) |
@ -0,0 +1,275 @@
@@ -0,0 +1,275 @@
|
||||
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); |
||||
await 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: { |
||||
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; |
||||
} |
||||
} |
||||
|
||||
function canvas_css_rect(context) { |
||||
const el = context.canvas; |
||||
return { |
||||
'width': el.clientWidth, |
||||
'height': el.clientHeight |
||||
}; |
||||
} |
@ -0,0 +1,333 @@
@@ -0,0 +1,333 @@
|
||||
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_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) { |
||||
if (quad_fully_inside(quad, node.bbox)) { |
||||
tv_append(result_buffer, tv_data(node.stroke_indices)); |
||||
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 canvas = canvas_css_rect(context); |
||||
|
||||
const screen_topleft = screen_to_canvas(state, {'x': 0, 'y': 0}); |
||||
const screen_bottomright = screen_to_canvas(state, {'x': canvas.width, 'y': 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, vertical, strokes, depth) { |
||||
if (strokes.length > 1) { |
||||
// internal
|
||||
let sorted_strokes; |
||||
|
||||
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, !vertical, sorted_strokes.slice(0, left_of_split_count), depth + 1); |
||||
const child2 = bvh_construct_rec(state, bvh, !vertical, 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, true, strokes, 0); |
||||
} |
||||
} |
@ -0,0 +1,613 @@
@@ -0,0 +1,613 @@
|
||||
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: { |
||||
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}, |
||||
}; |
||||
} |
||||
|
||||
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: { |
||||
geometry_add_point(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: { |
||||
geometry_clear_player(state, context, 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; |
||||
|
||||
// TODO: do not do this for my own strokes when we bake locally
|
||||
geometry_clear_player(state, context, event.user_id); |
||||
need_draw = true; |
||||
|
||||
event.index = state.events.length; |
||||
|
||||
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); |
||||
|
||||
for (let i = state.events.length - 1; i >=0; --i) { |
||||
const other_event = state.events[i]; |
||||
|
||||
// Users can only undo their own, undeleted (not already undone) events
|
||||
if (other_event.user_id === event.user_id && !other_event.deleted) { |
||||
if (other_event.type === EVENT.STROKE) { |
||||
other_event.deleted = true; |
||||
if (other_event.bvh_node && !options.skip_bvh) { |
||||
bvh_delete_stroke(state, other_event); |
||||
} |
||||
need_draw = true; |
||||
break; |
||||
} else if (other_event.type === EVENT.UNDO) { |
||||
// do not undo an undo, we are not Notepad
|
||||
} else if (other_event.type === EVENT.IMAGE_MOVE) { |
||||
// TODO
|
||||
console.log('TODO: undo image scale'); |
||||
break; |
||||
} else if (other_event.type === EVENT.IMAGE_SCALE) { |
||||
// TODO
|
||||
console.log('TODO: undo image scale'); |
||||
break; |
||||
} else if (other_event.type === EVENT.ERASER) { |
||||
// TODO
|
||||
console.log('TODO: undo eraser'); |
||||
break; |
||||
} else { |
||||
console.error('cant undo event type', other_event.type); |
||||
break; |
||||
} |
||||
} |
||||
} |
||||
|
||||
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);
|
||||
image.at.x = event.x; |
||||
image.at.y = 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) { |
||||
scale_image(context, image, event.corner, {'x': event.x, 'y': 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; |
||||
} |
||||
|
||||
async 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); // await?
|
||||
|
||||
break; |
||||
} |
||||
|
||||
default: { |
||||
console.error('fuck'); |
||||
return; |
||||
} |
||||
} |
||||
|
||||
if (do_draw) { |
||||
schedule_draw(state, context); |
||||
} |
||||
} |
@ -0,0 +1,429 @@
@@ -0,0 +1,429 @@
|
||||
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 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: { |
||||
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'); |
||||
} |
||||
} |
||||
} |
||||
|
||||
async 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) await ws.send(s.buffer); |
||||
} catch(e) { |
||||
ws.close(); |
||||
} |
||||
} |
||||
|
||||
async 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) await ws.send(s.buffer); |
||||
} catch (e) { |
||||
ws.close(); |
||||
} |
||||
} |
||||
|
||||
async function sync_queue(state) { |
||||
if (ws === null) { |
||||
if (config.debug_print) console.debug('socket has closed, stopping SYNs'); |
||||
return; |
||||
} |
||||
|
||||
let size = 4 + 4 + 4; // opcode + lsn + event count
|
||||
let 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) await 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
|
||||
async 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) await ws.send(s.buffer); |
||||
} catch(e) { |
||||
ws.close(); |
||||
} |
||||
} |
||||
|
||||
function predraw_event(x, y) { |
||||
return { |
||||
'type': EVENT.PREDRAW, |
||||
'x': x, |
||||
'y': y |
||||
}; |
||||
} |
||||
|
||||
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, |
||||
} |
||||
} |
@ -1,315 +0,0 @@
@@ -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 @@
@@ -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 @@
@@ -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 @@
@@ -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 @@
@@ -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 @@
@@ -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 @@
@@ -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,237 @@
@@ -0,0 +1,237 @@
|
||||
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); |
||||
|
||||
// TODO: this is a very naive and dumb way of distributing work. Better way
|
||||
// would be to distrubute strokes based on total point count, so that
|
||||
// each worker gets approximately the same amout of _points_
|
||||
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 @@
@@ -1,23 +1,166 @@
|
||||
function tools_switch(tool) { |
||||
if (storage.tools.active_element) { |
||||
storage.tools.active_element.classList.remove('active'); |
||||
function switch_tool(state, item) { |
||||
const tool = item.getAttribute('data-tool'); |
||||
|
||||
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; |
||||
} |
||||
|
||||
storage.tools.active = tool; |
||||
storage.tools.active_element = document.querySelector(`.tool[data-tool="${tool}"]`); |
||||
storage.tools.active_element.classList.add('active'); |
||||
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 tools_init() { |
||||
const pencil = document.querySelector('.tool[data-tool="pencil"]'); |
||||
const ruler = document.querySelector('.tool[data-tool="ruler"]'); |
||||
const eraser = document.querySelector('.tool[data-tool="eraser"]'); |
||||
const undo = document.querySelector('.tool[data-tool="undo"]'); |
||||
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; |
||||
} |
||||
|
||||
const major_color = color_minor.parentElement; |
||||
select_color(state, major_color, color_u32); |
||||
|
||||
state.players[state.me].color = color_u32 |
||||
update_cursor(state); |
||||
fire_event(state, color_event(color_u32)); |
||||
} |
||||
|
||||
function switch_stroke_width(e, state) { |
||||
if (!state.online) return; |
||||
|
||||
const value = parseInt(e.target.value); |
||||
|
||||
state.players[state.me].width = value; |
||||
update_cursor(state); |
||||
} |
||||
|
||||
function broadcast_stroke_width(e, state) { |
||||
const value = e.target.value; |
||||
fire_event(state, width_event(value)); |
||||
} |
||||
|
||||
function init_tools(state) { |
||||
const tools = document.querySelectorAll('.tools .tool'); |
||||
const color_groups = document.querySelectorAll('.pallete .color-major'); |
||||
|
||||
tools.forEach((item) => { item.addEventListener('click', () => switch_tool(state, item)); }); |
||||
color_groups.forEach((item) => { |
||||
item.setAttribute('data-last-color', item.lastElementChild.getAttribute('data-color')); |
||||
|
||||
let longtouch_timer = null; |
||||
|
||||
item.addEventListener('touchstart', (e) => { |
||||
longtouch_timer = setTimeout(() => { |
||||
extend_major_color(state, item); |
||||
}, 500); |
||||
}); |
||||
|
||||
item.addEventListener('touchmove', (e) => { |
||||
if (longtouch_timer) { |
||||
clearTimeout(longtouch_timer); |
||||
} |
||||
longtouch_timer = null; |
||||
}); |
||||
|
||||
item.addEventListener('touchend', (e) => { |
||||
if (longtouch_timer) { |
||||
clearTimeout(longtouch_timer); |
||||
} |
||||
longtouch_timer = null; |
||||
}); |
||||
|
||||
item.addEventListener('click', (e) => { |
||||
if (e.ctrlKey) { |
||||
extend_major_color(state, item); |
||||
return; |
||||
} |
||||
|
||||
let color_element = e.target; |
||||
let target = e.target; |
||||
|
||||
while (!target.classList.contains('color-minor')) { |
||||
target = target.parentElement; |
||||
} |
||||
|
||||
const color_str = target.getAttribute('data-color'); |
||||
const color_u32 = color_to_u32(color_str); |
||||
|
||||
set_color_u32(state, color_u32); |
||||
}) |
||||
}); |
||||
|
||||
switch_tool(state, document.querySelector('.tool[data-tool="pencil"]')); |
||||
|
||||
const slider = document.querySelector('#stroke-width'); |
||||
|
||||
pencil.addEventListener('click', () => tools_switch('pencil')); |
||||
ruler.addEventListener('click', () => tools_switch('ruler')); |
||||
eraser.addEventListener('click', () => tools_switch('eraser')); |
||||
undo.addEventListener('click', queue_undo); |
||||
// 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)); |
||||
|
||||
tools_switch('pencil'); |
||||
document.querySelector('.phone-extra-controls').addEventListener('click', zenmode); |
||||
} |
@ -1,359 +0,0 @@
@@ -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 @@
@@ -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 @@
@@ -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,490 @@
@@ -0,0 +1,490 @@
|
||||
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) { |
||||
// TODO: only subupload what's needed
|
||||
|
||||
const bpp = 2 * 4; |
||||
const data_size = serializer.offset; |
||||
const data_pixels = data_size / bpp; // data_size % bpp is expected to always be zero here
|
||||
|
||||
const rows = Math.ceil(data_pixels / texture_size); |
||||
const last_row = data_pixels % texture_size; |
||||
const whole_upload = (rows - 1) * texture_size * bpp; |
||||
|
||||
// Upload whole rows
|
||||
if (rows > 1) { |
||||
gl.texSubImage2D(gl.TEXTURE_2D, 0, 0, 0, texture_size, rows - 1, gl.RGBA_INTEGER, gl.UNSIGNED_SHORT, new Uint16Array(serializer.buffer, 0, whole_upload / 2)); |
||||
} |
||||
|
||||
// Upload last row
|
||||
if (last_row > 0) { |
||||
const last_row_upload = last_row * bpp; |
||||
gl.texSubImage2D(gl.TEXTURE_2D, 0, 0, rows - 1, last_row, 1, gl.RGBA_INTEGER, gl.UNSIGNED_SHORT, new Uint16Array(serializer.buffer, whole_upload, last_row_upload / 2)); |
||||
} |
||||
} |
||||
|
||||
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; |
||||
|
||||
const canvas = canvas_css_rect(context); |
||||
|
||||
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, canvas.width, 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 | gl.STENCIL_BUFFER_BIT); |
||||
|
||||
// Draw the background pattern
|
||||
if (state.background_pattern === 'dots') { |
||||
gl.useProgram(context.programs['pattern'].dots); |
||||
buffers = context.buffers['pattern']; |
||||
locations = context.locations['pattern'].dots; |
||||
|
||||
gl.bindBuffer(gl.ARRAY_BUFFER, buffers['b_instance_dot']); |
||||
gl.enableVertexAttribArray(locations['a_center']); |
||||
gl.vertexAttribPointer(locations['a_center'], 2, gl.FLOAT, false, 2 * 4, 0); |
||||
gl.vertexAttribDivisor(locations['a_center'], 1); |
||||
|
||||
gl.uniform2f(locations['u_res'], canvas.width, canvas.height); |
||||
gl.uniform2f(locations['u_scale'], state.canvas.zoom, state.canvas.zoom); |
||||
gl.uniform2f(locations['u_translation'], state.canvas.offset.x, state.canvas.offset.y); |
||||
|
||||
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)); |
||||
const grid_step = 32; |
||||
|
||||
// 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, grid_step / zoom_previous, grid_step / zoom_previous)); |
||||
const t = Math.min(1.0, 1.0 - (zoom / zoom_previous) / 2.0); |
||||
|
||||
gl.uniform1f(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, grid_step / zoom_next, grid_step / zoom_next)); |
||||
const t = Math.min(1.0, 1.0 - (zoom_next / zoom) / 2.0); |
||||
|
||||
gl.uniform1f(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 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(context.programs['pattern'].grid); |
||||
buffers = context.buffers['pattern']; |
||||
locations = context.locations['pattern'].grid; |
||||
|
||||
gl.bindBuffer(gl.ARRAY_BUFFER, buffers['b_instance_grid']); |
||||
gl.enableVertexAttribArray(locations['a_data']); |
||||
gl.vertexAttribPointer(locations['a_data'], 2, gl.FLOAT, false, 2 * 4, 0); |
||||
gl.vertexAttribDivisor(locations['a_data'], 1); |
||||
|
||||
gl.uniform2f(locations['u_res'], canvas.width, canvas.height); |
||||
gl.uniform2f(locations['u_scale'], state.canvas.zoom, state.canvas.zoom); |
||||
gl.uniform2f(locations['u_translation'], state.canvas.offset.x, state.canvas.offset.y); |
||||
gl.uniform1f(locations['u_fadeout'], 1.0); |
||||
|
||||
// Previous level (major lines)
|
||||
{ |
||||
const grid_instances = new Float32Array(geometry_gen_fullscreen_grid_1d(state, context, grid_step / zoom_previous, grid_step / zoom_previous)); |
||||
let t = (zoom / zoom_previous - 1) / -7 + 1; |
||||
t = 0.25; |
||||
|
||||
gl.uniform1f(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, grid_step / zoom_next, grid_step / zoom_next)); |
||||
let t = (zoom_next / zoom - 1) / 7; |
||||
t = Math.min(0.1, -t + 1); // slight fade-in
|
||||
|
||||
gl.uniform1f(locations['u_fadeout'], t); |
||||
|
||||
gl.bindBuffer(gl.ARRAY_BUFFER, buffers['b_instance_grid']); |
||||
gl.bufferData(gl.ARRAY_BUFFER, grid_instances, gl.STREAM_DRAW); |
||||
|
||||
gl.drawArraysInstanced(gl.TRIANGLES, 0, 6, grid_instances.length / 2); |
||||
} |
||||
} |
||||
|
||||
gl.clear(gl.DEPTH_BUFFER_BIT); // draw images above the background pattern
|
||||
gl.useProgram(context.programs['image']); |
||||
buffers = context.buffers['image']; |
||||
locations = context.locations['image']; |
||||
{ |
||||
let offset = 0; |
||||
|
||||
const quads = geometry_image_quads(state, context); |
||||
|
||||
gl.bindBuffer(gl.ARRAY_BUFFER, buffers['b_quads']); |
||||
gl.bufferData(gl.ARRAY_BUFFER, quads, gl.STATIC_DRAW); |
||||
gl.vertexAttribDivisor(locations['a_pos'], 0); |
||||
|
||||
gl.enableVertexAttribArray(locations['a_pos']); |
||||
gl.vertexAttribPointer(locations['a_pos'], 2, gl.FLOAT, false, 2 * 4, 0); |
||||
|
||||
for (const entry of context.images) { |
||||
gl.uniform2f(locations['u_res'], canvas.width, canvas.height); |
||||
gl.uniform2f(locations['u_scale'], state.canvas.zoom, state.canvas.zoom); |
||||
gl.uniform2f(locations['u_translation'], state.canvas.offset.x, state.canvas.offset.y); |
||||
gl.uniform1i(locations['u_texture'], 0); // Only 1 active texture for each drawcall
|
||||
gl.uniform1i(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(locations['u_solid'], 1); |
||||
gl.uniform4f(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; |
||||
} |
||||
} |
||||
|
||||
gl.clear(gl.DEPTH_BUFFER_BIT); // draw strokes above the images
|
||||
gl.useProgram(context.programs['sdf'].main); |
||||
buffers = context.buffers['sdf']; |
||||
locations = context.locations['sdf'].main; |
||||
|
||||
// "Static" data upload
|
||||
if (segment_count > 0) { |
||||
const total_static_size = context.instance_data_points.size * 4 + |
||||
context.instance_data_ids.size * 4 + |
||||
context.instance_data_pressures.size; |
||||
|
||||
gl.bindBuffer(gl.ARRAY_BUFFER, buffers['b_instance']); |
||||
gl.bufferData(gl.ARRAY_BUFFER, total_static_size, gl.STREAM_DRAW); |
||||
gl.bufferSubData(gl.ARRAY_BUFFER, 0, tv_data(context.instance_data_points)); |
||||
gl.bufferSubData(gl.ARRAY_BUFFER, context.instance_data_points.size * 4, tv_data(context.instance_data_ids)); |
||||
gl.bufferSubData(gl.ARRAY_BUFFER, context.instance_data_points.size * 4 + context.instance_data_ids.size * 4, |
||||
tv_data(context.instance_data_pressures)); |
||||
gl.bindTexture(gl.TEXTURE_2D, context.textures['stroke_data']); |
||||
upload_square_rgba16ui_texture(gl, context.stroke_data, config.stroke_texture_size); |
||||
|
||||
gl.uniform2f(locations['u_res'], canvas.width, canvas.height); |
||||
gl.uniform2f(locations['u_scale'], state.canvas.zoom, state.canvas.zoom); |
||||
gl.uniform2f(locations['u_translation'], state.canvas.offset.x, state.canvas.offset.y); |
||||
gl.uniform1i(locations['u_stroke_count'], state.events.length); |
||||
gl.uniform1i(locations['u_debug_mode'], state.debug.red); |
||||
gl.uniform1i(locations['u_stroke_data'], 0); |
||||
gl.uniform1i(locations['u_stroke_texture_size'], config.stroke_texture_size); |
||||
gl.uniform1f(locations['u_fixed_pixel_width'], 0); |
||||
gl.uniform2f(locations['u_ssao'], config.ssao, config.ssao); |
||||
|
||||
gl.enableVertexAttribArray(locations['a_a']); |
||||
gl.enableVertexAttribArray(locations['a_b']); |
||||
gl.enableVertexAttribArray(locations['a_stroke_id']); |
||||
gl.enableVertexAttribArray(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)
|
||||
gl.vertexAttribPointer(locations['a_a'], 2, gl.FLOAT, false, 2 * 4, 0); |
||||
gl.vertexAttribPointer(locations['a_b'], 2, gl.FLOAT, false, 2 * 4, 2 * 4); |
||||
gl.vertexAttribIPointer(locations['a_stroke_id'], 1, gl.INT, 4, context.instance_data_points.size * 4); |
||||
gl.vertexAttribPointer(locations['a_pressure'], 2, gl.UNSIGNED_BYTE, true, 1, context.instance_data_points.size * 4 + context.instance_data_ids.size * 4); |
||||
|
||||
gl.vertexAttribDivisor(locations['a_a'], 1); |
||||
gl.vertexAttribDivisor(locations['a_b'], 1); |
||||
gl.vertexAttribDivisor(locations['a_stroke_id'], 1); |
||||
gl.vertexAttribDivisor(locations['a_pressure'], 1); |
||||
|
||||
// Static draw (everything already bound)
|
||||
gl.drawArraysInstanced(gl.TRIANGLES, 0, 6, segment_count); |
||||
|
||||
// I don't really know why I need to do this, but it
|
||||
// makes background patter drawcall work properly
|
||||
gl.vertexAttribDivisor(locations['a_a'], 0); |
||||
gl.vertexAttribDivisor(locations['a_b'], 0); |
||||
gl.vertexAttribDivisor(locations['a_stroke_id'], 0); |
||||
gl.vertexAttribDivisor(locations['a_pressure'], 0); |
||||
} |
||||
|
||||
// Dynamic strokes should be drawn above static strokes
|
||||
gl.clear(gl.DEPTH_BUFFER_BIT); |
||||
|
||||
// Dynamic draw (strokes currently being drawn)
|
||||
if (dynamic_segment_count > 0) { |
||||
gl.uniform1i(locations['u_stroke_count'], dynamic_stroke_count); |
||||
gl.uniform1i(locations['u_stroke_data'], 0); |
||||
gl.uniform1i(locations['u_stroke_texture_size'], config.dynamic_stroke_texture_size); |
||||
|
||||
gl.bindBuffer(gl.ARRAY_BUFFER, buffers['b_dynamic_instance']); |
||||
|
||||
// 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, context.textures['dynamic_stroke_data']); |
||||
upload_square_rgba16ui_texture(gl, context.dynamic_stroke_data, config.dynamic_stroke_texture_size); |
||||
|
||||
gl.uniform2f(locations['u_res'], canvas.width, canvas.height); |
||||
gl.uniform2f(locations['u_scale'], state.canvas.zoom, state.canvas.zoom); |
||||
gl.uniform2f(locations['u_translation'], state.canvas.offset.x, state.canvas.offset.y); |
||||
|
||||
gl.uniform1i(locations['u_stroke_count'], context.dynamic_stroke_count); |
||||
gl.uniform1i(locations['u_debug_mode'], state.debug.red); |
||||
gl.uniform1i(locations['u_stroke_data'], 0); |
||||
gl.uniform1i(locations['u_stroke_texture_size'], config.dynamic_stroke_texture_size); |
||||
gl.uniform1f(locations['u_fixed_pixel_width'], 0); |
||||
gl.uniform2f(locations['u_ssao'], config.ssao, config.ssao); |
||||
|
||||
gl.enableVertexAttribArray(locations['a_a']); |
||||
gl.enableVertexAttribArray(locations['a_b']); |
||||
gl.enableVertexAttribArray(locations['a_stroke_id']); |
||||
gl.enableVertexAttribArray(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)
|
||||
gl.vertexAttribPointer(locations['a_a'], 2, gl.FLOAT, false, 2 * 4, 0); |
||||
gl.vertexAttribPointer(locations['a_b'], 2, gl.FLOAT, false, 2 * 4, 2 * 4); |
||||
gl.vertexAttribIPointer(locations['a_stroke_id'], 1, gl.INT, 4, context.dynamic_instance_points.size * 4); |
||||
gl.vertexAttribPointer(locations['a_pressure'], 2, gl.UNSIGNED_BYTE, true, 1, context.dynamic_instance_points.size * 4 + context.dynamic_instance_ids.size * 4); |
||||
|
||||
gl.vertexAttribDivisor(locations['a_a'], 1); |
||||
gl.vertexAttribDivisor(locations['a_b'], 1); |
||||
gl.vertexAttribDivisor(locations['a_stroke_id'], 1); |
||||
gl.vertexAttribDivisor(locations['a_pressure'], 1); |
||||
|
||||
gl.drawArraysInstanced(gl.TRIANGLES, 0, 6, dynamic_segment_count); |
||||
|
||||
gl.vertexAttribDivisor(locations['a_a'], 0); |
||||
gl.vertexAttribDivisor(locations['a_b'], 0); |
||||
gl.vertexAttribDivisor(locations['a_stroke_id'], 0); |
||||
gl.vertexAttribDivisor(locations['a_pressure'], 0); |
||||
} |
||||
|
||||
// HUD: resize handles, etc
|
||||
if (state.active_image !== null) { |
||||
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_instance']); |
||||
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, context.textures['ui']); |
||||
upload_square_rgba16ui_texture(gl, handles.stroke_data, config.ui_texture_size); |
||||
|
||||
gl.uniform2f(locations['u_res'], canvas.width, canvas.height); |
||||
gl.uniform2f(locations['u_scale'], state.canvas.zoom, state.canvas.zoom); |
||||
gl.uniform2f(locations['u_translation'], state.canvas.offset.x, state.canvas.offset.y); |
||||
gl.uniform1i(locations['u_stroke_count'], 8); |
||||
gl.uniform1i(locations['u_debug_mode'], 0); |
||||
gl.uniform1i(locations['u_stroke_data'], 0); |
||||
gl.uniform1i(locations['u_stroke_texture_size'], config.ui_texture_size); |
||||
gl.uniform1f(locations['u_fixed_pixel_width'], 2); |
||||
|
||||
gl.enableVertexAttribArray(locations['a_a']); |
||||
gl.enableVertexAttribArray(locations['a_b']); |
||||
gl.enableVertexAttribArray(locations['a_stroke_id']); |
||||
gl.enableVertexAttribArray(locations['a_pressure']); |
||||
|
||||
gl.vertexAttribPointer(locations['a_a'], 2, gl.FLOAT, false, 2 * 4, 0); |
||||
gl.vertexAttribPointer(locations['a_b'], 2, gl.FLOAT, false, 2 * 4, 2 * 4); |
||||
gl.vertexAttribIPointer(locations['a_stroke_id'], 1, gl.INT, 4, handles.points.byteLength); |
||||
gl.vertexAttribPointer(locations['a_pressure'], 2, gl.UNSIGNED_BYTE, true, 1, handles.points.byteLength + handles.ids.byteLength); |
||||
|
||||
gl.vertexAttribDivisor(locations['a_a'], 1); |
||||
gl.vertexAttribDivisor(locations['a_b'], 1); |
||||
gl.vertexAttribDivisor(locations['a_stroke_id'], 1); |
||||
gl.vertexAttribDivisor(locations['a_pressure'], 1); |
||||
|
||||
// Static draw (everything already bound)
|
||||
gl.drawArraysInstanced(gl.TRIANGLES, 0, 6, ui_segments); |
||||
|
||||
// I don't really know why I need to do this, but it
|
||||
// makes background patter drawcall work properly
|
||||
gl.vertexAttribDivisor(locations['a_a'], 0); |
||||
gl.vertexAttribDivisor(locations['a_b'], 0); |
||||
gl.vertexAttribDivisor(locations['a_stroke_id'], 0); |
||||
gl.vertexAttribDivisor(locations['a_pressure'], 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,504 @@
@@ -0,0 +1,504 @@
|
||||
function push_stroke(s, stroke, stroke_index) { |
||||
const points = stroke.points; |
||||
|
||||
if (points.length < 2) { |
||||
return; |
||||
} |
||||
|
||||
for (let i = 0; i < points.length - 1; ++i) { |
||||
const from = points[i]; |
||||
const to = points[i + 1]; |
||||
|
||||
ser_f32(s, from.x); |
||||
ser_f32(s, from.y); |
||||
ser_f32(s, to.x); |
||||
ser_f32(s, to.y); |
||||
ser_u32(s, stroke_index); |
||||
} |
||||
} |
||||
|
||||
function geometry_prepare_stroke(state) { |
||||
if (!state.online) { |
||||
return null; |
||||
} |
||||
|
||||
if (state.players[state.me].points.length === 0) { |
||||
return null; |
||||
} |
||||
|
||||
const points = process_stroke2(state.canvas.zoom, state.players[state.me].points); |
||||
|
||||
return { |
||||
'color': state.players[state.me].color, |
||||
'width': state.players[state.me].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); |
||||
} |
||||
|
||||
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]; |
||||
if (player.points.length > 0) { |
||||
total_points += player.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 (let i = 0; i < player.points.length; ++i) { |
||||
const p = player.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 !== player.points.length - 1) { |
||||
tv_add(context.dynamic_instance_ids, stroke_index); |
||||
} else { |
||||
tv_add(context.dynamic_instance_ids, stroke_index | (1 << 31)); |
||||
} |
||||
} |
||||
|
||||
if (player.points.length > 0) { |
||||
const color_u32 = player.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, player.width); |
||||
|
||||
stroke_index += 1; // TODO: proper player Z order
|
||||
} |
||||
} |
||||
|
||||
context.dynamic_segment_count = total_points; |
||||
context.dynamic_stroke_count = total_strokes; |
||||
} |
||||
|
||||
function geometry_add_point(state, context, player_id, point, is_pen, raw = false) { |
||||
if (!state.online) return; |
||||
|
||||
const player = state.players[player_id]; |
||||
const points = player.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.5; |
||||
const t = 0.15 + (1 - streamline) * 0.85 |
||||
const smooth_pressure = exponential_smoothing(points, point, 3); |
||||
|
||||
points.push({ |
||||
'x': player.dynamic_head.x * t + point.x * (1 - t), |
||||
'y': player.dynamic_head.y * t + point.y * (1 - t), |
||||
'pressure': is_pen ? player.dynamic_head.pressure * t + smooth_pressure * (1 - t) : point.pressure, |
||||
}); |
||||
|
||||
if (is_pen) { |
||||
point.pressure = smooth_pressure; |
||||
} |
||||
} else { |
||||
state.players[player_id].points.push(point); |
||||
} |
||||
|
||||
recompute_dynamic_data(state, context); |
||||
player.dynamic_head = point; |
||||
} |
||||
|
||||
function geometry_clear_player(state, context, player_id) { |
||||
if (!state.online) return; |
||||
state.players[player_id].points.length = 0; |
||||
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, |
||||
}; |
||||
|
||||
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); |
||||
} |
||||
} |
||||
|
||||
function move_image(context, image, dx, dy) { |
||||
consol.error('wtf is this'); |
||||
} |
||||
|
||||
function scale_image(context, 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) { |
||||
for (const image of context.images) { |
||||
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, |
||||
}; |
||||
} |
@ -0,0 +1,786 @@
@@ -0,0 +1,786 @@
|
||||
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-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-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.zoom = Math.pow(1.0 + dz, Math.abs(state.canvas.zoom_level)) |
||||
|
||||
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 (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 === 'KeyD') { |
||||
document.querySelector('.debug-window').classList.toggle('dhide'); |
||||
} else if (e.code === 'KeyZ') { |
||||
if (e.ctrlKey) { |
||||
queue_event(state, undo_event(state)); |
||||
} else { |
||||
state.zoomdown = true; |
||||
} |
||||
} |
||||
} |
||||
|
||||
function keyup(e, state, context) { |
||||
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_clear_player(state, context, state.me); |
||||
geometry_add_point(state, context, state.me, canvasp); |
||||
|
||||
state.drawing = true; |
||||
state.active_image = null; |
||||
|
||||
schedule_draw(state, context); |
||||
} else if (state.tools.active === 'ruler') { |
||||
state.linedrawing = true; |
||||
state.ruler_origin = canvasp; |
||||
} 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; |
||||
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(context, 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; |
||||
} |
||||
|
||||
do_draw = true; |
||||
} |
||||
} |
||||
|
||||
if (state.drawing) { |
||||
canvasp.pressure = Math.ceil(e.pressure * 255); |
||||
geometry_add_point(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) { |
||||
// TODO: we should do something different when we allow multiple dynamic strokes per player
|
||||
geometry_clear_player(state, context, state.me); |
||||
|
||||
const p1 = {'x': state.ruler_origin.x, 'y': state.ruler_origin.y, 'pressure': 128}; |
||||
const p2 = {'x': canvasp.x, 'y': canvasp.y, 'pressure': 128}; |
||||
|
||||
geometry_add_point(state, context, state.me, p1, false, true); |
||||
geometry_add_point(state, context, state.me, p2, false, true); |
||||
|
||||
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; |
||||
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
|
||||
|
||||
|
||||
//geometry_add_stroke(state, context, stroke, 0);
|
||||
queue_event(state, stroke_event(state)); |
||||
//geometry_clear_player(state, context, state.me);
|
||||
schedule_draw(state, context); |
||||
} |
||||
|
||||
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
|
||||
|
||||
geometry_clear_player(state, context, state.me); // Hide predraws of this stroke that is not means to be
|
||||
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
|
||||
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)); |
||||
//geometry_clear_player(state, context, state.me);
|
||||
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; |
||||
} |
@ -0,0 +1,466 @@
@@ -0,0 +1,466 @@
|
||||
const sdf_vs_src = `#version 300 es
|
||||
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 vec2 u_ssao; |
||||
|
||||
out vec4 v_line; |
||||
out vec2 v_texcoord; |
||||
out vec3 v_color; |
||||
|
||||
flat out vec2 v_thickness; |
||||
|
||||
void main() { |
||||
vec2 screen02; |
||||
float apron = 1.0; // google "futanari inflation rule 34"
|
||||
|
||||
int stroke_data_y = a_stroke_id / u_stroke_texture_size; |
||||
int stroke_data_x = a_stroke_id % u_stroke_texture_size; |
||||
|
||||
vec2 line_dir = normalize(a_b - a_a); |
||||
vec2 up_dir = vec2(line_dir.y, -line_dir.x); |
||||
vec2 pixel = vec2(2.0) / u_res; |
||||
|
||||
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; |
||||
} |
||||
|
||||
float rscale = apron / u_scale.x; |
||||
int vertex_index = gl_VertexID % 6; |
||||
|
||||
vec2 outwards; |
||||
vec2 origin; |
||||
|
||||
if (vertex_index == 0) { |
||||
// "top left" aka "p1"
|
||||
origin = a_a; |
||||
outwards = up_dir - line_dir; |
||||
} else if (vertex_index == 1 || vertex_index == 5) { |
||||
// "top right" aka "p2"
|
||||
origin = a_b; |
||||
outwards = up_dir + line_dir; |
||||
} else if (vertex_index == 2 || vertex_index == 4) { |
||||
// "bottom left" aka "p3"
|
||||
origin = a_a; |
||||
outwards = -up_dir - line_dir; |
||||
} else { |
||||
// "bottom right" aka "p4"
|
||||
origin = a_b; |
||||
outwards = -up_dir + line_dir; |
||||
} |
||||
|
||||
vec2 pos = origin + normalize(outwards) * radius * 2.0 * max(a_pressure.x, a_pressure.y); // doubling is to account for max possible pressure
|
||||
screen02 = (pos.xy * u_scale + u_translation) / u_res * u_ssao * 2.0 + outwards * pixel * apron; |
||||
v_texcoord = pos.xy; |
||||
// v_texcoord = pos.xy + outwards * rscale;
|
||||
screen02.y = 2.0 - screen02.y; |
||||
v_line = vec4(a_a, a_b); |
||||
v_thickness = radius * a_pressure; // pressure 0.5 is the "neutral" pressure
|
||||
v_color = vec3(stroke_data.xyz) / 255.0; |
||||
if (a_stroke_id >> 31 != 0) { |
||||
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 vec4 v_line; |
||||
in vec2 v_texcoord; |
||||
in vec3 v_color; |
||||
|
||||
flat in vec2 v_thickness; |
||||
|
||||
layout(location = 0) out vec4 FragColor; |
||||
|
||||
void main() { |
||||
if (u_debug_mode == 0) { |
||||
vec2 a = v_line.xy; |
||||
vec2 b = v_line.zw; |
||||
|
||||
vec2 pa = v_texcoord - a.xy, ba = b.xy - a.xy; |
||||
float h = clamp(dot(pa, ba) / dot(ba, ba), 0.0, 1.0); |
||||
float dist = length(v_texcoord - (a + ba * h)) - mix(v_thickness.x, v_thickness.y, h); |
||||
|
||||
/* |
||||
float fade = 0.5 * length(fwidth(v_texcoord)); |
||||
float alpha = 1.0 - smoothstep(-fade, fade, dist); |
||||
*/ |
||||
|
||||
float alpha = 1.0 - step(0.0, dist); |
||||
alpha = clamp(0.0, 1.0, alpha); |
||||
|
||||
if (alpha == 0.0) { |
||||
discard; |
||||
} else { |
||||
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); |
||||
} |
||||
`;
|
||||
|
||||
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); |
||||
|
||||
context.programs['image'] = create_program(gl, quad_vs, quad_fs); |
||||
context.programs['sdf'] = { |
||||
'main': create_program(gl, sdf_vs, sdf_fs), |
||||
}; |
||||
context.programs['pattern'] = { |
||||
'dots': create_program(gl, dots_vs, dots_fs), |
||||
'grid': create_program(gl, grid_vs, dots_fs), |
||||
}; |
||||
|
||||
context.locations['image'] = { |
||||
'a_pos': gl.getAttribLocation(context.programs['image'], 'a_pos'), |
||||
|
||||
'u_res': gl.getUniformLocation(context.programs['image'], 'u_res'), |
||||
'u_scale': gl.getUniformLocation(context.programs['image'], 'u_scale'), |
||||
'u_translation': gl.getUniformLocation(context.programs['image'], 'u_translation'), |
||||
'u_texture': gl.getUniformLocation(context.programs['image'], 'u_texture'), |
||||
'u_solid': gl.getUniformLocation(context.programs['image'], 'u_solid'), |
||||
'u_color': gl.getUniformLocation(context.programs['image'], 'u_color'), |
||||
}; |
||||
|
||||
context.locations['sdf'] = { |
||||
'main': { |
||||
'a_a': gl.getAttribLocation(context.programs['sdf'].main, 'a_a'), |
||||
'a_b': gl.getAttribLocation(context.programs['sdf'].main, 'a_b'), |
||||
'a_stroke_id': gl.getAttribLocation(context.programs['sdf'].main, 'a_stroke_id'), |
||||
'a_pressure': gl.getAttribLocation(context.programs['sdf'].main, 'a_pressure'), |
||||
|
||||
'u_res': gl.getUniformLocation(context.programs['sdf'].main, 'u_res'), |
||||
'u_scale': gl.getUniformLocation(context.programs['sdf'].main, 'u_scale'), |
||||
'u_translation': gl.getUniformLocation(context.programs['sdf'].main, 'u_translation'), |
||||
'u_debug_mode': gl.getUniformLocation(context.programs['sdf'].main, 'u_debug_mode'), |
||||
'u_stroke_count': gl.getUniformLocation(context.programs['sdf'].main, 'u_stroke_count'), |
||||
'u_stroke_data': gl.getUniformLocation(context.programs['sdf'].main, 'u_stroke_data'), |
||||
'u_stroke_texture_size': gl.getUniformLocation(context.programs['sdf'].main, 'u_stroke_texture_size'), |
||||
'u_fixed_pixel_width': gl.getUniformLocation(context.programs['sdf'].main, 'u_fixed_pixel_width'), |
||||
'u_ssao': gl.getUniformLocation(context.programs['sdf'].main, 'u_ssao'), |
||||
} |
||||
}; |
||||
|
||||
context.locations['pattern'] = { |
||||
'dots': { |
||||
'a_xy': gl.getAttribLocation(context.programs['pattern'].dots, 'a_xy'), |
||||
'a_center': gl.getAttribLocation(context.programs['pattern'].dots, 'a_center'), |
||||
|
||||
'u_res': gl.getUniformLocation(context.programs['pattern'].dots, 'u_res'), |
||||
'u_scale': gl.getUniformLocation(context.programs['pattern'].dots, 'u_scale'), |
||||
'u_translation': gl.getUniformLocation(context.programs['pattern'].dots, 'u_translation'), |
||||
'u_fadeout': gl.getUniformLocation(context.programs['pattern'].dots, 'u_fadeout'), |
||||
}, |
||||
|
||||
'grid': { |
||||
'a_data': gl.getAttribLocation(context.programs['pattern'].grid, 'a_data'), |
||||
|
||||
'u_res': gl.getUniformLocation(context.programs['pattern'].grid, 'u_res'), |
||||
'u_scale': gl.getUniformLocation(context.programs['pattern'].grid, 'u_scale'), |
||||
'u_translation': gl.getUniformLocation(context.programs['pattern'].grid, 'u_translation'), |
||||
'u_fadeout': gl.getUniformLocation(context.programs['pattern'].grid, 'u_fadeout'), |
||||
} |
||||
}; |
||||
|
||||
context.buffers['image'] = { |
||||
'b_quads': gl.createBuffer(), |
||||
}; |
||||
|
||||
context.buffers['sdf'] = { |
||||
'b_instance': gl.createBuffer(), |
||||
'b_dynamic_instance': gl.createBuffer(), |
||||
}; |
||||
|
||||
context.buffers['pattern'] = { |
||||
'b_instance_dot': gl.createBuffer(), |
||||
'b_instance_grid': gl.createBuffer(), |
||||
'b_dot': 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 * config.ssao; |
||||
context.canvas.height = height * config.ssao; |
||||
|
||||
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)) { |
||||
return program; |
||||
} |
||||
|
||||
console.error('link:', gl.getProgramInfoLog(program)); |
||||
|
||||
gl.deleteProgram(program); |
||||
} |
@ -0,0 +1,85 @@
@@ -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(); |