Browse Source

Keep multiple current strokes ("prestrokes") per player in a queue

sdf
A.Olokhtonov 2 months ago
parent
commit
29ec265632
  1. 8
      README.txt
  2. 3
      client/aux.js
  3. 24
      client/client_recv.js
  4. 9
      client/client_send.js
  5. 1
      client/index.js
  6. 142
      client/webgl_geometry.js
  7. 35
      client/webgl_listeners.js
  8. 3
      server/deserializer.js
  9. 1
      server/enums.js
  10. 3
      server/send.js
  11. 3
      server/serializer.js

8
README.txt

@ -16,6 +16,8 @@ Release:
+ Stroke previews get connected when drawn without panning on touch devices + Stroke previews get connected when drawn without panning on touch devices
+ Redraw HTML (cursors) on local canvas moves + Redraw HTML (cursors) on local canvas moves
+ New strokes dissapear on the HMH desk + 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 - Debug
- Restore ability to limit event range - Restore ability to limit event range
* Listeners/events/multiplayer * 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] + Investigate skipped inputs on mobile (panning, zooming) [Events were not actually getting skipped. The stroke previews were just not being drawn]
+ Smooth zoom + Smooth zoom
+ Infinite background pattern + 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 - 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 - Save events to indexeddb (as some kind of a blob), restore on reconnect and page reload
- Local prediction for tools! - Local prediction for tools!
@ -42,8 +44,8 @@ Release:
+ Dynamic svg cursor to represent the brush + Dynamic svg cursor to represent the brush
+ Eraser + Eraser
* Line drawing * Line drawing
- Live preview + Live preview
- Alignment (horizontal, vertical, diagonal, etc) ~ Alignment (horizontal, vertical, diagonal, etc) [kinda gets covered by the snapping? question mark?]
+ Undo + Undo
+ Undo for eraser + Undo for eraser
+ Undo for images (add, move, scale) + Undo for images (add, move, scale)

3
client/aux.js

@ -54,7 +54,8 @@ function event_size(event) {
case EVENT.USER_JOINED: case EVENT.USER_JOINED:
case EVENT.LEAVE: case EVENT.LEAVE:
case EVENT.CLEAR: { case EVENT.CLEAR:
case EVENT.LIFT: {
break; break;
} }

24
client/client_recv.js

@ -72,7 +72,8 @@ function des_event(d, state = null) {
case EVENT.USER_JOINED: case EVENT.USER_JOINED:
case EVENT.LEAVE: case EVENT.LEAVE:
case EVENT.CLEAR: { case EVENT.CLEAR:
case EVENT.LIFT: {
break; break;
} }
@ -187,6 +188,8 @@ function init_player_defaults(state, player_id, color = config.default_color, wi
'points': [], 'points': [],
'online': false, 'online': false,
'cursor': {'x': 0, 'y': 0}, 'cursor': {'x': 0, 'y': 0},
'strokes': [],
'current_prestroke': false,
}; };
} }
@ -207,13 +210,25 @@ function handle_event(state, context, event, options = {}) {
} }
case EVENT.PREDRAW: { 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; need_draw = true;
break; break;
} }
case EVENT.CLEAR: { 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; break;
} }
@ -336,12 +351,11 @@ function handle_event(state, context, event, options = {}) {
delete event.coords; delete event.coords;
delete event.press; 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; need_draw = true;
event.index = state.events.length; 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); geometry_add_stroke(state, context, event, state.events.length, options.skip_bvh === true);
state.stroke_count++; state.stroke_count++;

9
client/client_send.js

