Add hover effects and dynamic anchor computation to Station component
- Integrated `onTextHover` callback for handling text hover states. - Implemented dynamic anchor calculation using `useMemo` for improved positioning. - Updated visual feedback by highlighting labels on hover. - Adjusted offset calculations and added interactive pointer movement refinements. - Added `packageManager` field to `package.json`.
This commit is contained in:
@ -56,5 +56,6 @@
|
||||
"typescript": "~5.8.3",
|
||||
"typescript-eslint": "^8.30.1",
|
||||
"vite": "^6.3.5"
|
||||
}
|
||||
},
|
||||
"packageManager": "yarn@1.22.22+sha512.a6b2f7906b721bba3d67d4aff083df04dad64c399707841b7acf00f6b133b7ac24255f2652fa22ae3534329dc6180534e98d17432037ff6fd140556e2bb3137e"
|
||||
}
|
||||
|
@ -1,5 +1,5 @@
|
||||
import { FederatedMouseEvent, Graphics } from "pixi.js";
|
||||
import { useCallback, useState, useEffect, useRef, FC } from "react";
|
||||
import {useCallback, useState, useEffect, useRef, FC, useMemo} from "react";
|
||||
import { observer } from "mobx-react-lite";
|
||||
|
||||
// --- Заглушки для зависимостей (замените на ваши реальные импорты) ---
|
||||
@ -67,6 +67,8 @@ interface StationProps {
|
||||
labelAlign?: LabelAlign;
|
||||
/** Callback для изменения внутреннего выравнивания */
|
||||
onLabelAlignChange?: (align: LabelAlign) => void;
|
||||
/** Callback для отслеживания наведения на текст */
|
||||
onTextHover?: (isHovered: boolean) => void;
|
||||
}
|
||||
|
||||
interface LabelAlignmentControlProps {
|
||||
@ -82,6 +84,18 @@ interface LabelAlignmentControlProps {
|
||||
interface StationLabelProps
|
||||
extends Omit<StationProps, "ruLabelAnchor" | "nameLabelAnchor"> {}
|
||||
|
||||
const getAnchorFromOffset = (offsetX: number, offsetY: number): { x: number; y: number } => {
|
||||
if (offsetX === 0 && offsetY === 0) {
|
||||
return { x: 0.5, y: 0.5 };
|
||||
}
|
||||
|
||||
const length = Math.hypot(offsetX, offsetY);
|
||||
const nx = offsetX / length;
|
||||
const ny = offsetY / length;
|
||||
|
||||
return { x: (1 - nx) / 2, y: (1 - ny) / 2 };
|
||||
};
|
||||
|
||||
// =========================================================================
|
||||
// Компонент: Панель управления выравниванием в стиле УрФУ
|
||||
// =========================================================================
|
||||
@ -240,6 +254,7 @@ const StationLabel = observer(
|
||||
labelBlockAnchor: labelBlockAnchorProp,
|
||||
labelAlign: labelAlignProp = "center",
|
||||
onLabelAlignChange,
|
||||
onTextHover,
|
||||
}: Readonly<StationLabelProps>) => {
|
||||
const { language } = languageStore;
|
||||
const { rotation, scale } = useTransform();
|
||||
@ -272,6 +287,7 @@ const StationLabel = observer(
|
||||
hideTimer.current = null;
|
||||
}
|
||||
setIsHovered(true);
|
||||
onTextHover?.(true); // Call the callback to indicate text is hovered
|
||||
};
|
||||
|
||||
const handleControlPointerEnter = () => {
|
||||
@ -282,6 +298,7 @@ const StationLabel = observer(
|
||||
}
|
||||
setIsControlHovered(true);
|
||||
setIsHovered(true);
|
||||
onTextHover?.(true); // Call the callback to indicate text/control is hovered
|
||||
};
|
||||
|
||||
const handleControlPointerLeave = () => {
|
||||
@ -290,6 +307,7 @@ const StationLabel = observer(
|
||||
if (!isHovered) {
|
||||
hideTimer.current = setTimeout(() => {
|
||||
setIsHovered(false);
|
||||
onTextHover?.(false); // Call the callback to indicate neither text nor control is hovered
|
||||
}, 0);
|
||||
}
|
||||
};
|
||||
@ -302,6 +320,7 @@ const StationLabel = observer(
|
||||
if (!isControlHovered) {
|
||||
setIsControlHovered(false);
|
||||
}
|
||||
onTextHover?.(false); // Call the callback to indicate text is no longer hovered
|
||||
}, 100); // Увеличиваем время до скрытия панели
|
||||
};
|
||||
|
||||
@ -347,7 +366,7 @@ const StationLabel = observer(
|
||||
const compensatedRuFontSize = (26 * 0.75) / scale;
|
||||
const compensatedNameFontSize = (16 * 0.75) / scale;
|
||||
const minDistance = 30;
|
||||
const compensatedOffset = Math.max(minDistance / scale, 24 / scale);
|
||||
const compensatedOffset = minDistance / scale;
|
||||
const textBlockPosition = anchorPoint || position;
|
||||
const finalLabelBlockAnchor = labelBlockAnchorProp || "right center";
|
||||
const labelBlockAnchor =
|
||||
@ -372,17 +391,20 @@ const StationLabel = observer(
|
||||
|
||||
const handlePointerMove = (e: FederatedMouseEvent) => {
|
||||
if (!isPointerDown) return;
|
||||
|
||||
if (!isDragging) {
|
||||
const dx = e.global.x - mouseStartPos.current.x;
|
||||
const dy = e.global.y - mouseStartPos.current.y;
|
||||
if (Math.hypot(dx, dy) > 3) setIsDragging(true);
|
||||
else return;
|
||||
}
|
||||
const dx = (e.global.x - mouseStartPos.current.x) / scale;
|
||||
const dy = (e.global.y - mouseStartPos.current.y) / scale;
|
||||
|
||||
const dx_screen = e.global.x - mouseStartPos.current.x;
|
||||
const dy_screen = e.global.y - mouseStartPos.current.y;
|
||||
|
||||
const newPosition = {
|
||||
x: dragStartPos.current.x + dx,
|
||||
y: dragStartPos.current.y + dy,
|
||||
x: dragStartPos.current.x + dx_screen,
|
||||
y: dragStartPos.current.y + dy_screen,
|
||||
};
|
||||
|
||||
// Проверяем, изменилась ли позиция
|
||||
@ -410,6 +432,11 @@ const StationLabel = observer(
|
||||
setStationAlign(station.id, numericAlign);
|
||||
};
|
||||
|
||||
const dynamicAnchor = useMemo(
|
||||
() => getAnchorFromOffset(position.x, position.y),
|
||||
[position.x, position.y]
|
||||
);
|
||||
|
||||
// Функция для расчета позиции нижнего лейбла относительно ширины верхнего
|
||||
const getSecondLabelPosition = (): number => {
|
||||
if (!ruLabelWidth) return 0;
|
||||
@ -459,13 +486,11 @@ const StationLabel = observer(
|
||||
onGlobalPointerMove={handlePointerMove}
|
||||
>
|
||||
<pixiContainer
|
||||
position={{
|
||||
x:
|
||||
textBlockPosition.x +
|
||||
compensatedOffset * (labelBlockAnchor.x === 1 ? -1 : 1),
|
||||
y: textBlockPosition.y,
|
||||
}}
|
||||
anchor={labelBlockAnchor}
|
||||
position={{
|
||||
x: (position.x + Math.cos(Math.atan2(position.y, position.x))) / scale,
|
||||
y: (position.y + Math.sin(Math.atan2(position.y, position.x))) / scale,
|
||||
}}
|
||||
anchor={dynamicAnchor}
|
||||
>
|
||||
{ruLabel && (
|
||||
<pixiText
|
||||
@ -524,6 +549,8 @@ export const Station = ({
|
||||
labelAlign,
|
||||
onLabelAlignChange,
|
||||
}: Readonly<StationProps>) => {
|
||||
const [isTextHovered, setIsTextHovered] = useState(false);
|
||||
|
||||
const draw = useCallback(
|
||||
(g: Graphics) => {
|
||||
g.clear();
|
||||
@ -531,12 +558,22 @@ export const Station = ({
|
||||
station.latitude,
|
||||
station.longitude
|
||||
);
|
||||
|
||||
const radius = STATION_RADIUS;
|
||||
const strokeWidth = STATION_OUTLINE_WIDTH;
|
||||
|
||||
g.circle(coordinates.x * UP_SCALE, coordinates.y * UP_SCALE, radius);
|
||||
g.fill({ color: PATH_COLOR });
|
||||
g.stroke({ color: BACKGROUND_COLOR, width: STATION_OUTLINE_WIDTH });
|
||||
|
||||
// Change fill color when text is hovered
|
||||
if (isTextHovered) {
|
||||
g.fill({ color: 0x00AAFF }); // Highlight color when hovered
|
||||
g.stroke({ color: 0xFFFFFF, width: strokeWidth + 1 }); // Brighter outline when hovered
|
||||
} else {
|
||||
g.fill({ color: PATH_COLOR });
|
||||
g.stroke({ color: BACKGROUND_COLOR, width: strokeWidth });
|
||||
}
|
||||
},
|
||||
[station.latitude, station.longitude]
|
||||
[station.latitude, station.longitude, isTextHovered]
|
||||
);
|
||||
|
||||
return (
|
||||
@ -549,6 +586,7 @@ export const Station = ({
|
||||
labelBlockAnchor={labelBlockAnchor}
|
||||
labelAlign={labelAlign}
|
||||
onLabelAlignChange={onLabelAlignChange}
|
||||
onTextHover={setIsTextHovered}
|
||||
/>
|
||||
</pixiContainer>
|
||||
);
|
||||
|
Reference in New Issue
Block a user