@ericchen1990/pano-view 0.1.6 → 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 +528 -90
- package/dist/index.cjs +3556 -861
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.mts +552 -40
- package/dist/index.d.ts +552 -40
- package/dist/index.mjs +3530 -827
- 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,51 +249,104 @@ 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
|
-
|
|
259
|
+
<PanoViewer style={{ height: 560 }}>
|
|
260
|
+
<AutoRotate enabled speed={12} acceleration={6} startDelay={2_000} />
|
|
261
|
+
<Sphere src="/panoramas/room.webp" />
|
|
262
|
+
</PanoViewer>
|
|
263
|
+
```
|
|
264
|
+
|
|
265
|
+
## Mouse, touch, and keyboard controls
|
|
266
|
+
|
|
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.
|
|
268
|
+
|
|
269
|
+
```tsx
|
|
270
|
+
<PanoViewer
|
|
271
|
+
controls={{
|
|
272
|
+
invert: false,
|
|
273
|
+
bouncingLimits: false,
|
|
274
|
+
mouse: { zoomSpeed: 0.12, wheel: true, buttons: ["left"] },
|
|
275
|
+
touch: { pinchZoom: true },
|
|
276
|
+
keyboard: { enabled: true, rotateSpeed: 90 },
|
|
277
|
+
}}
|
|
278
|
+
style={{ height: 560 }}
|
|
279
|
+
>
|
|
280
|
+
<Sphere src="/panoramas/room.webp" />
|
|
281
|
+
</PanoViewer>
|
|
282
|
+
```
|
|
283
|
+
|
|
284
|
+
**Mouse** (pointer types `mouse` / `pen`): drag look and optional wheel zoom. Defaults: `rotateSpeed` `0.35`, `zoomSpeed` `0.08`, `wheel` `true`, `buttons` `["left"]`.
|
|
285
|
+
|
|
286
|
+
### MouseControls
|
|
287
|
+
|
|
288
|
+
See the defaults above. Override through `controls.mouse` or by rendering `<MouseControls />` as a child.
|
|
289
|
+
|
|
290
|
+
### TouchControls
|
|
291
|
+
|
|
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.
|
|
297
|
+
|
|
298
|
+
Shared all-mode options on `controls`: `enabled`, `invert` (drag direction for mouse/touch), `bouncingLimits`, `fovSpeed`, `frictionStop` (default `0.01`), plus existing damping / inertia.
|
|
299
|
+
|
|
300
|
+
### Overriding a control channel
|
|
301
|
+
|
|
302
|
+
Render the component with the props you need. The default instance for that channel is skipped:
|
|
303
|
+
|
|
304
|
+
```tsx
|
|
305
|
+
import { useState } from "react";
|
|
158
306
|
import {
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
307
|
+
KeyboardControls,
|
|
308
|
+
Scenes,
|
|
309
|
+
PanoViewer,
|
|
310
|
+
cycleSceneId,
|
|
311
|
+
type Scene,
|
|
163
312
|
} from "@ericchen1990/pano-view";
|
|
164
313
|
|
|
165
|
-
export function
|
|
166
|
-
const
|
|
314
|
+
export function KeyboardExample({ scenes }: { scenes: Scene[] }) {
|
|
315
|
+
const [activeSceneId, setActiveSceneId] = useState(scenes[0]!.id);
|
|
167
316
|
|
|
168
317
|
return (
|
|
169
|
-
|
|
170
|
-
<
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
318
|
+
<PanoViewer style={{ height: 560 }}>
|
|
319
|
+
<KeyboardControls
|
|
320
|
+
keys={{
|
|
321
|
+
left: ["ArrowLeft", "a"],
|
|
322
|
+
right: ["ArrowRight", "d"],
|
|
323
|
+
up: ["ArrowUp", "w"],
|
|
324
|
+
down: ["ArrowDown", "s"],
|
|
325
|
+
zoomIn: ["=", "+"],
|
|
326
|
+
zoomOut: ["-", "_"],
|
|
327
|
+
previousScene: ["[", "PageUp"],
|
|
328
|
+
nextScene: ["]", "PageDown"],
|
|
179
329
|
}}
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
330
|
+
rotateSpeed={60}
|
|
331
|
+
zoomSpeed={30}
|
|
332
|
+
onPreviousScene={() => {
|
|
333
|
+
const next = cycleSceneId(scenes, activeSceneId, -1);
|
|
334
|
+
if (next) setActiveSceneId(next);
|
|
335
|
+
}}
|
|
336
|
+
onNextScene={() => {
|
|
337
|
+
const next = cycleSceneId(scenes, activeSceneId, 1);
|
|
338
|
+
if (next) setActiveSceneId(next);
|
|
339
|
+
}}
|
|
340
|
+
/>
|
|
341
|
+
<Scenes scenes={scenes} activeSceneId={activeSceneId} />
|
|
342
|
+
</PanoViewer>
|
|
190
343
|
);
|
|
191
344
|
}
|
|
192
345
|
```
|
|
193
346
|
|
|
194
|
-
|
|
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).
|
|
348
|
+
|
|
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.
|
|
195
350
|
|
|
196
351
|
## Panorama coordinate events
|
|
197
352
|
|
|
@@ -201,7 +356,7 @@ convention as the camera: positive yaw looks right and positive pitch looks up.
|
|
|
201
356
|
|
|
202
357
|
```tsx
|
|
203
358
|
import {
|
|
204
|
-
|
|
359
|
+
PanoViewer,
|
|
205
360
|
Sphere,
|
|
206
361
|
panoPositionToVector3,
|
|
207
362
|
type HotspotPosition,
|
|
@@ -214,14 +369,14 @@ export function PlacementExample() {
|
|
|
214
369
|
};
|
|
215
370
|
|
|
216
371
|
return (
|
|
217
|
-
<
|
|
372
|
+
<PanoViewer
|
|
218
373
|
onPanoramaClick={({ position }) => place(position)}
|
|
219
374
|
onPanoramaDoubleClick={({ position }) => console.log("double", position)}
|
|
220
375
|
onPanoramaPointerMove={({ position }) => console.log("move", position)}
|
|
221
376
|
style={{ height: 560 }}
|
|
222
377
|
>
|
|
223
378
|
<Sphere src="/panoramas/room.webp" />
|
|
224
|
-
</
|
|
379
|
+
</PanoViewer>
|
|
225
380
|
);
|
|
226
381
|
}
|
|
227
382
|
```
|
|
@@ -231,6 +386,166 @@ export function PlacementExample() {
|
|
|
231
386
|
is normalized to `[-180, 180)`. Pitch is clamped to `[-90, 90]`; at either
|
|
232
387
|
pole, yaw is normalized to `0` because it does not identify a unique point.
|
|
233
388
|
|
|
389
|
+
## PanoEvents
|
|
390
|
+
|
|
391
|
+
`PanoEvents` is the React counterpart to krpano's global `<events>` element.
|
|
392
|
+
Render one or more instances inside `PanoViewer` to subscribe to viewer-level
|
|
393
|
+
lifecycle and interaction callbacks. Multiple instances coexist (like named
|
|
394
|
+
krpano events); each tracks its own `idleTime`. For composition inside a custom
|
|
395
|
+
child, use `usePanoEvents` instead.
|
|
396
|
+
|
|
397
|
+
### usePanoEvents
|
|
398
|
+
|
|
399
|
+
```tsx
|
|
400
|
+
import {
|
|
401
|
+
AutoRotate,
|
|
402
|
+
PanoEvents,
|
|
403
|
+
PanoViewer,
|
|
404
|
+
Sphere,
|
|
405
|
+
} from "@ericchen1990/pano-view";
|
|
406
|
+
|
|
407
|
+
<PanoViewer style={{ height: 560 }}>
|
|
408
|
+
<PanoEvents
|
|
409
|
+
idleTime={3000}
|
|
410
|
+
onIdle={() => console.log("idle")}
|
|
411
|
+
onIdleEnd={() => console.log("active again")}
|
|
412
|
+
onViewSettled={(view) => console.log("settled", view)}
|
|
413
|
+
onViewInteractionStart={({ source }) => console.log("drag", source)}
|
|
414
|
+
onClick={({ position }) => console.log("pano click", position)}
|
|
415
|
+
onEnterFullscreen={() => console.log("fullscreen")}
|
|
416
|
+
onAutoRotateOneRound={() => console.log("full turn")}
|
|
417
|
+
/>
|
|
418
|
+
<AutoRotate enabled />
|
|
419
|
+
<Sphere src="/panoramas/room.webp" />
|
|
420
|
+
</PanoViewer>;
|
|
421
|
+
```
|
|
422
|
+
|
|
423
|
+
Supported callbacks (krpano names in parentheses where applicable):
|
|
424
|
+
|
|
425
|
+
- View: `onViewChange` (`onviewchange`), `onViewSettled`, `onViewInteractionStart` / `onViewInteractionEnd`
|
|
426
|
+
- Pointer on the panorama shell (not hotspots): `onClick`, `onDoubleClick`, `onPointerDown`, `onPointerUp`, `onPointerMove`, `onContextMenu`
|
|
427
|
+
- `onWheel` — mouse wheel only; touch pinch does not synthesize a wheel event
|
|
428
|
+
- Idle: `onIdle` / `onIdleEnd` with per-instance `idleTime` (ms, default `2000`)
|
|
429
|
+
- Fullscreen: `onEnterFullscreen` / `onExitFullscreen`
|
|
430
|
+
- `onResize` — canvas content-box size via `ResizeObserver`
|
|
431
|
+
- Auto-rotate: `onAutoRotateStart` / `onAutoRotateStop` / `onAutoRotateOneRound`
|
|
432
|
+
|
|
433
|
+
`PanoViewer`'s existing `onViewChange` / `onPanoramaClick` / `onPanoramaDoubleClick` /
|
|
434
|
+
`onPanoramaPointerMove` props remain supported and share the same event bus.
|
|
435
|
+
|
|
436
|
+
Resource loading and scene blending stay on their owners: use `Sphere` /
|
|
437
|
+
`Tile` `onLoad` / `onError` / `onLoadProgress`, and `Scenes`
|
|
438
|
+
`onTransitionEnd` / `onTransitionError`. This package does not mirror krpano
|
|
439
|
+
xml/VR/gyro/frame-render events.
|
|
440
|
+
|
|
441
|
+
## PanoContextMenu
|
|
442
|
+
|
|
443
|
+
`PanoViewer` mounts a default context menu that replaces the browser menu on
|
|
444
|
+
right-click. Default items: **Reset view** and **Enter fullscreen** / **Exit
|
|
445
|
+
fullscreen** (label and icon follow the current fullscreen state), with a
|
|
446
|
+
separator between them.
|
|
447
|
+
|
|
448
|
+
```tsx
|
|
449
|
+
<PanoViewer style={{ height: 560 }}>
|
|
450
|
+
<Sphere src="/panoramas/room.webp" />
|
|
451
|
+
</PanoViewer>
|
|
452
|
+
```
|
|
453
|
+
|
|
454
|
+
Disable the default menu (restore the browser menu, or mount your own):
|
|
455
|
+
|
|
456
|
+
```tsx
|
|
457
|
+
<PanoViewer contextMenu={false}>
|
|
458
|
+
<Sphere src="/panoramas/room.webp" />
|
|
459
|
+
</PanoViewer>
|
|
460
|
+
```
|
|
461
|
+
|
|
462
|
+
Tune appearance while keeping the default items:
|
|
463
|
+
|
|
464
|
+
```tsx
|
|
465
|
+
<PanoViewer contextMenu={{ appearance: { opacity: 0.92, borderRadius: 8 } }}>
|
|
466
|
+
<Sphere src="/panoramas/room.webp" />
|
|
467
|
+
</PanoViewer>
|
|
468
|
+
```
|
|
469
|
+
|
|
470
|
+
### Adding items without rebuilding defaults
|
|
471
|
+
|
|
472
|
+
Use `append` or `prepend` to keep the built-in Reset / Fullscreen entries and
|
|
473
|
+
only add your own:
|
|
474
|
+
|
|
475
|
+
```tsx
|
|
476
|
+
<PanoViewer
|
|
477
|
+
contextMenu={{
|
|
478
|
+
append: [
|
|
479
|
+
"separator",
|
|
480
|
+
{
|
|
481
|
+
id: "copy",
|
|
482
|
+
label: "Copy yaw / pitch",
|
|
483
|
+
onSelect: ({ position }) => {
|
|
484
|
+
void navigator.clipboard.writeText(
|
|
485
|
+
`${position.yaw.toFixed(1)}, ${position.pitch.toFixed(1)}`,
|
|
486
|
+
);
|
|
487
|
+
},
|
|
488
|
+
},
|
|
489
|
+
],
|
|
490
|
+
}}
|
|
491
|
+
style={{ height: 560 }}
|
|
492
|
+
>
|
|
493
|
+
<Sphere src="/panoramas/room.webp" />
|
|
494
|
+
</PanoViewer>
|
|
495
|
+
```
|
|
496
|
+
|
|
497
|
+
### Built-in presets
|
|
498
|
+
|
|
499
|
+
When you need a custom order, reuse built-in presets by id instead of
|
|
500
|
+
reimplementing fullscreen enter/exit state:
|
|
501
|
+
|
|
502
|
+
- `"resetView"` — resets the view
|
|
503
|
+
- `"fullscreen"` — Enter / Exit fullscreen with the matching icon
|
|
504
|
+
- `"separator"` — a horizontal rule
|
|
505
|
+
|
|
506
|
+
```tsx
|
|
507
|
+
<PanoViewer
|
|
508
|
+
contextMenu={{
|
|
509
|
+
items: [
|
|
510
|
+
"resetView",
|
|
511
|
+
"separator",
|
|
512
|
+
{
|
|
513
|
+
id: "copy",
|
|
514
|
+
label: "Copy yaw / pitch",
|
|
515
|
+
onSelect: ({ position }) => {
|
|
516
|
+
void navigator.clipboard.writeText(
|
|
517
|
+
`${position.yaw.toFixed(1)}, ${position.pitch.toFixed(1)}`,
|
|
518
|
+
);
|
|
519
|
+
},
|
|
520
|
+
},
|
|
521
|
+
"separator",
|
|
522
|
+
"fullscreen",
|
|
523
|
+
],
|
|
524
|
+
}}
|
|
525
|
+
style={{ height: 560 }}
|
|
526
|
+
>
|
|
527
|
+
<Sphere src="/panoramas/room.webp" />
|
|
528
|
+
</PanoViewer>
|
|
529
|
+
```
|
|
530
|
+
|
|
531
|
+
Override presentation on a preset with `{ preset: "fullscreen", label: "…" }`.
|
|
532
|
+
For fully manual assembly outside the prop (for example a custom
|
|
533
|
+
`PanoContextMenu` child), call `createPanoContextMenuPresets({ reset,
|
|
534
|
+
toggleFullscreen, isFullscreen })` and use `.resetView` / `.fullscreen` /
|
|
535
|
+
`.defaults`.
|
|
536
|
+
|
|
537
|
+
Concrete items may use an `icon` React node or an `image` URL. Separators
|
|
538
|
+
always come from the `"separator"` preset. Appearance covers background, text
|
|
539
|
+
color, border, radius, shadow, background-only `opacity` (clamped from
|
|
540
|
+
`MIN_PANO_CONTEXT_MENU_BACKGROUND_OPACITY` / `0.4` to `1`; labels and icons
|
|
541
|
+
stay opaque), hover/disabled colors, and icon size. For an explicit translucent
|
|
542
|
+
fill without using `opacity`, you can still pass `background: "rgba(...)"`.
|
|
543
|
+
|
|
544
|
+
For a fully custom DOM menu, set `contextMenu={false}` and render
|
|
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.
|
|
548
|
+
|
|
234
549
|
## Shared hotspot contract and saved definitions
|
|
235
550
|
|
|
236
551
|
Every hotspot has an `id`, optional `visible`, `interactive`, `renderOrder`,
|
|
@@ -238,6 +553,8 @@ and semantic interaction callbacks. Point hotspots additionally use a
|
|
|
238
553
|
controlled `position`, angular `width`/`height`, hotspot mode, and the zoom
|
|
239
554
|
options described below. `interactive={false}` keeps a hotspot visible while
|
|
240
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.
|
|
241
558
|
|
|
242
559
|
`onClick` and `onHoverChange` receive a position and input source (`"pointer"`
|
|
243
560
|
or `"keyboard"`). Point hotspots can use `draggable`, `onDragStart`,
|
|
@@ -245,9 +562,43 @@ or `"keyboard"`). Point hotspots can use `draggable`, `onDragStart`,
|
|
|
245
562
|
back to its state. Polygon and polyline hotspots report the corresponding
|
|
246
563
|
controlled `vertices` through `onVerticesChange`.
|
|
247
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
|
+
|
|
248
598
|
For persistence or a host-owned editor, use the exported discriminated
|
|
249
|
-
`HotspotDefinition` union.
|
|
250
|
-
|
|
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:
|
|
251
602
|
|
|
252
603
|
```ts
|
|
253
604
|
import type { HotspotDefinition } from "@ericchen1990/pano-view";
|
|
@@ -257,6 +608,8 @@ const hotspots: HotspotDefinition[] = [
|
|
|
257
608
|
{ type: "graphic", id: "marker", position: { yaw: -18, pitch: 9 }, graphic: { kind: "ring" } },
|
|
258
609
|
{ type: "sequence", id: "pulse", position: { yaw: -42, pitch: -7 }, src: "/hotspots/pulse.png", frameCount: 20 },
|
|
259
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" },
|
|
260
613
|
{ type: "polygon", id: "zone", vertices: [{ yaw: 12, pitch: 4 }, { yaw: 22, pitch: 4 }, { yaw: 18, pitch: 14 }] },
|
|
261
614
|
{ type: "polyline", id: "route", vertices: [{ yaw: -8, pitch: 1 }, { yaw: 4, pitch: 8 }] },
|
|
262
615
|
];
|
|
@@ -266,15 +619,15 @@ Definitions intentionally contain data only. Render them with a switch in the
|
|
|
266
619
|
host, where application-specific click actions, controlled media state, and
|
|
267
620
|
error reporting belong.
|
|
268
621
|
|
|
269
|
-
##
|
|
622
|
+
## ImageHotspot
|
|
270
623
|
|
|
271
624
|
`ImageHotspot` renders an image at a controlled spherical position. Dimensions
|
|
272
625
|
are angular degrees, so they remain independent of the canvas resolution.
|
|
273
626
|
|
|
274
627
|
```tsx
|
|
275
|
-
import { ImageHotspot,
|
|
628
|
+
import { ImageHotspot, PanoViewer, Sphere } from "@ericchen1990/pano-view";
|
|
276
629
|
|
|
277
|
-
<
|
|
630
|
+
<PanoViewer style={{ height: 560 }}>
|
|
278
631
|
<Sphere src="/panoramas/room.webp" />
|
|
279
632
|
<ImageHotspot
|
|
280
633
|
id="gallery"
|
|
@@ -286,13 +639,23 @@ import { ImageHotspot, PanoView, Sphere } from "@ericchen1990/pano-view";
|
|
|
286
639
|
src="/hotspots/gallery.webp"
|
|
287
640
|
onClick={({ position }) => console.log("gallery", position)}
|
|
288
641
|
/>
|
|
289
|
-
</
|
|
642
|
+
</PanoViewer>;
|
|
290
643
|
```
|
|
291
644
|
|
|
292
|
-
`
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
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
|
+
|
|
654
|
+
`GraphicHotspot` accepts built-in `circle`, `triangle`, `diamond`, `star`,
|
|
655
|
+
`arrow`, `rectangle`, and `ring` graphics, an SVG URL, or safe SVG path data
|
|
656
|
+
with an explicit viewBox. Built-in graphics support `fill`, `stroke`, and
|
|
657
|
+
`strokeWidth`; rectangles support a relative `cornerRadius` from `0` to `0.5`,
|
|
658
|
+
and rings support `innerRadius`.
|
|
296
659
|
|
|
297
660
|
```tsx
|
|
298
661
|
import { GraphicHotspot } from "@ericchen1990/pano-view";
|
|
@@ -314,13 +677,33 @@ import { GraphicHotspot } from "@ericchen1990/pano-view";
|
|
|
314
677
|
/>
|
|
315
678
|
```
|
|
316
679
|
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
visible WebGL focus outline.
|
|
680
|
+
Triangle, diamond, star, and arrow graphics use the same paint properties. The
|
|
681
|
+
triangle and arrow point upward at `rotation={0}`; use the hotspot's `rotation`
|
|
682
|
+
property to orient them in another direction.
|
|
321
683
|
|
|
322
|
-
|
|
323
|
-
|
|
684
|
+
```tsx
|
|
685
|
+
<GraphicHotspot
|
|
686
|
+
id="direction"
|
|
687
|
+
ariaLabel="Open next scene"
|
|
688
|
+
position={{ yaw: 8, pitch: 2 }}
|
|
689
|
+
rotation={45}
|
|
690
|
+
width={8}
|
|
691
|
+
height={8}
|
|
692
|
+
graphic={{
|
|
693
|
+
kind: "arrow",
|
|
694
|
+
fill: "#df6b42",
|
|
695
|
+
stroke: "#f5fbfc",
|
|
696
|
+
strokeWidth: 8,
|
|
697
|
+
}}
|
|
698
|
+
/>
|
|
699
|
+
```
|
|
700
|
+
|
|
701
|
+
`cornerRadius` is relative to the rendered graphic's shorter side: `0` keeps
|
|
702
|
+
square corners and `0.5` produces the maximum rounded corners. This replaces
|
|
703
|
+
the previous Canvas-texture pixel interpretation.
|
|
704
|
+
|
|
705
|
+
`GraphicHotspot` uses the same `onLoad(texture)` and `onError(error)` callbacks
|
|
706
|
+
as `ImageHotspot`. Built-in paths are rasterized locally; only an SVG URL
|
|
324
707
|
or SVG `path` data plus an explicit `viewBox` is accepted, never arbitrary SVG
|
|
325
708
|
markup.
|
|
326
709
|
|
|
@@ -337,9 +720,10 @@ placement properties:
|
|
|
337
720
|
- `scaleMode="fov"` (default) lets a hotspot grow when the user zooms in.
|
|
338
721
|
`scaleMode="fixed"` compensates for FOV changes and keeps its screen size
|
|
339
722
|
close to the size at `referenceFov` (default `75`).
|
|
340
|
-
- `rotation` rotates the hotspot around its own normal in degrees
|
|
341
|
-
|
|
342
|
-
|
|
723
|
+
- `rotation` rotates the hotspot around its own normal in degrees; positive
|
|
724
|
+
values rotate clockwise and negative values rotate counterclockwise.
|
|
725
|
+
`scale` applies an overall positive multiplier to its angular width and
|
|
726
|
+
height and defaults to `1`.
|
|
343
727
|
|
|
344
728
|
```tsx
|
|
345
729
|
<ImageHotspot
|
|
@@ -357,7 +741,7 @@ placement properties:
|
|
|
357
741
|
/>
|
|
358
742
|
```
|
|
359
743
|
|
|
360
|
-
##
|
|
744
|
+
## PolygonHotspot
|
|
361
745
|
|
|
362
746
|
`PolygonHotspot` renders a local spherical area from three or more controlled
|
|
363
747
|
yaw/pitch vertices. Concave polygons and the `-180°/180°` seam are supported.
|
|
@@ -396,7 +780,7 @@ Set `fillOpacity={0}` for a closed outline-only polygon. The fill and outline
|
|
|
396
780
|
share the same spherical edge sampling, so there is no intentional seam
|
|
397
781
|
between them.
|
|
398
782
|
|
|
399
|
-
##
|
|
783
|
+
## PolylineHotspot
|
|
400
784
|
|
|
401
785
|
`PolylineHotspot` is an open path of at least two yaw/pitch vertices. It uses
|
|
402
786
|
the same CSS-pixel `strokeWidth`, visibility, semantic interaction, controlled
|
|
@@ -420,7 +804,7 @@ import { PolylineHotspot } from "@ericchen1990/pano-view";
|
|
|
420
804
|
|
|
421
805
|
`onInvalid` reports the only invalid runtime shape: fewer than two vertices.
|
|
422
806
|
|
|
423
|
-
##
|
|
807
|
+
## SequenceHotspot
|
|
424
808
|
|
|
425
809
|
`SequenceHotspot` animates a sprite sheet: one image containing equally sized
|
|
426
810
|
frames in a vertical or horizontal strip. This matches the common krpano
|
|
@@ -448,6 +832,12 @@ import { SequenceHotspot } from "@ericchen1990/pano-view";
|
|
|
448
832
|
/>;
|
|
449
833
|
```
|
|
450
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
|
+
|
|
451
841
|
`VideoHotspot` uses an `HTMLVideoElement` and `VideoTexture`. Its `playing`
|
|
452
842
|
prop is likewise controlled: toggle it from your click handler or application
|
|
453
843
|
state. On source change or unmount, the element is paused and its texture is
|
|
@@ -481,24 +871,72 @@ Browsers can reject unmuted or otherwise non-gesture playback. In that case
|
|
|
481
871
|
the browser error; retain control of `playing` in the host and offer an
|
|
482
872
|
explicit user action.
|
|
483
873
|
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
playback error. This keeps source changes, unmounts, and React StrictMode
|
|
488
|
-
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.
|
|
489
877
|
|
|
490
|
-
##
|
|
878
|
+
## TextHotspot
|
|
491
879
|
|
|
492
|
-
|
|
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`.
|
|
493
885
|
|
|
494
886
|
```tsx
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
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
|
+
/>;
|
|
499
905
|
```
|
|
500
906
|
|
|
501
|
-
|
|
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.
|
|
502
940
|
|
|
503
941
|
## Next.js and SSR
|
|
504
942
|
|