Browse Source

Add option to use "demetri smoothing". Small fixes here and there

sdf
Aleksey Olokhtonov 5 days ago
parent
commit
61f93e0ef0
  1. 3
      Caddyfile
  2. 2
      README.txt
  3. 4
      client/bvh.js
  4. 2
      client/client_send.js
  5. 4
      client/config.js
  6. 4
      client/default.css
  7. 94
      client/math.js
  8. 9
      client/offline.html
  9. 26
      client/speed.js
  10. 4
      client/wasm/lod.c
  11. BIN
      client/wasm/lod.wasm
  12. 7
      client/webgl_draw.js
  13. 92
      client/webgl_geometry.js
  14. 5
      client/webgl_listeners.js
  15. 3
      client/webgl_shaders.js

3
Caddyfile

@ -1,7 +1,8 @@
desk.local { localhost {
header { header {
Cross-Origin-Opener-Policy same-origin Cross-Origin-Opener-Policy same-origin
Cross-Origin-Embedder-Policy require-corp Cross-Origin-Embedder-Policy require-corp
Cross-Origin-Resource-Policy same-origin
} }
redir /ws /ws/ redir /ws /ws/

2
README.txt

@ -81,7 +81,7 @@ Bonus:
+ Account for pressure in quad/bbox calc + Account for pressure in quad/bbox calc
+ Adjust curve simplification to include pressure info + Adjust curve simplification to include pressure info
+ Migrate old non-pressure desks + Migrate old non-pressure desks
- Check out e.pressure on touch devices + Check out e.pressure on touch devices
- Send pressure in PREDRAW event - Send pressure in PREDRAW event
- Stroke smoothing - Stroke smoothing
https://github.com/xournalpp/xournalpp/issues/2320 https://github.com/xournalpp/xournalpp/issues/2320

4
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); const vertical = (max_y - min_y) > (max_x - min_x);
if (vertical) { if (vertical) {
sorted_strokes = strokes.toSorted((a, b) => a.bbox.cy - b.bbox.cy); sorted_strokes = [...strokes].sort();
} else { } else {
sorted_strokes = strokes.toSorted((a, b) => a.bbox.cx - b.bbox.cx); sorted_strokes = [...strokes].sort();
} }
const node_index = bvh_make_internal(bvh); const node_index = bvh_make_internal(bvh);

2
client/client_send.js

@ -248,7 +248,7 @@ function sync_queue(state) {
ws.close(); 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) { function push_event(state, event) {

4
client/config.js

@ -32,9 +32,11 @@ const config = {
frames: 500, frames: 500,
}, },
debug_force_lod: null, 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 * 1/32/-2075/1020
*/ */
}; };

4
client/default.css

