Browse Source

Follow and unfollow players like in Figma

ssao
A.Olokhtonov 10 months ago
parent
commit
99bd99a465
  1. 5
      README.md
  2. 40
      client/aux.js
  3. 26
      client/client_recv.js
  4. 34
      client/client_send.js
  5. 45
      client/default.css
  6. 57
      client/icons/player.svg
  7. 36
      client/index.html
  8. 3
      client/index.js
  9. 6
      client/math.js
  10. 5
      client/webgl_draw.js
  11. 27
      client/webgl_listeners.js
  12. 7
      server/deserializer.js
  13. 2
      server/enums.js
  14. 19
      server/recv.js
  15. 12
      server/send.js
  16. 7
      server/serializer.js
  17. 1
      server/storage.js

5
README.md

@ -20,8 +20,9 @@ Release:
- 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!
* Missing features I do not consider bonus * Missing features I do not consider bonus
+ Player pointers + Player pointers
- Player list + Pretty player pointers
- Follow player + Player list
+ Follow player
- Eraser - Eraser
- Line drawing - Line drawing
- Follow player (like Ligma) - Follow player (like Ligma)

40
client/aux.js

@ -41,7 +41,12 @@ function event_size(event) {
size += 4 * 2; size += 4 * 2;
break; break;
} }
case EVENT.MOVE_CANVAS: {
size += 4 * 2 + 4;
break;
}
case EVENT.LEAVE: case EVENT.LEAVE:
case EVENT.CLEAR: { case EVENT.CLEAR: {
break; break;
@ -161,12 +166,45 @@ function HTML(html) {
return template.content.firstChild; return template.content.firstChild;
} }
function toggle_follow_player(state, player_id) {
document.querySelectorAll('.player-list .player').forEach(p => p.classList.remove('following'));
if (state.following_player === null) {
state.following_player = player_id;
} else {
if (player_id === state.following_player) {
state.following_player = null;
} else {
state.following_player = player_id;
}
}
const player_element = document.querySelector(`.player-list .player[data-player-id="${state.following_player}"]`);
if (player_element) player_element.classList.add('following');
send_follow(state.following_player);
}
function insert_player_cursor(state, player_id) { function insert_player_cursor(state, player_id) {
const color = random_bright_color_from_seed(parseInt(player_id)); const color = random_bright_color_from_seed(parseInt(player_id));
const path_copy = state.cursor_path.cloneNode(); const path_copy = state.cursor_path.cloneNode();
path_copy.style.fill = color; path_copy.style.fill = color;
const cursor = HTML(`<svg viewBox="0 0 28 28" xmlns="http://www.w3.org/2000/svg" class="player-cursor" data-player-id="${player_id}">${path_copy.outerHTML}</svg>`); const cursor = HTML(`<svg viewBox="0 0 28 28" xmlns="http://www.w3.org/2000/svg" class="player-cursor" data-player-id="${player_id}">${path_copy.outerHTML}</svg>`);
const player = HTML(`<div class="player" data-player-id="${player_id}"><img src="icons/player.svg"></div>`);
player.style.background = color;
player.addEventListener('click', () => {
toggle_follow_player(state, player_id);
});
document.querySelector('.html-hud').appendChild(cursor); document.querySelector('.html-hud').appendChild(cursor);
document.querySelector('.player-list').appendChild(player);
document.querySelector('.player-list').classList.remove('vhide');
return cursor; return cursor;
} }

26
client/client_recv.js

@ -26,6 +26,12 @@ function des_u32(d) {
return value; return value;
} }
function des_s32(d) {
const value = d.view.getInt32(d.offset, true);
d.offset += 4;
return value;
}
function des_f32(d) { function des_f32(d) {
const value = d.view.getFloat32(d.offset, true); const value = d.view.getFloat32(d.offset, true);
d.offset += 4; d.offset += 4;
@ -69,6 +75,13 @@ function des_event(d, state = null) {
break; break;
} }
case EVENT.MOVE_CANVAS: {
event.offset_x = des_s32(d);
event.offset_y = des_s32(d);
event.zoom = des_f32(d);
break;
}
case EVENT.SET_COLOR: { case EVENT.SET_COLOR: {
event.color = des_u32(d); event.color = des_u32(d);
break; break;
@ -191,6 +204,19 @@ function handle_event(state, context, event, options = {}) {
break; break;
} }
case EVENT.MOVE_CANVAS: {
// Double-check just in case
// Non-triple equals in on purpose
if (event.user_id == state.following_player) {
state.canvas.offset.x = event.offset_x;
state.canvas.offset.y = event.offset_y;
state.canvas.zoom = event.zoom;
need_draw = true;
}
break;
}
case EVENT.SET_COLOR: { case EVENT.SET_COLOR: {
state.players[event.user_id].color = event.color; state.players[event.user_id].color = event.color;
break; break;

34
client/client_send.js

@ -85,6 +85,13 @@ function ser_event(s, event) {
break; break;
} }
case EVENT.MOVE_CANVAS: {
ser_u32(s, event.offset_x);
ser_u32(s, event.offset_y);
ser_f32(s, event.zoom);
break;
}
case EVENT.SET_COLOR: { case EVENT.SET_COLOR: {
ser_u32(s, event.color); ser_u32(s, event.color);
break; break;
@ -150,8 +157,24 @@ async function send_ack(sn) {
} }
} }
async function sync_queue(state) { async function send_follow(player_id) {
const s = serializer_create(4 + 4);
player_id = player_id === null ? -1 : player_id;
ser_u32(s, MESSAGE.FOLLOW);
ser_u32(s, player_id);
if (config.debug_print) console.debug(`follow ${player_id} out`);
try {
if (ws) await ws.send(s.buffer);
} catch (e) {
ws.close();
}
}
async function sync_queue(state) {
if (ws === null) { if (ws === null) {
if (config.debug_print) console.debug('socket has closed, stopping SYNs'); if (config.debug_print) console.debug('socket has closed, stopping SYNs');
return; return;
@ -326,3 +349,12 @@ function movecursor_event(x, y) {
'y': y, 'y': y,
}; };
} }
function movecanvas_event(state) {
return {
'type': EVENT.MOVE_CANVAS,
'offset_x': state.canvas.offset.x,
'offset_y': state.canvas.offset.y,
'zoom': state.canvas.zoom,
};
}

45
client/default.css

@ -27,6 +27,10 @@ body.offline .main {
display: none !important; display: none !important;
} }
.vhide {
visibility: hidden !important;
}
.flexcol { .flexcol {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
@ -79,7 +83,7 @@ canvas.mousemoving {
} }
.pallete-wrapper, .pallete-wrapper,
.sizer-wrapper { .top-wrapper {
position: fixed; position: fixed;
top: 0; top: 0;
left: 0; left: 0;
@ -91,12 +95,47 @@ canvas.mousemoving {
transition: var(--transform-amimate); transition: var(--transform-amimate);
} }
.sizer-wrapper { .top-wrapper {
height: unset; height: unset;
width: 100%; width: 100%;
flex-direction: row; flex-direction: row;
} }
.player-list {
position: absolute;
right: 0;
height: 42px;
padding-left: var(--gap);
padding-right: var(--gap);
background: var(--dark-blue);
display: flex;
gap: var(--gap);
align-items: center;
border-bottom-left-radius: var(--radius);
pointer-events: all;
}
.player-list .player {
width: 24px;
height: 24px;
border-radius: var(--radius);
display: flex;
justify-content: center;
align-items: center;
border: 2px solid transparent;
box-sizing: border-box;
}
.player-list .player.following {
border-color: white;
}
.player-list .player img {
height: 12px;
width: 12px;
filter: invert(100%);
}
.pallete { .pallete {
pointer-events: all; pointer-events: all;
display: grid; display: grid;
@ -114,7 +153,7 @@ canvas.mousemoving {
transform: translateX(-125%); /* to account for a selected color, which is also offset to the right */ transform: translateX(-125%); /* to account for a selected color, which is also offset to the right */
} }
.sizer-wrapper.hidden { .top-wrapper.hidden {
transform: translateY(-125%); transform: translateY(-125%);
} }

57
client/icons/player.svg

@ -0,0 +1,57 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
width="16"
height="16"
viewBox="0 0 16 16"
version="1.1"
id="svg5"
sodipodi:docname="player.svg"
inkscape:version="1.2.2 (b0a8486541, 2022-12-01)"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg">
<sodipodi:namedview
id="namedview7"
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1.0"
inkscape:showpageshadow="2"
inkscape:pageopacity="0.0"
inkscape:pagecheckerboard="0"
inkscape:deskcolor="#d1d1d1"
inkscape:document-units="px"
showgrid="true"
inkscape:zoom="53.821773"
inkscape:cx="10.070274"
inkscape:cy="8.3237689"
inkscape:window-width="2558"
inkscape:window-height="1412"
inkscape:window-x="0"
inkscape:window-y="0"
inkscape:window-maximized="0"
inkscape:current-layer="layer1">
<inkscape:grid
type="xygrid"
id="grid132" />
</sodipodi:namedview>
<defs
id="defs2" />
<g
inkscape:label="Layer 1"
inkscape:groupmode="layer"
id="layer1">
<path
id="path186"
style="fill:#000000;fill-opacity:1;stroke:none;stroke-width:2;stroke-linecap:round;stroke-linejoin:round;paint-order:stroke fill markers"
d="M 11.03125 8.609375 A 4 4 0 0 1 8 10 A 4 4 0 0 1 4.9746094 8.6152344 A 8 8 0 0 0 0 16 L 8 16 L 16 16 A 8 8 0 0 0 11.03125 8.609375 z " />
<circle
style="fill:#000000;fill-opacity:1;stroke:none;stroke-width:2;stroke-linecap:round;stroke-linejoin:round;stroke-dasharray:none;stroke-opacity:1;paint-order:stroke fill markers"
id="circle1073"
cx="8"
cy="4"
r="4" />
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.9 KiB

36
client/index.html

@ -7,22 +7,22 @@
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no"> <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
<link rel="shortcut icon" href="icons/favicon.svg" id="favicon"> <link rel="shortcut icon" href="icons/favicon.svg" id="favicon">
<link rel="stylesheet" type="text/css" href="default.css?v=67"> <link rel="stylesheet" type="text/css" href="default.css?v=68">
<script type="text/javascript" src="aux.js?v=67"></script> <script type="text/javascript" src="aux.js?v=68"></script>
<script type="text/javascript" src="heapify.js?v=67"></script> <script type="text/javascript" src="heapify.js?v=68"></script>
<script type="text/javascript" src="bvh.js?v=67"></script> <script type="text/javascript" src="bvh.js?v=68"></script>
<script type="text/javascript" src="math.js?v=67"></script> <script type="text/javascript" src="math.js?v=68"></script>
<script type="text/javascript" src="tools.js?v=67"></script> <script type="text/javascript" src="tools.js?v=68"></script>
<script type="text/javascript" src="webgl_geometry.js?v=67"></script> <script type="text/javascript" src="webgl_geometry.js?v=68"></script>
<script type="text/javascript" src="webgl_shaders.js?v=67"></script> <script type="text/javascript" src="webgl_shaders.js?v=68"></script>
<script type="text/javascript" src="webgl_listeners.js?v=67"></script> <script type="text/javascript" src="webgl_listeners.js?v=68"></script>
<script type="text/javascript" src="webgl_draw.js?v=67"></script> <script type="text/javascript" src="webgl_draw.js?v=68"></script>
<script type="text/javascript" src="index.js?v=67"></script> <script type="text/javascript" src="index.js?v=68"></script>
<script type="text/javascript" src="client_send.js?v=67"></script> <script type="text/javascript" src="client_send.js?v=68"></script>
<script type="text/javascript" src="client_recv.js?v=67"></script> <script type="text/javascript" src="client_recv.js?v=68"></script>
<script type="text/javascript" src="websocket.js?v=67"></script> <script type="text/javascript" src="websocket.js?v=68"></script>
</head> </head>
<body> <body>
<div class="main"> <div class="main">
@ -52,11 +52,13 @@
<button id="debug-begin-benchmark" title="Do not forget to enable recording in your browser!">Benchmark</button> <button id="debug-begin-benchmark" title="Do not forget to enable recording in your browser!">Benchmark</button>
</div> </div>
<div class="sizer-wrapper"> <div class="top-wrapper">
<div class="topleft"></div>
<div class="sizer"> <div class="sizer">
<input type="range" class="slider" id="stroke-width" min="0.01" step="0.01" max="64"> <input type="range" class="slider" id="stroke-width" min="0.01" step="0.01" max="64">
</div> </div>
<div id="stroke-preview" class="dhide"></div> <div id="stroke-preview" class="dhide"></div>
<div class="player-list vhide"></div>
</div> </div>
<div class="pallete-wrapper"> <div class="pallete-wrapper">

3
client/index.js

@ -39,6 +39,7 @@ const EVENT = Object.freeze({
MOVE_CURSOR: 14, MOVE_CURSOR: 14,
MOVE_SCREEN: 15, MOVE_SCREEN: 15,
LEAVE: 16, LEAVE: 16,
MOVE_CANVAS: 17,
STROKE: 20, STROKE: 20,
RULER: 21, // gets re-written with EVENT.STROKE before sending to server RULER: 21, // gets re-written with EVENT.STROKE before sending to server
@ -59,6 +60,7 @@ const MESSAGE = Object.freeze({
FULL: 103, FULL: 103,
FIRE: 104, FIRE: 104,
JOIN: 105, JOIN: 105,
FOLLOW: 106,
}); });
// Source: // Source:
@ -223,6 +225,7 @@ function main() {
'rdp_cache': {}, 'rdp_cache': {},
'stats': {}, 'stats': {},
'following_player': null,
}; };
const context = { const context = {

6
client/math.js

@ -11,8 +11,8 @@ function screen_to_canvas(state, p) {
} }
function canvas_to_screen(state, p) { function canvas_to_screen(state, p) {
const xs = p.x * state.canvas.zoom + state.canvas.offset.x; const xs = (p.x * state.canvas.zoom + state.canvas.offset.x) / window.devicePixelRatio;
const ys = p.y * state.canvas.zoom + state.canvas.offset.y; const ys = (p.y * state.canvas.zoom + state.canvas.offset.y) / window.devicePixelRatio;
return {'x': xs, 'y': ys}; return {'x': xs, 'y': ys};
} }
@ -352,7 +352,7 @@ function mulberry32(seed) {
function random_bright_color_from_seed(seed) { function random_bright_color_from_seed(seed) {
const h = Math.round(mulberry32(seed) * 360); const h = Math.round(mulberry32(seed) * 360);
const s = 75; const s = 25;
const l = 50; const l = 50;
return `hsl(${h}deg ${s}% ${l}%)`; return `hsl(${h}deg ${s}% ${l}%)`;

5
client/webgl_draw.js

@ -59,6 +59,11 @@ function draw_html(state) {
if (!player.online && player_cursor_element !== null) { if (!player.online && player_cursor_element !== null) {
player_cursor_element.remove(); player_cursor_element.remove();
const player_list_item = document.querySelector(`.player-list .player[data-player-id="${player_id}"]`);
if (player_list_item) player_list_item.remove();
if (document.querySelector('.player-list').childElementCount === 0) {
document.querySelector('.player-list').classList.add('vhide');
}
} }
if (player_cursor_element && player.online) { if (player_cursor_element && player.online) {

27
client/webgl_listeners.js

@ -99,7 +99,7 @@ function cancel(e) {
function zenmode() { function zenmode() {
document.querySelector('.pallete-wrapper').classList.toggle('hidden'); document.querySelector('.pallete-wrapper').classList.toggle('hidden');
document.querySelector('.sizer-wrapper').classList.toggle('hidden'); document.querySelector('.top-wrapper').classList.toggle('hidden');
} }
async function paste(e, state, context) { async function paste(e, state, context) {
@ -228,6 +228,13 @@ function mousemove(e, state, context) {
if (state.moving) { if (state.moving) {
state.canvas.offset.x += e.movementX; state.canvas.offset.x += e.movementX;
state.canvas.offset.y += e.movementY; state.canvas.offset.y += e.movementY;
// If we are moving our canvas, we don't need to follow anymore
if (state.following_player !== null) {
toggle_follow_player(state, state.following_player);
}
fire_event(state, movecanvas_event(state));
do_draw = true; do_draw = true;
} }
@ -333,6 +340,13 @@ function wheel(e, state, context) {
return; return;
} }
// If we are moving our canvas, we don't need to follow anymore
if (state.following_player !== null) {
toggle_follow_player(state, state.following_player);
}
fire_event(state, movecanvas_event(state));
const zoom_offset_x = Math.round((dz * old_zoom) * canvasp.x); const zoom_offset_x = Math.round((dz * old_zoom) * canvasp.x);
const zoom_offset_y = Math.round((dz * old_zoom) * canvasp.y); const zoom_offset_y = Math.round((dz * old_zoom) * canvasp.y);
@ -420,6 +434,7 @@ function touchmove(e, state, context) {
state.touch.waiting_for_second_finger = false; state.touch.waiting_for_second_finger = false;
} }
geometry_add_point(state, context, state.me, canvasp); 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));
@ -479,9 +494,16 @@ function touchmove(e, state, context) {
state.canvas.offset.y -= zoom_offset_y; state.canvas.offset.y -= zoom_offset_y;
} }
// If we are moving our canvas, we don't need to follow anymore
if (state.following_player !== null) {
toggle_follow_player(state, state.following_player);
}
state.touch.first_finger_position = first_finger_position; state.touch.first_finger_position = first_finger_position;
state.touch.second_finger_position = second_finger_position; state.touch.second_finger_position = second_finger_position;
fire_event(state, movecanvas_event(state));
schedule_draw(state, context); schedule_draw(state, context);
return; return;
@ -492,9 +514,6 @@ function touchend(e, state, context) {
for (const touch of e.changedTouches) { for (const touch of e.changedTouches) {
if (state.touch.drawing) { if (state.touch.drawing) {
if (state.touch.ids[0] == touch.identifier) { if (state.touch.ids[0] == touch.identifier) {
// const event = stroke_event();
// await queue_event(event);
const stroke = geometry_prepare_stroke(state); const stroke = geometry_prepare_stroke(state);
if (stroke) { if (stroke) {

7
server/deserializer.js

@ -62,6 +62,13 @@ export function event(d) {
break; break;
} }
case EVENT.MOVE_CANVAS: {
event.offset_x = u32(d);
event.offset_y = u32(d);
event.zoom = f32(d);
break;
}
case EVENT.CLEAR: { case EVENT.CLEAR: {
break; break;
} }

2
server/enums.js

@ -12,6 +12,7 @@ export const EVENT = Object.freeze({
MOVE_CURSOR: 14, MOVE_CURSOR: 14,
MOVE_SCREEN: 15, MOVE_SCREEN: 15,
LEAVE: 16, LEAVE: 16,
MOVE_CANVAS: 17,
STROKE: 20, STROKE: 20,
UNDO: 30, UNDO: 30,
@ -28,4 +29,5 @@ export const MESSAGE = Object.freeze({
FULL: 103, FULL: 103,
FIRE: 104, FIRE: 104,
JOIN: 105, JOIN: 105,
FOLLOW: 106,
}); });

19
server/recv.js

@ -90,6 +90,20 @@ function recv_fire(d, session) {
send.fire_event(session, event); send.fire_event(session, event);
} }
function recv_follow(d, session) {
const user_id = des.u32(d);
if (config.DEBUG_PRINT) console.log(`follow ${user_id} in`);
if (user_id === 4294967295) {
// unfollow
session.follow = null;
} else {
// follow
session.follow = user_id;
}
}
function handle_event(session, event) { function handle_event(session, event) {
switch (event.type) { switch (event.type) {
case EVENT.STROKE: { case EVENT.STROKE: {
@ -166,6 +180,11 @@ export async function handle_message(ws, d) {
break; break;
} }
case MESSAGE.FOLLOW: {
recv_follow(d, session);
break;
}
default: { default: {
console.error('fuck'); console.error('fuck');
console.trace(); console.trace();

12
server/send.js

@ -15,7 +15,12 @@ function event_size(event) {
size += 4 * 2; size += 4 * 2;
break; break;
} }
case EVENT.MOVE_CANVAS: {
size += 4 * 2 + 4;
break;
}
case EVENT.LEAVE: case EVENT.LEAVE:
case EVENT.CLEAR: { case EVENT.CLEAR: {
break; break;
@ -210,6 +215,11 @@ export function fire_event(from_session, event) {
continue; continue;
} }
if (event.type === EVENT.MOVE_CANVAS && other.follow !== from_session.id) {
// Do not spam canvas move events to those who don't follow us
continue;
}
send_fire(other.ws, event); send_fire(other.ws, event);
} }
} }

7
server/serializer.js

@ -60,6 +60,13 @@ export function event(s, event) {
break; break;
} }
case EVENT.MOVE_CANVAS: {
u32(s, event.offset_x);
u32(s, event.offset_y);
f32(s, event.zoom);
break;
}
case EVENT.LEAVE: case EVENT.LEAVE:
case EVENT.CLEAR: { case EVENT.CLEAR: {
break; break;

1
server/storage.js

@ -132,6 +132,7 @@ export function startup() {
session.state = SESSION.CLOSED; session.state = SESSION.CLOSED;
session.ws = null; session.ws = null;
session.sync_attempts = 0; session.sync_attempts = 0;
session.follow = null;
sessions[session.id] = session; sessions[session.id] = session;
} }
} }

Loading…
Cancel
Save