image-map-route 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +164 -0
- package/dist/hooks/useBoundingClientRect.d.ts +3 -0
- package/dist/hooks/useBoundingClientRect.d.ts.map +1 -0
- package/dist/hooks/useBoundingClientRect.js +12 -0
- package/dist/hooks/useControlledState.d.ts +4 -0
- package/dist/hooks/useControlledState.d.ts.map +1 -0
- package/dist/hooks/useControlledState.js +7 -0
- package/dist/hooks/useResizeObserver.d.ts +3 -0
- package/dist/hooks/useResizeObserver.d.ts.map +1 -0
- package/dist/hooks/useResizeObserver.js +14 -0
- package/dist/imageMapRoute.d.ts +3 -0
- package/dist/imageMapRoute.d.ts.map +1 -0
- package/dist/imageMapRoute.js +76 -0
- package/dist/imageMapRouteContainer.d.ts +22 -0
- package/dist/imageMapRouteContainer.d.ts.map +1 -0
- package/dist/imageMapRouteContainer.js +166 -0
- package/dist/imageMapRoutePaths.d.ts +9 -0
- package/dist/imageMapRoutePaths.d.ts.map +1 -0
- package/dist/imageMapRoutePaths.js +18 -0
- package/dist/imageMapRoutePoints.d.ts +11 -0
- package/dist/imageMapRoutePoints.d.ts.map +1 -0
- package/dist/imageMapRoutePoints.js +8 -0
- package/dist/index.d.ts +5 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +4 -0
- package/dist/types.d.ts +65 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +1 -0
- package/dist/useRouteVideoSync.d.ts +10 -0
- package/dist/useRouteVideoSync.d.ts.map +1 -0
- package/dist/useRouteVideoSync.js +61 -0
- package/dist/utils/calculateCenterZoom.d.ts +9 -0
- package/dist/utils/calculateCenterZoom.d.ts.map +1 -0
- package/dist/utils/calculateCenterZoom.js +13 -0
- package/dist/utils/calculateOptimalZoom.d.ts +9 -0
- package/dist/utils/calculateOptimalZoom.d.ts.map +1 -0
- package/dist/utils/calculateOptimalZoom.js +32 -0
- package/dist/utils/clampPosition.d.ts +5 -0
- package/dist/utils/clampPosition.d.ts.map +1 -0
- package/dist/utils/clampPosition.js +12 -0
- package/dist/utils/findSpotByTime.d.ts +3 -0
- package/dist/utils/findSpotByTime.d.ts.map +1 -0
- package/dist/utils/findSpotByTime.js +62 -0
- package/dist/utils/findTimeBySpot.d.ts +3 -0
- package/dist/utils/findTimeBySpot.d.ts.map +1 -0
- package/dist/utils/findTimeBySpot.js +60 -0
- package/dist/utils/getClosestPointOnPath.d.ts +3 -0
- package/dist/utils/getClosestPointOnPath.d.ts.map +1 -0
- package/dist/utils/getClosestPointOnPath.js +63 -0
- package/dist/utils/index.d.ts +5 -0
- package/dist/utils/index.d.ts.map +1 -0
- package/dist/utils/index.js +4 -0
- package/dist/utils/mouseToContainer.d.ts +11 -0
- package/dist/utils/mouseToContainer.d.ts.map +1 -0
- package/dist/utils/mouseToContainer.js +9 -0
- package/dist/utils/offsetToZoom.d.ts +8 -0
- package/dist/utils/offsetToZoom.d.ts.map +1 -0
- package/dist/utils/offsetToZoom.js +9 -0
- package/package.json +34 -0
package/README.md
ADDED
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
# image-map-route
|
|
2
|
+
|
|
3
|
+
React utilities for rendering and syncing image map routes.
|
|
4
|
+
|
|
5
|
+
## Install
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npm i image-map-route
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Example
|
|
12
|
+
|
|
13
|
+
- Example site: Coming soon.
|
|
14
|
+
|
|
15
|
+
## Usage
|
|
16
|
+
|
|
17
|
+
### Basic route rendering
|
|
18
|
+
|
|
19
|
+
```tsx
|
|
20
|
+
import { ImageRoute, type Point } from 'image-map-route';
|
|
21
|
+
|
|
22
|
+
const points: Point[] = [
|
|
23
|
+
{ x: 0.12, y: 0.3, marked: 1, start: 0, end: 1.2 },
|
|
24
|
+
{ x: 0.2, y: 0.35, marked: 0 },
|
|
25
|
+
{ x: 0.45, y: 0.5, marked: 2 },
|
|
26
|
+
];
|
|
27
|
+
|
|
28
|
+
export function RoutePreview() {
|
|
29
|
+
return <ImageRoute points={points} />;
|
|
30
|
+
}
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
### Sync with video playback
|
|
34
|
+
|
|
35
|
+
```tsx
|
|
36
|
+
import {
|
|
37
|
+
ImageRoute,
|
|
38
|
+
type Point,
|
|
39
|
+
type Spot,
|
|
40
|
+
useRouteVideoSync,
|
|
41
|
+
} from 'image-map-route';
|
|
42
|
+
|
|
43
|
+
const points: Point[] = [
|
|
44
|
+
{ x: 0.12, y: 0.3, start: 0, end: 1.2 },
|
|
45
|
+
{ x: 0.2, y: 0.35 },
|
|
46
|
+
{ x: 0.45, y: 0.5 },
|
|
47
|
+
];
|
|
48
|
+
|
|
49
|
+
export function RouteWithVideo() {
|
|
50
|
+
const { routeRef, videoRef, activeSpot, setActiveSpot } = useRouteVideoSync(points);
|
|
51
|
+
|
|
52
|
+
return (
|
|
53
|
+
<div>
|
|
54
|
+
<video ref={videoRef} src="/route.mp4" controls />
|
|
55
|
+
<ImageRoute
|
|
56
|
+
ref={routeRef}
|
|
57
|
+
points={points}
|
|
58
|
+
activeSpot={activeSpot}
|
|
59
|
+
setActiveSpot={setActiveSpot}
|
|
60
|
+
followActiveSpot
|
|
61
|
+
/>
|
|
62
|
+
</div>
|
|
63
|
+
);
|
|
64
|
+
}
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
## API
|
|
68
|
+
|
|
69
|
+
### Components
|
|
70
|
+
|
|
71
|
+
#### `ImageRoute`
|
|
72
|
+
|
|
73
|
+
```ts
|
|
74
|
+
function ImageRoute(props: MapImageRouteProps): JSX.Element;
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
Main component that renders a scalable route on an image map.
|
|
78
|
+
|
|
79
|
+
**Props (MapImageRouteProps)**
|
|
80
|
+
|
|
81
|
+
- `points: Point[]`
|
|
82
|
+
Route points in normalized coordinates (0..1).
|
|
83
|
+
- `addPoint?: Dispatch<Point>`
|
|
84
|
+
If provided, clicking the route adds a point. Otherwise clicks select a spot.
|
|
85
|
+
- `activeSpot?: Spot` / `setActiveSpot?: Dispatch<Spot>`
|
|
86
|
+
Controlled active spot selection.
|
|
87
|
+
- `followActiveSpot?: boolean`
|
|
88
|
+
Auto-center the view on the active spot.
|
|
89
|
+
- `RenderPoint?: ComponentType<RenderPointProps>`
|
|
90
|
+
Custom point renderer.
|
|
91
|
+
- `RenderPath?: ComponentType<RenderPathProps>`
|
|
92
|
+
Custom path renderer.
|
|
93
|
+
- `RenderExtra?: ComponentType<RenderExtraProps>`
|
|
94
|
+
Custom extra overlay renderer.
|
|
95
|
+
- `deps?: string`
|
|
96
|
+
Trigger recalculation of initial/animated position when changed.
|
|
97
|
+
-
|
|
98
|
+
|
|
99
|
+
`getInitialPosition?: (containerSize: DOMRect) => { scale?: number; offset?: { x: number; y: number } }`
|
|
100
|
+
Initial zoom/offset when the container is ready.
|
|
101
|
+
|
|
102
|
+
-
|
|
103
|
+
|
|
104
|
+
`getAnimatedPosition?: (containerSize: DOMRect) => { scale?: number; offset?: { x: number; y: number } }`
|
|
105
|
+
Optional animated zoom/offset after initial render.
|
|
106
|
+
|
|
107
|
+
- `innerChildren?: ReactNode`
|
|
108
|
+
Children rendered inside the container.
|
|
109
|
+
- `sx?: CSSProperties`
|
|
110
|
+
Inline styles for the container.
|
|
111
|
+
|
|
112
|
+
### Hooks
|
|
113
|
+
|
|
114
|
+
#### `useRouteVideoSync`
|
|
115
|
+
|
|
116
|
+
```ts
|
|
117
|
+
function useRouteVideoSync(points: Point[]): {
|
|
118
|
+
routeRef: React.RefObject<HTMLDivElement>;
|
|
119
|
+
videoRef: React.RefObject<HTMLVideoElement>;
|
|
120
|
+
time: number;
|
|
121
|
+
setTime: (time: number) => void;
|
|
122
|
+
activeSpot: Spot | null;
|
|
123
|
+
setActiveSpot: (spot: Spot) => void;
|
|
124
|
+
};
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
Syncs the map route with an HTML video element. Call `setActiveSpot` to seek the
|
|
128
|
+
video to the corresponding time, or scrub the video to update the active spot.
|
|
129
|
+
|
|
130
|
+
### Types
|
|
131
|
+
|
|
132
|
+
```ts
|
|
133
|
+
type Point = {
|
|
134
|
+
x: number;
|
|
135
|
+
y: number;
|
|
136
|
+
marked?: number;
|
|
137
|
+
start?: number;
|
|
138
|
+
end?: number;
|
|
139
|
+
type?: string;
|
|
140
|
+
extra?: string;
|
|
141
|
+
};
|
|
142
|
+
|
|
143
|
+
type Spot = {
|
|
144
|
+
point: { x: number; y: number };
|
|
145
|
+
pointIndex?: number;
|
|
146
|
+
percentage?: number;
|
|
147
|
+
};
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
### Utilities
|
|
151
|
+
|
|
152
|
+
```ts
|
|
153
|
+
import {
|
|
154
|
+
calculateCenterZoom,
|
|
155
|
+
calculateOptimalZoom,
|
|
156
|
+
findSpotByTime,
|
|
157
|
+
findTimeBySpot,
|
|
158
|
+
} from 'image-map-route';
|
|
159
|
+
```
|
|
160
|
+
|
|
161
|
+
- `calculateCenterZoom(point, containerSize, scale)`
|
|
162
|
+
- `calculateOptimalZoom(points, containerSize, paddingPercent)`
|
|
163
|
+
- `findSpotByTime(points, time)`
|
|
164
|
+
- `findTimeBySpot(points, spot)`
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"useBoundingClientRect.d.ts","sourceRoot":"","sources":["../../src/hooks/useBoundingClientRect.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,SAAS,EAAyB,MAAM,OAAO,CAAC;AAI9D,MAAM,CAAC,OAAO,UAAU,qBAAqB,CAAC,GAAG,EAAE,SAAS,CAAC,WAAW,CAAC,GAAG,OAAO,CAYlF"}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { useCallback, useState } from 'react';
|
|
2
|
+
import { useDidMount } from 'rooks';
|
|
3
|
+
import useResizeObserver from './useResizeObserver';
|
|
4
|
+
export default function useBoundingClientRect(ref) {
|
|
5
|
+
const [domRect, setDomRect] = useState(null);
|
|
6
|
+
const update = useCallback(() => {
|
|
7
|
+
setDomRect(ref.current ? ref.current.getBoundingClientRect() : null);
|
|
8
|
+
}, [ref]);
|
|
9
|
+
useDidMount(update);
|
|
10
|
+
useResizeObserver(ref, update);
|
|
11
|
+
return domRect;
|
|
12
|
+
}
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import { type Dispatch, type SetStateAction } from 'react';
|
|
2
|
+
export default function useControlledState<S>(defaultState?: S): [S, Dispatch<SetStateAction<S>>];
|
|
3
|
+
export default function useControlledState<S>(state: S, setState: Dispatch<SetStateAction<S>>): [S, Dispatch<SetStateAction<S>>];
|
|
4
|
+
//# sourceMappingURL=useControlledState.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"useControlledState.d.ts","sourceRoot":"","sources":["../../src/hooks/useControlledState.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,QAAQ,EAAE,KAAK,cAAc,EAAY,MAAM,OAAO,CAAC;AAGrE,MAAM,CAAC,OAAO,UAAU,kBAAkB,CAAC,CAAC,EAAE,YAAY,CAAC,EAAE,CAAC,GAAG,CAAC,CAAC,EAAE,QAAQ,CAAC,cAAc,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;AAClG,MAAM,CAAC,OAAO,UAAU,kBAAkB,CAAC,CAAC,EAC3C,KAAK,EAAE,CAAC,EACR,QAAQ,EAAE,QAAQ,CAAC,cAAc,CAAC,CAAC,CAAC,CAAC,GACnC,CAAC,CAAC,EAAE,QAAQ,CAAC,cAAc,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"useResizeObserver.d.ts","sourceRoot":"","sources":["../../src/hooks/useResizeObserver.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,SAAS,EAAqB,MAAM,OAAO,CAAC;AAE1D,MAAM,CAAC,OAAO,UAAU,iBAAiB,CACxC,GAAG,EAAE,SAAS,CAAC,WAAW,CAAC,EAC3B,QAAQ,EAAE,sBAAsB,GAC9B,IAAI,CAcN"}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { useEffect, useRef } from 'react';
|
|
2
|
+
export default function useResizeObserver(ref, callback) {
|
|
3
|
+
const callbackRef = useRef(callback);
|
|
4
|
+
useEffect(() => {
|
|
5
|
+
callbackRef.current = callback;
|
|
6
|
+
}, [callback]);
|
|
7
|
+
useEffect(() => {
|
|
8
|
+
if (ref.current) {
|
|
9
|
+
const observer = new ResizeObserver((...args) => callbackRef.current(...args));
|
|
10
|
+
observer.observe(ref.current);
|
|
11
|
+
return () => observer.disconnect();
|
|
12
|
+
}
|
|
13
|
+
}, [ref]);
|
|
14
|
+
}
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
import { type MapImageRouteProps } from './types';
|
|
2
|
+
export default function ImageMapRoute({ ref, points, addPoint, activeSpot: _activeSpot, setActiveSpot: _setActiveSpot, RenderPoint, RenderPath, RenderExtra, deps, getInitialPosition, getAnimatedPosition, followActiveSpot, sx, children, ...props }: MapImageRouteProps): import("react/jsx-runtime").JSX.Element;
|
|
3
|
+
//# sourceMappingURL=imageMapRoute.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"imageMapRoute.d.ts","sourceRoot":"","sources":["../src/imageMapRoute.tsx"],"names":[],"mappings":"AAMA,OAAO,EAAE,KAAK,kBAAkB,EAAa,MAAM,SAAS,CAAC;AAI7D,MAAM,CAAC,OAAO,UAAU,aAAa,CAAC,EACrC,GAAG,EACH,MAAM,EACN,QAAQ,EACR,UAAU,EAAE,WAAW,EACvB,aAAa,EAAE,cAAc,EAC7B,WAAW,EACX,UAAU,EACV,WAAW,EACX,IAAI,EACJ,kBAAiE,EACjE,mBAAmB,EACnB,gBAAgB,EAChB,EAAE,EACF,QAAQ,EACR,GAAG,KAAK,EACR,EAAE,kBAAkB,2CA6FpB"}
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
var __rest = (this && this.__rest) || function (s, e) {
|
|
2
|
+
var t = {};
|
|
3
|
+
for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p) && e.indexOf(p) < 0)
|
|
4
|
+
t[p] = s[p];
|
|
5
|
+
if (s != null && typeof Object.getOwnPropertySymbols === "function")
|
|
6
|
+
for (var i = 0, p = Object.getOwnPropertySymbols(s); i < p.length; i++) {
|
|
7
|
+
if (e.indexOf(p[i]) < 0 && Object.prototype.propertyIsEnumerable.call(s, p[i]))
|
|
8
|
+
t[p[i]] = s[p[i]];
|
|
9
|
+
}
|
|
10
|
+
return t;
|
|
11
|
+
};
|
|
12
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
13
|
+
import { useEffect, useRef, useState } from 'react';
|
|
14
|
+
import useBoundingClientRect from './hooks/useBoundingClientRect';
|
|
15
|
+
import useControlledState from './hooks/useControlledState';
|
|
16
|
+
import ImageMapRouteContainer from './imageMapRouteContainer';
|
|
17
|
+
import ImageMapRoutePaths from './imageMapRoutePaths';
|
|
18
|
+
import ImageMapRoutePoints from './imageMapRoutePoints';
|
|
19
|
+
import { calculateCenterZoom } from './utils';
|
|
20
|
+
import { getClosestPointOnPath } from './utils/getClosestPointOnPath';
|
|
21
|
+
export default function ImageMapRoute(_a) {
|
|
22
|
+
var { ref, points, addPoint, activeSpot: _activeSpot, setActiveSpot: _setActiveSpot, RenderPoint, RenderPath, RenderExtra, deps, getInitialPosition = () => ({ scale: 1, offset: { x: 0, y: 0 } }), getAnimatedPosition, followActiveSpot, sx, children } = _a, props = __rest(_a, ["ref", "points", "addPoint", "activeSpot", "setActiveSpot", "RenderPoint", "RenderPath", "RenderExtra", "deps", "getInitialPosition", "getAnimatedPosition", "followActiveSpot", "sx", "children"]);
|
|
23
|
+
const internalRef = useRef(null);
|
|
24
|
+
const containerRef = ref !== null && ref !== void 0 ? ref : internalRef;
|
|
25
|
+
const containerSize = useBoundingClientRect(containerRef);
|
|
26
|
+
const [scale, setScale] = useState(1);
|
|
27
|
+
const [mapOffset, setMapOffset] = useState({ x: 0, y: 0 });
|
|
28
|
+
const [isAnimating, setIsAnimating] = useState(false);
|
|
29
|
+
const [activeSpot, setActiveSpot] = useControlledState(_activeSpot, _setActiveSpot);
|
|
30
|
+
const [hoverSpot, setHoverSpot] = useState(null);
|
|
31
|
+
useEffect(() => {
|
|
32
|
+
if (!points || !(containerSize === null || containerSize === void 0 ? void 0 : containerSize.width) || !(containerSize === null || containerSize === void 0 ? void 0 : containerSize.height))
|
|
33
|
+
return;
|
|
34
|
+
const { scale, offset } = getInitialPosition(containerSize);
|
|
35
|
+
if (scale)
|
|
36
|
+
setScale(scale);
|
|
37
|
+
if (offset)
|
|
38
|
+
setMapOffset(offset);
|
|
39
|
+
setIsAnimating(false);
|
|
40
|
+
if (!getAnimatedPosition)
|
|
41
|
+
return;
|
|
42
|
+
const animationFrame = setTimeout(() => {
|
|
43
|
+
const { scale, offset } = getAnimatedPosition(containerSize);
|
|
44
|
+
if (scale)
|
|
45
|
+
setScale(scale);
|
|
46
|
+
if (offset)
|
|
47
|
+
setMapOffset(offset);
|
|
48
|
+
setIsAnimating(true);
|
|
49
|
+
setTimeout(() => setIsAnimating(false), 2000);
|
|
50
|
+
}, 1000);
|
|
51
|
+
return () => clearTimeout(animationFrame);
|
|
52
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
53
|
+
}, [deps, Boolean(points), Boolean(containerSize)]);
|
|
54
|
+
useEffect(() => {
|
|
55
|
+
if (!followActiveSpot ||
|
|
56
|
+
!(activeSpot === null || activeSpot === void 0 ? void 0 : activeSpot.point) ||
|
|
57
|
+
!(containerSize === null || containerSize === void 0 ? void 0 : containerSize.width) ||
|
|
58
|
+
!(containerSize === null || containerSize === void 0 ? void 0 : containerSize.height))
|
|
59
|
+
return;
|
|
60
|
+
requestAnimationFrame(() => {
|
|
61
|
+
const { offset } = calculateCenterZoom(activeSpot.point, containerSize, scale);
|
|
62
|
+
setMapOffset(offset);
|
|
63
|
+
});
|
|
64
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
65
|
+
}, [followActiveSpot, activeSpot === null || activeSpot === void 0 ? void 0 : activeSpot.point]);
|
|
66
|
+
return (_jsxs(ImageMapRouteContainer, Object.assign({ containerRef: containerRef, containerSize: containerSize, scale: scale, setScale: setScale, mapOffset: mapOffset, setMapOffset: setMapOffset, isAnimating: isAnimating, onHoverRoute: (point) => {
|
|
67
|
+
if (addPoint)
|
|
68
|
+
return;
|
|
69
|
+
setHoverSpot(getClosestPointOnPath(points, point.x, point.y, 15 / containerSize.width));
|
|
70
|
+
}, onClickRoute: (point) => {
|
|
71
|
+
if (addPoint)
|
|
72
|
+
return addPoint(point);
|
|
73
|
+
if (hoverSpot)
|
|
74
|
+
setActiveSpot(hoverSpot);
|
|
75
|
+
}, sx: sx }, props, { children: [children, containerSize && (_jsxs("svg", { style: { width: '100%', height: '100%' }, children: [_jsx(ImageMapRoutePaths, { containerSize: containerSize, scale: scale, points: points, RenderPath: RenderPath }), _jsx(ImageMapRoutePoints, { containerSize: containerSize, scale: scale, points: points, activeSpot: activeSpot, hoverSpot: hoverSpot, RenderPoint: RenderPoint }), RenderExtra && (_jsx(RenderExtra, { containerSize: containerSize, scale: scale, points: points }))] }))] })));
|
|
76
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import type { CSSProperties, Dispatch, HTMLAttributes, ReactNode, RefObject } from 'react';
|
|
2
|
+
import { type Point } from './types';
|
|
3
|
+
export default function ImageMapRouteContainer({ containerRef, containerSize, scale, setScale, mapOffset, setMapOffset, isAnimating, onHoverRoute, onClickRoute, sx, children, ...props }: {
|
|
4
|
+
containerRef: RefObject<HTMLDivElement>;
|
|
5
|
+
containerSize: DOMRect;
|
|
6
|
+
scale: number;
|
|
7
|
+
setScale: Dispatch<number>;
|
|
8
|
+
mapOffset: Point;
|
|
9
|
+
setMapOffset: Dispatch<Point>;
|
|
10
|
+
isAnimating: boolean;
|
|
11
|
+
onHoverRoute?: (point: {
|
|
12
|
+
x: number;
|
|
13
|
+
y: number;
|
|
14
|
+
}) => void;
|
|
15
|
+
onClickRoute?: (point: {
|
|
16
|
+
x: number;
|
|
17
|
+
y: number;
|
|
18
|
+
}) => void;
|
|
19
|
+
innerChildren?: ReactNode;
|
|
20
|
+
sx?: CSSProperties;
|
|
21
|
+
} & Omit<HTMLAttributes<HTMLDivElement>, 'ref'>): import("react/jsx-runtime").JSX.Element;
|
|
22
|
+
//# sourceMappingURL=imageMapRouteContainer.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"imageMapRouteContainer.d.ts","sourceRoot":"","sources":["../src/imageMapRouteContainer.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACX,aAAa,EACb,QAAQ,EACR,cAAc,EACd,SAAS,EACT,SAAS,EAET,MAAM,OAAO,CAAC;AAGf,OAAO,EAAE,KAAK,KAAK,EAAE,MAAM,SAAS,CAAC;AAIrC,MAAM,CAAC,OAAO,UAAU,sBAAsB,CAAC,EAC9C,YAAY,EACZ,aAAa,EACb,KAAK,EACL,QAAQ,EACR,SAAS,EACT,YAAY,EACZ,WAAW,EACX,YAAY,EACZ,YAAY,EACZ,EAAE,EACF,QAAQ,EACR,GAAG,KAAK,EACR,EAAE;IACF,YAAY,EAAE,SAAS,CAAC,cAAc,CAAC,CAAC;IACxC,aAAa,EAAE,OAAO,CAAC;IACvB,KAAK,EAAE,MAAM,CAAC;IACd,QAAQ,EAAE,QAAQ,CAAC,MAAM,CAAC,CAAC;IAC3B,SAAS,EAAE,KAAK,CAAC;IACjB,YAAY,EAAE,QAAQ,CAAC,KAAK,CAAC,CAAC;IAC9B,WAAW,EAAE,OAAO,CAAC;IACrB,YAAY,CAAC,EAAE,CAAC,KAAK,EAAE;QAAE,CAAC,EAAE,MAAM,CAAC;QAAC,CAAC,EAAE,MAAM,CAAA;KAAE,KAAK,IAAI,CAAC;IACzD,YAAY,CAAC,EAAE,CAAC,KAAK,EAAE;QAAE,CAAC,EAAE,MAAM,CAAC;QAAC,CAAC,EAAE,MAAM,CAAA;KAAE,KAAK,IAAI,CAAC;IACzD,aAAa,CAAC,EAAE,SAAS,CAAC;IAC1B,EAAE,CAAC,EAAE,aAAa,CAAC;CACnB,GAAG,IAAI,CAAC,cAAc,CAAC,cAAc,CAAC,EAAE,KAAK,CAAC,2CAoL9C"}
|
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
var __rest = (this && this.__rest) || function (s, e) {
|
|
2
|
+
var t = {};
|
|
3
|
+
for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p) && e.indexOf(p) < 0)
|
|
4
|
+
t[p] = s[p];
|
|
5
|
+
if (s != null && typeof Object.getOwnPropertySymbols === "function")
|
|
6
|
+
for (var i = 0, p = Object.getOwnPropertySymbols(s); i < p.length; i++) {
|
|
7
|
+
if (e.indexOf(p[i]) < 0 && Object.prototype.propertyIsEnumerable.call(s, p[i]))
|
|
8
|
+
t[p[i]] = s[p[i]];
|
|
9
|
+
}
|
|
10
|
+
return t;
|
|
11
|
+
};
|
|
12
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
13
|
+
import { useEffect, useRef } from 'react';
|
|
14
|
+
import { clamp } from 'remeda';
|
|
15
|
+
import { clampPosition } from './utils/clampPosition';
|
|
16
|
+
import { mouseToContainer } from './utils/mouseToContainer';
|
|
17
|
+
export default function ImageMapRouteContainer(_a) {
|
|
18
|
+
var { containerRef, containerSize, scale, setScale, mapOffset, setMapOffset, isAnimating, onHoverRoute, onClickRoute, sx, children } = _a, props = __rest(_a, ["containerRef", "containerSize", "scale", "setScale", "mapOffset", "setMapOffset", "isAnimating", "onHoverRoute", "onClickRoute", "sx", "children"]);
|
|
19
|
+
const isDragging = useRef(false);
|
|
20
|
+
const hasMoved = useRef(false);
|
|
21
|
+
const dragStart = useRef({ x: 0, y: 0 });
|
|
22
|
+
const lastTouchDistance = useRef(null);
|
|
23
|
+
const touchStartTime = useRef(0);
|
|
24
|
+
const interactionStartPos = useRef(null);
|
|
25
|
+
const { style } = props, restProps = __rest(props, ["style"]);
|
|
26
|
+
const startDrag = (clientX, clientY) => {
|
|
27
|
+
isDragging.current = true;
|
|
28
|
+
hasMoved.current = false;
|
|
29
|
+
interactionStartPos.current = { x: clientX, y: clientY };
|
|
30
|
+
dragStart.current = { x: clientX - mapOffset.x, y: clientY - mapOffset.y };
|
|
31
|
+
touchStartTime.current = Date.now();
|
|
32
|
+
};
|
|
33
|
+
const performDrag = (clientX, clientY) => {
|
|
34
|
+
if (!containerSize)
|
|
35
|
+
return;
|
|
36
|
+
hasMoved.current = true;
|
|
37
|
+
const newX = clientX - dragStart.current.x;
|
|
38
|
+
const newY = clientY - dragStart.current.y;
|
|
39
|
+
setMapOffset(clampPosition(containerSize, newX, newY, scale));
|
|
40
|
+
};
|
|
41
|
+
const performZoom = (zoomCenter, scaleDelta) => {
|
|
42
|
+
var _a, _b;
|
|
43
|
+
if (!containerSize)
|
|
44
|
+
return;
|
|
45
|
+
const newScale = clamp(scale * scaleDelta, { min: 1, max: 16 }) || 1;
|
|
46
|
+
const liveRect = (_b = (_a = containerRef.current) === null || _a === void 0 ? void 0 : _a.getBoundingClientRect()) !== null && _b !== void 0 ? _b : containerSize;
|
|
47
|
+
const mouseX = zoomCenter.x - liveRect.left;
|
|
48
|
+
const mouseY = zoomCenter.y - liveRect.top;
|
|
49
|
+
const centerX = containerSize.width / 2;
|
|
50
|
+
const centerY = containerSize.height / 2;
|
|
51
|
+
const imagePointX = (mouseX - centerX - mapOffset.x) / scale;
|
|
52
|
+
const imagePointY = (mouseY - centerY - mapOffset.y) / scale;
|
|
53
|
+
const newX = mouseX - centerX - imagePointX * newScale;
|
|
54
|
+
const newY = mouseY - centerY - imagePointY * newScale;
|
|
55
|
+
setScale(newScale);
|
|
56
|
+
setMapOffset(clampPosition(containerSize, newX, newY, newScale));
|
|
57
|
+
};
|
|
58
|
+
const handleClick = (click) => {
|
|
59
|
+
var _a, _b;
|
|
60
|
+
if (!containerSize)
|
|
61
|
+
return;
|
|
62
|
+
const liveRect = (_b = (_a = containerRef.current) === null || _a === void 0 ? void 0 : _a.getBoundingClientRect()) !== null && _b !== void 0 ? _b : containerSize;
|
|
63
|
+
onClickRoute === null || onClickRoute === void 0 ? void 0 : onClickRoute(mouseToContainer({ clientX: click.x, clientY: click.y }, liveRect, mapOffset, scale));
|
|
64
|
+
};
|
|
65
|
+
const endDrag = () => {
|
|
66
|
+
isDragging.current = false;
|
|
67
|
+
lastTouchDistance.current = null;
|
|
68
|
+
interactionStartPos.current = null;
|
|
69
|
+
};
|
|
70
|
+
const getTouchDistance = (touches) => {
|
|
71
|
+
if (touches.length < 2)
|
|
72
|
+
return null;
|
|
73
|
+
const dx = touches[0].clientX - touches[1].clientX;
|
|
74
|
+
const dy = touches[0].clientY - touches[1].clientY;
|
|
75
|
+
return Math.sqrt(dx * dx + dy * dy);
|
|
76
|
+
};
|
|
77
|
+
const getTouchCenter = (touches) => {
|
|
78
|
+
if (touches.length < 2)
|
|
79
|
+
return null;
|
|
80
|
+
return {
|
|
81
|
+
x: (touches[0].clientX + touches[1].clientX) / 2,
|
|
82
|
+
y: (touches[0].clientY + touches[1].clientY) / 2,
|
|
83
|
+
};
|
|
84
|
+
};
|
|
85
|
+
useEffect(() => {
|
|
86
|
+
const element = containerRef.current;
|
|
87
|
+
if (!element)
|
|
88
|
+
return;
|
|
89
|
+
const handleWheel = (e) => {
|
|
90
|
+
e.preventDefault();
|
|
91
|
+
const delta = e.deltaY > 0 ? 0.9 : 1.1;
|
|
92
|
+
performZoom({ x: e.clientX, y: e.clientY }, delta);
|
|
93
|
+
};
|
|
94
|
+
element.addEventListener('wheel', handleWheel, { passive: false });
|
|
95
|
+
return () => element.removeEventListener('wheel', handleWheel);
|
|
96
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
97
|
+
}, [containerRef, containerSize, scale, mapOffset]);
|
|
98
|
+
const outerStyle = Object.assign(Object.assign({ overflow: 'hidden', cursor: 'crosshair', touchAction: 'none', backgroundColor: 'black' }, sx), style);
|
|
99
|
+
return (_jsx("div", Object.assign({ ref: containerRef, style: outerStyle, onMouseDown: (e) => {
|
|
100
|
+
if (e.button !== 0 && e.button !== 2)
|
|
101
|
+
return;
|
|
102
|
+
if (e.button === 2)
|
|
103
|
+
e.preventDefault();
|
|
104
|
+
startDrag(e.clientX, e.clientY);
|
|
105
|
+
}, onMouseMove: (e) => {
|
|
106
|
+
if (isDragging.current) {
|
|
107
|
+
performDrag(e.clientX, e.clientY);
|
|
108
|
+
return;
|
|
109
|
+
}
|
|
110
|
+
if (!containerSize)
|
|
111
|
+
return;
|
|
112
|
+
const liveRect = e.currentTarget.getBoundingClientRect();
|
|
113
|
+
onHoverRoute === null || onHoverRoute === void 0 ? void 0 : onHoverRoute(mouseToContainer(e, liveRect, mapOffset, scale));
|
|
114
|
+
}, onMouseUp: endDrag, onClick: (e) => {
|
|
115
|
+
if (hasMoved.current) {
|
|
116
|
+
hasMoved.current = false;
|
|
117
|
+
return;
|
|
118
|
+
}
|
|
119
|
+
if (!containerSize)
|
|
120
|
+
return;
|
|
121
|
+
const liveRect = e.currentTarget.getBoundingClientRect();
|
|
122
|
+
onClickRoute === null || onClickRoute === void 0 ? void 0 : onClickRoute(mouseToContainer(e, liveRect, mapOffset, scale));
|
|
123
|
+
}, onContextMenu: (e) => e.preventDefault(), onTouchStart: (e) => {
|
|
124
|
+
if (e.touches.length === 1) {
|
|
125
|
+
const touch = e.touches[0];
|
|
126
|
+
startDrag(touch.clientX, touch.clientY);
|
|
127
|
+
}
|
|
128
|
+
else if (e.touches.length === 2) {
|
|
129
|
+
lastTouchDistance.current = getTouchDistance(e.touches);
|
|
130
|
+
}
|
|
131
|
+
}, onTouchMove: (e) => {
|
|
132
|
+
e.preventDefault();
|
|
133
|
+
if (e.touches.length === 1) {
|
|
134
|
+
const touch = e.touches[0];
|
|
135
|
+
performDrag(touch.clientX, touch.clientY);
|
|
136
|
+
}
|
|
137
|
+
else if (e.touches.length === 2) {
|
|
138
|
+
const distance = getTouchDistance(e.touches);
|
|
139
|
+
const center = getTouchCenter(e.touches);
|
|
140
|
+
if (distance && center && lastTouchDistance.current) {
|
|
141
|
+
const scaleDelta = distance / lastTouchDistance.current;
|
|
142
|
+
performZoom(center, scaleDelta);
|
|
143
|
+
lastTouchDistance.current = distance;
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
}, onTouchEnd: (e) => {
|
|
147
|
+
if (e.touches.length === 0) {
|
|
148
|
+
const touchDuration = Date.now() - touchStartTime.current;
|
|
149
|
+
const wasTap = touchDuration < 200 && !isDragging.current;
|
|
150
|
+
if (wasTap && interactionStartPos.current) {
|
|
151
|
+
handleClick(interactionStartPos.current);
|
|
152
|
+
}
|
|
153
|
+
endDrag();
|
|
154
|
+
}
|
|
155
|
+
else if (e.touches.length === 1) {
|
|
156
|
+
lastTouchDistance.current = null;
|
|
157
|
+
}
|
|
158
|
+
} }, restProps, { children: _jsx("div", { style: {
|
|
159
|
+
position: 'relative',
|
|
160
|
+
transform: `translate(${mapOffset.x}px, ${mapOffset.y}px) scale(${scale})`,
|
|
161
|
+
transformOrigin: '50% 50%',
|
|
162
|
+
width: '100%',
|
|
163
|
+
height: '100%',
|
|
164
|
+
transition: isAnimating ? 'transform 1s ease' : 'none',
|
|
165
|
+
}, children: children }) })));
|
|
166
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { type ComponentType } from 'react';
|
|
2
|
+
import { type Point, type RenderPathProps } from './types';
|
|
3
|
+
export default function ImageMapRoutePaths({ containerSize, scale, points, RenderPath, }: {
|
|
4
|
+
containerSize: DOMRect;
|
|
5
|
+
scale: number;
|
|
6
|
+
points: Point[];
|
|
7
|
+
RenderPath?: ComponentType<RenderPathProps>;
|
|
8
|
+
}): import("react/jsx-runtime").JSX.Element;
|
|
9
|
+
//# sourceMappingURL=imageMapRoutePaths.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"imageMapRoutePaths.d.ts","sourceRoot":"","sources":["../src/imageMapRoutePaths.tsx"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,aAAa,EAAY,MAAM,OAAO,CAAC;AACrD,OAAO,EAAE,KAAK,KAAK,EAAE,KAAK,eAAe,EAAE,MAAM,SAAS,CAAC;AAW3D,MAAM,CAAC,OAAO,UAAU,kBAAkB,CAAC,EAC1C,aAAa,EACb,KAAK,EACL,MAAM,EACN,UAA8B,GAC9B,EAAE;IACF,aAAa,EAAE,OAAO,CAAC;IACvB,KAAK,EAAE,MAAM,CAAC;IACd,MAAM,EAAE,KAAK,EAAE,CAAC;IAChB,UAAU,CAAC,EAAE,aAAa,CAAC,eAAe,CAAC,CAAC;CAC5C,2CAmBA"}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { Fragment } from 'react';
|
|
3
|
+
function DefaultRenderPath({ point1, point2, containerSize }) {
|
|
4
|
+
const x1 = point1.x * containerSize.width;
|
|
5
|
+
const y1 = point1.y * containerSize.height;
|
|
6
|
+
const x2 = point2.x * containerSize.width;
|
|
7
|
+
const y2 = point2.y * containerSize.height;
|
|
8
|
+
return _jsx("line", { x1: x1, y1: y1, x2: x2, y2: y2, stroke: 'white' });
|
|
9
|
+
}
|
|
10
|
+
export default function ImageMapRoutePaths({ containerSize, scale, points, RenderPath = DefaultRenderPath, }) {
|
|
11
|
+
var _a;
|
|
12
|
+
return (_jsx(Fragment, { children: (_a = points === null || points === void 0 ? void 0 : points.map) === null || _a === void 0 ? void 0 : _a.call(points, (point, i) => {
|
|
13
|
+
if (i === 0)
|
|
14
|
+
return null;
|
|
15
|
+
const prevPoint = points[i - 1];
|
|
16
|
+
return (_jsx(RenderPath, { point1: prevPoint, point2: point, containerSize: containerSize, scale: scale }, `path-${i}`));
|
|
17
|
+
}) }));
|
|
18
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { type ComponentType } from 'react';
|
|
2
|
+
import { type Point, type RenderPointProps, type Spot } from './types';
|
|
3
|
+
export default function ImageMapRoutePoints({ containerSize, scale, points, hoverSpot, activeSpot, RenderPoint, }: {
|
|
4
|
+
containerSize: DOMRect;
|
|
5
|
+
scale: number;
|
|
6
|
+
points: Point[];
|
|
7
|
+
hoverSpot: Spot;
|
|
8
|
+
activeSpot: Spot;
|
|
9
|
+
RenderPoint?: ComponentType<RenderPointProps>;
|
|
10
|
+
}): import("react/jsx-runtime").JSX.Element;
|
|
11
|
+
//# sourceMappingURL=imageMapRoutePoints.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"imageMapRoutePoints.d.ts","sourceRoot":"","sources":["../src/imageMapRoutePoints.tsx"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,aAAa,EAAY,MAAM,OAAO,CAAC;AACrD,OAAO,EAAE,KAAK,KAAK,EAAE,KAAK,gBAAgB,EAAE,KAAK,IAAI,EAAE,MAAM,SAAS,CAAC;AAcvE,MAAM,CAAC,OAAO,UAAU,mBAAmB,CAAC,EAC3C,aAAa,EACb,KAAK,EACL,MAAM,EACN,SAAS,EACT,UAAU,EACV,WAAgC,GAChC,EAAE;IACF,aAAa,EAAE,OAAO,CAAC;IACvB,KAAK,EAAE,MAAM,CAAC;IACd,MAAM,EAAE,KAAK,EAAE,CAAC;IAChB,SAAS,EAAE,IAAI,CAAC;IAChB,UAAU,EAAE,IAAI,CAAC;IACjB,WAAW,CAAC,EAAE,aAAa,CAAC,gBAAgB,CAAC,CAAC;CAC9C,2CA+BA"}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { Fragment } from 'react';
|
|
3
|
+
function DefaultRenderPoint({ point, containerSize, type }) {
|
|
4
|
+
return (_jsx("circle", { cx: point.x * containerSize.width, cy: point.y * containerSize.height, r: type ? 2 : 1, fill: type === 'active' ? 'blue' : 'lime', fillOpacity: type ? 0.5 : 1 }));
|
|
5
|
+
}
|
|
6
|
+
export default function ImageMapRoutePoints({ containerSize, scale, points, hoverSpot, activeSpot, RenderPoint = DefaultRenderPoint, }) {
|
|
7
|
+
return (_jsxs(Fragment, { children: [points === null || points === void 0 ? void 0 : points.map((point, i) => (_jsx(RenderPoint, { point: point, containerSize: containerSize, scale: scale }, `point-${i}`))), hoverSpot && (_jsx(RenderPoint, { point: hoverSpot.point, containerSize: containerSize, scale: scale, percentage: hoverSpot.percentage, type: 'hover' })), activeSpot && (_jsx(RenderPoint, { point: activeSpot.point, containerSize: containerSize, scale: scale, percentage: activeSpot.percentage, type: 'active' }))] }));
|
|
8
|
+
}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,IAAI,UAAU,EAAE,MAAM,iBAAiB,CAAC;AACxD,cAAc,SAAS,CAAC;AACxB,cAAc,SAAS,CAAC;AACxB,OAAO,EAAE,OAAO,IAAI,iBAAiB,EAAE,MAAM,qBAAqB,CAAC"}
|
package/dist/index.js
ADDED
package/dist/types.d.ts
ADDED
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import type { ComponentType, CSSProperties, Dispatch, HTMLAttributes, ReactNode, RefObject } from 'react';
|
|
2
|
+
export type Point = {
|
|
3
|
+
x: number;
|
|
4
|
+
y: number;
|
|
5
|
+
marked?: number;
|
|
6
|
+
start?: number;
|
|
7
|
+
end?: number;
|
|
8
|
+
type?: string;
|
|
9
|
+
extra?: string;
|
|
10
|
+
};
|
|
11
|
+
export type Spot = {
|
|
12
|
+
point: {
|
|
13
|
+
x: number;
|
|
14
|
+
y: number;
|
|
15
|
+
};
|
|
16
|
+
pointIndex?: number;
|
|
17
|
+
percentage?: number;
|
|
18
|
+
};
|
|
19
|
+
export type MapImageRouteProps = {
|
|
20
|
+
ref?: RefObject<HTMLDivElement>;
|
|
21
|
+
points: Point[];
|
|
22
|
+
addPoint?: Dispatch<Point>;
|
|
23
|
+
activeSpot?: Spot;
|
|
24
|
+
setActiveSpot?: Dispatch<Spot>;
|
|
25
|
+
followActiveSpot?: boolean;
|
|
26
|
+
RenderPoint?: ComponentType<RenderPointProps>;
|
|
27
|
+
RenderPath?: ComponentType<RenderPathProps>;
|
|
28
|
+
RenderExtra?: ComponentType<RenderExtraProps>;
|
|
29
|
+
deps?: string;
|
|
30
|
+
getInitialPosition?: (containerSize: DOMRect) => {
|
|
31
|
+
scale?: number;
|
|
32
|
+
offset?: {
|
|
33
|
+
x: number;
|
|
34
|
+
y: number;
|
|
35
|
+
};
|
|
36
|
+
};
|
|
37
|
+
getAnimatedPosition?: (containerSize: DOMRect) => {
|
|
38
|
+
scale?: number;
|
|
39
|
+
offset?: {
|
|
40
|
+
x: number;
|
|
41
|
+
y: number;
|
|
42
|
+
};
|
|
43
|
+
};
|
|
44
|
+
innerChildren?: ReactNode;
|
|
45
|
+
sx?: CSSProperties;
|
|
46
|
+
} & HTMLAttributes<HTMLDivElement>;
|
|
47
|
+
export type RenderPointProps = {
|
|
48
|
+
point: Point;
|
|
49
|
+
containerSize: DOMRect;
|
|
50
|
+
scale: number;
|
|
51
|
+
percentage?: number;
|
|
52
|
+
type?: string;
|
|
53
|
+
};
|
|
54
|
+
export type RenderPathProps = {
|
|
55
|
+
point1: Point;
|
|
56
|
+
point2: Point;
|
|
57
|
+
containerSize: DOMRect;
|
|
58
|
+
scale: number;
|
|
59
|
+
};
|
|
60
|
+
export type RenderExtraProps = {
|
|
61
|
+
containerSize: DOMRect;
|
|
62
|
+
scale: number;
|
|
63
|
+
points: Point[];
|
|
64
|
+
};
|
|
65
|
+
//# sourceMappingURL=types.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACX,aAAa,EACb,aAAa,EACb,QAAQ,EACR,cAAc,EACd,SAAS,EACT,SAAS,EACT,MAAM,OAAO,CAAC;AAEf,MAAM,MAAM,KAAK,GAAG;IACnB,CAAC,EAAE,MAAM,CAAC;IACV,CAAC,EAAE,MAAM,CAAC;IACV,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,KAAK,CAAC,EAAE,MAAM,CAAC;CACf,CAAC;AAEF,MAAM,MAAM,IAAI,GAAG;IAClB,KAAK,EAAE;QAAE,CAAC,EAAE,MAAM,CAAC;QAAC,CAAC,EAAE,MAAM,CAAA;KAAE,CAAC;IAChC,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,UAAU,CAAC,EAAE,MAAM,CAAC;CACpB,CAAC;AAEF,MAAM,MAAM,kBAAkB,GAAG;IAChC,GAAG,CAAC,EAAE,SAAS,CAAC,cAAc,CAAC,CAAC;IAChC,MAAM,EAAE,KAAK,EAAE,CAAC;IAChB,QAAQ,CAAC,EAAE,QAAQ,CAAC,KAAK,CAAC,CAAC;IAC3B,UAAU,CAAC,EAAE,IAAI,CAAC;IAClB,aAAa,CAAC,EAAE,QAAQ,CAAC,IAAI,CAAC,CAAC;IAC/B,gBAAgB,CAAC,EAAE,OAAO,CAAC;IAC3B,WAAW,CAAC,EAAE,aAAa,CAAC,gBAAgB,CAAC,CAAC;IAC9C,UAAU,CAAC,EAAE,aAAa,CAAC,eAAe,CAAC,CAAC;IAC5C,WAAW,CAAC,EAAE,aAAa,CAAC,gBAAgB,CAAC,CAAC;IAC9C,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,kBAAkB,CAAC,EAAE,CAAC,aAAa,EAAE,OAAO,KAAK;QAChD,KAAK,CAAC,EAAE,MAAM,CAAC;QACf,MAAM,CAAC,EAAE;YAAE,CAAC,EAAE,MAAM,CAAC;YAAC,CAAC,EAAE,MAAM,CAAA;SAAE,CAAC;KAClC,CAAC;IACF,mBAAmB,CAAC,EAAE,CAAC,aAAa,EAAE,OAAO,KAAK;QACjD,KAAK,CAAC,EAAE,MAAM,CAAC;QACf,MAAM,CAAC,EAAE;YAAE,CAAC,EAAE,MAAM,CAAC;YAAC,CAAC,EAAE,MAAM,CAAA;SAAE,CAAC;KAClC,CAAC;IACF,aAAa,CAAC,EAAE,SAAS,CAAC;IAC1B,EAAE,CAAC,EAAE,aAAa,CAAC;CACnB,GAAG,cAAc,CAAC,cAAc,CAAC,CAAC;AAEnC,MAAM,MAAM,gBAAgB,GAAG;IAC9B,KAAK,EAAE,KAAK,CAAC;IACb,aAAa,EAAE,OAAO,CAAC;IACvB,KAAK,EAAE,MAAM,CAAC;IACd,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,IAAI,CAAC,EAAE,MAAM,CAAC;CACd,CAAC;AAEF,MAAM,MAAM,eAAe,GAAG;IAC7B,MAAM,EAAE,KAAK,CAAC;IACd,MAAM,EAAE,KAAK,CAAC;IACd,aAAa,EAAE,OAAO,CAAC;IACvB,KAAK,EAAE,MAAM,CAAC;CACd,CAAC;AAEF,MAAM,MAAM,gBAAgB,GAAG;IAC9B,aAAa,EAAE,OAAO,CAAC;IACvB,KAAK,EAAE,MAAM,CAAC;IACd,MAAM,EAAE,KAAK,EAAE,CAAC;CAChB,CAAC"}
|
package/dist/types.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { type Point, type Spot } from './types';
|
|
2
|
+
export default function useRouteVideoSync(points: Point[]): {
|
|
3
|
+
routeRef: import("react").RefObject<HTMLDivElement>;
|
|
4
|
+
videoRef: import("react").RefObject<HTMLVideoElement>;
|
|
5
|
+
time: number;
|
|
6
|
+
setTime: (time: number) => void;
|
|
7
|
+
activeSpot: Spot;
|
|
8
|
+
setActiveSpot: (spot: Spot) => void;
|
|
9
|
+
};
|
|
10
|
+
//# sourceMappingURL=useRouteVideoSync.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"useRouteVideoSync.d.ts","sourceRoot":"","sources":["../src/useRouteVideoSync.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,KAAK,KAAK,EAAE,KAAK,IAAI,EAAE,MAAM,SAAS,CAAC;AAGhD,MAAM,CAAC,OAAO,UAAU,iBAAiB,CAAC,MAAM,EAAE,KAAK,EAAE;;;;oBAiDvC,MAAM;;0BAKA,IAAI;EAU3B"}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import { useEffect, useRef, useState } from 'react';
|
|
2
|
+
import { useVideo } from 'rooks';
|
|
3
|
+
import { findSpotByTime, findTimeBySpot } from './utils';
|
|
4
|
+
export default function useRouteVideoSync(points) {
|
|
5
|
+
const routeRef = useRef(null);
|
|
6
|
+
const [videoRef, videoState, videoControls] = useVideo();
|
|
7
|
+
const animationFrameRef = useRef(null);
|
|
8
|
+
const [time, setTime] = useState(0);
|
|
9
|
+
const [activeSpot, setActiveSpot] = useState(null);
|
|
10
|
+
// Sync activeSpot with time
|
|
11
|
+
useEffect(() => {
|
|
12
|
+
const spot = findSpotByTime(points, time);
|
|
13
|
+
if (!spot)
|
|
14
|
+
return;
|
|
15
|
+
setActiveSpot(spot);
|
|
16
|
+
}, [points, time]);
|
|
17
|
+
useEffect(() => {
|
|
18
|
+
videoControls.pause();
|
|
19
|
+
videoControls.setCurrentTime(0);
|
|
20
|
+
setTime(0);
|
|
21
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
22
|
+
}, [Boolean(points)]);
|
|
23
|
+
useEffect(() => {
|
|
24
|
+
const updateTime = () => {
|
|
25
|
+
if (videoRef.current && !videoState.isPaused) {
|
|
26
|
+
setTime(videoRef.current.currentTime);
|
|
27
|
+
}
|
|
28
|
+
animationFrameRef.current = requestAnimationFrame(updateTime);
|
|
29
|
+
};
|
|
30
|
+
if (!videoState.isPaused) {
|
|
31
|
+
animationFrameRef.current = requestAnimationFrame(updateTime);
|
|
32
|
+
}
|
|
33
|
+
return () => {
|
|
34
|
+
if (animationFrameRef.current) {
|
|
35
|
+
cancelAnimationFrame(animationFrameRef.current);
|
|
36
|
+
}
|
|
37
|
+
};
|
|
38
|
+
}, [videoRef, videoState.isPaused]);
|
|
39
|
+
useEffect(() => {
|
|
40
|
+
if (videoState.isPaused)
|
|
41
|
+
setTime(videoState.currentTime);
|
|
42
|
+
}, [videoState]);
|
|
43
|
+
return {
|
|
44
|
+
routeRef,
|
|
45
|
+
videoRef,
|
|
46
|
+
time,
|
|
47
|
+
setTime: (time) => {
|
|
48
|
+
videoRef.current.currentTime = time;
|
|
49
|
+
setTime(time);
|
|
50
|
+
},
|
|
51
|
+
activeSpot,
|
|
52
|
+
setActiveSpot: (spot) => {
|
|
53
|
+
setActiveSpot(spot);
|
|
54
|
+
const calculatedTime = findTimeBySpot(points, spot);
|
|
55
|
+
if (calculatedTime !== null) {
|
|
56
|
+
setTime(calculatedTime);
|
|
57
|
+
videoControls.setCurrentTime(calculatedTime);
|
|
58
|
+
}
|
|
59
|
+
},
|
|
60
|
+
};
|
|
61
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"calculateCenterZoom.d.ts","sourceRoot":"","sources":["../../src/utils/calculateCenterZoom.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,KAAK,EAAE,MAAM,UAAU,CAAC;AAGtC,wBAAgB,mBAAmB,CAAC,KAAK,EAAE,KAAK,EAAE,aAAa,EAAE,OAAO,EAAE,KAAK,EAAE,MAAM;;;;;;EActF"}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { offsetToZoom } from './offsetToZoom';
|
|
2
|
+
export function calculateCenterZoom(point, containerSize, scale) {
|
|
3
|
+
if (!point || !(containerSize === null || containerSize === void 0 ? void 0 : containerSize.width) || !(containerSize === null || containerSize === void 0 ? void 0 : containerSize.height) || !scale) {
|
|
4
|
+
return { scale: 1, offset: { x: 0, y: 0 } };
|
|
5
|
+
}
|
|
6
|
+
// Convert normalized point (0-1) to pixel coordinates
|
|
7
|
+
const pointX = point.x * containerSize.width;
|
|
8
|
+
const pointY = point.y * containerSize.height;
|
|
9
|
+
// Calculate offset to center the point
|
|
10
|
+
const offsetX = (containerSize.width / 2 - pointX) * scale;
|
|
11
|
+
const offsetY = (containerSize.height / 2 - pointY) * scale;
|
|
12
|
+
return offsetToZoom(offsetX, offsetY, containerSize, scale);
|
|
13
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { type Point } from '../types';
|
|
2
|
+
export declare function calculateOptimalZoom(points: Point[], containerSize: DOMRect, zoom: number, maxScale?: number): {
|
|
3
|
+
scale: number;
|
|
4
|
+
offset: {
|
|
5
|
+
x: number;
|
|
6
|
+
y: number;
|
|
7
|
+
};
|
|
8
|
+
};
|
|
9
|
+
//# sourceMappingURL=calculateOptimalZoom.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"calculateOptimalZoom.d.ts","sourceRoot":"","sources":["../../src/utils/calculateOptimalZoom.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,KAAK,EAAE,MAAM,UAAU,CAAC;AAGtC,wBAAgB,oBAAoB,CACnC,MAAM,EAAE,KAAK,EAAE,EACf,aAAa,EAAE,OAAO,EACtB,IAAI,EAAE,MAAM,EACZ,QAAQ,GAAE,MAAU;;;;;;EAyCpB"}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { offsetToZoom } from './offsetToZoom';
|
|
2
|
+
export function calculateOptimalZoom(points, containerSize, zoom, maxScale = 2) {
|
|
3
|
+
if (!(points === null || points === void 0 ? void 0 : points.length) || !(containerSize === null || containerSize === void 0 ? void 0 : containerSize.width) || !(containerSize === null || containerSize === void 0 ? void 0 : containerSize.height) || !zoom) {
|
|
4
|
+
return { scale: 1, offset: { x: 0, y: 0 } };
|
|
5
|
+
}
|
|
6
|
+
// Find bounding box of all points (normalized coordinates)
|
|
7
|
+
let minX = Infinity, maxX = -Infinity, minY = Infinity, maxY = -Infinity;
|
|
8
|
+
for (const point of points) {
|
|
9
|
+
minX = Math.min(minX, point.x);
|
|
10
|
+
maxX = Math.max(maxX, point.x);
|
|
11
|
+
minY = Math.min(minY, point.y);
|
|
12
|
+
maxY = Math.max(maxY, point.y);
|
|
13
|
+
}
|
|
14
|
+
// Convert to pixel coordinates
|
|
15
|
+
const boundingBox = {
|
|
16
|
+
width: (maxX - minX) * containerSize.width,
|
|
17
|
+
height: (maxY - minY) * containerSize.height,
|
|
18
|
+
centerX: ((minX + maxX) / 2) * containerSize.width,
|
|
19
|
+
centerY: ((minY + maxY) / 2) * containerSize.height,
|
|
20
|
+
};
|
|
21
|
+
// Calculate scale to fit bounding box with some padding
|
|
22
|
+
const scaleX = (containerSize.width * zoom) / boundingBox.width;
|
|
23
|
+
const scaleY = (containerSize.height * zoom) / boundingBox.height;
|
|
24
|
+
const scale = Math.max(1, Math.min(scaleX, scaleY, maxScale));
|
|
25
|
+
// Calculate offset to center the bounding box
|
|
26
|
+
const containerCenterX = containerSize.width / 2;
|
|
27
|
+
const containerCenterY = containerSize.height / 2;
|
|
28
|
+
// We need to translate the image center by -offsetToBounding * scale
|
|
29
|
+
const offsetX = -(boundingBox.centerX - containerCenterX) * scale;
|
|
30
|
+
const offsetY = -(boundingBox.centerY - containerCenterY) * scale;
|
|
31
|
+
return offsetToZoom(offsetX, offsetY, containerSize, scale);
|
|
32
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"clampPosition.d.ts","sourceRoot":"","sources":["../../src/utils/clampPosition.ts"],"names":[],"mappings":"AAEA,wBAAgB,aAAa,CAAC,aAAa,EAAE,OAAO,EAAE,CAAC,EAAE,MAAM,EAAE,CAAC,EAAE,MAAM,EAAE,YAAY,EAAE,MAAM;;;EAY/F"}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { clamp } from 'remeda';
|
|
2
|
+
export function clampPosition(containerSize, x, y, currentScale) {
|
|
3
|
+
const imageWidth = containerSize.width * currentScale;
|
|
4
|
+
const imageHeight = containerSize.height * currentScale;
|
|
5
|
+
// Calculate bounds - with center origin, the offset represents the center position
|
|
6
|
+
const halfScaledWidth = (imageWidth - containerSize.width) / 2;
|
|
7
|
+
const halfScaledHeight = (imageHeight - containerSize.height) / 2;
|
|
8
|
+
return {
|
|
9
|
+
x: clamp(x, { min: -halfScaledWidth, max: halfScaledWidth }),
|
|
10
|
+
y: clamp(y, { min: -halfScaledHeight, max: halfScaledHeight }),
|
|
11
|
+
};
|
|
12
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"findSpotByTime.d.ts","sourceRoot":"","sources":["../../src/utils/findSpotByTime.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,KAAK,EAAE,KAAK,IAAI,EAAE,MAAM,UAAU,CAAC;AAGjD,wBAAgB,cAAc,CAAC,MAAM,EAAE,KAAK,EAAE,EAAE,IAAI,EAAE,MAAM,GAAG,IAAI,CAuElE"}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
// Find active spot based on time
|
|
2
|
+
export function findSpotByTime(points, time) {
|
|
3
|
+
var _a, _b, _c, _d, _e, _f, _g, _h;
|
|
4
|
+
if (!(points === null || points === void 0 ? void 0 : points.length))
|
|
5
|
+
return null;
|
|
6
|
+
let lastPoint = { point: points[0], i: 0, maxTime: 0 };
|
|
7
|
+
// Iterate through points to find the spot
|
|
8
|
+
for (let i = 0; i < points.length; i++) {
|
|
9
|
+
const point = points[i];
|
|
10
|
+
if (point.start === undefined && point.end === undefined && point.marked === undefined)
|
|
11
|
+
continue;
|
|
12
|
+
const minTime = (_b = (_a = point.start) !== null && _a !== void 0 ? _a : point.marked) !== null && _b !== void 0 ? _b : 0;
|
|
13
|
+
const maxTime = (_e = (_d = (_c = point.end) !== null && _c !== void 0 ? _c : point.marked) !== null && _d !== void 0 ? _d : point.start) !== null && _e !== void 0 ? _e : 0;
|
|
14
|
+
// Time is at this point
|
|
15
|
+
if (minTime <= time && time <= maxTime) {
|
|
16
|
+
return { point, pointIndex: i, percentage: 0 };
|
|
17
|
+
}
|
|
18
|
+
if (time > maxTime) {
|
|
19
|
+
lastPoint = { point, i, maxTime };
|
|
20
|
+
continue;
|
|
21
|
+
}
|
|
22
|
+
const startTime = (_h = (_g = (_f = point.start) !== null && _f !== void 0 ? _f : point.marked) !== null && _g !== void 0 ? _g : point.end) !== null && _h !== void 0 ? _h : 0;
|
|
23
|
+
const endTime = lastPoint.maxTime;
|
|
24
|
+
const segmentPoints = points.slice(lastPoint.i, i + 1);
|
|
25
|
+
// Calculate cumulative distances
|
|
26
|
+
const distances = [0];
|
|
27
|
+
let totalDistance = 0;
|
|
28
|
+
for (let j = 1; j < segmentPoints.length; j++) {
|
|
29
|
+
const dx = segmentPoints[j].x - segmentPoints[j - 1].x;
|
|
30
|
+
const dy = segmentPoints[j].y - segmentPoints[j - 1].y;
|
|
31
|
+
totalDistance += Math.sqrt(dx * dx + dy * dy);
|
|
32
|
+
distances.push(totalDistance);
|
|
33
|
+
}
|
|
34
|
+
if (totalDistance === 0) {
|
|
35
|
+
return { point: lastPoint.point, pointIndex: lastPoint.i, percentage: 0 };
|
|
36
|
+
}
|
|
37
|
+
// Interpolate position based on time
|
|
38
|
+
const timePercentage = (time - endTime) / (startTime - endTime);
|
|
39
|
+
const targetDistance = totalDistance * timePercentage;
|
|
40
|
+
// Find segment containing target distance
|
|
41
|
+
let segIdx = distances.findIndex((d, idx) => idx > 0 && targetDistance <= d) - 1;
|
|
42
|
+
if (segIdx < 0)
|
|
43
|
+
segIdx = distances.length - 2;
|
|
44
|
+
// Interpolate within segment
|
|
45
|
+
const segStart = distances[segIdx];
|
|
46
|
+
const segEnd = distances[segIdx + 1];
|
|
47
|
+
const segT = (targetDistance - segStart) / (segEnd - segStart);
|
|
48
|
+
const p1 = segmentPoints[segIdx];
|
|
49
|
+
const p2 = segmentPoints[segIdx + 1];
|
|
50
|
+
return {
|
|
51
|
+
point: { x: p1.x + (p2.x - p1.x) * segT, y: p1.y + (p2.y - p1.y) * segT },
|
|
52
|
+
pointIndex: lastPoint.i + segIdx,
|
|
53
|
+
percentage: Math.round(segT * 1000) / 10,
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
// Time is beyond all points, return last point
|
|
57
|
+
return {
|
|
58
|
+
point: points[points.length - 1],
|
|
59
|
+
pointIndex: points.length - 1,
|
|
60
|
+
percentage: 0,
|
|
61
|
+
};
|
|
62
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"findTimeBySpot.d.ts","sourceRoot":"","sources":["../../src/utils/findTimeBySpot.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,KAAK,EAAE,KAAK,IAAI,EAAE,MAAM,UAAU,CAAC;AAGjD,wBAAgB,cAAc,CAAC,MAAM,EAAE,KAAK,EAAE,EAAE,IAAI,EAAE,IAAI,UAiEzD"}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
// Find time based on spot
|
|
2
|
+
export function findTimeBySpot(points, spot) {
|
|
3
|
+
var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l;
|
|
4
|
+
if (!(points === null || points === void 0 ? void 0 : points.length) || !spot)
|
|
5
|
+
return null;
|
|
6
|
+
// Check if we're snapped directly to a point
|
|
7
|
+
if (spot.percentage === 100) {
|
|
8
|
+
const point = points[spot.pointIndex + 1];
|
|
9
|
+
const res = (_a = point.marked) !== null && _a !== void 0 ? _a : point.start;
|
|
10
|
+
if (res)
|
|
11
|
+
return res;
|
|
12
|
+
}
|
|
13
|
+
else if (spot.percentage === 0) {
|
|
14
|
+
const point = points[spot.pointIndex];
|
|
15
|
+
return (_c = (_b = point.marked) !== null && _b !== void 0 ? _b : point.start) !== null && _c !== void 0 ? _c : 0;
|
|
16
|
+
}
|
|
17
|
+
// Find timing segment boundaries in a single pass
|
|
18
|
+
let startIdx = 0;
|
|
19
|
+
let endIdx = points.length - 1;
|
|
20
|
+
for (let i = spot.pointIndex; i >= 0; i--) {
|
|
21
|
+
if ((_e = (_d = points[i].start) !== null && _d !== void 0 ? _d : points[i].marked) !== null && _e !== void 0 ? _e : points[i].end) {
|
|
22
|
+
startIdx = i;
|
|
23
|
+
break;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
for (let i = spot.pointIndex + 1; i < points.length; i++) {
|
|
27
|
+
if ((_g = (_f = points[i].start) !== null && _f !== void 0 ? _f : points[i].marked) !== null && _g !== void 0 ? _g : points[i].end) {
|
|
28
|
+
endIdx = i;
|
|
29
|
+
break;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
const startPoint = points[startIdx];
|
|
33
|
+
const endPoint = points[endIdx];
|
|
34
|
+
const startPointTime = (_j = (_h = startPoint.end) !== null && _h !== void 0 ? _h : startPoint.marked) !== null && _j !== void 0 ? _j : startPoint.start;
|
|
35
|
+
const endPointTime = (_l = (_k = endPoint.start) !== null && _k !== void 0 ? _k : endPoint.marked) !== null && _l !== void 0 ? _l : endPoint.end;
|
|
36
|
+
// Calculate cumulative distances along the path
|
|
37
|
+
const pathPoints = points.slice(startIdx, endIdx + 1);
|
|
38
|
+
const distances = [0];
|
|
39
|
+
let totalDistance = 0;
|
|
40
|
+
for (let i = 1; i < pathPoints.length; i++) {
|
|
41
|
+
const dx = pathPoints[i].x - pathPoints[i - 1].x;
|
|
42
|
+
const dy = pathPoints[i].y - pathPoints[i - 1].y;
|
|
43
|
+
totalDistance += Math.sqrt(dx * dx + dy * dy);
|
|
44
|
+
distances.push(totalDistance);
|
|
45
|
+
}
|
|
46
|
+
if (totalDistance === 0)
|
|
47
|
+
return startPointTime;
|
|
48
|
+
// Calculate distance to the spot
|
|
49
|
+
const pointIndexInPath = spot.pointIndex - startIdx;
|
|
50
|
+
const distanceToSegmentStart = distances[pointIndexInPath];
|
|
51
|
+
const segmentStart = pathPoints[pointIndexInPath];
|
|
52
|
+
const segmentEnd = pathPoints[pointIndexInPath + 1];
|
|
53
|
+
const dx = segmentEnd.x - segmentStart.x;
|
|
54
|
+
const dy = segmentEnd.y - segmentStart.y;
|
|
55
|
+
const segmentLength = Math.sqrt(dx * dx + dy * dy);
|
|
56
|
+
const totalDistanceToSpot = distanceToSegmentStart + segmentLength * (spot.percentage / 100);
|
|
57
|
+
// Interpolate time based on distance ratio
|
|
58
|
+
const distanceRatio = totalDistanceToSpot / totalDistance;
|
|
59
|
+
return startPointTime + (endPointTime - startPointTime) * distanceRatio;
|
|
60
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"getClosestPointOnPath.d.ts","sourceRoot":"","sources":["../../src/utils/getClosestPointOnPath.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,KAAK,KAAK,EAAE,KAAK,IAAI,EAAE,MAAM,UAAU,CAAC;AAGjD,wBAAgB,qBAAqB,CACpC,MAAM,EAAE,KAAK,EAAE,EACf,MAAM,EAAE,MAAM,EACd,MAAM,EAAE,MAAM,EACd,aAAa,EAAE,MAAM,GACnB,IAAI,CAuEN"}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import { clamp } from 'remeda';
|
|
2
|
+
// Calculate closest point on path with segment info and snapping
|
|
3
|
+
export function getClosestPointOnPath(points, mouseX, mouseY, snapThreshold) {
|
|
4
|
+
if (!points || points.length < 2)
|
|
5
|
+
return null;
|
|
6
|
+
let closestPoint = null;
|
|
7
|
+
let minDistance = Infinity;
|
|
8
|
+
let closestPointIndex = -1;
|
|
9
|
+
let segmentPercentage = 0;
|
|
10
|
+
// Check each segment
|
|
11
|
+
for (let i = 1; i < points.length; i++) {
|
|
12
|
+
const x1 = points[i - 1].x;
|
|
13
|
+
const y1 = points[i - 1].y;
|
|
14
|
+
const x2 = points[i].x;
|
|
15
|
+
const y2 = points[i].y;
|
|
16
|
+
const segmentLength = Math.pow(Math.sqrt(Math.pow((x2 - x1), 2) + Math.pow((y2 - y1), 2)), 2);
|
|
17
|
+
// Vector from start to end of segment
|
|
18
|
+
const dx = x2 - x1;
|
|
19
|
+
const dy = y2 - y1;
|
|
20
|
+
// Vector from start to mouse
|
|
21
|
+
const mx = mouseX - x1;
|
|
22
|
+
const my = mouseY - y1;
|
|
23
|
+
// Project mouse onto segment
|
|
24
|
+
const t = clamp((mx * dx + my * dy) / segmentLength, {
|
|
25
|
+
min: 0,
|
|
26
|
+
max: 1,
|
|
27
|
+
});
|
|
28
|
+
// Closest point on this segment
|
|
29
|
+
let px = x1 + t * dx;
|
|
30
|
+
let py = y1 + t * dy;
|
|
31
|
+
// Check if we should snap to start point
|
|
32
|
+
const distToStart = Math.sqrt(Math.pow((mouseX - x1), 2) + Math.pow((mouseY - y1), 2));
|
|
33
|
+
const distToEnd = Math.sqrt(Math.pow((mouseX - x2), 2) + Math.pow((mouseY - y2), 2));
|
|
34
|
+
let snappedT = t;
|
|
35
|
+
let preferredSegmentIndex = i - 1;
|
|
36
|
+
if (points[i - 1].marked && distToStart < snapThreshold && distToStart < distToEnd) {
|
|
37
|
+
px = x1;
|
|
38
|
+
py = y1;
|
|
39
|
+
snappedT = 0;
|
|
40
|
+
}
|
|
41
|
+
else if (points[i].marked && distToEnd < snapThreshold) {
|
|
42
|
+
px = x2;
|
|
43
|
+
py = y2;
|
|
44
|
+
preferredSegmentIndex = i;
|
|
45
|
+
snappedT = 0;
|
|
46
|
+
}
|
|
47
|
+
// Distance from mouse to closest point
|
|
48
|
+
const dist = Math.sqrt(Math.pow((mouseX - px), 2) + Math.pow((mouseY - py), 2));
|
|
49
|
+
if (dist < minDistance) {
|
|
50
|
+
minDistance = dist;
|
|
51
|
+
closestPoint = { x: px, y: py };
|
|
52
|
+
closestPointIndex = preferredSegmentIndex;
|
|
53
|
+
segmentPercentage = snappedT * 100;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
if (minDistance >= snapThreshold)
|
|
57
|
+
return null;
|
|
58
|
+
return {
|
|
59
|
+
point: closestPoint,
|
|
60
|
+
pointIndex: closestPointIndex,
|
|
61
|
+
percentage: Math.round(segmentPercentage * 10) / 10,
|
|
62
|
+
};
|
|
63
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/utils/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,mBAAmB,EAAE,MAAM,uBAAuB,CAAC;AAC5D,OAAO,EAAE,oBAAoB,EAAE,MAAM,wBAAwB,CAAC;AAC9D,OAAO,EAAE,cAAc,EAAE,MAAM,kBAAkB,CAAC;AAClD,OAAO,EAAE,cAAc,EAAE,MAAM,kBAAkB,CAAC"}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
export declare function mouseToContainer(mouse: {
|
|
2
|
+
clientX: number;
|
|
3
|
+
clientY: number;
|
|
4
|
+
}, containerSize: DOMRect, mapOffset: {
|
|
5
|
+
x: number;
|
|
6
|
+
y: number;
|
|
7
|
+
}, scale: number): {
|
|
8
|
+
x: number;
|
|
9
|
+
y: number;
|
|
10
|
+
};
|
|
11
|
+
//# sourceMappingURL=mouseToContainer.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"mouseToContainer.d.ts","sourceRoot":"","sources":["../../src/utils/mouseToContainer.ts"],"names":[],"mappings":"AAAA,wBAAgB,gBAAgB,CAC/B,KAAK,EAAE;IAAE,OAAO,EAAE,MAAM,CAAC;IAAC,OAAO,EAAE,MAAM,CAAA;CAAE,EAC3C,aAAa,EAAE,OAAO,EACtB,SAAS,EAAE;IAAE,CAAC,EAAE,MAAM,CAAC;IAAC,CAAC,EAAE,MAAM,CAAA;CAAE,EACnC,KAAK,EAAE,MAAM;;;EASb"}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
export function mouseToContainer(mouse, containerSize, mapOffset, scale) {
|
|
2
|
+
const centerX = containerSize.width / 2;
|
|
3
|
+
const centerY = containerSize.height / 2;
|
|
4
|
+
const mouseX = (mouse.clientX - containerSize.x - centerX - mapOffset.x) / scale;
|
|
5
|
+
const mouseY = (mouse.clientY - containerSize.y - centerY - mapOffset.y) / scale;
|
|
6
|
+
const normalizedX = (mouseX + centerX) / containerSize.width;
|
|
7
|
+
const normalizedY = (mouseY + centerY) / containerSize.height;
|
|
8
|
+
return { x: normalizedX, y: normalizedY };
|
|
9
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"offsetToZoom.d.ts","sourceRoot":"","sources":["../../src/utils/offsetToZoom.ts"],"names":[],"mappings":"AAEA,wBAAgB,YAAY,CAC3B,OAAO,EAAE,MAAM,EACf,OAAO,EAAE,MAAM,EACf,aAAa,EAAE,OAAO,EACtB,KAAK,EAAE,MAAM;;;;;;EAUb"}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { clamp } from 'remeda';
|
|
2
|
+
export function offsetToZoom(offsetX, offsetY, containerSize, scale) {
|
|
3
|
+
// Constrain offset to never show beyond image bounds
|
|
4
|
+
const halfScaledWidth = (containerSize.width * scale - containerSize.width) / 2;
|
|
5
|
+
const halfScaledHeight = (containerSize.height * scale - containerSize.height) / 2;
|
|
6
|
+
offsetX = clamp(offsetX, { min: -halfScaledWidth, max: halfScaledWidth });
|
|
7
|
+
offsetY = clamp(offsetY, { min: -halfScaledHeight, max: halfScaledHeight });
|
|
8
|
+
return { scale, offset: { x: offsetX, y: offsetY } };
|
|
9
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "image-map-route",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"private": false,
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "./dist/index.js",
|
|
7
|
+
"module": "./dist/index.js",
|
|
8
|
+
"types": "./dist/index.d.ts",
|
|
9
|
+
"exports": {
|
|
10
|
+
".": {
|
|
11
|
+
"types": "./dist/index.d.ts",
|
|
12
|
+
"import": "./dist/index.js"
|
|
13
|
+
}
|
|
14
|
+
},
|
|
15
|
+
"files": [
|
|
16
|
+
"dist"
|
|
17
|
+
],
|
|
18
|
+
"scripts": {
|
|
19
|
+
"build": "tsc -p tsconfig.build.json",
|
|
20
|
+
"publish": "npm publish --access public",
|
|
21
|
+
"prepack": "npm run build"
|
|
22
|
+
},
|
|
23
|
+
"dependencies": {
|
|
24
|
+
"remeda": "2.33.6",
|
|
25
|
+
"rooks": "9.5.0"
|
|
26
|
+
},
|
|
27
|
+
"peerDependencies": {
|
|
28
|
+
"react": ">=18"
|
|
29
|
+
},
|
|
30
|
+
"devDependencies": {
|
|
31
|
+
"@types/react": "19.2.14",
|
|
32
|
+
"typescript": "5.9.3"
|
|
33
|
+
}
|
|
34
|
+
}
|