diff --git a/README.md b/README.md index 7f47b08..499ac15 100644 --- a/README.md +++ b/README.md @@ -13,6 +13,7 @@ Release: + 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 @@ -40,16 +41,20 @@ Release: - 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 - Icon - Product page (github readme, demo videos) Bonus: - - Handle pressure - - Add pressure data to quads - - Draw capsules instead of segments - - Adjust curve simplification to include pressure info + * 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 diff --git a/client/aux.js b/client/aux.js index 797c7c2..ceb2897 100644 --- a/client/aux.js +++ b/client/aux.js @@ -64,7 +64,8 @@ function event_size(event) { } case EVENT.STROKE: { - size += 4 + 2 + 2 + 4 + event.points.length * 4 * 2; // u32 stroke id + u16 (count) + u16 (width) + u32 (color + count * (f32, f32) points + // 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; } diff --git a/client/client_recv.js b/client/client_recv.js index 3b01a78..ace817d 100644 --- a/client/client_recv.js +++ b/client/client_recv.js @@ -51,6 +51,12 @@ function des_f32array(d, 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 = {}; @@ -100,10 +106,14 @@ function des_event(d, state = null) { const color = des_u32(d); const coords = des_f32array(d, point_count * 2); - + const press = des_u8array(d, point_count); + + des_align(d, 4); + wasm_ensure_by(state, 1, coords.length); const coordinates = state.wasm.buffers['coordinates']; + const pressures = state.wasm.buffers['pressures']; tv_add(state.wasm.buffers['coords_from'].tv, coordinates.tv.size + point_count * 2); state.wasm.buffers['coords_from'].used += 4; // 4 bytes, not 4 ints @@ -115,6 +125,9 @@ function des_event(d, state = null) { tv_append(coordinates.tv, coords); state.wasm.buffers['coordinates'].used += point_count * 2 * 4; + tv_append(pressures.tv, press); + state.wasm.buffers['pressures'].used += point_count; + event.stroke_id = stroke_id; event.color = color; @@ -187,7 +200,7 @@ function handle_event(state, context, event, options = {}) { } case EVENT.PREDRAW: { - geometry_add_point(state, context, event.user_id, {'x': event.x, 'y': event.y}); + geometry_add_point(state, context, event.user_id, {'x': event.x, 'y': event.y}, 0.5); // TODO: event.pressure need_draw = true; break; } diff --git a/client/client_send.js b/client/client_send.js index 8780d45..4877fd8 100644 --- a/client/client_send.js +++ b/client/client_send.js @@ -43,6 +43,11 @@ function ser_clear(s) { 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; @@ -114,6 +119,12 @@ function ser_event(s, event) { ser_f32(s, point.y); } + for (const point of event.points) { + ser_u8(s, point.pressure); + } + + ser_align(s, 4); + break; } diff --git a/client/index.js b/client/index.js index 21a7a32..eee568c 100644 --- a/client/index.js +++ b/client/index.js @@ -14,7 +14,7 @@ const config = { buffer_first_touchmoves: 5, debug_print: false, min_zoom: 0.00001, - max_zoom: 1, + max_zoom: 10, initial_offline_timeout: 1000, default_color: 0x00, default_width: 8, @@ -237,8 +237,10 @@ async function main() { 'instance_data_points': tv_create(Float32Array, 4096), 'instance_data_ids': tv_create(Uint32Array, 4096), + 'instance_data_pressures': tv_create(Uint8Array, 4096), 'dynamic_instance_points': tv_create(Float32Array, 4096), + 'dynamic_instance_pressure': tv_create(Uint8Array, 4096), 'dynamic_instance_ids': tv_create(Uint32Array, 4096), 'stroke_data': serializer_create(config.initial_static_bytes), diff --git a/client/math.js b/client/math.js index 729aaa3..c9769e6 100644 --- a/client/math.js +++ b/client/math.js @@ -251,8 +251,7 @@ function segment_interesects_quad(a, b, quad_topleft, quad_bottomright, quad_top } function stroke_bbox(state, stroke) { - const radius = stroke.width / 2; - + const radius = stroke.width; // do not divide by 2 to account for max possible pressure const coordinates = state.wasm.buffers['coordinates'].tv.data; let min_x = coordinates[stroke.coords_from + 0] - radius; diff --git a/client/speed.js b/client/speed.js index cbbf007..97ab900 100644 --- a/client/speed.js +++ b/client/speed.js @@ -19,6 +19,10 @@ async function init_wasm(state) { 'offset': state.wasm.exports.alloc_static(state.wasm.stroke_bytes), 'used': 0, }, + 'pressures': { + 'offset': state.wasm.exports.alloc_static(state.wasm.coords_bytes / 8), + 'used': 0 + }, }; const mem = state.wasm.exports.memory.buffer; @@ -29,7 +33,8 @@ async function init_wasm(state) { mem, state.wasm.buffers['coords_from'].offset); state.wasm.buffers['line_threshold'].tv = tv_create_on(Float32Array, state.wasm.stroke_bytes / 4, mem, state.wasm.buffers['line_threshold'].offset); - + state.wasm.buffers['pressures'].tv = tv_create_on(Uint8Array, state.wasm.coords_bytes / 8, + mem, state.wasm.buffers['pressures'].offset); tv_add(state.wasm.buffers['coords_from'].tv, 0); state.wasm.buffers['coords_from'].used = 4; @@ -40,6 +45,7 @@ function wasm_ensure_by(state, nstrokes, ncoords) { const old_coords_from_offset = buffers['coords_from'].offset; const old_line_threshold_offset = buffers['line_threshold'].offset; + const old_pressures_offset = buffers['pressures'].offset; const old_size_coords = state.wasm.coords_bytes; const old_size_strokes = state.wasm.stroke_bytes; @@ -67,23 +73,29 @@ function wasm_ensure_by(state, nstrokes, ncoords) { buffers['coordinates'].offset = state.wasm.exports.alloc_static(state.wasm.coords_bytes); buffers['coords_from'].offset = state.wasm.exports.alloc_static(state.wasm.stroke_bytes); buffers['line_threshold'].offset = state.wasm.exports.alloc_static(state.wasm.stroke_bytes); + buffers['pressures'].offset = state.wasm.exports.alloc_static(state.wasm.coords_bytes / 8); buffers['coordinates'].tv = tv_create_on(Float32Array, state.wasm.coords_bytes / 4, mem, buffers['coordinates'].offset); buffers['coords_from'].tv = tv_create_on(Uint32Array, state.wasm.stroke_bytes / 4, mem, buffers['coords_from'].offset); buffers['line_threshold'].tv = tv_create_on(Float32Array, state.wasm.stroke_bytes / 4, mem, buffers['line_threshold'].offset); + buffers['pressures'].tv = tv_create_on(Uint8Array, state.wasm.coords_bytes / 8, mem, buffers['pressures'].offset); buffers['coordinates'].tv.size = buffers['coordinates'].used / 4; buffers['coords_from'].tv.size = buffers['coords_from'].used / 4; buffers['line_threshold'].tv.size = buffers['line_threshold'].used / 4; + buffers['pressures'].tv.size = buffers['pressures'].used; + + const tmp = new Uint8Array(Math.max(state.wasm.coords_bytes / 8, state.wasm.stroke_bytes)); // TODO: needed? - const tmp = new Uint8Array(state.wasm.stroke_bytes); // TODO: needed? + // Copy from back to front (otherwise we will overwrite) + tmp.set(new Uint8Array(mem, old_pressures_offset, buffers['pressures'].used)); + memv.set(new Uint8Array(tmp.buffer, 0, buffers['pressures'].used), buffers['pressures'].offset); - // First we move the line_threshold, only then coords_from (otherwise we will overwrite) tmp.set(new Uint8Array(mem, old_line_threshold_offset, old_size_strokes)); - memv.set(tmp, buffers['coordinates'].offset + state.wasm.coords_bytes + state.wasm.stroke_bytes); + memv.set(new Uint8Array(tmp.buffer, 0, old_size_strokes), buffers['line_threshold'].offset); tmp.set(new Uint8Array(mem, old_coords_from_offset, old_size_strokes)); - memv.set(tmp, buffers['coordinates'].offset + state.wasm.coords_bytes); + memv.set(new Uint8Array(tmp.buffer, 0, old_size_strokes), buffers['coords_from'].offset); } } @@ -102,6 +114,7 @@ function do_lod_wasm(state, context) { buffers['coords_from'].offset, buffers['line_threshold'].offset, buffers['coordinates'].offset, + buffers['pressures'].offset, buffers['coordinates'].used / 4, ); @@ -113,13 +126,18 @@ function do_lod_wasm(state, context) { result_offset, segment_count * 2); const wasm_ids = new Uint32Array(state.wasm.exports.memory.buffer, result_offset + segment_count * 2 * 4, segment_count); - + const wasm_pressures = new Uint8Array(state.wasm.exports.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; } diff --git a/client/wasm/lod.c b/client/wasm/lod.c index 7f83927..bec7683 100644 --- a/client/wasm/lod.c +++ b/client/wasm/lod.c @@ -75,6 +75,7 @@ do_lod(int *clipped_indices, int clipped_count, float zoom, int *stroke_coords_from, float *line_threshold, float *coordinates, + unsigned char *pressures, int coordinates_count) { if (clipped_count == 0) { @@ -154,6 +155,7 @@ do_lod(int *clipped_indices, int clipped_count, float zoom, // Write actual coordinates (points) and stroke ids float *points = alloc_dynamic(segments_head * 2 * 4); int *ids = alloc_dynamic(segments_head * 4); + unsigned char *pressures_res = alloc_dynamic(segments_head); int phead = 0; int ihead = 0; @@ -174,6 +176,8 @@ do_lod(int *clipped_indices, int clipped_count, float zoom, points[phead++] = x; points[phead++] = y; + pressures_res[ihead] = pressures[base_stroke / 2 + point_index]; + if (j != to - 1) { ids[ihead++] = stroke_index; } else { diff --git a/client/wasm/lod.wasm b/client/wasm/lod.wasm index 7ea392c..b11d67b 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 9b4a547..c945569 100644 --- a/client/webgl_draw.js +++ b/client/webgl_draw.js @@ -107,10 +107,16 @@ function draw(state, context) { // "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, context.instance_data_points.size * 4 + context.instance_data_ids.size * 4, gl.STREAM_DRAW); + 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); @@ -125,15 +131,18 @@ function draw(state, context) { 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'], 1, 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); @@ -151,24 +160,33 @@ function draw(state, context) { gl.bindBuffer(gl.ARRAY_BUFFER, buffers['b_dynamic_instance']); // Dynamic data upload - gl.bufferData(gl.ARRAY_BUFFER, context.dynamic_instance_points.size * 4 + context.dynamic_instance_ids.size * 4, gl.STREAM_DRAW); + 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.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'], 1, 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); } diff --git a/client/webgl_geometry.js b/client/webgl_geometry.js index f254157..314844f 100644 --- a/client/webgl_geometry.js +++ b/client/webgl_geometry.js @@ -105,9 +105,11 @@ function recompute_dynamic_data(state, context) { } 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); @@ -124,6 +126,7 @@ function recompute_dynamic_data(state, context) { 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); diff --git a/client/webgl_listeners.js b/client/webgl_listeners.js index 65b3d4f..a2309c1 100644 --- a/client/webgl_listeners.js +++ b/client/webgl_listeners.js @@ -204,6 +204,7 @@ function mousedown(e, state, context) { } if (state.tools.active === 'pencil') { + canvasp.pressure = 128; geometry_clear_player(state, context, state.me); geometry_add_point(state, context, state.me, canvasp); @@ -253,7 +254,7 @@ function mousemove(e, state, context) { } if (state.drawing) { - //console.log(e.pressure); + canvasp.pressure = Math.ceil(e.pressure * 255); geometry_add_point(state, context, state.me, canvasp); fire_event(state, predraw_event(canvasp.x, canvasp.y)); do_draw = true; @@ -442,7 +443,7 @@ function touchmove(e, state, context) { 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)); diff --git a/client/webgl_shaders.js b/client/webgl_shaders.js index c03f614..4ba9f84 100644 --- a/client/webgl_shaders.js +++ b/client/webgl_shaders.js @@ -104,8 +104,8 @@ const nop_fs_src = `#version 300 es const sdf_vs_src = `#version 300 es in vec2 a_a; // point from in vec2 a_b; // point to - in float a_radius; in int a_stroke_id; + in float a_pressure; uniform vec2 u_scale; uniform vec2 u_res; @@ -159,21 +159,21 @@ const sdf_vs_src = `#version 300 es outwards = -up_dir + line_dir; } - vec2 pos = origin + normalize(outwards) * radius; + vec2 pos = origin + normalize(outwards) * radius * 2.0; // doubling is to account for max possible pressure screen02 = (pos.xy * u_scale + u_translation) / u_res * 2.0 + outwards * pixel; v_texcoord = pos.xy + outwards * rscale; screen02.y = 2.0 - screen02.y; v_line = vec4(a_a, a_b); - v_thickness = radius; + v_thickness = radius * a_pressure * 2.0; // 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); + gl_Position = vec4(screen02 - 1.0, (float(a_stroke_id) / float(u_stroke_count)) * 2.0 - 1.0, 1.0); } `; @@ -312,6 +312,7 @@ function init_webgl(state, context) { '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'), diff --git a/server/deserializer.js b/server/deserializer.js index 9e23e2b..419030e 100644 --- a/server/deserializer.js +++ b/server/deserializer.js @@ -38,6 +38,12 @@ function f32array(d, count) { return array; } +function u8array(d, count) { + const array = new Uint8Array(d.view.buffer, d.offset, count); + d.offset += count; + return array; +} + export function align(d, to) { while (d.offset % to !== 0) { d.offset++; @@ -91,6 +97,8 @@ export function event(d) { event.width = width; event.color = color; event.points = f32array(d, point_count * 2); + event.pressures = u8array(d, point_count); + align(d, 4); break; } diff --git a/server/math.js b/server/math.js index ded4ae2..3cea35f 100644 --- a/server/math.js +++ b/server/math.js @@ -8,3 +8,7 @@ export function crypto_random32() { return dataview.getUint32(0); } + +export function round_to_pow2(value, multiple) { + return (value + multiple - 1) & -multiple; +} diff --git a/server/recv.js b/server/recv.js index e9ade78..01b9606 100644 --- a/server/recv.js +++ b/server/recv.js @@ -110,7 +110,8 @@ function handle_event(session, event) { const stroke_result = storage.queries.insert_stroke.get({ '$width': event.width, '$color': event.color, - '$points': event.points + '$points': event.points, + '$pressures': event.pressures, }); event.stroke_id = stroke_result.id; diff --git a/server/send.js b/server/send.js index 78cecc5..b6e6f9f 100644 --- a/server/send.js +++ b/server/send.js @@ -40,6 +40,7 @@ function event_size(event) { case EVENT.STROKE: { size += 4 + 2 + 2 + 4; // stroke id + point count + width + color size += event.points.byteLength; + size += math.round_to_pow2(event.pressures.byteLength, 4); break; } diff --git a/server/serializer.js b/server/serializer.js index d535184..e75e1e2 100644 --- a/server/serializer.js +++ b/server/serializer.js @@ -85,11 +85,14 @@ export function event(s, event) { case EVENT.STROKE: { const points_bytes = event.points; + const pressures_bytes = event.pressures; u32(s, event.stroke_id); u16(s, points_bytes.byteLength / 2 / 4); // each point is 2 * f32 u16(s, event.width); u32(s, event.color); bytes(s, points_bytes); + bytes(s, pressures_bytes); + align(s, 4); break; } diff --git a/server/storage.js b/server/storage.js index dd959f4..b88cd6d 100644 --- a/server/storage.js +++ b/server/storage.js @@ -38,7 +38,8 @@ export function startup() { id INTEGER PRIMARY KEY, width INTEGER, color INTEGER, - points BLOB + points BLOB, + pressures BLOB );`).run(); db.query(`CREATE TABLE IF NOT EXISTS events ( @@ -69,7 +70,7 @@ export function startup() { // INSERT queries.insert_desk = db.query('INSERT INTO desks (id, title, sn) VALUES ($id, $title, 0) RETURNING id'); - queries.insert_stroke = db.query('INSERT INTO strokes (width, color, points) VALUES ($width, $color, $points) RETURNING id'); + queries.insert_stroke = db.query('INSERT INTO strokes (width, color, points, pressures) VALUES ($width, $color, $points, $pressures) RETURNING id'); queries.insert_session = db.query('INSERT INTO sessions (id, desk_id, lsn) VALUES ($id, $desk_id, 0) RETURNING id'); queries.insert_event = db.query('INSERT INTO events (type, desk_id, session_id, stroke_id, image_id, x, y) VALUES ($type, $desk_id, $session_id, $stroke_id, $image_id, $x, $y) RETURNING id'); @@ -115,6 +116,7 @@ export function startup() { if (event.type === EVENT.STROKE) { const stroke = stroke_dict[event.stroke_id]; event.points = stroke.points; + event.pressures = stroke.pressures; event.color = stroke.color; event.width = stroke.width;