@ericchen1990/pano-view 1.0.0 → 2.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +433 -51
- package/dist/index.cjs +6689 -1231
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.mts +426 -45
- package/dist/index.d.ts +426 -45
- package/dist/index.mjs +6741 -1227
- package/dist/index.mjs.map +1 -1
- package/package.json +3 -1
package/README.md
CHANGED
|
@@ -7,10 +7,13 @@ Try the components in the [live playground](https://pano-view-playground.vercel.
|
|
|
7
7
|
## krpano-compatible tile output
|
|
8
8
|
|
|
9
9
|
Use this package when you want a React-native viewer around equirectangular
|
|
10
|
-
images or an existing krpano cube-tile pyramid.
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
10
|
+
images or an existing krpano cube-tile pyramid. Copy the krpano `<cube>` `url`
|
|
11
|
+
and `multires` attributes into `Tile`'s `urlTemplate` and `multires`, and copy
|
|
12
|
+
`<preview url>` into `previewUrl`; krpano's on-disk naming is not the same as
|
|
13
|
+
this package's defaults, so omitting them loads the wrong tiles or preview.
|
|
14
|
+
Rendering, controls, hotspots, and scene transitions stay in your React
|
|
15
|
+
application. This package is compatible with that tile output format but is
|
|
16
|
+
not affiliated with krpano.
|
|
14
17
|
|
|
15
18
|
## Install
|
|
16
19
|
|
|
@@ -18,7 +21,25 @@ compatible with that tile output format but is not affiliated with krpano.
|
|
|
18
21
|
npm install @ericchen1990/pano-view
|
|
19
22
|
```
|
|
20
23
|
|
|
21
|
-
React 19, React DOM 19, Three.js, `@react-three/fiber` 9,
|
|
24
|
+
React 19, React DOM 19, Three.js, `@react-three/fiber` 9,
|
|
25
|
+
`@react-three/drei` 10, and `@react-three/xr` 6 are peer dependencies. npm 7+
|
|
26
|
+
and pnpm install them automatically; they must be a single copy in the host
|
|
27
|
+
app.
|
|
28
|
+
|
|
29
|
+
Tailwind CSS v4 is now required for the built-in HTML chrome: video controls,
|
|
30
|
+
captions, context menus, tooltips, and accessibility helpers render with
|
|
31
|
+
Tailwind utility classes instead of a bundled stylesheet. Add a source entry in
|
|
32
|
+
your app so Tailwind can scan this package:
|
|
33
|
+
|
|
34
|
+
```css
|
|
35
|
+
@import "tailwindcss";
|
|
36
|
+
@source "../node_modules/@ericchen1990/pano-view";
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
This package does not ship a separate CSS file. If your app does not scan
|
|
40
|
+
`@ericchen1990/pano-view`, those built-in overlays will render without their
|
|
41
|
+
default styling. Customization still works through `className`, `style`, and
|
|
42
|
+
component appearance props.
|
|
22
43
|
|
|
23
44
|
## Exported components
|
|
24
45
|
|
|
@@ -26,8 +47,10 @@ React 19, React DOM 19, Three.js, `@react-three/fiber` 9, and `@react-three/drei
|
|
|
26
47
|
|
|
27
48
|
- [`PanoViewer`](#panoviewer) — canvas shell, camera, default controls, and imperative view API
|
|
28
49
|
- [`Sphere`](#sphere) — single 2:1 equirectangular image
|
|
50
|
+
- [`PanoVideo`](#panovideo) — 2:1 equirectangular video on the panorama sphere
|
|
29
51
|
- [`Tile`](#tile) — krpano-style multires cube tiles
|
|
30
52
|
- [`Scenes`](#scenes) — controlled multi-scene transitions
|
|
53
|
+
- [`PanoFilter`](#panofilter) — color and artistic filters on the panorama source
|
|
31
54
|
|
|
32
55
|
### Controls
|
|
33
56
|
|
|
@@ -35,11 +58,14 @@ React 19, React DOM 19, Three.js, `@react-three/fiber` 9, and `@react-three/drei
|
|
|
35
58
|
- [`TouchControls`](#touchcontrols) — one-finger drag and pinch zoom
|
|
36
59
|
- [`KeyboardControls`](#keyboardcontrols) — arrow-key look, FOV, and scene cycling
|
|
37
60
|
- [`AutoRotate`](#autorotate) — automatic yaw rotation
|
|
61
|
+
- [`Gyro`](#gyro) — opt-in device-orientation control
|
|
62
|
+
- [`WebVR`](#webvr) — WebXR-first immersive VR with MobileVR fallback
|
|
38
63
|
|
|
39
64
|
### Events and chrome
|
|
40
65
|
|
|
41
66
|
- [`PanoEvents`](#panoevents) — viewer-level lifecycle and interaction callbacks
|
|
42
67
|
- [`PanoContextMenu`](#panocontextmenu) — right-click reset / fullscreen menu
|
|
68
|
+
- [`PanoVideoControls`](#panovideo) — playback chrome for `PanoVideo` (mounted by default)
|
|
43
69
|
|
|
44
70
|
### Hotspots
|
|
45
71
|
|
|
@@ -97,7 +123,7 @@ export function ControlledExample() {
|
|
|
97
123
|
style={{ height: 560 }}
|
|
98
124
|
>
|
|
99
125
|
<AutoRotate enabled speed={18} acceleration={18} startDelay={1_000} />
|
|
100
|
-
<Sphere src="/panoramas/room.webp" />
|
|
126
|
+
<Sphere src="/panoramas/room.webp" previewUrl="preview.webp" />
|
|
101
127
|
</PanoViewer>
|
|
102
128
|
</>
|
|
103
129
|
);
|
|
@@ -128,6 +154,12 @@ The canvas cursor defaults to `grab`, becomes `grabbing` while dragging with the
|
|
|
128
154
|
|
|
129
155
|
`Sphere` expects a 2:1 equirectangular image. Use `yawOffset` when the source's forward direction needs horizontal adjustment.
|
|
130
156
|
|
|
157
|
+
`previewUrl` is required and shows a low-resolution 2:1 image while `src` loads.
|
|
158
|
+
Relative paths resolve against the directory of `src`; root-absolute and
|
|
159
|
+
`http(s)` URLs are used as-is. For krpano sphere scenes, copy
|
|
160
|
+
[`<preview url>`](https://krpano.com/docu/xml/#preview) into `previewUrl`. That
|
|
161
|
+
preview must be equirectangular, not a cube strip.
|
|
162
|
+
|
|
131
163
|
```tsx
|
|
132
164
|
"use client";
|
|
133
165
|
|
|
@@ -136,21 +168,97 @@ import { PanoViewer, Sphere } from "@ericchen1990/pano-view";
|
|
|
136
168
|
export function SphereExample() {
|
|
137
169
|
return (
|
|
138
170
|
<PanoViewer style={{ width: "100%", height: 560 }}>
|
|
139
|
-
<Sphere
|
|
171
|
+
<Sphere
|
|
172
|
+
src="/panoramas/room.webp"
|
|
173
|
+
previewUrl="preview.webp"
|
|
174
|
+
/>
|
|
175
|
+
</PanoViewer>
|
|
176
|
+
);
|
|
177
|
+
}
|
|
178
|
+
```
|
|
179
|
+
|
|
180
|
+
## PanoVideo
|
|
181
|
+
|
|
182
|
+
`PanoVideo` maps a 2:1 equirectangular video onto the same inward sphere as
|
|
183
|
+
`Sphere`. Pass one `src`, or `variants` with multiple files per quality so the
|
|
184
|
+
browser can pick mp4 / webm and the control bar can switch resolution. Changing
|
|
185
|
+
quality keeps the current time and play/pause state.
|
|
186
|
+
|
|
187
|
+
A default playback bar is mounted on the viewer overlay: play/pause, seek, time,
|
|
188
|
+
volume, speed, resolution, captions, and fullscreen. Volume is a hover (or tap)
|
|
189
|
+
vertical slider when the browser allows `HTMLMediaElement.volume`; iOS Safari
|
|
190
|
+
only exposes mute. The speed menu lists rates the current element accepts. The
|
|
191
|
+
captions menu also includes a settings panel for subtitle font size and
|
|
192
|
+
background opacity. When the viewer is narrow, speed, quality, and captions
|
|
193
|
+
move into a More menu.
|
|
194
|
+
Pass `controls={false}` to hide it, an appearance object to restyle the default
|
|
195
|
+
instance, or render `PanoVideoControls` as a `PanoViewer` child to replace that
|
|
196
|
+
instance.
|
|
197
|
+
|
|
198
|
+
Captions use WebVTT `tracks`. The video element is off-screen, so cue text is
|
|
199
|
+
rendered as a HUD overlay. `captions={false}` hides the overlay and the language
|
|
200
|
+
menu; an object merges with the default appearance (`color`, `background`,
|
|
201
|
+
`fontSize`, `fontFamily`, `textShadow`, `padding`, `borderRadius`, `maxWidth`,
|
|
202
|
+
`bottom`). Those values act as the initial caption appearance; viewers can still
|
|
203
|
+
adjust subtitle size and background from the default control-bar menu at
|
|
204
|
+
runtime.
|
|
205
|
+
|
|
206
|
+
```tsx
|
|
207
|
+
"use client";
|
|
208
|
+
|
|
209
|
+
import { PanoViewer, PanoVideo } from "@ericchen1990/pano-view";
|
|
210
|
+
|
|
211
|
+
export function VideoExample() {
|
|
212
|
+
return (
|
|
213
|
+
<PanoViewer style={{ width: "100%", height: 560 }}>
|
|
214
|
+
<PanoVideo
|
|
215
|
+
defaultVariantId="1024"
|
|
216
|
+
variants={[
|
|
217
|
+
{
|
|
218
|
+
id: "1024",
|
|
219
|
+
label: "1024p",
|
|
220
|
+
poster: "/video/1024-poster.jpg",
|
|
221
|
+
sources: [
|
|
222
|
+
{ src: "/video/1024.mp4", type: "video/mp4" },
|
|
223
|
+
{ src: "/video/1024.webm", type: "video/webm" },
|
|
224
|
+
],
|
|
225
|
+
},
|
|
226
|
+
{
|
|
227
|
+
id: "1920",
|
|
228
|
+
label: "1920p",
|
|
229
|
+
poster: "/video/1920-poster.jpg",
|
|
230
|
+
sources: [
|
|
231
|
+
{ src: "/video/1920.mp4", type: "video/mp4" },
|
|
232
|
+
{ src: "/video/1920.webm", type: "video/webm" },
|
|
233
|
+
],
|
|
234
|
+
},
|
|
235
|
+
]}
|
|
236
|
+
tracks={[
|
|
237
|
+
{ src: "/video/en.vtt", srcLang: "en", label: "English", default: true },
|
|
238
|
+
{ src: "/video/zh.vtt", srcLang: "zh", label: "中文" },
|
|
239
|
+
]}
|
|
240
|
+
captions={{ fontSize: 16, color: "#fff" }}
|
|
241
|
+
muted
|
|
242
|
+
/>
|
|
140
243
|
</PanoViewer>
|
|
141
244
|
);
|
|
142
245
|
}
|
|
143
246
|
```
|
|
144
247
|
|
|
248
|
+
`muted` defaults to `true` so `autoPlay` has a chance under browser autoplay
|
|
249
|
+
rules. Unmuted playback still needs a user gesture; `onPlaybackStateChange`
|
|
250
|
+
receives `"blocked"` when `play()` is rejected. Remote videos and VTT files
|
|
251
|
+
that are not same-origin should set `crossOrigin` (usually `"anonymous"`).
|
|
252
|
+
|
|
145
253
|
## Tile
|
|
146
254
|
|
|
147
|
-
`Tile` renders six inward-facing cube faces and loads only tiles around the current view.
|
|
255
|
+
`Tile` renders six inward-facing cube faces and loads only tiles around the current view. When `urlTemplate` is omitted, paths relative to `baseUrl` use this package default:
|
|
148
256
|
|
|
149
257
|
```text
|
|
150
|
-
tiles/
|
|
258
|
+
tiles/%s/l%l/%v/l%l_%s_%h_%v.webp
|
|
151
259
|
```
|
|
152
260
|
|
|
153
|
-
Faces are `f`, `r`, `b`, `l`, `u`, and `d`; rows and columns are 1-based.
|
|
261
|
+
which expands to `tiles/{face}/l{level}/{row}/l{level}_{face}_{col}_{row}.webp`. Faces are `f`, `r`, `b`, `l`, `u`, and `d`; rows and columns are 1-based.
|
|
154
262
|
|
|
155
263
|
```tsx
|
|
156
264
|
import { PanoViewer, Tile } from "@ericchen1990/pano-view";
|
|
@@ -161,33 +269,81 @@ export function TileExample() {
|
|
|
161
269
|
<Tile
|
|
162
270
|
baseUrl="https://cdn.example.com/panoramas/room"
|
|
163
271
|
multires="512,500,1000,2000"
|
|
272
|
+
previewUrl="previews/cube-vertical.webp"
|
|
164
273
|
/>
|
|
165
274
|
</PanoViewer>
|
|
166
275
|
);
|
|
167
276
|
}
|
|
168
277
|
```
|
|
169
278
|
|
|
170
|
-
|
|
279
|
+
krpano cube-tile output does not follow that default. Read `url`, `multires`,
|
|
280
|
+
and the preview path from the krpano XML
|
|
281
|
+
([`<cube>`](https://krpano.com/docu/xml/#image.cube),
|
|
282
|
+
[`<preview>`](https://krpano.com/docu/xml/#preview)) and pass them through. If
|
|
283
|
+
those values are missing or do not match the files on disk, tiles resolve to
|
|
284
|
+
the wrong face, level, or row/column and the panorama looks scrambled.
|
|
285
|
+
|
|
286
|
+
| krpano | `Tile` / `TileScene` prop |
|
|
287
|
+
|---|---|
|
|
288
|
+
| `<cube url>` | `urlTemplate` |
|
|
289
|
+
| `<cube multires>` | `multires` |
|
|
290
|
+
| `<preview url>` | `previewUrl` |
|
|
291
|
+
| `<preview striporder>` | `previewFaceOrder` |
|
|
292
|
+
|
|
293
|
+
`Sphere` uses the same `<preview url>` → `previewUrl` mapping; that file must be a 2:1 equirectangular image, not a cube strip.
|
|
294
|
+
|
|
295
|
+
krpano's documented short syntax:
|
|
296
|
+
|
|
297
|
+
```xml
|
|
298
|
+
<preview url="preview.jpg" />
|
|
299
|
+
<image>
|
|
300
|
+
<cube url="pano_%s_%l_%v_%h.jpg"
|
|
301
|
+
multires="512,1024,2048,4096"
|
|
302
|
+
/>
|
|
303
|
+
</image>
|
|
304
|
+
```
|
|
305
|
+
|
|
306
|
+
maps to:
|
|
307
|
+
|
|
308
|
+
```tsx
|
|
309
|
+
<Tile
|
|
310
|
+
baseUrl="/panoramas/room"
|
|
311
|
+
urlTemplate="pano_%s_%l_%v_%h.jpg"
|
|
312
|
+
multires="512,1024,2048,4096"
|
|
313
|
+
previewUrl="preview.jpg"
|
|
314
|
+
/>
|
|
315
|
+
```
|
|
316
|
+
|
|
317
|
+
A MAKE PANO (MULTIRES) folder layout is often
|
|
318
|
+
`tiles/%s/l%l/%v/l%l_%s_%v_%h.jpg` with `preview.jpg` at the scene root. That
|
|
319
|
+
filename is `%v_%h` (row then column). This package's default tile template is
|
|
320
|
+
`%h_%v` (column then row). Copy the XML values instead of assuming the two
|
|
321
|
+
conventions match.
|
|
322
|
+
|
|
323
|
+
The first `multires` value is the tile size. Remaining values are ascending cube-face sizes for `l1`, `l2`, and later levels. `previewUrl` is required. Relative paths resolve against `baseUrl`; root-absolute and `http(s)` URLs are used as-is. A typical krpano MAKE PANO (MULTIRES) scene looks like this:
|
|
171
324
|
|
|
172
325
|
```tsx
|
|
173
326
|
<Tile
|
|
174
327
|
baseUrl="/panoramas/room"
|
|
328
|
+
urlTemplate="tiles/%s/l%l/%v/l%l_%s_%v_%h.jpg"
|
|
175
329
|
multires="512,1000,2000"
|
|
176
|
-
previewUrl="
|
|
330
|
+
previewUrl="preview.jpg"
|
|
177
331
|
previewFaceOrder={["l", "f", "r", "b", "u", "d"]}
|
|
178
332
|
/>
|
|
179
333
|
```
|
|
180
334
|
|
|
181
|
-
`previewFaceOrder` must list the six face codes in their top-to-bottom atlas order: `f`, `r`, `b`, `l`, `u`, and `d`.
|
|
335
|
+
`previewFaceOrder` must list the six face codes in their top-to-bottom atlas order: `f`, `r`, `b`, `l`, `u`, and `d`. The preview image is a vertical 1×6 cubestrip. krpano `thumb.jpg` is a small thumbnail and is not this atlas.
|
|
182
336
|
|
|
183
337
|
During rapid rotation or zoom, loaded tiles remain visible while newly visible tiles use their parent level or the preview as a local fallback.
|
|
184
338
|
|
|
185
|
-
|
|
339
|
+
For layouts that are not krpano XML, override the default with a placeholder
|
|
340
|
+
template or a resolver. Both return paths relative to `baseUrl`:
|
|
186
341
|
|
|
187
342
|
```tsx
|
|
188
343
|
<Tile
|
|
189
344
|
baseUrl="/panoramas/room"
|
|
190
345
|
multires={{ tileSize: 512, levels: [500, 1000, 2000] }}
|
|
346
|
+
previewUrl="previews/cube-vertical.webp"
|
|
191
347
|
urlTemplate="assets/%s/%l/%v_%h.webp"
|
|
192
348
|
resolveTileUrl={({ face, level, row, col }) =>
|
|
193
349
|
`api/tile/${face}/${level}/${row}/${col}`
|
|
@@ -208,7 +364,7 @@ a single non-stereo cube panorama.
|
|
|
208
364
|
## Scenes
|
|
209
365
|
|
|
210
366
|
`Scenes` switches controlled sphere and cube-tile scenes with GPU-only
|
|
211
|
-
snapshot blending. The target scene loads its sphere
|
|
367
|
+
snapshot blending. The target scene loads its sphere or tile preview first;
|
|
212
368
|
once ready, the current framebuffer becomes a temporary GPU texture, its source
|
|
213
369
|
textures are released, and the target scene blends in. This avoids holding two
|
|
214
370
|
high-resolution tile scenes in WebGL memory at once.
|
|
@@ -221,12 +377,14 @@ import {
|
|
|
221
377
|
} from "@ericchen1990/pano-view";
|
|
222
378
|
|
|
223
379
|
const scenes: Scene[] = [
|
|
224
|
-
{ id: "lobby", type: "sphere", src: "/panoramas/lobby.webp" },
|
|
380
|
+
{ id: "lobby", type: "sphere", src: "/panoramas/lobby.webp", previewUrl: "preview.webp" },
|
|
225
381
|
{
|
|
226
382
|
id: "terrace",
|
|
227
383
|
type: "tile",
|
|
228
384
|
baseUrl: "/panoramas/terrace",
|
|
229
385
|
multires: "512,1000,2000",
|
|
386
|
+
urlTemplate: "tiles/%s/l%l/%v/l%l_%s_%v_%h.webp",
|
|
387
|
+
previewUrl: "previews/cube-vertical.webp",
|
|
230
388
|
},
|
|
231
389
|
];
|
|
232
390
|
|
|
@@ -241,16 +399,64 @@ const scenes: Scene[] = [
|
|
|
241
399
|
</PanoViewer>;
|
|
242
400
|
```
|
|
243
401
|
|
|
244
|
-
Available presets are `none`, `crossfade`, `zoom`, `blackout`,
|
|
245
|
-
`slideRightToLeft`, `slideTopToBottom`, `slideDiagonal`,
|
|
246
|
-
`verticalOpen`, `horizontalOpen`, and `ellipticZoomOpen`.
|
|
247
|
-
|
|
402
|
+
Available KRpano-style presets are `none`, `crossfade`, `zoom`, `blackout`,
|
|
403
|
+
`whiteFlash`, `slideRightToLeft`, `slideTopToBottom`, `slideDiagonal`,
|
|
404
|
+
`circleOpen`, `verticalOpen`, `horizontalOpen`, and `ellipticZoomOpen`.
|
|
405
|
+
Grid presets are `pixelate`, `gridWipe`, `gridWipeUp`, `gridWipeRight`,
|
|
406
|
+
`gridWipeDiagonal`, `gridWipeCenter`, `gridWipeChecker`, and `hexDissolve`.
|
|
407
|
+
Cinematic presets are `dissolve`, `shatter`, `particles`, `glitch`, `swirl`,
|
|
408
|
+
`clockWipe`, `ripple`, `zoomBlur`, and `filmBurn`. All of them still blend a
|
|
409
|
+
GPU framebuffer snapshot over the incoming panorama; they do not mix two full
|
|
410
|
+
360 textures. Pass `{ preset: "crossfade", duration: 0.6 }` to override a
|
|
411
|
+
preset duration.
|
|
248
412
|
|
|
249
413
|
While a transition runs, panorama drag/zoom input is locked and
|
|
250
414
|
`renderHotspots` is hidden. New `activeSceneId` values supersede a target that
|
|
251
415
|
is still being prepared. `maxTextureMemoryMb` and `maxConcurrentTileLoads`
|
|
252
416
|
apply to the whole `Scenes` viewer rather than to each tile scene.
|
|
253
417
|
|
|
418
|
+
## PanoFilter
|
|
419
|
+
|
|
420
|
+
Render `PanoFilter` inside `PanoViewer` to color-grade or stylize the panorama
|
|
421
|
+
source (`Sphere`, `Tile`, and `PanoVideo`). The filter runs on those materials
|
|
422
|
+
only: 3D hotspots, tooltips, video chrome, and iframe overlays keep their
|
|
423
|
+
original color. WebXR / MobileVR inherit the same look because the panorama
|
|
424
|
+
shader itself is patched.
|
|
425
|
+
|
|
426
|
+
`intensity` mixes the filtered result with the original image (`0`–`1`,
|
|
427
|
+
default `1`). Omit the component, pass `preset="none"`, or set `enabled={false}`
|
|
428
|
+
to leave the source unfiltered.
|
|
429
|
+
|
|
430
|
+
```tsx
|
|
431
|
+
<PanoViewer style={{ height: 560 }}>
|
|
432
|
+
<PanoFilter preset="pencil" intensity={0.85} />
|
|
433
|
+
<Sphere src="/panoramas/room.webp" previewUrl="preview.webp" />
|
|
434
|
+
<ImageHotspot
|
|
435
|
+
id="door"
|
|
436
|
+
position={{ yaw: 24, pitch: -6 }}
|
|
437
|
+
src="/hotspots/door.webp"
|
|
438
|
+
/>
|
|
439
|
+
</PanoViewer>
|
|
440
|
+
```
|
|
441
|
+
|
|
442
|
+
| Preset | Look |
|
|
443
|
+
|---|---|
|
|
444
|
+
| `none` | Identity (no filter) |
|
|
445
|
+
| `grayscale` | Rec.709 black and white |
|
|
446
|
+
| `sepia` | Warm brown antique print |
|
|
447
|
+
| `vintage` | Faded film with grain |
|
|
448
|
+
| `cool` / `warm` | Blue or amber temperature shift |
|
|
449
|
+
| `pencil` | Graphite sketch on paper |
|
|
450
|
+
| `coloredPencil` | Tinted pencil lines |
|
|
451
|
+
| `crayon` | Posterized wax strokes |
|
|
452
|
+
| `watercolor` | Soft pigment on paper |
|
|
453
|
+
| `cartoon` | Cel shading with outlines |
|
|
454
|
+
| `crosshatch` | Luminance hatching |
|
|
455
|
+
|
|
456
|
+
Built-in artistic looks use screen-space edges and world-space grain so cube
|
|
457
|
+
tiles do not show filter seams. Scene-transition snapshots capture the filtered
|
|
458
|
+
panorama when a filter is active.
|
|
459
|
+
|
|
254
460
|
## AutoRotate
|
|
255
461
|
|
|
256
462
|
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.
|
|
@@ -258,10 +464,140 @@ Auto-rotation is off by default. Render `AutoRotate` inside `PanoViewer` to enab
|
|
|
258
464
|
```tsx
|
|
259
465
|
<PanoViewer style={{ height: 560 }}>
|
|
260
466
|
<AutoRotate enabled speed={12} acceleration={6} startDelay={2_000} />
|
|
261
|
-
<Sphere src="/panoramas/room.webp" />
|
|
467
|
+
<Sphere src="/panoramas/room.webp" previewUrl="preview.webp" />
|
|
262
468
|
</PanoViewer>
|
|
263
469
|
```
|
|
264
470
|
|
|
471
|
+
## Gyro
|
|
472
|
+
|
|
473
|
+
`Gyro` is an opt-in device-orientation control and is disabled by default.
|
|
474
|
+
Render it inside `PanoViewer` and control its `enabled` prop. Relative mode
|
|
475
|
+
(the default) treats the view at activation time as the starting direction.
|
|
476
|
+
Set `absolute` to follow compass-oriented headings and use `north` to identify
|
|
477
|
+
the panorama yaw that points North.
|
|
478
|
+
|
|
479
|
+
```tsx
|
|
480
|
+
import { useRef, useState } from "react";
|
|
481
|
+
import {
|
|
482
|
+
Gyro,
|
|
483
|
+
PanoViewer,
|
|
484
|
+
Sphere,
|
|
485
|
+
type GyroHandle,
|
|
486
|
+
} from "@ericchen1990/pano-view";
|
|
487
|
+
|
|
488
|
+
export function GyroExample() {
|
|
489
|
+
const gyroRef = useRef<GyroHandle>(null);
|
|
490
|
+
const [enabled, setEnabled] = useState(false);
|
|
491
|
+
|
|
492
|
+
const toggle = async () => {
|
|
493
|
+
if (enabled) {
|
|
494
|
+
setEnabled(false);
|
|
495
|
+
return;
|
|
496
|
+
}
|
|
497
|
+
// iOS requires this call to run directly inside a user gesture.
|
|
498
|
+
setEnabled((await gyroRef.current?.requestPermission()) ?? false);
|
|
499
|
+
};
|
|
500
|
+
|
|
501
|
+
return (
|
|
502
|
+
<>
|
|
503
|
+
<button onClick={() => void toggle()}>
|
|
504
|
+
{enabled ? "Disable gyro" : "Enable gyro"}
|
|
505
|
+
</button>
|
|
506
|
+
<PanoViewer style={{ height: 560 }}>
|
|
507
|
+
<Gyro
|
|
508
|
+
ref={gyroRef}
|
|
509
|
+
enabled={enabled}
|
|
510
|
+
camroll
|
|
511
|
+
friction={0}
|
|
512
|
+
softstart={0.5}
|
|
513
|
+
touchMode="full"
|
|
514
|
+
onDenied={() => setEnabled(false)}
|
|
515
|
+
/>
|
|
516
|
+
<Sphere src="/panoramas/room.webp" previewUrl="preview.webp" />
|
|
517
|
+
</PanoViewer>
|
|
518
|
+
</>
|
|
519
|
+
);
|
|
520
|
+
}
|
|
521
|
+
```
|
|
522
|
+
|
|
523
|
+
`camroll` (default `true`) levels the camera from device roll. `friction`
|
|
524
|
+
accepts `0` to `0.99`; higher values smooth more strongly but add latency.
|
|
525
|
+
`softstart` is the activation blend duration in seconds (default `0.5`).
|
|
526
|
+
`desktopSupport` defaults to `false` because desktop browsers can expose the
|
|
527
|
+
API without a physical sensor.
|
|
528
|
+
|
|
529
|
+
`touchMode` controls how touch drag combines with device movement:
|
|
530
|
+
|
|
531
|
+
- `off` ignores touch yaw/pitch while leaving pinch FOV available.
|
|
532
|
+
- `horizontaloffset` adds only a horizontal offset.
|
|
533
|
+
- `full` adds horizontal and vertical offsets; the vertical offset returns
|
|
534
|
+
toward the physical orientation as the device moves.
|
|
535
|
+
- `disablegyro` disables the active gyro session on the next touch.
|
|
536
|
+
|
|
537
|
+
The `GyroHandle` exposes `resetSensor(yaw?, pitch?)`, `isAvailable()`,
|
|
538
|
+
`isEnabled()`, and `requestPermission()`. Gyro activity pauses `AutoRotate`
|
|
539
|
+
and counts as viewer interaction for idle tracking. Sensor APIs generally
|
|
540
|
+
require HTTPS. A cross-origin iframe must be permitted by the host page, for
|
|
541
|
+
example with `allow="gyroscope; accelerometer"` and a compatible
|
|
542
|
+
`Permissions-Policy` header.
|
|
543
|
+
|
|
544
|
+
## WebVR
|
|
545
|
+
|
|
546
|
+
`WebVR` adds immersive viewing to the nearest `PanoViewer`. It prefers an
|
|
547
|
+
`immersive-vr` WebXR session on supported headsets. On phones without WebXR it
|
|
548
|
+
uses a stereoscopic split-screen view with device orientation, and on desktop
|
|
549
|
+
it can provide a mouse-driven simulated VR preview. The built-in chrome includes
|
|
550
|
+
Enter VR, Exit VR, and MobileVR setup controls.
|
|
551
|
+
|
|
552
|
+
```tsx
|
|
553
|
+
import { useRef } from "react";
|
|
554
|
+
import {
|
|
555
|
+
PanoViewer,
|
|
556
|
+
Sphere,
|
|
557
|
+
WebVR,
|
|
558
|
+
type WebVRHandle,
|
|
559
|
+
} from "@ericchen1990/pano-view";
|
|
560
|
+
|
|
561
|
+
export function WebVRExample() {
|
|
562
|
+
const webVRRef = useRef<WebVRHandle>(null);
|
|
563
|
+
|
|
564
|
+
return (
|
|
565
|
+
<PanoViewer style={{ height: 560 }}>
|
|
566
|
+
<WebVR
|
|
567
|
+
ref={webVRRef}
|
|
568
|
+
chrome
|
|
569
|
+
fakeSupport
|
|
570
|
+
mobileVr
|
|
571
|
+
onEnterVR={(mode) => console.log("Entered", mode)}
|
|
572
|
+
/>
|
|
573
|
+
<Sphere src="/panoramas/room.webp" previewUrl="preview.webp" />
|
|
574
|
+
</PanoViewer>
|
|
575
|
+
);
|
|
576
|
+
}
|
|
577
|
+
```
|
|
578
|
+
|
|
579
|
+
The handle exposes `enterVR()`, `exitVR()`, `toggleVR()`, `isAvailable()`,
|
|
580
|
+
`isEnabled()`, `getMode()`, and `requestPermission()`. Modes are `"webxr"`,
|
|
581
|
+
`"mobilevr"`, and `"fake"`. `profile` accepts `"none"`, `"cardboard-v1"`,
|
|
582
|
+
`"cardboard-v2"`, `"gear-vr"`, `"daydream"`, or a custom
|
|
583
|
+
`{ fov, ipdMm, k1, k2 }` lens profile. MobileVR setup values are versioned and
|
|
584
|
+
stored in the current origin's `localStorage`. In VR, a center reticle shows a
|
|
585
|
+
crosshair; looking at an interactive hotspot draws a clockwise ring that fills
|
|
586
|
+
over `cursorDwellMs` (default 1500), then the hotspot click handler runs. Set
|
|
587
|
+
`cursorDwellMs={0}` to keep the reticle without auto-click. Simulated desktop
|
|
588
|
+
VR (`fake` mode) requests pointer lock by default (`mousePointerLock`) so the
|
|
589
|
+
cursor hides and mouse movement looks around; click the view again if the
|
|
590
|
+
browser releases the lock. Fallback sessions keep the screen awake
|
|
591
|
+
(`wakelock`) with the Screen Wake Lock API, or a looping hidden video on
|
|
592
|
+
browsers that lack it.
|
|
593
|
+
|
|
594
|
+
WebXR requires HTTPS and a user gesture. Mobile sensor access also requires
|
|
595
|
+
HTTPS and may require an explicit iOS permission prompt. Embedded viewers need
|
|
596
|
+
appropriate `xr-spatial-tracking`, `gyroscope`, and `accelerometer` permissions.
|
|
597
|
+
DOM overlays such as iframe hotspots, tooltips, and video controls are not
|
|
598
|
+
visible inside an immersive headset session; mesh-based hotspots remain
|
|
599
|
+
interactive through the default WebXR controller rays.
|
|
600
|
+
|
|
265
601
|
## Mouse, touch, and keyboard controls
|
|
266
602
|
|
|
267
603
|
`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.
|
|
@@ -277,7 +613,7 @@ Auto-rotation is off by default. Render `AutoRotate` inside `PanoViewer` to enab
|
|
|
277
613
|
}}
|
|
278
614
|
style={{ height: 560 }}
|
|
279
615
|
>
|
|
280
|
-
<Sphere src="/panoramas/room.webp" />
|
|
616
|
+
<Sphere src="/panoramas/room.webp" previewUrl="preview.webp" />
|
|
281
617
|
</PanoViewer>
|
|
282
618
|
```
|
|
283
619
|
|
|
@@ -346,7 +682,7 @@ export function KeyboardExample({ scenes }: { scenes: Scene[] }) {
|
|
|
346
682
|
|
|
347
683
|
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
684
|
|
|
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.
|
|
685
|
+
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.
|
|
350
686
|
|
|
351
687
|
## Panorama coordinate events
|
|
352
688
|
|
|
@@ -375,7 +711,7 @@ export function PlacementExample() {
|
|
|
375
711
|
onPanoramaPointerMove={({ position }) => console.log("move", position)}
|
|
376
712
|
style={{ height: 560 }}
|
|
377
713
|
>
|
|
378
|
-
<Sphere src="/panoramas/room.webp" />
|
|
714
|
+
<Sphere src="/panoramas/room.webp" previewUrl="preview.webp" />
|
|
379
715
|
</PanoViewer>
|
|
380
716
|
);
|
|
381
717
|
}
|
|
@@ -416,7 +752,7 @@ import {
|
|
|
416
752
|
onAutoRotateOneRound={() => console.log("full turn")}
|
|
417
753
|
/>
|
|
418
754
|
<AutoRotate enabled />
|
|
419
|
-
<Sphere src="/panoramas/room.webp" />
|
|
755
|
+
<Sphere src="/panoramas/room.webp" previewUrl="preview.webp" />
|
|
420
756
|
</PanoViewer>;
|
|
421
757
|
```
|
|
422
758
|
|
|
@@ -429,6 +765,8 @@ Supported callbacks (krpano names in parentheses where applicable):
|
|
|
429
765
|
- Fullscreen: `onEnterFullscreen` / `onExitFullscreen`
|
|
430
766
|
- `onResize` — canvas content-box size via `ResizeObserver`
|
|
431
767
|
- Auto-rotate: `onAutoRotateStart` / `onAutoRotateStop` / `onAutoRotateOneRound`
|
|
768
|
+
- Gyro: `onGyroAvailable` / `onGyroUnavailable` / `onGyroEnable` /
|
|
769
|
+
`onGyroDisable` / `onGyroDenied`
|
|
432
770
|
|
|
433
771
|
`PanoViewer`'s existing `onViewChange` / `onPanoramaClick` / `onPanoramaDoubleClick` /
|
|
434
772
|
`onPanoramaPointerMove` props remain supported and share the same event bus.
|
|
@@ -436,7 +774,7 @@ Supported callbacks (krpano names in parentheses where applicable):
|
|
|
436
774
|
Resource loading and scene blending stay on their owners: use `Sphere` /
|
|
437
775
|
`Tile` `onLoad` / `onError` / `onLoadProgress`, and `Scenes`
|
|
438
776
|
`onTransitionEnd` / `onTransitionError`. This package does not mirror krpano
|
|
439
|
-
xml/VR/
|
|
777
|
+
xml/VR/frame-render events.
|
|
440
778
|
|
|
441
779
|
## PanoContextMenu
|
|
442
780
|
|
|
@@ -447,7 +785,7 @@ separator between them.
|
|
|
447
785
|
|
|
448
786
|
```tsx
|
|
449
787
|
<PanoViewer style={{ height: 560 }}>
|
|
450
|
-
<Sphere src="/panoramas/room.webp" />
|
|
788
|
+
<Sphere src="/panoramas/room.webp" previewUrl="preview.webp" />
|
|
451
789
|
</PanoViewer>
|
|
452
790
|
```
|
|
453
791
|
|
|
@@ -455,7 +793,7 @@ Disable the default menu (restore the browser menu, or mount your own):
|
|
|
455
793
|
|
|
456
794
|
```tsx
|
|
457
795
|
<PanoViewer contextMenu={false}>
|
|
458
|
-
<Sphere src="/panoramas/room.webp" />
|
|
796
|
+
<Sphere src="/panoramas/room.webp" previewUrl="preview.webp" />
|
|
459
797
|
</PanoViewer>
|
|
460
798
|
```
|
|
461
799
|
|
|
@@ -463,7 +801,7 @@ Tune appearance while keeping the default items:
|
|
|
463
801
|
|
|
464
802
|
```tsx
|
|
465
803
|
<PanoViewer contextMenu={{ appearance: { opacity: 0.92, borderRadius: 8 } }}>
|
|
466
|
-
<Sphere src="/panoramas/room.webp" />
|
|
804
|
+
<Sphere src="/panoramas/room.webp" previewUrl="preview.webp" />
|
|
467
805
|
</PanoViewer>
|
|
468
806
|
```
|
|
469
807
|
|
|
@@ -490,7 +828,7 @@ only add your own:
|
|
|
490
828
|
}}
|
|
491
829
|
style={{ height: 560 }}
|
|
492
830
|
>
|
|
493
|
-
<Sphere src="/panoramas/room.webp" />
|
|
831
|
+
<Sphere src="/panoramas/room.webp" previewUrl="preview.webp" />
|
|
494
832
|
</PanoViewer>
|
|
495
833
|
```
|
|
496
834
|
|
|
@@ -524,7 +862,7 @@ reimplementing fullscreen enter/exit state:
|
|
|
524
862
|
}}
|
|
525
863
|
style={{ height: 560 }}
|
|
526
864
|
>
|
|
527
|
-
<Sphere src="/panoramas/room.webp" />
|
|
865
|
+
<Sphere src="/panoramas/room.webp" previewUrl="preview.webp" />
|
|
528
866
|
</PanoViewer>
|
|
529
867
|
```
|
|
530
868
|
|
|
@@ -548,14 +886,25 @@ For a fully custom DOM menu, set `contextMenu={false}` and render
|
|
|
548
886
|
|
|
549
887
|
## Shared hotspot contract and saved definitions
|
|
550
888
|
|
|
551
|
-
Every hotspot has an `id`, optional `visible`, `interactive`, `
|
|
552
|
-
and semantic interaction callbacks. Point hotspots additionally
|
|
553
|
-
controlled `position`, angular `width`/`height`, hotspot mode, and the
|
|
554
|
-
options described below. `interactive={false}` keeps a hotspot visible
|
|
555
|
-
letting a drawing tool receive the panorama pointer events beneath it.
|
|
889
|
+
Every hotspot has an `id`, optional `visible`, `interactive`, `pointerEvents`,
|
|
890
|
+
`renderOrder`, and semantic interaction callbacks. Point hotspots additionally
|
|
891
|
+
use a controlled `position`, angular `width`/`height`, hotspot mode, and the
|
|
892
|
+
zoom options described below. `interactive={false}` keeps a hotspot visible
|
|
893
|
+
while letting a drawing tool receive the panorama pointer events beneath it.
|
|
894
|
+
`pointerEvents="none"` ignores mouse, touch, and pen so those events pass
|
|
895
|
+
through to the panorama even when the hotspot stays keyboard-interactive.
|
|
556
896
|
Interactive hotspots set the canvas cursor to `pointer` while hovered, or to
|
|
557
897
|
the hotspot's `cursor` prop when that is set.
|
|
558
898
|
|
|
899
|
+
```tsx
|
|
900
|
+
<TextHotspot
|
|
901
|
+
id="caption"
|
|
902
|
+
pointerEvents="none"
|
|
903
|
+
position={{ yaw: 12, pitch: -8 }}
|
|
904
|
+
text="Decorative label"
|
|
905
|
+
/>
|
|
906
|
+
```
|
|
907
|
+
|
|
559
908
|
`onClick` and `onHoverChange` receive a position and input source (`"pointer"`
|
|
560
909
|
or `"keyboard"`). Point hotspots can use `draggable`, `onDragStart`,
|
|
561
910
|
`onPositionChange`, and `onDragEnd`; the host must write the reported position
|
|
@@ -568,11 +917,18 @@ Every hotspot can show a DOM tooltip at its anchor. Pass a string or
|
|
|
568
917
|
on screen. Clicking the hotspot does not toggle it. Optional
|
|
569
918
|
`tooltipTrigger="hover"` shows the bubble on pointer hover or keyboard focus;
|
|
570
919
|
`"click"` pins it until the hotspot is clicked again or the empty panorama is
|
|
571
|
-
clicked. Hover and click triggers need `interactive
|
|
920
|
+
clicked. Hover and click triggers need `interactive` and `pointerEvents`
|
|
921
|
+
other than `"none"`.
|
|
572
922
|
|
|
573
923
|
`tooltipPlacement` is `"top"` (default), `"bottom"`, `"left"`, or `"right"`.
|
|
574
|
-
|
|
575
|
-
screen
|
|
924
|
+
These are screen directions: the bubble sits outside the hotspot's projected
|
|
925
|
+
screen bounds on that edge, so rotated graphics such as arrows are not covered.
|
|
926
|
+
`tooltipOffset` is the screen-space gap in CSS pixels (default `12` /
|
|
927
|
+
`DEFAULT_HOTSPOT_TOOLTIP_OFFSET`).
|
|
928
|
+
|
|
929
|
+
Optional `tooltipAppearance` overrides the bubble paint. Unset fields keep the
|
|
930
|
+
library default theme (`DEFAULT_HOTSPOT_TOOLTIP_APPEARANCE`): background, text
|
|
931
|
+
color, border, corner radius, shadow, padding, and font size.
|
|
576
932
|
|
|
577
933
|
```tsx
|
|
578
934
|
<ImageHotspot
|
|
@@ -583,6 +939,15 @@ screen-space gap in CSS pixels (default `12` / `DEFAULT_HOTSPOT_TOOLTIP_OFFSET`)
|
|
|
583
939
|
tooltip={{ text: "Courtyard gallery", image: "/hotspots/gallery-thumb.webp" }}
|
|
584
940
|
tooltipPlacement="top"
|
|
585
941
|
tooltipOffset={16}
|
|
942
|
+
tooltipAppearance={{
|
|
943
|
+
background: "rgba(12, 28, 36, 0.88)",
|
|
944
|
+
color: "#e7f2f5",
|
|
945
|
+
border: "1px solid rgba(56, 84, 91, 0.9)",
|
|
946
|
+
borderRadius: 10,
|
|
947
|
+
shadow: "0 10px 28px rgba(0, 0, 0, 0.42)",
|
|
948
|
+
padding: 10,
|
|
949
|
+
fontSize: 13,
|
|
950
|
+
}}
|
|
586
951
|
/>
|
|
587
952
|
<GraphicHotspot
|
|
588
953
|
id="marker"
|
|
@@ -628,7 +993,7 @@ are angular degrees, so they remain independent of the canvas resolution.
|
|
|
628
993
|
import { ImageHotspot, PanoViewer, Sphere } from "@ericchen1990/pano-view";
|
|
629
994
|
|
|
630
995
|
<PanoViewer style={{ height: 560 }}>
|
|
631
|
-
<Sphere src="/panoramas/room.webp" />
|
|
996
|
+
<Sphere src="/panoramas/room.webp" previewUrl="preview.webp" />
|
|
632
997
|
<ImageHotspot
|
|
633
998
|
id="gallery"
|
|
634
999
|
ariaLabel="Open gallery"
|
|
@@ -765,6 +1130,8 @@ import { PolygonHotspot } from "@ericchen1990/pano-view";
|
|
|
765
1130
|
stroke="#f5fbfc"
|
|
766
1131
|
strokeWidth={2}
|
|
767
1132
|
strokeOpacity={0.8}
|
|
1133
|
+
strokeDashSize={8}
|
|
1134
|
+
strokeGapSize={4}
|
|
768
1135
|
draggable
|
|
769
1136
|
onVerticesChange={({ vertices }) => setZoneVertices(vertices)}
|
|
770
1137
|
onInvalid={(issues) => console.error(issues)}
|
|
@@ -772,9 +1139,11 @@ import { PolygonHotspot } from "@ericchen1990/pano-view";
|
|
|
772
1139
|
```
|
|
773
1140
|
|
|
774
1141
|
`strokeWidth` is a screen-space CSS pixel value, so it remains visually
|
|
775
|
-
consistent across FOV changes.
|
|
776
|
-
|
|
777
|
-
|
|
1142
|
+
consistent across FOV changes. `strokeDashSize` and `strokeGapSize` use the
|
|
1143
|
+
same CSS-pixel units; omit them or set `strokeDashSize` to `0` for a solid
|
|
1144
|
+
stroke. Dragging moves all vertices together while keeping yaw normalized and
|
|
1145
|
+
pitch clamped. Individual vertex editing is provided by the authoring
|
|
1146
|
+
workflow, not the runtime hotspot component.
|
|
778
1147
|
|
|
779
1148
|
Set `fillOpacity={0}` for a closed outline-only polygon. The fill and outline
|
|
780
1149
|
share the same spherical edge sampling, so there is no intentional seam
|
|
@@ -783,7 +1152,8 @@ between them.
|
|
|
783
1152
|
## PolylineHotspot
|
|
784
1153
|
|
|
785
1154
|
`PolylineHotspot` is an open path of at least two yaw/pitch vertices. It uses
|
|
786
|
-
the same CSS-pixel `strokeWidth`,
|
|
1155
|
+
the same CSS-pixel `strokeWidth`, optional dashed stroke (`strokeDashSize` /
|
|
1156
|
+
`strokeGapSize`), visibility, semantic interaction, controlled
|
|
787
1157
|
whole-path drag, and vertex-change callback as `PolygonHotspot`, but it never
|
|
788
1158
|
connects the final point to the first and has no fill.
|
|
789
1159
|
|
|
@@ -797,6 +1167,8 @@ import { PolylineHotspot } from "@ericchen1990/pano-view";
|
|
|
797
1167
|
stroke="#75cbd3"
|
|
798
1168
|
strokeWidth={2}
|
|
799
1169
|
strokeOpacity={0.9}
|
|
1170
|
+
strokeDashSize={8}
|
|
1171
|
+
strokeGapSize={4}
|
|
800
1172
|
draggable
|
|
801
1173
|
onVerticesChange={({ vertices }) => setRouteVertices(vertices)}
|
|
802
1174
|
/>;
|
|
@@ -880,8 +1252,12 @@ changes, unmounts, and React StrictMode lifecycles deterministic.
|
|
|
880
1252
|
`TextHotspot` rasterizes plain text onto a canvas texture. It does not accept
|
|
881
1253
|
HTML or Markdown. `\n` is a hard line break; `whiteSpace="normal"` (default)
|
|
882
1254
|
wraps words to the hotspot's angular width. Dimensions stay in degrees like
|
|
883
|
-
the other point hotspots.
|
|
884
|
-
|
|
1255
|
+
the other point hotspots. Typography uses the same names as CSS
|
|
1256
|
+
(`fontFamily`, `fontSize`, `fontWeight`, `fontStyle`) but is painted onto the
|
|
1257
|
+
canvas, not applied as DOM style. `fontSize` is canvas pixels (default 96) and
|
|
1258
|
+
does not scale with panel height. `fontWeight` defaults to `600` and
|
|
1259
|
+
`fontStyle` to `"normal"`. `padding` and `borderRadius` remain fractions of
|
|
1260
|
+
the texture's shorter side, so they still follow `width` / `height`.
|
|
885
1261
|
|
|
886
1262
|
```tsx
|
|
887
1263
|
import { TextHotspot } from "@ericchen1990/pano-view";
|
|
@@ -895,18 +1271,22 @@ import { TextHotspot } from "@ericchen1990/pano-view";
|
|
|
895
1271
|
mode="billboard"
|
|
896
1272
|
scaleMode="fixed"
|
|
897
1273
|
text={"Courtyard overlook\nNorth terrace"}
|
|
1274
|
+
fontFamily="system-ui, sans-serif"
|
|
1275
|
+
fontSize={96}
|
|
1276
|
+
fontWeight={600}
|
|
1277
|
+
fontStyle="normal"
|
|
898
1278
|
align="center"
|
|
899
1279
|
verticalAlign="middle"
|
|
900
|
-
fontSize={0.18}
|
|
901
1280
|
color="#f8fafc"
|
|
902
1281
|
background="#111827"
|
|
903
1282
|
backgroundOpacity={0.72}
|
|
904
1283
|
/>;
|
|
905
1284
|
```
|
|
906
1285
|
|
|
907
|
-
Load a custom `fontFamily` on the page before the hotspot paints
|
|
908
|
-
|
|
909
|
-
`onError(error)` match
|
|
1286
|
+
Load a custom `fontFamily` on the page before the hotspot paints. The hotspot
|
|
1287
|
+
waits on `document.fonts.load` when that API exists, then draws with whatever
|
|
1288
|
+
faces the document has. `onLoad(texture)` and `onError(error)` match
|
|
1289
|
+
`ImageHotspot`.
|
|
910
1290
|
|
|
911
1291
|
## IframeHotspot
|
|
912
1292
|
|
|
@@ -932,7 +1312,9 @@ import { IframeHotspot } from "@ericchen1990/pano-view";
|
|
|
932
1312
|
|
|
933
1313
|
`pointerPolicy="hotspot"` (default) sets the iframe to `pointer-events: none`
|
|
934
1314
|
so clicks and drags hit the WebGL plane. `"content"` makes the page
|
|
935
|
-
interactive and blocks dragging through the iframe.
|
|
1315
|
+
interactive and blocks dragging through the iframe. A hotspot
|
|
1316
|
+
`pointerEvents="none"` also forces the iframe overlay not to capture pointer
|
|
1317
|
+
events. `sandbox` defaults to
|
|
936
1318
|
`allow-scripts allow-popups allow-forms` without `allow-same-origin`.
|
|
937
1319
|
`referrerPolicy` defaults to `strict-origin-when-cross-origin`. Prefer a
|
|
938
1320
|
same-origin document; many third-party sites send `X-Frame-Options` and will
|