@ericchen1990/pano-view 1.0.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 +346 -26
- package/dist/index.cjs +6621 -1226
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.mts +395 -38
- package/dist/index.d.ts +395 -38
- package/dist/index.mjs +6673 -1222
- package/dist/index.mjs.map +1 -1
- package/package.json +3 -1
package/README.md
CHANGED
|
@@ -18,7 +18,25 @@ compatible with that tile output format but is not affiliated with krpano.
|
|
|
18
18
|
npm install @ericchen1990/pano-view
|
|
19
19
|
```
|
|
20
20
|
|
|
21
|
-
React 19, React DOM 19, Three.js, `@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.
|
|
25
|
+
|
|
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:
|
|
30
|
+
|
|
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.
|
|
22
40
|
|
|
23
41
|
## Exported components
|
|
24
42
|
|
|
@@ -26,8 +44,10 @@ React 19, React DOM 19, Three.js, `@react-three/fiber` 9, and `@react-three/drei
|
|
|
26
44
|
|
|
27
45
|
- [`PanoViewer`](#panoviewer) — canvas shell, camera, default controls, and imperative view API
|
|
28
46
|
- [`Sphere`](#sphere) — single 2:1 equirectangular image
|
|
47
|
+
- [`PanoVideo`](#panovideo) — 2:1 equirectangular video on the panorama sphere
|
|
29
48
|
- [`Tile`](#tile) — krpano-style multires cube tiles
|
|
30
49
|
- [`Scenes`](#scenes) — controlled multi-scene transitions
|
|
50
|
+
- [`PanoFilter`](#panofilter) — color and artistic filters on the panorama source
|
|
31
51
|
|
|
32
52
|
### Controls
|
|
33
53
|
|
|
@@ -35,11 +55,14 @@ React 19, React DOM 19, Three.js, `@react-three/fiber` 9, and `@react-three/drei
|
|
|
35
55
|
- [`TouchControls`](#touchcontrols) — one-finger drag and pinch zoom
|
|
36
56
|
- [`KeyboardControls`](#keyboardcontrols) — arrow-key look, FOV, and scene cycling
|
|
37
57
|
- [`AutoRotate`](#autorotate) — automatic yaw rotation
|
|
58
|
+
- [`Gyro`](#gyro) — opt-in device-orientation control
|
|
59
|
+
- [`WebVR`](#webvr) — WebXR-first immersive VR with MobileVR fallback
|
|
38
60
|
|
|
39
61
|
### Events and chrome
|
|
40
62
|
|
|
41
63
|
- [`PanoEvents`](#panoevents) — viewer-level lifecycle and interaction callbacks
|
|
42
64
|
- [`PanoContextMenu`](#panocontextmenu) — right-click reset / fullscreen menu
|
|
65
|
+
- [`PanoVideoControls`](#panovideo) — playback chrome for `PanoVideo` (mounted by default)
|
|
43
66
|
|
|
44
67
|
### Hotspots
|
|
45
68
|
|
|
@@ -142,6 +165,79 @@ export function SphereExample() {
|
|
|
142
165
|
}
|
|
143
166
|
```
|
|
144
167
|
|
|
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";
|
|
198
|
+
|
|
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
|
+
|
|
145
241
|
## Tile
|
|
146
242
|
|
|
147
243
|
`Tile` renders six inward-facing cube faces and loads only tiles around the current view. The default layout matches krpano-style output:
|
|
@@ -241,16 +337,64 @@ const scenes: Scene[] = [
|
|
|
241
337
|
</PanoViewer>;
|
|
242
338
|
```
|
|
243
339
|
|
|
244
|
-
Available presets are `none`, `crossfade`, `zoom`, `blackout`,
|
|
245
|
-
`slideRightToLeft`, `slideTopToBottom`, `slideDiagonal`,
|
|
246
|
-
`verticalOpen`, `horizontalOpen`, and `ellipticZoomOpen`.
|
|
247
|
-
|
|
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.
|
|
248
350
|
|
|
249
351
|
While a transition runs, panorama drag/zoom input is locked and
|
|
250
352
|
`renderHotspots` is hidden. New `activeSceneId` values supersede a target that
|
|
251
353
|
is still being prepared. `maxTextureMemoryMb` and `maxConcurrentTileLoads`
|
|
252
354
|
apply to the whole `Scenes` viewer rather than to each tile scene.
|
|
253
355
|
|
|
356
|
+
## PanoFilter
|
|
357
|
+
|
|
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.
|
|
367
|
+
|
|
368
|
+
```tsx
|
|
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
|
+
|
|
254
398
|
## AutoRotate
|
|
255
399
|
|
|
256
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.
|
|
@@ -262,6 +406,136 @@ Auto-rotation is off by default. Render `AutoRotate` inside `PanoViewer` to enab
|
|
|
262
406
|
</PanoViewer>
|
|
263
407
|
```
|
|
264
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";
|
|
419
|
+
import {
|
|
420
|
+
Gyro,
|
|
421
|
+
PanoViewer,
|
|
422
|
+
Sphere,
|
|
423
|
+
type GyroHandle,
|
|
424
|
+
} from "@ericchen1990/pano-view";
|
|
425
|
+
|
|
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
|
+
};
|
|
438
|
+
|
|
439
|
+
return (
|
|
440
|
+
<>
|
|
441
|
+
<button onClick={() => void toggle()}>
|
|
442
|
+
{enabled ? "Disable gyro" : "Enable gyro"}
|
|
443
|
+
</button>
|
|
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
|
+
/>
|
|
454
|
+
<Sphere src="/panoramas/room.webp" />
|
|
455
|
+
</PanoViewer>
|
|
456
|
+
</>
|
|
457
|
+
);
|
|
458
|
+
}
|
|
459
|
+
```
|
|
460
|
+
|
|
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.
|
|
538
|
+
|
|
265
539
|
## Mouse, touch, and keyboard controls
|
|
266
540
|
|
|
267
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.
|
|
@@ -346,7 +620,7 @@ export function KeyboardExample({ scenes }: { scenes: Scene[] }) {
|
|
|
346
620
|
|
|
347
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).
|
|
348
622
|
|
|
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.
|
|
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.
|
|
350
624
|
|
|
351
625
|
## Panorama coordinate events
|
|
352
626
|
|
|
@@ -429,6 +703,8 @@ Supported callbacks (krpano names in parentheses where applicable):
|
|
|
429
703
|
- Fullscreen: `onEnterFullscreen` / `onExitFullscreen`
|
|
430
704
|
- `onResize` — canvas content-box size via `ResizeObserver`
|
|
431
705
|
- Auto-rotate: `onAutoRotateStart` / `onAutoRotateStop` / `onAutoRotateOneRound`
|
|
706
|
+
- Gyro: `onGyroAvailable` / `onGyroUnavailable` / `onGyroEnable` /
|
|
707
|
+
`onGyroDisable` / `onGyroDenied`
|
|
432
708
|
|
|
433
709
|
`PanoViewer`'s existing `onViewChange` / `onPanoramaClick` / `onPanoramaDoubleClick` /
|
|
434
710
|
`onPanoramaPointerMove` props remain supported and share the same event bus.
|
|
@@ -436,7 +712,7 @@ Supported callbacks (krpano names in parentheses where applicable):
|
|
|
436
712
|
Resource loading and scene blending stay on their owners: use `Sphere` /
|
|
437
713
|
`Tile` `onLoad` / `onError` / `onLoadProgress`, and `Scenes`
|
|
438
714
|
`onTransitionEnd` / `onTransitionError`. This package does not mirror krpano
|
|
439
|
-
xml/VR/
|
|
715
|
+
xml/VR/frame-render events.
|
|
440
716
|
|
|
441
717
|
## PanoContextMenu
|
|
442
718
|
|
|
@@ -548,14 +824,25 @@ For a fully custom DOM menu, set `contextMenu={false}` and render
|
|
|
548
824
|
|
|
549
825
|
## Shared hotspot contract and saved definitions
|
|
550
826
|
|
|
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.
|
|
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.
|
|
556
834
|
Interactive hotspots set the canvas cursor to `pointer` while hovered, or to
|
|
557
835
|
the hotspot's `cursor` prop when that is set.
|
|
558
836
|
|
|
837
|
+
```tsx
|
|
838
|
+
<TextHotspot
|
|
839
|
+
id="caption"
|
|
840
|
+
pointerEvents="none"
|
|
841
|
+
position={{ yaw: 12, pitch: -8 }}
|
|
842
|
+
text="Decorative label"
|
|
843
|
+
/>
|
|
844
|
+
```
|
|
845
|
+
|
|
559
846
|
`onClick` and `onHoverChange` receive a position and input source (`"pointer"`
|
|
560
847
|
or `"keyboard"`). Point hotspots can use `draggable`, `onDragStart`,
|
|
561
848
|
`onPositionChange`, and `onDragEnd`; the host must write the reported position
|
|
@@ -568,11 +855,18 @@ Every hotspot can show a DOM tooltip at its anchor. Pass a string or
|
|
|
568
855
|
on screen. Clicking the hotspot does not toggle it. Optional
|
|
569
856
|
`tooltipTrigger="hover"` shows the bubble on pointer hover or keyboard focus;
|
|
570
857
|
`"click"` pins it until the hotspot is clicked again or the empty panorama is
|
|
571
|
-
clicked. Hover and click triggers need `interactive
|
|
858
|
+
clicked. Hover and click triggers need `interactive` and `pointerEvents`
|
|
859
|
+
other than `"none"`.
|
|
572
860
|
|
|
573
861
|
`tooltipPlacement` is `"top"` (default), `"bottom"`, `"left"`, or `"right"`.
|
|
574
|
-
|
|
575
|
-
screen
|
|
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.
|
|
576
870
|
|
|
577
871
|
```tsx
|
|
578
872
|
<ImageHotspot
|
|
@@ -583,6 +877,15 @@ screen-space gap in CSS pixels (default `12` / `DEFAULT_HOTSPOT_TOOLTIP_OFFSET`)
|
|
|
583
877
|
tooltip={{ text: "Courtyard gallery", image: "/hotspots/gallery-thumb.webp" }}
|
|
584
878
|
tooltipPlacement="top"
|
|
585
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
|
+
}}
|
|
586
889
|
/>
|
|
587
890
|
<GraphicHotspot
|
|
588
891
|
id="marker"
|
|
@@ -765,6 +1068,8 @@ import { PolygonHotspot } from "@ericchen1990/pano-view";
|
|
|
765
1068
|
stroke="#f5fbfc"
|
|
766
1069
|
strokeWidth={2}
|
|
767
1070
|
strokeOpacity={0.8}
|
|
1071
|
+
strokeDashSize={8}
|
|
1072
|
+
strokeGapSize={4}
|
|
768
1073
|
draggable
|
|
769
1074
|
onVerticesChange={({ vertices }) => setZoneVertices(vertices)}
|
|
770
1075
|
onInvalid={(issues) => console.error(issues)}
|
|
@@ -772,9 +1077,11 @@ import { PolygonHotspot } from "@ericchen1990/pano-view";
|
|
|
772
1077
|
```
|
|
773
1078
|
|
|
774
1079
|
`strokeWidth` is a screen-space CSS pixel value, so it remains visually
|
|
775
|
-
consistent across FOV changes.
|
|
776
|
-
|
|
777
|
-
|
|
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.
|
|
778
1085
|
|
|
779
1086
|
Set `fillOpacity={0}` for a closed outline-only polygon. The fill and outline
|
|
780
1087
|
share the same spherical edge sampling, so there is no intentional seam
|
|
@@ -783,7 +1090,8 @@ between them.
|
|
|
783
1090
|
## PolylineHotspot
|
|
784
1091
|
|
|
785
1092
|
`PolylineHotspot` is an open path of at least two yaw/pitch vertices. It uses
|
|
786
|
-
the same CSS-pixel `strokeWidth`,
|
|
1093
|
+
the same CSS-pixel `strokeWidth`, optional dashed stroke (`strokeDashSize` /
|
|
1094
|
+
`strokeGapSize`), visibility, semantic interaction, controlled
|
|
787
1095
|
whole-path drag, and vertex-change callback as `PolygonHotspot`, but it never
|
|
788
1096
|
connects the final point to the first and has no fill.
|
|
789
1097
|
|
|
@@ -797,6 +1105,8 @@ import { PolylineHotspot } from "@ericchen1990/pano-view";
|
|
|
797
1105
|
stroke="#75cbd3"
|
|
798
1106
|
strokeWidth={2}
|
|
799
1107
|
strokeOpacity={0.9}
|
|
1108
|
+
strokeDashSize={8}
|
|
1109
|
+
strokeGapSize={4}
|
|
800
1110
|
draggable
|
|
801
1111
|
onVerticesChange={({ vertices }) => setRouteVertices(vertices)}
|
|
802
1112
|
/>;
|
|
@@ -880,8 +1190,12 @@ changes, unmounts, and React StrictMode lifecycles deterministic.
|
|
|
880
1190
|
`TextHotspot` rasterizes plain text onto a canvas texture. It does not accept
|
|
881
1191
|
HTML or Markdown. `\n` is a hard line break; `whiteSpace="normal"` (default)
|
|
882
1192
|
wraps words to the hotspot's angular width. Dimensions stay in degrees like
|
|
883
|
-
the other point hotspots.
|
|
884
|
-
|
|
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`.
|
|
885
1199
|
|
|
886
1200
|
```tsx
|
|
887
1201
|
import { TextHotspot } from "@ericchen1990/pano-view";
|
|
@@ -895,18 +1209,22 @@ import { TextHotspot } from "@ericchen1990/pano-view";
|
|
|
895
1209
|
mode="billboard"
|
|
896
1210
|
scaleMode="fixed"
|
|
897
1211
|
text={"Courtyard overlook\nNorth terrace"}
|
|
1212
|
+
fontFamily="system-ui, sans-serif"
|
|
1213
|
+
fontSize={96}
|
|
1214
|
+
fontWeight={600}
|
|
1215
|
+
fontStyle="normal"
|
|
898
1216
|
align="center"
|
|
899
1217
|
verticalAlign="middle"
|
|
900
|
-
fontSize={0.18}
|
|
901
1218
|
color="#f8fafc"
|
|
902
1219
|
background="#111827"
|
|
903
1220
|
backgroundOpacity={0.72}
|
|
904
1221
|
/>;
|
|
905
1222
|
```
|
|
906
1223
|
|
|
907
|
-
Load a custom `fontFamily` on the page before the hotspot paints
|
|
908
|
-
|
|
909
|
-
`onError(error)` match
|
|
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`.
|
|
910
1228
|
|
|
911
1229
|
## IframeHotspot
|
|
912
1230
|
|
|
@@ -932,7 +1250,9 @@ import { IframeHotspot } from "@ericchen1990/pano-view";
|
|
|
932
1250
|
|
|
933
1251
|
`pointerPolicy="hotspot"` (default) sets the iframe to `pointer-events: none`
|
|
934
1252
|
so clicks and drags hit the WebGL plane. `"content"` makes the page
|
|
935
|
-
interactive and blocks dragging through the iframe.
|
|
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
|
|
936
1256
|
`allow-scripts allow-popups allow-forms` without `allow-same-origin`.
|
|
937
1257
|
`referrerPolicy` defaults to `strict-origin-when-cross-origin`. Prefer a
|
|
938
1258
|
same-origin document; many third-party sites send `X-Frame-Options` and will
|