@ericchen1990/pano-view 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +479 -0
- package/dist/index.cjs +4001 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.mts +519 -0
- package/dist/index.d.ts +519 -0
- package/dist/index.mjs +4092 -0
- package/dist/index.mjs.map +1 -0
- package/package.json +61 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Eric Chen
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,479 @@
|
|
|
1
|
+
# @ericchen1990/pano-view
|
|
2
|
+
|
|
3
|
+
Composable React components for equirectangular and krpano-style multiresolution panorama viewers.
|
|
4
|
+
|
|
5
|
+
## Install
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npm install @ericchen1990/pano-view react react-dom three @react-three/fiber
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
React 19, React DOM 19, `@react-three/fiber` 9, and Three.js are peer dependencies.
|
|
12
|
+
|
|
13
|
+
## Sphere panorama
|
|
14
|
+
|
|
15
|
+
`PanoView` owns the canvas, perspective camera, and controls. Give its container an explicit size and place one panorama source inside it.
|
|
16
|
+
|
|
17
|
+
```tsx
|
|
18
|
+
"use client";
|
|
19
|
+
|
|
20
|
+
import { PanoView, Sphere } from "@pano-view/react";
|
|
21
|
+
|
|
22
|
+
export function SphereExample() {
|
|
23
|
+
return (
|
|
24
|
+
<PanoView style={{ width: "100%", height: 560 }}>
|
|
25
|
+
<Sphere src="/panoramas/room.webp" />
|
|
26
|
+
</PanoView>
|
|
27
|
+
);
|
|
28
|
+
}
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
`Sphere` expects a 2:1 equirectangular image. Use `yawOffset` when the source's forward direction needs horizontal adjustment.
|
|
32
|
+
|
|
33
|
+
## Cube Tile panorama
|
|
34
|
+
|
|
35
|
+
`Tile` renders six inward-facing cube faces and loads only tiles around the current view. The default layout matches krpano-style output:
|
|
36
|
+
|
|
37
|
+
```text
|
|
38
|
+
tiles/{face}/l{level}/{row}/l{level}_{face}_{col}_{row}.webp
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
Faces are `f`, `r`, `b`, `l`, `u`, and `d`; rows and columns are 1-based.
|
|
42
|
+
|
|
43
|
+
```tsx
|
|
44
|
+
import { PanoView, Tile } from "@pano-view/react";
|
|
45
|
+
|
|
46
|
+
export function TileExample() {
|
|
47
|
+
return (
|
|
48
|
+
<PanoView style={{ width: "100%", height: 560 }}>
|
|
49
|
+
<Tile
|
|
50
|
+
baseUrl="https://cdn.example.com/panoramas/room"
|
|
51
|
+
multires="512,500,1000,2000"
|
|
52
|
+
/>
|
|
53
|
+
</PanoView>
|
|
54
|
+
);
|
|
55
|
+
}
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
The first `multires` value is the tile size. Remaining values are ascending cube-face sizes for `l1`, `l2`, and later levels. The default preview is `${baseUrl}/previews/cube-vertical.webp`, with faces stacked as `f/r/b/l/u/d`.
|
|
59
|
+
|
|
60
|
+
During rapid rotation or zoom, loaded tiles remain visible while newly visible tiles use their parent level or the preview as a local fallback.
|
|
61
|
+
|
|
62
|
+
Override storage conventions with either a krpano placeholder template or a resolver. Both return paths relative to `baseUrl`:
|
|
63
|
+
|
|
64
|
+
```tsx
|
|
65
|
+
<Tile
|
|
66
|
+
baseUrl="/panoramas/room"
|
|
67
|
+
multires={{ tileSize: 512, levels: [500, 1000, 2000] }}
|
|
68
|
+
urlTemplate="assets/%s/%l/%v_%h.webp"
|
|
69
|
+
resolveTileUrl={({ face, level, row, col }) =>
|
|
70
|
+
`api/tile/${face}/${level}/${row}/${col}`
|
|
71
|
+
}
|
|
72
|
+
/>
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
`resolveTileUrl` takes precedence over `urlTemplate`.
|
|
76
|
+
|
|
77
|
+
For krpano cube-tile templates, `%s` is the cube face and `%l` is the multires
|
|
78
|
+
level. Horizontal tile indices support the equivalent `%h`, `%x`, `%u`, and
|
|
79
|
+
`%c` placeholders; vertical tile indices support `%v`, `%y`, `%w`, and `%r`.
|
|
80
|
+
Prefix an index placeholder with zeroes for padding: `%0h` produces a two-digit
|
|
81
|
+
horizontal index and `%00v` produces a three-digit vertical index. The stereo
|
|
82
|
+
`%t` and frame `%f` placeholders are not applicable to `Tile`, which represents
|
|
83
|
+
a single non-stereo cube panorama.
|
|
84
|
+
|
|
85
|
+
## Scene transitions
|
|
86
|
+
|
|
87
|
+
`PanoramaScenes` switches controlled sphere and cube-tile scenes with GPU-only
|
|
88
|
+
snapshot blending. The target scene loads its sphere image or tile preview first;
|
|
89
|
+
once ready, the current framebuffer becomes a temporary GPU texture, its source
|
|
90
|
+
textures are released, and the target scene blends in. This avoids holding two
|
|
91
|
+
high-resolution tile scenes in WebGL memory at once.
|
|
92
|
+
|
|
93
|
+
```tsx
|
|
94
|
+
import {
|
|
95
|
+
PanoramaScenes,
|
|
96
|
+
PanoView,
|
|
97
|
+
type PanoramaScene,
|
|
98
|
+
} from "@pano-view/react";
|
|
99
|
+
|
|
100
|
+
const scenes: PanoramaScene[] = [
|
|
101
|
+
{ id: "lobby", type: "sphere", src: "/panoramas/lobby.webp" },
|
|
102
|
+
{
|
|
103
|
+
id: "terrace",
|
|
104
|
+
type: "tile",
|
|
105
|
+
baseUrl: "/panoramas/terrace",
|
|
106
|
+
multires: "512,1000,2000",
|
|
107
|
+
},
|
|
108
|
+
];
|
|
109
|
+
|
|
110
|
+
<PanoView style={{ height: 560 }}>
|
|
111
|
+
<PanoramaScenes
|
|
112
|
+
scenes={scenes}
|
|
113
|
+
activeSceneId={activeSceneId}
|
|
114
|
+
transition="ellipticZoomOpen"
|
|
115
|
+
renderHotspots={(scene) => <SceneHotspots sceneId={scene.id} />}
|
|
116
|
+
onTransitionError={({ sceneId }) => console.warn("Could not load", sceneId)}
|
|
117
|
+
/>
|
|
118
|
+
</PanoView>;
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
Available presets are `none`, `crossfade`, `zoom`, `blackout`, `whiteFlash`,
|
|
122
|
+
`slideRightToLeft`, `slideTopToBottom`, `slideDiagonal`, `circleOpen`,
|
|
123
|
+
`verticalOpen`, `horizontalOpen`, and `ellipticZoomOpen`. Pass
|
|
124
|
+
`{ preset: "crossfade", duration: 0.6 }` to override a preset duration.
|
|
125
|
+
|
|
126
|
+
While a transition runs, panorama drag/zoom input is locked and
|
|
127
|
+
`renderHotspots` is hidden. New `activeSceneId` values supersede a target that
|
|
128
|
+
is still being prepared. `maxTextureMemoryMb` and `maxConcurrentTileLoads`
|
|
129
|
+
apply to the whole `PanoramaScenes` viewer rather than to each tile scene.
|
|
130
|
+
|
|
131
|
+
## Controls and imperative API
|
|
132
|
+
|
|
133
|
+
Angles are public degrees. Positive yaw looks right and positive pitch looks up.
|
|
134
|
+
|
|
135
|
+
```tsx
|
|
136
|
+
import { useRef } from "react";
|
|
137
|
+
import {
|
|
138
|
+
PanoView,
|
|
139
|
+
Sphere,
|
|
140
|
+
AutoRotate,
|
|
141
|
+
type PanoViewHandle,
|
|
142
|
+
} from "@pano-view/react";
|
|
143
|
+
|
|
144
|
+
export function ControlledExample() {
|
|
145
|
+
const ref = useRef<PanoViewHandle>(null);
|
|
146
|
+
|
|
147
|
+
return (
|
|
148
|
+
<>
|
|
149
|
+
<button onClick={() => ref.current?.setView({ yaw: 90, fov: 55 })}>
|
|
150
|
+
Look right
|
|
151
|
+
</button>
|
|
152
|
+
<PanoView
|
|
153
|
+
ref={ref}
|
|
154
|
+
controls={{
|
|
155
|
+
inertia: true,
|
|
156
|
+
rotateDamping: 14,
|
|
157
|
+
zoomDamping: 16,
|
|
158
|
+
}}
|
|
159
|
+
initialView={{ yaw: 0, pitch: 0, fov: 75 }}
|
|
160
|
+
minFov={30}
|
|
161
|
+
maxFov={100}
|
|
162
|
+
onViewChange={(view) => console.log(view)}
|
|
163
|
+
style={{ height: 560 }}
|
|
164
|
+
>
|
|
165
|
+
<AutoRotate enabled speed={18} acceleration={18} startDelay={1_000} />
|
|
166
|
+
<Sphere src="/panoramas/room.webp" />
|
|
167
|
+
</PanoView>
|
|
168
|
+
</>
|
|
169
|
+
);
|
|
170
|
+
}
|
|
171
|
+
```
|
|
172
|
+
|
|
173
|
+
The handle exposes `getView`, `setView`, `reset`, `startAutoRotate`, `stopAutoRotate`, and `toggleFullscreen`. Built-in input supports pointer drag, touch drag, pinch, wheel, arrow keys, `+/-`, and `0` to reset. The two auto-rotation handle methods remain supported for compatibility; prefer rendering `AutoRotate` for new code.
|
|
174
|
+
|
|
175
|
+
## Panorama coordinate events
|
|
176
|
+
|
|
177
|
+
Use the panorama pointer callbacks when an authoring UI needs the spherical
|
|
178
|
+
position under the cursor. Event positions use the same public degree
|
|
179
|
+
convention as the camera: positive yaw looks right and positive pitch looks up.
|
|
180
|
+
|
|
181
|
+
```tsx
|
|
182
|
+
import {
|
|
183
|
+
PanoView,
|
|
184
|
+
Sphere,
|
|
185
|
+
panoPositionToVector3,
|
|
186
|
+
type HotspotPosition,
|
|
187
|
+
} from "@pano-view/react";
|
|
188
|
+
|
|
189
|
+
export function PlacementExample() {
|
|
190
|
+
const place = (position: HotspotPosition) => {
|
|
191
|
+
const worldPosition = panoPositionToVector3(position, 10);
|
|
192
|
+
console.log(position, worldPosition);
|
|
193
|
+
};
|
|
194
|
+
|
|
195
|
+
return (
|
|
196
|
+
<PanoView
|
|
197
|
+
onPanoramaClick={({ position }) => place(position)}
|
|
198
|
+
onPanoramaDoubleClick={({ position }) => console.log("double", position)}
|
|
199
|
+
onPanoramaPointerMove={({ position }) => console.log("move", position)}
|
|
200
|
+
style={{ height: 560 }}
|
|
201
|
+
>
|
|
202
|
+
<Sphere src="/panoramas/room.webp" />
|
|
203
|
+
</PanoView>
|
|
204
|
+
);
|
|
205
|
+
}
|
|
206
|
+
```
|
|
207
|
+
|
|
208
|
+
`normalizePanoPosition`, `normalizePanoYaw`, `clampPanoPitch`, and
|
|
209
|
+
`vector3ToPanoPosition` are also exported for controlled authoring flows. Yaw
|
|
210
|
+
is normalized to `[-180, 180)`. Pitch is clamped to `[-89.9, 89.9]` to avoid
|
|
211
|
+
the spherical-coordinate singularity at the poles.
|
|
212
|
+
|
|
213
|
+
## Shared hotspot contract and saved definitions
|
|
214
|
+
|
|
215
|
+
Every hotspot has an `id`, optional `visible`, `interactive`, `renderOrder`,
|
|
216
|
+
and semantic interaction callbacks. Point hotspots additionally use a
|
|
217
|
+
controlled `position`, angular `width`/`height`, hotspot mode, and the zoom
|
|
218
|
+
options described below. `interactive={false}` keeps a hotspot visible while
|
|
219
|
+
letting a drawing tool receive the panorama pointer events beneath it.
|
|
220
|
+
|
|
221
|
+
`onClick` and `onHoverChange` receive a position and input source (`"pointer"`
|
|
222
|
+
or `"keyboard"`). Point hotspots can use `draggable`, `onDragStart`,
|
|
223
|
+
`onPositionChange`, and `onDragEnd`; the host must write the reported position
|
|
224
|
+
back to its state. Polygon and polyline hotspots report the corresponding
|
|
225
|
+
controlled `vertices` through `onVerticesChange`.
|
|
226
|
+
|
|
227
|
+
For persistence or a host-owned editor, use the exported discriminated
|
|
228
|
+
`HotspotDefinition` union. It contains the original five categories plus the
|
|
229
|
+
open `polyline` extension:
|
|
230
|
+
|
|
231
|
+
```ts
|
|
232
|
+
import type { HotspotDefinition } from "@pano-view/react";
|
|
233
|
+
|
|
234
|
+
const hotspots: HotspotDefinition[] = [
|
|
235
|
+
{ type: "image", id: "gallery", position: { yaw: 24, pitch: -5 }, src: "/hotspots/card.webp" },
|
|
236
|
+
{ type: "graphic", id: "marker", position: { yaw: -18, pitch: 9 }, graphic: { kind: "ring" } },
|
|
237
|
+
{ type: "sequence", id: "pulse", position: { yaw: -42, pitch: -7 }, src: "/hotspots/pulse.png", frameCount: 20 },
|
|
238
|
+
{ type: "video", id: "clip", position: { yaw: 48, pitch: 6 }, src: "/hotspots/clip.webm" },
|
|
239
|
+
{ type: "polygon", id: "zone", vertices: [{ yaw: 12, pitch: 4 }, { yaw: 22, pitch: 4 }, { yaw: 18, pitch: 14 }] },
|
|
240
|
+
{ type: "polyline", id: "route", vertices: [{ yaw: -8, pitch: 1 }, { yaw: 4, pitch: 8 }] },
|
|
241
|
+
];
|
|
242
|
+
```
|
|
243
|
+
|
|
244
|
+
Definitions intentionally contain data only. Render them with a switch in the
|
|
245
|
+
host, where application-specific click actions, controlled media state, and
|
|
246
|
+
error reporting belong.
|
|
247
|
+
|
|
248
|
+
## Image and graphic hotspots
|
|
249
|
+
|
|
250
|
+
`ImageHotspot` renders an image at a controlled spherical position. Dimensions
|
|
251
|
+
are angular degrees, so they remain independent of the canvas resolution.
|
|
252
|
+
|
|
253
|
+
```tsx
|
|
254
|
+
import { ImageHotspot, PanoView, Sphere } from "@pano-view/react";
|
|
255
|
+
|
|
256
|
+
<PanoView style={{ height: 560 }}>
|
|
257
|
+
<Sphere src="/panoramas/room.webp" />
|
|
258
|
+
<ImageHotspot
|
|
259
|
+
id="gallery"
|
|
260
|
+
ariaLabel="Open gallery"
|
|
261
|
+
position={{ yaw: 28, pitch: -4 }}
|
|
262
|
+
width={18}
|
|
263
|
+
height={10}
|
|
264
|
+
mode="surface"
|
|
265
|
+
src="/hotspots/gallery.webp"
|
|
266
|
+
onClick={({ position }) => console.log("gallery", position)}
|
|
267
|
+
/>
|
|
268
|
+
</PanoView>;
|
|
269
|
+
```
|
|
270
|
+
|
|
271
|
+
`GraphicHotspot` accepts built-in `circle`, `rectangle`, and `ring` graphics,
|
|
272
|
+
an SVG URL, or safe SVG path data with an explicit viewBox. Built-in graphics
|
|
273
|
+
support `fill`, `stroke`, and `strokeWidth`; rectangles also support
|
|
274
|
+
`cornerRadius` and rings support `innerRadius`.
|
|
275
|
+
|
|
276
|
+
```tsx
|
|
277
|
+
import { GraphicHotspot } from "@pano-view/react";
|
|
278
|
+
|
|
279
|
+
<GraphicHotspot
|
|
280
|
+
id="wayfinding"
|
|
281
|
+
ariaLabel="Open wayfinding point"
|
|
282
|
+
position={{ yaw: -18, pitch: 9 }}
|
|
283
|
+
width={8}
|
|
284
|
+
height={8}
|
|
285
|
+
graphic={{
|
|
286
|
+
kind: "ring",
|
|
287
|
+
fill: "#df6b42",
|
|
288
|
+
stroke: "#f5fbfc",
|
|
289
|
+
strokeWidth: 8,
|
|
290
|
+
innerRadius: 0.64,
|
|
291
|
+
}}
|
|
292
|
+
onClick={({ position }) => console.log("wayfinding", position)}
|
|
293
|
+
/>
|
|
294
|
+
```
|
|
295
|
+
|
|
296
|
+
Set `draggable` and update the controlled position from `onPositionChange` to
|
|
297
|
+
move a selected hotspot. Clickable hotspots need `ariaLabel`: PanoView creates
|
|
298
|
+
an internal semantic control for Tab, Enter, and Space activation, with a
|
|
299
|
+
visible WebGL focus outline.
|
|
300
|
+
|
|
301
|
+
`ImageHotspot` calls `onLoad(texture)` and `onError(error)`. `GraphicHotspot`
|
|
302
|
+
uses the same callbacks. Built-in paths are rasterized locally; only an SVG URL
|
|
303
|
+
or SVG `path` data plus an explicit `viewBox` is accepted, never arbitrary SVG
|
|
304
|
+
markup.
|
|
305
|
+
|
|
306
|
+
## Hotspot mode and zoom behaviour
|
|
307
|
+
|
|
308
|
+
Point hotspots use one safe `mode` instead of separate orientation and
|
|
309
|
+
placement properties:
|
|
310
|
+
|
|
311
|
+
- `mode="surface"` attaches the plane to the local panorama surface.
|
|
312
|
+
- `mode="billboard"` keeps the plane facing the camera and floats it in front
|
|
313
|
+
of the shell. Set `distance` (world units, default `10`) to bring it nearer.
|
|
314
|
+
The built-in panorama shell is at roughly `1000` units; distance is
|
|
315
|
+
automatically capped so the hotspot's corners cannot cross it.
|
|
316
|
+
- `scaleMode="fov"` (default) lets a hotspot grow when the user zooms in.
|
|
317
|
+
`scaleMode="fixed"` compensates for FOV changes and keeps its screen size
|
|
318
|
+
close to the size at `referenceFov` (default `75`).
|
|
319
|
+
|
|
320
|
+
```tsx
|
|
321
|
+
<ImageHotspot
|
|
322
|
+
id="callout"
|
|
323
|
+
ariaLabel="Open callout"
|
|
324
|
+
position={{ yaw: 18, pitch: 5 }}
|
|
325
|
+
width={10}
|
|
326
|
+
height={6}
|
|
327
|
+
mode="billboard"
|
|
328
|
+
distance={14}
|
|
329
|
+
scaleMode="fixed"
|
|
330
|
+
src="/hotspots/callout.webp"
|
|
331
|
+
/>
|
|
332
|
+
```
|
|
333
|
+
|
|
334
|
+
## Polygon hotspots
|
|
335
|
+
|
|
336
|
+
`PolygonHotspot` renders a local spherical area from three or more controlled
|
|
337
|
+
yaw/pitch vertices. Concave polygons and the `-180°/180°` seam are supported.
|
|
338
|
+
The component reports, rather than renders, self-intersecting polygons, shapes
|
|
339
|
+
that contain a pole, and shapes that cannot fit within one hemisphere.
|
|
340
|
+
|
|
341
|
+
```tsx
|
|
342
|
+
import { PolygonHotspot } from "@pano-view/react";
|
|
343
|
+
|
|
344
|
+
<PolygonHotspot
|
|
345
|
+
id="exhibit-zone"
|
|
346
|
+
ariaLabel="Open exhibit zone"
|
|
347
|
+
vertices={[
|
|
348
|
+
{ yaw: 16, pitch: 8 },
|
|
349
|
+
{ yaw: 31, pitch: 7 },
|
|
350
|
+
{ yaw: 27, pitch: 19 },
|
|
351
|
+
{ yaw: 19, pitch: 16 },
|
|
352
|
+
]}
|
|
353
|
+
fill="#df6b42"
|
|
354
|
+
fillOpacity={0.32}
|
|
355
|
+
stroke="#f5fbfc"
|
|
356
|
+
strokeWidth={2}
|
|
357
|
+
strokeOpacity={0.8}
|
|
358
|
+
draggable
|
|
359
|
+
onVerticesChange={({ vertices }) => setZoneVertices(vertices)}
|
|
360
|
+
onInvalid={(issues) => console.error(issues)}
|
|
361
|
+
/>
|
|
362
|
+
```
|
|
363
|
+
|
|
364
|
+
`strokeWidth` is a screen-space CSS pixel value, so it remains visually
|
|
365
|
+
consistent across FOV changes. Dragging moves all vertices together while
|
|
366
|
+
keeping yaw normalized and pitch clamped. Individual vertex editing is provided
|
|
367
|
+
by the authoring workflow, not the runtime hotspot component.
|
|
368
|
+
|
|
369
|
+
Set `fillOpacity={0}` for a closed outline-only polygon. The fill and outline
|
|
370
|
+
share the same spherical edge sampling, so there is no intentional seam
|
|
371
|
+
between them.
|
|
372
|
+
|
|
373
|
+
## Polyline hotspots
|
|
374
|
+
|
|
375
|
+
`PolylineHotspot` is an open path of at least two yaw/pitch vertices. It uses
|
|
376
|
+
the same CSS-pixel `strokeWidth`, visibility, semantic interaction, controlled
|
|
377
|
+
whole-path drag, and vertex-change callback as `PolygonHotspot`, but it never
|
|
378
|
+
connects the final point to the first and has no fill.
|
|
379
|
+
|
|
380
|
+
```tsx
|
|
381
|
+
import { PolylineHotspot } from "@pano-view/react";
|
|
382
|
+
|
|
383
|
+
<PolylineHotspot
|
|
384
|
+
id="guided-route"
|
|
385
|
+
ariaLabel="Guided route"
|
|
386
|
+
vertices={routeVertices}
|
|
387
|
+
stroke="#75cbd3"
|
|
388
|
+
strokeWidth={2}
|
|
389
|
+
strokeOpacity={0.9}
|
|
390
|
+
draggable
|
|
391
|
+
onVerticesChange={({ vertices }) => setRouteVertices(vertices)}
|
|
392
|
+
/>;
|
|
393
|
+
```
|
|
394
|
+
|
|
395
|
+
`onInvalid` reports the only invalid runtime shape: fewer than two vertices.
|
|
396
|
+
|
|
397
|
+
## Sequence and video hotspots
|
|
398
|
+
|
|
399
|
+
`SequenceHotspot` animates a sprite sheet: one image containing equally sized
|
|
400
|
+
frames in a vertical or horizontal strip. This matches the common krpano
|
|
401
|
+
animated-hotspot format. Give `width` and `height` the aspect ratio of a single
|
|
402
|
+
frame, rather than the full strip. `playing` is controlled by the host; `fps`
|
|
403
|
+
defaults to `12` and `loop` defaults to `true`.
|
|
404
|
+
|
|
405
|
+
```tsx
|
|
406
|
+
import { SequenceHotspot } from "@pano-view/react";
|
|
407
|
+
|
|
408
|
+
<SequenceHotspot
|
|
409
|
+
id="pulse"
|
|
410
|
+
ariaLabel="Play marker sequence"
|
|
411
|
+
position={{ yaw: -42, pitch: -7 }}
|
|
412
|
+
width={8}
|
|
413
|
+
height={8}
|
|
414
|
+
src="/hotspots/pulse-strip.png"
|
|
415
|
+
frameCount={20}
|
|
416
|
+
frameDirection="vertical"
|
|
417
|
+
fps={12}
|
|
418
|
+
loop
|
|
419
|
+
playing={isPulsePlaying}
|
|
420
|
+
onEnded={() => setPulsePlaying(false)}
|
|
421
|
+
onError={({ error }) => console.error(error)}
|
|
422
|
+
/>;
|
|
423
|
+
```
|
|
424
|
+
|
|
425
|
+
`VideoHotspot` uses an `HTMLVideoElement` and `VideoTexture`. Its `playing`
|
|
426
|
+
prop is likewise controlled: toggle it from your click handler or application
|
|
427
|
+
state. On source change or unmount, the element is paused and its texture is
|
|
428
|
+
released. `playsInline` defaults to `true`, `muted` to `true`, `loop` to
|
|
429
|
+
`false`, and `preload` to `"metadata"`.
|
|
430
|
+
|
|
431
|
+
```tsx
|
|
432
|
+
import { VideoHotspot } from "@pano-view/react";
|
|
433
|
+
|
|
434
|
+
<VideoHotspot
|
|
435
|
+
id="clip"
|
|
436
|
+
ariaLabel="Play room video"
|
|
437
|
+
position={{ yaw: 48, pitch: 6 }}
|
|
438
|
+
width={18}
|
|
439
|
+
height={10.125}
|
|
440
|
+
mode="surface"
|
|
441
|
+
src="/hotspots/room.webm"
|
|
442
|
+
poster="/hotspots/room-poster.webp"
|
|
443
|
+
playing={isClipPlaying}
|
|
444
|
+
muted
|
|
445
|
+
volume={0.8}
|
|
446
|
+
onClick={() => setClipPlaying((playing) => !playing)}
|
|
447
|
+
onEnded={() => setClipPlaying(false)}
|
|
448
|
+
onPlaybackStateChange={(state) => console.log(state)}
|
|
449
|
+
onPlaybackError={(error) => console.error(error)}
|
|
450
|
+
/>;
|
|
451
|
+
```
|
|
452
|
+
|
|
453
|
+
Browsers can reject unmuted or otherwise non-gesture playback. In that case
|
|
454
|
+
`onPlaybackStateChange` receives `"blocked"` and `onPlaybackError` receives
|
|
455
|
+
the browser error; retain control of `playing` in the host and offer an
|
|
456
|
+
explicit user action.
|
|
457
|
+
|
|
458
|
+
Sequence loading uses `onLoadProgress` and `onError`; video reports media and
|
|
459
|
+
poster failures through `onError`. Neither component changes `playing` by
|
|
460
|
+
itself: update that prop in response to your UI, click handler, `onEnded`, or
|
|
461
|
+
playback error. This keeps source changes, unmounts, and React StrictMode
|
|
462
|
+
lifecycles deterministic.
|
|
463
|
+
|
|
464
|
+
## Automatic rotation
|
|
465
|
+
|
|
466
|
+
Render `AutoRotate` inside `PanoView` to 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.
|
|
467
|
+
|
|
468
|
+
```tsx
|
|
469
|
+
<PanoView style={{ height: 560 }}>
|
|
470
|
+
<AutoRotate enabled speed={12} acceleration={6} startDelay={2_000} />
|
|
471
|
+
<Sphere src="/panoramas/room.webp" />
|
|
472
|
+
</PanoView>
|
|
473
|
+
```
|
|
474
|
+
|
|
475
|
+
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.
|
|
476
|
+
|
|
477
|
+
## Next.js and SSR
|
|
478
|
+
|
|
479
|
+
The distributed entry is marked as a client module. Render it below a Client Component boundary. Panorama resources are loaded in the browser; no API keys or server configuration are required.
|