diff --git a/src/actions/actionNavigate.tsx b/src/actions/actionNavigate.tsx index 1b808e4a..126e547a 100644 --- a/src/actions/actionNavigate.tsx +++ b/src/actions/actionNavigate.tsx @@ -1,4 +1,4 @@ -import { getClientColors } from "../clients"; +import { getClientColor } from "../clients"; import { Avatar } from "../components/Avatar"; import { centerScrollOn } from "../scene/scroll"; import { Collaborator } from "../types"; @@ -31,15 +31,14 @@ export const actionGoToCollaborator = register({ commitToHistory: false, }; }, - PanelComponent: ({ appState, updateData, data }) => { + PanelComponent: ({ updateData, data }) => { const [clientId, collaborator] = data as [string, Collaborator]; - const { background, stroke } = getClientColors(clientId, appState); + const background = getClientColor(clientId); return ( updateData(collaborator.pointer)} name={collaborator.username || ""} src={collaborator.avatarUrl} diff --git a/src/clients.ts b/src/clients.ts index 604936e3..35409891 100644 --- a/src/clients.ts +++ b/src/clients.ts @@ -1,31 +1,31 @@ -import { - DEFAULT_ELEMENT_BACKGROUND_COLOR_INDEX, - DEFAULT_ELEMENT_STROKE_COLOR_INDEX, - getAllColorsSpecificShade, -} from "./colors"; -import { AppState } from "./types"; - -const BG_COLORS = getAllColorsSpecificShade( - DEFAULT_ELEMENT_BACKGROUND_COLOR_INDEX, -); -const STROKE_COLORS = getAllColorsSpecificShade( - DEFAULT_ELEMENT_STROKE_COLOR_INDEX, -); - -export const getClientColors = (clientId: string, appState: AppState) => { - if (appState?.collaborators) { - const currentUser = appState.collaborators.get(clientId); - if (currentUser?.color) { - return currentUser.color; - } +function hashToInteger(id: string) { + let hash = 0; + if (id.length === 0) { + return hash; } - // Naive way of getting an integer out of the clientId - const sum = clientId.split("").reduce((a, str) => a + str.charCodeAt(0), 0); + for (let i = 0; i < id.length; i++) { + const char = id.charCodeAt(i); + hash = (hash << 5) - hash + char; + } + return hash; +} - return { - background: BG_COLORS[sum % BG_COLORS.length], - stroke: STROKE_COLORS[sum % STROKE_COLORS.length], - }; +export const getClientColor = ( + /** + * any uniquely identifying key, such as user id or socket id + */ + id: string, +) => { + // to get more even distribution in case `id` is not uniformly distributed to + // begin with, we hash it + const hash = Math.abs(hashToInteger(id)); + // we want to get a multiple of 10 number in the range of 0-360 (in other + // words a hue value of step size 10). There are 37 such values including 0. + const hue = (hash % 37) * 10; + const saturation = 100; + const lightness = 83; + + return `hsl(${hue}, ${saturation}%, ${lightness}%)`; }; /** diff --git a/src/components/Avatar.scss b/src/components/Avatar.scss index 86b9c423..c0c66f0a 100644 --- a/src/components/Avatar.scss +++ b/src/components/Avatar.scss @@ -10,10 +10,9 @@ display: flex; justify-content: center; align-items: center; - color: $oc-white; cursor: pointer; - font-size: 0.625rem; - font-weight: 500; + font-size: 0.75rem; + font-weight: 800; line-height: 1; &-img { diff --git a/src/components/Avatar.tsx b/src/components/Avatar.tsx index 20dc7b9f..8b4624b7 100644 --- a/src/components/Avatar.tsx +++ b/src/components/Avatar.tsx @@ -6,7 +6,6 @@ import { getNameInitial } from "../clients"; type AvatarProps = { onClick: (e: React.MouseEvent) => void; color: string; - border: string; name: string; src?: string; }; diff --git a/src/renderer/renderScene.ts b/src/renderer/renderScene.ts index c8b64b47..53c4f387 100644 --- a/src/renderer/renderScene.ts +++ b/src/renderer/renderScene.ts @@ -30,7 +30,7 @@ import { import { getSelectedElements } from "../scene/selection"; import { renderElement, renderElementToSvg } from "./renderElement"; -import { getClientColors } from "../clients"; +import { getClientColor } from "../clients"; import { LinearElementEditor } from "../element/linearElementEditor"; import { isSelectedViaGroup, @@ -48,11 +48,7 @@ import { TransformHandles, TransformHandleType, } from "../element/transformHandles"; -import { - viewportCoordsToSceneCoords, - supportsEmoji, - throttleRAF, -} from "../utils"; +import { viewportCoordsToSceneCoords, throttleRAF } from "../utils"; import { UserIdleState } from "../types"; import { THEME_FILTER } from "../constants"; import { @@ -61,7 +57,6 @@ import { } from "../element/Hyperlink"; import { isLinearElement } from "../element/typeChecks"; -const hasEmojiSupport = supportsEmoji(); export const DEFAULT_SPACING = 2; const strokeRectWithRotation = ( @@ -159,7 +154,6 @@ const strokeGrid = ( const renderSingleLinearPoint = ( context: CanvasRenderingContext2D, - appState: AppState, renderConfig: RenderConfig, point: Point, radius: number, @@ -206,14 +200,7 @@ const renderLinearPointHandles = ( const isSelected = !!appState.editingLinearElement?.selectedPointsIndices?.includes(idx); - renderSingleLinearPoint( - context, - appState, - renderConfig, - point, - radius, - isSelected, - ); + renderSingleLinearPoint(context, renderConfig, point, radius, isSelected); }); //Rendering segment mid points @@ -237,7 +224,6 @@ const renderLinearPointHandles = ( if (appState.editingLinearElement) { renderSingleLinearPoint( context, - appState, renderConfig, segmentMidPoint, radius, @@ -248,7 +234,6 @@ const renderLinearPointHandles = ( highlightPoint(segmentMidPoint, context, renderConfig); renderSingleLinearPoint( context, - appState, renderConfig, segmentMidPoint, radius, @@ -258,7 +243,6 @@ const renderLinearPointHandles = ( } else if (appState.editingLinearElement || points.length === 2) { renderSingleLinearPoint( context, - appState, renderConfig, segmentMidPoint, POINT_HANDLE_SIZE / 2, @@ -527,7 +511,7 @@ export const _renderScene = ({ selectionColors.push( ...renderConfig.remoteSelectedElementIds[element.id].map( (socketId) => { - const { background } = getClientColors(socketId, appState); + const background = getClientColor(socketId); return background; }, ), @@ -647,7 +631,7 @@ export const _renderScene = ({ x -= appState.offsetLeft; y -= appState.offsetTop; - const width = 9; + const width = 11; const height = 14; const isOutOfBounds = @@ -661,15 +645,20 @@ export const _renderScene = ({ y = Math.max(y, 0); y = Math.min(y, normalizedCanvasHeight - height); - const { background, stroke } = getClientColors(clientId, appState); + const background = getClientColor(clientId); context.save(); - context.strokeStyle = stroke; + context.strokeStyle = background; context.fillStyle = background; const userState = renderConfig.remotePointerUserStates[clientId]; - if (isOutOfBounds || userState === UserIdleState.AWAY) { - context.globalAlpha = 0.48; + const isInactive = + isOutOfBounds || + userState === UserIdleState.IDLE || + userState === UserIdleState.AWAY; + + if (isInactive) { + context.globalAlpha = 0.3; } if ( @@ -686,73 +675,91 @@ export const _renderScene = ({ context.beginPath(); context.arc(x, y, 15, 0, 2 * Math.PI, false); context.lineWidth = 1; - context.strokeStyle = stroke; + context.strokeStyle = background; context.stroke(); context.closePath(); } + // Background (white outline) for arrow + context.fillStyle = oc.white; + context.strokeStyle = oc.white; + context.lineWidth = 6; + context.lineJoin = "round"; context.beginPath(); context.moveTo(x, y); - context.lineTo(x + 1, y + 14); + context.lineTo(x + 0, y + 14); context.lineTo(x + 4, y + 9); - context.lineTo(x + 9, y + 10); - context.lineTo(x, y); - context.fill(); + context.lineTo(x + 11, y + 8); + context.closePath(); context.stroke(); + context.fill(); - const username = renderConfig.remotePointerUsernames[clientId]; - - let idleState = ""; - if (userState === UserIdleState.AWAY) { - idleState = hasEmojiSupport ? "⚫️" : ` (${UserIdleState.AWAY})`; - } else if (userState === UserIdleState.IDLE) { - idleState = hasEmojiSupport ? "💤" : ` (${UserIdleState.IDLE})`; + // Arrow + context.fillStyle = background; + context.strokeStyle = background; + context.lineWidth = 2; + context.lineJoin = "round"; + context.beginPath(); + if (isInactive) { + context.moveTo(x - 1, y - 1); + context.lineTo(x - 1, y + 15); + context.lineTo(x + 5, y + 10); + context.lineTo(x + 12, y + 9); + context.closePath(); + context.fill(); + } else { + context.moveTo(x, y); + context.lineTo(x + 0, y + 14); + context.lineTo(x + 4, y + 9); + context.lineTo(x + 11, y + 8); + context.closePath(); + context.fill(); + context.stroke(); } - const usernameAndIdleState = `${username || ""}${ - idleState ? ` ${idleState}` : "" - }`; + const username = renderConfig.remotePointerUsernames[clientId] || ""; - if (!isOutOfBounds && usernameAndIdleState) { - const offsetX = x + width; - const offsetY = y + height; - const paddingHorizontal = 4; - const paddingVertical = 4; - const measure = context.measureText(usernameAndIdleState); + if (!isOutOfBounds && username) { + context.font = "600 12px sans-serif"; // font has to be set before context.measureText() + + const offsetX = x + width / 2; + const offsetY = y + height + 2; + const paddingHorizontal = 5; + const paddingVertical = 3; + const measure = context.measureText(username); const measureHeight = measure.actualBoundingBoxDescent + measure.actualBoundingBoxAscent; + const finalHeight = Math.max(measureHeight, 12); const boxX = offsetX - 1; const boxY = offsetY - 1; - const boxWidth = measure.width + 2 * paddingHorizontal + 2; - const boxHeight = measureHeight + 2 * paddingVertical + 2; + const boxWidth = measure.width + 2 + paddingHorizontal * 2 + 2; + const boxHeight = finalHeight + 2 + paddingVertical * 2 + 2; if (context.roundRect) { context.beginPath(); - context.roundRect( - boxX, - boxY, - boxWidth, - boxHeight, - 4 / renderConfig.zoom.value, - ); + context.roundRect(boxX, boxY, boxWidth, boxHeight, 8); context.fillStyle = background; context.fill(); - context.fillStyle = stroke; + context.strokeStyle = oc.white; context.stroke(); } else { // Border - context.fillStyle = stroke; + context.fillStyle = oc.white; context.fillRect(boxX, boxY, boxWidth, boxHeight); // Background context.fillStyle = background; context.fillRect(offsetX, offsetY, boxWidth - 2, boxHeight - 2); } - context.fillStyle = oc.white; + context.fillStyle = oc.black; context.fillText( - usernameAndIdleState, - offsetX + paddingHorizontal, - offsetY + paddingVertical + measure.actualBoundingBoxAscent, + username, + offsetX + paddingHorizontal + 1, + offsetY + + paddingVertical + + measure.actualBoundingBoxAscent + + Math.floor((finalHeight - measureHeight) / 2) + + 1, ); } @@ -1145,7 +1152,7 @@ export const renderSceneToSvg = ( return; } // render elements - elements.forEach((element, index) => { + elements.forEach((element) => { if (!element.isDeleted) { try { renderElementToSvg(