diff --git a/Caddyfile b/Caddyfile index 9d42ea1..d773b40 100644 --- a/Caddyfile +++ b/Caddyfile @@ -1,7 +1,8 @@ -desk.local { +localhost { header { Cross-Origin-Opener-Policy same-origin Cross-Origin-Embedder-Policy require-corp + Cross-Origin-Resource-Policy same-origin } redir /ws /ws/ diff --git a/README.txt b/README.txt index e373c6c..d3c252b 100644 --- a/README.txt +++ b/README.txt @@ -81,7 +81,7 @@ Bonus: + 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 + + Check out e.pressure on touch devices - Send pressure in PREDRAW event - Stroke smoothing https://github.com/xournalpp/xournalpp/issues/2320 diff --git a/client/bvh.js b/client/bvh.js index 0ce7712..6c1988f 100644 --- a/client/bvh.js +++ b/client/bvh.js @@ -335,9 +335,9 @@ function bvh_construct_rec(state, bvh, strokes, depth) { const vertical = (max_y - min_y) > (max_x - min_x); if (vertical) { - sorted_strokes = strokes.toSorted((a, b) => a.bbox.cy - b.bbox.cy); + sorted_strokes = [...strokes].sort(); } else { - sorted_strokes = strokes.toSorted((a, b) => a.bbox.cx - b.bbox.cx); + sorted_strokes = [...strokes].sort(); } const node_index = bvh_make_internal(bvh); diff --git a/client/client_send.js b/client/client_send.js index 5ce8b83..e512cda 100644 --- a/client/client_send.js +++ b/client/client_send.js @@ -248,7 +248,7 @@ function sync_queue(state) { ws.close(); } - setTimeout(() => sync_queue(state), config.sync_timeout); + state.timers.queue_sync = setTimeout(() => sync_queue(state), config.sync_timeout); } function push_event(state, event) { diff --git a/client/config.js b/client/config.js index a0c1301..ddb3cc3 100644 --- a/client/config.js +++ b/client/config.js @@ -32,9 +32,11 @@ const config = { frames: 500, }, debug_force_lod: null, + demetri_ms: 40, + p: 'demetri', /* - * points of interest (desk/zoomlevel/x/y + * points of interest (desk/zoomlevel/x/y) * 1/32/-2075/1020 */ }; diff --git a/client/default.css b/client/default.css index c110912..fa61d62 100644 --- a/client/default.css +++ b/client/default.css @@ -359,6 +359,10 @@ canvas.mousemoving { } @media (hover: none) and (pointer: coarse) { + html, body { + touch-action: none; + } + .phone-extra-controls { display: flex; } diff --git a/client/math.js b/client/math.js index 46da54c..e1262d7 100644 --- a/client/math.js +++ b/client/math.js @@ -196,6 +196,7 @@ function process_rdp2(zoom, points) { // TODO: unify with regular process stroke function process_stroke2(zoom, points) { + //const result = smooth_curve(points); const result = process_rdp2(zoom, points); return result; } @@ -286,6 +287,16 @@ function dot(a, b) { return a.x * b.x + a.y * b.y; } +function dotn(a, b) { + let r = 0; + + for (let i = 0; i < a.length; ++i) { + r += a[i] * b[i]; + } + + return r; +} + function mix(a, b, t) { return a * t + b * (1 - t); } @@ -466,3 +477,86 @@ function stroke_intersects_capsule(state, stroke, a, b, radius) { return false; } + +function estimate_next_point(ts, xs, ys, dt=0) { + const N = ts.length; + + // mean values + let mx = 0, my = 0, mt = 0; + + for (let i = 0; i < N; ++i) { + mt += ts[i]; + mx += xs[i]; + my += ys[i]; + } + + mt /= N; + mx /= N; + my /= N; + + if (N < 2) { + return [xs[N - 1], ys[N - 1]]; + } + + // orthogonalize against constant term + for (let i = 0; i < N; ++i) { + ts[i] -= mt; + xs[i] -= mx; + ys[i] -= my; + } + + // dot products against time basis + const dtt = dotn(ts, ts); + const dtx = dotn(ts, xs); + const dty = dotn(ts, ys); + + // reconstruction coefficients + const cx = dtx / dtt; + const cy = dty / dtt; + + // estimated next values + const nx = cx * (ts[N - 1] + dt) + mx; + const ny = cy * (ts[N - 1] + dt) + my; + + return [nx, ny]; +} + +function smooth_curve(points, window_ms=config.demetri_ms, dt=0) { + const result = []; + + for (let i = 0; i < points.length; ++i) { + let start_i = i; + let end_i = i; + const curr_t = points[i].t; + + while (start_i - 1 >= 0) { + if (curr_t - points[start_i - 1].t < window_ms) { + start_i--; + } else { + break; + } + } + + const t_window = []; + const x_window = []; + const y_window = [] + + for (let j = start_i; j < i + 1; ++j) { + const p = points[j]; + t_window.push(p.t); + x_window.push(p.x); + y_window.push(p.y); + } + + const [nx, ny] = estimate_next_point(t_window, x_window, y_window, dt); + + result.push({ + 'x': nx, + 'y': ny, + 'pressure': points[i].pressure, + }); + } + + return result; +} + diff --git a/client/offline.html b/client/offline.html new file mode 100644 index 0000000..b164655 --- /dev/null +++ b/client/offline.html @@ -0,0 +1,9 @@ + + diff --git a/client/speed.js b/client/speed.js index 1fdc480..61261b2 100644 --- a/client/speed.js +++ b/client/speed.js @@ -20,7 +20,10 @@ function workers_thread_message(workers, message, thread_field=null) { for (let i = 0; i < workers.length; ++i) { if (thread_field !== null) { - const m = structuredClone(message); + const m = {}; + for (const key in message) { + m[key] = message[key]; + } m[thread_field] = i; messages.push(m); } else { @@ -33,15 +36,24 @@ function workers_thread_message(workers, message, thread_field=null) { async function init_wasm(state) { const memory = new WebAssembly.Memory({ - initial: 16384, // F U - maximum: 16384, // 1GiB + initial: 2048, // F U + maximum: 2048, // 128MiB 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 } - }); + let master_wasm; + if (WebAssembly.hasOwnProperty('instantiateStreaming')) { + // "Master thread" to do maintance on (static allocations, merging results etc) + master_wasm = await WebAssembly.instantiateStreaming(fetch('wasm/lod.wasm'), { + env: { 'memory': memory } + }); + } else { + const f = await fetch('wasm/lod.wasm'); + const bytes = await f.arrayBuffer(); + master_wasm = await WebAssembly.instantiate(bytes, { + env: { 'memory': memory } + }); + } const nworkers = navigator.hardwareConcurrency; diff --git a/client/wasm/lod.c b/client/wasm/lod.c index c3ae222..2785d5c 100644 --- a/client/wasm/lod.c +++ b/client/wasm/lod.c @@ -1,4 +1,6 @@ +#ifndef FORCE_SCALAR #include +#endif extern char __heap_base; @@ -82,7 +84,7 @@ rdp_find_max(float *xs, float *ys, unsigned char *pressures, float zoom, int coo float dir_nx = dy / dist_ab * 255.0f; float dir_ny = -dx / dist_ab * 255.0f; -#if 0 +#ifdef FORCE_SCALAR // Scalar version preserved for reference for (int i = segment_start + 1; i < segment_end; ++i) { diff --git a/client/wasm/lod.wasm b/client/wasm/lod.wasm index 269a030..6d825d3 100755 Binary files a/client/wasm/lod.wasm and b/client/wasm/lod.wasm differ diff --git a/client/webgl_draw.js b/client/webgl_draw.js index f99cc85..cb18230 100644 --- a/client/webgl_draw.js +++ b/client/webgl_draw.js @@ -131,6 +131,7 @@ function draw_strokes(state, width, height, programs, gl, lod_levels, segment_co stroke_texture_size, stroke_data, stroke_count, + opacity_multiplier, ) { const pr = programs['main']; @@ -176,6 +177,7 @@ function draw_strokes(state, width, height, programs, gl, lod_levels, segment_co gl.uniform1i(pr.locations['u_debug_mode'], state.debug.red); gl.uniform1i(pr.locations['u_stroke_data'], 0); gl.uniform1i(pr.locations['u_stroke_texture_size'], config.stroke_texture_size); + gl.uniform1f(pr.locations['u_opacity_multipliter'], opacity_multiplier); gl.enableVertexAttribArray(pr.locations['a_pos']); gl.enableVertexAttribArray(pr.locations['a_a']); @@ -441,6 +443,7 @@ async function draw(state, context, animate, ts) { config.stroke_texture_size, context.stroke_data, state.events.length, // not really + 1.0, ); } @@ -463,6 +466,7 @@ async function draw(state, context, animate, ts) { config.stroke_texture_size, context.dynamic_stroke_data, context.dynamic_stroke_count, + 0.5, ); } @@ -491,7 +495,8 @@ async function draw(state, context, animate, ts) { textures['ui'], config.ui_texture_size, handles.stroke_data, - 8 + 8, + 1.0, ); } diff --git a/client/webgl_geometry.js b/client/webgl_geometry.js index d7bdc89..dd0bfcb 100644 --- a/client/webgl_geometry.js +++ b/client/webgl_geometry.js @@ -193,6 +193,7 @@ function geometry_start_prestroke(state, player_id) { player.strokes.push({ 'empty': false, 'points': [], + 'raw_points': [], 'head': null, 'color': player.color, 'width': player.width, @@ -208,6 +209,97 @@ function geometry_end_prestroke(state, player_id) { } function geometry_add_prepoint(state, context, player_id, point, is_pen, raw = false) { + if (config.p === 'demetri') { + return geometry_add_prepoint_demetri(state, context, player_id, point, is_pen, raw); + } else if (config.p == 'raw') { + return geometry_add_prepoint_raw(state, context, player_id, point, is_pen, raw); + } else if (config.p == 'avg') { + return geometry_add_prepoint_new(state, context, player_id, point, is_pen, raw); + } else if (config.p == 'perf') { + return geometry_add_prepoint_old(state, context, player_id, point, is_pen, raw); + } +} + +function geometry_add_prepoint_demetri(state, context, player_id, point, is_pen, raw = false) { + if (!state.online) return; + + const player = state.players[player_id]; + const stroke = player.strokes[player.strokes.length - 1]; + + stroke.raw_points.push(point); + stroke.points = smooth_curve(stroke.raw_points); + + if (point.pressure < config.min_pressure) { + point.pressure = config.min_pressure; + } + + recompute_dynamic_data(state, context); +} + +function geometry_add_prepoint_raw(state, context, player_id, point, is_pen, raw = false) { + if (!state.online) return; + + const player = state.players[player_id]; + const stroke = player.strokes[player.strokes.length - 1]; + const points = stroke.points; + + if (point.pressure < config.min_pressure) { + point.pressure = config.min_pressure; + } + + points.push(point); + + recompute_dynamic_data(state, context); +} + +function geometry_add_prepoint_new(state, context, player_id, point, is_pen, raw = false) { + if (!state.online) return; + + const player = state.players[player_id]; + const stroke = player.strokes[player.strokes.length - 1]; + const points = stroke.points; + + if (point.pressure < config.min_pressure) { + point.pressure = config.min_pressure; + } + + const exp_window = 3; + + if (points.length > exp_window) { + let xsum = 0; + let ysum = 0; + + const screen_last = canvas_to_screen(state, points[points.length - 1]); + const screen_this = canvas_to_screen(state, point); + + const screen_dx = screen_this.x - screen_last.x; + const screen_dy = screen_this.y - screen_last.y; + + // TODO: Higher (screen space!) speed gives more weight to the new point + const weight_x = 1; + const weight_y = 1; + + for (let i = points.length - exp_window; i < points.length; ++i) { + xsum += points[i].x * weight_x; + ysum += points[i].y * weight_y; + } + + xsum += point.x * weight_x; + ysum += point.y * weight_y + + points.push({ + 'x': xsum / (exp_window + 1), + 'y': ysum / (exp_window + 1), + 'pressure': point.pressure + }); + } else { + points.push(point); + } + + recompute_dynamic_data(state, context); +} + +function geometry_add_prepoint_old(state, context, player_id, point, is_pen, raw = false) { if (!state.online) return; const player = state.players[player_id]; diff --git a/client/webgl_listeners.js b/client/webgl_listeners.js index d66d027..edb0bba 100644 --- a/client/webgl_listeners.js +++ b/client/webgl_listeners.js @@ -196,6 +196,8 @@ function pointerdown(e, state, context) { const canvasp = screen_to_canvas(state, screenp); const raw_canvasp = {...canvasp}; + canvasp.t = performance.now(); + if (state.snap === 'grid') { const step = grid_snap_step(state); canvasp.x = Math.round(canvasp.x / step) * step; @@ -317,6 +319,8 @@ function pointermove(e, state, context) { const canvasp = screen_to_canvas(state, screenp); const raw_canvasp = {...canvasp}; + canvasp.t = performance.now(); + if (state.snap === 'grid') { const step = grid_snap_step(state); canvasp.x = Math.round(canvasp.x / step) * step; @@ -430,6 +434,7 @@ function pointermove(e, state, context) { } if (state.drawing) { + //console.debug(performance.now(), screenp.x, screenp.y); canvasp.pressure = Math.ceil(e.pressure * 255); geometry_add_prepoint(state, context, state.me, canvasp, e.pointerType === "pen"); fire_event(state, predraw_event(canvasp.x, canvasp.y)); diff --git a/client/webgl_shaders.js b/client/webgl_shaders.js index dfe78f8..7a6654e 100644 --- a/client/webgl_shaders.js +++ b/client/webgl_shaders.js @@ -77,6 +77,7 @@ const sdf_fs_src = `#version 300 es uniform int u_debug_mode; uniform vec3 u_debug_color; + uniform float u_opacity_multipliter; in vec3 v_color; @@ -84,7 +85,7 @@ const sdf_fs_src = `#version 300 es void main() { if (u_debug_mode == 0) { - float alpha = 0.75; + float alpha = 0.75 * u_opacity_multipliter; FragColor = vec4(v_color * alpha, alpha); } else { FragColor = vec4(u_debug_color, 0.8);