From eb66ffbcadcf1af380c809abdb0c615424fc4af9 Mon Sep 17 00:00:00 2001 From: "A.Olokhtonov" Date: Thu, 9 May 2024 15:41:34 +0300 Subject: [PATCH] Significantly improve stroke smoothing and pressure handling. "Fix" issue with underallocation of WASM memory by doubling the size --- client/math.js | 69 +++++++++++++++++++++++++++++++------- client/speed.js | 3 +- client/wasm/lod.c | 2 ++ client/wasm/lod.wasm | Bin 2784 -> 2784 bytes client/webgl_geometry.js | 31 ++++++++++++++++- client/webgl_listeners.js | 11 +++--- 6 files changed, 96 insertions(+), 20 deletions(-) diff --git a/client/math.js b/client/math.js index f0e2579..cb60f30 100644 --- a/client/math.js +++ b/client/math.js @@ -60,20 +60,24 @@ function process_rdp_indices(state, zoom, stroke) { return npoints; } -function process_ewmv(points, round = false) { - const result = []; +function exponential_smoothing(points, last, up_to) { const alpha = 0.5; - result.push(points[0]); + let pr = 0; - for (let i = 1; i < points.length; ++i) { + let start = points.length - up_to; + if (start < 0) { + start = 0; + } + + for (let i = start; i < points.length; ++i) { const p = points[i]; - const x = Math.round(alpha * p.x + (1 - alpha) * result[result.length - 1].x); - const y = Math.round(alpha * p.y + (1 - alpha) * result[result.length - 1].y); - result.push({'x': x, 'y': y}); + pr = alpha * p.pressure + (1 - alpha) * pr; } - return result; + pr = alpha * last.pressure + (1 - alpha) * pr; + + return pr; } function process_stroke(state, zoom, stroke) { @@ -92,7 +96,7 @@ function process_stroke(state, zoom, stroke) { } function rdp_find_max2(zoom, points, start, end) { - const EPS = 1.0 / zoom; + const EPS = 0.125 / zoom; let result = -1; let max_dist = 0; @@ -107,7 +111,7 @@ function rdp_find_max2(zoom, points, start, end) { const sin_theta = dy / dist_ab; const cos_theta = dx / dist_ab; - for (let i = start; i < end; ++i) { + for (let i = start + 1; i < end; ++i) { const p = points[i]; const ox = p.x - a.x; @@ -132,7 +136,7 @@ function rdp_find_max2(zoom, points, start, end) { function process_rdp_r2(zoom, points, start, end) { let result = []; - + const max = rdp_find_max2(zoom, points, start, end); if (max !== -1) { @@ -145,9 +149,48 @@ function process_rdp_r2(zoom, points, start, end) { } function process_rdp2(zoom, points) { - const result = process_rdp_r2(zoom, points, 0, points.length - 1); - result.unshift(points[0]); + const result = []; + const stack = []; + + stack.push({ + 'type': 0, + 'start': 0, + 'end': points.length - 1, + }); + + result.push(points[0]); + + while (stack.length > 0) { + const entry = stack.pop(); + + if (entry.type === 0) { + const max = rdp_find_max2(zoom, points, entry.start, entry.end); + + if (max !== -1) { + stack.push({ + 'type': 0, + 'start': max, + 'end': entry.end + }); + + stack.push({ + 'type': 1, + 'index': max, + }); + + stack.push({ + 'type': 0, + 'start': entry.start, + 'end': max, + }); + } + } else { + result.push(points[entry.index]); + } + } + result.push(points[points.length - 1]); + return result; } diff --git a/client/speed.js b/client/speed.js index 0eabc76..eda7432 100644 --- a/client/speed.js +++ b/client/speed.js @@ -120,8 +120,9 @@ function wasm_ensure_by(state, nstrokes, ncoords) { if (realloc) { const current_pages = Math.ceil(state.wasm.memory.buffer.byteLength / (4096 * 16)); - const need_pages = Math.ceil((state.wasm.coords_bytes * 3 + state.wasm.stroke_bytes * 2) / (4096 * 16)); // TODO: figure out actual memory requirements + const need_pages = 2 * Math.ceil((state.wasm.coords_bytes * 3 + state.wasm.stroke_bytes * 2) / (4096 * 16)); // TODO: figure out actual memory requirements const grow_by = Math.max(1, need_pages - current_pages); + // const grow_by = 16; state.wasm.memory.grow(grow_by); state.wasm.exports.free_static(); diff --git a/client/wasm/lod.c b/client/wasm/lod.c index 0e6ce43..f4bde15 100644 --- a/client/wasm/lod.c +++ b/client/wasm/lod.c @@ -220,6 +220,7 @@ do_lod(int *clipped_indices, int clipped_count, float zoom, 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]; @@ -243,6 +244,7 @@ do_lod(int *clipped_indices, int clipped_count, float zoom, 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]; diff --git a/client/wasm/lod.wasm b/client/wasm/lod.wasm index 2e56d77fa67992d6396eeb6d9d9feeee965a015f..268250085ae243ef6d21b3814b4a8134b7b65954 100755 GIT binary patch literal 2784 zcmZuzOLN@D5uO?B3m9Nuq)3U>qcM_gQ4}STwn;gMlpxD0m+i_SKE^f#av_S?C08cE z*h=EsJ>=l3_@GG2J-;A_DE;>u(++$q%Fu zLQ?&q5D(>qKRJ2mP9|SX>?0mJDCI=;rIQ~1vT=|ejE|lwr#GESM|(oz6-qg^#>c@# zM$f+fo6r*xeg8X^Oj|7f#{QpG|Cw&pGoAkZ`$3X*zi#Q~Kh#1mj=w;d@oq92#kO$u<6@M@`Tnm`N9wAPjP~~q9o3ns?;q|LtR!S3Pm4HzA{_nbC{5$MNQ(VkQS)tU z@boY_K(p?{{CM}Yw*jM%)8t9KljN!61Xbs$acZiW+I1Ol4t;hR&kDX=^$9tpkPBN-IwlLEA z=aDWtE8MG24`YjDXOvb=y?A!YGsQ=vWY6*YKkgB#C6)K8>p4Pd{_YEPmtWu=iAbKj zuS8o3sdO}L-4ok_6A`JCMP)0K6QRVCFEvZ$y~&A0Q|pExaZP{^7!-*uWvjoPRhL;x z{JXA%5gEGCVq#?aGPr_ULkG@HZnEWU7&YHaW#)w#Izt`Opqnr!hL|AhaK0svW5i7!? z4giQmxB%0gC>WicyamMsko%(y*16^sS$Kh#XgRCAhD#L8;^<}JGAz0H&cV;Q%1_IB zpe8NDk>AvCd=8Gy9D`xn(ImGLnHd&ap=40nB=NT2UpDvy9yHvHYba?k19}(8ZO+N1%v?s7xsu&bB2?6)er5TCA|9z#Wz}^PU64Qjy;(V>^%`jX7PP+;+=Ae3kP?j2We*jVl@}#S2c<$; zL+OMYNP{y(QixRw<1^`eGMu#r_31*!2{v#11?ILO2~H)~S+Y$x@xMc>_`g&7|NX%e zZql3KI$(53e*|2*v{~xfv#CUF9W|U+(Ij+vg@SHaFm}I)y1-B|Dd!PDz!{8cW21-P z#aGAf5&F>tb`{I=H{lGe!Q#?2QmpZ^Yh zSL_sBx;`}XG#_0#HuE{n5C8lequv-A7rBGX&B=w#tk8|m80b$ww|;5m)ah&+^O@w9 z@)>qBW^)~r=|-4#M%u1yFPc0EKv1ksE8}#-%xCXGxPtJ#(A8Kou!HSTGk2ix&}-x} zpsP1tZI@?y>%_;uHEX5}YBltEYG)=_UX`>v>Td{8CG1iiwI4#^A2F+tjcg6mQs2XoKy~has1(=YK97AQJ|>ey-AUSrAmmR zMh*w#V0%(`K~M)nYCkw1&K{0fmoumS7M_drBJj%5JOINntnBq1;! zd#gj+=>K>~2*}D3jeXa0-U?flu+HYGZbfy%4%&h`mV*mpItv?m7BMw>CQS=sN|WLkH>>(;GbzTnq~)ufgJXN~S*z|!`D!!YKNxIm-dWW<$D`p|b@^Fs<+uv2 IrC%2R1Gl+CApigX literal 2784 zcmZuzOLG+06~2#o^-J@RkU#_C+$-4z31K9#z${WU#+1vh^1@ywcGJ={LOml5R*!6x z#5J?X!d3BtfU?go$Rb&%DvSJr`~ojX{zTG>ztcSmDw6K%zV|%tIp00!+=qy>eJO;H zRDC4GBRS?zjvqPW@pt3m5sx%lIaVF1lY_5o`^o$wX?;6%wydN>^&12V)sN z|NgJS8;kJu@02rXF#m_*|GfN9bxX-)^0(K6B7=4n&PowQPOSBG3;i+=H)*D8HVNW!I{p4^ri4K$OXfVoz<3~|1 z8%9s!gWe#~72xc}qc}Mco6Suzw^Hmp&C-TuUf7a(&O)eQ1|#nyh9PnlaG{` zQ9>#&oHXu>+kz7js*?p(bjBw_iA7&}%$4`YClaLR)dYzZfqKB8P;9B9`ig7Jq~Xgr9N`Em4Z-EzkuXVO%og z7d@W@m3EiH37&5c%?ep+7Q#g&V`=KbbODg_Q=vu9NjJ4Eh-@=mRl+E8>3nKu5k;y} z!Mg*cSyJ2tkWr0V>ADbig#a4ROSD2xmuqQ}W50;hBBu_yVS@n%A?6fJ6AiFX@fm$; zE+FkBjqfW@6faN;y2N0LnJ{3>bpn_e!_{QY0m{WPb95Owfl5*4)WH(bf;zx-4uMVW z!j@_P>$%jNr>o$>st=!#&oM|yLbY$-PR&xNO%pMhVW2)bO-x^Wj0ny0AfgVVX!NaN z0ByLK4Rs&sV|W(4P;yaSE)zj*j)ZS!eC=_O_n~0tsr@jJi1<00Pi>dYx*TqB+Ca#( z!?eBj(yAXwkvc~7OO+~>qV<<{l~#{m{LcQjtgoZ2-=o#nb^b0GThzuxte3aZQE7fr zqSa^>+6r22HfbI+FfF>kDvX8GMT?xL8qL$&DJR%2{~6|PgVR3ctT5*et>gbbUBmxw z?*HeF$K0e_W`k>tTAv~=9lDu&mFE+Q-UfO&ufnl$d4;0fuwaV)B5Wasii@%f0Bz1- zSQ*-GpYgRAUlnC%f^ZC=iq?K^TivMtd}Gj^{r`v^{7#9OP>+>D^g<(QXFr`uS-WnC0` zirtK>xr)hjLQFd)uUOemIDQzQK(RUt9;X{-zWfl(Rls+Q>tU6^4z@!N;8|Z|eY;#x zc3S0F{wPMtEOuQAQJv|1PDAr{!EdEy6*}SERAV-*rKaiYDs7RIBl2`N|A)-~Dd{~5 z3i|z$&R@y=N16MaowE-jwv^_-5F!s+61R?Ot=@55Z#I}|=KqrR2HoNDKT5hmx62+| zdc~3r@HX-X_D24h*`Q6jN!+^${6$^^Za8#2bK1=P3jCh^@;^$miI8wq5tq|-hV{*x z1=j2(-Um{wIM(2orso}kjk7And6-M-j&V|HQla1vCso}j6pKQMD&DXZX)IL&iVA54 zqp>}ydniziA(c1IhqH&HRS@$r(LhyHiethdWoocq{}6>JNEa1=Gmdwa8c8s$%igNc zw%)&8Bm^svIUe?1u6e&;HcDod%~Q2tRSDZ_1M1k#92oObQ}d>Xsm?R07lA;i0Goq^ z?RC?6dN}MI?Ieep^wHf(vMlMXZ$Fd%gJgRzKA@dJe6Z_nY+b)`eT%Ma-nuG2X^vvt zsYcP`Bpw~%W>sCQ$D<)GY1vwH|ERe5te1DCY^|Q{?e{h|@2q*-M}z))dHGpuWVi~h IC*O{K0%`C=?*IS* diff --git a/client/webgl_geometry.js b/client/webgl_geometry.js index 5d3f86b..8af79df 100644 --- a/client/webgl_geometry.js +++ b/client/webgl_geometry.js @@ -151,10 +151,39 @@ function recompute_dynamic_data(state, context) { context.dynamic_stroke_count = total_strokes; } +let _head = null; + function geometry_add_point(state, context, player_id, point) { if (!state.online) return; - state.players[player_id].points.push(point); + + const points = state.players[player_id].points; + + if (false && points.length === 1) { + // Fix up pressure of first point to get rid of ugly spike bit + const first_point = points[0]; + if (first_point.pressure === 0) { + first_point.pressure = point.pressure; + } + } + + if (points.length > 0) { + // 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': _head.x * t + point.x * (1 - t), + 'y': _head.y * t + point.y * (1 - t), + 'pressure': _head.pressure * t + smooth_pressure * (1 - t), + }); + point.pressure = smooth_pressure; + } else { + state.players[player_id].points.push(point); + } + recompute_dynamic_data(state, context); + _head = point; } function geometry_clear_player(state, context, player_id) { diff --git a/client/webgl_listeners.js b/client/webgl_listeners.js index 7b3d0a6..dfe4a3f 100644 --- a/client/webgl_listeners.js +++ b/client/webgl_listeners.js @@ -210,7 +210,7 @@ function mousedown(e, state, context) { } if (state.tools.active === 'pencil') { - canvasp.pressure = 128; + canvasp.pressure = Math.ceil(e.pressure * 255); geometry_clear_player(state, context, state.me); geometry_add_point(state, context, state.me, canvasp); @@ -249,9 +249,10 @@ function mousemove(e, state, context) { if (state.me in state.players) { const me = state.players[state.me]; const width = Math.max(me.width * state.canvas.zoom, 2.0); - const brush_x = screenp.x - width / 2 - 2; - const brush_y = screenp.y - width / 2 - 2; - document.querySelector('.brush-dom').style.transform = `translate(${Math.round(brush_x)}px, ${Math.round(brush_y)}px)`; + const radius = Math.round(width / 2); + const brush_x = screenp.x - radius - 2; + const brush_y = screenp.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) { @@ -376,7 +377,7 @@ function update_cursor(state) { const me = state.players[state.me]; const width = Math.max(me.width * state.canvas.zoom, 2.0); - const radius = width / 2; + const radius = Math.round(width / 2); const current_color = color_from_u32(me.color); const stroke = (me.color === 0xFFFFFF ? 'black' : 'white'); const svg = `