@ericchen1990/pano-view 0.2.0 → 2.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 +624 -130
- package/dist/index.cjs +7611 -1259
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.mts +576 -87
- package/dist/index.d.ts +576 -87
- package/dist/index.mjs +7649 -1228
- package/dist/index.mjs.map +1 -1
- package/package.json +5 -1
package/README.md
CHANGED
|
@@ -15,32 +15,230 @@ 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,
|
|
21
|
+
React 19, React DOM 19, Three.js, `@react-three/fiber` 9,
|
|
22
|
+
`@react-three/drei` 10, and `@react-three/xr` 6 are peer dependencies. npm 7+
|
|
23
|
+
and pnpm install them automatically; they must be a single copy in the host
|
|
24
|
+
app.
|
|
22
25
|
|
|
23
|
-
|
|
26
|
+
Tailwind CSS v4 is now required for the built-in HTML chrome: video controls,
|
|
27
|
+
captions, context menus, tooltips, and accessibility helpers render with
|
|
28
|
+
Tailwind utility classes instead of a bundled stylesheet. Add a source entry in
|
|
29
|
+
your app so Tailwind can scan this package:
|
|
24
30
|
|
|
25
|
-
|
|
31
|
+
```css
|
|
32
|
+
@import "tailwindcss";
|
|
33
|
+
@source "../node_modules/@ericchen1990/pano-view";
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
This package does not ship a separate CSS file. If your app does not scan
|
|
37
|
+
`@ericchen1990/pano-view`, those built-in overlays will render without their
|
|
38
|
+
default styling. Customization still works through `className`, `style`, and
|
|
39
|
+
component appearance props.
|
|
40
|
+
|
|
41
|
+
## Exported components
|
|
42
|
+
|
|
43
|
+
### Viewer
|
|
44
|
+
|
|
45
|
+
- [`PanoViewer`](#panoviewer) — canvas shell, camera, default controls, and imperative view API
|
|
46
|
+
- [`Sphere`](#sphere) — single 2:1 equirectangular image
|
|
47
|
+
- [`PanoVideo`](#panovideo) — 2:1 equirectangular video on the panorama sphere
|
|
48
|
+
- [`Tile`](#tile) — krpano-style multires cube tiles
|
|
49
|
+
- [`Scenes`](#scenes) — controlled multi-scene transitions
|
|
50
|
+
- [`PanoFilter`](#panofilter) — color and artistic filters on the panorama source
|
|
51
|
+
|
|
52
|
+
### Controls
|
|
53
|
+
|
|
54
|
+
- [`MouseControls`](#mousecontrols) — drag look and wheel zoom
|
|
55
|
+
- [`TouchControls`](#touchcontrols) — one-finger drag and pinch zoom
|
|
56
|
+
- [`KeyboardControls`](#keyboardcontrols) — arrow-key look, FOV, and scene cycling
|
|
57
|
+
- [`AutoRotate`](#autorotate) — automatic yaw rotation
|
|
58
|
+
- [`Gyro`](#gyro) — opt-in device-orientation control
|
|
59
|
+
- [`WebVR`](#webvr) — WebXR-first immersive VR with MobileVR fallback
|
|
60
|
+
|
|
61
|
+
### Events and chrome
|
|
62
|
+
|
|
63
|
+
- [`PanoEvents`](#panoevents) — viewer-level lifecycle and interaction callbacks
|
|
64
|
+
- [`PanoContextMenu`](#panocontextmenu) — right-click reset / fullscreen menu
|
|
65
|
+
- [`PanoVideoControls`](#panovideo) — playback chrome for `PanoVideo` (mounted by default)
|
|
66
|
+
|
|
67
|
+
### Hotspots
|
|
68
|
+
|
|
69
|
+
- [`ImageHotspot`](#imagehotspot) — image texture at a spherical position
|
|
70
|
+
- [`GraphicHotspot`](#graphichotspot) — built-in shapes or SVG at a position
|
|
71
|
+
- [`SequenceHotspot`](#sequencehotspot) — sprite-sheet animation
|
|
72
|
+
- [`VideoHotspot`](#videohotspot) — HTML video texture
|
|
73
|
+
- [`TextHotspot`](#texthotspot) — rasterized plain text at a position
|
|
74
|
+
- [`IframeHotspot`](#iframehotspot) — embedded document overlay at a position
|
|
75
|
+
- [`PolygonHotspot`](#polygonhotspot) — closed spherical area
|
|
76
|
+
- [`PolylineHotspot`](#polylinehotspot) — open spherical path
|
|
77
|
+
|
|
78
|
+
### Hooks and helpers
|
|
79
|
+
|
|
80
|
+
- `usePanoEvents` — subscribe to viewer events inside a custom child ([`PanoEvents`](#panoevents))
|
|
81
|
+
- Coordinate helpers — `normalizePanoPosition`, `normalizePanoYaw`, `clampPanoPitch`, `panoPositionToVector3`, `vector3ToPanoPosition` ([Panorama coordinate events](#panorama-coordinate-events))
|
|
82
|
+
- `cycleSceneId` — wrap previous/next scene ids ([`KeyboardControls`](#keyboardcontrols))
|
|
83
|
+
- Context menu helpers — `createPanoContextMenuPresets`, `composePanoContextMenuItems`, and related APIs ([`PanoContextMenu`](#panocontextmenu))
|
|
84
|
+
- Polygon / polyline validation — `validatePolygonVertices`, `validatePolylineVertices`, `unwrapPolygonVertices`
|
|
85
|
+
|
|
86
|
+
## PanoViewer
|
|
87
|
+
|
|
88
|
+
`PanoViewer` owns the canvas, perspective camera, and controls. Give its container an explicit size and place one panorama source inside it.
|
|
89
|
+
|
|
90
|
+
Angles are public degrees. Positive yaw looks right and positive pitch looks up.
|
|
91
|
+
|
|
92
|
+
```tsx
|
|
93
|
+
import { useRef } from "react";
|
|
94
|
+
import {
|
|
95
|
+
PanoViewer,
|
|
96
|
+
Sphere,
|
|
97
|
+
AutoRotate,
|
|
98
|
+
type PanoViewerHandle,
|
|
99
|
+
} from "@ericchen1990/pano-view";
|
|
100
|
+
|
|
101
|
+
export function ControlledExample() {
|
|
102
|
+
const ref = useRef<PanoViewerHandle>(null);
|
|
103
|
+
|
|
104
|
+
return (
|
|
105
|
+
<>
|
|
106
|
+
<button onClick={() => ref.current?.setView({ yaw: 90, fov: 55 })}>
|
|
107
|
+
Look right
|
|
108
|
+
</button>
|
|
109
|
+
<PanoViewer
|
|
110
|
+
ref={ref}
|
|
111
|
+
controls={{
|
|
112
|
+
inertia: true,
|
|
113
|
+
rotateDamping: 14,
|
|
114
|
+
zoomDamping: 16,
|
|
115
|
+
}}
|
|
116
|
+
initialView={{ yaw: 0, pitch: 0, fov: 75 }}
|
|
117
|
+
minFov={30}
|
|
118
|
+
maxFov={100}
|
|
119
|
+
onViewChange={(view) => console.log(view)}
|
|
120
|
+
style={{ height: 560 }}
|
|
121
|
+
>
|
|
122
|
+
<AutoRotate enabled speed={18} acceleration={18} startDelay={1_000} />
|
|
123
|
+
<Sphere src="/panoramas/room.webp" />
|
|
124
|
+
</PanoViewer>
|
|
125
|
+
</>
|
|
126
|
+
);
|
|
127
|
+
}
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
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).
|
|
131
|
+
|
|
132
|
+
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.
|
|
133
|
+
|
|
134
|
+
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.
|
|
135
|
+
|
|
136
|
+
```tsx
|
|
137
|
+
<PanoViewer
|
|
138
|
+
cursors={{ default: "grab", dragging: "grabbing", hotspot: "pointer" }}
|
|
139
|
+
style={{ height: 560 }}
|
|
140
|
+
>
|
|
141
|
+
<ImageHotspot
|
|
142
|
+
id="door"
|
|
143
|
+
cursor="zoom-in"
|
|
144
|
+
position={{ yaw: 24, pitch: -6 }}
|
|
145
|
+
src="/hotspots/door.webp"
|
|
146
|
+
/>
|
|
147
|
+
</PanoViewer>
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
## Sphere
|
|
151
|
+
|
|
152
|
+
`Sphere` expects a 2:1 equirectangular image. Use `yawOffset` when the source's forward direction needs horizontal adjustment.
|
|
26
153
|
|
|
27
154
|
```tsx
|
|
28
155
|
"use client";
|
|
29
156
|
|
|
30
|
-
import {
|
|
157
|
+
import { PanoViewer, Sphere } from "@ericchen1990/pano-view";
|
|
31
158
|
|
|
32
159
|
export function SphereExample() {
|
|
33
160
|
return (
|
|
34
|
-
<
|
|
161
|
+
<PanoViewer style={{ width: "100%", height: 560 }}>
|
|
35
162
|
<Sphere src="/panoramas/room.webp" />
|
|
36
|
-
</
|
|
163
|
+
</PanoViewer>
|
|
37
164
|
);
|
|
38
165
|
}
|
|
39
166
|
```
|
|
40
167
|
|
|
41
|
-
|
|
168
|
+
## PanoVideo
|
|
169
|
+
|
|
170
|
+
`PanoVideo` maps a 2:1 equirectangular video onto the same inward sphere as
|
|
171
|
+
`Sphere`. Pass one `src`, or `variants` with multiple files per quality so the
|
|
172
|
+
browser can pick mp4 / webm and the control bar can switch resolution. Changing
|
|
173
|
+
quality keeps the current time and play/pause state.
|
|
174
|
+
|
|
175
|
+
A default playback bar is mounted on the viewer overlay: play/pause, seek, time,
|
|
176
|
+
volume, speed, resolution, captions, and fullscreen. Volume is a hover (or tap)
|
|
177
|
+
vertical slider when the browser allows `HTMLMediaElement.volume`; iOS Safari
|
|
178
|
+
only exposes mute. The speed menu lists rates the current element accepts. The
|
|
179
|
+
captions menu also includes a settings panel for subtitle font size and
|
|
180
|
+
background opacity. When the viewer is narrow, speed, quality, and captions
|
|
181
|
+
move into a More menu.
|
|
182
|
+
Pass `controls={false}` to hide it, an appearance object to restyle the default
|
|
183
|
+
instance, or render `PanoVideoControls` as a `PanoViewer` child to replace that
|
|
184
|
+
instance.
|
|
185
|
+
|
|
186
|
+
Captions use WebVTT `tracks`. The video element is off-screen, so cue text is
|
|
187
|
+
rendered as a HUD overlay. `captions={false}` hides the overlay and the language
|
|
188
|
+
menu; an object merges with the default appearance (`color`, `background`,
|
|
189
|
+
`fontSize`, `fontFamily`, `textShadow`, `padding`, `borderRadius`, `maxWidth`,
|
|
190
|
+
`bottom`). Those values act as the initial caption appearance; viewers can still
|
|
191
|
+
adjust subtitle size and background from the default control-bar menu at
|
|
192
|
+
runtime.
|
|
193
|
+
|
|
194
|
+
```tsx
|
|
195
|
+
"use client";
|
|
196
|
+
|
|
197
|
+
import { PanoViewer, PanoVideo } from "@ericchen1990/pano-view";
|
|
42
198
|
|
|
43
|
-
|
|
199
|
+
export function VideoExample() {
|
|
200
|
+
return (
|
|
201
|
+
<PanoViewer style={{ width: "100%", height: 560 }}>
|
|
202
|
+
<PanoVideo
|
|
203
|
+
defaultVariantId="1024"
|
|
204
|
+
variants={[
|
|
205
|
+
{
|
|
206
|
+
id: "1024",
|
|
207
|
+
label: "1024p",
|
|
208
|
+
poster: "/video/1024-poster.jpg",
|
|
209
|
+
sources: [
|
|
210
|
+
{ src: "/video/1024.mp4", type: "video/mp4" },
|
|
211
|
+
{ src: "/video/1024.webm", type: "video/webm" },
|
|
212
|
+
],
|
|
213
|
+
},
|
|
214
|
+
{
|
|
215
|
+
id: "1920",
|
|
216
|
+
label: "1920p",
|
|
217
|
+
poster: "/video/1920-poster.jpg",
|
|
218
|
+
sources: [
|
|
219
|
+
{ src: "/video/1920.mp4", type: "video/mp4" },
|
|
220
|
+
{ src: "/video/1920.webm", type: "video/webm" },
|
|
221
|
+
],
|
|
222
|
+
},
|
|
223
|
+
]}
|
|
224
|
+
tracks={[
|
|
225
|
+
{ src: "/video/en.vtt", srcLang: "en", label: "English", default: true },
|
|
226
|
+
{ src: "/video/zh.vtt", srcLang: "zh", label: "中文" },
|
|
227
|
+
]}
|
|
228
|
+
captions={{ fontSize: 16, color: "#fff" }}
|
|
229
|
+
muted
|
|
230
|
+
/>
|
|
231
|
+
</PanoViewer>
|
|
232
|
+
);
|
|
233
|
+
}
|
|
234
|
+
```
|
|
235
|
+
|
|
236
|
+
`muted` defaults to `true` so `autoPlay` has a chance under browser autoplay
|
|
237
|
+
rules. Unmuted playback still needs a user gesture; `onPlaybackStateChange`
|
|
238
|
+
receives `"blocked"` when `play()` is rejected. Remote videos and VTT files
|
|
239
|
+
that are not same-origin should set `crossOrigin` (usually `"anonymous"`).
|
|
240
|
+
|
|
241
|
+
## Tile
|
|
44
242
|
|
|
45
243
|
`Tile` renders six inward-facing cube faces and loads only tiles around the current view. The default layout matches krpano-style output:
|
|
46
244
|
|
|
@@ -51,16 +249,16 @@ tiles/{face}/l{level}/{row}/l{level}_{face}_{col}_{row}.webp
|
|
|
51
249
|
Faces are `f`, `r`, `b`, `l`, `u`, and `d`; rows and columns are 1-based.
|
|
52
250
|
|
|
53
251
|
```tsx
|
|
54
|
-
import {
|
|
252
|
+
import { PanoViewer, Tile } from "@ericchen1990/pano-view";
|
|
55
253
|
|
|
56
254
|
export function TileExample() {
|
|
57
255
|
return (
|
|
58
|
-
<
|
|
256
|
+
<PanoViewer style={{ width: "100%", height: 560 }}>
|
|
59
257
|
<Tile
|
|
60
258
|
baseUrl="https://cdn.example.com/panoramas/room"
|
|
61
259
|
multires="512,500,1000,2000"
|
|
62
260
|
/>
|
|
63
|
-
</
|
|
261
|
+
</PanoViewer>
|
|
64
262
|
);
|
|
65
263
|
}
|
|
66
264
|
```
|
|
@@ -103,9 +301,9 @@ horizontal index and `%00v` produces a three-digit vertical index. The stereo
|
|
|
103
301
|
`%t` and frame `%f` placeholders are not applicable to `Tile`, which represents
|
|
104
302
|
a single non-stereo cube panorama.
|
|
105
303
|
|
|
106
|
-
##
|
|
304
|
+
## Scenes
|
|
107
305
|
|
|
108
|
-
`
|
|
306
|
+
`Scenes` switches controlled sphere and cube-tile scenes with GPU-only
|
|
109
307
|
snapshot blending. The target scene loads its sphere image or tile preview first;
|
|
110
308
|
once ready, the current framebuffer becomes a temporary GPU texture, its source
|
|
111
309
|
textures are released, and the target scene blends in. This avoids holding two
|
|
@@ -113,12 +311,12 @@ high-resolution tile scenes in WebGL memory at once.
|
|
|
113
311
|
|
|
114
312
|
```tsx
|
|
115
313
|
import {
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
type
|
|
314
|
+
Scenes,
|
|
315
|
+
PanoViewer,
|
|
316
|
+
type Scene,
|
|
119
317
|
} from "@ericchen1990/pano-view";
|
|
120
318
|
|
|
121
|
-
const scenes:
|
|
319
|
+
const scenes: Scene[] = [
|
|
122
320
|
{ id: "lobby", type: "sphere", src: "/panoramas/lobby.webp" },
|
|
123
321
|
{
|
|
124
322
|
id: "terrace",
|
|
@@ -128,77 +326,222 @@ const scenes: PanoramaScene[] = [
|
|
|
128
326
|
},
|
|
129
327
|
];
|
|
130
328
|
|
|
131
|
-
<
|
|
132
|
-
<
|
|
329
|
+
<PanoViewer style={{ height: 560 }}>
|
|
330
|
+
<Scenes
|
|
133
331
|
scenes={scenes}
|
|
134
332
|
activeSceneId={activeSceneId}
|
|
135
333
|
transition="ellipticZoomOpen"
|
|
136
334
|
renderHotspots={(scene) => <SceneHotspots sceneId={scene.id} />}
|
|
137
335
|
onTransitionError={({ sceneId }) => console.warn("Could not load", sceneId)}
|
|
138
336
|
/>
|
|
139
|
-
</
|
|
337
|
+
</PanoViewer>;
|
|
140
338
|
```
|
|
141
339
|
|
|
142
|
-
Available presets are `none`, `crossfade`, `zoom`, `blackout`,
|
|
143
|
-
`slideRightToLeft`, `slideTopToBottom`, `slideDiagonal`,
|
|
144
|
-
`verticalOpen`, `horizontalOpen`, and `ellipticZoomOpen`.
|
|
145
|
-
|
|
340
|
+
Available KRpano-style presets are `none`, `crossfade`, `zoom`, `blackout`,
|
|
341
|
+
`whiteFlash`, `slideRightToLeft`, `slideTopToBottom`, `slideDiagonal`,
|
|
342
|
+
`circleOpen`, `verticalOpen`, `horizontalOpen`, and `ellipticZoomOpen`.
|
|
343
|
+
Grid presets are `pixelate`, `gridWipe`, `gridWipeUp`, `gridWipeRight`,
|
|
344
|
+
`gridWipeDiagonal`, `gridWipeCenter`, `gridWipeChecker`, and `hexDissolve`.
|
|
345
|
+
Cinematic presets are `dissolve`, `shatter`, `particles`, `glitch`, `swirl`,
|
|
346
|
+
`clockWipe`, `ripple`, `zoomBlur`, and `filmBurn`. All of them still blend a
|
|
347
|
+
GPU framebuffer snapshot over the incoming panorama; they do not mix two full
|
|
348
|
+
360 textures. Pass `{ preset: "crossfade", duration: 0.6 }` to override a
|
|
349
|
+
preset duration.
|
|
146
350
|
|
|
147
351
|
While a transition runs, panorama drag/zoom input is locked and
|
|
148
352
|
`renderHotspots` is hidden. New `activeSceneId` values supersede a target that
|
|
149
353
|
is still being prepared. `maxTextureMemoryMb` and `maxConcurrentTileLoads`
|
|
150
|
-
apply to the whole `
|
|
354
|
+
apply to the whole `Scenes` viewer rather than to each tile scene.
|
|
151
355
|
|
|
152
|
-
##
|
|
356
|
+
## PanoFilter
|
|
153
357
|
|
|
154
|
-
|
|
358
|
+
Render `PanoFilter` inside `PanoViewer` to color-grade or stylize the panorama
|
|
359
|
+
source (`Sphere`, `Tile`, and `PanoVideo`). The filter runs on those materials
|
|
360
|
+
only: 3D hotspots, tooltips, video chrome, and iframe overlays keep their
|
|
361
|
+
original color. WebXR / MobileVR inherit the same look because the panorama
|
|
362
|
+
shader itself is patched.
|
|
363
|
+
|
|
364
|
+
`intensity` mixes the filtered result with the original image (`0`–`1`,
|
|
365
|
+
default `1`). Omit the component, pass `preset="none"`, or set `enabled={false}`
|
|
366
|
+
to leave the source unfiltered.
|
|
155
367
|
|
|
156
368
|
```tsx
|
|
157
|
-
|
|
369
|
+
<PanoViewer style={{ height: 560 }}>
|
|
370
|
+
<PanoFilter preset="pencil" intensity={0.85} />
|
|
371
|
+
<Sphere src="/panoramas/room.webp" />
|
|
372
|
+
<ImageHotspot
|
|
373
|
+
id="door"
|
|
374
|
+
position={{ yaw: 24, pitch: -6 }}
|
|
375
|
+
src="/hotspots/door.webp"
|
|
376
|
+
/>
|
|
377
|
+
</PanoViewer>
|
|
378
|
+
```
|
|
379
|
+
|
|
380
|
+
| Preset | Look |
|
|
381
|
+
|---|---|
|
|
382
|
+
| `none` | Identity (no filter) |
|
|
383
|
+
| `grayscale` | Rec.709 black and white |
|
|
384
|
+
| `sepia` | Warm brown antique print |
|
|
385
|
+
| `vintage` | Faded film with grain |
|
|
386
|
+
| `cool` / `warm` | Blue or amber temperature shift |
|
|
387
|
+
| `pencil` | Graphite sketch on paper |
|
|
388
|
+
| `coloredPencil` | Tinted pencil lines |
|
|
389
|
+
| `crayon` | Posterized wax strokes |
|
|
390
|
+
| `watercolor` | Soft pigment on paper |
|
|
391
|
+
| `cartoon` | Cel shading with outlines |
|
|
392
|
+
| `crosshatch` | Luminance hatching |
|
|
393
|
+
|
|
394
|
+
Built-in artistic looks use screen-space edges and world-space grain so cube
|
|
395
|
+
tiles do not show filter seams. Scene-transition snapshots capture the filtered
|
|
396
|
+
panorama when a filter is active.
|
|
397
|
+
|
|
398
|
+
## AutoRotate
|
|
399
|
+
|
|
400
|
+
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.
|
|
401
|
+
|
|
402
|
+
```tsx
|
|
403
|
+
<PanoViewer style={{ height: 560 }}>
|
|
404
|
+
<AutoRotate enabled speed={12} acceleration={6} startDelay={2_000} />
|
|
405
|
+
<Sphere src="/panoramas/room.webp" />
|
|
406
|
+
</PanoViewer>
|
|
407
|
+
```
|
|
408
|
+
|
|
409
|
+
## Gyro
|
|
410
|
+
|
|
411
|
+
`Gyro` is an opt-in device-orientation control and is disabled by default.
|
|
412
|
+
Render it inside `PanoViewer` and control its `enabled` prop. Relative mode
|
|
413
|
+
(the default) treats the view at activation time as the starting direction.
|
|
414
|
+
Set `absolute` to follow compass-oriented headings and use `north` to identify
|
|
415
|
+
the panorama yaw that points North.
|
|
416
|
+
|
|
417
|
+
```tsx
|
|
418
|
+
import { useRef, useState } from "react";
|
|
158
419
|
import {
|
|
159
|
-
|
|
420
|
+
Gyro,
|
|
421
|
+
PanoViewer,
|
|
160
422
|
Sphere,
|
|
161
|
-
|
|
162
|
-
type PanoViewHandle,
|
|
423
|
+
type GyroHandle,
|
|
163
424
|
} from "@ericchen1990/pano-view";
|
|
164
425
|
|
|
165
|
-
export function
|
|
166
|
-
const
|
|
426
|
+
export function GyroExample() {
|
|
427
|
+
const gyroRef = useRef<GyroHandle>(null);
|
|
428
|
+
const [enabled, setEnabled] = useState(false);
|
|
429
|
+
|
|
430
|
+
const toggle = async () => {
|
|
431
|
+
if (enabled) {
|
|
432
|
+
setEnabled(false);
|
|
433
|
+
return;
|
|
434
|
+
}
|
|
435
|
+
// iOS requires this call to run directly inside a user gesture.
|
|
436
|
+
setEnabled((await gyroRef.current?.requestPermission()) ?? false);
|
|
437
|
+
};
|
|
167
438
|
|
|
168
439
|
return (
|
|
169
440
|
<>
|
|
170
|
-
<button onClick={() =>
|
|
171
|
-
|
|
441
|
+
<button onClick={() => void toggle()}>
|
|
442
|
+
{enabled ? "Disable gyro" : "Enable gyro"}
|
|
172
443
|
</button>
|
|
173
|
-
<
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
onViewChange={(view) => console.log(view)}
|
|
184
|
-
style={{ height: 560 }}
|
|
185
|
-
>
|
|
186
|
-
<AutoRotate enabled speed={18} acceleration={18} startDelay={1_000} />
|
|
444
|
+
<PanoViewer style={{ height: 560 }}>
|
|
445
|
+
<Gyro
|
|
446
|
+
ref={gyroRef}
|
|
447
|
+
enabled={enabled}
|
|
448
|
+
camroll
|
|
449
|
+
friction={0}
|
|
450
|
+
softstart={0.5}
|
|
451
|
+
touchMode="full"
|
|
452
|
+
onDenied={() => setEnabled(false)}
|
|
453
|
+
/>
|
|
187
454
|
<Sphere src="/panoramas/room.webp" />
|
|
188
|
-
</
|
|
455
|
+
</PanoViewer>
|
|
189
456
|
</>
|
|
190
457
|
);
|
|
191
458
|
}
|
|
192
459
|
```
|
|
193
460
|
|
|
194
|
-
|
|
461
|
+
`camroll` (default `true`) levels the camera from device roll. `friction`
|
|
462
|
+
accepts `0` to `0.99`; higher values smooth more strongly but add latency.
|
|
463
|
+
`softstart` is the activation blend duration in seconds (default `0.5`).
|
|
464
|
+
`desktopSupport` defaults to `false` because desktop browsers can expose the
|
|
465
|
+
API without a physical sensor.
|
|
466
|
+
|
|
467
|
+
`touchMode` controls how touch drag combines with device movement:
|
|
468
|
+
|
|
469
|
+
- `off` ignores touch yaw/pitch while leaving pinch FOV available.
|
|
470
|
+
- `horizontaloffset` adds only a horizontal offset.
|
|
471
|
+
- `full` adds horizontal and vertical offsets; the vertical offset returns
|
|
472
|
+
toward the physical orientation as the device moves.
|
|
473
|
+
- `disablegyro` disables the active gyro session on the next touch.
|
|
474
|
+
|
|
475
|
+
The `GyroHandle` exposes `resetSensor(yaw?, pitch?)`, `isAvailable()`,
|
|
476
|
+
`isEnabled()`, and `requestPermission()`. Gyro activity pauses `AutoRotate`
|
|
477
|
+
and counts as viewer interaction for idle tracking. Sensor APIs generally
|
|
478
|
+
require HTTPS. A cross-origin iframe must be permitted by the host page, for
|
|
479
|
+
example with `allow="gyroscope; accelerometer"` and a compatible
|
|
480
|
+
`Permissions-Policy` header.
|
|
481
|
+
|
|
482
|
+
## WebVR
|
|
483
|
+
|
|
484
|
+
`WebVR` adds immersive viewing to the nearest `PanoViewer`. It prefers an
|
|
485
|
+
`immersive-vr` WebXR session on supported headsets. On phones without WebXR it
|
|
486
|
+
uses a stereoscopic split-screen view with device orientation, and on desktop
|
|
487
|
+
it can provide a mouse-driven simulated VR preview. The built-in chrome includes
|
|
488
|
+
Enter VR, Exit VR, and MobileVR setup controls.
|
|
489
|
+
|
|
490
|
+
```tsx
|
|
491
|
+
import { useRef } from "react";
|
|
492
|
+
import {
|
|
493
|
+
PanoViewer,
|
|
494
|
+
Sphere,
|
|
495
|
+
WebVR,
|
|
496
|
+
type WebVRHandle,
|
|
497
|
+
} from "@ericchen1990/pano-view";
|
|
498
|
+
|
|
499
|
+
export function WebVRExample() {
|
|
500
|
+
const webVRRef = useRef<WebVRHandle>(null);
|
|
501
|
+
|
|
502
|
+
return (
|
|
503
|
+
<PanoViewer style={{ height: 560 }}>
|
|
504
|
+
<WebVR
|
|
505
|
+
ref={webVRRef}
|
|
506
|
+
chrome
|
|
507
|
+
fakeSupport
|
|
508
|
+
mobileVr
|
|
509
|
+
onEnterVR={(mode) => console.log("Entered", mode)}
|
|
510
|
+
/>
|
|
511
|
+
<Sphere src="/panoramas/room.webp" />
|
|
512
|
+
</PanoViewer>
|
|
513
|
+
);
|
|
514
|
+
}
|
|
515
|
+
```
|
|
516
|
+
|
|
517
|
+
The handle exposes `enterVR()`, `exitVR()`, `toggleVR()`, `isAvailable()`,
|
|
518
|
+
`isEnabled()`, `getMode()`, and `requestPermission()`. Modes are `"webxr"`,
|
|
519
|
+
`"mobilevr"`, and `"fake"`. `profile` accepts `"none"`, `"cardboard-v1"`,
|
|
520
|
+
`"cardboard-v2"`, `"gear-vr"`, `"daydream"`, or a custom
|
|
521
|
+
`{ fov, ipdMm, k1, k2 }` lens profile. MobileVR setup values are versioned and
|
|
522
|
+
stored in the current origin's `localStorage`. In VR, a center reticle shows a
|
|
523
|
+
crosshair; looking at an interactive hotspot draws a clockwise ring that fills
|
|
524
|
+
over `cursorDwellMs` (default 1500), then the hotspot click handler runs. Set
|
|
525
|
+
`cursorDwellMs={0}` to keep the reticle without auto-click. Simulated desktop
|
|
526
|
+
VR (`fake` mode) requests pointer lock by default (`mousePointerLock`) so the
|
|
527
|
+
cursor hides and mouse movement looks around; click the view again if the
|
|
528
|
+
browser releases the lock. Fallback sessions keep the screen awake
|
|
529
|
+
(`wakelock`) with the Screen Wake Lock API, or a looping hidden video on
|
|
530
|
+
browsers that lack it.
|
|
531
|
+
|
|
532
|
+
WebXR requires HTTPS and a user gesture. Mobile sensor access also requires
|
|
533
|
+
HTTPS and may require an explicit iOS permission prompt. Embedded viewers need
|
|
534
|
+
appropriate `xr-spatial-tracking`, `gyroscope`, and `accelerometer` permissions.
|
|
535
|
+
DOM overlays such as iframe hotspots, tooltips, and video controls are not
|
|
536
|
+
visible inside an immersive headset session; mesh-based hotspots remain
|
|
537
|
+
interactive through the default WebXR controller rays.
|
|
195
538
|
|
|
196
539
|
## Mouse, touch, and keyboard controls
|
|
197
540
|
|
|
198
|
-
`
|
|
541
|
+
`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
542
|
|
|
200
543
|
```tsx
|
|
201
|
-
<
|
|
544
|
+
<PanoViewer
|
|
202
545
|
controls={{
|
|
203
546
|
invert: false,
|
|
204
547
|
bouncingLimits: false,
|
|
@@ -209,36 +552,44 @@ The handle exposes `getView`, `setView`, `reset`, `startAutoRotate`, `stopAutoRo
|
|
|
209
552
|
style={{ height: 560 }}
|
|
210
553
|
>
|
|
211
554
|
<Sphere src="/panoramas/room.webp" />
|
|
212
|
-
</
|
|
555
|
+
</PanoViewer>
|
|
213
556
|
```
|
|
214
557
|
|
|
215
558
|
**Mouse** (pointer types `mouse` / `pen`): drag look and optional wheel zoom. Defaults: `rotateSpeed` `0.35`, `zoomSpeed` `0.08`, `wheel` `true`, `buttons` `["left"]`.
|
|
216
559
|
|
|
217
|
-
|
|
560
|
+
### MouseControls
|
|
561
|
+
|
|
562
|
+
See the defaults above. Override through `controls.mouse` or by rendering `<MouseControls />` as a child.
|
|
563
|
+
|
|
564
|
+
### TouchControls
|
|
565
|
+
|
|
566
|
+
One-finger drag and optional two-finger pinch zoom (`pinchZoom`, default `true`). Override through `controls.touch` or by rendering `<TouchControls />` as a child.
|
|
567
|
+
|
|
568
|
+
### KeyboardControls
|
|
218
569
|
|
|
219
|
-
|
|
570
|
+
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
571
|
|
|
221
572
|
Shared all-mode options on `controls`: `enabled`, `invert` (drag direction for mouse/touch), `bouncingLimits`, `fovSpeed`, `frictionStop` (default `0.01`), plus existing damping / inertia.
|
|
222
573
|
|
|
223
574
|
### Overriding a control channel
|
|
224
575
|
|
|
225
|
-
|
|
576
|
+
Render the component with the props you need. The default instance for that channel is skipped:
|
|
226
577
|
|
|
227
578
|
```tsx
|
|
228
579
|
import { useState } from "react";
|
|
229
580
|
import {
|
|
230
581
|
KeyboardControls,
|
|
231
|
-
|
|
232
|
-
|
|
582
|
+
Scenes,
|
|
583
|
+
PanoViewer,
|
|
233
584
|
cycleSceneId,
|
|
234
|
-
type
|
|
585
|
+
type Scene,
|
|
235
586
|
} from "@ericchen1990/pano-view";
|
|
236
587
|
|
|
237
|
-
export function KeyboardExample({ scenes }: { scenes:
|
|
588
|
+
export function KeyboardExample({ scenes }: { scenes: Scene[] }) {
|
|
238
589
|
const [activeSceneId, setActiveSceneId] = useState(scenes[0]!.id);
|
|
239
590
|
|
|
240
591
|
return (
|
|
241
|
-
<
|
|
592
|
+
<PanoViewer style={{ height: 560 }}>
|
|
242
593
|
<KeyboardControls
|
|
243
594
|
keys={{
|
|
244
595
|
left: ["ArrowLeft", "a"],
|
|
@@ -261,15 +612,15 @@ export function KeyboardExample({ scenes }: { scenes: PanoramaScene[] }) {
|
|
|
261
612
|
if (next) setActiveSceneId(next);
|
|
262
613
|
}}
|
|
263
614
|
/>
|
|
264
|
-
<
|
|
265
|
-
</
|
|
615
|
+
<Scenes scenes={scenes} activeSceneId={activeSceneId} />
|
|
616
|
+
</PanoViewer>
|
|
266
617
|
);
|
|
267
618
|
}
|
|
268
619
|
```
|
|
269
620
|
|
|
270
621
|
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
622
|
|
|
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
|
|
623
|
+
Default bindings: arrows and WASD 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
624
|
|
|
274
625
|
## Panorama coordinate events
|
|
275
626
|
|
|
@@ -279,7 +630,7 @@ convention as the camera: positive yaw looks right and positive pitch looks up.
|
|
|
279
630
|
|
|
280
631
|
```tsx
|
|
281
632
|
import {
|
|
282
|
-
|
|
633
|
+
PanoViewer,
|
|
283
634
|
Sphere,
|
|
284
635
|
panoPositionToVector3,
|
|
285
636
|
type HotspotPosition,
|
|
@@ -292,14 +643,14 @@ export function PlacementExample() {
|
|
|
292
643
|
};
|
|
293
644
|
|
|
294
645
|
return (
|
|
295
|
-
<
|
|
646
|
+
<PanoViewer
|
|
296
647
|
onPanoramaClick={({ position }) => place(position)}
|
|
297
648
|
onPanoramaDoubleClick={({ position }) => console.log("double", position)}
|
|
298
649
|
onPanoramaPointerMove={({ position }) => console.log("move", position)}
|
|
299
650
|
style={{ height: 560 }}
|
|
300
651
|
>
|
|
301
652
|
<Sphere src="/panoramas/room.webp" />
|
|
302
|
-
</
|
|
653
|
+
</PanoViewer>
|
|
303
654
|
);
|
|
304
655
|
}
|
|
305
656
|
```
|
|
@@ -309,23 +660,25 @@ export function PlacementExample() {
|
|
|
309
660
|
is normalized to `[-180, 180)`. Pitch is clamped to `[-90, 90]`; at either
|
|
310
661
|
pole, yaw is normalized to `0` because it does not identify a unique point.
|
|
311
662
|
|
|
312
|
-
##
|
|
663
|
+
## PanoEvents
|
|
313
664
|
|
|
314
665
|
`PanoEvents` is the React counterpart to krpano's global `<events>` element.
|
|
315
|
-
Render one or more instances inside `
|
|
666
|
+
Render one or more instances inside `PanoViewer` to subscribe to viewer-level
|
|
316
667
|
lifecycle and interaction callbacks. Multiple instances coexist (like named
|
|
317
668
|
krpano events); each tracks its own `idleTime`. For composition inside a custom
|
|
318
669
|
child, use `usePanoEvents` instead.
|
|
319
670
|
|
|
671
|
+
### usePanoEvents
|
|
672
|
+
|
|
320
673
|
```tsx
|
|
321
674
|
import {
|
|
322
675
|
AutoRotate,
|
|
323
676
|
PanoEvents,
|
|
324
|
-
|
|
677
|
+
PanoViewer,
|
|
325
678
|
Sphere,
|
|
326
679
|
} from "@ericchen1990/pano-view";
|
|
327
680
|
|
|
328
|
-
<
|
|
681
|
+
<PanoViewer style={{ height: 560 }}>
|
|
329
682
|
<PanoEvents
|
|
330
683
|
idleTime={3000}
|
|
331
684
|
onIdle={() => console.log("idle")}
|
|
@@ -338,7 +691,7 @@ import {
|
|
|
338
691
|
/>
|
|
339
692
|
<AutoRotate enabled />
|
|
340
693
|
<Sphere src="/panoramas/room.webp" />
|
|
341
|
-
</
|
|
694
|
+
</PanoViewer>;
|
|
342
695
|
```
|
|
343
696
|
|
|
344
697
|
Supported callbacks (krpano names in parentheses where applicable):
|
|
@@ -350,42 +703,44 @@ Supported callbacks (krpano names in parentheses where applicable):
|
|
|
350
703
|
- Fullscreen: `onEnterFullscreen` / `onExitFullscreen`
|
|
351
704
|
- `onResize` — canvas content-box size via `ResizeObserver`
|
|
352
705
|
- Auto-rotate: `onAutoRotateStart` / `onAutoRotateStop` / `onAutoRotateOneRound`
|
|
706
|
+
- Gyro: `onGyroAvailable` / `onGyroUnavailable` / `onGyroEnable` /
|
|
707
|
+
`onGyroDisable` / `onGyroDenied`
|
|
353
708
|
|
|
354
|
-
`
|
|
709
|
+
`PanoViewer`'s existing `onViewChange` / `onPanoramaClick` / `onPanoramaDoubleClick` /
|
|
355
710
|
`onPanoramaPointerMove` props remain supported and share the same event bus.
|
|
356
711
|
|
|
357
712
|
Resource loading and scene blending stay on their owners: use `Sphere` /
|
|
358
|
-
`Tile` `onLoad` / `onError` / `onLoadProgress`, and `
|
|
713
|
+
`Tile` `onLoad` / `onError` / `onLoadProgress`, and `Scenes`
|
|
359
714
|
`onTransitionEnd` / `onTransitionError`. This package does not mirror krpano
|
|
360
|
-
xml/VR/
|
|
715
|
+
xml/VR/frame-render events.
|
|
361
716
|
|
|
362
|
-
##
|
|
717
|
+
## PanoContextMenu
|
|
363
718
|
|
|
364
|
-
`
|
|
719
|
+
`PanoViewer` mounts a default context menu that replaces the browser menu on
|
|
365
720
|
right-click. Default items: **Reset view** and **Enter fullscreen** / **Exit
|
|
366
721
|
fullscreen** (label and icon follow the current fullscreen state), with a
|
|
367
722
|
separator between them.
|
|
368
723
|
|
|
369
724
|
```tsx
|
|
370
|
-
<
|
|
725
|
+
<PanoViewer style={{ height: 560 }}>
|
|
371
726
|
<Sphere src="/panoramas/room.webp" />
|
|
372
|
-
</
|
|
727
|
+
</PanoViewer>
|
|
373
728
|
```
|
|
374
729
|
|
|
375
730
|
Disable the default menu (restore the browser menu, or mount your own):
|
|
376
731
|
|
|
377
732
|
```tsx
|
|
378
|
-
<
|
|
733
|
+
<PanoViewer contextMenu={false}>
|
|
379
734
|
<Sphere src="/panoramas/room.webp" />
|
|
380
|
-
</
|
|
735
|
+
</PanoViewer>
|
|
381
736
|
```
|
|
382
737
|
|
|
383
738
|
Tune appearance while keeping the default items:
|
|
384
739
|
|
|
385
740
|
```tsx
|
|
386
|
-
<
|
|
741
|
+
<PanoViewer contextMenu={{ appearance: { opacity: 0.92, borderRadius: 8 } }}>
|
|
387
742
|
<Sphere src="/panoramas/room.webp" />
|
|
388
|
-
</
|
|
743
|
+
</PanoViewer>
|
|
389
744
|
```
|
|
390
745
|
|
|
391
746
|
### Adding items without rebuilding defaults
|
|
@@ -394,7 +749,7 @@ Use `append` or `prepend` to keep the built-in Reset / Fullscreen entries and
|
|
|
394
749
|
only add your own:
|
|
395
750
|
|
|
396
751
|
```tsx
|
|
397
|
-
<
|
|
752
|
+
<PanoViewer
|
|
398
753
|
contextMenu={{
|
|
399
754
|
append: [
|
|
400
755
|
"separator",
|
|
@@ -412,7 +767,7 @@ only add your own:
|
|
|
412
767
|
style={{ height: 560 }}
|
|
413
768
|
>
|
|
414
769
|
<Sphere src="/panoramas/room.webp" />
|
|
415
|
-
</
|
|
770
|
+
</PanoViewer>
|
|
416
771
|
```
|
|
417
772
|
|
|
418
773
|
### Built-in presets
|
|
@@ -425,7 +780,7 @@ reimplementing fullscreen enter/exit state:
|
|
|
425
780
|
- `"separator"` — a horizontal rule
|
|
426
781
|
|
|
427
782
|
```tsx
|
|
428
|
-
<
|
|
783
|
+
<PanoViewer
|
|
429
784
|
contextMenu={{
|
|
430
785
|
items: [
|
|
431
786
|
"resetView",
|
|
@@ -446,7 +801,7 @@ reimplementing fullscreen enter/exit state:
|
|
|
446
801
|
style={{ height: 560 }}
|
|
447
802
|
>
|
|
448
803
|
<Sphere src="/panoramas/room.webp" />
|
|
449
|
-
</
|
|
804
|
+
</PanoViewer>
|
|
450
805
|
```
|
|
451
806
|
|
|
452
807
|
Override presentation on a preset with `{ preset: "fullscreen", label: "…" }`.
|
|
@@ -463,18 +818,30 @@ stay opaque), hover/disabled colors, and icon size. For an explicit translucent
|
|
|
463
818
|
fill without using `opacity`, you can still pass `background: "rgba(...)"`.
|
|
464
819
|
|
|
465
820
|
For a fully custom DOM menu, set `contextMenu={false}` and render
|
|
466
|
-
`PanoContextMenu` as a child
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
claim the right mouse button.
|
|
821
|
+
`PanoContextMenu` as a child. Do not mount both the default menu and a child
|
|
822
|
+
`PanoContextMenu` at once. Avoid combining the context menu with
|
|
823
|
+
`controls.mouse.buttons: ["right"]` — both claim the right mouse button.
|
|
470
824
|
|
|
471
825
|
## Shared hotspot contract and saved definitions
|
|
472
826
|
|
|
473
|
-
Every hotspot has an `id`, optional `visible`, `interactive`, `
|
|
474
|
-
and semantic interaction callbacks. Point hotspots additionally
|
|
475
|
-
controlled `position`, angular `width`/`height`, hotspot mode, and the
|
|
476
|
-
options described below. `interactive={false}` keeps a hotspot visible
|
|
477
|
-
letting a drawing tool receive the panorama pointer events beneath it.
|
|
827
|
+
Every hotspot has an `id`, optional `visible`, `interactive`, `pointerEvents`,
|
|
828
|
+
`renderOrder`, and semantic interaction callbacks. Point hotspots additionally
|
|
829
|
+
use a controlled `position`, angular `width`/`height`, hotspot mode, and the
|
|
830
|
+
zoom options described below. `interactive={false}` keeps a hotspot visible
|
|
831
|
+
while letting a drawing tool receive the panorama pointer events beneath it.
|
|
832
|
+
`pointerEvents="none"` ignores mouse, touch, and pen so those events pass
|
|
833
|
+
through to the panorama even when the hotspot stays keyboard-interactive.
|
|
834
|
+
Interactive hotspots set the canvas cursor to `pointer` while hovered, or to
|
|
835
|
+
the hotspot's `cursor` prop when that is set.
|
|
836
|
+
|
|
837
|
+
```tsx
|
|
838
|
+
<TextHotspot
|
|
839
|
+
id="caption"
|
|
840
|
+
pointerEvents="none"
|
|
841
|
+
position={{ yaw: 12, pitch: -8 }}
|
|
842
|
+
text="Decorative label"
|
|
843
|
+
/>
|
|
844
|
+
```
|
|
478
845
|
|
|
479
846
|
`onClick` and `onHoverChange` receive a position and input source (`"pointer"`
|
|
480
847
|
or `"keyboard"`). Point hotspots can use `draggable`, `onDragStart`,
|
|
@@ -482,9 +849,59 @@ or `"keyboard"`). Point hotspots can use `draggable`, `onDragStart`,
|
|
|
482
849
|
back to its state. Polygon and polyline hotspots report the corresponding
|
|
483
850
|
controlled `vertices` through `onVerticesChange`.
|
|
484
851
|
|
|
852
|
+
Every hotspot can show a DOM tooltip at its anchor. Pass a string or
|
|
853
|
+
`{ text?, image?, imageAlt? }` as `tooltip`. The default `tooltipTrigger` is
|
|
854
|
+
`"always"`: if content is present and the hotspot is visible, the bubble stays
|
|
855
|
+
on screen. Clicking the hotspot does not toggle it. Optional
|
|
856
|
+
`tooltipTrigger="hover"` shows the bubble on pointer hover or keyboard focus;
|
|
857
|
+
`"click"` pins it until the hotspot is clicked again or the empty panorama is
|
|
858
|
+
clicked. Hover and click triggers need `interactive` and `pointerEvents`
|
|
859
|
+
other than `"none"`.
|
|
860
|
+
|
|
861
|
+
`tooltipPlacement` is `"top"` (default), `"bottom"`, `"left"`, or `"right"`.
|
|
862
|
+
These are screen directions: the bubble sits outside the hotspot's projected
|
|
863
|
+
screen bounds on that edge, so rotated graphics such as arrows are not covered.
|
|
864
|
+
`tooltipOffset` is the screen-space gap in CSS pixels (default `12` /
|
|
865
|
+
`DEFAULT_HOTSPOT_TOOLTIP_OFFSET`).
|
|
866
|
+
|
|
867
|
+
Optional `tooltipAppearance` overrides the bubble paint. Unset fields keep the
|
|
868
|
+
library default theme (`DEFAULT_HOTSPOT_TOOLTIP_APPEARANCE`): background, text
|
|
869
|
+
color, border, corner radius, shadow, padding, and font size.
|
|
870
|
+
|
|
871
|
+
```tsx
|
|
872
|
+
<ImageHotspot
|
|
873
|
+
id="gallery"
|
|
874
|
+
ariaLabel="Open gallery"
|
|
875
|
+
position={{ yaw: 28, pitch: -4 }}
|
|
876
|
+
src="/hotspots/gallery.webp"
|
|
877
|
+
tooltip={{ text: "Courtyard gallery", image: "/hotspots/gallery-thumb.webp" }}
|
|
878
|
+
tooltipPlacement="top"
|
|
879
|
+
tooltipOffset={16}
|
|
880
|
+
tooltipAppearance={{
|
|
881
|
+
background: "rgba(12, 28, 36, 0.88)",
|
|
882
|
+
color: "#e7f2f5",
|
|
883
|
+
border: "1px solid rgba(56, 84, 91, 0.9)",
|
|
884
|
+
borderRadius: 10,
|
|
885
|
+
shadow: "0 10px 28px rgba(0, 0, 0, 0.42)",
|
|
886
|
+
padding: 10,
|
|
887
|
+
fontSize: 13,
|
|
888
|
+
}}
|
|
889
|
+
/>
|
|
890
|
+
<GraphicHotspot
|
|
891
|
+
id="marker"
|
|
892
|
+
ariaLabel="Signal"
|
|
893
|
+
position={{ yaw: -18, pitch: 9 }}
|
|
894
|
+
graphic={{ kind: "ring" }}
|
|
895
|
+
tooltip="Hover for details"
|
|
896
|
+
tooltipTrigger="hover"
|
|
897
|
+
tooltipPlacement="right"
|
|
898
|
+
/>
|
|
899
|
+
```
|
|
900
|
+
|
|
485
901
|
For persistence or a host-owned editor, use the exported discriminated
|
|
486
|
-
`HotspotDefinition` union.
|
|
487
|
-
|
|
902
|
+
`HotspotDefinition` union. Adding a variant is a TypeScript breaking change for
|
|
903
|
+
exhaustive switches; handle `text` and `iframe` alongside the existing point
|
|
904
|
+
and path categories:
|
|
488
905
|
|
|
489
906
|
```ts
|
|
490
907
|
import type { HotspotDefinition } from "@ericchen1990/pano-view";
|
|
@@ -494,6 +911,8 @@ const hotspots: HotspotDefinition[] = [
|
|
|
494
911
|
{ type: "graphic", id: "marker", position: { yaw: -18, pitch: 9 }, graphic: { kind: "ring" } },
|
|
495
912
|
{ type: "sequence", id: "pulse", position: { yaw: -42, pitch: -7 }, src: "/hotspots/pulse.png", frameCount: 20 },
|
|
496
913
|
{ type: "video", id: "clip", position: { yaw: 48, pitch: 6 }, src: "/hotspots/clip.webm" },
|
|
914
|
+
{ type: "text", id: "caption", position: { yaw: 0, pitch: -16 }, text: "Courtyard overlook" },
|
|
915
|
+
{ type: "iframe", id: "guide", position: { yaw: -62, pitch: 4 }, src: "/hotspots/embed.html" },
|
|
497
916
|
{ type: "polygon", id: "zone", vertices: [{ yaw: 12, pitch: 4 }, { yaw: 22, pitch: 4 }, { yaw: 18, pitch: 14 }] },
|
|
498
917
|
{ type: "polyline", id: "route", vertices: [{ yaw: -8, pitch: 1 }, { yaw: 4, pitch: 8 }] },
|
|
499
918
|
];
|
|
@@ -503,15 +922,15 @@ Definitions intentionally contain data only. Render them with a switch in the
|
|
|
503
922
|
host, where application-specific click actions, controlled media state, and
|
|
504
923
|
error reporting belong.
|
|
505
924
|
|
|
506
|
-
##
|
|
925
|
+
## ImageHotspot
|
|
507
926
|
|
|
508
927
|
`ImageHotspot` renders an image at a controlled spherical position. Dimensions
|
|
509
928
|
are angular degrees, so they remain independent of the canvas resolution.
|
|
510
929
|
|
|
511
930
|
```tsx
|
|
512
|
-
import { ImageHotspot,
|
|
931
|
+
import { ImageHotspot, PanoViewer, Sphere } from "@ericchen1990/pano-view";
|
|
513
932
|
|
|
514
|
-
<
|
|
933
|
+
<PanoViewer style={{ height: 560 }}>
|
|
515
934
|
<Sphere src="/panoramas/room.webp" />
|
|
516
935
|
<ImageHotspot
|
|
517
936
|
id="gallery"
|
|
@@ -523,9 +942,18 @@ import { ImageHotspot, PanoView, Sphere } from "@ericchen1990/pano-view";
|
|
|
523
942
|
src="/hotspots/gallery.webp"
|
|
524
943
|
onClick={({ position }) => console.log("gallery", position)}
|
|
525
944
|
/>
|
|
526
|
-
</
|
|
945
|
+
</PanoViewer>;
|
|
527
946
|
```
|
|
528
947
|
|
|
948
|
+
Set `draggable` and update the controlled position from `onPositionChange` to
|
|
949
|
+
move a selected hotspot. Clickable hotspots need `ariaLabel`: PanoViewer creates
|
|
950
|
+
an internal semantic control for Tab, Enter, and Space activation, with a
|
|
951
|
+
visible WebGL focus outline.
|
|
952
|
+
|
|
953
|
+
`ImageHotspot` calls `onLoad(texture)` and `onError(error)`.
|
|
954
|
+
|
|
955
|
+
## GraphicHotspot
|
|
956
|
+
|
|
529
957
|
`GraphicHotspot` accepts built-in `circle`, `triangle`, `diamond`, `star`,
|
|
530
958
|
`arrow`, `rectangle`, and `ring` graphics, an SVG URL, or safe SVG path data
|
|
531
959
|
with an explicit viewBox. Built-in graphics support `fill`, `stroke`, and
|
|
@@ -577,13 +1005,8 @@ property to orient them in another direction.
|
|
|
577
1005
|
square corners and `0.5` produces the maximum rounded corners. This replaces
|
|
578
1006
|
the previous Canvas-texture pixel interpretation.
|
|
579
1007
|
|
|
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
|
|
1008
|
+
`GraphicHotspot` uses the same `onLoad(texture)` and `onError(error)` callbacks
|
|
1009
|
+
as `ImageHotspot`. Built-in paths are rasterized locally; only an SVG URL
|
|
587
1010
|
or SVG `path` data plus an explicit `viewBox` is accepted, never arbitrary SVG
|
|
588
1011
|
markup.
|
|
589
1012
|
|
|
@@ -621,7 +1044,7 @@ placement properties:
|
|
|
621
1044
|
/>
|
|
622
1045
|
```
|
|
623
1046
|
|
|
624
|
-
##
|
|
1047
|
+
## PolygonHotspot
|
|
625
1048
|
|
|
626
1049
|
`PolygonHotspot` renders a local spherical area from three or more controlled
|
|
627
1050
|
yaw/pitch vertices. Concave polygons and the `-180°/180°` seam are supported.
|
|
@@ -645,6 +1068,8 @@ import { PolygonHotspot } from "@ericchen1990/pano-view";
|
|
|
645
1068
|
stroke="#f5fbfc"
|
|
646
1069
|
strokeWidth={2}
|
|
647
1070
|
strokeOpacity={0.8}
|
|
1071
|
+
strokeDashSize={8}
|
|
1072
|
+
strokeGapSize={4}
|
|
648
1073
|
draggable
|
|
649
1074
|
onVerticesChange={({ vertices }) => setZoneVertices(vertices)}
|
|
650
1075
|
onInvalid={(issues) => console.error(issues)}
|
|
@@ -652,18 +1077,21 @@ import { PolygonHotspot } from "@ericchen1990/pano-view";
|
|
|
652
1077
|
```
|
|
653
1078
|
|
|
654
1079
|
`strokeWidth` is a screen-space CSS pixel value, so it remains visually
|
|
655
|
-
consistent across FOV changes.
|
|
656
|
-
|
|
657
|
-
|
|
1080
|
+
consistent across FOV changes. `strokeDashSize` and `strokeGapSize` use the
|
|
1081
|
+
same CSS-pixel units; omit them or set `strokeDashSize` to `0` for a solid
|
|
1082
|
+
stroke. Dragging moves all vertices together while keeping yaw normalized and
|
|
1083
|
+
pitch clamped. Individual vertex editing is provided by the authoring
|
|
1084
|
+
workflow, not the runtime hotspot component.
|
|
658
1085
|
|
|
659
1086
|
Set `fillOpacity={0}` for a closed outline-only polygon. The fill and outline
|
|
660
1087
|
share the same spherical edge sampling, so there is no intentional seam
|
|
661
1088
|
between them.
|
|
662
1089
|
|
|
663
|
-
##
|
|
1090
|
+
## PolylineHotspot
|
|
664
1091
|
|
|
665
1092
|
`PolylineHotspot` is an open path of at least two yaw/pitch vertices. It uses
|
|
666
|
-
the same CSS-pixel `strokeWidth`,
|
|
1093
|
+
the same CSS-pixel `strokeWidth`, optional dashed stroke (`strokeDashSize` /
|
|
1094
|
+
`strokeGapSize`), visibility, semantic interaction, controlled
|
|
667
1095
|
whole-path drag, and vertex-change callback as `PolygonHotspot`, but it never
|
|
668
1096
|
connects the final point to the first and has no fill.
|
|
669
1097
|
|
|
@@ -677,6 +1105,8 @@ import { PolylineHotspot } from "@ericchen1990/pano-view";
|
|
|
677
1105
|
stroke="#75cbd3"
|
|
678
1106
|
strokeWidth={2}
|
|
679
1107
|
strokeOpacity={0.9}
|
|
1108
|
+
strokeDashSize={8}
|
|
1109
|
+
strokeGapSize={4}
|
|
680
1110
|
draggable
|
|
681
1111
|
onVerticesChange={({ vertices }) => setRouteVertices(vertices)}
|
|
682
1112
|
/>;
|
|
@@ -684,7 +1114,7 @@ import { PolylineHotspot } from "@ericchen1990/pano-view";
|
|
|
684
1114
|
|
|
685
1115
|
`onInvalid` reports the only invalid runtime shape: fewer than two vertices.
|
|
686
1116
|
|
|
687
|
-
##
|
|
1117
|
+
## SequenceHotspot
|
|
688
1118
|
|
|
689
1119
|
`SequenceHotspot` animates a sprite sheet: one image containing equally sized
|
|
690
1120
|
frames in a vertical or horizontal strip. This matches the common krpano
|
|
@@ -712,6 +1142,12 @@ import { SequenceHotspot } from "@ericchen1990/pano-view";
|
|
|
712
1142
|
/>;
|
|
713
1143
|
```
|
|
714
1144
|
|
|
1145
|
+
Sequence loading uses `onLoadProgress` and `onError`. The component does not
|
|
1146
|
+
change `playing` by itself: update that prop in response to your UI, click
|
|
1147
|
+
handler, `onEnded`, or playback error.
|
|
1148
|
+
|
|
1149
|
+
## VideoHotspot
|
|
1150
|
+
|
|
715
1151
|
`VideoHotspot` uses an `HTMLVideoElement` and `VideoTexture`. Its `playing`
|
|
716
1152
|
prop is likewise controlled: toggle it from your click handler or application
|
|
717
1153
|
state. On source change or unmount, the element is paused and its texture is
|
|
@@ -745,24 +1181,82 @@ Browsers can reject unmuted or otherwise non-gesture playback. In that case
|
|
|
745
1181
|
the browser error; retain control of `playing` in the host and offer an
|
|
746
1182
|
explicit user action.
|
|
747
1183
|
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
playback error. This keeps source changes, unmounts, and React StrictMode
|
|
752
|
-
lifecycles deterministic.
|
|
1184
|
+
Video reports media and poster failures through `onError`. Like
|
|
1185
|
+
`SequenceHotspot`, it does not change `playing` by itself. This keeps source
|
|
1186
|
+
changes, unmounts, and React StrictMode lifecycles deterministic.
|
|
753
1187
|
|
|
754
|
-
##
|
|
1188
|
+
## TextHotspot
|
|
755
1189
|
|
|
756
|
-
|
|
1190
|
+
`TextHotspot` rasterizes plain text onto a canvas texture. It does not accept
|
|
1191
|
+
HTML or Markdown. `\n` is a hard line break; `whiteSpace="normal"` (default)
|
|
1192
|
+
wraps words to the hotspot's angular width. Dimensions stay in degrees like
|
|
1193
|
+
the other point hotspots. Typography uses the same names as CSS
|
|
1194
|
+
(`fontFamily`, `fontSize`, `fontWeight`, `fontStyle`) but is painted onto the
|
|
1195
|
+
canvas, not applied as DOM style. `fontSize` is canvas pixels (default 96) and
|
|
1196
|
+
does not scale with panel height. `fontWeight` defaults to `600` and
|
|
1197
|
+
`fontStyle` to `"normal"`. `padding` and `borderRadius` remain fractions of
|
|
1198
|
+
the texture's shorter side, so they still follow `width` / `height`.
|
|
757
1199
|
|
|
758
1200
|
```tsx
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
1201
|
+
import { TextHotspot } from "@ericchen1990/pano-view";
|
|
1202
|
+
|
|
1203
|
+
<TextHotspot
|
|
1204
|
+
id="caption"
|
|
1205
|
+
ariaLabel="Courtyard overlook"
|
|
1206
|
+
position={{ yaw: 0, pitch: -16 }}
|
|
1207
|
+
width={16}
|
|
1208
|
+
height={6}
|
|
1209
|
+
mode="billboard"
|
|
1210
|
+
scaleMode="fixed"
|
|
1211
|
+
text={"Courtyard overlook\nNorth terrace"}
|
|
1212
|
+
fontFamily="system-ui, sans-serif"
|
|
1213
|
+
fontSize={96}
|
|
1214
|
+
fontWeight={600}
|
|
1215
|
+
fontStyle="normal"
|
|
1216
|
+
align="center"
|
|
1217
|
+
verticalAlign="middle"
|
|
1218
|
+
color="#f8fafc"
|
|
1219
|
+
background="#111827"
|
|
1220
|
+
backgroundOpacity={0.72}
|
|
1221
|
+
/>;
|
|
763
1222
|
```
|
|
764
1223
|
|
|
765
|
-
|
|
1224
|
+
Load a custom `fontFamily` on the page before the hotspot paints. The hotspot
|
|
1225
|
+
waits on `document.fonts.load` when that API exists, then draws with whatever
|
|
1226
|
+
faces the document has. `onLoad(texture)` and `onError(error)` match
|
|
1227
|
+
`ImageHotspot`.
|
|
1228
|
+
|
|
1229
|
+
## IframeHotspot
|
|
1230
|
+
|
|
1231
|
+
`IframeHotspot` places an embedded document with `@react-three/drei` `Html`
|
|
1232
|
+
`transform`, so it follows the same hotspot mode, rotation, and zoom scaling
|
|
1233
|
+
as a texture plane. The iframe is a DOM overlay and is not occluded by the
|
|
1234
|
+
panorama shell. There is no `srcdoc` prop.
|
|
1235
|
+
|
|
1236
|
+
```tsx
|
|
1237
|
+
import { IframeHotspot } from "@ericchen1990/pano-view";
|
|
1238
|
+
|
|
1239
|
+
<IframeHotspot
|
|
1240
|
+
id="guide"
|
|
1241
|
+
ariaLabel="Open visitor guide"
|
|
1242
|
+
position={{ yaw: -62, pitch: 4 }}
|
|
1243
|
+
width={18}
|
|
1244
|
+
height={12}
|
|
1245
|
+
mode="billboard"
|
|
1246
|
+
src="/hotspots/embed.html"
|
|
1247
|
+
pointerPolicy="hotspot"
|
|
1248
|
+
/>;
|
|
1249
|
+
```
|
|
1250
|
+
|
|
1251
|
+
`pointerPolicy="hotspot"` (default) sets the iframe to `pointer-events: none`
|
|
1252
|
+
so clicks and drags hit the WebGL plane. `"content"` makes the page
|
|
1253
|
+
interactive and blocks dragging through the iframe. A hotspot
|
|
1254
|
+
`pointerEvents="none"` also forces the iframe overlay not to capture pointer
|
|
1255
|
+
events. `sandbox` defaults to
|
|
1256
|
+
`allow-scripts allow-popups allow-forms` without `allow-same-origin`.
|
|
1257
|
+
`referrerPolicy` defaults to `strict-origin-when-cross-origin`. Prefer a
|
|
1258
|
+
same-origin document; many third-party sites send `X-Frame-Options` and will
|
|
1259
|
+
not load.
|
|
766
1260
|
|
|
767
1261
|
## Next.js and SSR
|
|
768
1262
|
|