@ -359,6 +359,10 @@ canvas.mousemoving {
} }
@media (hover: none) and (pointer: coarse) { @media (hover: none) and (pointer: coarse) {
html, body {
touch-action: none;
}
.phone-extra-controls { .phone-extra-controls {
display: flex; display: flex;
} }

94
client/math.js

@ -196,6 +196,7 @@ function process_rdp2(zoom, points) {
// TODO: unify with regular process stroke // TODO: unify with regular process stroke
function process_stroke2(zoom, points) { function process_stroke2(zoom, points) {
//const result = smooth_curve(points);
const result = process_rdp2(zoom, points); const result = process_rdp2(zoom, points);
return result; return result;
} }
@ -286,6 +287,16 @@ function dot(a, b) {
return a.x * b.x + a.y * b.y; 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) { function mix(a, b, t) {
return a * t + b * (1 - t); return a * t + b * (1 - t);
} }
@ -466,3 +477,86 @@ function stroke_intersects_capsule(state, stroke, a, b, radius) {
return false; 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;
}

9
client/offline.html

@ -0,0 +1,9 @@
<script>
async function getFile() {
// Open file picker and destructure the result the first handle
const [fileHandle] = await window.showOpenFilePicker();
const file = await fileHandle.getFile();
return file;
}
</script>

26
client/speed.js

@ -20,7 +20,10 @@ function workers_thread_message(workers, message, thread_field=null) {
for (let i = 0; i < workers.length; ++i) { for (let i = 0; i < workers.length; ++i) {
if (thread_field !== null) { if (thread_field !== null) {
const m = structuredClone(message); const m = {};
for (const key in message) {
m[key] = message[key];
}
m[thread_field] = i; m[thread_field] = i;
messages.push(m); messages.push(m);
} else { } else {
@ -33,15 +36,24 @@ function workers_thread_message(workers, message, thread_field=null) {
async function init_wasm(state) { async function init_wasm(state) {
const memory = new WebAssembly.Memory({ const memory = new WebAssembly.Memory({
initial: 16384, // F U initial: 2048, // F U
maximum: 16384, // 1GiB maximum: 2048, // 128MiB
shared: true, shared: true,
}); });
// "Master thread" to do maintance on (static allocations, merging results etc) let master_wasm;
const master_wasm = await WebAssembly.instantiateStreaming(fetch('wasm/lod.wasm'), { if (WebAssembly.hasOwnProperty('instantiateStreaming')) {
env: { 'memory': memory } // "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; const nworkers = navigator.hardwareConcurrency;

4
client/wasm/lod.c

@ -1,4 +1,6 @@
#ifndef FORCE_SCALAR
#include <wasm_simd128.h> #include <wasm_simd128.h>
#endif
extern char __heap_base; 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_nx = dy / dist_ab * 255.0f;
float dir_ny = -dx / dist_ab * 255.0f; float dir_ny = -dx / dist_ab * 255.0f;
#if 0 #ifdef FORCE_SCALAR
// Scalar version preserved for reference // Scalar version preserved for reference
for (int i = segment_start + 1; i < segment_end; ++i) { for (int i = segment_start + 1; i < segment_end; ++i) {

BIN
client/wasm/lod.wasm

Binary file not shown.

7
client/webgl_draw.js

@ -131,6 +131,7 @@ function draw_strokes(state, width, height, programs, gl, lod_levels, segment_co
stroke_texture_size, stroke_texture_size,
stroke_data, stroke_data,
stroke_count, stroke_count,
opacity_multiplier,
) { ) {
const pr = programs['main']; 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_debug_mode'], state.debug.red);
gl.uniform1i(pr.locations['u_stroke_data'], 0); gl.uniform1i(pr.locations['u_stroke_data'], 0);
gl.uniform1i(pr.locations['u_stroke_texture_size'], config.stroke_texture_size); 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_pos']);
gl.enableVertexAttribArray(pr.locations['a_a']); gl.enableVertexAttribArray(pr.locations['a_a']);
@ -441,6 +443,7 @@ async function draw(state, context, animate, ts) {
config.stroke_texture_size, config.stroke_texture_size,
context.stroke_data, context.stroke_data,
state.events.length, // not really state.events.length, // not really
1.0,
); );
} }
@ -463,6 +466,7 @@ async function draw(state, context, animate, ts) {
config.stroke_texture_size, config.stroke_texture_size,
context.dynamic_stroke_data, context.dynamic_stroke_data,
context.dynamic_stroke_count, context.dynamic_stroke_count,
0.5,
); );
} }
@ -491,7 +495,8 @@ async function draw(state, context, animate, ts) {
textures['ui'], textures['ui'],
config.ui_texture_size, config.ui_texture_size,
handles.stroke_data, handles.stroke_data,
8 8,
1.0,
); );
} }

92
client/webgl_geometry.js

@ -193,6 +193,7 @@ function geometry_start_prestroke(state, player_id) {
player.strokes.push({ player.strokes.push({
'empty': false, 'empty': false,
'points': [], 'points': [],
'raw_points': [],
'head': null, 'head': null,
'color': player.color, 'color': player.color,
'width': player.width, '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) { 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; if (!state.online) return;
const player = state.players[player_id]; const player = state.players[player_id];

5
client/webgl_listeners.js

@ -196,6 +196,8 @@ function pointerdown(e, state, context) {
const canvasp = screen_to_canvas(state, screenp); const canvasp = screen_to_canvas(state, screenp);
const raw_canvasp = {...canvasp}; const raw_canvasp = {...canvasp};
canvasp.t = performance.now();
if (state.snap === 'grid') { if (state.snap === 'grid') {
const step = grid_snap_step(state); const step = grid_snap_step(state);
canvasp.x = Math.round(canvasp.x / step) * step; 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 canvasp = screen_to_canvas(state, screenp);
const raw_canvasp = {...canvasp}; const raw_canvasp = {...canvasp};
canvasp.t = performance.now();
if (state.snap === 'grid') { if (state.snap === 'grid') {
const step = grid_snap_step(state); const step = grid_snap_step(state);
canvasp.x = Math.round(canvasp.x / step) * step; canvasp.x = Math.round(canvasp.x / step) * step;
@ -430,6 +434,7 @@ function pointermove(e, state, context) {
} }
if (state.drawing) { if (state.drawing) {
//console.debug(performance.now(), screenp.x, screenp.y);
canvasp.pressure = Math.ceil(e.pressure * 255); canvasp.pressure = Math.ceil(e.pressure * 255);
geometry_add_prepoint(state, context, state.me, canvasp, e.pointerType === "pen"); geometry_add_prepoint(state, context, state.me, canvasp, e.pointerType === "pen");
fire_event(state, predraw_event(canvasp.x, canvasp.y)); fire_event(state, predraw_event(canvasp.x, canvasp.y));

3
client/webgl_shaders.js

@ -77,6 +77,7 @@ const sdf_fs_src = `#version 300 es
uniform int u_debug_mode; uniform int u_debug_mode;
uniform vec3 u_debug_color; uniform vec3 u_debug_color;
uniform float u_opacity_multipliter;
in vec3 v_color; in vec3 v_color;
@ -84,7 +85,7 @@ const sdf_fs_src = `#version 300 es
void main() { void main() {
if (u_debug_mode == 0) { if (u_debug_mode == 0) {
float alpha = 0.75; float alpha = 0.75 * u_opacity_multipliter;
FragColor = vec4(v_color * alpha, alpha); FragColor = vec4(v_color * alpha, alpha);
} else { } else {
FragColor = vec4(u_debug_color, 0.8); FragColor = vec4(u_debug_color, 0.8);

Loading…
Cancel
Save