Add mock cursors
This commit is contained in:
@@ -7,9 +7,11 @@ import { useStyles2 } from '@grafana/ui';
|
||||
import { useDispatch, useSelector } from 'app/types/store';
|
||||
|
||||
import { useTransformContext } from '../context/TransformContext';
|
||||
import { useMockCursors } from '../hooks/useMockCursors';
|
||||
import { selectPanel, updateViewport } from '../state/exploreMapSlice';
|
||||
|
||||
import { ExploreMapPanelContainer } from './ExploreMapPanelContainer';
|
||||
import { UserCursor } from './UserCursor';
|
||||
|
||||
export function ExploreMapCanvas() {
|
||||
const styles = useStyles2(getStyles);
|
||||
@@ -19,6 +21,10 @@ export function ExploreMapCanvas() {
|
||||
|
||||
const panels = useSelector((state) => state.exploreMap.panels);
|
||||
const viewport = useSelector((state) => state.exploreMap.viewport);
|
||||
const cursors = useSelector((state) => state.exploreMap.cursors);
|
||||
|
||||
// Initialize mock cursors
|
||||
useMockCursors();
|
||||
|
||||
const handleCanvasClick = useCallback(
|
||||
(e: React.MouseEvent) => {
|
||||
@@ -67,6 +73,9 @@ export function ExploreMapCanvas() {
|
||||
{Object.values(panels).map((panel) => (
|
||||
<ExploreMapPanelContainer key={panel.id} panel={panel} />
|
||||
))}
|
||||
{Object.values(cursors).map((cursor) => (
|
||||
<UserCursor key={cursor.userId} cursor={cursor} />
|
||||
))}
|
||||
</div>
|
||||
</TransformComponent>
|
||||
</TransformWrapper>
|
||||
|
||||
@@ -0,0 +1,72 @@
|
||||
import { css } from '@emotion/css';
|
||||
|
||||
import { GrafanaTheme2 } from '@grafana/data';
|
||||
import { useStyles2 } from '@grafana/ui';
|
||||
|
||||
import { UserCursor as UserCursorType } from '../state/types';
|
||||
|
||||
interface UserCursorProps {
|
||||
cursor: UserCursorType;
|
||||
}
|
||||
|
||||
export function UserCursor({ cursor }: UserCursorProps) {
|
||||
const styles = useStyles2(getStyles);
|
||||
|
||||
return (
|
||||
<div
|
||||
className={styles.cursorContainer}
|
||||
style={{
|
||||
left: cursor.x,
|
||||
top: cursor.y,
|
||||
}}
|
||||
>
|
||||
<svg
|
||||
className={styles.cursorSvg}
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M5 3L5 16L9 12L12 19L14 18L11 11L17 11L5 3Z"
|
||||
fill={cursor.color}
|
||||
stroke="white"
|
||||
strokeWidth="1.5"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
/>
|
||||
</svg>
|
||||
<div className={styles.label} style={{ backgroundColor: cursor.color }}>
|
||||
{cursor.userName}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const getStyles = (theme: GrafanaTheme2) => {
|
||||
return {
|
||||
cursorContainer: css({
|
||||
position: 'absolute',
|
||||
pointerEvents: 'none',
|
||||
zIndex: 10000,
|
||||
transition: 'left 0.15s ease-out, top 0.15s ease-out',
|
||||
}),
|
||||
cursorSvg: css({
|
||||
display: 'block',
|
||||
filter: 'drop-shadow(0 1px 2px rgba(0, 0, 0, 0.3))',
|
||||
}),
|
||||
label: css({
|
||||
position: 'absolute',
|
||||
left: '20px',
|
||||
top: '20px',
|
||||
padding: '4px 8px',
|
||||
borderRadius: theme.shape.radius.default,
|
||||
color: 'white',
|
||||
fontSize: theme.typography.bodySmall.fontSize,
|
||||
fontWeight: theme.typography.fontWeightMedium,
|
||||
whiteSpace: 'nowrap',
|
||||
boxShadow: theme.shadows.z2,
|
||||
}),
|
||||
};
|
||||
};
|
||||
@@ -0,0 +1,134 @@
|
||||
import { useEffect, useRef } from 'react';
|
||||
|
||||
import { useDispatch } from 'app/types/store';
|
||||
|
||||
import { updateCursor } from '../state/exploreMapSlice';
|
||||
|
||||
interface MockUser {
|
||||
userId: string;
|
||||
userName: string;
|
||||
color: string;
|
||||
targetX: number;
|
||||
targetY: number;
|
||||
currentX: number;
|
||||
currentY: number;
|
||||
speed: number;
|
||||
pauseUntil: number;
|
||||
}
|
||||
|
||||
const MOCK_USERS: Array<Omit<MockUser, 'targetX' | 'targetY' | 'currentX' | 'currentY' | 'pauseUntil'>> = [
|
||||
{ userId: 'mock-1', userName: 'Christian', color: '#FF6B6B', speed: 8 },
|
||||
{ userId: 'mock-2', userName: 'Ryan', color: '#4ECDC4', speed: 6 },
|
||||
{ userId: 'mock-3', userName: 'Marc', color: '#45B7D1', speed: 10 },
|
||||
];
|
||||
|
||||
// Constrain cursors to upper left area of canvas
|
||||
const MOVEMENT_AREA = {
|
||||
minX: 100,
|
||||
maxX: 1000,
|
||||
minY: 100,
|
||||
maxY: 800,
|
||||
};
|
||||
const UPDATE_INTERVAL = 50; // ms
|
||||
|
||||
function getRandomPosition() {
|
||||
return {
|
||||
x: Math.random() * (MOVEMENT_AREA.maxX - MOVEMENT_AREA.minX) + MOVEMENT_AREA.minX,
|
||||
y: Math.random() * (MOVEMENT_AREA.maxY - MOVEMENT_AREA.minY) + MOVEMENT_AREA.minY,
|
||||
};
|
||||
}
|
||||
|
||||
function getRandomPauseDuration() {
|
||||
return Math.random() * 1500 + 300; // 300-1800ms pause
|
||||
}
|
||||
|
||||
export function useMockCursors() {
|
||||
const dispatch = useDispatch();
|
||||
const mockUsersRef = useRef<MockUser[]>([]);
|
||||
const animationFrameRef = useRef<number>();
|
||||
const lastUpdateRef = useRef<number>(0);
|
||||
|
||||
useEffect(() => {
|
||||
// Initialize mock users
|
||||
mockUsersRef.current = MOCK_USERS.map((user) => {
|
||||
const startPos = getRandomPosition();
|
||||
const targetPos = getRandomPosition();
|
||||
return {
|
||||
...user,
|
||||
currentX: startPos.x,
|
||||
currentY: startPos.y,
|
||||
targetX: targetPos.x,
|
||||
targetY: targetPos.y,
|
||||
pauseUntil: 0,
|
||||
};
|
||||
});
|
||||
|
||||
const animate = (timestamp: number) => {
|
||||
const deltaTime = timestamp - lastUpdateRef.current;
|
||||
|
||||
if (deltaTime >= UPDATE_INTERVAL) {
|
||||
lastUpdateRef.current = timestamp;
|
||||
|
||||
mockUsersRef.current = mockUsersRef.current.map((user) => {
|
||||
const now = Date.now();
|
||||
|
||||
// If paused, skip movement
|
||||
if (now < user.pauseUntil) {
|
||||
return user;
|
||||
}
|
||||
|
||||
const dx = user.targetX - user.currentX;
|
||||
const dy = user.targetY - user.currentY;
|
||||
const distance = Math.sqrt(dx * dx + dy * dy);
|
||||
|
||||
// If close to target, set new target and maybe pause
|
||||
if (distance < 10) {
|
||||
const newTarget = getRandomPosition();
|
||||
const shouldPause = Math.random() > 0.5; // 50% chance to pause
|
||||
|
||||
return {
|
||||
...user,
|
||||
targetX: newTarget.x,
|
||||
targetY: newTarget.y,
|
||||
pauseUntil: shouldPause ? now + getRandomPauseDuration() : 0,
|
||||
};
|
||||
}
|
||||
|
||||
// Move towards target
|
||||
const moveDistance = user.speed;
|
||||
const ratio = moveDistance / distance;
|
||||
const newX = user.currentX + dx * ratio;
|
||||
const newY = user.currentY + dy * ratio;
|
||||
|
||||
// Update cursor in Redux
|
||||
dispatch(
|
||||
updateCursor({
|
||||
userId: user.userId,
|
||||
userName: user.userName,
|
||||
color: user.color,
|
||||
x: newX,
|
||||
y: newY,
|
||||
lastUpdated: now,
|
||||
})
|
||||
);
|
||||
|
||||
return {
|
||||
...user,
|
||||
currentX: newX,
|
||||
currentY: newY,
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
animationFrameRef.current = requestAnimationFrame(animate);
|
||||
};
|
||||
|
||||
animationFrameRef.current = requestAnimationFrame(animate);
|
||||
|
||||
return () => {
|
||||
if (animationFrameRef.current) {
|
||||
cancelAnimationFrame(animationFrameRef.current);
|
||||
}
|
||||
};
|
||||
}, [dispatch]);
|
||||
}
|
||||
@@ -3,7 +3,7 @@ import { v4 as uuidv4 } from 'uuid';
|
||||
|
||||
import { generateExploreId } from 'app/core/utils/explore';
|
||||
|
||||
import { CanvasViewport, ExploreMapState, initialExploreMapState, PanelPosition } from './types';
|
||||
import { CanvasViewport, ExploreMapState, initialExploreMapState, PanelPosition, UserCursor } from './types';
|
||||
|
||||
const exploreMapSlice = createSlice({
|
||||
name: 'exploreMap',
|
||||
@@ -99,6 +99,14 @@ const exploreMapSlice = createSlice({
|
||||
loadCanvas: (state, action: PayloadAction<ExploreMapState>) => {
|
||||
return action.payload;
|
||||
},
|
||||
|
||||
updateCursor: (state, action: PayloadAction<UserCursor>) => {
|
||||
state.cursors[action.payload.userId] = action.payload;
|
||||
},
|
||||
|
||||
removeCursor: (state, action: PayloadAction<{ userId: string }>) => {
|
||||
delete state.cursors[action.payload.userId];
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
@@ -112,6 +120,8 @@ export const {
|
||||
resetCanvas,
|
||||
duplicatePanel,
|
||||
loadCanvas,
|
||||
updateCursor,
|
||||
removeCursor,
|
||||
} = exploreMapSlice.actions;
|
||||
|
||||
export const exploreMapReducer = exploreMapSlice.reducer;
|
||||
|
||||
@@ -18,11 +18,21 @@ export interface CanvasViewport {
|
||||
panY: number;
|
||||
}
|
||||
|
||||
export interface UserCursor {
|
||||
userId: string;
|
||||
userName: string;
|
||||
color: string;
|
||||
x: number;
|
||||
y: number;
|
||||
lastUpdated: number;
|
||||
}
|
||||
|
||||
export interface ExploreMapState {
|
||||
viewport: CanvasViewport;
|
||||
panels: Record<string, ExploreMapPanel>;
|
||||
selectedPanelId?: string;
|
||||
nextZIndex: number;
|
||||
cursors: Record<string, UserCursor>;
|
||||
}
|
||||
|
||||
export const initialExploreMapState: ExploreMapState = {
|
||||
@@ -34,4 +44,5 @@ export const initialExploreMapState: ExploreMapState = {
|
||||
panels: {},
|
||||
selectedPanelId: undefined,
|
||||
nextZIndex: 1,
|
||||
cursors: {},
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user