@ericchen1990/pano-view 3.9.0 → 4.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 +207 -95
- package/README.zh-CN.md +170 -75
- package/dist/data.cjs +64 -17
- package/dist/data.cjs.map +1 -1
- package/dist/data.d.mts +24 -12
- package/dist/data.d.ts +24 -12
- package/dist/data.mjs +64 -17
- package/dist/data.mjs.map +1 -1
- package/dist/index.cjs +1998 -1031
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.mts +162 -48
- package/dist/index.d.ts +162 -48
- package/dist/index.mjs +1789 -822
- package/dist/index.mjs.map +1 -1
- package/dist/styles.css +87 -74
- package/dist/ui.cjs.map +1 -1
- package/dist/ui.d.mts +1 -1
- package/dist/ui.d.ts +1 -1
- package/dist/ui.mjs.map +1 -1
- package/docs/dialogs.md +52 -42
- package/docs/dialogs.zh-CN.md +52 -42
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -230,17 +230,18 @@ Store this content structure in `project.json`; store media as separate files. P
|
|
|
230
230
|
|
|
231
231
|
```tsx
|
|
232
232
|
import { useState } from "react";
|
|
233
|
-
import { PanoViewer, ProjectScenes, type LoadedPanoProject, type ProjectPopupDefinition } from "@ericchen1990/pano-view";
|
|
233
|
+
import { PanoOverlay, PanoViewer, ProjectScenes, type LoadedPanoProject, type ProjectPopupDefinition } from "@ericchen1990/pano-view";
|
|
234
234
|
import { ProjectPopupDialog } from "@ericchen1990/pano-view/ui";
|
|
235
235
|
import "@ericchen1990/pano-view/ui/styles.css";
|
|
236
236
|
|
|
237
237
|
export function Tour({ project }: { project: LoadedPanoProject }) {
|
|
238
238
|
const [popup, setPopup] = useState<ProjectPopupDefinition | null>(null);
|
|
239
|
-
return <PanoViewer
|
|
240
|
-
<ProjectPopupDialog popup={popup} onOpenChange={(open) => { if (!open) setPopup(null); }} />
|
|
241
|
-
)}>
|
|
239
|
+
return <PanoViewer>
|
|
242
240
|
<ProjectScenes project={project} onPopupRequest={({ popup }) => setPopup(popup)}
|
|
243
241
|
onActiveSceneIdChange={() => setPopup(null)} />
|
|
242
|
+
<PanoOverlay>
|
|
243
|
+
<ProjectPopupDialog popup={popup} onOpenChange={(open) => { if (!open) setPopup(null); }} />
|
|
244
|
+
</PanoOverlay>
|
|
244
245
|
</PanoViewer>;
|
|
245
246
|
}
|
|
246
247
|
```
|
|
@@ -258,12 +259,14 @@ An optional project-level `map` places scene spots on one floor image. Projects
|
|
|
258
259
|
"maxScale": 4,
|
|
259
260
|
"iconSize": 28,
|
|
260
261
|
"presentation": {
|
|
261
|
-
"mode": "
|
|
262
|
-
"
|
|
262
|
+
"mode": "panel",
|
|
263
|
+
"anchor": {
|
|
264
|
+
"align": "bottom-left",
|
|
265
|
+
"axis": "horizontal",
|
|
266
|
+
"inset": 16
|
|
267
|
+
},
|
|
263
268
|
"width": 240,
|
|
264
|
-
"height": 180
|
|
265
|
-
"offsetX": 16,
|
|
266
|
-
"offsetY": 16
|
|
269
|
+
"height": 180
|
|
267
270
|
},
|
|
268
271
|
"controls": { "collapse": true, "zoom": true },
|
|
269
272
|
"sector": {
|
|
@@ -289,70 +292,70 @@ An optional project-level `map` places scene spots on one floor image. Projects
|
|
|
289
292
|
|
|
290
293
|
`image` is a project-relative path or an absolute URL. `spots` is required. Spot IDs are unique, and each `sceneId` must name a scene in the project. `x` and `y` are normalized image coordinates in [0, 1], with the origin at the top-left. `heading` is clockwise degrees from image-up when viewer yaw is 0; the drawn sector uses `heading + yaw`. Several spots may share a scene. The sector is drawn on the first spot whose `sceneId` matches the active scene. `icon` is optional; a spot without one uses the built-in marker.
|
|
291
294
|
|
|
292
|
-
`interaction` is `"fixed"` (the default) or `"zoom"`. Fixed mode fits the image in the panel and ignores the wheel, drag-pan, and zoom buttons. Zoom mode allows wheel zoom, and drag-pan once the scale is above the fitted size. Wheel events on the map do not scroll the page. `maxScale` is the maximum multiple of that fitted size, at least 1, default 8. `dragMotion` controls pan inertia and edge bounce while zoomed (both on by default). Set `false` to disable both, or `{ inertia?: boolean; bounce?: boolean }` to toggle either flag. Spot and heading edits stay direct. `controls.zoom` shows the zoom buttons only when interaction is `"zoom"`. `controls.collapse` shows
|
|
295
|
+
`interaction` is `"fixed"` (the default) or `"zoom"`. Fixed mode fits the image in the panel and ignores the wheel, drag-pan, and zoom buttons. Zoom mode allows wheel zoom, and drag-pan once the scale is above the fitted size. Wheel events on the map do not scroll the page. `maxScale` is the maximum multiple of that fitted size, at least 1, default 8. `dragMotion` controls pan inertia and edge bounce while zoomed (both on by default). Set `false` to disable both, or `{ inertia?: boolean; bounce?: boolean }` to toggle either flag. Spot and heading edits stay direct. `controls.zoom` shows the zoom buttons only when interaction is `"zoom"`. `controls.collapse` shows an edge tab that slides a panel off-screen. `iconSize` is the marker edge in CSS pixels, default 28. Icons and the sector keep that screen size while the floor image zooms.
|
|
293
296
|
|
|
294
297
|
`sector` is optional. `angle` is the opening in degrees, greater than 0 and less than 360. `radius` is CSS pixels and does not scale with the image. `followFov` replaces `angle` with the horizontal field of view derived from the viewer FOV and the map panel's aspect ratio. Without `gradient`, the fill is solid `color` (default `#ffffff`) at `opacity` (default 0.55). `gradient.type` is `"radial"` or `"linear"`, with at least two stops. Each stop has `offset` in [0, 1], `color`, and an optional `opacity`. Stop opacities describe only the fade shape. `sector.opacity` scales the whole fill in both solid and gradient mode. `strokeColor`, `strokeWidth`, and `strokeOpacity` draw an outline when the width is greater than 0. The outline width stays in CSS pixels.
|
|
295
298
|
|
|
296
|
-
`presentation.mode` is `"
|
|
299
|
+
`presentation.mode` is `"panel"` (the default) or `"dialog"`. Both accept `anchor`, plus `width` and `height`. `anchor` is the shared overlay anchor: `align` is `top`, `bottom`, `left`, `right`, or one of the four corners (default `bottom-left`); `axis` is `"horizontal"` or `"vertical"` and applies only to corners (default `"horizontal"`); `inset` is a number, a CSS length string, or `{ top, right, bottom, left }` (default `16`). Safe-area insets are added on top. A panel defaults to 220×160. Dialog mode places a launcher (48×48 when width and height are omitted) and opens the same map in a viewer-local dialog. Choosing a spot closes that dialog. The chrome is positioned on the full viewer overlay, so scrolling a parent page does not move or resize it. Edges center a sized box on that edge. Corners pin to both adjacent edges. Collapsing keeps the panel size and slides it off the outer edge: edges slide toward that edge, and corners slide vertically when `axis` is horizontal or horizontally when `axis` is vertical. The tab stays on the inner side of that edge.
|
|
297
300
|
|
|
298
|
-
The document stores saved defaults (`image`, `spots`, `sector`, `presentation`, `interaction`, `maxScale`, `dragMotion`, `controls`, `iconSize`). Mount-time props on `PanoMapOverlay` override presentation and zoom for that render without cloning the map. Priority is built-in defaults → map document → `presentationOverride` (same shape as `map.presentation`) → individual props. Spot geometry and sector styling stay in the document. `selectedSpotId` / `onSelectedSpotIdChange` are forwarded to the inner `PanoMap`.
|
|
301
|
+
The document stores saved defaults (`image`, `spots`, `sector`, `presentation`, `interaction`, `maxScale`, `dragMotion`, `controls`, `iconSize`). Mount-time props on `PanoMapOverlay` override presentation and zoom for that render without cloning the map. Priority is built-in defaults → map document → `presentationOverride` (same shape as `map.presentation`) → individual props. Spot geometry and sector styling stay in the document. `selectedSpotId` / `onSelectedSpotIdChange` are forwarded to the inner `PanoMap`. Panel collapse state is owned by the overlay; dialog mode and `editorSurface` force the edge tab off. `PanoMap`'s own `collapsed` prop still hides the surface inside a host-owned frame.
|
|
299
302
|
|
|
300
303
|
#### Mounting `PanoMapOverlay`
|
|
301
304
|
|
|
302
|
-
Mount it
|
|
305
|
+
Mount it inside `PanoOverlay` and pass the same `activeSceneId` you pass to `ProjectScenes`. Import `@ericchen1990/pano-view/styles.css` for the built-in `pano-map-*` classes. The overlay reads yaw and FOV from the viewer. It does not change scenes by itself: set the active scene from `onSelectScene`. Pass `view` when the camera state comes from outside the viewer; outside the viewer with no `view`, the surface stays at yaw `0`, pitch `0`, fov `75`. `PanoMap` is the surface alone, for a host-owned frame.
|
|
303
306
|
|
|
304
|
-
`PanoMap` also accepts controlled selection and collapse: `selectedSpotId` / `onSelectedSpotIdChange` (internal selection defaults to `null`); `collapsed` / `onCollapsedChange` (uncontrolled starts expanded); `renderChrome` to draw host controls above the surface. `
|
|
307
|
+
`PanoMap` also accepts controlled selection and collapse: `selectedSpotId` / `onSelectedSpotIdChange` (internal selection defaults to `null`); `collapsed` / `onCollapsedChange` (uncontrolled starts expanded and hides the surface inside the host frame); `renderChrome` to draw host controls above the surface. On `PanoMapOverlay`, `showCollapseControl` shows the shared edge tab instead. `className` / `style` attach to the surface root.
|
|
305
308
|
|
|
306
309
|
```tsx
|
|
307
310
|
import { useState } from "react";
|
|
308
311
|
import {
|
|
309
|
-
PanoViewer, ProjectScenes, PanoMapOverlay, type LoadedPanoProject,
|
|
312
|
+
PanoOverlay, PanoViewer, ProjectScenes, PanoMapOverlay, type LoadedPanoProject,
|
|
310
313
|
} from "@ericchen1990/pano-view";
|
|
311
314
|
import "@ericchen1990/pano-view/styles.css";
|
|
312
315
|
|
|
313
316
|
export function Tour({ project }: { project: LoadedPanoProject }) {
|
|
314
317
|
const [activeSceneId, setActiveSceneId] = useState(project.initialSceneId);
|
|
315
318
|
const map = project.map;
|
|
316
|
-
return <PanoViewer style={{ height: 600 }}
|
|
317
|
-
<PanoMapOverlay
|
|
318
|
-
map={map}
|
|
319
|
-
activeSceneId={activeSceneId}
|
|
320
|
-
onSelectScene={setActiveSceneId}
|
|
321
|
-
/>
|
|
322
|
-
) : undefined}>
|
|
319
|
+
return <PanoViewer style={{ height: 600 }}>
|
|
323
320
|
<ProjectScenes
|
|
324
321
|
project={project}
|
|
325
322
|
activeSceneId={activeSceneId}
|
|
326
323
|
onActiveSceneIdChange={setActiveSceneId}
|
|
327
324
|
/>
|
|
325
|
+
{map ? (
|
|
326
|
+
<PanoOverlay>
|
|
327
|
+
<PanoMapOverlay
|
|
328
|
+
map={map}
|
|
329
|
+
activeSceneId={activeSceneId}
|
|
330
|
+
onSelectScene={setActiveSceneId}
|
|
331
|
+
/>
|
|
332
|
+
</PanoOverlay>
|
|
333
|
+
) : null}
|
|
328
334
|
</PanoViewer>;
|
|
329
335
|
}
|
|
330
336
|
```
|
|
331
337
|
|
|
332
|
-
#### Size,
|
|
338
|
+
#### Size, align, and inset
|
|
333
339
|
|
|
334
|
-
- `presentation` — `"
|
|
335
|
-
- `
|
|
336
|
-
- `width` / `height` —
|
|
337
|
-
- `offsetX` / `offsetY` — inset from the chosen corner in CSS pixels (default 16).
|
|
340
|
+
- `presentation` — `"panel"` | `"dialog"` (defaults from the document, otherwise `"panel"`).
|
|
341
|
+
- `anchor` — shared `PanoOverlayAnchor`. `align` is one of eight positions (default `"bottom-left"`). `axis` is used for corners. `inset` defaults to `16`.
|
|
342
|
+
- `width` / `height` — panel size, or the dialog launcher size, in CSS pixels. Panel defaults to 220×160; the dialog launcher defaults to 48×48.
|
|
338
343
|
|
|
339
344
|
```tsx
|
|
340
345
|
<PanoMapOverlay
|
|
341
346
|
map={map}
|
|
342
347
|
activeSceneId={activeSceneId}
|
|
343
348
|
onSelectScene={setActiveSceneId}
|
|
344
|
-
presentation="
|
|
345
|
-
|
|
349
|
+
presentation="panel"
|
|
350
|
+
anchor={{ align: "bottom-right", axis: "vertical", inset: 20 }}
|
|
346
351
|
width={280}
|
|
347
352
|
height={200}
|
|
348
|
-
offsetX={20}
|
|
349
|
-
offsetY={20}
|
|
350
353
|
/>
|
|
351
354
|
```
|
|
352
355
|
|
|
353
356
|
#### Dialog mode
|
|
354
357
|
|
|
355
|
-
Dialog mode
|
|
358
|
+
Dialog mode places a launcher button and opens the map in a viewer-local modal. Launcher size uses `width` / `height`, and the launcher uses the same `anchor`. The dialog body uses a separate pair:
|
|
356
359
|
|
|
357
360
|
- `dialogWidth` / `dialogHeight` — preferred dialog size in CSS pixels (default 720×520). Both are clamped to the viewer with a 16 px margin on each side.
|
|
358
361
|
- `openLabel` / `closeLabel` / `title` — launcher accessible name, close button label, and dialog heading (defaults `"Map"`, `"Close"`, `"Map"`).
|
|
@@ -365,7 +368,7 @@ Choosing a spot closes the dialog.
|
|
|
365
368
|
activeSceneId={activeSceneId}
|
|
366
369
|
onSelectScene={setActiveSceneId}
|
|
367
370
|
presentation="dialog"
|
|
368
|
-
|
|
371
|
+
anchor={{ align: "bottom-left", inset: 16 }}
|
|
369
372
|
width={52}
|
|
370
373
|
height={52}
|
|
371
374
|
dialogWidth={640}
|
|
@@ -401,6 +404,36 @@ These props override the map document for the current mount:
|
|
|
401
404
|
/>
|
|
402
405
|
```
|
|
403
406
|
|
|
407
|
+
`toggle` replaces the panel edge-tab icon or restyles that button. Dialog mode has no edge tab. Omit `toggle` to keep the default chevron. Pass an element (`<Icon />`) for `icon`, not a component type. When the glyph depends on the collapsed state, pass a function of `{ collapsed }`. `className` is appended to the button. `style` overrides background, border, color, and size. Position and `transform` belong to the edge layout; do not set them in `style`. The default chevron uses `currentColor`, so `style.color` recolors it without replacing the icon.
|
|
408
|
+
|
|
409
|
+
```tsx
|
|
410
|
+
<PanoMapOverlay
|
|
411
|
+
map={map}
|
|
412
|
+
activeSceneId={activeSceneId}
|
|
413
|
+
onSelectScene={setActiveSceneId}
|
|
414
|
+
showCollapseControl
|
|
415
|
+
toggle={{ style: { color: "#7dd3fc" } }}
|
|
416
|
+
/>
|
|
417
|
+
```
|
|
418
|
+
|
|
419
|
+
```tsx
|
|
420
|
+
<PanoMapOverlay
|
|
421
|
+
map={map}
|
|
422
|
+
activeSceneId={activeSceneId}
|
|
423
|
+
onSelectScene={setActiveSceneId}
|
|
424
|
+
showCollapseControl
|
|
425
|
+
toggle={{
|
|
426
|
+
className: "map-tab",
|
|
427
|
+
style: {
|
|
428
|
+
background: "#0c4a6e",
|
|
429
|
+
color: "#7dd3fc",
|
|
430
|
+
border: "1px solid #7dd3fc",
|
|
431
|
+
},
|
|
432
|
+
icon: ({ collapsed }) => (collapsed ? "+" : "–"),
|
|
433
|
+
}}
|
|
434
|
+
/>
|
|
435
|
+
```
|
|
436
|
+
|
|
404
437
|
#### Edit mode
|
|
405
438
|
|
|
406
439
|
`mode="edit"` is the authoring surface: with `placementSceneId` set, a click adds a spot whose heading is the negation of the current yaw, so the sector points image-up at that yaw; dragging a spot moves it, and dragging the sector sets `heading`. `onEditChange` reports `{ spots?, selectedSpotId? }`. Visitor playback stays on the default `mode="view"`. `editorSurface` enlarges the panel for authoring; pass `width` / `height` to size that surface, otherwise it scales with the viewer.
|
|
@@ -679,14 +712,15 @@ Presets and hotspot rendering are in [Scenes](#scenes).
|
|
|
679
712
|
### Events and chrome
|
|
680
713
|
|
|
681
714
|
- [`PanoEvents`](#panoevents) — viewer-level lifecycle and interaction callbacks
|
|
715
|
+
- [`PanoOverlay`](#dom-overlays-and-dialogs) — screen-plane DOM wrapper for fullscreen-safe UI
|
|
682
716
|
- [`PanoContextMenu`](#panocontextmenu) — right-click reset / fullscreen menu
|
|
683
|
-
- [`PanoSceneMenu`](#panoscenemenu) — scrollable thumbnail scene picker for `
|
|
717
|
+
- [`PanoSceneMenu`](#panoscenemenu) — scrollable thumbnail scene picker for `PanoOverlay`
|
|
684
718
|
- [`PanoVideoControls`](#panovideo) — playback chrome for `PanoVideo` (mounted by default)
|
|
685
719
|
|
|
686
720
|
### Floor plan
|
|
687
721
|
|
|
688
722
|
- [`PanoMap`](#floor-plan-maps) — floor image, scene spots, and the yaw-linked sector
|
|
689
|
-
- [`PanoMapOverlay`](#floor-plan-maps) —
|
|
723
|
+
- [`PanoMapOverlay`](#floor-plan-maps) — panel or dialog launcher for `PanoOverlay`
|
|
690
724
|
|
|
691
725
|
### Hotspots
|
|
692
726
|
|
|
@@ -711,7 +745,25 @@ Presets and hotspot rendering are in [Scenes](#scenes).
|
|
|
711
745
|
|
|
712
746
|
## DOM overlays and dialogs
|
|
713
747
|
|
|
714
|
-
|
|
748
|
+
Prefer `PanoOverlay` for ordinary DOM UI outside the Three.js tree. It mounts on the viewer plane inside the fullscreen root, so chrome stays visible when the viewer goes fullscreen. Place it as a direct child of `PanoViewer` (or inside a Fragment under it) beside scene content. `renderOverlay` remains supported for callback-style mounts.
|
|
749
|
+
|
|
750
|
+
```tsx
|
|
751
|
+
<PanoViewer style={{ height: 560 }}>
|
|
752
|
+
<Sphere src="/pano.jpg" />
|
|
753
|
+
<PanoOverlay>
|
|
754
|
+
<PanoSceneMenu items={items} activeId={id} onSelect={setId} />
|
|
755
|
+
<button
|
|
756
|
+
type="button"
|
|
757
|
+
style={{ position: "absolute", right: 16, bottom: 16, pointerEvents: "auto" }}
|
|
758
|
+
onClick={() => void viewer.toggleFullscreen()}
|
|
759
|
+
>
|
|
760
|
+
Fullscreen
|
|
761
|
+
</button>
|
|
762
|
+
</PanoOverlay>
|
|
763
|
+
</PanoViewer>
|
|
764
|
+
```
|
|
765
|
+
|
|
766
|
+
The overlay layer is `position: absolute; inset: 0; pointer-events: none`. Position children yourself and set `pointerEvents: "auto"` on interactive nodes. When both are used, `renderOverlay` renders first and each `PanoOverlay` follows in document order. Call `usePanoOverlay()` inside `PanoOverlay` (or `renderOverlay`) to get `portalContainer`, `size` (viewer width/height), `visibleRect`, and `isFullscreen`. `usePanoModal(open)` holds that viewer's modal lock while `open === true` (pauses input and auto-rotate, marks the backdrop inert, closes the context menu); it does not trap focus. `PanoOverlayPortalProvider` takes a required `container` and `children` and only remaps `portalContainer` for descendants; `size` / `visibleRect` still describe the whole viewer. Field meanings, reference counting, and third-party Dialog wiring live in the separate **[Dialog and styling guide](./docs/dialogs.md)**. The optional `/ui` entry provides `PanoDialog` and composable parts. The guide ships in the npm package; run `pnpm dev` and open `/overlays` for local examples.
|
|
715
767
|
|
|
716
768
|
### Default dialog
|
|
717
769
|
|
|
@@ -798,7 +850,7 @@ which use their spherical center. Hotspot ids must be unique within a viewer.
|
|
|
798
850
|
|
|
799
851
|
**Scenes** (requires `<Scenes />`) — `setScene(id)`, `nextScene`, `previousScene`, `getActiveSceneId`, `getSceneIds`, `isSceneTransitioning`. `Scenes` accepts `defaultActiveSceneId` (uncontrolled) or `activeSceneId` (controlled). In controlled mode imperative calls invoke `onActiveSceneIdChange`.
|
|
800
852
|
|
|
801
|
-
**WebVR** (requires `<WebVR />`) — `enterVR`, `exitVR`, `toggleVR`, `isVRAvailable`, `isVREnabled`, `getVRMode`, `requestVRPermission`.
|
|
853
|
+
**WebVR** (requires `<WebVR />`) — `enterVR`, `exitVR`, `toggleVR`, `isVRAvailable`, `isVREnabled`, `getVRMode`, `getVRState`, `subscribeVR`, `requestVRPermission`. Use `subscribeVR` with `useSyncExternalStore` for reactive Enter / Exit buttons; the built-in chrome no longer renders an Enter VR control.
|
|
802
854
|
|
|
803
855
|
**Video** (requires `<PanoVideo />`) — `getVideo` returns the `PanoVideoController`, `subscribeVideo` subscribes to video controller and playback-state changes, plus convenience aliases `playVideo`, `pauseVideo`, `toggleVideo`, `seekVideo`, `setVideoVolume`, `setVideoMuted`, `toggleVideoMuted`.
|
|
804
856
|
|
|
@@ -814,7 +866,7 @@ which use their spherical center. Hotspot ids must be unique within a viewer.
|
|
|
814
866
|
- `cursors` — `true` / omit uses grab / grabbing / pointer / move; `false` leaves the canvas cursor unchanged; an object overrides those four states.
|
|
815
867
|
- `contextMenu` — `true` / omit mounts Reset view + Fullscreen; `false` skips the default menu; an object can change appearance or `items` / `prepend` / `append`. See [PanoContextMenu](#panocontextmenu).
|
|
816
868
|
- `mediaActivation` — enabled by default when a mounted child intends to play; `false` disables automatic activation.
|
|
817
|
-
- `renderOverlay` — DOM UI outside the Three.js tree and inside fullscreen; the callback receives `portalContainer`, `size`, `visibleRect`, and `isFullscreen`.
|
|
869
|
+
- `renderOverlay` — optional callback-style DOM UI outside the Three.js tree and inside fullscreen; the callback receives `portalContainer`, `size`, `visibleRect`, and `isFullscreen`. Prefer `<PanoOverlay>` as a child.
|
|
818
870
|
- `onViewChange` — `{ yaw, pitch, fov }` in degrees.
|
|
819
871
|
- `onPanoramaClick` / `onPanoramaDoubleClick` / `onPanoramaPointerMove` — panorama pointer events with `position` (yaw / pitch), `nativeEvent`, `button`, `buttons`, `clientX`, `clientY`, and optional `pointerId`.
|
|
820
872
|
- Other standard `div` props (`className`, `style`, `id`, …) are accepted; conflicting `onChange` and `contextMenu` are omitted from the type.
|
|
@@ -913,25 +965,29 @@ function SceneExample() {
|
|
|
913
965
|
### WebVR
|
|
914
966
|
|
|
915
967
|
`enterVR` and `requestVRPermission` must be called from a user gesture.
|
|
968
|
+
Drive entry from host UI with `getVRState` / `subscribeVR`; session chrome only
|
|
969
|
+
shows Exit VR and MobileVR Setup (which fade after a few idle seconds).
|
|
916
970
|
|
|
917
971
|
```tsx
|
|
972
|
+
import { useSyncExternalStore } from "react";
|
|
918
973
|
import { PanoViewer, Sphere, WebVR, usePanoViewer } from "@ericchen1990/pano-view";
|
|
919
974
|
|
|
920
975
|
function VRExample() {
|
|
921
976
|
const viewer = usePanoViewer();
|
|
977
|
+
const vr = useSyncExternalStore(
|
|
978
|
+
viewer.subscribeVR,
|
|
979
|
+
viewer.getVRState,
|
|
980
|
+
viewer.getVRState,
|
|
981
|
+
);
|
|
922
982
|
|
|
923
983
|
return (
|
|
924
984
|
<>
|
|
925
985
|
<button
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
await viewer.enterVR();
|
|
929
|
-
}}
|
|
986
|
+
disabled={!vr.available}
|
|
987
|
+
onClick={() => void (vr.enabled ? viewer.exitVR() : viewer.enterVR())}
|
|
930
988
|
>
|
|
931
|
-
Enter VR
|
|
989
|
+
{vr.enabled ? "Exit VR" : "Enter VR"}
|
|
932
990
|
</button>
|
|
933
|
-
<button onClick={() => void viewer.exitVR()}>Exit VR</button>
|
|
934
|
-
<button onClick={() => console.log(viewer.getVRMode())}>Mode?</button>
|
|
935
991
|
<PanoViewer ref={viewer.ref} style={{ height: 560 }}>
|
|
936
992
|
<WebVR />
|
|
937
993
|
<Sphere src="/panoramas/room.webp" previewUrl="preview.webp" />
|
|
@@ -1470,8 +1526,8 @@ Scene `viewLimits` are covered in the FAQ; `yawOffset` matches the source compon
|
|
|
1470
1526
|
## PanoSceneMenu
|
|
1471
1527
|
|
|
1472
1528
|
`PanoSceneMenu` is a scrollable thumbnail strip for switching scenes. Mount it
|
|
1473
|
-
|
|
1474
|
-
|
|
1529
|
+
inside `PanoOverlay` so it stays inside the viewer (including fullscreen).
|
|
1530
|
+
Import `@ericchen1990/pano-view/styles.css` for the default layout.
|
|
1475
1531
|
|
|
1476
1532
|
The menu is host-driven: you pass `items`, `activeId`, and `onSelect`. It does
|
|
1477
1533
|
not read `Scenes` state or scene `previewUrl`. Cube `previewUrl` values are
|
|
@@ -1485,6 +1541,7 @@ Keep one controlled scene id and share it with `Scenes`:
|
|
|
1485
1541
|
```tsx
|
|
1486
1542
|
import { useState } from "react";
|
|
1487
1543
|
import {
|
|
1544
|
+
PanoOverlay,
|
|
1488
1545
|
PanoSceneMenu,
|
|
1489
1546
|
PanoViewer,
|
|
1490
1547
|
Scenes,
|
|
@@ -1509,21 +1566,19 @@ export function Tour() {
|
|
|
1509
1566
|
const [activeSceneId, setActiveSceneId] = useState("lobby");
|
|
1510
1567
|
|
|
1511
1568
|
return (
|
|
1512
|
-
<PanoViewer
|
|
1513
|
-
style={{ height: 560 }}
|
|
1514
|
-
renderOverlay={() => (
|
|
1515
|
-
<PanoSceneMenu
|
|
1516
|
-
activeId={activeSceneId}
|
|
1517
|
-
items={menuItems}
|
|
1518
|
-
onSelect={setActiveSceneId}
|
|
1519
|
-
/>
|
|
1520
|
-
)}
|
|
1521
|
-
>
|
|
1569
|
+
<PanoViewer style={{ height: 560 }}>
|
|
1522
1570
|
<Scenes
|
|
1523
1571
|
scenes={scenes}
|
|
1524
1572
|
activeSceneId={activeSceneId}
|
|
1525
1573
|
onActiveSceneIdChange={setActiveSceneId}
|
|
1526
1574
|
/>
|
|
1575
|
+
<PanoOverlay>
|
|
1576
|
+
<PanoSceneMenu
|
|
1577
|
+
activeId={activeSceneId}
|
|
1578
|
+
items={menuItems}
|
|
1579
|
+
onSelect={setActiveSceneId}
|
|
1580
|
+
/>
|
|
1581
|
+
</PanoOverlay>
|
|
1527
1582
|
</PanoViewer>
|
|
1528
1583
|
);
|
|
1529
1584
|
}
|
|
@@ -1535,18 +1590,21 @@ active item gets a selection ring above the thumbnail and `data-active="true"`.
|
|
|
1535
1590
|
Scene names sit on a dark translucent bar at the bottom of each thumbnail;
|
|
1536
1591
|
use `appearance.labelAlign` for `"center"` or `"end"` alignment.
|
|
1537
1592
|
|
|
1538
|
-
###
|
|
1593
|
+
### Align, inset, and size
|
|
1594
|
+
|
|
1595
|
+
`anchor` is the shared `PanoOverlayAnchor` used by the scene menu and the floor-plan map.
|
|
1539
1596
|
|
|
1540
|
-
- `
|
|
1541
|
-
Top
|
|
1542
|
-
|
|
1543
|
-
|
|
1544
|
-
|
|
1545
|
-
- `inset` — gap from the viewer edges (default `12`). A number is pixels; a
|
|
1597
|
+
- `anchor.align` — `"top"` | `"bottom"` | `"left"` | `"right"` | `"top-left"` | `"top-right"` | `"bottom-left"` | `"bottom-right"` (default `"bottom"`).
|
|
1598
|
+
Top and bottom are horizontal strips. Left and right are vertical strips.
|
|
1599
|
+
A corner uses `anchor.axis`.
|
|
1600
|
+
- `anchor.axis` — `"horizontal"` | `"vertical"`. Edges ignore it. Corners default to `"horizontal"`: a horizontal strip slides up or down, and a vertical strip slides left or right.
|
|
1601
|
+
- `anchor.inset` — gap from the viewer edges (default `12`). A number is pixels; a
|
|
1546
1602
|
string keeps units such as `"1rem"`; an object sets `top` / `right` /
|
|
1547
|
-
`bottom` / `left` independently.
|
|
1603
|
+
`bottom` / `left` independently. Safe-area insets are added.
|
|
1548
1604
|
- `width` / `height` — menu size. A number is pixels; a string is used as CSS as-is,
|
|
1549
1605
|
including `%`, `rem`, `calc()`, `min()`, `max()`, and `clamp()`.
|
|
1606
|
+
On an edge, a set main-axis size is centered and a missing one stretches between the cross-axis insets. A corner pins to both edges and shrinks to content when a size is omitted.
|
|
1607
|
+
Short lists pack toward the corner, or stay centered on an edge.
|
|
1550
1608
|
- `dragMotion` — scroll inertia and edge bounce. Omit for both on (custom pointer
|
|
1551
1609
|
scrolling, including desktop drag). Set `false` for native overflow scroll.
|
|
1552
1610
|
Pass `{ inertia?: boolean; bounce?: boolean }` to toggle either flag. A short
|
|
@@ -1554,9 +1612,7 @@ use `appearance.labelAlign` for `"center"` or `"end"` alignment.
|
|
|
1554
1612
|
|
|
1555
1613
|
```tsx
|
|
1556
1614
|
<PanoSceneMenu
|
|
1557
|
-
|
|
1558
|
-
align="center"
|
|
1559
|
-
inset={16}
|
|
1615
|
+
anchor={{ align: "bottom", inset: 16 }}
|
|
1560
1616
|
width="min(720px, 100%)"
|
|
1561
1617
|
items={menuItems}
|
|
1562
1618
|
activeId={activeSceneId}
|
|
@@ -1564,9 +1620,11 @@ use `appearance.labelAlign` for `"center"` or `"end"` alignment.
|
|
|
1564
1620
|
/>
|
|
1565
1621
|
|
|
1566
1622
|
<PanoSceneMenu
|
|
1567
|
-
|
|
1568
|
-
|
|
1569
|
-
|
|
1623
|
+
anchor={{
|
|
1624
|
+
align: "top-left",
|
|
1625
|
+
axis: "vertical",
|
|
1626
|
+
inset: { top: 24, right: 12, bottom: 24, left: 12 },
|
|
1627
|
+
}}
|
|
1570
1628
|
height="70%"
|
|
1571
1629
|
width={160}
|
|
1572
1630
|
items={menuItems}
|
|
@@ -1575,8 +1633,8 @@ use `appearance.labelAlign` for `"center"` or `"end"` alignment.
|
|
|
1575
1633
|
/>
|
|
1576
1634
|
```
|
|
1577
1635
|
|
|
1578
|
-
If you omit `width` on a top
|
|
1579
|
-
and right insets. If you omit `height` on a left
|
|
1636
|
+
If you omit `width` on a top or bottom menu, the strip stretches between the left
|
|
1637
|
+
and right insets. If you omit `height` on a left or right menu, it stretches between
|
|
1580
1638
|
the top and bottom insets. The menu also applies a max size that subtracts
|
|
1581
1639
|
`inset` and safe-area insets, so `100%` still stays inside the viewer.
|
|
1582
1640
|
|
|
@@ -1585,9 +1643,8 @@ Use `style` when you need one-off positioning overrides on top of these props.
|
|
|
1585
1643
|
### Collapsible menu and arrows
|
|
1586
1644
|
|
|
1587
1645
|
By default the menu stays open. Set `collapsible` to show a toggle centered on
|
|
1588
|
-
the inner
|
|
1589
|
-
|
|
1590
|
-
edge and leaves only the expand button on the safe-area edge:
|
|
1646
|
+
the inner side of the collapse edge. Collapsing slides the list off-screen by
|
|
1647
|
+
its size plus that edge's inset, and leaves the expand button on the viewer edge:
|
|
1591
1648
|
|
|
1592
1649
|
```tsx
|
|
1593
1650
|
<PanoSceneMenu
|
|
@@ -1623,7 +1680,7 @@ via `label`. When collapsible, `defaultExpanded` defaults to `true`.
|
|
|
1623
1680
|
showArrows
|
|
1624
1681
|
loop
|
|
1625
1682
|
collapsible
|
|
1626
|
-
|
|
1683
|
+
anchor={{ align: "bottom" }}
|
|
1627
1684
|
width="min(720px, 100%)"
|
|
1628
1685
|
items={menuItems}
|
|
1629
1686
|
activeId={activeSceneId}
|
|
@@ -1677,6 +1734,43 @@ The root also exposes `data-placement`, `data-align`, `data-orientation`
|
|
|
1677
1734
|
(`horizontal` | `vertical`), `data-expanded`, and `data-collapsible`. The active
|
|
1678
1735
|
item has `data-active="true"`.
|
|
1679
1736
|
|
|
1737
|
+
When `collapsible` is set, `toggle` replaces the edge-tab icon or restyles that
|
|
1738
|
+
button. Omit `toggle` to keep the default chevron. Pass an element (`<Icon />`)
|
|
1739
|
+
for `icon`, not a component type. When the glyph depends on the collapsed
|
|
1740
|
+
state, pass a function of `{ collapsed }` (`collapsed` is `false` while the
|
|
1741
|
+
menu is expanded). `className` is appended to the button. `style` overrides
|
|
1742
|
+
background, border, color, and size. Position and `transform` belong to the
|
|
1743
|
+
edge layout; do not set them in `style`. The default chevron uses
|
|
1744
|
+
`currentColor`, so `style.color` recolors it without replacing the icon.
|
|
1745
|
+
|
|
1746
|
+
```tsx
|
|
1747
|
+
<PanoSceneMenu
|
|
1748
|
+
collapsible
|
|
1749
|
+
toggle={{ style: { color: "#7dd3fc" } }}
|
|
1750
|
+
items={menuItems}
|
|
1751
|
+
activeId={activeSceneId}
|
|
1752
|
+
onSelect={setActiveSceneId}
|
|
1753
|
+
/>
|
|
1754
|
+
```
|
|
1755
|
+
|
|
1756
|
+
```tsx
|
|
1757
|
+
<PanoSceneMenu
|
|
1758
|
+
collapsible
|
|
1759
|
+
toggle={{
|
|
1760
|
+
className: "scene-tab",
|
|
1761
|
+
style: {
|
|
1762
|
+
background: "#0c4a6e",
|
|
1763
|
+
color: "#7dd3fc",
|
|
1764
|
+
border: "1px solid #7dd3fc",
|
|
1765
|
+
},
|
|
1766
|
+
icon: ({ collapsed }) => (collapsed ? "+" : "–"),
|
|
1767
|
+
}}
|
|
1768
|
+
items={menuItems}
|
|
1769
|
+
activeId={activeSceneId}
|
|
1770
|
+
onSelect={setActiveSceneId}
|
|
1771
|
+
/>
|
|
1772
|
+
```
|
|
1773
|
+
|
|
1680
1774
|
## PanoFilter
|
|
1681
1775
|
|
|
1682
1776
|
Render `PanoFilter` inside `PanoViewer` to color-grade or stylize the panorama
|
|
@@ -1908,8 +2002,9 @@ These are props on `Gyro` itself. `PanoEvents` `onGyro*` callbacks subscribe to
|
|
|
1908
2002
|
`WebVR` adds immersive viewing to the nearest `PanoViewer`. It prefers an
|
|
1909
2003
|
`immersive-vr` WebXR session on supported headsets. On phones without WebXR it
|
|
1910
2004
|
uses a stereoscopic split-screen view with device orientation, and on desktop
|
|
1911
|
-
it can provide a mouse-driven simulated VR preview.
|
|
1912
|
-
|
|
2005
|
+
it can provide a mouse-driven simulated VR preview. Host apps provide Enter VR
|
|
2006
|
+
through the viewer handle; the built-in chrome only shows Exit VR and MobileVR
|
|
2007
|
+
setup controls while a session is active (they fade after idle interaction).
|
|
1913
2008
|
|
|
1914
2009
|
```tsx
|
|
1915
2010
|
import { useRef } from "react";
|
|
@@ -1939,19 +2034,28 @@ export function WebVRExample() {
|
|
|
1939
2034
|
```
|
|
1940
2035
|
|
|
1941
2036
|
The handle exposes `enterVR()`, `exitVR()`, `toggleVR()`, `isAvailable()`,
|
|
1942
|
-
`isEnabled()`, `getMode()`,
|
|
1943
|
-
|
|
1944
|
-
`"cardboard-
|
|
1945
|
-
`{ fov, ipdMm, k1, k2 }`
|
|
1946
|
-
|
|
1947
|
-
|
|
1948
|
-
|
|
1949
|
-
`
|
|
1950
|
-
|
|
1951
|
-
|
|
1952
|
-
|
|
1953
|
-
|
|
1954
|
-
|
|
2037
|
+
`isEnabled()`, `getMode()`, `getState()`, `subscribe()`, `requestPermission()`,
|
|
2038
|
+
and `resetSettings()`. Modes are `"webxr"`, `"mobilevr"`, and `"fake"`.
|
|
2039
|
+
`profile` accepts `"none"`, `"cardboard-v1"`, `"cardboard-v2"`, `"gear-vr"`,
|
|
2040
|
+
`"daydream"`, or a custom `{ fov, ipdMm, k1, k2, screenToLensMm?, trayDistanceMm? }`
|
|
2041
|
+
lens profile. When the physical screen size is known, MobileVR places each eye's
|
|
2042
|
+
optical axis with the inter-lens distance and optional tray-to-lens height
|
|
2043
|
+
(`trayDistanceMm`; Cardboard V1 uses `35`). `0` or omitted keeps the vertical
|
|
2044
|
+
center. When `screensize` is `"auto"`, MobileVR loads the Cardboard Device
|
|
2045
|
+
Parameter Database (`databaseUrl`, default Google's public DPDB) to estimate
|
|
2046
|
+
diagonal inches; pass `""` or `null` to skip the request. Unknown devices fire
|
|
2047
|
+
`onUnknownDevice` and keep the lens-profile FOV (no tray offset until a size is
|
|
2048
|
+
known). MobileVR setup values are versioned and stored in the current origin's
|
|
2049
|
+
`localStorage`; `resetSettings()` clears that storage and restores prop or
|
|
2050
|
+
built-in defaults (also available from the Setup panel Reset control). In VR, a
|
|
2051
|
+
center reticle shows a crosshair; looking at an interactive hotspot draws a
|
|
2052
|
+
clockwise ring that fills over `cursorDwellMs` (default 1500), then the hotspot
|
|
2053
|
+
click handler runs. Set `cursorDwellMs={0}` to keep the reticle without
|
|
2054
|
+
auto-click. Simulated desktop VR (`fake` mode) requests pointer lock by default
|
|
2055
|
+
(`mousePointerLock`) so the cursor hides and mouse movement looks around; click
|
|
2056
|
+
the view again if the browser releases the lock. Fallback sessions keep the
|
|
2057
|
+
screen awake (`wakelock`) with the Screen Wake Lock API, or a looping hidden
|
|
2058
|
+
video on browsers that lack it.
|
|
1955
2059
|
|
|
1956
2060
|
WebXR requires HTTPS and a user gesture. Mobile sensor access also requires
|
|
1957
2061
|
HTTPS and may require an explicit iOS permission prompt. Embedded viewers need
|
|
@@ -1962,19 +2066,27 @@ interactive through the default WebXR controller rays.
|
|
|
1962
2066
|
|
|
1963
2067
|
### Props
|
|
1964
2068
|
|
|
1965
|
-
- `chrome` — default `true` mounts
|
|
2069
|
+
- `chrome` — default `true` mounts session Exit / Setup UI (no Enter VR button). An appearance object may set `background` (default `rgba(16, 24, 32, 0.78)`), `color` (`#ffffff`), `accent` (`#7dd3fc`), `borderRadius` (`6`), `fontSize` (`14`), and optional `setupTitle`. Session buttons fade after ~3s idle and return on pointer / wheel / keyboard interaction with the viewer.
|
|
1966
2070
|
- `mobileVr` — default `true`.
|
|
1967
2071
|
- `desktopSupport` — default `false` (independent of the same-named `Gyro` prop).
|
|
1968
2072
|
- `fakeSupport` — default `true` for mouse-driven desktop simulation.
|
|
1969
2073
|
- `screensize` — `"auto"` (factory / omitted storage default) or inches; numbers are clamped to **3–20** with one decimal place.
|
|
1970
|
-
- `
|
|
2074
|
+
- `databaseUrl` — Cardboard DPDB URL for `"auto"` screensize. Default `https://storage.googleapis.com/cardboard-dpdb/dpdb.json`. Pass `""` or `null` to disable the online lookup.
|
|
2075
|
+
- `profile` — omitted values use stored settings; factory default is `"cardboard-v1"`. Also accepts `"none"`, `"cardboard-v2"`, `"gear-vr"`, `"daydream"`, or a custom `{ fov, ipdMm, k1, k2, screenToLensMm?, trayDistanceMm?, label?, id? }`. `trayDistanceMm` is millimetres from the headset tray bottom to the lens center; `0` / omitted keeps the vertical optical axis at screen center.
|
|
2076
|
+
- `absolute` — default `false`. Compass-oriented MobileVR heading (no effect in WebXR).
|
|
2077
|
+
- `north` — default `0`. Panorama yaw that represents North when `absolute` is enabled.
|
|
2078
|
+
- `friction` — default `0` (range `0–0.99`). MobileVR gyro smoothing; keep `0` for normal viewing.
|
|
2079
|
+
- `touchSupport` — default `false`. When `true`, allows horizontal touch dragging to correct MobileVR heading (`horizontaloffset`). Disabled by default because headset trays can trigger false touches.
|
|
2080
|
+
- `oversampling` — default `1`, clamped to **0.5–2**. Scales the MobileVR distortion render target only (viewports unchanged). For WebXR, sets the framebuffer scale factor before the session starts (cannot change mid-session).
|
|
2081
|
+
- `foveation` — optional WebXR fixed foveation in **0–1**. Omit to keep the device default; `0` is full resolution, `1` is maximum edge downsampling.
|
|
2082
|
+
- `frameRate` — optional WebXR target rate: `"low" | "mid" | "high"` or a numeric Hz (closest supported rate). Omit to keep the store default (`"high"`).
|
|
1971
2083
|
- `cursor` — default `true`; `false` hides the center reticle.
|
|
1972
2084
|
- `cursorDwellMs` — default `1500`; `0` disables auto-click.
|
|
1973
2085
|
- `wakelock` / `mousePointerLock` — both default to `true`.
|
|
1974
2086
|
- `onAvailable` / `onUnavailable` — VR capability changes.
|
|
1975
2087
|
- `onEnterVR` / `onExitVR` — mode `"webxr" | "mobilevr" | "fake"`.
|
|
1976
2088
|
- `onDenied` — permission or session denial, optional `error`.
|
|
1977
|
-
- `onUnknownDevice` — when
|
|
2089
|
+
- `onUnknownDevice` — when `"auto"` screensize cannot be resolved from the DPDB (disabled URL, no match, or fetch failure).
|
|
1978
2090
|
|
|
1979
2091
|
## Mouse, touch, and keyboard controls
|
|
1980
2092
|
|