From 29ec26563249443df54b0c758c7303f0687e117b Mon Sep 17 00:00:00 2001 From: "A.Olokhtonov" Date: Sat, 14 Sep 2024 19:31:16 +0300 Subject: [PATCH] Keep multiple current strokes ("prestrokes") per player in a queue --- README.txt | 8 ++- client/aux.js | 3 +- client/client_recv.js | 24 +++++-- client/client_send.js | 9 ++- client/index.js | 1 + client/webgl_geometry.js | 142 +++++++++++++++++++++----------------- client/webgl_listeners.js | 35 +++++----- server/deserializer.js | 3 +- server/enums.js | 1 + server/send.js | 3 +- server/serializer.js | 3 +- 11 files changed, 140 insertions(+), 92 deletions(-) diff --git a/README.txt b/README.txt index e24fdab..762eb38 100644 --- a/README.txt +++ b/README.txt @@ -16,6 +16,8 @@ Release: + 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 + - Nothing get's drawn if we enable snapping and draw a curve where first and last point match + - Undo history of moving and scaling images seems messed up - Debug - Restore ability to limit event range * Listeners/events/multiplayer @@ -25,7 +27,7 @@ Release: + Investigate skipped inputs on mobile (panning, zooming) [Events were not actually getting skipped. The stroke previews were just not being drawn] + Smooth zoom + Infinite background pattern - - Be able to have multiple "current" strokes per player. In case of bad internet this can happen! + + Be able to have multiple "current" strokes per player. In case of bad internet this can happen! - Do NOT use session id as player id LUL - Save events to indexeddb (as some kind of a blob), restore on reconnect and page reload - Local prediction for tools! @@ -42,8 +44,8 @@ Release: + Dynamic svg cursor to represent the brush + Eraser * Line drawing - - Live preview - - Alignment (horizontal, vertical, diagonal, etc) + + Live preview + ~ Alignment (horizontal, vertical, diagonal, etc) [kinda gets covered by the snapping? question mark?] + Undo + Undo for eraser + Undo for images (add, move, scale) diff --git a/client/aux.js b/client/aux.js index 0ad3ad2..1a1d8b4 100644 --- a/client/aux.js +++ b/client/aux.js @@ -54,7 +54,8 @@ function event_size(event) { case EVENT.USER_JOINED: case EVENT.LEAVE: - case EVENT.CLEAR: { + case EVENT.CLEAR: + case EVENT.LIFT: { break; } diff --git a/client/client_recv.js b/client/client_recv.js index f9d3556..34b3f89 100644 --- a/client/client_recv.js +++ b/client/client_recv.js @@ -72,7 +72,8 @@ function des_event(d, state = null) { case EVENT.USER_JOINED: case EVENT.LEAVE: - case EVENT.CLEAR: { + case EVENT.CLEAR: + case EVENT.LIFT: { break; } @@ -187,6 +188,8 @@ function init_player_defaults(state, player_id, color = config.default_color, wi 'points': [], 'online': false, 'cursor': {'x': 0, 'y': 0}, + 'strokes': [], + 'current_prestroke': false, }; } @@ -207,13 +210,25 @@ function handle_event(state, context, event, options = {}) { } case EVENT.PREDRAW: { - geometry_add_point(state, context, event.user_id, {'x': event.x, 'y': event.y, 'pressure': 128}, false); // TODO: add pressure to predraw events + const player = state.players[event.user_id]; + + if (!player.current_prestroke) { + geometry_start_prestroke(state, event.user_id); + } + + geometry_add_prepoint(state, context, event.user_id, {'x': event.x, 'y': event.y, 'pressure': 128}, false); // TODO: add pressure to predraw events need_draw = true; break; } case EVENT.CLEAR: { - geometry_clear_player(state, context, event.user_id); + // TODO: @touch + break; + } + + case EVENT.LIFT: { + // Current stroke from player ended. Handle following PREDRAWN events as next stroke + geometry_end_prestroke(state, event.user_id); break; } @@ -336,12 +351,11 @@ function handle_event(state, context, event, options = {}) { delete event.coords; delete event.press; - // TODO: do not do this for my own strokes when we bake locally - geometry_clear_player(state, context, event.user_id); need_draw = true; event.index = state.events.length; + geometry_clear_oldest_prestroke(state, context, event.user_id); geometry_add_stroke(state, context, event, state.events.length, options.skip_bvh === true); state.stroke_count++; diff --git a/client/client_send.js b/client/client_send.js index 8f1aecb..e1a749b 100644 --- a/client/client_send.js +++ b/client/client_send.js @@ -85,7 +85,8 @@ function ser_event(s, event) { break; } - case EVENT.CLEAR: { + case EVENT.CLEAR: + case EVENT.LIFT: { break; } @@ -328,6 +329,12 @@ function predraw_event(x, y) { }; } +function lift_event() { + return { + 'type': EVENT.LIFT, + }; +} + function color_event(color_u32) { return { 'type': EVENT.SET_COLOR, diff --git a/client/index.js b/client/index.js index 1aef4d9..43502f4 100644 --- a/client/index.js +++ b/client/index.js @@ -44,6 +44,7 @@ const EVENT = Object.freeze({ SET_WIDTH: 12, CLEAR: 13, // clear predraw events from me (because I started a pan instead of drawing) MOVE_CURSOR: 14, + LIFT: 15, LEAVE: 16, MOVE_CANVAS: 17, diff --git a/client/webgl_geometry.js b/client/webgl_geometry.js index 751b7c3..0344f34 100644 --- a/client/webgl_geometry.js +++ b/client/webgl_geometry.js @@ -1,42 +1,25 @@ -function push_stroke(s, stroke, stroke_index) { - const points = stroke.points; - - if (points.length < 2) { - return; - } - - for (let i = 0; i < points.length - 1; ++i) { - const from = points[i]; - const to = points[i + 1]; - - ser_f32(s, from.x); - ser_f32(s, from.y); - ser_f32(s, to.x); - ser_f32(s, to.y); - ser_u32(s, stroke_index); - } -} - function geometry_prepare_stroke(state) { if (!state.online) { return null; } - if (state.players[state.me].points.length === 0) { + const player = state.players[state.me]; + const stroke = player.strokes[player.strokes.length - 1]; // MY OWN player.strokes should never be bigger than 1 element + + if (stroke.points.length === 0) { return null; } - const points = process_stroke2(state.canvas.zoom, state.players[state.me].points); + const points = process_stroke2(state.canvas.zoom, stroke.points); return { - 'color': state.players[state.me].color, - 'width': state.players[state.me].width, + 'color': stroke.color, + 'width': stroke.width, 'points': points, 'user_id': state.me, }; } - async function geometry_write_instances(state, context, callback) { state.stats.rdp_max_count = 0; state.stats.rdp_segments = 0; @@ -56,6 +39,7 @@ function geometry_add_dummy_stroke(context) { ser_u16(context.stroke_data, 0); } +// Real stroke, add forever function geometry_add_stroke(state, context, stroke, stroke_index, skip_bvh = false) { if (!state.online || !stroke || stroke.coords_to - stroke.coords_from === 0 || stroke.deleted) return; @@ -83,9 +67,11 @@ function recompute_dynamic_data(state, context) { for (const player_id in state.players) { const player = state.players[player_id]; - if (player.points.length > 0) { - total_points += player.points.length; - total_strokes += 1; + for (const stroke of player.strokes) { + if (!stroke.empty && stroke.points.length > 0) { + total_points += stroke.points.length; + total_strokes += 1; + } } } @@ -106,44 +92,69 @@ function recompute_dynamic_data(state, context) { // player has the same data as their current stroke: points, color, width const player = state.players[player_id]; - for (let i = 0; i < player.points.length; ++i) { - const p = player.points[i]; - - 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); - } else { - tv_add(context.dynamic_instance_ids, stroke_index | (1 << 31)); + for (const stroke of player.strokes) { + if (!stroke.empty && stroke.points.length > 0) { + for (let i = 0; i < stroke.points.length; ++i) { + const p = stroke.points[i]; + + 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 !== stroke.points.length - 1) { + tv_add(context.dynamic_instance_ids, stroke_index); + } else { + tv_add(context.dynamic_instance_ids, stroke_index | (1 << 31)); + } + } + + const color_u32 = stroke.color; + const r = (color_u32 >> 16) & 0xFF; + const g = (color_u32 >> 8) & 0xFF; + const b = color_u32 & 0xFF; + + ser_u16(context.dynamic_stroke_data, r); + ser_u16(context.dynamic_stroke_data, g); + ser_u16(context.dynamic_stroke_data, b); + ser_u16(context.dynamic_stroke_data, stroke.width); + + stroke_index += 1; // TODO: proper player Z order } } - - if (player.points.length > 0) { - const color_u32 = player.color; - const r = (color_u32 >> 16) & 0xFF; - const g = (color_u32 >> 8) & 0xFF; - const b = color_u32 & 0xFF; - - ser_u16(context.dynamic_stroke_data, r); - ser_u16(context.dynamic_stroke_data, g); - ser_u16(context.dynamic_stroke_data, b); - ser_u16(context.dynamic_stroke_data, player.width); - - stroke_index += 1; // TODO: proper player Z order - } } context.dynamic_segment_count = total_points; context.dynamic_stroke_count = total_strokes; } -function geometry_add_point(state, context, player_id, point, is_pen, raw = false) { +function geometry_start_prestroke(state, player_id) { + if (!state.online) return; + + const player = state.players[player_id]; + + player.strokes.push({ + 'empty': false, + 'points': [], + 'head': null, + 'color': player.color, + 'width': player.width, + }); + + player.current_prestroke = true; +} + +function geometry_end_prestroke(state, player_id) { + if (!state.online) return; + const player = state.players[player_id]; + player.current_prestroke = false; +} + +function geometry_add_prepoint(state, context, player_id, point, is_pen, raw = false) { if (!state.online) return; const player = state.players[player_id]; - const points = player.points; + const stroke = player.strokes[player.strokes.length - 1]; + const points = stroke.points; if (point.pressure < config.min_pressure) { point.pressure = config.min_pressure; @@ -152,30 +163,35 @@ function geometry_add_point(state, context, player_id, point, is_pen, raw = fals if (points.length > 0 && !raw) { // pulled from "perfect-freehand" package. MIT // https://github.com/steveruizok/perfect-freehand/ - const streamline = 0.5; + const streamline = 0.75; const t = 0.15 + (1 - streamline) * 0.85 const smooth_pressure = exponential_smoothing(points, point, 3); points.push({ - 'x': player.dynamic_head.x * t + point.x * (1 - t), - 'y': player.dynamic_head.y * t + point.y * (1 - t), - 'pressure': is_pen ? player.dynamic_head.pressure * t + smooth_pressure * (1 - t) : point.pressure, + 'x': stroke.head.x * t + point.x * (1 - t), + 'y': stroke.head.y * t + point.y * (1 - t), + 'pressure': is_pen ? stroke.head.pressure * t + smooth_pressure * (1 - t) : point.pressure, }); if (is_pen) { point.pressure = smooth_pressure; } } else { - state.players[player_id].points.push(point); + points.push(point); } - + + stroke.head = point; + recompute_dynamic_data(state, context); - player.dynamic_head = point; } -function geometry_clear_player(state, context, player_id) { +// Remove prestroke from dynamic data (usually because it's now a real stroke) +function geometry_clear_oldest_prestroke(state, context, player_id) { if (!state.online) return; - state.players[player_id].points.length = 0; + + const player = state.players[player_id]; + player.strokes.shift(); + recompute_dynamic_data(state, context); } diff --git a/client/webgl_listeners.js b/client/webgl_listeners.js index abd7a0d..b71d03f 100644 --- a/client/webgl_listeners.js +++ b/client/webgl_listeners.js @@ -218,8 +218,8 @@ function mousedown(e, state, context) { if (state.tools.active === 'pencil') { canvasp.pressure = Math.ceil(e.pressure * 255); - geometry_clear_player(state, context, state.me); - geometry_add_point(state, context, state.me, canvasp); + geometry_start_prestroke(state, state.me); + geometry_add_prepoint(state, context, state.me, canvasp, e.pointerType === "pen"); state.drawing = true; state.active_image = null; @@ -228,6 +228,7 @@ function mousedown(e, state, context) { } else if (state.tools.active === 'ruler') { state.linedrawing = true; state.ruler_origin = canvasp; + geometry_start_prestroke(state, state.me); } else if (state.tools.active === 'eraser') { state.erasing = true; } else if (state.tools.active === 'pointer') { @@ -408,7 +409,7 @@ function mousemove(e, state, context) { if (state.drawing) { canvasp.pressure = Math.ceil(e.pressure * 255); - geometry_add_point(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)); do_draw = true; } @@ -446,16 +447,21 @@ function mousemove(e, state, context) { } if (state.linedrawing) { - // TODO: we should do something different when we allow multiple dynamic strokes per player - geometry_clear_player(state, context, state.me); - const p1 = {'x': state.ruler_origin.x, 'y': state.ruler_origin.y, 'pressure': 128}; const p2 = {'x': canvasp.x, 'y': canvasp.y, 'pressure': 128}; - geometry_add_point(state, context, state.me, p1, false, true); - geometry_add_point(state, context, state.me, p2, false, true); + if (state.online) { + const me = state.players[state.me]; + const prestroke = me.strokes[me.strokes.length - 1]; // TODO: might as well be me.strokes[0] ? - do_draw = true; + prestroke.points.length = 2; + prestroke.points[0] = p1; + prestroke.points[1] = p2; + + recompute_dynamic_data(state, context); + + do_draw = true; + } } if (do_draw) { @@ -520,14 +526,11 @@ function mouseup(e, state, context) { if (stroke) { // TODO: be able to add a baked stroke locally - - - //geometry_add_stroke(state, context, stroke, 0); queue_event(state, stroke_event(state)); - //geometry_clear_player(state, context, state.me); schedule_draw(state, context); } + fire_event(state, lift_event()); state.drawing = false; return; @@ -620,7 +623,7 @@ function wheel(e, state, context) { function start_move(e, state, context) { // two touch identifiers are expected to be pushed into state.touch.ids at this point - geometry_clear_player(state, context, state.me); // Hide predraws of this stroke that is not means to be + // TODO: @touch, remove preview fire_event(state, clear_event(state)); // Tell others to hide predraws of this stroke for (const touch of e.touches) { @@ -696,7 +699,8 @@ function touchmove(e, state, context) { } canvasp.pressure = 128; // TODO: check out touch devices' e.pressure - geometry_add_point(state, context, state.me, canvasp); + // TODO: fix when doing @touch + //geometry_add_point(state, context, state.me, canvasp); fire_event(state, predraw_event(canvasp.x, canvasp.y)); schedule_draw(state, context); @@ -779,7 +783,6 @@ function touchend(e, state, context) { if (stroke) { queue_event(state, stroke_event(state)); - //geometry_clear_player(state, context, state.me); schedule_draw(state, context); } diff --git a/server/deserializer.js b/server/deserializer.js index 2f234a1..bcde2c5 100644 --- a/server/deserializer.js +++ b/server/deserializer.js @@ -88,7 +88,8 @@ export function event(d) { break; } - case EVENT.CLEAR: { + case EVENT.CLEAR: + case EVENT.LIFT: { break; } diff --git a/server/enums.js b/server/enums.js index 51bc768..fa0e803 100644 --- a/server/enums.js +++ b/server/enums.js @@ -10,6 +10,7 @@ export const EVENT = Object.freeze({ SET_WIDTH: 12, CLEAR: 13, MOVE_CURSOR: 14, + LIFT: 15, LEAVE: 16, MOVE_CANVAS: 17, diff --git a/server/send.js b/server/send.js index a0324bd..c2e38c3 100644 --- a/server/send.js +++ b/server/send.js @@ -28,7 +28,8 @@ function event_size(event) { case EVENT.USER_JOINED: case EVENT.LEAVE: - case EVENT.CLEAR: { + case EVENT.CLEAR: + case EVENT.LIFT: { break; } diff --git a/server/serializer.js b/server/serializer.js index ed59b0d..3885afb 100644 --- a/server/serializer.js +++ b/server/serializer.js @@ -81,7 +81,8 @@ export function event(s, event) { case EVENT.USER_JOINED: case EVENT.LEAVE: - case EVENT.CLEAR: { + case EVENT.CLEAR: + case EVENT.LIFT: { break; }