diff --git a/README.md b/README.md index aa482a4..6fad53b 100644 --- a/README.md +++ b/README.md @@ -20,8 +20,9 @@ Release: - Be able to have multiple "current" strokes per player. In case of bad internet this can happen! * Missing features I do not consider bonus + Player pointers - - Player list - - Follow player + + Pretty player pointers + + Player list + + Follow player - Eraser - Line drawing - Follow player (like Ligma) diff --git a/client/aux.js b/client/aux.js index 7b81a18..39acf24 100644 --- a/client/aux.js +++ b/client/aux.js @@ -41,7 +41,12 @@ function event_size(event) { size += 4 * 2; break; } - + + case EVENT.MOVE_CANVAS: { + size += 4 * 2 + 4; + break; + } + case EVENT.LEAVE: case EVENT.CLEAR: { break; @@ -161,12 +166,45 @@ function HTML(html) { 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) { const color = random_bright_color_from_seed(parseInt(player_id)); const path_copy = state.cursor_path.cloneNode(); + path_copy.style.fill = color; + const cursor = HTML(`${path_copy.outerHTML}`); + const player = HTML(`
`); + + player.style.background = color; + + player.addEventListener('click', () => { + toggle_follow_player(state, player_id); + }); + document.querySelector('.html-hud').appendChild(cursor); + document.querySelector('.player-list').appendChild(player); + + document.querySelector('.player-list').classList.remove('vhide'); + return cursor; } diff --git a/client/client_recv.js b/client/client_recv.js index 5aed22e..f0e4d4c 100644 --- a/client/client_recv.js +++ b/client/client_recv.js @@ -26,6 +26,12 @@ function des_u32(d) { return value; } +function des_s32(d) { + const value = d.view.getInt32(d.offset, true); + d.offset += 4; + return value; +} + function des_f32(d) { const value = d.view.getFloat32(d.offset, true); d.offset += 4; @@ -69,6 +75,13 @@ function des_event(d, state = null) { 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: { event.color = des_u32(d); break; @@ -191,6 +204,19 @@ function handle_event(state, context, event, options = {}) { 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: { state.players[event.user_id].color = event.color; break; diff --git a/client/client_send.js b/client/client_send.js index e7525d8..8780d45 100644 --- a/client/client_send.js +++ b/client/client_send.js @@ -85,6 +85,13 @@ function ser_event(s, event) { 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: { ser_u32(s, event.color); 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 (config.debug_print) console.debug('socket has closed, stopping SYNs'); return; @@ -326,3 +349,12 @@ function movecursor_event(x, 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, + }; +} diff --git a/client/default.css b/client/default.css index bd9507b..5cf06a9 100644 --- a/client/default.css +++ b/client/default.css @@ -27,6 +27,10 @@ body.offline .main { display: none !important; } +.vhide { + visibility: hidden !important; +} + .flexcol { display: flex; flex-direction: column; @@ -79,7 +83,7 @@ canvas.mousemoving { } .pallete-wrapper, -.sizer-wrapper { +.top-wrapper { position: fixed; top: 0; left: 0; @@ -91,12 +95,47 @@ canvas.mousemoving { transition: var(--transform-amimate); } -.sizer-wrapper { +.top-wrapper { height: unset; width: 100%; 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 { pointer-events: all; display: grid; @@ -114,7 +153,7 @@ canvas.mousemoving { 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%); } diff --git a/client/icons/player.svg b/client/icons/player.svg new file mode 100644 index 0000000..01839ed --- /dev/null +++ b/client/icons/player.svg @@ -0,0 +1,57 @@ + + + + + + + + + + + + + diff --git a/client/index.html b/client/index.html index 2723500..e02d380 100644 --- a/client/index.html +++ b/client/index.html @@ -7,22 +7,22 @@ - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + +
@@ -52,11 +52,13 @@
-
+
+
+
diff --git a/client/index.js b/client/index.js index f4a6633..73242ea 100644 --- a/client/index.js +++ b/client/index.js @@ -39,6 +39,7 @@ const EVENT = Object.freeze({ MOVE_CURSOR: 14, MOVE_SCREEN: 15, LEAVE: 16, + MOVE_CANVAS: 17, STROKE: 20, RULER: 21, // gets re-written with EVENT.STROKE before sending to server @@ -59,6 +60,7 @@ const MESSAGE = Object.freeze({ FULL: 103, FIRE: 104, JOIN: 105, + FOLLOW: 106, }); // Source: @@ -223,6 +225,7 @@ function main() { 'rdp_cache': {}, 'stats': {}, + 'following_player': null, }; const context = { diff --git a/client/math.js b/client/math.js index 95b8d97..e5db202 100644 --- a/client/math.js +++ b/client/math.js @@ -11,8 +11,8 @@ function screen_to_canvas(state, p) { } function canvas_to_screen(state, p) { - const xs = p.x * state.canvas.zoom + state.canvas.offset.x; - const ys = p.y * state.canvas.zoom + state.canvas.offset.y; + const xs = (p.x * state.canvas.zoom + state.canvas.offset.x) / window.devicePixelRatio; + const ys = (p.y * state.canvas.zoom + state.canvas.offset.y) / window.devicePixelRatio; return {'x': xs, 'y': ys}; } @@ -352,7 +352,7 @@ function mulberry32(seed) { function random_bright_color_from_seed(seed) { const h = Math.round(mulberry32(seed) * 360); - const s = 75; + const s = 25; const l = 50; return `hsl(${h}deg ${s}% ${l}%)`; diff --git a/client/webgl_draw.js b/client/webgl_draw.js index 0f41335..9b4a547 100644 --- a/client/webgl_draw.js +++ b/client/webgl_draw.js @@ -59,6 +59,11 @@ function draw_html(state) { if (!player.online && player_cursor_element !== null) { 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) { diff --git a/client/webgl_listeners.js b/client/webgl_listeners.js index 6070649..c41051a 100644 --- a/client/webgl_listeners.js +++ b/client/webgl_listeners.js @@ -99,7 +99,7 @@ function cancel(e) { function zenmode() { 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) { @@ -228,6 +228,13 @@ function mousemove(e, state, context) { if (state.moving) { state.canvas.offset.x += e.movementX; 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; } @@ -333,6 +340,13 @@ function wheel(e, state, context) { 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_y = Math.round((dz * old_zoom) * canvasp.y); @@ -420,6 +434,7 @@ function touchmove(e, state, context) { state.touch.waiting_for_second_finger = false; } + geometry_add_point(state, context, state.me, canvasp); 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; } + // 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.second_finger_position = second_finger_position; + fire_event(state, movecanvas_event(state)); + schedule_draw(state, context); return; @@ -492,9 +514,6 @@ function touchend(e, state, context) { for (const touch of e.changedTouches) { if (state.touch.drawing) { if (state.touch.ids[0] == touch.identifier) { - // const event = stroke_event(); - // await queue_event(event); - const stroke = geometry_prepare_stroke(state); if (stroke) { diff --git a/server/deserializer.js b/server/deserializer.js index 19ada58..9e23e2b 100644 --- a/server/deserializer.js +++ b/server/deserializer.js @@ -62,6 +62,13 @@ export function event(d) { break; } + case EVENT.MOVE_CANVAS: { + event.offset_x = u32(d); + event.offset_y = u32(d); + event.zoom = f32(d); + break; + } + case EVENT.CLEAR: { break; } diff --git a/server/enums.js b/server/enums.js index fbd7e2e..121d98f 100644 --- a/server/enums.js +++ b/server/enums.js @@ -12,6 +12,7 @@ export const EVENT = Object.freeze({ MOVE_CURSOR: 14, MOVE_SCREEN: 15, LEAVE: 16, + MOVE_CANVAS: 17, STROKE: 20, UNDO: 30, @@ -28,4 +29,5 @@ export const MESSAGE = Object.freeze({ FULL: 103, FIRE: 104, JOIN: 105, + FOLLOW: 106, }); diff --git a/server/recv.js b/server/recv.js index e1ca4b8..e9ade78 100644 --- a/server/recv.js +++ b/server/recv.js @@ -90,6 +90,20 @@ function recv_fire(d, session) { 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) { switch (event.type) { case EVENT.STROKE: { @@ -166,6 +180,11 @@ export async function handle_message(ws, d) { break; } + case MESSAGE.FOLLOW: { + recv_follow(d, session); + break; + } + default: { console.error('fuck'); console.trace(); diff --git a/server/send.js b/server/send.js index f677604..504d984 100644 --- a/server/send.js +++ b/server/send.js @@ -15,7 +15,12 @@ function event_size(event) { size += 4 * 2; break; } - + + case EVENT.MOVE_CANVAS: { + size += 4 * 2 + 4; + break; + } + case EVENT.LEAVE: case EVENT.CLEAR: { break; @@ -210,6 +215,11 @@ export function fire_event(from_session, event) { 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); } } diff --git a/server/serializer.js b/server/serializer.js index fdd27f4..936e057 100644 --- a/server/serializer.js +++ b/server/serializer.js @@ -60,6 +60,13 @@ export function event(s, event) { 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.CLEAR: { break; diff --git a/server/storage.js b/server/storage.js index 03d17a0..dd959f4 100644 --- a/server/storage.js +++ b/server/storage.js @@ -132,6 +132,7 @@ export function startup() { session.state = SESSION.CLOSED; session.ws = null; session.sync_attempts = 0; + session.follow = null; sessions[session.id] = session; } }