@ -85,7 +85,8 @@ function ser_event(s, event) {
break; break;
} }
case EVENT.CLEAR: { case EVENT.CLEAR:
case EVENT.LIFT: {
break; break;
} }
@ -328,6 +329,12 @@ function predraw_event(x, y) {
}; };
} }
function lift_event() {
return {
'type': EVENT.LIFT,
};
}
function color_event(color_u32) { function color_event(color_u32) {
return { return {
'type': EVENT.SET_COLOR, 'type': EVENT.SET_COLOR,

1
client/index.js

@ -44,6 +44,7 @@ const EVENT = Object.freeze({
SET_WIDTH: 12, SET_WIDTH: 12,
CLEAR: 13, // clear predraw events from me (because I started a pan instead of drawing) CLEAR: 13, // clear predraw events from me (because I started a pan instead of drawing)
MOVE_CURSOR: 14, MOVE_CURSOR: 14,
LIFT: 15,
LEAVE: 16, LEAVE: 16,
MOVE_CANVAS: 17, MOVE_CANVAS: 17,

142
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) { function geometry_prepare_stroke(state) {
if (!state.online) { if (!state.online) {
return null; 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; return null;
} }
const points = process_stroke2(state.canvas.zoom, state.players[state.me].points); const points = process_stroke2(state.canvas.zoom, stroke.points);
return { return {
'color': state.players[state.me].color, 'color': stroke.color,
'width': state.players[state.me].width, 'width': stroke.width,
'points': points, 'points': points,
'user_id': state.me, 'user_id': state.me,
}; };
} }
async function geometry_write_instances(state, context, callback) { async function geometry_write_instances(state, context, callback) {
state.stats.rdp_max_count = 0; state.stats.rdp_max_count = 0;
state.stats.rdp_segments = 0; state.stats.rdp_segments = 0;
@ -56,6 +39,7 @@ function geometry_add_dummy_stroke(context) {
ser_u16(context.stroke_data, 0); ser_u16(context.stroke_data, 0);
} }
// Real stroke, add forever
function geometry_add_stroke(state, context, stroke, stroke_index, skip_bvh = false) { 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; 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) { for (const player_id in state.players) {
const player = state.players[player_id]; const player = state.players[player_id];
if (player.points.length > 0) { for (const stroke of player.strokes) {
total_points += player.points.length; if (!stroke.empty && stroke.points.length > 0) {
total_strokes += 1; 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 // player has the same data as their current stroke: points, color, width
const player = state.players[player_id]; const player = state.players[player_id];
for (let i = 0; i < player.points.length; ++i) { for (const stroke of player.strokes) {
const p = player.points[i]; if (!stroke.empty && stroke.points.length > 0) {
for (let i = 0; i < stroke.points.length; ++i) {
tv_add(context.dynamic_instance_points, p.x); const p = stroke.points[i];
tv_add(context.dynamic_instance_points, p.y);
tv_add(context.dynamic_instance_pressure, p.pressure); tv_add(context.dynamic_instance_points, p.x);
tv_add(context.dynamic_instance_points, p.y);
if (i !== player.points.length - 1) { tv_add(context.dynamic_instance_pressure, p.pressure);
tv_add(context.dynamic_instance_ids, stroke_index);
} else { if (i !== stroke.points.length - 1) {
tv_add(context.dynamic_instance_ids, stroke_index | (1 << 31)); 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_segment_count = total_points;
context.dynamic_stroke_count = total_strokes; 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; if (!state.online) return;
const player = state.players[player_id]; 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) { if (point.pressure < config.min_pressure) {
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) { if (points.length > 0 && !raw) {
// pulled from "perfect-freehand" package. MIT // pulled from "perfect-freehand" package. MIT
// https://github.com/steveruizok/perfect-freehand/ // https://github.com/steveruizok/perfect-freehand/
const streamline = 0.5; const streamline = 0.75;
const t = 0.15 + (1 - streamline) * 0.85 const t = 0.15 + (1 - streamline) * 0.85
const smooth_pressure = exponential_smoothing(points, point, 3); const smooth_pressure = exponential_smoothing(points, point, 3);
points.push({ points.push({
'x': player.dynamic_head.x * t + point.x * (1 - t), 'x': stroke.head.x * t + point.x * (1 - t),
'y': player.dynamic_head.y * t + point.y * (1 - t), 'y': stroke.head.y * t + point.y * (1 - t),
'pressure': is_pen ? player.dynamic_head.pressure * t + smooth_pressure * (1 - t) : point.pressure, 'pressure': is_pen ? stroke.head.pressure * t + smooth_pressure * (1 - t) : point.pressure,
}); });
if (is_pen) { if (is_pen) {
point.pressure = smooth_pressure; point.pressure = smooth_pressure;
} }
} else { } else {
state.players[player_id].points.push(point); points.push(point);
} }
stroke.head = point;
recompute_dynamic_data(state, context); 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; 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); recompute_dynamic_data(state, context);
} }

35
client/webgl_listeners.js

@ -218,8 +218,8 @@ function mousedown(e, state, context) {
if (state.tools.active === 'pencil') { if (state.tools.active === 'pencil') {
canvasp.pressure = Math.ceil(e.pressure * 255); canvasp.pressure = Math.ceil(e.pressure * 255);
geometry_clear_player(state, context, state.me); geometry_start_prestroke(state, state.me);
geometry_add_point(state, context, state.me, canvasp); geometry_add_prepoint(state, context, state.me, canvasp, e.pointerType === "pen");
state.drawing = true; state.drawing = true;
state.active_image = null; state.active_image = null;
@ -228,6 +228,7 @@ function mousedown(e, state, context) {
} else if (state.tools.active === 'ruler') { } else if (state.tools.active === 'ruler') {
state.linedrawing = true; state.linedrawing = true;
state.ruler_origin = canvasp; state.ruler_origin = canvasp;
geometry_start_prestroke(state, state.me);
} else if (state.tools.active === 'eraser') { } else if (state.tools.active === 'eraser') {
state.erasing = true; state.erasing = true;
} else if (state.tools.active === 'pointer') { } else if (state.tools.active === 'pointer') {
@ -408,7 +409,7 @@ function mousemove(e, state, context) {
if (state.drawing) { if (state.drawing) {
canvasp.pressure = Math.ceil(e.pressure * 255); 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)); fire_event(state, predraw_event(canvasp.x, canvasp.y));
do_draw = true; do_draw = true;
} }
@ -446,16 +447,21 @@ function mousemove(e, state, context) {
} }
if (state.linedrawing) { 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 p1 = {'x': state.ruler_origin.x, 'y': state.ruler_origin.y, 'pressure': 128};
const p2 = {'x': canvasp.x, 'y': canvasp.y, 'pressure': 128}; const p2 = {'x': canvasp.x, 'y': canvasp.y, 'pressure': 128};
geometry_add_point(state, context, state.me, p1, false, true); if (state.online) {
geometry_add_point(state, context, state.me, p2, false, true); 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) { if (do_draw) {
@ -520,14 +526,11 @@ function mouseup(e, state, context) {
if (stroke) { if (stroke) {
// TODO: be able to add a baked stroke locally // TODO: be able to add a baked stroke locally
//geometry_add_stroke(state, context, stroke, 0);
queue_event(state, stroke_event(state)); queue_event(state, stroke_event(state));
//geometry_clear_player(state, context, state.me);
schedule_draw(state, context); schedule_draw(state, context);
} }
fire_event(state, lift_event());
state.drawing = false; state.drawing = false;
return; return;
@ -620,7 +623,7 @@ function wheel(e, state, context) {
function start_move(e, state, context) { function start_move(e, state, context) {
// two touch identifiers are expected to be pushed into state.touch.ids at this point // 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 fire_event(state, clear_event(state)); // Tell others to hide predraws of this stroke
for (const touch of e.touches) { 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 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)); fire_event(state, predraw_event(canvasp.x, canvasp.y));
schedule_draw(state, context); schedule_draw(state, context);
@ -779,7 +783,6 @@ function touchend(e, state, context) {
if (stroke) { if (stroke) {
queue_event(state, stroke_event(state)); queue_event(state, stroke_event(state));
//geometry_clear_player(state, context, state.me);
schedule_draw(state, context); schedule_draw(state, context);
} }

3
server/deserializer.js

@ -88,7 +88,8 @@ export function event(d) {
break; break;
} }
case EVENT.CLEAR: { case EVENT.CLEAR:
case EVENT.LIFT: {
break; break;
} }

1
server/enums.js

@ -10,6 +10,7 @@ export const EVENT = Object.freeze({
SET_WIDTH: 12, SET_WIDTH: 12,
CLEAR: 13, CLEAR: 13,
MOVE_CURSOR: 14, MOVE_CURSOR: 14,
LIFT: 15,
LEAVE: 16, LEAVE: 16,
MOVE_CANVAS: 17, MOVE_CANVAS: 17,

3
server/send.js

@ -28,7 +28,8 @@ function event_size(event) {
case EVENT.USER_JOINED: case EVENT.USER_JOINED:
case EVENT.LEAVE: case EVENT.LEAVE:
case EVENT.CLEAR: { case EVENT.CLEAR:
case EVENT.LIFT: {
break; break;
} }

3
server/serializer.js

@ -81,7 +81,8 @@ export function event(s, event) {
case EVENT.USER_JOINED: case EVENT.USER_JOINED:
case EVENT.LEAVE: case EVENT.LEAVE:
case EVENT.CLEAR: { case EVENT.CLEAR:
case EVENT.LIFT: {
break; break;
} }

Loading…
Cancel
Save