mirror of https://github.com/node-red/node-red.git
Merge pull request #4845 from node-red/multiplayer-cursor
Multiplayer cursor trackingpull/4878/head
commit
380d3be819
|
@ -100,16 +100,36 @@ RED.multiplayer = (function () {
|
||||||
break
|
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
|
return location
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let publishLocationTimeout
|
||||||
|
let lastPosition = [0,0]
|
||||||
|
let isInWorkspace = false
|
||||||
|
|
||||||
function publishLocation () {
|
function publishLocation () {
|
||||||
const location = getLocation()
|
if (!publishLocationTimeout) {
|
||||||
if (location.workspace !== 0) {
|
publishLocationTimeout = setTimeout(() => {
|
||||||
log('send', 'multiplayer/location', location)
|
const location = getLocation()
|
||||||
RED.comms.send('multiplayer/location', location)
|
if (location.workspace !== 0) {
|
||||||
|
log('send', 'multiplayer/location', location)
|
||||||
|
RED.comms.send('multiplayer/location', location)
|
||||||
|
}
|
||||||
|
publishLocationTimeout = null
|
||||||
|
}, 100)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
function revealUser(location, skipWorkspace) {
|
function revealUser(location, skipWorkspace) {
|
||||||
if (location.node) {
|
if (location.node) {
|
||||||
// Need to check if this is a known node, so we can fall back to revealing
|
// 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) {
|
function removeUserLocation (sessionId) {
|
||||||
updateUserLocation(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) {
|
function updateUserLocation (sessionId, location) {
|
||||||
let viewTouched = false
|
let viewTouched = false
|
||||||
const oldLocation = sessions[sessionId].location
|
const oldLocation = sessions[sessionId].location
|
||||||
|
@ -291,6 +320,28 @@ RED.multiplayer = (function () {
|
||||||
// console.log(`updateUserLocation sessionId:${sessionId} oldWS:${oldLocation?.workspace} newWS:${location.workspace}`)
|
// console.log(`updateUserLocation sessionId:${sessionId} oldWS:${oldLocation?.workspace} newWS:${location.workspace}`)
|
||||||
if (location.workspace) {
|
if (location.workspace) {
|
||||||
getWorkspaceTray(location.workspace).addUser(sessionId)
|
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) {
|
if (location.node) {
|
||||||
addUserToNode(sessionId, 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 {
|
return {
|
||||||
init: function () {
|
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",{
|
RED.view.annotations.register("red-ui-multiplayer",{
|
||||||
type: 'badge',
|
type: 'badge',
|
||||||
|
@ -479,6 +532,24 @@ RED.multiplayer = (function () {
|
||||||
RED.comms.send('multiplayer/disconnect', disconnectInfo)
|
RED.comms.send('multiplayer/disconnect', disconnectInfo)
|
||||||
RED.settings.removeLocal('multiplayer:sessionId')
|
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 = {
|
const payload = {
|
||||||
session: sessionId,
|
session: sessionId,
|
||||||
workspace: opts.data.workspace,
|
workspace: opts.data.workspace,
|
||||||
node: opts.data.node
|
node: opts.data.node,
|
||||||
|
cursor: opts.data.cursor
|
||||||
}
|
}
|
||||||
runtime.events.emit('comms', {
|
runtime.events.emit('comms', {
|
||||||
topic: 'multiplayer/location',
|
topic: 'multiplayer/location',
|
||||||
|
|
Loading…
Reference in New Issue