Created
September 10, 2024 09:41
-
-
Save hamster1963/3b53ed604b5aeabbfb7c26c182a2ab4f to your computer and use it in GitHub Desktop.
Live Cursor
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| '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