mirror of
				https://github.com/node-red/node-red.git
				synced 2025-03-01 10:36:34 +00:00 
			
		
		
		
	Multiplayer: add real-time cursor tracking
This commit is contained in:
		| @@ -100,16 +100,36 @@ RED.multiplayer = (function () { | ||||
|                 break | ||||
|             } | ||||
|         } | ||||
|         if (isInWorkspace) { | ||||
|             const chart = $('#red-ui-workspace-chart') | ||||
|             const chartOffset = chart.offset() | ||||
|             const scaleFactor = RED.view.scale() | ||||
|             location.cursor = { | ||||
|                 x: (lastPosition[0] - chartOffset.left + chart.scrollLeft()) / scaleFactor, | ||||
|                 y: (lastPosition[1] - chartOffset.top + chart.scrollTop()) / scaleFactor | ||||
|             } | ||||
|         } | ||||
|         return location | ||||
|     } | ||||
|  | ||||
|     let publishLocationTimeout | ||||
|     let lastPosition = [0,0] | ||||
|     let isInWorkspace = false | ||||
|  | ||||
|     function publishLocation () { | ||||
|         const location = getLocation() | ||||
|         if (location.workspace !== 0) { | ||||
|             log('send', 'multiplayer/location', location) | ||||
|             RED.comms.send('multiplayer/location', location) | ||||
|         if (!publishLocationTimeout) { | ||||
|             publishLocationTimeout = setTimeout(() => { | ||||
|                 const location = getLocation() | ||||
|                 if (location.workspace !== 0) { | ||||
|                     log('send', 'multiplayer/location', location) | ||||
|                     RED.comms.send('multiplayer/location', location) | ||||
|                 } | ||||
|                 publishLocationTimeout = null | ||||
|             }, 100) | ||||
|         } | ||||
|     } | ||||
|  | ||||
|  | ||||
|     function revealUser(location, skipWorkspace) { | ||||
|         if (location.node) { | ||||
|             // Need to check if this is a known node, so we can fall back to revealing | ||||
| @@ -271,7 +291,16 @@ RED.multiplayer = (function () { | ||||
|  | ||||
|     function removeUserLocation (sessionId) { | ||||
|         updateUserLocation(sessionId, {}) | ||||
|         removeUserCursor(sessionId) | ||||
|     } | ||||
|     function removeUserCursor (sessionId) { | ||||
|         // return | ||||
|         if (sessions[sessionId]?.cursor) { | ||||
|             sessions[sessionId].cursor.parentNode.removeChild(sessions[sessionId].cursor) | ||||
|             delete sessions[sessionId].cursor | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     function updateUserLocation (sessionId, location) { | ||||
|         let viewTouched = false | ||||
|         const oldLocation = sessions[sessionId].location | ||||
| @@ -291,6 +320,28 @@ RED.multiplayer = (function () { | ||||
|         // console.log(`updateUserLocation sessionId:${sessionId} oldWS:${oldLocation?.workspace} newWS:${location.workspace}`) | ||||
|         if (location.workspace) { | ||||
|             getWorkspaceTray(location.workspace).addUser(sessionId) | ||||
|             if (location.cursor && location.workspace === RED.workspaces.active()) { | ||||
|                 if (!sessions[sessionId].cursor) { | ||||
|                     const user = sessions[sessionId].user | ||||
|                     const cursorIcon = document.createElementNS("http://www.w3.org/2000/svg","g"); | ||||
|                     cursorIcon.setAttribute("class", "red-ui-multiplayer-annotation") | ||||
|                     cursorIcon.appendChild(createAnnotationUser(user, true)) | ||||
|                     $(cursorIcon).css({ | ||||
|                         transform: `translate( ${location.cursor.x}px, ${location.cursor.y}px)`, | ||||
|                         transition: 'transform 0.1s linear' | ||||
|                     }) | ||||
|                     $("#red-ui-workspace-chart svg").append(cursorIcon) | ||||
|                     sessions[sessionId].cursor = cursorIcon | ||||
|                 } else { | ||||
|                     const cursorIcon = sessions[sessionId].cursor | ||||
|                     $(cursorIcon).css({ | ||||
|                         transform: `translate( ${location.cursor.x}px, ${location.cursor.y}px)` | ||||
|                     }) | ||||
|      | ||||
|                 } | ||||
|             } else if (sessions[sessionId].cursor) { | ||||
|                 removeUserCursor(sessionId) | ||||
|             } | ||||
|         } | ||||
|         if (location.node) { | ||||
|             addUserToNode(sessionId, location.node) | ||||
| @@ -309,67 +360,69 @@ RED.multiplayer = (function () { | ||||
|     //     } | ||||
|     // } | ||||
|  | ||||
|  | ||||
|     function createAnnotationUser(user, pointer = false) { | ||||
|         const radius = 20 | ||||
|         const halfRadius = radius/2 | ||||
|         const group = document.createElementNS("http://www.w3.org/2000/svg","g"); | ||||
|         const badge = document.createElementNS("http://www.w3.org/2000/svg","path"); | ||||
|         let shapePath | ||||
|         if (!pointer) { | ||||
|             shapePath = `M 0 ${halfRadius} a ${halfRadius} ${halfRadius} 0 1 1 ${radius} 0 a ${halfRadius} ${halfRadius} 0 1 1 -${radius} 0 z` | ||||
|         } else { | ||||
|             shapePath = `M 0 0 h ${halfRadius} a ${halfRadius} ${halfRadius} 0 1 1 -${halfRadius} ${halfRadius} z` | ||||
|         } | ||||
|         badge.setAttribute('d', shapePath) | ||||
|         badge.setAttribute("class", "red-ui-multiplayer-annotation-background") | ||||
|         group.appendChild(badge) | ||||
|         if (user && user.profileColor !== undefined) { | ||||
|             badge.setAttribute("class", "red-ui-multiplayer-annotation-background red-ui-user-profile-color-" + user.profileColor) | ||||
|         } | ||||
|         if (user && user.image) { | ||||
|             const image = document.createElementNS("http://www.w3.org/2000/svg","image"); | ||||
|             image.setAttribute("width", radius) | ||||
|             image.setAttribute("height", radius) | ||||
|             image.setAttribute("href", user.image) | ||||
|             image.setAttribute("clip-path", "circle("+Math.floor(radius/2)+")") | ||||
|             group.appendChild(image) | ||||
|         } else if (user && user.anonymous) { | ||||
|             const anonIconHead = document.createElementNS("http://www.w3.org/2000/svg","circle"); | ||||
|             anonIconHead.setAttribute("cx", radius/2) | ||||
|             anonIconHead.setAttribute("cy", radius/2 - 2) | ||||
|             anonIconHead.setAttribute("r", 2.4) | ||||
|             anonIconHead.setAttribute("class","red-ui-multiplayer-annotation-anon-label"); | ||||
|             group.appendChild(anonIconHead) | ||||
|             const anonIconBody = document.createElementNS("http://www.w3.org/2000/svg","path"); | ||||
|             anonIconBody.setAttribute("class","red-ui-multiplayer-annotation-anon-label"); | ||||
|             // anonIconBody.setAttribute("d",`M ${radius/2 - 4} ${radius/2 + 1} h 8 v4 h -8 z`); | ||||
|             anonIconBody.setAttribute("d",`M ${radius/2} ${radius/2 + 5} h -2.5 c -2 1 -2 -5 0.5 -4.5 c 2 1 2 1 4 0 c 2.5 -0.5  2.5 5.5  0 4.5  z`); | ||||
|             group.appendChild(anonIconBody) | ||||
|         } else { | ||||
|             const labelText = user.username ? user.username.substring(0,2) : user | ||||
|             const label = document.createElementNS("http://www.w3.org/2000/svg","text"); | ||||
|             if (user.username) { | ||||
|                 label.setAttribute("class","red-ui-multiplayer-annotation-label"); | ||||
|                 label.textContent = user.username.substring(0,2) | ||||
|             } else { | ||||
|                 label.setAttribute("class","red-ui-multiplayer-annotation-label red-ui-multiplayer-user-count") | ||||
|                 label.textContent = user | ||||
|             } | ||||
|             label.setAttribute("text-anchor", "middle") | ||||
|             label.setAttribute("x",radius/2); | ||||
|             label.setAttribute("y",radius/2 + 3); | ||||
|             group.appendChild(label) | ||||
|         } | ||||
|         const border = document.createElementNS("http://www.w3.org/2000/svg","path"); | ||||
|         border.setAttribute('d', shapePath) | ||||
|         border.setAttribute("class", "red-ui-multiplayer-annotation-border") | ||||
|         group.appendChild(border) | ||||
|         return group | ||||
|     } | ||||
|  | ||||
|     return { | ||||
|         init: function () { | ||||
|  | ||||
|             function createAnnotationUser(user) { | ||||
|  | ||||
|                 const group = document.createElementNS("http://www.w3.org/2000/svg","g"); | ||||
|                 const badge = document.createElementNS("http://www.w3.org/2000/svg","circle"); | ||||
|                 const radius = 20 | ||||
|                 badge.setAttribute("cx",radius/2); | ||||
|                 badge.setAttribute("cy",radius/2); | ||||
|                 badge.setAttribute("r",radius/2); | ||||
|                 badge.setAttribute("class", "red-ui-multiplayer-annotation-background") | ||||
|                 group.appendChild(badge) | ||||
|                 if (user && user.profileColor !== undefined) { | ||||
|                     badge.setAttribute("class", "red-ui-multiplayer-annotation-background red-ui-user-profile-color-" + user.profileColor) | ||||
|                 } | ||||
|                 if (user && user.image) { | ||||
|                     const image = document.createElementNS("http://www.w3.org/2000/svg","image"); | ||||
|                     image.setAttribute("width", radius) | ||||
|                     image.setAttribute("height", radius) | ||||
|                     image.setAttribute("href", user.image) | ||||
|                     image.setAttribute("clip-path", "circle("+Math.floor(radius/2)+")") | ||||
|                     group.appendChild(image) | ||||
|                 } else if (user && user.anonymous) { | ||||
|                     const anonIconHead = document.createElementNS("http://www.w3.org/2000/svg","circle"); | ||||
|                     anonIconHead.setAttribute("cx", radius/2) | ||||
|                     anonIconHead.setAttribute("cy", radius/2 - 2) | ||||
|                     anonIconHead.setAttribute("r", 2.4) | ||||
|                     anonIconHead.setAttribute("class","red-ui-multiplayer-annotation-anon-label"); | ||||
|                     group.appendChild(anonIconHead) | ||||
|                     const anonIconBody = document.createElementNS("http://www.w3.org/2000/svg","path"); | ||||
|                     anonIconBody.setAttribute("class","red-ui-multiplayer-annotation-anon-label"); | ||||
|                     // anonIconBody.setAttribute("d",`M ${radius/2 - 4} ${radius/2 + 1} h 8 v4 h -8 z`); | ||||
|                     anonIconBody.setAttribute("d",`M ${radius/2} ${radius/2 + 5} h -2.5 c -2 1 -2 -5 0.5 -4.5 c 2 1 2 1 4 0 c 2.5 -0.5  2.5 5.5  0 4.5  z`); | ||||
|                     group.appendChild(anonIconBody) | ||||
|                 } else { | ||||
|                     const labelText = user.username ? user.username.substring(0,2) : user | ||||
|                     const label = document.createElementNS("http://www.w3.org/2000/svg","text"); | ||||
|                     if (user.username) { | ||||
|                         label.setAttribute("class","red-ui-multiplayer-annotation-label"); | ||||
|                         label.textContent = user.username.substring(0,2) | ||||
|                     } else { | ||||
|                         label.setAttribute("class","red-ui-multiplayer-annotation-label red-ui-multiplayer-user-count") | ||||
|                         label.textContent = user | ||||
|                     } | ||||
|                     label.setAttribute("text-anchor", "middle") | ||||
|                     label.setAttribute("x",radius/2); | ||||
|                     label.setAttribute("y",radius/2 + 3); | ||||
|                     group.appendChild(label) | ||||
|                 } | ||||
|                 const border = document.createElementNS("http://www.w3.org/2000/svg","circle"); | ||||
|                 border.setAttribute("cx",radius/2); | ||||
|                 border.setAttribute("cy",radius/2); | ||||
|                 border.setAttribute("r",radius/2); | ||||
|                 border.setAttribute("class", "red-ui-multiplayer-annotation-border") | ||||
|                 group.appendChild(border) | ||||
|            | ||||
|  | ||||
|  | ||||
|                 return group | ||||
|             } | ||||
|              | ||||
|              | ||||
|             RED.view.annotations.register("red-ui-multiplayer",{ | ||||
|                 type: 'badge', | ||||
| @@ -479,6 +532,24 @@ RED.multiplayer = (function () { | ||||
|                 RED.comms.send('multiplayer/disconnect', disconnectInfo) | ||||
|                 RED.settings.removeLocal('multiplayer:sessionId') | ||||
|             }) | ||||
|              | ||||
|             const chart = $('#red-ui-workspace-chart') | ||||
|             chart.on('mousemove', function (evt) { | ||||
|                 lastPosition[0] = evt.clientX | ||||
|                 lastPosition[1] = evt.clientY | ||||
|                 publishLocation() | ||||
|             }) | ||||
|             chart.on('scroll', function (evt) { | ||||
|                 publishLocation() | ||||
|             }) | ||||
|             chart.on('mouseenter', function () { | ||||
|                 isInWorkspace = true | ||||
|                 publishLocation() | ||||
|             }) | ||||
|             chart.on('mouseleave', function () { | ||||
|                 isInWorkspace = false | ||||
|                 publishLocation() | ||||
|             }) | ||||
|         } | ||||
|     } | ||||
|  | ||||
|   | ||||
| @@ -110,7 +110,8 @@ module.exports = { | ||||
|                 const payload = { | ||||
|                     session: sessionId, | ||||
|                     workspace: opts.data.workspace, | ||||
|                     node: opts.data.node | ||||
|                     node: opts.data.node, | ||||
|                     cursor: opts.data.cursor | ||||
|                 } | ||||
|                 runtime.events.emit('comms', { | ||||
|                     topic: 'multiplayer/location', | ||||
|   | ||||
		Reference in New Issue
	
	Block a user