From 61f93e0ef07b58f6708f853909585cc52dc9aedd Mon Sep 17 00:00:00 2001 From: Aleksey Olokhtonov Date: Thu, 9 Oct 2025 00:44:27 +0300 Subject: [PATCH] Add option to use "demetri smoothing". Small fixes here and there --- Caddyfile | 3 +- README.txt | 2 +- client/bvh.js | 4 +- client/client_send.js | 2 +- client/config.js | 4 +- client/default.css | 4 ++ client/math.js | 94 ++++++++++++++++++++++++++++++++++++++ client/offline.html | 9 ++++ client/speed.js | 26 ++++++++--- client/wasm/lod.c | 4 +- client/wasm/lod.wasm | Bin 3207 -> 2744 bytes client/webgl_draw.js | 7 ++- client/webgl_geometry.js | 92 +++++++++++++++++++++++++++++++++++++ client/webgl_listeners.js | 5 ++ client/webgl_shaders.js | 3 +- 15 files changed, 243 insertions(+), 16 deletions(-) create mode 100644 client/offline.html 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 269a030ff7498c0b8435c12ab34065068edfd938..6d825d30ccbdec2f505baa9286524bc5e0afe98b 100755 GIT binary patch delta 1314 zcmX|BO>Y}T7@l3nj+-5Sd+l{LiJuueY2q|#5NfHAKp6@>^uP&@ZPi0Snk|RMuEYVf z7D56E4v-Bu&OIab!ja#=FW`TGDxTMyQy#D;} zC>-bO-%gZUE`!XDrYbx|uyzV%ItZMS^*FV+O;kBWM0*9D{ZP4Z;jMOSo@cZ@H22K3 z(V@w7ihsl7yv>-mJK^_ro4RG4emii#{P>sh-tl!ay#oLIk%EG&EUapFiPun5=`h_<@E=L9!gW*;EO>ZqC zW=QD)QeP&SiTgb_J-*-M>T|JYqluzlqM-RP!R(+R4_bvL;Xn~PdU9&Gz?(*dTPvKX z!_t~#0}bway~;NFY{L$OE!%SWd}Rr|74yoNk~OrD$|Bt{TA5*oh&@lxh+RM$TKvsS zOglWu9zsjL(J&=XCV)a&Wt>ycs>R~!_%k1<6j6;jfHXgeUpx*+VhAY+Sd zp{iYcT>UOD$qY&|gKIL$eDj7-(vhL*X^Bja&BaFRAX1U4vJ>H*P$TI@+PfY{(_2lW zX(7UPIXV=wDLI=?dZ=4_h`a z=OC7{79-A}t7mp1TsHYy>?)bvT0xl#ra&gRkDge{E_YA9XDbkTnCt3AB_eC{&x=5m6j;E$-}j*OVb zxSX8LTy!(LEVa2h?YZ1&`4XwS3y zi@RJHnm~(k(<=dC3*5SQ5WAW;TM`%D@JuPMKS$SvPI*!U+%?qUC( g1NE%6cz*un+2Yw3XU`X}&d=tx;zXy#XZqd$0i8npZ*_ZZuh+2?ub*+G$W6e99CAPc4j@n@%%dj}!rn+o zfW|_R00B~zH4+DeI3d9SA?3o63;%;d;KUz@U<_Z)*b!n^z1>}1-SyR1U-$l8|G53r z-_rTBIysf!z4TSd?M3+yz4zyY`Sw_by51Mg9?8re2~{F#1h<9!@Xf(ZrutJrAY1#yLj0*r*a>v$u@NN<)Jzlr5 z?%#>5BH|$&l%n<-h8e3h`PjkXThy`XCN*cij#LBZ7tyL{$Xoe9QB6_6d=p@1smupz zK}EnJ0sPe9QQ1hV${t-va8iLH&E_d8qx17wq)|m0y2Xs?iiwUAi?o`}iLnFbs6riX zpu9OI*AtGVgv)LKO?qX5mB8QASODpnzoz* zQmL?5W)re-tb$gkXVo0980W54Gsab25H`(8G%)DuW-5&)Bk5C}??oZh^Blj308QhD zx|^U|V3?9c-KGXCFW5BmhMM+WkBT5F6T(MV_X9~;2%&mb44;buOA!D$j>mJoGSS1& zX-LBp)FySwkvBQbH6wi)w`dD%%jehX3Jlv+1a9rc6$~63cR@WaQrvaWXVAN99XfSG zR7iE!e;>6$n`*>TBmWi{ zsMAL5WgZ+$j7At8A4?M{pWgzg4iZQn98(5c&K@$aA$pN3Wisz*FMJ9uhv!EHCPJZx zbm?VX>+m5j;uOp=8DgJ@iR&m`OTcOp74)k`@;Ldm6eEGBe>yVoK~*8_vnklud=mHgVB 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);