cursors!
This commit is contained in:
parent
36fc386269
commit
80d2ba5f00
9 changed files with 179 additions and 50 deletions
|
@ -7,11 +7,13 @@ import useCamera from 'hooks/useCamera'
|
|||
import Defs from './defs'
|
||||
import Page from './page'
|
||||
import Brush from './brush'
|
||||
import Cursor from './cursor'
|
||||
import Bounds from './bounds/bounding-box'
|
||||
import BoundsBg from './bounds/bounds-bg'
|
||||
import Handles from './bounds/handles'
|
||||
import useCanvasEvents from 'hooks/useCanvasEvents'
|
||||
import ContextMenu from './context-menu/context-menu'
|
||||
import { deepCompareArrays } from 'utils'
|
||||
|
||||
function resetError() {
|
||||
null
|
||||
|
@ -41,6 +43,7 @@ export default function Canvas(): JSX.Element {
|
|||
<Bounds />
|
||||
<Handles />
|
||||
<Brush />
|
||||
<Peers />
|
||||
</g>
|
||||
)}
|
||||
</ErrorBoundary>
|
||||
|
@ -49,6 +52,31 @@ export default function Canvas(): JSX.Element {
|
|||
)
|
||||
}
|
||||
|
||||
function Peers(): JSX.Element {
|
||||
const peerIds = useSelector((s) => {
|
||||
return s.data.room ? Object.keys(s.data.room?.peers) : []
|
||||
}, deepCompareArrays)
|
||||
|
||||
return (
|
||||
<>
|
||||
{peerIds.map((id) => (
|
||||
<Peer key={id} id={id} />
|
||||
))}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
function Peer({ id }: { id: string }): JSX.Element {
|
||||
const hasPeer = useSelector((s) => {
|
||||
return s.data.room && s.data.room.peers[id] !== undefined
|
||||
})
|
||||
|
||||
const point = useSelector(
|
||||
(s) => hasPeer && s.data.room.peers[id].cursor.point
|
||||
)
|
||||
|
||||
return <Cursor point={point} />
|
||||
}
|
||||
const MainSVG = styled('svg', {
|
||||
position: 'fixed',
|
||||
overflow: 'hidden',
|
||||
|
|
|
@ -1,28 +1,19 @@
|
|||
import React, { useEffect, useRef } from 'react'
|
||||
import React from 'react'
|
||||
import styled from 'styles'
|
||||
|
||||
export default function Cursor(): JSX.Element {
|
||||
const rCursor = useRef<SVGSVGElement>(null)
|
||||
|
||||
useEffect(() => {
|
||||
function updatePosition(e: PointerEvent) {
|
||||
const cursor = rCursor.current
|
||||
|
||||
cursor.setAttribute(
|
||||
'transform',
|
||||
`translate(${e.clientX - 12} ${e.clientY - 10})`
|
||||
)
|
||||
}
|
||||
|
||||
document.body.addEventListener('pointermove', updatePosition)
|
||||
return () => {
|
||||
document.body.removeEventListener('pointermove', updatePosition)
|
||||
}
|
||||
}, [])
|
||||
export default function Cursor({
|
||||
color = 'dodgerblue',
|
||||
point = [0, 0],
|
||||
}: {
|
||||
color?: string
|
||||
point: number[]
|
||||
}): JSX.Element {
|
||||
const transform = `translate(${point[0] - 12} ${point[1] - 10})`
|
||||
|
||||
return (
|
||||
<StyledCursor
|
||||
ref={rCursor}
|
||||
color={color}
|
||||
transform={transform}
|
||||
width="35px"
|
||||
height="35px"
|
||||
viewBox="0 0 35 35"
|
||||
|
@ -33,23 +24,19 @@ export default function Cursor(): JSX.Element {
|
|||
>
|
||||
<path
|
||||
d="M12,24.4219 L12,8.4069 L23.591,20.0259 L16.81,20.0259 L16.399,20.1499 L12,24.4219 Z"
|
||||
id="point-border"
|
||||
fill="#FFFFFF"
|
||||
fill="#ffffff"
|
||||
/>
|
||||
<path
|
||||
d="M21.0845,25.0962 L17.4795,26.6312 L12.7975,15.5422 L16.4835,13.9892 L21.0845,25.0962 Z"
|
||||
id="stem-border"
|
||||
fill="#FFFFFF"
|
||||
fill="#ffffff"
|
||||
/>
|
||||
<path
|
||||
d="M19.751,24.4155 L17.907,25.1895 L14.807,17.8155 L16.648,17.0405 L19.751,24.4155 Z"
|
||||
id="stem"
|
||||
fill="#000000"
|
||||
fill="currentColor"
|
||||
/>
|
||||
<path
|
||||
d="M13,10.814 L13,22.002 L15.969,19.136 L16.397,18.997 L21.165,18.997 L13,10.814 Z"
|
||||
id="point"
|
||||
fill="#000000"
|
||||
fill="currentColor"
|
||||
/>
|
||||
</StyledCursor>
|
||||
)
|
||||
|
|
|
@ -28,7 +28,7 @@ function Shape({ id, isSelecting }: ShapeProps): JSX.Element {
|
|||
|
||||
const strokeWidth = useSelector((s) => {
|
||||
const shape = getShape(s.data, id)
|
||||
const style = getShapeStyle(shape.style)
|
||||
const style = getShapeStyle(shape?.style)
|
||||
return +style.strokeWidth
|
||||
})
|
||||
|
||||
|
|
|
@ -57,6 +57,7 @@
|
|||
"next-auth": "^3.27.0",
|
||||
"next-pwa": "^5.2.21",
|
||||
"perfect-freehand": "^0.4.91",
|
||||
"pusher": "^5.0.0",
|
||||
"pusher-js": "^7.0.3",
|
||||
"react": "^17.0.2",
|
||||
"react-dom": "^17.0.2",
|
||||
|
|
33
pages/api/pusher-auth.ts
Normal file
33
pages/api/pusher-auth.ts
Normal file
|
@ -0,0 +1,33 @@
|
|||
import { NextApiHandler } from 'next'
|
||||
import Pusher from 'pusher'
|
||||
import { v4 as uuid } from 'uuid'
|
||||
|
||||
const pusher = new Pusher({
|
||||
key: '5dc87c88b8684bda655a',
|
||||
appId: '1226484',
|
||||
secret: process.env.PUSHER_SECRET,
|
||||
cluster: 'eu',
|
||||
})
|
||||
|
||||
const PusherAuth: NextApiHandler = (req, res) => {
|
||||
try {
|
||||
const { socket_id, channel_name } = req.body
|
||||
|
||||
const presenceData = {
|
||||
user_id: uuid(),
|
||||
user_info: { name: 'Anonymous' },
|
||||
}
|
||||
|
||||
const auth = pusher.authenticate(
|
||||
socket_id.toString(),
|
||||
channel_name.toString(),
|
||||
presenceData
|
||||
)
|
||||
|
||||
return res.send(auth)
|
||||
} catch (err) {
|
||||
res.status(403).end()
|
||||
}
|
||||
}
|
||||
|
||||
export default PusherAuth
|
|
@ -2,19 +2,21 @@ import Pusher from 'pusher-js'
|
|||
import * as PusherTypes from 'pusher-js'
|
||||
import state from 'state/state'
|
||||
import { Shape } from 'types'
|
||||
import { v4 as uuid } from 'uuid'
|
||||
|
||||
class RoomClient {
|
||||
room: string
|
||||
pusher: Pusher
|
||||
channel: PusherTypes.Channel
|
||||
channel: PusherTypes.PresenceChannel
|
||||
lastCursorEventTime = 0
|
||||
id = uuid()
|
||||
id: string
|
||||
|
||||
constructor() {
|
||||
// Create pusher instance and bind events
|
||||
|
||||
this.pusher = new Pusher('5dc87c88b8684bda655a', { cluster: 'eu' })
|
||||
this.pusher = new Pusher('5dc87c88b8684bda655a', {
|
||||
cluster: 'eu',
|
||||
authEndpoint: 'http://localhost:3000/api/pusher-auth',
|
||||
})
|
||||
|
||||
this.pusher.connection.bind('connecting', () =>
|
||||
state.send('RT_CHANGED_STATUS', { status: 'connecting' })
|
||||
|
@ -28,33 +30,42 @@ class RoomClient {
|
|||
state.send('RT_CHANGED_STATUS', { status: 'unavailable' })
|
||||
)
|
||||
|
||||
this.pusher.connection.bind('failed', () =>
|
||||
this.pusher.connection.bind('failed', () => {
|
||||
state.send('RT_CHANGED_STATUS', { status: 'failed' })
|
||||
)
|
||||
})
|
||||
|
||||
this.pusher.connection.bind('disconnected', () =>
|
||||
this.pusher.connection.bind('disconnected', () => {
|
||||
state.send('RT_CHANGED_STATUS', { status: 'disconnected' })
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
connect(room: string) {
|
||||
this.room = room
|
||||
connect(roomId: string) {
|
||||
this.room = 'presence-' + roomId
|
||||
|
||||
// Subscribe to channel
|
||||
|
||||
this.channel = this.pusher.subscribe(this.room)
|
||||
this.channel = this.pusher.subscribe(
|
||||
this.room
|
||||
) as PusherTypes.PresenceChannel
|
||||
|
||||
this.channel.bind('pusher:subscription_error', () => {
|
||||
this.channel.bind('pusher:subscription_error', (err: string) => {
|
||||
console.warn(err)
|
||||
state.send('RT_CHANGED_STATUS', { status: 'subscription-error' })
|
||||
})
|
||||
|
||||
this.channel.bind('pusher:subscription_succeeded', () => {
|
||||
const me = this.channel.members.me
|
||||
const userId = me.id
|
||||
|
||||
this.id = userId
|
||||
|
||||
state.send('RT_CHANGED_STATUS', { status: 'subscribed' })
|
||||
})
|
||||
|
||||
this.channel.bind(
|
||||
'created_shape',
|
||||
(payload: { id: string; pageId: string; shape: Shape }) => {
|
||||
if (payload.id === this.id) return
|
||||
state.send('RT_CREATED_SHAPE', payload)
|
||||
}
|
||||
)
|
||||
|
@ -62,6 +73,7 @@ class RoomClient {
|
|||
this.channel.bind(
|
||||
'deleted_shape',
|
||||
(payload: { id: string; pageId: string; shape: Shape }) => {
|
||||
if (payload.id === this.id) return
|
||||
state.send('RT_DELETED_SHAPE', payload)
|
||||
}
|
||||
)
|
||||
|
@ -69,12 +81,13 @@ class RoomClient {
|
|||
this.channel.bind(
|
||||
'edited_shape',
|
||||
(payload: { id: string; pageId: string; change: Partial<Shape> }) => {
|
||||
if (payload.id === this.id) return
|
||||
state.send('RT_EDITED_SHAPE', payload)
|
||||
}
|
||||
)
|
||||
|
||||
this.channel.bind(
|
||||
'moved_cursor',
|
||||
'client-moved-cursor',
|
||||
(payload: { id: string; pageId: string; point: number[] }) => {
|
||||
if (payload.id === this.id) return
|
||||
state.send('RT_MOVED_CURSOR', payload)
|
||||
|
@ -95,10 +108,10 @@ class RoomClient {
|
|||
|
||||
const now = Date.now()
|
||||
|
||||
if (now - this.lastCursorEventTime > 42) {
|
||||
if (now - this.lastCursorEventTime > 200) {
|
||||
this.lastCursorEventTime = now
|
||||
|
||||
this.channel?.trigger('RT_MOVED_CURSOR', {
|
||||
this.channel?.trigger('client-moved-cursor', {
|
||||
id: this.id,
|
||||
pageId,
|
||||
point,
|
||||
|
|
|
@ -188,6 +188,7 @@ const state = createState({
|
|||
RT_EDITED_SHAPE: 'editRtShape',
|
||||
RT_MOVED_CURSOR: 'moveRtCursor',
|
||||
// Client
|
||||
MOVED_POINTER: { secretlyDo: 'sendRtCursorMove' },
|
||||
RESIZED_WINDOW: 'resetPageState',
|
||||
RESET_PAGE: 'resetPage',
|
||||
TOGGLED_READ_ONLY: 'toggleReadOnly',
|
||||
|
@ -1145,10 +1146,16 @@ const state = createState({
|
|||
// Networked Room
|
||||
setRtStatus(data, payload: { id: string; status: string }) {
|
||||
const { status } = payload
|
||||
|
||||
if (!data.room) {
|
||||
data.room = { id: null, status: '' }
|
||||
data.room = {
|
||||
id: null,
|
||||
status: '',
|
||||
peers: {},
|
||||
}
|
||||
}
|
||||
|
||||
data.room.peers = {}
|
||||
data.room.status = status
|
||||
},
|
||||
addRtShape(data, payload: { pageId: string; shape: Shape }) {
|
||||
|
@ -1166,8 +1173,26 @@ const state = createState({
|
|||
// What if the page is in storage?
|
||||
Object.assign(data.document[pageId].shapes[shape.id], shape)
|
||||
},
|
||||
moveRtCursor() {
|
||||
null
|
||||
sendRtCursorMove(data, payload: PointerInfo) {
|
||||
const point = screenToWorld(payload.point, data)
|
||||
pusher.moveCursor(data.currentPageId, point)
|
||||
},
|
||||
moveRtCursor(
|
||||
data,
|
||||
payload: { id: string; pageId: string; point: number[] }
|
||||
) {
|
||||
const { room } = data
|
||||
|
||||
if (room.peers[payload.id] === undefined) {
|
||||
room.peers[payload.id] = {
|
||||
id: payload.id,
|
||||
cursor: {
|
||||
point: payload.point,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
room.peers[payload.id].cursor.point = payload.point
|
||||
},
|
||||
clearRoom(data) {
|
||||
data.room = undefined
|
||||
|
@ -1201,9 +1226,10 @@ const state = createState({
|
|||
}
|
||||
},
|
||||
connectToRoom(data, payload: { id: string }) {
|
||||
data.room = { id: payload.id, status: 'connecting' }
|
||||
data.room = { id: payload.id, status: 'connecting', peers: {} }
|
||||
pusher.connect(payload.id)
|
||||
},
|
||||
|
||||
resetPageState(data) {
|
||||
const pageState = data.pageStates[data.currentPageId]
|
||||
data.pageStates[data.currentPageId] = { ...pageState }
|
||||
|
|
8
types.ts
8
types.ts
|
@ -17,6 +17,7 @@ export interface Data {
|
|||
room?: {
|
||||
id: string
|
||||
status: string
|
||||
peers: Record<string, Peer>
|
||||
}
|
||||
currentStyle: ShapeStyles
|
||||
activeTool: ShapeType | 'select'
|
||||
|
@ -37,6 +38,13 @@ export interface Data {
|
|||
/* Document */
|
||||
/* -------------------------------------------------- */
|
||||
|
||||
export interface Peer {
|
||||
id: string
|
||||
cursor: {
|
||||
point: number[]
|
||||
}
|
||||
}
|
||||
|
||||
export interface TLDocument {
|
||||
id: string
|
||||
name: string
|
||||
|
|
37
yarn.lock
37
yarn.lock
|
@ -2243,6 +2243,13 @@ abab@^2.0.3, abab@^2.0.5:
|
|||
resolved "https://registry.yarnpkg.com/abab/-/abab-2.0.5.tgz#c0b678fb32d60fc1219c784d6a826fe385aeb79a"
|
||||
integrity sha512-9IK9EadsbHo6jLWIpxpR6pL0sazTXV6+SQv25ZB+F7Bj9mJNaOc4nCRabwd5M/JwmUa8idz6Eci6eKfJryPs6Q==
|
||||
|
||||
abort-controller@^3.0.0:
|
||||
version "3.0.0"
|
||||
resolved "https://registry.yarnpkg.com/abort-controller/-/abort-controller-3.0.0.tgz#eaf54d53b62bae4138e809ca225c8439a6efb392"
|
||||
integrity sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==
|
||||
dependencies:
|
||||
event-target-shim "^5.0.0"
|
||||
|
||||
acorn-globals@^6.0.0:
|
||||
version "6.0.0"
|
||||
resolved "https://registry.yarnpkg.com/acorn-globals/-/acorn-globals-6.0.0.tgz#46cdd39f0f8ff08a876619b55f5ac8a6dc770b45"
|
||||
|
@ -3831,6 +3838,11 @@ etag@1.8.1:
|
|||
resolved "https://registry.yarnpkg.com/etag/-/etag-1.8.1.tgz#41ae2eeb65efa62268aebfea83ac7d79299b0887"
|
||||
integrity sha1-Qa4u62XvpiJorr/qg6x9eSmbCIc=
|
||||
|
||||
event-target-shim@^5.0.0:
|
||||
version "5.0.1"
|
||||
resolved "https://registry.yarnpkg.com/event-target-shim/-/event-target-shim-5.0.1.tgz#5d4d3ebdf9583d63a5333ce2deb7480ab2b05789"
|
||||
integrity sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==
|
||||
|
||||
events@^3.0.0:
|
||||
version "3.3.0"
|
||||
resolved "https://registry.yarnpkg.com/events/-/events-3.3.0.tgz#31a95ad0a924e2d2c419a813aeb2c4e878ea7400"
|
||||
|
@ -4546,6 +4558,11 @@ is-arrayish@^0.2.1:
|
|||
resolved "https://registry.yarnpkg.com/is-arrayish/-/is-arrayish-0.2.1.tgz#77c99840527aa8ecb1a8ba697b80645a7a926a9d"
|
||||
integrity sha1-d8mYQFJ6qOyxqLppe4BkWnqSap0=
|
||||
|
||||
is-base64@^1.1.0:
|
||||
version "1.1.0"
|
||||
resolved "https://registry.yarnpkg.com/is-base64/-/is-base64-1.1.0.tgz#8ce1d719895030a457c59a7dcaf39b66d99d56b4"
|
||||
integrity sha512-Nlhg7Z2dVC4/PTvIFkgVVNvPHSO2eR/Yd0XzhGiXCXEvWnptXlXa/clQ8aePPiMuxEGcWfzWbGw2Fe3d+Y3v1g==
|
||||
|
||||
is-bigint@^1.0.1:
|
||||
version "1.0.2"
|
||||
resolved "https://registry.yarnpkg.com/is-bigint/-/is-bigint-1.0.2.tgz#ffb381442503235ad245ea89e45b3dbff040ee5a"
|
||||
|
@ -5971,7 +5988,7 @@ next@^11.0.1:
|
|||
vm-browserify "1.1.2"
|
||||
watchpack "2.1.1"
|
||||
|
||||
node-fetch@2.6.1, node-fetch@^2.6.0:
|
||||
node-fetch@2.6.1, node-fetch@^2.6.0, node-fetch@^2.6.1:
|
||||
version "2.6.1"
|
||||
resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-2.6.1.tgz#045bd323631f76ed2e2b55573394416b639a0052"
|
||||
integrity sha512-V4aYg89jEoVRxRb2fJdAg8FHvI7cEyYdVAh94HH0UIK8oJxUfkjlDQN9RbMx+bEjP7+ggMiFRprSti032Oipxw==
|
||||
|
@ -6658,6 +6675,17 @@ pusher-js@^7.0.3:
|
|||
dependencies:
|
||||
tweetnacl "^1.0.3"
|
||||
|
||||
pusher@^5.0.0:
|
||||
version "5.0.0"
|
||||
resolved "https://registry.yarnpkg.com/pusher/-/pusher-5.0.0.tgz#3dc39ff527637a4b4597652357b0ec562514c8e6"
|
||||
integrity sha512-YaSZHkukytHR9+lklJp4yefwfR4685kfS6pqrSDUxPj45Ga29lIgyN7Jcnsz+bN5WKwXaf2+4c/x/j3pzWIAkw==
|
||||
dependencies:
|
||||
abort-controller "^3.0.0"
|
||||
is-base64 "^1.1.0"
|
||||
node-fetch "^2.6.1"
|
||||
tweetnacl "^1.0.0"
|
||||
tweetnacl-util "^0.15.0"
|
||||
|
||||
querystring-es3@0.2.1, querystring-es3@^0.2.0:
|
||||
version "0.2.1"
|
||||
resolved "https://registry.yarnpkg.com/querystring-es3/-/querystring-es3-0.2.1.tgz#9ec61f79049875707d69414596fd907a4d711e73"
|
||||
|
@ -7822,7 +7850,12 @@ tty-browserify@0.0.1:
|
|||
resolved "https://registry.yarnpkg.com/tty-browserify/-/tty-browserify-0.0.1.tgz#3f05251ee17904dfd0677546670db9651682b811"
|
||||
integrity sha512-C3TaO7K81YvjCgQH9Q1S3R3P3BtN3RIM8n+OvX4il1K1zgE8ZhI0op7kClgkxtutIE8hQrcrHBXvIheqKUUCxw==
|
||||
|
||||
tweetnacl@^1.0.3:
|
||||
tweetnacl-util@^0.15.0:
|
||||
version "0.15.1"
|
||||
resolved "https://registry.yarnpkg.com/tweetnacl-util/-/tweetnacl-util-0.15.1.tgz#b80fcdb5c97bcc508be18c44a4be50f022eea00b"
|
||||
integrity sha512-RKJBIj8lySrShN4w6i/BonWp2Z/uxwC3h4y7xsRrpP59ZboCd0GpEVsOnMDYLMmKBpYhb5TgHzZXy7wTfYFBRw==
|
||||
|
||||
tweetnacl@^1.0.0, tweetnacl@^1.0.3:
|
||||
version "1.0.3"
|
||||
resolved "https://registry.yarnpkg.com/tweetnacl/-/tweetnacl-1.0.3.tgz#ac0af71680458d8a6378d0d0d050ab1407d35596"
|
||||
integrity sha512-6rt+RN7aOi1nGMyC4Xa5DdYiukl2UWCbcJft7YhxReBGQD7OAM8Pbxw6YMo4r2diNEA8FEmu32YOn9rhaiE5yw==
|
||||
|
|
Loading…
Reference in a new issue