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: @@ -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: @@ -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: @@ -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)

3
client/aux.js

@ -54,7 +54,8 @@ function event_size(event) { @@ -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;
}

24
client/client_recv.js

@ -72,7 +72,8 @@ function des_event(d, state = null) { @@ -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 @@ -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 = {}) { @@ -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 = {}) { @@ -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++;

9
client/client_send.js

@ -85,7 +85,8 @@ function ser_event(s, event) { @@ -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) { @@ -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,

1
client/index.js

@ -44,6 +44,7 @@ const EVENT = Object.freeze({ @@ -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,

142
client/webgl_geometry.js

@ -1,42 +1,25 @@ @@ -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) { @@ -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) { @@ -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) { @@ -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 @@ -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);
}

35
client/webgl_listeners.js

@ -218,8 +218,8 @@ function mousedown(e, state, context) { @@ -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) { @@ -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) { @@ -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) { @@ -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) { @@ -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) { @@ -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) { @@ -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) { @@ -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);
}

3
server/deserializer.js

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

1
server/enums.js

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

3
server/send.js

@ -28,7 +28,8 @@ function event_size(event) { @@ -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;
}

3
server/serializer.js

@ -81,7 +81,8 @@ export function event(s, event) { @@ -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;
}

Loading…
Cancel
Save