@mmmmzxe/react-360-viewer 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 +214 -0
- package/dist/index.d.ts +377 -0
- package/dist/index.js +1237 -0
- package/dist/index.js.map +1 -0
- package/package.json +74 -0
- package/src/components/ui/Badge/index.tsx +44 -0
- package/src/components/ui/Button/index.tsx +68 -0
- package/src/components/ui/Card/index.tsx +74 -0
- package/src/components/ui/Item/index.tsx +136 -0
- package/src/components/ui/Label/index.tsx +20 -0
- package/src/components/ui/Popover/index.tsx +56 -0
- package/src/components/ui/Separator/index.tsx +30 -0
- package/src/components/ui/Spinner/index.tsx +11 -0
- package/src/components/utils/index.ts +6 -0
- package/src/constants/viewer360ClassNames.ts +41 -0
- package/src/constants/viewer360Config.ts +11 -0
- package/src/constants/viewer360Labels.ts +14 -0
- package/src/constants/viewer360MarkerLabels.ts +3 -0
- package/src/feature/Viewer360.test.tsx +47 -0
- package/src/feature/Viewer360.tsx +218 -0
- package/src/feature/Viewer360AddModeBanner.tsx +20 -0
- package/src/feature/Viewer360FrameIndicator.tsx +20 -0
- package/src/feature/Viewer360HotspotOverlay.tsx +70 -0
- package/src/feature/Viewer360LoadingOverlay.tsx +28 -0
- package/src/feature/Viewer360MarkerPin.tsx +115 -0
- package/src/feature/Viewer360Toolbar.tsx +75 -0
- package/src/helpers/adjustViewerZoom.test.ts +29 -0
- package/src/helpers/adjustViewerZoom.ts +64 -0
- package/src/helpers/computeDragFrameIndex.test.ts +20 -0
- package/src/helpers/computeDragFrameIndex.ts +23 -0
- package/src/helpers/computeViewerImageLayout.test.ts +48 -0
- package/src/helpers/computeViewerImageLayout.ts +114 -0
- package/src/helpers/computeViewerPanOffset.ts +18 -0
- package/src/helpers/markerHelpers.test.ts +38 -0
- package/src/helpers/markerHelpers.ts +33 -0
- package/src/helpers/viewer360PropsHelpers.ts +46 -0
- package/src/helpers/viewerHelpers.ts +74 -0
- package/src/hooks/useViewer360.ts +306 -0
- package/src/index.ts +68 -0
- package/src/types/Viewer360Hotspot.ts +66 -0
- package/src/types/Viewer360Marker.ts +51 -0
- package/src/types/Viewer360Props.ts +108 -0
- package/src/types/index.ts +30 -0
- package/src/utils/index.ts +6 -0
package/README.md
ADDED
|
@@ -0,0 +1,214 @@
|
|
|
1
|
+
# @mmmmzxe/react-360-viewer
|
|
2
|
+
|
|
3
|
+
A standalone, configurable 360° image viewer for React. Drag to rotate through frames, scroll or use controls to zoom, optionally auto-rotate, and place interactive hotspots on any frame.
|
|
4
|
+
|
|
5
|
+
## Features
|
|
6
|
+
|
|
7
|
+
- Drag-to-rotate frame navigation
|
|
8
|
+
- Zoom via scroll wheel or toolbar controls
|
|
9
|
+
- Pan when zoomed in
|
|
10
|
+
- Optional auto-rotate
|
|
11
|
+
- Hotspot support with built-in marker pins
|
|
12
|
+
- shadcn-style UI with Tailwind CSS design tokens
|
|
13
|
+
- TypeScript-first API
|
|
14
|
+
- Peer dependencies: React, lucide-react, and a Tailwind setup in the host app
|
|
15
|
+
|
|
16
|
+
## Installation
|
|
17
|
+
|
|
18
|
+
```bash
|
|
19
|
+
npm install @mmmmzxe/react-360-viewer
|
|
20
|
+
# or
|
|
21
|
+
bun add @mmmmzxe/react-360-viewer
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
Peer dependencies: `react`, `react-dom` (v18+), and `lucide-react`.
|
|
25
|
+
|
|
26
|
+
### Tailwind setup
|
|
27
|
+
|
|
28
|
+
The package uses Tailwind utility classes with shadcn design tokens (`bg-card`, `border-border`, `text-muted-foreground`, etc.). Add the package source to your Tailwind CSS entry so those classes are generated:
|
|
29
|
+
|
|
30
|
+
```css
|
|
31
|
+
@source "../node_modules/@mmmmzxe/react-360-viewer/src/**/*.{ts,tsx}";
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
Your host app should also define the standard shadcn CSS variables (`--background`, `--foreground`, `--primary`, `--border`, `--muted-foreground`, and related tokens) in your global stylesheet.
|
|
35
|
+
|
|
36
|
+
## Quick start
|
|
37
|
+
|
|
38
|
+
```tsx
|
|
39
|
+
import { useState } from 'react';
|
|
40
|
+
import { Viewer360, type Viewer360Frame } from '@mmmmzxe/react-360-viewer';
|
|
41
|
+
|
|
42
|
+
const frames: Viewer360Frame[] = [
|
|
43
|
+
{ id: '1', src: '/images/frame-01.jpg', label: 'Front' },
|
|
44
|
+
{ id: '2', src: '/images/frame-02.jpg', label: 'Front-right' },
|
|
45
|
+
// ...
|
|
46
|
+
];
|
|
47
|
+
|
|
48
|
+
export function ProductViewer() {
|
|
49
|
+
const [frameIndex, setFrameIndex] = useState(0);
|
|
50
|
+
|
|
51
|
+
return (
|
|
52
|
+
<Viewer360
|
|
53
|
+
frames={frames}
|
|
54
|
+
currentFrameIndex={frameIndex}
|
|
55
|
+
onFrameChange={setFrameIndex}
|
|
56
|
+
config={{ dragSensitivity: 8, autoRotate: false }}
|
|
57
|
+
/>
|
|
58
|
+
);
|
|
59
|
+
}
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
## Hotspots & markers
|
|
63
|
+
|
|
64
|
+
Hotspots are percentage-based positions (`positionX`, `positionY` from 0–100) tied to a specific frame.
|
|
65
|
+
|
|
66
|
+
### Built-in marker pins
|
|
67
|
+
|
|
68
|
+
Use `hotspotPin` for tooltip pins with optional delete — no custom UI required:
|
|
69
|
+
|
|
70
|
+
```tsx
|
|
71
|
+
import { Viewer360, toViewer360Hotspots } from '@mmmmzxe/react-360-viewer';
|
|
72
|
+
|
|
73
|
+
const hotspots = toViewer360Hotspots([
|
|
74
|
+
{ id: '1', frameIndex: 0, positionX: 42, positionY: 58, title: 'Scratch', description: 'Front bumper' },
|
|
75
|
+
]);
|
|
76
|
+
|
|
77
|
+
<Viewer360
|
|
78
|
+
frames={frames}
|
|
79
|
+
hotspots={hotspots}
|
|
80
|
+
showHotspotModeControl
|
|
81
|
+
hotspotPin={{
|
|
82
|
+
onDelete: (id) => removeMarker(id),
|
|
83
|
+
deletingMarkerId: pendingDeleteId,
|
|
84
|
+
}}
|
|
85
|
+
onHotspotAdd={(position) => saveMarker(position)}
|
|
86
|
+
/>
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
Use `renderTag` inside `hotspotPin` for badges (e.g. damage type). Use `renderHotspot` only when you need fully custom marker UI.
|
|
90
|
+
|
|
91
|
+
### Custom hotspot UI
|
|
92
|
+
|
|
93
|
+
```tsx
|
|
94
|
+
import { Viewer360, type Viewer360Hotspot } from '@mmmmzxe/react-360-viewer';
|
|
95
|
+
|
|
96
|
+
type DamageHotspot = { title: string; severity: 'low' | 'high' };
|
|
97
|
+
|
|
98
|
+
const hotspots: Viewer360Hotspot<DamageHotspot>[] = [
|
|
99
|
+
{ id: 'a', frameIndex: 0, positionX: 42, positionY: 58, data: { title: 'Scratch', severity: 'low' } },
|
|
100
|
+
];
|
|
101
|
+
|
|
102
|
+
<Viewer360
|
|
103
|
+
frames={frames}
|
|
104
|
+
hotspots={hotspots}
|
|
105
|
+
showHotspotModeControl
|
|
106
|
+
onHotspotAdd={(position) => {
|
|
107
|
+
// position: { frameIndex, frameId, positionX, positionY }
|
|
108
|
+
console.log('Place hotspot at', position);
|
|
109
|
+
}}
|
|
110
|
+
renderHotspot={({ hotspot, leftPercent, topPercent }) => (
|
|
111
|
+
<button
|
|
112
|
+
style={{ position: 'absolute', left: `${leftPercent}%`, top: `${topPercent}%` }}
|
|
113
|
+
onClick={() => alert(hotspot.data?.title)}
|
|
114
|
+
/>
|
|
115
|
+
)}
|
|
116
|
+
/>
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
When hotspot add mode is active, clicking the viewport calls `onHotspotAdd` with normalized coordinates that stay aligned while zooming or panning.
|
|
120
|
+
|
|
121
|
+
## Configuration
|
|
122
|
+
|
|
123
|
+
| Prop | Type | Default | Description |
|
|
124
|
+
|------|------|---------|-------------|
|
|
125
|
+
| `frames` | `Viewer360Frame[]` | required | Image sources |
|
|
126
|
+
| `config.minZoom` | `number` | `1` | Minimum zoom level |
|
|
127
|
+
| `config.maxZoom` | `number` | `3` | Maximum zoom level |
|
|
128
|
+
| `config.zoomStep` | `number` | `0.15` | Zoom increment |
|
|
129
|
+
| `config.dragSensitivity` | `number` | `8` | Pixels per frame while dragging |
|
|
130
|
+
| `config.autoRotate` | `boolean` | `false` | Enable automatic rotation |
|
|
131
|
+
| `config.autoRotateIntervalMs` | `number` | `100` | Delay between auto-rotate steps |
|
|
132
|
+
| `config.autoRotateDirection` | `'forward' \| 'backward'` | `'forward'` | Auto-rotate direction |
|
|
133
|
+
| `showZoomControls` | `boolean` | `true` | Toolbar zoom buttons |
|
|
134
|
+
| `showResetControl` | `boolean` | `true` | Reset zoom/pan button |
|
|
135
|
+
| `showFrameIndicator` | `boolean` | `true` | Frame counter badge |
|
|
136
|
+
| `showDragHint` | `boolean` | `true` | Helper text in toolbar |
|
|
137
|
+
| `aspectRatio` | `string` | `'16 / 10'` | CSS aspect-ratio value |
|
|
138
|
+
| `labels` | `Viewer360Labels` | English defaults | Localized strings |
|
|
139
|
+
| `classNames` | `Viewer360ClassNames` | BEM classes | Override CSS hooks |
|
|
140
|
+
| `theme` | `Viewer360Theme` | light palette | CSS variable overrides |
|
|
141
|
+
| `hotspotPin` | `Viewer360HotspotPinOptions` | — | Built-in marker pin with tooltip/delete |
|
|
142
|
+
| `renderHotspot` | `function` | — | Fully custom hotspot UI |
|
|
143
|
+
| `renderToolbar` | `function` | — | App design-system toolbar |
|
|
144
|
+
| `renderLoading` | `function` | — | Custom loading overlay |
|
|
145
|
+
| `renderFrameIndicator` | `function` | — | Custom frame badge |
|
|
146
|
+
| `renderHotspotModeBanner` | `function` | — | Custom add-mode banner |
|
|
147
|
+
|
|
148
|
+
## Headless usage
|
|
149
|
+
|
|
150
|
+
Use `useViewer360` when you need full control over markup:
|
|
151
|
+
|
|
152
|
+
```tsx
|
|
153
|
+
import { useViewer360 } from '@mmmmzxe/react-360-viewer';
|
|
154
|
+
|
|
155
|
+
const viewer = useViewer360({
|
|
156
|
+
frames,
|
|
157
|
+
currentFrameIndex,
|
|
158
|
+
onFrameChange: setFrameIndex,
|
|
159
|
+
});
|
|
160
|
+
```
|
|
161
|
+
|
|
162
|
+
The hook exposes canvas refs, interaction handlers, zoom state, and hotspot positioning helpers.
|
|
163
|
+
|
|
164
|
+
## Theming
|
|
165
|
+
|
|
166
|
+
Override CSS variables on the root element or pass `theme`:
|
|
167
|
+
|
|
168
|
+
```tsx
|
|
169
|
+
<Viewer360
|
|
170
|
+
theme={{
|
|
171
|
+
'--viewer-bg': '#111827',
|
|
172
|
+
'--viewer-text': '#f9fafb',
|
|
173
|
+
'--viewer-accent': '#3b82f6',
|
|
174
|
+
}}
|
|
175
|
+
/>
|
|
176
|
+
```
|
|
177
|
+
|
|
178
|
+
See the `theme` prop on `Viewer360` for supported CSS variable overrides.
|
|
179
|
+
|
|
180
|
+
## Package structure
|
|
181
|
+
|
|
182
|
+
```
|
|
183
|
+
src/
|
|
184
|
+
components/
|
|
185
|
+
ui/ Badge, Button, Card, Item, Label, Popover, Separator, Spinner
|
|
186
|
+
utils/ cn helper
|
|
187
|
+
feature/ Viewer360 and sub-components
|
|
188
|
+
constants/
|
|
189
|
+
helpers/
|
|
190
|
+
hooks/
|
|
191
|
+
types/
|
|
192
|
+
```
|
|
193
|
+
|
|
194
|
+
The published package includes `dist/` (ESM + type declarations) and `src/` (for Tailwind `@source` scanning).
|
|
195
|
+
|
|
196
|
+
## Development
|
|
197
|
+
|
|
198
|
+
```bash
|
|
199
|
+
npm install
|
|
200
|
+
npm run test-run
|
|
201
|
+
npm run build
|
|
202
|
+
```
|
|
203
|
+
|
|
204
|
+
## Publishing
|
|
205
|
+
|
|
206
|
+
```bash
|
|
207
|
+
npm publish
|
|
208
|
+
```
|
|
209
|
+
|
|
210
|
+
`prepublishOnly` runs the build automatically. The package ships ESM + type declarations from `dist/`.
|
|
211
|
+
|
|
212
|
+
## License
|
|
213
|
+
|
|
214
|
+
MIT
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,377 @@
|
|
|
1
|
+
import * as React$1 from 'react';
|
|
2
|
+
import { ReactNode, CSSProperties, MouseEvent, JSX, RefObject, PointerEvent, WheelEvent } from 'react';
|
|
3
|
+
import { ClassValue } from 'clsx';
|
|
4
|
+
import * as class_variance_authority_types from 'class-variance-authority/types';
|
|
5
|
+
import { VariantProps } from 'class-variance-authority';
|
|
6
|
+
|
|
7
|
+
declare const defaultViewer360Config: {
|
|
8
|
+
minZoom: number;
|
|
9
|
+
maxZoom: number;
|
|
10
|
+
zoomStep: number;
|
|
11
|
+
dragSensitivity: number;
|
|
12
|
+
autoRotate: boolean;
|
|
13
|
+
autoRotateIntervalMs: number;
|
|
14
|
+
autoRotateDirection: "forward";
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
type Viewer360Frame = {
|
|
18
|
+
id: string;
|
|
19
|
+
src: string;
|
|
20
|
+
label?: string;
|
|
21
|
+
};
|
|
22
|
+
type Viewer360PanOffset = {
|
|
23
|
+
panX: number;
|
|
24
|
+
panY: number;
|
|
25
|
+
};
|
|
26
|
+
type Viewer360Config = {
|
|
27
|
+
minZoom?: number;
|
|
28
|
+
maxZoom?: number;
|
|
29
|
+
zoomStep?: number;
|
|
30
|
+
dragSensitivity?: number;
|
|
31
|
+
autoRotate?: boolean;
|
|
32
|
+
autoRotateIntervalMs?: number;
|
|
33
|
+
autoRotateDirection?: 'forward' | 'backward';
|
|
34
|
+
};
|
|
35
|
+
type Viewer360Hotspot<TData = unknown> = {
|
|
36
|
+
id: string;
|
|
37
|
+
frameIndex: number;
|
|
38
|
+
positionX: number;
|
|
39
|
+
positionY: number;
|
|
40
|
+
data?: TData;
|
|
41
|
+
};
|
|
42
|
+
type Viewer360HotspotPosition = {
|
|
43
|
+
frameIndex: number;
|
|
44
|
+
frameId: string;
|
|
45
|
+
positionX: number;
|
|
46
|
+
positionY: number;
|
|
47
|
+
};
|
|
48
|
+
type Viewer360HotspotRenderProps<TData = unknown> = {
|
|
49
|
+
hotspot: Viewer360Hotspot<TData>;
|
|
50
|
+
leftPercent: number;
|
|
51
|
+
topPercent: number;
|
|
52
|
+
};
|
|
53
|
+
type Viewer360OverlayRenderProps = {
|
|
54
|
+
currentFrameIndex: number;
|
|
55
|
+
frameCount: number;
|
|
56
|
+
frameLabel?: string;
|
|
57
|
+
isHotspotMode: boolean;
|
|
58
|
+
labels: Required<Viewer360Labels>;
|
|
59
|
+
};
|
|
60
|
+
type Viewer360ToolbarRenderProps = {
|
|
61
|
+
zoom: number;
|
|
62
|
+
minZoom: number;
|
|
63
|
+
maxZoom: number;
|
|
64
|
+
isResetDisabled: boolean;
|
|
65
|
+
isHotspotMode: boolean;
|
|
66
|
+
showHotspotModeControl: boolean;
|
|
67
|
+
showZoomControls: boolean;
|
|
68
|
+
showResetControl: boolean;
|
|
69
|
+
showDragHint: boolean;
|
|
70
|
+
labels: Required<Viewer360Labels>;
|
|
71
|
+
onZoomIn: () => void;
|
|
72
|
+
onZoomOut: () => void;
|
|
73
|
+
onResetView: () => void;
|
|
74
|
+
onHotspotModeChange: (active: boolean) => void;
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
type Viewer360Marker = {
|
|
78
|
+
id: string;
|
|
79
|
+
title: string;
|
|
80
|
+
description?: string;
|
|
81
|
+
};
|
|
82
|
+
type Viewer360MarkerPinClassNames = {
|
|
83
|
+
root?: string;
|
|
84
|
+
ping?: string;
|
|
85
|
+
dot?: string;
|
|
86
|
+
tooltip?: string;
|
|
87
|
+
tooltipHeader?: string;
|
|
88
|
+
tooltipBody?: string;
|
|
89
|
+
tooltipTitle?: string;
|
|
90
|
+
tooltipDescription?: string;
|
|
91
|
+
deleteButton?: string;
|
|
92
|
+
};
|
|
93
|
+
type Viewer360MarkerPinLabels = {
|
|
94
|
+
delete?: string;
|
|
95
|
+
};
|
|
96
|
+
type Viewer360MarkerPinRenderProps<TData = unknown> = {
|
|
97
|
+
marker: Viewer360Marker;
|
|
98
|
+
hotspot?: Viewer360Hotspot<TData>;
|
|
99
|
+
};
|
|
100
|
+
type Viewer360MarkerPinProps<TData = unknown> = {
|
|
101
|
+
marker: Viewer360Marker;
|
|
102
|
+
hotspot?: Viewer360Hotspot<TData>;
|
|
103
|
+
leftPercent: number;
|
|
104
|
+
topPercent: number;
|
|
105
|
+
onDelete?: (id: string) => void;
|
|
106
|
+
isDeletePending?: boolean;
|
|
107
|
+
renderTag?: (props: Viewer360MarkerPinRenderProps<TData>) => ReactNode;
|
|
108
|
+
classNames?: Viewer360MarkerPinClassNames;
|
|
109
|
+
labels?: Viewer360MarkerPinLabels;
|
|
110
|
+
};
|
|
111
|
+
type Viewer360HotspotPinOptions<TData = unknown> = {
|
|
112
|
+
onDelete?: (id: string) => void;
|
|
113
|
+
deletingMarkerId?: string | null;
|
|
114
|
+
getMarker?: (hotspot: Viewer360Hotspot<TData>) => Viewer360Marker;
|
|
115
|
+
renderTag?: (props: Viewer360MarkerPinRenderProps<TData>) => ReactNode;
|
|
116
|
+
classNames?: Viewer360MarkerPinClassNames;
|
|
117
|
+
labels?: Viewer360MarkerPinLabels;
|
|
118
|
+
};
|
|
119
|
+
|
|
120
|
+
type Viewer360ClassNames = {
|
|
121
|
+
root?: string;
|
|
122
|
+
viewport?: string;
|
|
123
|
+
canvas?: string;
|
|
124
|
+
overlay?: string;
|
|
125
|
+
loading?: string;
|
|
126
|
+
loadingText?: string;
|
|
127
|
+
frameIndicator?: string;
|
|
128
|
+
hotspotModeBanner?: string;
|
|
129
|
+
toolbar?: string;
|
|
130
|
+
dragHint?: string;
|
|
131
|
+
controls?: string;
|
|
132
|
+
controlButton?: string;
|
|
133
|
+
controlButtonActive?: string;
|
|
134
|
+
controlButtonDisabled?: string;
|
|
135
|
+
zoomDisplay?: string;
|
|
136
|
+
divider?: string;
|
|
137
|
+
};
|
|
138
|
+
type Viewer360Labels = {
|
|
139
|
+
loading?: string;
|
|
140
|
+
dragHint?: string;
|
|
141
|
+
frameIndicator?: (params: {
|
|
142
|
+
current: number;
|
|
143
|
+
total: number;
|
|
144
|
+
label?: string;
|
|
145
|
+
}) => string;
|
|
146
|
+
zoom?: (percent: number) => string;
|
|
147
|
+
hotspotModeActive?: string;
|
|
148
|
+
addHotspot?: string;
|
|
149
|
+
zoomIn?: string;
|
|
150
|
+
zoomOut?: string;
|
|
151
|
+
resetView?: string;
|
|
152
|
+
deleteMarker?: string;
|
|
153
|
+
};
|
|
154
|
+
type Viewer360Theme = {
|
|
155
|
+
'--viewer-bg'?: string;
|
|
156
|
+
'--viewer-border'?: string;
|
|
157
|
+
'--viewer-text'?: string;
|
|
158
|
+
'--viewer-muted'?: string;
|
|
159
|
+
'--viewer-accent'?: string;
|
|
160
|
+
'--viewer-accent-foreground'?: string;
|
|
161
|
+
'--viewer-control-bg'?: string;
|
|
162
|
+
'--viewer-control-border'?: string;
|
|
163
|
+
'--viewer-hotspot-banner-bg'?: string;
|
|
164
|
+
'--viewer-hotspot-banner-border'?: string;
|
|
165
|
+
'--viewer-hotspot-banner-text'?: string;
|
|
166
|
+
};
|
|
167
|
+
type Viewer360Props<TData = unknown> = {
|
|
168
|
+
frames: Viewer360Frame[];
|
|
169
|
+
currentFrameIndex?: number;
|
|
170
|
+
defaultFrameIndex?: number;
|
|
171
|
+
onFrameChange?: (index: number) => void;
|
|
172
|
+
config?: Viewer360Config;
|
|
173
|
+
className?: string;
|
|
174
|
+
classNames?: Viewer360ClassNames;
|
|
175
|
+
style?: CSSProperties;
|
|
176
|
+
theme?: Viewer360Theme;
|
|
177
|
+
labels?: Viewer360Labels;
|
|
178
|
+
aspectRatio?: string;
|
|
179
|
+
showZoomControls?: boolean;
|
|
180
|
+
showResetControl?: boolean;
|
|
181
|
+
showFrameIndicator?: boolean;
|
|
182
|
+
showDragHint?: boolean;
|
|
183
|
+
showHotspotModeControl?: boolean;
|
|
184
|
+
hotspotPin?: Viewer360HotspotPinOptions<TData>;
|
|
185
|
+
hotspots?: Viewer360Hotspot<TData>[];
|
|
186
|
+
renderHotspot?: (props: Viewer360HotspotRenderProps<TData>) => ReactNode;
|
|
187
|
+
renderLoading?: () => ReactNode;
|
|
188
|
+
renderFrameIndicator?: (props: Viewer360OverlayRenderProps) => ReactNode;
|
|
189
|
+
renderHotspotModeBanner?: (props: Pick<Viewer360OverlayRenderProps, 'labels'>) => ReactNode;
|
|
190
|
+
renderToolbar?: (props: Viewer360ToolbarRenderProps) => ReactNode;
|
|
191
|
+
onHotspotClick?: (hotspot: Viewer360Hotspot<TData>, event: MouseEvent<HTMLDivElement>) => void;
|
|
192
|
+
hotspotMode?: boolean;
|
|
193
|
+
defaultHotspotMode?: boolean;
|
|
194
|
+
onHotspotModeChange?: (active: boolean) => void;
|
|
195
|
+
onHotspotAdd?: (position: Viewer360HotspotPosition) => void;
|
|
196
|
+
children?: ReactNode;
|
|
197
|
+
};
|
|
198
|
+
|
|
199
|
+
declare const defaultViewer360Labels: Required<Viewer360Labels>;
|
|
200
|
+
|
|
201
|
+
declare const defaultViewer360MarkerPinLabels: {
|
|
202
|
+
readonly delete: "Remove marker";
|
|
203
|
+
};
|
|
204
|
+
|
|
205
|
+
declare const viewer360ClassNames: Required<Viewer360ClassNames>;
|
|
206
|
+
declare const viewer360MarkerPinClassNames: Required<Viewer360MarkerPinClassNames>;
|
|
207
|
+
/** @deprecated Use `viewer360ClassNames` */
|
|
208
|
+
declare const defaultViewer360ClassNames: Required<Viewer360ClassNames>;
|
|
209
|
+
/** @deprecated Use `viewer360MarkerPinClassNames` */
|
|
210
|
+
declare const defaultViewer360MarkerPinClassNames: Required<Viewer360MarkerPinClassNames>;
|
|
211
|
+
|
|
212
|
+
declare function Viewer360<TData = unknown>({ frames, currentFrameIndex: controlledFrameIndex, defaultFrameIndex, onFrameChange, config, className, classNames, style, theme, labels, aspectRatio, showZoomControls, showResetControl, showFrameIndicator, showDragHint, showHotspotModeControl, hotspotPin, hotspots, renderHotspot, renderLoading, renderFrameIndicator, renderHotspotModeBanner, renderToolbar, onHotspotClick, hotspotMode: controlledHotspotMode, defaultHotspotMode, onHotspotModeChange, onHotspotAdd, children, }: Viewer360Props<TData>): JSX.Element;
|
|
213
|
+
|
|
214
|
+
type Viewer360AddModeBannerProps = {
|
|
215
|
+
className?: string;
|
|
216
|
+
label: string;
|
|
217
|
+
};
|
|
218
|
+
declare function Viewer360AddModeBanner({ className, label }: Viewer360AddModeBannerProps): JSX.Element;
|
|
219
|
+
|
|
220
|
+
type Viewer360FrameIndicatorProps = {
|
|
221
|
+
className?: string;
|
|
222
|
+
label: string;
|
|
223
|
+
};
|
|
224
|
+
declare function Viewer360FrameIndicator({ className, label }: Viewer360FrameIndicatorProps): JSX.Element;
|
|
225
|
+
|
|
226
|
+
type Viewer360LoadingOverlayProps = {
|
|
227
|
+
className?: string;
|
|
228
|
+
textClassName?: string;
|
|
229
|
+
label: string;
|
|
230
|
+
};
|
|
231
|
+
declare function Viewer360LoadingOverlay({ className, textClassName, label }: Viewer360LoadingOverlayProps): JSX.Element;
|
|
232
|
+
|
|
233
|
+
declare function Viewer360MarkerPin<TData = unknown>({ marker, hotspot, leftPercent, topPercent, onDelete, isDeletePending, renderTag, classNames, labels, }: Viewer360MarkerPinProps<TData>): JSX.Element;
|
|
234
|
+
|
|
235
|
+
type Viewer360ToolbarProps = Viewer360ToolbarRenderProps;
|
|
236
|
+
declare function Viewer360Toolbar({ showDragHint, showHotspotModeControl, showZoomControls, showResetControl, labels, isHotspotMode, zoom, minZoom, maxZoom, isResetDisabled, onHotspotModeChange, onZoomIn, onZoomOut, onResetView, }: Viewer360ToolbarProps): JSX.Element;
|
|
237
|
+
|
|
238
|
+
type ViewerImageLayout = {
|
|
239
|
+
width: number;
|
|
240
|
+
height: number;
|
|
241
|
+
centerX: number;
|
|
242
|
+
centerY: number;
|
|
243
|
+
drawWidth: number;
|
|
244
|
+
drawHeight: number;
|
|
245
|
+
offsetX: number;
|
|
246
|
+
offsetY: number;
|
|
247
|
+
};
|
|
248
|
+
type PanOffset = {
|
|
249
|
+
panX: number;
|
|
250
|
+
panY: number;
|
|
251
|
+
};
|
|
252
|
+
type ComputeViewerImageLayoutParams = {
|
|
253
|
+
containerWidth: number;
|
|
254
|
+
containerHeight: number;
|
|
255
|
+
imageWidth: number;
|
|
256
|
+
imageHeight: number;
|
|
257
|
+
pan?: PanOffset;
|
|
258
|
+
};
|
|
259
|
+
declare function computeViewerImageLayout({ containerWidth, containerHeight, imageWidth, imageHeight, pan, }: ComputeViewerImageLayoutParams): ViewerImageLayout;
|
|
260
|
+
declare function computeHotspotScreenPosition(hotspotX: number, hotspotY: number, layout: ViewerImageLayout, zoom: number): {
|
|
261
|
+
leftPercent: number;
|
|
262
|
+
topPercent: number;
|
|
263
|
+
};
|
|
264
|
+
declare function computeHotspotPositionFromClick(clientX: number, clientY: number, containerRect: DOMRect, layout: ViewerImageLayout, zoom: number): {
|
|
265
|
+
positionX: number;
|
|
266
|
+
positionY: number;
|
|
267
|
+
};
|
|
268
|
+
|
|
269
|
+
type UseViewer360Params<TData> = {
|
|
270
|
+
frames: Viewer360Frame[];
|
|
271
|
+
currentFrameIndex: number;
|
|
272
|
+
onFrameChange: (index: number) => void;
|
|
273
|
+
hotspots?: Viewer360Hotspot<TData>[];
|
|
274
|
+
config?: Viewer360Config;
|
|
275
|
+
hotspotMode?: boolean;
|
|
276
|
+
onHotspotAdd?: (position: Viewer360HotspotPosition) => void;
|
|
277
|
+
};
|
|
278
|
+
type UseViewer360Return<TData> = {
|
|
279
|
+
canvasRef: RefObject<HTMLCanvasElement | null>;
|
|
280
|
+
containerRef: RefObject<HTMLDivElement | null>;
|
|
281
|
+
currentFrame: Viewer360Frame | undefined;
|
|
282
|
+
currentFrameHotspots: Viewer360Hotspot<TData>[];
|
|
283
|
+
imagesLoaded: boolean;
|
|
284
|
+
isHotspotMode: boolean;
|
|
285
|
+
isResetDisabled: boolean;
|
|
286
|
+
maxZoom: number;
|
|
287
|
+
minZoom: number;
|
|
288
|
+
viewerCursorClass: string;
|
|
289
|
+
zoom: number;
|
|
290
|
+
getCurrentImageLayout: () => ViewerImageLayout | null;
|
|
291
|
+
getHotspotScreenPosition: (hotspot: Viewer360Hotspot<TData>) => {
|
|
292
|
+
leftPercent: number;
|
|
293
|
+
topPercent: number;
|
|
294
|
+
};
|
|
295
|
+
handleCanvasClick: (event: React.MouseEvent<HTMLDivElement>) => void;
|
|
296
|
+
handlePointerDown: (event: PointerEvent<HTMLDivElement>) => void;
|
|
297
|
+
handlePointerMove: (event: PointerEvent<HTMLDivElement>) => void;
|
|
298
|
+
handlePointerUp: (event: PointerEvent<HTMLDivElement>) => void;
|
|
299
|
+
handleResetView: () => void;
|
|
300
|
+
handleWheel: (event: WheelEvent<HTMLDivElement>) => void;
|
|
301
|
+
handleZoomIn: () => void;
|
|
302
|
+
handleZoomOut: () => void;
|
|
303
|
+
};
|
|
304
|
+
declare function useViewer360<TData = unknown>({ frames, hotspots, currentFrameIndex, onFrameChange, config, hotspotMode: hotspotModeProp, onHotspotAdd, }: UseViewer360Params<TData>): UseViewer360Return<TData>;
|
|
305
|
+
|
|
306
|
+
type ResolvedViewer360Config = Required<Viewer360Config>;
|
|
307
|
+
declare function resolveViewer360Config(config?: Viewer360Config): ResolvedViewer360Config;
|
|
308
|
+
declare function clampZoom(zoom: number, config: ResolvedViewer360Config): number;
|
|
309
|
+
declare function applyWheelZoom(currentZoom: number, deltaY: number, currentPan: PanOffset, config: ResolvedViewer360Config): {
|
|
310
|
+
zoom: number;
|
|
311
|
+
pan: PanOffset;
|
|
312
|
+
};
|
|
313
|
+
declare function stepZoomIn(currentZoom: number, config: ResolvedViewer360Config): number;
|
|
314
|
+
declare function stepZoomOut(currentZoom: number, currentPan: PanOffset, config: ResolvedViewer360Config): {
|
|
315
|
+
zoom: number;
|
|
316
|
+
pan: PanOffset;
|
|
317
|
+
};
|
|
318
|
+
declare function isResetDisabled(zoom: number, pan: PanOffset, config: ResolvedViewer360Config): boolean;
|
|
319
|
+
declare function getViewerCursorClass(isHotspotMode: boolean, zoom: number, isDragging: boolean, minZoom: number): string;
|
|
320
|
+
|
|
321
|
+
declare function clampFrameIndex(index: number, frameCount: number): number;
|
|
322
|
+
type DragStart = {
|
|
323
|
+
pointerX: number;
|
|
324
|
+
frameIndex: number;
|
|
325
|
+
};
|
|
326
|
+
declare function computeDragFrameIndex(dragStart: DragStart, clientX: number, frameCount: number, dragSensitivity: number): number | null;
|
|
327
|
+
|
|
328
|
+
type PanStart = {
|
|
329
|
+
pointerX: number;
|
|
330
|
+
pointerY: number;
|
|
331
|
+
panX: number;
|
|
332
|
+
panY: number;
|
|
333
|
+
};
|
|
334
|
+
declare function computeViewerPanOffset(panStart: PanStart, clientX: number, clientY: number): PanOffset;
|
|
335
|
+
|
|
336
|
+
declare function hotspotToViewer360Marker<TData>(hotspot: Viewer360Hotspot<TData>): Viewer360Marker;
|
|
337
|
+
declare function toViewer360Hotspots<TData extends Viewer360Marker>(markers: Array<Viewer360Marker & {
|
|
338
|
+
frameIndex: number;
|
|
339
|
+
positionX: number;
|
|
340
|
+
positionY: number;
|
|
341
|
+
}>, mapData?: (marker: Viewer360Marker & {
|
|
342
|
+
frameIndex: number;
|
|
343
|
+
positionX: number;
|
|
344
|
+
positionY: number;
|
|
345
|
+
}) => TData): Viewer360Hotspot<TData>[];
|
|
346
|
+
|
|
347
|
+
type DrawFrameOnCanvasParams = {
|
|
348
|
+
canvas: HTMLCanvasElement;
|
|
349
|
+
container: HTMLDivElement;
|
|
350
|
+
image: HTMLImageElement;
|
|
351
|
+
zoom: number;
|
|
352
|
+
pan: PanOffset;
|
|
353
|
+
};
|
|
354
|
+
declare function drawFrameOnCanvas({ canvas, container, image, zoom, pan }: DrawFrameOnCanvasParams): void;
|
|
355
|
+
declare function getFramesSignature(frames: Viewer360Frame[]): string;
|
|
356
|
+
declare function preloadFrameImage(frame: Viewer360Frame): Promise<HTMLImageElement>;
|
|
357
|
+
declare function preloadViewerFrames(frames: Viewer360Frame[]): Promise<HTMLImageElement[]>;
|
|
358
|
+
declare function hasLoadedViewerFrame(images: HTMLImageElement[]): boolean;
|
|
359
|
+
declare function filterHotspotsByFrame<TData>(hotspots: Array<{
|
|
360
|
+
frameIndex: number;
|
|
361
|
+
data?: TData;
|
|
362
|
+
}>, frameIndex: number): Array<{
|
|
363
|
+
frameIndex: number;
|
|
364
|
+
data?: TData;
|
|
365
|
+
}>;
|
|
366
|
+
|
|
367
|
+
declare function cn(...inputs: ClassValue[]): string;
|
|
368
|
+
|
|
369
|
+
declare const buttonVariants: (props?: ({
|
|
370
|
+
variant?: "default" | "link" | "secondary" | "destructive" | "outline" | "ghost" | null | undefined;
|
|
371
|
+
size?: "default" | "icon" | "sm" | "xs" | "lg" | "icon-xs" | "icon-sm" | "icon-lg" | null | undefined;
|
|
372
|
+
} & class_variance_authority_types.ClassProp) | undefined) => string;
|
|
373
|
+
declare function Button({ className, variant, size, asChild, ...props }: React$1.ComponentProps<'button'> & VariantProps<typeof buttonVariants> & {
|
|
374
|
+
asChild?: boolean;
|
|
375
|
+
}): JSX.Element;
|
|
376
|
+
|
|
377
|
+
export { Button, Viewer360, Viewer360AddModeBanner, type Viewer360ClassNames, type Viewer360Config, type Viewer360Frame, Viewer360FrameIndicator, type Viewer360Hotspot, type Viewer360HotspotPinOptions, type Viewer360HotspotPosition, type Viewer360HotspotRenderProps, type Viewer360Labels, Viewer360LoadingOverlay, type Viewer360Marker, Viewer360MarkerPin, type Viewer360MarkerPinClassNames, type Viewer360MarkerPinLabels, type Viewer360MarkerPinProps, type Viewer360MarkerPinRenderProps, type Viewer360OverlayRenderProps, type Viewer360PanOffset, type Viewer360Props, type Viewer360Theme, Viewer360Toolbar, type Viewer360ToolbarRenderProps, type ViewerImageLayout, applyWheelZoom, buttonVariants, clampFrameIndex, clampZoom, cn, computeDragFrameIndex, computeHotspotPositionFromClick, computeHotspotScreenPosition, computeViewerImageLayout, computeViewerPanOffset, defaultViewer360ClassNames, defaultViewer360Config, defaultViewer360Labels, defaultViewer360MarkerPinClassNames, defaultViewer360MarkerPinLabels, drawFrameOnCanvas, filterHotspotsByFrame, getFramesSignature, getViewerCursorClass, hasLoadedViewerFrame, hotspotToViewer360Marker, isResetDisabled, preloadFrameImage, preloadViewerFrames, resolveViewer360Config, stepZoomIn, stepZoomOut, toViewer360Hotspots, useViewer360, viewer360ClassNames, viewer360MarkerPinClassNames };
|