Skip to content

Instantly share code, notes, and snippets.

@hamster1963
Created September 10, 2024 09:41
Show Gist options
  • Select an option

  • Save hamster1963/3b53ed604b5aeabbfb7c26c182a2ab4f to your computer and use it in GitHub Desktop.

Select an option

Save hamster1963/3b53ed604b5aeabbfb7c26c182a2ab4f to your computer and use it in GitHub Desktop.
Live Cursor
'use client'
import React, { useState, useEffect, useCallback } from 'react'
import { motion, AnimatePresence } from 'framer-motion'
import { usePathname } from 'next/navigation'
import { useWebSocketContext } from 'lib/websocketProvider'
import { throttle } from 'lodash'
import { isMobile } from 'react-device-detect'
type PointerMap = {
[key: string]: {
id: string
x: number
y: number
name: string
path: string
screenWidth: number
screenHeight: number
text?: string
}
}
// 添加一个颜色数组
const colors = [
'#FF6B6B',
'#4ECDC4',
'#45B7D1',
'#FFA07A',
'#98D8C8',
'#F06292',
'#AED581',
'#FFD54F',
]
// 添加一个函数来为用户分配颜色
const getColorForUser = (userId: string) => {
const index = userId.charCodeAt(0) % colors.length
return colors[index]
}
export default function Cursor() {
const pathname = usePathname()
const [localPointer, setLocalPointer] = useState<PointerMap>({})
const [remotePointers, setRemotePointers] = useState<PointerMap>({})
const [isTyping, setIsTyping] = useState(false)
const [typedText, setTypedText] = useState('')
const { message, sendMessage } = useWebSocketContext()
const sendText = useCallback((text: string, path: string) => {
sendMessage(
JSON.stringify({
type: 'live-cursor-text',
data: { text, path },
})
)
}, [sendMessage])
const throttledSendMessage = useCallback(
throttle(
(
x: number,
y: number,
screenWidth: number,
screenHeight: number,
path: string
) => {
sendMessage(
JSON.stringify({
type: 'live-cursor',
data: { x, y, screenWidth, screenHeight, path },
})
)
},
200
),
[sendMessage]
)
useEffect(() => {
if (message) {
const msgJson = JSON.parse(message)
if (msgJson.type === 'live-cursor-list') {
setRemotePointers(msgJson.data)
} else if (msgJson.type === 'live-cursor-text') {
// 处理接收到的文本消息
setRemotePointers(prev => ({
...prev,
[msgJson.data.id]: {
...prev[msgJson.data.id],
text: msgJson.data.text,
},
}))
}
}
}, [message])
useEffect(() => {
if (isMobile) return // 如果是移动设备,不添加鼠标移动事件监听器
const handleMouseMove = (e: MouseEvent) => {
const x = e.pageX
const y = e.pageY
const screenWidth = window.innerWidth
const screenHeight = window.innerHeight
setLocalPointer((prev) => {
const newPointer = {
You: {
id: 'You',
x: x,
y: y,
name: 'You',
path: pathname,
screenWidth: screenWidth,
screenHeight: screenHeight,
},
}
if (JSON.stringify(prev) === JSON.stringify(newPointer)) {
return prev
}
return newPointer
})
throttledSendMessage(x, y, screenWidth, screenHeight, pathname)
}
const handleKeyDown = (e: KeyboardEvent) => {
if (e.key === '/') {
setIsTyping(true)
setTypedText('/')
sendText('/', pathname)
} else if (isTyping) {
let newText = typedText
if (e.key === 'Backspace') {
newText = typedText.slice(0, -1)
} else if (e.key.length === 1) {
newText = typedText + e.key
} else if (e.key === 'Enter') {
setIsTyping(false)
newText = ''
}
// 在打字状态下阻止空格键的默认行为
if (e.key === ' ') {
e.preventDefault()
}
setTypedText(newText)
sendText(newText, pathname)
}
}
window.addEventListener('mousemove', handleMouseMove)
window.addEventListener('keydown', handleKeyDown)
return () => {
window.removeEventListener('mousemove', handleMouseMove)
window.removeEventListener('keydown', handleKeyDown)
throttledSendMessage.cancel()
}
}, [pathname, throttledSendMessage, sendText, isTyping, typedText])
// 如果是移动设备,直接返回null
if (isMobile) return null
const allPointers = { ...remotePointers, ...localPointer }
return (
<div className="pointer-events-none fixed inset-0 z-[9999]">
<AnimatePresence>
{Object.values(allPointers).map((pointer) =>
pointer.path === pathname || pointer.id === 'You' ? (
<motion.div
key={pointer.id}
className="pointer-events-none absolute z-[9999]"
initial={{ opacity: 0, filter: 'blur(10px)' }}
animate={{
opacity: 1,
scale: 1,
filter: 'blur(0px)',
x:
(pointer.x / pointer.screenWidth) * window.innerWidth -
window.scrollX,
y:
(pointer.y / pointer.screenHeight) * window.innerHeight -
window.scrollY,
}}
exit={{ opacity: 0, filter: 'blur(10px)' }}
transition={{ type: 'spring', damping: 20 }}
>
<div className="relative">
<svg
width="24"
height="36"
viewBox="0 0 24 36"
fill="none"
xmlns="http://www.w3.org/2000/svg"
className="drop-shadow-md"
>
<path
d="M5.65376 12.3673H5.46026L5.31717 12.4976L0.500002 16.8829L0.500002 1.19841L11.7841 12.3673H5.65376Z"
fill={
pointer.id === 'You'
? 'black'
: getColorForUser(pointer.id)
}
stroke="white"
strokeWidth="1.5"
/>
</svg>
<div
className="box-shadow-custom absolute left-2 top-4 whitespace-nowrap rounded-full border border-white px-2 py-1 text-xs font-medium text-white"
style={{
backgroundColor:
pointer.id === 'You'
? 'black'
: getColorForUser(pointer.id),
}}
>
{pointer.id === 'You'
? isTyping
? typedText
: '按下/进行输入'
: pointer.text || pointer.name}
</div>
</div>
</motion.div>
) : null
)}
</AnimatePresence>
</div>
)
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment