diff --git a/src/components/App.tsx b/src/components/App.tsx index 19c43abf..7571d4c0 100644 --- a/src/components/App.tsx +++ b/src/components/App.tsx @@ -42,6 +42,7 @@ import { SOCKET_SERVER, SocketUpdateDataSource, exportCanvas, + createNameFromSocketId, } from "../data"; import { restore } from "../data/restore"; @@ -360,13 +361,15 @@ export class App extends React.Component { } break; case "MOUSE_LOCATION": - const { socketID, pointerCoords } = decryptedData.payload; + const { + socketID, + pointerCoords, + username, + } = decryptedData.payload; this.setState(state => { - if (!state.collaborators.has(socketID)) { - state.collaborators.set(socketID, {}); - } - const user = state.collaborators.get(socketID)!; + const user = state.collaborators.get(socketID) || {}; user.pointer = pointerCoords; + user.username = username; state.collaborators.set(socketID, user); return state; }); @@ -411,6 +414,7 @@ export class App extends React.Component { payload: { socketID: this.socket.id, pointerCoords: payload.pointerCoords, + username: createNameFromSocketId(this.socket.id), }, }; return this._broadcastSocketData( @@ -2282,6 +2286,7 @@ export class App extends React.Component { const pointerViewportCoords: { [id: string]: { x: number; y: number }; } = {}; + const pointerUsernames: { [id: string]: string } = {}; this.state.collaborators.forEach((user, socketID) => { if (!user.pointer) { return; @@ -2295,6 +2300,9 @@ export class App extends React.Component { this.canvas, window.devicePixelRatio, ); + if (user.username) { + pointerUsernames[socketID] = user.username; + } }); const { atLeastOneVisibleElement, scrollBars } = renderScene( globalSceneState.getAllElements(), @@ -2309,6 +2317,7 @@ export class App extends React.Component { viewBackgroundColor: this.state.viewBackgroundColor, zoom: this.state.zoom, remotePointerViewportCoords: pointerViewportCoords, + remotePointerUsernames: pointerUsernames, }, { renderOptimizations: true, diff --git a/src/data/index.ts b/src/data/index.ts index b4b5ce0e..fa794936 100644 --- a/src/data/index.ts +++ b/src/data/index.ts @@ -43,6 +43,7 @@ export type SocketUpdateDataSource = { payload: { socketID: string; pointerCoords: { x: number; y: number }; + username: string; }; }; }; @@ -359,3 +360,93 @@ export async function loadScene(id: string | null, privateKey?: string) { appState: data.appState && { ...data.appState }, }; } + +const ADJ = [ + "aggressive", + "agreeable", + "ambitious", + "brave", + "calm", + "delightful", + "eager", + "faithful", + "gentle", + "happy", + "jolly", + "kind", + "lively", + "nice", + "obedient", + "polite", + "proud", + "silly", + "thankful", + "victorious", + "witty", + "wonderful", + "zealous", +]; + +const NOUN = [ + "alligator", + "ant", + "bear", + "bee", + "bird", + "camel", + "cat", + "cheetah", + "chicken", + "chimpanzee", + "cow", + "crocodile", + "deer", + "dog", + "dolphin", + "duck", + "eagle", + "elephant", + "fish", + "fly", + "fox", + "frog", + "giraffe", + "goat", + "goldfish", + "hamster", + "hippopotamus", + "horse", + "kangaroo", + "kitten", + "lion", + "lobster", + "monkey", + "octopus", + "owl", + "panda", + "pig", + "puppy", + "rabbit", + "rat", + "scorpion", + "seal", + "shark", + "sheep", + "snail", + "snake", + "spider", + "squirrel", + "tiger", + "turtle", + "wolf", + "zebra", +]; + +export function createNameFromSocketId(socketId: string) { + const buf = new Buffer(socketId, "utf8"); + + return [ + ADJ[buf.readUInt32LE(0) % ADJ.length], + NOUN[buf.readUInt32LE(4) % NOUN.length], + ].join(" "); +} diff --git a/src/renderer/renderScene.ts b/src/renderer/renderScene.ts index 8426edfd..d67992be 100644 --- a/src/renderer/renderScene.ts +++ b/src/renderer/renderScene.ts @@ -167,6 +167,7 @@ export function renderScene( // Paint remote pointers for (const clientId in sceneState.remotePointerViewportCoords) { let { x, y } = sceneState.remotePointerViewportCoords[clientId]; + const username = sceneState.remotePointerUsernames[clientId]; const width = 9; const height = 14; @@ -200,6 +201,30 @@ export function renderScene( context.lineTo(x, y); context.fill(); context.stroke(); + + if (!isOutOfBounds && username) { + const offsetX = x + width; + const offsetY = y + height; + const paddingHorizontal = 4; + const paddingVertical = 4; + const measure = context.measureText(username); + const measureHeight = + measure.actualBoundingBoxDescent + measure.actualBoundingBoxAscent; + + context.fillRect( + offsetX, + offsetY, + measure.width + 2 * paddingHorizontal, + measureHeight + 2 * paddingVertical, + ); + context.fillStyle = "white"; + context.fillText( + username, + offsetX + paddingHorizontal, + offsetY + paddingVertical + measure.actualBoundingBoxAscent, + ); + } + context.strokeStyle = strokeStyle; context.fillStyle = fillStyle; context.globalAlpha = globalAlpha; diff --git a/src/scene/export.ts b/src/scene/export.ts index bdad08db..9ccff82b 100644 --- a/src/scene/export.ts +++ b/src/scene/export.ts @@ -50,6 +50,7 @@ export function exportToCanvas( scrollY: normalizeScroll(-minY + exportPadding), zoom: 1, remotePointerViewportCoords: {}, + remotePointerUsernames: {}, }, { renderScrollbars: false, diff --git a/src/scene/types.ts b/src/scene/types.ts index cd48c4ab..0a4c506c 100644 --- a/src/scene/types.ts +++ b/src/scene/types.ts @@ -8,6 +8,7 @@ export type SceneState = { viewBackgroundColor: string | null; zoom: number; remotePointerViewportCoords: { [id: string]: { x: number; y: number } }; + remotePointerUsernames: { [id: string]: string }; }; export type SceneScroll = { diff --git a/src/types.ts b/src/types.ts index 1cdc1e1b..c437583b 100644 --- a/src/types.ts +++ b/src/types.ts @@ -36,7 +36,10 @@ export type AppState = { openMenu: "canvas" | "shape" | null; lastPointerDownWith: PointerType; selectedElementIds: { [id: string]: boolean }; - collaborators: Map; + collaborators: Map< + string, + { pointer?: { x: number; y: number }; username?: string } + >; }; export type PointerCoords = Readonly<{