@ericchen1990/pano-view 0.2.0 → 1.0.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 +304 -130
- package/dist/index.cjs +1587 -630
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.mts +194 -62
- package/dist/index.d.ts +194 -62
- package/dist/index.mjs +1472 -502
- package/dist/index.mjs.map +1 -1
- package/package.json +3 -1
package/README.md
CHANGED
|
@@ -15,32 +15,134 @@ compatible with that tile output format but is not affiliated with krpano.
|
|
|
15
15
|
## Install
|
|
16
16
|
|
|
17
17
|
```bash
|
|
18
|
-
npm install @ericchen1990/pano-view
|
|
18
|
+
npm install @ericchen1990/pano-view
|
|
19
19
|
```
|
|
20
20
|
|
|
21
|
-
React 19, React DOM 19, `@react-three/fiber` 9, and
|
|
21
|
+
React 19, React DOM 19, Three.js, `@react-three/fiber` 9, and `@react-three/drei` 10 are peer dependencies. npm 7+ and pnpm install them automatically; they must be a single copy in the host app.
|
|
22
22
|
|
|
23
|
-
##
|
|
23
|
+
## Exported components
|
|
24
24
|
|
|
25
|
-
|
|
25
|
+
### Viewer
|
|
26
|
+
|
|
27
|
+
- [`PanoViewer`](#panoviewer) — canvas shell, camera, default controls, and imperative view API
|
|
28
|
+
- [`Sphere`](#sphere) — single 2:1 equirectangular image
|
|
29
|
+
- [`Tile`](#tile) — krpano-style multires cube tiles
|
|
30
|
+
- [`Scenes`](#scenes) — controlled multi-scene transitions
|
|
31
|
+
|
|
32
|
+
### Controls
|
|
33
|
+
|
|
34
|
+
- [`MouseControls`](#mousecontrols) — drag look and wheel zoom
|
|
35
|
+
- [`TouchControls`](#touchcontrols) — one-finger drag and pinch zoom
|
|
36
|
+
- [`KeyboardControls`](#keyboardcontrols) — arrow-key look, FOV, and scene cycling
|
|
37
|
+
- [`AutoRotate`](#autorotate) — automatic yaw rotation
|
|
38
|
+
|
|
39
|
+
### Events and chrome
|
|
40
|
+
|
|
41
|
+
- [`PanoEvents`](#panoevents) — viewer-level lifecycle and interaction callbacks
|
|
42
|
+
- [`PanoContextMenu`](#panocontextmenu) — right-click reset / fullscreen menu
|
|
43
|
+
|
|
44
|
+
### Hotspots
|
|
45
|
+
|
|
46
|
+
- [`ImageHotspot`](#imagehotspot) — image texture at a spherical position
|
|
47
|
+
- [`GraphicHotspot`](#graphichotspot) — built-in shapes or SVG at a position
|
|
48
|
+
- [`SequenceHotspot`](#sequencehotspot) — sprite-sheet animation
|
|
49
|
+
- [`VideoHotspot`](#videohotspot) — HTML video texture
|
|
50
|
+
- [`TextHotspot`](#texthotspot) — rasterized plain text at a position
|
|
51
|
+
- [`IframeHotspot`](#iframehotspot) — embedded document overlay at a position
|
|
52
|
+
- [`PolygonHotspot`](#polygonhotspot) — closed spherical area
|
|
53
|
+
- [`PolylineHotspot`](#polylinehotspot) — open spherical path
|
|
54
|
+
|
|
55
|
+
### Hooks and helpers
|
|
56
|
+
|
|
57
|
+
- `usePanoEvents` — subscribe to viewer events inside a custom child ([`PanoEvents`](#panoevents))
|
|
58
|
+
- Coordinate helpers — `normalizePanoPosition`, `normalizePanoYaw`, `clampPanoPitch`, `panoPositionToVector3`, `vector3ToPanoPosition` ([Panorama coordinate events](#panorama-coordinate-events))
|
|
59
|
+
- `cycleSceneId` — wrap previous/next scene ids ([`KeyboardControls`](#keyboardcontrols))
|
|
60
|
+
- Context menu helpers — `createPanoContextMenuPresets`, `composePanoContextMenuItems`, and related APIs ([`PanoContextMenu`](#panocontextmenu))
|
|
61
|
+
- Polygon / polyline validation — `validatePolygonVertices`, `validatePolylineVertices`, `unwrapPolygonVertices`
|
|
62
|
+
|
|
63
|
+
## PanoViewer
|
|
64
|
+
|
|
65
|
+
`PanoViewer` owns the canvas, perspective camera, and controls. Give its container an explicit size and place one panorama source inside it.
|
|
66
|
+
|
|
67
|
+
Angles are public degrees. Positive yaw looks right and positive pitch looks up.
|
|
68
|
+
|
|
69
|
+
```tsx
|
|
70
|
+
import { useRef } from "react";
|
|
71
|
+
import {
|
|
72
|
+
PanoViewer,
|
|
73
|
+
Sphere,
|
|
74
|
+
AutoRotate,
|
|
75
|
+
type PanoViewerHandle,
|
|
76
|
+
} from "@ericchen1990/pano-view";
|
|
77
|
+
|
|
78
|
+
export function ControlledExample() {
|
|
79
|
+
const ref = useRef<PanoViewerHandle>(null);
|
|
80
|
+
|
|
81
|
+
return (
|
|
82
|
+
<>
|
|
83
|
+
<button onClick={() => ref.current?.setView({ yaw: 90, fov: 55 })}>
|
|
84
|
+
Look right
|
|
85
|
+
</button>
|
|
86
|
+
<PanoViewer
|
|
87
|
+
ref={ref}
|
|
88
|
+
controls={{
|
|
89
|
+
inertia: true,
|
|
90
|
+
rotateDamping: 14,
|
|
91
|
+
zoomDamping: 16,
|
|
92
|
+
}}
|
|
93
|
+
initialView={{ yaw: 0, pitch: 0, fov: 75 }}
|
|
94
|
+
minFov={30}
|
|
95
|
+
maxFov={100}
|
|
96
|
+
onViewChange={(view) => console.log(view)}
|
|
97
|
+
style={{ height: 560 }}
|
|
98
|
+
>
|
|
99
|
+
<AutoRotate enabled speed={18} acceleration={18} startDelay={1_000} />
|
|
100
|
+
<Sphere src="/panoramas/room.webp" />
|
|
101
|
+
</PanoViewer>
|
|
102
|
+
</>
|
|
103
|
+
);
|
|
104
|
+
}
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
The handle exposes `getView`, `setView`, `reset`, `startAutoRotate`, `stopAutoRotate`, and `toggleFullscreen`. Mouse, touch, and keyboard input are enabled by default — you do not need to render control components for ordinary viewing. Render `MouseControls`, `TouchControls`, or `KeyboardControls` only to override properties (the child replaces that channel's default instance). Tune shared behaviour through `controls` (`inertia`, `invert`, `bouncingLimits`, `fovSpeed`, `frictionStop`, `rotateDamping`, `zoomDamping`, and top-level `rotateSpeed` / `zoomSpeed`). Disable a channel with `controls.mouse` / `touch` / `keyboard` set to `false`, or pass an options object (including `enabled`) to override defaults without mounting a child. Auto-rotation is off until you render `AutoRotate`. The two auto-rotation handle methods remain supported for compatibility. A default context menu (Reset view / Enter fullscreen) is also mounted — see [`PanoContextMenu`](#panocontextmenu).
|
|
108
|
+
|
|
109
|
+
Drag and zoom update a target view that the camera follows smoothly. `rotateDamping` and `zoomDamping` control that following speed in seconds^-1 (defaults: `14` and `16` respectively); lower values feel softer, while `0` disables smoothing for that axis. Both values must be non-negative finite numbers. Imperative `setView()` and `reset()` remain immediate.
|
|
110
|
+
|
|
111
|
+
The canvas cursor defaults to `grab`, becomes `grabbing` while dragging with the mouse, `pointer` over an interactive hotspot, and `move` while dragging a hotspot. Override those values with `cursors` (`default`, `dragging`, `hotspot`, `hotspotDragging`), or pass `cursors={false}` to leave the canvas cursor unchanged. A hotspot may set `cursor` to replace the hover cursor for that hotspot only.
|
|
112
|
+
|
|
113
|
+
```tsx
|
|
114
|
+
<PanoViewer
|
|
115
|
+
cursors={{ default: "grab", dragging: "grabbing", hotspot: "pointer" }}
|
|
116
|
+
style={{ height: 560 }}
|
|
117
|
+
>
|
|
118
|
+
<ImageHotspot
|
|
119
|
+
id="door"
|
|
120
|
+
cursor="zoom-in"
|
|
121
|
+
position={{ yaw: 24, pitch: -6 }}
|
|
122
|
+
src="/hotspots/door.webp"
|
|
123
|
+
/>
|
|
124
|
+
</PanoViewer>
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
## Sphere
|
|
128
|
+
|
|
129
|
+
`Sphere` expects a 2:1 equirectangular image. Use `yawOffset` when the source's forward direction needs horizontal adjustment.
|
|
26
130
|
|
|
27
131
|
```tsx
|
|
28
132
|
"use client";
|
|
29
133
|
|
|
30
|
-
import {
|
|
134
|
+
import { PanoViewer, Sphere } from "@ericchen1990/pano-view";
|
|
31
135
|
|
|
32
136
|
export function SphereExample() {
|
|
33
137
|
return (
|
|
34
|
-
<
|
|
138
|
+
<PanoViewer style={{ width: "100%", height: 560 }}>
|
|
35
139
|
<Sphere src="/panoramas/room.webp" />
|
|
36
|
-
</
|
|
140
|
+
</PanoViewer>
|
|
37
141
|
);
|
|
38
142
|
}
|
|
39
143
|
```
|
|
40
144
|
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
## Cube Tile panorama
|
|
145
|
+
## Tile
|
|
44
146
|
|
|
45
147
|
`Tile` renders six inward-facing cube faces and loads only tiles around the current view. The default layout matches krpano-style output:
|
|
46
148
|
|
|
@@ -51,16 +153,16 @@ tiles/{face}/l{level}/{row}/l{level}_{face}_{col}_{row}.webp
|
|
|
51
153
|
Faces are `f`, `r`, `b`, `l`, `u`, and `d`; rows and columns are 1-based.
|
|
52
154
|
|
|
53
155
|
```tsx
|
|
54
|
-
import {
|
|
156
|
+
import { PanoViewer, Tile } from "@ericchen1990/pano-view";
|
|
55
157
|
|
|
56
158
|
export function TileExample() {
|
|
57
159
|
return (
|
|
58
|
-
<
|
|
160
|
+
<PanoViewer style={{ width: "100%", height: 560 }}>
|
|
59
161
|
<Tile
|
|
60
162
|
baseUrl="https://cdn.example.com/panoramas/room"
|
|
61
163
|
multires="512,500,1000,2000"
|
|
62
164
|
/>
|
|
63
|
-
</
|
|
165
|
+
</PanoViewer>
|
|
64
166
|
);
|
|
65
167
|
}
|
|
66
168
|
```
|
|
@@ -103,9 +205,9 @@ horizontal index and `%00v` produces a three-digit vertical index. The stereo
|
|
|
103
205
|
`%t` and frame `%f` placeholders are not applicable to `Tile`, which represents
|
|
104
206
|
a single non-stereo cube panorama.
|
|
105
207
|
|
|
106
|
-
##
|
|
208
|
+
## Scenes
|
|
107
209
|
|
|
108
|
-
`
|
|
210
|
+
`Scenes` switches controlled sphere and cube-tile scenes with GPU-only
|
|
109
211
|
snapshot blending. The target scene loads its sphere image or tile preview first;
|
|
110
212
|
once ready, the current framebuffer becomes a temporary GPU texture, its source
|
|
111
213
|
textures are released, and the target scene blends in. This avoids holding two
|
|
@@ -113,12 +215,12 @@ high-resolution tile scenes in WebGL memory at once.
|
|
|
113
215
|
|
|
114
216
|
```tsx
|
|
115
217
|
import {
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
type
|
|
218
|
+
Scenes,
|
|
219
|
+
PanoViewer,
|
|
220
|
+
type Scene,
|
|
119
221
|
} from "@ericchen1990/pano-view";
|
|
120
222
|
|
|
121
|
-
const scenes:
|
|
223
|
+
const scenes: Scene[] = [
|
|
122
224
|
{ id: "lobby", type: "sphere", src: "/panoramas/lobby.webp" },
|
|
123
225
|
{
|
|
124
226
|
id: "terrace",
|
|
@@ -128,15 +230,15 @@ const scenes: PanoramaScene[] = [
|
|
|
128
230
|
},
|
|
129
231
|
];
|
|
130
232
|
|
|
131
|
-
<
|
|
132
|
-
<
|
|
233
|
+
<PanoViewer style={{ height: 560 }}>
|
|
234
|
+
<Scenes
|
|
133
235
|
scenes={scenes}
|
|
134
236
|
activeSceneId={activeSceneId}
|
|
135
237
|
transition="ellipticZoomOpen"
|
|
136
238
|
renderHotspots={(scene) => <SceneHotspots sceneId={scene.id} />}
|
|
137
239
|
onTransitionError={({ sceneId }) => console.warn("Could not load", sceneId)}
|
|
138
240
|
/>
|
|
139
|
-
</
|
|
241
|
+
</PanoViewer>;
|
|
140
242
|
```
|
|
141
243
|
|
|
142
244
|
Available presets are `none`, `crossfade`, `zoom`, `blackout`, `whiteFlash`,
|
|
@@ -147,58 +249,25 @@ Available presets are `none`, `crossfade`, `zoom`, `blackout`, `whiteFlash`,
|
|
|
147
249
|
While a transition runs, panorama drag/zoom input is locked and
|
|
148
250
|
`renderHotspots` is hidden. New `activeSceneId` values supersede a target that
|
|
149
251
|
is still being prepared. `maxTextureMemoryMb` and `maxConcurrentTileLoads`
|
|
150
|
-
apply to the whole `
|
|
252
|
+
apply to the whole `Scenes` viewer rather than to each tile scene.
|
|
151
253
|
|
|
152
|
-
##
|
|
254
|
+
## AutoRotate
|
|
153
255
|
|
|
154
|
-
|
|
256
|
+
Auto-rotation is off by default. Render `AutoRotate` inside `PanoViewer` to enable it and keep rotation configuration separate from user-input controls. `speed` is measured in degrees per second; use a negative value to rotate left. `acceleration` is measured in degrees per second squared and smoothly ramps from zero to `speed` (default: `18`, so the default speed takes one second to reach). Set it to `0` for an immediate fixed speed. `startDelay` is measured in milliseconds from when `enabled` becomes true. While the user is dragging, or while drag inertia is still settling, rotation pauses and resumes from zero speed automatically.
|
|
155
257
|
|
|
156
258
|
```tsx
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
AutoRotate,
|
|
162
|
-
type PanoViewHandle,
|
|
163
|
-
} from "@ericchen1990/pano-view";
|
|
164
|
-
|
|
165
|
-
export function ControlledExample() {
|
|
166
|
-
const ref = useRef<PanoViewHandle>(null);
|
|
167
|
-
|
|
168
|
-
return (
|
|
169
|
-
<>
|
|
170
|
-
<button onClick={() => ref.current?.setView({ yaw: 90, fov: 55 })}>
|
|
171
|
-
Look right
|
|
172
|
-
</button>
|
|
173
|
-
<PanoView
|
|
174
|
-
ref={ref}
|
|
175
|
-
controls={{
|
|
176
|
-
inertia: true,
|
|
177
|
-
rotateDamping: 14,
|
|
178
|
-
zoomDamping: 16,
|
|
179
|
-
}}
|
|
180
|
-
initialView={{ yaw: 0, pitch: 0, fov: 75 }}
|
|
181
|
-
minFov={30}
|
|
182
|
-
maxFov={100}
|
|
183
|
-
onViewChange={(view) => console.log(view)}
|
|
184
|
-
style={{ height: 560 }}
|
|
185
|
-
>
|
|
186
|
-
<AutoRotate enabled speed={18} acceleration={18} startDelay={1_000} />
|
|
187
|
-
<Sphere src="/panoramas/room.webp" />
|
|
188
|
-
</PanoView>
|
|
189
|
-
</>
|
|
190
|
-
);
|
|
191
|
-
}
|
|
259
|
+
<PanoViewer style={{ height: 560 }}>
|
|
260
|
+
<AutoRotate enabled speed={12} acceleration={6} startDelay={2_000} />
|
|
261
|
+
<Sphere src="/panoramas/room.webp" />
|
|
262
|
+
</PanoViewer>
|
|
192
263
|
```
|
|
193
264
|
|
|
194
|
-
The handle exposes `getView`, `setView`, `reset`, `startAutoRotate`, `stopAutoRotate`, and `toggleFullscreen`. Mouse, touch, and keyboard input are enabled by default — you do not need to render control components for ordinary viewing. Tune shared behaviour through `controls` (`inertia`, `invert`, `bouncingLimits`, `fovSpeed`, `frictionStop`, `rotateDamping`, `zoomDamping`, and top-level `rotateSpeed` / `zoomSpeed`). Disable a channel with `controls.mouse` / `touch` / `keyboard` set to `false`, or pass an options object (including `enabled`) to override defaults without mounting a child. The two auto-rotation handle methods remain supported for compatibility; prefer rendering `AutoRotate` for new code. A default context menu (Reset view / Enter fullscreen) is also mounted — see [Context menu](#context-menu).
|
|
195
|
-
|
|
196
265
|
## Mouse, touch, and keyboard controls
|
|
197
266
|
|
|
198
|
-
`
|
|
267
|
+
`PanoViewer` enables `MouseControls`, `TouchControls`, and `KeyboardControls` by default. Configure shared options through `controls`; render a control component only to override that channel's properties. A child instance replaces the default — you do not need to set the channel to `false` first. Set `controls.mouse` / `touch` / `keyboard` to `false` to turn a channel off.
|
|
199
268
|
|
|
200
269
|
```tsx
|
|
201
|
-
<
|
|
270
|
+
<PanoViewer
|
|
202
271
|
controls={{
|
|
203
272
|
invert: false,
|
|
204
273
|
bouncingLimits: false,
|
|
@@ -209,36 +278,44 @@ The handle exposes `getView`, `setView`, `reset`, `startAutoRotate`, `stopAutoRo
|
|
|
209
278
|
style={{ height: 560 }}
|
|
210
279
|
>
|
|
211
280
|
<Sphere src="/panoramas/room.webp" />
|
|
212
|
-
</
|
|
281
|
+
</PanoViewer>
|
|
213
282
|
```
|
|
214
283
|
|
|
215
284
|
**Mouse** (pointer types `mouse` / `pen`): drag look and optional wheel zoom. Defaults: `rotateSpeed` `0.35`, `zoomSpeed` `0.08`, `wheel` `true`, `buttons` `["left"]`.
|
|
216
285
|
|
|
217
|
-
|
|
286
|
+
### MouseControls
|
|
287
|
+
|
|
288
|
+
See the defaults above. Override through `controls.mouse` or by rendering `<MouseControls />` as a child.
|
|
289
|
+
|
|
290
|
+
### TouchControls
|
|
218
291
|
|
|
219
|
-
|
|
292
|
+
One-finger drag and optional two-finger pinch zoom (`pinchZoom`, default `true`). Override through `controls.touch` or by rendering `<TouchControls />` as a child.
|
|
293
|
+
|
|
294
|
+
### KeyboardControls
|
|
295
|
+
|
|
296
|
+
Hold arrows (or custom bindings) for continuous look / FOV; `0` resets; optional scene bindings. Defaults: `rotateSpeed` `60`, `zoomSpeed` `30`, `shiftMultiplier` `3`. Set `invert` to flip up/down only. Override through `controls.keyboard` or by rendering `<KeyboardControls />` as a child.
|
|
220
297
|
|
|
221
298
|
Shared all-mode options on `controls`: `enabled`, `invert` (drag direction for mouse/touch), `bouncingLimits`, `fovSpeed`, `frictionStop` (default `0.01`), plus existing damping / inertia.
|
|
222
299
|
|
|
223
300
|
### Overriding a control channel
|
|
224
301
|
|
|
225
|
-
|
|
302
|
+
Render the component with the props you need. The default instance for that channel is skipped:
|
|
226
303
|
|
|
227
304
|
```tsx
|
|
228
305
|
import { useState } from "react";
|
|
229
306
|
import {
|
|
230
307
|
KeyboardControls,
|
|
231
|
-
|
|
232
|
-
|
|
308
|
+
Scenes,
|
|
309
|
+
PanoViewer,
|
|
233
310
|
cycleSceneId,
|
|
234
|
-
type
|
|
311
|
+
type Scene,
|
|
235
312
|
} from "@ericchen1990/pano-view";
|
|
236
313
|
|
|
237
|
-
export function KeyboardExample({ scenes }: { scenes:
|
|
314
|
+
export function KeyboardExample({ scenes }: { scenes: Scene[] }) {
|
|
238
315
|
const [activeSceneId, setActiveSceneId] = useState(scenes[0]!.id);
|
|
239
316
|
|
|
240
317
|
return (
|
|
241
|
-
<
|
|
318
|
+
<PanoViewer style={{ height: 560 }}>
|
|
242
319
|
<KeyboardControls
|
|
243
320
|
keys={{
|
|
244
321
|
left: ["ArrowLeft", "a"],
|
|
@@ -261,15 +338,15 @@ export function KeyboardExample({ scenes }: { scenes: PanoramaScene[] }) {
|
|
|
261
338
|
if (next) setActiveSceneId(next);
|
|
262
339
|
}}
|
|
263
340
|
/>
|
|
264
|
-
<
|
|
265
|
-
</
|
|
341
|
+
<Scenes scenes={scenes} activeSceneId={activeSceneId} />
|
|
342
|
+
</PanoViewer>
|
|
266
343
|
);
|
|
267
344
|
}
|
|
268
345
|
```
|
|
269
346
|
|
|
270
347
|
Hold movement and zoom keys for continuous motion in degrees per second (`rotateSpeed` / `zoomSpeed`). Hold Shift to multiply those rates (`shiftMultiplier`, default `3`). Scene and reset bindings fire once per press. The canvas must be focused to receive keys (click the viewer first).
|
|
271
348
|
|
|
272
|
-
Default bindings: arrows for look, `+/-` for FOV, `0` for reset, plus `[`/`PageUp` and `]`/`PageDown` for previous/next scene when callbacks are provided. The same override pattern works with `MouseControls` and `TouchControls
|
|
349
|
+
Default bindings: arrows for look, `+/-` for FOV, `0` for reset, plus `[`/`PageUp` and `]`/`PageDown` for previous/next scene when callbacks are provided. The same child-override pattern works with `MouseControls` and `TouchControls`. Set `controls.mouse={false}` / `controls.touch={false}` / `controls.keyboard={false}` only when you want that channel off.
|
|
273
350
|
|
|
274
351
|
## Panorama coordinate events
|
|
275
352
|
|
|
@@ -279,7 +356,7 @@ convention as the camera: positive yaw looks right and positive pitch looks up.
|
|
|
279
356
|
|
|
280
357
|
```tsx
|
|
281
358
|
import {
|
|
282
|
-
|
|
359
|
+
PanoViewer,
|
|
283
360
|
Sphere,
|
|
284
361
|
panoPositionToVector3,
|
|
285
362
|
type HotspotPosition,
|
|
@@ -292,14 +369,14 @@ export function PlacementExample() {
|
|
|
292
369
|
};
|
|
293
370
|
|
|
294
371
|
return (
|
|
295
|
-
<
|
|
372
|
+
<PanoViewer
|
|
296
373
|
onPanoramaClick={({ position }) => place(position)}
|
|
297
374
|
onPanoramaDoubleClick={({ position }) => console.log("double", position)}
|
|
298
375
|
onPanoramaPointerMove={({ position }) => console.log("move", position)}
|
|
299
376
|
style={{ height: 560 }}
|
|
300
377
|
>
|
|
301
378
|
<Sphere src="/panoramas/room.webp" />
|
|
302
|
-
</
|
|
379
|
+
</PanoViewer>
|
|
303
380
|
);
|
|
304
381
|
}
|
|
305
382
|
```
|
|
@@ -309,23 +386,25 @@ export function PlacementExample() {
|
|
|
309
386
|
is normalized to `[-180, 180)`. Pitch is clamped to `[-90, 90]`; at either
|
|
310
387
|
pole, yaw is normalized to `0` because it does not identify a unique point.
|
|
311
388
|
|
|
312
|
-
##
|
|
389
|
+
## PanoEvents
|
|
313
390
|
|
|
314
391
|
`PanoEvents` is the React counterpart to krpano's global `<events>` element.
|
|
315
|
-
Render one or more instances inside `
|
|
392
|
+
Render one or more instances inside `PanoViewer` to subscribe to viewer-level
|
|
316
393
|
lifecycle and interaction callbacks. Multiple instances coexist (like named
|
|
317
394
|
krpano events); each tracks its own `idleTime`. For composition inside a custom
|
|
318
395
|
child, use `usePanoEvents` instead.
|
|
319
396
|
|
|
397
|
+
### usePanoEvents
|
|
398
|
+
|
|
320
399
|
```tsx
|
|
321
400
|
import {
|
|
322
401
|
AutoRotate,
|
|
323
402
|
PanoEvents,
|
|
324
|
-
|
|
403
|
+
PanoViewer,
|
|
325
404
|
Sphere,
|
|
326
405
|
} from "@ericchen1990/pano-view";
|
|
327
406
|
|
|
328
|
-
<
|
|
407
|
+
<PanoViewer style={{ height: 560 }}>
|
|
329
408
|
<PanoEvents
|
|
330
409
|
idleTime={3000}
|
|
331
410
|
onIdle={() => console.log("idle")}
|
|
@@ -338,7 +417,7 @@ import {
|
|
|
338
417
|
/>
|
|
339
418
|
<AutoRotate enabled />
|
|
340
419
|
<Sphere src="/panoramas/room.webp" />
|
|
341
|
-
</
|
|
420
|
+
</PanoViewer>;
|
|
342
421
|
```
|
|
343
422
|
|
|
344
423
|
Supported callbacks (krpano names in parentheses where applicable):
|
|
@@ -351,41 +430,41 @@ Supported callbacks (krpano names in parentheses where applicable):
|
|
|
351
430
|
- `onResize` — canvas content-box size via `ResizeObserver`
|
|
352
431
|
- Auto-rotate: `onAutoRotateStart` / `onAutoRotateStop` / `onAutoRotateOneRound`
|
|
353
432
|
|
|
354
|
-
`
|
|
433
|
+
`PanoViewer`'s existing `onViewChange` / `onPanoramaClick` / `onPanoramaDoubleClick` /
|
|
355
434
|
`onPanoramaPointerMove` props remain supported and share the same event bus.
|
|
356
435
|
|
|
357
436
|
Resource loading and scene blending stay on their owners: use `Sphere` /
|
|
358
|
-
`Tile` `onLoad` / `onError` / `onLoadProgress`, and `
|
|
437
|
+
`Tile` `onLoad` / `onError` / `onLoadProgress`, and `Scenes`
|
|
359
438
|
`onTransitionEnd` / `onTransitionError`. This package does not mirror krpano
|
|
360
439
|
xml/VR/gyro/frame-render events.
|
|
361
440
|
|
|
362
|
-
##
|
|
441
|
+
## PanoContextMenu
|
|
363
442
|
|
|
364
|
-
`
|
|
443
|
+
`PanoViewer` mounts a default context menu that replaces the browser menu on
|
|
365
444
|
right-click. Default items: **Reset view** and **Enter fullscreen** / **Exit
|
|
366
445
|
fullscreen** (label and icon follow the current fullscreen state), with a
|
|
367
446
|
separator between them.
|
|
368
447
|
|
|
369
448
|
```tsx
|
|
370
|
-
<
|
|
449
|
+
<PanoViewer style={{ height: 560 }}>
|
|
371
450
|
<Sphere src="/panoramas/room.webp" />
|
|
372
|
-
</
|
|
451
|
+
</PanoViewer>
|
|
373
452
|
```
|
|
374
453
|
|
|
375
454
|
Disable the default menu (restore the browser menu, or mount your own):
|
|
376
455
|
|
|
377
456
|
```tsx
|
|
378
|
-
<
|
|
457
|
+
<PanoViewer contextMenu={false}>
|
|
379
458
|
<Sphere src="/panoramas/room.webp" />
|
|
380
|
-
</
|
|
459
|
+
</PanoViewer>
|
|
381
460
|
```
|
|
382
461
|
|
|
383
462
|
Tune appearance while keeping the default items:
|
|
384
463
|
|
|
385
464
|
```tsx
|
|
386
|
-
<
|
|
465
|
+
<PanoViewer contextMenu={{ appearance: { opacity: 0.92, borderRadius: 8 } }}>
|
|
387
466
|
<Sphere src="/panoramas/room.webp" />
|
|
388
|
-
</
|
|
467
|
+
</PanoViewer>
|
|
389
468
|
```
|
|
390
469
|
|
|
391
470
|
### Adding items without rebuilding defaults
|
|
@@ -394,7 +473,7 @@ Use `append` or `prepend` to keep the built-in Reset / Fullscreen entries and
|
|
|
394
473
|
only add your own:
|
|
395
474
|
|
|
396
475
|
```tsx
|
|
397
|
-
<
|
|
476
|
+
<PanoViewer
|
|
398
477
|
contextMenu={{
|
|
399
478
|
append: [
|
|
400
479
|
"separator",
|
|
@@ -412,7 +491,7 @@ only add your own:
|
|
|
412
491
|
style={{ height: 560 }}
|
|
413
492
|
>
|
|
414
493
|
<Sphere src="/panoramas/room.webp" />
|
|
415
|
-
</
|
|
494
|
+
</PanoViewer>
|
|
416
495
|
```
|
|
417
496
|
|
|
418
497
|
### Built-in presets
|
|
@@ -425,7 +504,7 @@ reimplementing fullscreen enter/exit state:
|
|
|
425
504
|
- `"separator"` — a horizontal rule
|
|
426
505
|
|
|
427
506
|
```tsx
|
|
428
|
-
<
|
|
507
|
+
<PanoViewer
|
|
429
508
|
contextMenu={{
|
|
430
509
|
items: [
|
|
431
510
|
"resetView",
|
|
@@ -446,7 +525,7 @@ reimplementing fullscreen enter/exit state:
|
|
|
446
525
|
style={{ height: 560 }}
|
|
447
526
|
>
|
|
448
527
|
<Sphere src="/panoramas/room.webp" />
|
|
449
|
-
</
|
|
528
|
+
</PanoViewer>
|
|
450
529
|
```
|
|
451
530
|
|
|
452
531
|
Override presentation on a preset with `{ preset: "fullscreen", label: "…" }`.
|
|
@@ -463,10 +542,9 @@ stay opaque), hover/disabled colors, and icon size. For an explicit translucent
|
|
|
463
542
|
fill without using `opacity`, you can still pass `background: "rgba(...)"`.
|
|
464
543
|
|
|
465
544
|
For a fully custom DOM menu, set `contextMenu={false}` and render
|
|
466
|
-
`PanoContextMenu` as a child
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
claim the right mouse button.
|
|
545
|
+
`PanoContextMenu` as a child. Do not mount both the default menu and a child
|
|
546
|
+
`PanoContextMenu` at once. Avoid combining the context menu with
|
|
547
|
+
`controls.mouse.buttons: ["right"]` — both claim the right mouse button.
|
|
470
548
|
|
|
471
549
|
## Shared hotspot contract and saved definitions
|
|
472
550
|
|
|
@@ -475,6 +553,8 @@ and semantic interaction callbacks. Point hotspots additionally use a
|
|
|
475
553
|
controlled `position`, angular `width`/`height`, hotspot mode, and the zoom
|
|
476
554
|
options described below. `interactive={false}` keeps a hotspot visible while
|
|
477
555
|
letting a drawing tool receive the panorama pointer events beneath it.
|
|
556
|
+
Interactive hotspots set the canvas cursor to `pointer` while hovered, or to
|
|
557
|
+
the hotspot's `cursor` prop when that is set.
|
|
478
558
|
|
|
479
559
|
`onClick` and `onHoverChange` receive a position and input source (`"pointer"`
|
|
480
560
|
or `"keyboard"`). Point hotspots can use `draggable`, `onDragStart`,
|
|
@@ -482,9 +562,43 @@ or `"keyboard"`). Point hotspots can use `draggable`, `onDragStart`,
|
|
|
482
562
|
back to its state. Polygon and polyline hotspots report the corresponding
|
|
483
563
|
controlled `vertices` through `onVerticesChange`.
|
|
484
564
|
|
|
565
|
+
Every hotspot can show a DOM tooltip at its anchor. Pass a string or
|
|
566
|
+
`{ text?, image?, imageAlt? }` as `tooltip`. The default `tooltipTrigger` is
|
|
567
|
+
`"always"`: if content is present and the hotspot is visible, the bubble stays
|
|
568
|
+
on screen. Clicking the hotspot does not toggle it. Optional
|
|
569
|
+
`tooltipTrigger="hover"` shows the bubble on pointer hover or keyboard focus;
|
|
570
|
+
`"click"` pins it until the hotspot is clicked again or the empty panorama is
|
|
571
|
+
clicked. Hover and click triggers need `interactive`.
|
|
572
|
+
|
|
573
|
+
`tooltipPlacement` is `"top"` (default), `"bottom"`, `"left"`, or `"right"`.
|
|
574
|
+
The bubble sits outside that edge of the hotspot. `tooltipOffset` is the
|
|
575
|
+
screen-space gap in CSS pixels (default `12` / `DEFAULT_HOTSPOT_TOOLTIP_OFFSET`).
|
|
576
|
+
|
|
577
|
+
```tsx
|
|
578
|
+
<ImageHotspot
|
|
579
|
+
id="gallery"
|
|
580
|
+
ariaLabel="Open gallery"
|
|
581
|
+
position={{ yaw: 28, pitch: -4 }}
|
|
582
|
+
src="/hotspots/gallery.webp"
|
|
583
|
+
tooltip={{ text: "Courtyard gallery", image: "/hotspots/gallery-thumb.webp" }}
|
|
584
|
+
tooltipPlacement="top"
|
|
585
|
+
tooltipOffset={16}
|
|
586
|
+
/>
|
|
587
|
+
<GraphicHotspot
|
|
588
|
+
id="marker"
|
|
589
|
+
ariaLabel="Signal"
|
|
590
|
+
position={{ yaw: -18, pitch: 9 }}
|
|
591
|
+
graphic={{ kind: "ring" }}
|
|
592
|
+
tooltip="Hover for details"
|
|
593
|
+
tooltipTrigger="hover"
|
|
594
|
+
tooltipPlacement="right"
|
|
595
|
+
/>
|
|
596
|
+
```
|
|
597
|
+
|
|
485
598
|
For persistence or a host-owned editor, use the exported discriminated
|
|
486
|
-
`HotspotDefinition` union.
|
|
487
|
-
|
|
599
|
+
`HotspotDefinition` union. Adding a variant is a TypeScript breaking change for
|
|
600
|
+
exhaustive switches; handle `text` and `iframe` alongside the existing point
|
|
601
|
+
and path categories:
|
|
488
602
|
|
|
489
603
|
```ts
|
|
490
604
|
import type { HotspotDefinition } from "@ericchen1990/pano-view";
|
|
@@ -494,6 +608,8 @@ const hotspots: HotspotDefinition[] = [
|
|
|
494
608
|
{ type: "graphic", id: "marker", position: { yaw: -18, pitch: 9 }, graphic: { kind: "ring" } },
|
|
495
609
|
{ type: "sequence", id: "pulse", position: { yaw: -42, pitch: -7 }, src: "/hotspots/pulse.png", frameCount: 20 },
|
|
496
610
|
{ type: "video", id: "clip", position: { yaw: 48, pitch: 6 }, src: "/hotspots/clip.webm" },
|
|
611
|
+
{ type: "text", id: "caption", position: { yaw: 0, pitch: -16 }, text: "Courtyard overlook" },
|
|
612
|
+
{ type: "iframe", id: "guide", position: { yaw: -62, pitch: 4 }, src: "/hotspots/embed.html" },
|
|
497
613
|
{ type: "polygon", id: "zone", vertices: [{ yaw: 12, pitch: 4 }, { yaw: 22, pitch: 4 }, { yaw: 18, pitch: 14 }] },
|
|
498
614
|
{ type: "polyline", id: "route", vertices: [{ yaw: -8, pitch: 1 }, { yaw: 4, pitch: 8 }] },
|
|
499
615
|
];
|
|
@@ -503,15 +619,15 @@ Definitions intentionally contain data only. Render them with a switch in the
|
|
|
503
619
|
host, where application-specific click actions, controlled media state, and
|
|
504
620
|
error reporting belong.
|
|
505
621
|
|
|
506
|
-
##
|
|
622
|
+
## ImageHotspot
|
|
507
623
|
|
|
508
624
|
`ImageHotspot` renders an image at a controlled spherical position. Dimensions
|
|
509
625
|
are angular degrees, so they remain independent of the canvas resolution.
|
|
510
626
|
|
|
511
627
|
```tsx
|
|
512
|
-
import { ImageHotspot,
|
|
628
|
+
import { ImageHotspot, PanoViewer, Sphere } from "@ericchen1990/pano-view";
|
|
513
629
|
|
|
514
|
-
<
|
|
630
|
+
<PanoViewer style={{ height: 560 }}>
|
|
515
631
|
<Sphere src="/panoramas/room.webp" />
|
|
516
632
|
<ImageHotspot
|
|
517
633
|
id="gallery"
|
|
@@ -523,9 +639,18 @@ import { ImageHotspot, PanoView, Sphere } from "@ericchen1990/pano-view";
|
|
|
523
639
|
src="/hotspots/gallery.webp"
|
|
524
640
|
onClick={({ position }) => console.log("gallery", position)}
|
|
525
641
|
/>
|
|
526
|
-
</
|
|
642
|
+
</PanoViewer>;
|
|
527
643
|
```
|
|
528
644
|
|
|
645
|
+
Set `draggable` and update the controlled position from `onPositionChange` to
|
|
646
|
+
move a selected hotspot. Clickable hotspots need `ariaLabel`: PanoViewer creates
|
|
647
|
+
an internal semantic control for Tab, Enter, and Space activation, with a
|
|
648
|
+
visible WebGL focus outline.
|
|
649
|
+
|
|
650
|
+
`ImageHotspot` calls `onLoad(texture)` and `onError(error)`.
|
|
651
|
+
|
|
652
|
+
## GraphicHotspot
|
|
653
|
+
|
|
529
654
|
`GraphicHotspot` accepts built-in `circle`, `triangle`, `diamond`, `star`,
|
|
530
655
|
`arrow`, `rectangle`, and `ring` graphics, an SVG URL, or safe SVG path data
|
|
531
656
|
with an explicit viewBox. Built-in graphics support `fill`, `stroke`, and
|
|
@@ -577,13 +702,8 @@ property to orient them in another direction.
|
|
|
577
702
|
square corners and `0.5` produces the maximum rounded corners. This replaces
|
|
578
703
|
the previous Canvas-texture pixel interpretation.
|
|
579
704
|
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
an internal semantic control for Tab, Enter, and Space activation, with a
|
|
583
|
-
visible WebGL focus outline.
|
|
584
|
-
|
|
585
|
-
`ImageHotspot` calls `onLoad(texture)` and `onError(error)`. `GraphicHotspot`
|
|
586
|
-
uses the same callbacks. Built-in paths are rasterized locally; only an SVG URL
|
|
705
|
+
`GraphicHotspot` uses the same `onLoad(texture)` and `onError(error)` callbacks
|
|
706
|
+
as `ImageHotspot`. Built-in paths are rasterized locally; only an SVG URL
|
|
587
707
|
or SVG `path` data plus an explicit `viewBox` is accepted, never arbitrary SVG
|
|
588
708
|
markup.
|
|
589
709
|
|
|
@@ -621,7 +741,7 @@ placement properties:
|
|
|
621
741
|
/>
|
|
622
742
|
```
|
|
623
743
|
|
|
624
|
-
##
|
|
744
|
+
## PolygonHotspot
|
|
625
745
|
|
|
626
746
|
`PolygonHotspot` renders a local spherical area from three or more controlled
|
|
627
747
|
yaw/pitch vertices. Concave polygons and the `-180°/180°` seam are supported.
|
|
@@ -660,7 +780,7 @@ Set `fillOpacity={0}` for a closed outline-only polygon. The fill and outline
|
|
|
660
780
|
share the same spherical edge sampling, so there is no intentional seam
|
|
661
781
|
between them.
|
|
662
782
|
|
|
663
|
-
##
|
|
783
|
+
## PolylineHotspot
|
|
664
784
|
|
|
665
785
|
`PolylineHotspot` is an open path of at least two yaw/pitch vertices. It uses
|
|
666
786
|
the same CSS-pixel `strokeWidth`, visibility, semantic interaction, controlled
|
|
@@ -684,7 +804,7 @@ import { PolylineHotspot } from "@ericchen1990/pano-view";
|
|
|
684
804
|
|
|
685
805
|
`onInvalid` reports the only invalid runtime shape: fewer than two vertices.
|
|
686
806
|
|
|
687
|
-
##
|
|
807
|
+
## SequenceHotspot
|
|
688
808
|
|
|
689
809
|
`SequenceHotspot` animates a sprite sheet: one image containing equally sized
|
|
690
810
|
frames in a vertical or horizontal strip. This matches the common krpano
|
|
@@ -712,6 +832,12 @@ import { SequenceHotspot } from "@ericchen1990/pano-view";
|
|
|
712
832
|
/>;
|
|
713
833
|
```
|
|
714
834
|
|
|
835
|
+
Sequence loading uses `onLoadProgress` and `onError`. The component does not
|
|
836
|
+
change `playing` by itself: update that prop in response to your UI, click
|
|
837
|
+
handler, `onEnded`, or playback error.
|
|
838
|
+
|
|
839
|
+
## VideoHotspot
|
|
840
|
+
|
|
715
841
|
`VideoHotspot` uses an `HTMLVideoElement` and `VideoTexture`. Its `playing`
|
|
716
842
|
prop is likewise controlled: toggle it from your click handler or application
|
|
717
843
|
state. On source change or unmount, the element is paused and its texture is
|
|
@@ -745,24 +871,72 @@ Browsers can reject unmuted or otherwise non-gesture playback. In that case
|
|
|
745
871
|
the browser error; retain control of `playing` in the host and offer an
|
|
746
872
|
explicit user action.
|
|
747
873
|
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
playback error. This keeps source changes, unmounts, and React StrictMode
|
|
752
|
-
lifecycles deterministic.
|
|
874
|
+
Video reports media and poster failures through `onError`. Like
|
|
875
|
+
`SequenceHotspot`, it does not change `playing` by itself. This keeps source
|
|
876
|
+
changes, unmounts, and React StrictMode lifecycles deterministic.
|
|
753
877
|
|
|
754
|
-
##
|
|
878
|
+
## TextHotspot
|
|
755
879
|
|
|
756
|
-
|
|
880
|
+
`TextHotspot` rasterizes plain text onto a canvas texture. It does not accept
|
|
881
|
+
HTML or Markdown. `\n` is a hard line break; `whiteSpace="normal"` (default)
|
|
882
|
+
wraps words to the hotspot's angular width. Dimensions stay in degrees like
|
|
883
|
+
the other point hotspots. `fontSize`, `padding`, and `borderRadius` are
|
|
884
|
+
fractions of the texture, so they scale with `width` / `height`.
|
|
757
885
|
|
|
758
886
|
```tsx
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
887
|
+
import { TextHotspot } from "@ericchen1990/pano-view";
|
|
888
|
+
|
|
889
|
+
<TextHotspot
|
|
890
|
+
id="caption"
|
|
891
|
+
ariaLabel="Courtyard overlook"
|
|
892
|
+
position={{ yaw: 0, pitch: -16 }}
|
|
893
|
+
width={16}
|
|
894
|
+
height={6}
|
|
895
|
+
mode="billboard"
|
|
896
|
+
scaleMode="fixed"
|
|
897
|
+
text={"Courtyard overlook\nNorth terrace"}
|
|
898
|
+
align="center"
|
|
899
|
+
verticalAlign="middle"
|
|
900
|
+
fontSize={0.18}
|
|
901
|
+
color="#f8fafc"
|
|
902
|
+
background="#111827"
|
|
903
|
+
backgroundOpacity={0.72}
|
|
904
|
+
/>;
|
|
763
905
|
```
|
|
764
906
|
|
|
765
|
-
|
|
907
|
+
Load a custom `fontFamily` on the page before the hotspot paints; canvas text
|
|
908
|
+
uses whatever faces the document already has. `onLoad(texture)` and
|
|
909
|
+
`onError(error)` match `ImageHotspot`.
|
|
910
|
+
|
|
911
|
+
## IframeHotspot
|
|
912
|
+
|
|
913
|
+
`IframeHotspot` places an embedded document with `@react-three/drei` `Html`
|
|
914
|
+
`transform`, so it follows the same hotspot mode, rotation, and zoom scaling
|
|
915
|
+
as a texture plane. The iframe is a DOM overlay and is not occluded by the
|
|
916
|
+
panorama shell. There is no `srcdoc` prop.
|
|
917
|
+
|
|
918
|
+
```tsx
|
|
919
|
+
import { IframeHotspot } from "@ericchen1990/pano-view";
|
|
920
|
+
|
|
921
|
+
<IframeHotspot
|
|
922
|
+
id="guide"
|
|
923
|
+
ariaLabel="Open visitor guide"
|
|
924
|
+
position={{ yaw: -62, pitch: 4 }}
|
|
925
|
+
width={18}
|
|
926
|
+
height={12}
|
|
927
|
+
mode="billboard"
|
|
928
|
+
src="/hotspots/embed.html"
|
|
929
|
+
pointerPolicy="hotspot"
|
|
930
|
+
/>;
|
|
931
|
+
```
|
|
932
|
+
|
|
933
|
+
`pointerPolicy="hotspot"` (default) sets the iframe to `pointer-events: none`
|
|
934
|
+
so clicks and drags hit the WebGL plane. `"content"` makes the page
|
|
935
|
+
interactive and blocks dragging through the iframe. `sandbox` defaults to
|
|
936
|
+
`allow-scripts allow-popups allow-forms` without `allow-same-origin`.
|
|
937
|
+
`referrerPolicy` defaults to `strict-origin-when-cross-origin`. Prefer a
|
|
938
|
+
same-origin document; many third-party sites send `X-Frame-Options` and will
|
|
939
|
+
not load.
|
|
766
940
|
|
|
767
941
|
## Next.js and SSR
|
|
768
942
|
|