@ericchen1990/pano-view 3.8.0 → 3.9.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 CHANGED
@@ -11,6 +11,7 @@ Try the components in the [live playground](https://pano-view-playground.vercel.
11
11
  - [pano-converter](#pano-converter)
12
12
  - [Data sources and choosing an approach](#data-sources-and-choosing-an-approach)
13
13
  - [Static projects and JSON data](#static-projects-and-json-data)
14
+ - [Floor plan maps](#floor-plan-maps)
14
15
  - [FAQ and troubleshooting](#faq-and-troubleshooting)
15
16
  - [krpano-compatible tile output](#krpano-compatible-tile-output)
16
17
  - [Install](#install)
@@ -22,6 +23,7 @@ Try the components in the [live playground](https://pano-view-playground.vercel.
22
23
  - [PanoVideo](#panovideo)
23
24
  - [Tile](#tile)
24
25
  - [Scenes](#scenes)
26
+ - [PanoSceneMenu](#panoscenemenu)
25
27
  - [PanoFilter](#panofilter)
26
28
  - [AutoRotate](#autorotate)
27
29
  - [BackgroundAudio](#backgroundaudio)
@@ -199,7 +201,9 @@ export function Tour() {
199
201
 
200
202
  `ProjectScenes` belongs inside `PanoViewer` and reuses all nine existing hotspot types. A `scene` action navigates on click. Without an action, audio/video/sequence hotspots toggle playback. Media unmounts on leaving a scene and starts from its JSON settings on re-entry; browser autoplay restrictions still apply.
201
203
 
202
- Configured `initialView` values apply at `onSceneReady`, defaulting missing fields to yaw 0, pitch 0, fov 75. With no view configured, the current view is retained. Hotspots hide during transitions. Control selection with `activeSceneId` / `onActiveSceneIdChange`. Call `event.preventDefault()` in `onHotspotClick` to cancel the default action. `onHotspotError` receives the scene, hotspot and original error (media errors may be events containing an `error` field). `renderHotspot(hotspot, context)` returns `undefined` for the default renderer or `null` to hide an item. Context includes `sceneId`, `playing`, `onClick`, `togglePlayback`, `reportError` and `renderDefault`.
204
+ Configured `initialView` values apply at `onSceneReady`, defaulting missing fields to yaw 0, pitch 0, fov 75. With no view configured, the current view is retained. Hotspots hide during transitions. Control selection with `activeSceneId` / `onActiveSceneIdChange`. When uncontrolled, internal state starts at `project.initialSceneId`; a new `project` reference resets that selection. Call `event.preventDefault()` in `onHotspotClick` to cancel the default action. `onHotspotError` receives `sceneId`, the hotspot, and the original error (media errors may be events containing an `error` field). `renderHotspot(hotspot, context)` returns `undefined` for the default renderer or `null` to hide an item. Context includes `sceneId`, `playing`, `onClick`, `onHoverChange`, `togglePlayback`, `reportError`, and `renderDefault`.
205
+
206
+ Beyond project-specific props, `ProjectScenes` accepts the `Scenes` transition and cache props: `transition`, `onSceneReady`, `transitionWaitForDetailMs`, `onTransitionEnd`, `onTransitionError`, `maxTextureMemoryMb`, `maxConcurrentTileLoads`, and `snapshotMaxPixels` (default `3686400`). It does not accept `scenes`, `defaultActiveSceneId`, or a host `renderHotspots` — the scene list comes from `project`, and hotspots are rendered by the component.
203
207
 
204
208
  ### Hotspot actions and shared popup content
205
209
 
@@ -243,7 +247,167 @@ export function Tour({ project }: { project: LoadedPanoProject }) {
243
247
 
244
248
  `ProjectPopupDialog` accepts `popup` (or `null` to close), `onOpenChange`, optional `closeLabel` and `onMediaError(block)`. It renders text as text, uses native audio/video controls without autoplay, and releases media when content closes or changes. It inherits `PanoDialog` fullscreen, sizing and focus behavior. Replace this component with your own overlay to customize rendering.
245
249
 
246
- The schema remains version 1 and old projects remain valid. Projects using new actions require a viewer that supports them. `parsePanoProject(input, { draft: true })` is for authoring only: it permits empty scene lists and unfinished popup titles/media. Loading, saving and package export validate strictly. Host applications may save the same document in a database or map it to tables; the viewer has no database connection.
250
+ ### Floor plan maps
251
+
252
+ An optional project-level `map` places scene spots on one floor image. Projects without `map` stay valid, and `schemaVersion` remains 1. `loadPanoProject` returns the map with the floor image and spot icons resolved from the project directory. `parsePanoMap` and `resolvePanoMapAssets` are also exported from `/data` for custom loaders.
253
+
254
+ ```json
255
+ "map": {
256
+ "image": "assets/map/floor.webp",
257
+ "interaction": "zoom",
258
+ "maxScale": 4,
259
+ "iconSize": 28,
260
+ "presentation": {
261
+ "mode": "docked",
262
+ "corner": "bottom-left",
263
+ "width": 240,
264
+ "height": 180,
265
+ "offsetX": 16,
266
+ "offsetY": 16
267
+ },
268
+ "controls": { "collapse": true, "zoom": true },
269
+ "sector": {
270
+ "angle": 70,
271
+ "radius": 56,
272
+ "followFov": false,
273
+ "color": "#38bdf8",
274
+ "opacity": 0.55,
275
+ "strokeColor": "#ffffff",
276
+ "strokeWidth": 1.5,
277
+ "strokeOpacity": 0.9
278
+ },
279
+ "spots": [{
280
+ "id": "entrance-spot",
281
+ "sceneId": "entrance",
282
+ "x": 0.31,
283
+ "y": 0.5,
284
+ "heading": 20,
285
+ "icon": "assets/map/pin.webp"
286
+ }]
287
+ }
288
+ ```
289
+
290
+ `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
+
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 expand and collapse on a docked panel. `iconSize` is the marker edge in CSS pixels, default 28. Icons and the sector keep that screen size while the floor image zooms.
293
+
294
+ `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
+
296
+ `presentation.mode` is `"docked"` (the default) or `"dialog"`. Both accept `corner` (`top-left`, `top-right`, `bottom-left`, or `bottom-right`; default `bottom-left`), `offsetX` and `offsetY` (default 16), plus `width` and `height`. A docked panel defaults to 220×160. Dialog mode docks 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.
297
+
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`. Docked collapse state is owned by the overlay; dialog mode and `editorSurface` force collapse controls off, so `collapsed` cannot be driven from outside.
299
+
300
+ #### Mounting `PanoMapOverlay`
301
+
302
+ Mount it from `PanoViewer.renderOverlay` 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
+
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. `showZoomControls` / `showCollapseControl` work on `PanoMap` the same way as on the overlay. `className` / `style` attach to the surface root.
305
+
306
+ ```tsx
307
+ import { useState } from "react";
308
+ import {
309
+ PanoViewer, ProjectScenes, PanoMapOverlay, type LoadedPanoProject,
310
+ } from "@ericchen1990/pano-view";
311
+ import "@ericchen1990/pano-view/styles.css";
312
+
313
+ export function Tour({ project }: { project: LoadedPanoProject }) {
314
+ const [activeSceneId, setActiveSceneId] = useState(project.initialSceneId);
315
+ const map = project.map;
316
+ return <PanoViewer style={{ height: 600 }} renderOverlay={map ? () => (
317
+ <PanoMapOverlay
318
+ map={map}
319
+ activeSceneId={activeSceneId}
320
+ onSelectScene={setActiveSceneId}
321
+ />
322
+ ) : undefined}>
323
+ <ProjectScenes
324
+ project={project}
325
+ activeSceneId={activeSceneId}
326
+ onActiveSceneIdChange={setActiveSceneId}
327
+ />
328
+ </PanoViewer>;
329
+ }
330
+ ```
331
+
332
+ #### Size, corner, and inset
333
+
334
+ - `presentation` — `"docked"` | `"dialog"` (defaults from the document, otherwise `"docked"`).
335
+ - `corner` — `"top-left"` | `"top-right"` | `"bottom-left"` | `"bottom-right"` (default `"bottom-left"`).
336
+ - `width` / `height` — docked panel size, or the dialog launcher size, in CSS pixels. Docked defaults to 220×160; the dialog launcher defaults to 48×48.
337
+ - `offsetX` / `offsetY` — inset from the chosen corner in CSS pixels (default 16).
338
+
339
+ ```tsx
340
+ <PanoMapOverlay
341
+ map={map}
342
+ activeSceneId={activeSceneId}
343
+ onSelectScene={setActiveSceneId}
344
+ presentation="docked"
345
+ corner="bottom-right"
346
+ width={280}
347
+ height={200}
348
+ offsetX={20}
349
+ offsetY={20}
350
+ />
351
+ ```
352
+
353
+ #### Dialog mode
354
+
355
+ Dialog mode docks a launcher button and opens the map in a viewer-local modal. Launcher size uses `width` / `height` (and `corner` / offsets). The dialog body uses a separate pair:
356
+
357
+ - `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
+ - `openLabel` / `closeLabel` / `title` — launcher accessible name, close button label, and dialog heading (defaults `"Map"`, `"Close"`, `"Map"`).
359
+
360
+ Choosing a spot closes the dialog.
361
+
362
+ ```tsx
363
+ <PanoMapOverlay
364
+ map={map}
365
+ activeSceneId={activeSceneId}
366
+ onSelectScene={setActiveSceneId}
367
+ presentation="dialog"
368
+ corner="bottom-left"
369
+ width={52}
370
+ height={52}
371
+ dialogWidth={640}
372
+ dialogHeight={480}
373
+ openLabel="Floor plan"
374
+ title="Floor plan"
375
+ closeLabel="Close"
376
+ />
377
+ ```
378
+
379
+ #### Zoom and controls
380
+
381
+ These props override the map document for the current mount:
382
+
383
+ - `interaction` — `"fixed"` | `"zoom"`.
384
+ - `maxScale` — maximum multiple of the contain-fit size; values below 1 are treated as 1.
385
+ - `dragMotion` — pan inertia / edge bounce when `interaction` is `"zoom"`. Omit for both on; `false` disables both.
386
+ - `initialScale` — opening zoom relative to the contain-fit size, clamped to `[1, maxScale]`. View state only; not written back to the document. Applies after the image is measured and recenters. Resets only when `initialScale` or `interaction` changes; later wheel and drag keep the user's view. Shrinking `maxScale` only clamps the current scale.
387
+ - `showZoomControls` / `showCollapseControl` — force zoom or collapse chrome on or off.
388
+ - `iconSize` — marker edge in CSS pixels.
389
+
390
+ ```tsx
391
+ <PanoMapOverlay
392
+ map={map}
393
+ activeSceneId={activeSceneId}
394
+ onSelectScene={setActiveSceneId}
395
+ interaction="zoom"
396
+ maxScale={4}
397
+ initialScale={1.5}
398
+ showZoomControls
399
+ showCollapseControl
400
+ iconSize={32}
401
+ />
402
+ ```
403
+
404
+ #### Edit mode
405
+
406
+ `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.
407
+
408
+ Run `pnpm dev` and open `/map` for the playground. Authoring the same document is described in the [editor floor-plan section](../editor/README.md#floor-plan).
409
+
410
+ The schema remains version 1 and old projects remain valid. Projects using new actions or a floor-plan map require a viewer that supports them. `parsePanoProject(input, { draft: true })` is for authoring only: it permits empty scene lists, unfinished popup titles/media, and an empty floor-plan image. Loading, saving and package export validate strictly. Host applications may save the same document in a database or map it to tables; the viewer has no database connection.
247
411
 
248
412
  For individual panoramas and existing configs use the pure data entry. Next.js viewer components need a client boundary; server-side data loading uses `/data`:
249
413
 
@@ -274,14 +438,14 @@ const project = await loadPanoProject({
274
438
 
275
439
  | Reference | Default base |
276
440
  | --- | --- |
277
- | Project `viewerUrl`, hotspot assets, `source.baseUrl` | Project directory |
441
+ | Project `viewerUrl`, hotspot assets, floor-plan image and spot icons, `source.baseUrl` | Project directory |
278
442
  | Tiles and preview in external `viewer.json` | That JSON directory |
279
443
  | Tiles and preview in inline config | Project directory |
280
444
  | Panorama with `source.baseUrl` | Specified directory |
281
445
 
282
446
  URL inputs use the final response directory after redirects. Object inputs require `baseUrl`; it can also override the project directory. Directory trailing slashes are normalized. Domains and cloud prefixes stay outside converter JSON. Browsers accept relative entry URLs; Node.js requires absolute HTTP(S) entry URLs. Full resource URLs remain intact; leading `/` paths resolve against the resource base origin.
283
447
 
284
- If uploads rename files or scatter directories, provide synchronous `resolveAssetUrl(path, context)`. Context has `defaultUrl`, `kind` (`tile`, `preview`, `hotspot`), `sceneId` and optional `hotspotId`. Return a final absolute URL or site-root path; it is not joined again. `uploadedUrls` below is application-owned:
448
+ If uploads rename files or scatter directories, provide synchronous `resolveAssetUrl(path, context)`. Context has `defaultUrl`, `kind` (`tile`, `preview`, `hotspot`, `map`), `sceneId` and optional `hotspotId`. Return a final absolute URL or site-root path; it is not joined again. `uploadedUrls` below is application-owned:
285
449
 
286
450
  ```ts
287
451
  const project = await loadPanoProject("/tour/project.json", {
@@ -293,13 +457,13 @@ const project = await loadPanoProject("/tour/project.json", {
293
457
  });
294
458
  ```
295
459
 
296
- Hotspot mapping covers images, SVG image URLs, audio source arrays, video posters, playback icons and tooltip images; text, CSS and SVG path data remain unchanged. Acquire signed URLs ahead of time in your application and return them synchronously.
460
+ Hotspot mapping covers images, SVG image URLs, audio source arrays, video posters, playback icons and tooltip images; text, CSS and SVG path data remain unchanged. Floor-plan images and spot icons use the same callback with `kind: "map"`. The floor image has an empty `sceneId` and no `hotspotId`. A spot icon sets both `sceneId` and `hotspotId` to the spot id. Include `kind` in an upload key when one path is reused as more than one kind. Acquire signed URLs ahead of time in your application and return them synchronously.
297
461
 
298
462
  `requestInit` accepts only `headers`, `credentials` and `signal`, for JSON requests only. They are not forwarded to textures, and JSON query parameters are not inherited by assets. `parsePanoProject(unknown)` validates and copies project data independently. `sceneFromViewerConfig` accepts the unwrapped compact config; `loadSceneFromJson` accepts a versioned `viewer.json`.
299
463
 
300
464
  `PanoDataError` exposes stable codes: `INVALID_OPTIONS`, `FETCH_FAILED`, `HTTP_ERROR`, `INVALID_JSON`, `INVALID_CONFIG`, `UNSUPPORTED_SCHEMA_VERSION`, `UNSUPPORTED_PROJECTION`, `INVALID_PROJECT`, `INVALID_REFERENCE`, `ABORTED`. HTTP errors carry `status`; project errors include `sceneId` and `field` where available. Successful loading means metadata is ready, not that textures have loaded; use `onSceneReady` for render readiness.
301
465
 
302
- See the `/project` playground route and `public/fixtures/projects/tour/project.json` for a complete static example.
466
+ See the `/project` playground route and `public/fixtures/projects/tour/project.json` for a complete static example. `/map` shows the floor-plan overlay on that tour.
303
467
 
304
468
  ## krpano-compatible tile output
305
469
 
@@ -377,7 +541,8 @@ Import the shipped stylesheet once in the host app. The WebGL panorama
377
541
  **HTML** overlays depend on its prefixed `pano-*` classes for layout.
378
542
 
379
543
  Those overlays include video controls, captions, the default context menu,
380
- hotspot tooltips, controls for assistive technology, and WebVR session UI.
544
+ hotspot tooltips, controls for assistive technology, WebVR session UI, and the
545
+ floor-plan map.
381
546
 
382
547
  ```ts
383
548
  import "@ericchen1990/pano-view/styles.css";
@@ -397,8 +562,9 @@ the stylesheet, the overlays still mount, but they render unstyled.
397
562
 
398
563
  Give PanoViewer an explicit size and render one panorama source inside it.
399
564
  Import the stylesheet once when you use built-in HTML overlays such as video
400
- controls, captions, tooltips, the context menu, or WebVR UI. Each example
401
- below shows a basic setup; the linked section has the full props.
565
+ controls, captions, tooltips, the context menu, WebVR UI, or the floor-plan
566
+ map. Each example below shows a basic setup; the linked section has the full
567
+ props.
402
568
 
403
569
  ### Equirectangular image
404
570
 
@@ -514,8 +680,14 @@ Presets and hotspot rendering are in [Scenes](#scenes).
514
680
 
515
681
  - [`PanoEvents`](#panoevents) — viewer-level lifecycle and interaction callbacks
516
682
  - [`PanoContextMenu`](#panocontextmenu) — right-click reset / fullscreen menu
683
+ - [`PanoSceneMenu`](#panoscenemenu) — scrollable thumbnail scene picker for `renderOverlay`
517
684
  - [`PanoVideoControls`](#panovideo) — playback chrome for `PanoVideo` (mounted by default)
518
685
 
686
+ ### Floor plan
687
+
688
+ - [`PanoMap`](#floor-plan-maps) — floor image, scene spots, and the yaw-linked sector
689
+ - [`PanoMapOverlay`](#floor-plan-maps) — docked panel or dialog launcher for `renderOverlay`
690
+
519
691
  ### Hotspots
520
692
 
521
693
  - [`ImageHotspot`](#imagehotspot) — image texture at a spherical position
@@ -539,9 +711,7 @@ Presets and hotspot rendering are in [Scenes](#scenes).
539
711
 
540
712
  ## DOM overlays and dialogs
541
713
 
542
- Use `PanoViewer.renderOverlay` for ordinary DOM UI outside the Three.js tree. `usePanoOverlay()` supplies its stable Portal target, measured viewer/visible dimensions and fullscreen state. The optional `/ui` entry provides `PanoDialog` and composable parts; your application can also integrate an existing Dialog.
543
-
544
- See the separate **[Dialog and styling guide](./docs/dialogs.md)** for complete examples, CSS variable defaults and placement, custom layouts, third-party integration, nested portals, lifecycle, API tables and troubleshooting. The guide is included in the npm package. Run `pnpm dev` and open `/overlays` for the local examples.
714
+ Use `PanoViewer.renderOverlay` for ordinary DOM UI outside the Three.js tree. Call `usePanoOverlay()` inside the `renderOverlay` subtree 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.
545
715
 
546
716
  ### Default dialog
547
717
 
@@ -634,6 +804,21 @@ which use their spherical center. Hotspot ids must be unique within a viewer.
634
804
 
635
805
  **Background Audio** (requires `<BackgroundAudio />`) — `getBackgroundAudio` returns the `BackgroundAudioController`, `subscribeBackgroundAudio` subscribes to background-audio controller and playback-state changes, plus convenience aliases `playBackgroundAudio`, `pauseBackgroundAudio`, `toggleBackgroundAudio`, `setBackgroundAudioVolume`, `setBackgroundAudioMuted`, `toggleBackgroundAudioMuted`. `BackgroundAudio` accepts `playing` (controlled) or `defaultPlaying` (uncontrolled).
636
806
 
807
+ ### Props
808
+
809
+ - `initialView` — when omitted, yaw `0`, pitch `0`, fov `75`. Partial objects fill the rest from those defaults.
810
+ - `minFov` / `maxFov` — default `30` / `100`. Used as FOV bounds when `viewLimits.fov` is not set.
811
+ - `viewLimits` — camera-center yaw / pitch / fov ranges; scene-level limits override matching axes. See [Camera-center limits](#camera-center-limits).
812
+ - `dpr` — canvas pixel ratio, default `[1, 2]` (clamped device pixel ratio). A single number is also accepted.
813
+ - `controls` — `false` disables all user input. An object configures shared options and `mouse` / `touch` / `keyboard` channels; see [Mouse, touch, and keyboard controls](#mouse-touch-and-keyboard-controls). Shared defaults: `enabled` `true`, `inertia` `true`, `invert` `false`, `bouncingLimits` `false`, `frictionStop` `0.01`, `rotateSpeed` `0.35`, `zoomSpeed` `0.08`, `rotateDamping` / `zoomDamping` `14` / `16`. Omit `fovSpeed` for no extra cap: mouse wheel clamps each step `deltaY * zoomSpeed` to `±fovSpeed`; keyboard zoom compares degrees-per-second × frame time against `fovSpeed`.
814
+ - `cursors` — `true` / omit uses grab / grabbing / pointer / move; `false` leaves the canvas cursor unchanged; an object overrides those four states.
815
+ - `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
+ - `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`.
818
+ - `onViewChange` — `{ yaw, pitch, fov }` in degrees.
819
+ - `onPanoramaClick` / `onPanoramaDoubleClick` / `onPanoramaPointerMove` — panorama pointer events with `position` (yaw / pitch), `nativeEvent`, `button`, `buttons`, `clientX`, `clientY`, and optional `pointerId`.
820
+ - Other standard `div` props (`className`, `style`, `id`, …) are accepted; conflicting `onChange` and `contextMenu` are omitted from the type.
821
+
637
822
  ### Mobile media activation
638
823
 
639
824
  By default, when a child has an initial playback intent (`PanoVideo autoPlay`, an active `BackgroundAudio`, `VideoHotspot`, or `AudioHotspot`), the viewer silently unlocks audible playback on the first user gesture when the browser requires one, or immediately when autoplay policy already allows it. Muted video can still preview inline. The same gesture resumes Web Audio and gives the host the final choice of which media becomes audible, so video, music, and positional sound never all start by assumption. Pass `mediaActivation={false}` to retain the 2.x no-automatic-activation behavior.
@@ -924,7 +1109,7 @@ equirectangular tiles, use [`SphereTile`](#spherical-multiresolution-tiles).
924
1109
  Provide a small preview to reduce download, decoding, and texture memory costs
925
1110
  for the initial display.
926
1111
  Relative paths resolve against the directory of `src`; root-absolute and
927
- `http(s)` URLs are used as-is. For krpano sphere scenes, copy
1112
+ `http(s)`, `blob:`, and `data:` URLs are used as-is. For krpano sphere scenes, copy
928
1113
  [`<preview url>`](https://krpano.com/docu/xml/#preview) into `previewUrl`. That
929
1114
  preview must be equirectangular, not a cube strip.
930
1115
 
@@ -945,6 +1130,17 @@ export function SphereExample() {
945
1130
  }
946
1131
  ```
947
1132
 
1133
+ ### Props
1134
+
1135
+ - `src` — required full 2:1 equirectangular image.
1136
+ - `previewUrl` — required low-resolution 2:1 preview while `src` loads.
1137
+ - `yawOffset` — horizontal rotation in degrees, default `0`. Set when the source forward direction differs from viewer yaw `0`.
1138
+ - `visible` — default `true`. `false` keeps the source mounted for preload without drawing it.
1139
+ - `onReady` — once when the first paintable texture (preview or `src`) is ready.
1140
+ - `onLoad` — when the `src` texture finishes loading; argument is a Three.js `Texture`.
1141
+ - `onError` — when `src` fails to load.
1142
+ - `onPreviewError` — when the preview fails; `src` still continues loading.
1143
+
948
1144
  ## PanoVideo
949
1145
 
950
1146
  `PanoVideo` maps a 2:1 equirectangular video onto the same inward sphere as
@@ -1025,6 +1221,30 @@ rejected. Remote videos and VTT files that are not same-origin should set
1025
1221
  `crossOrigin` (usually `"anonymous"`). For mobile delivery, provide an H.264 +
1026
1222
  AAC MP4 fallback, poster, CORS, and Range support.
1027
1223
 
1224
+ ### Props
1225
+
1226
+ - `src` — single-file shortcut; mutually exclusive with `variants`.
1227
+ - `poster` — cover image. Variants may also set `variants[].poster`.
1228
+ - `variants` / `defaultVariantId` — multi-quality list; missing `defaultVariantId` falls back to the first item. Switching keeps current time and play/pause.
1229
+ - `tracks` — WebVTT tracks. Each may include `src`, `srcLang`, `label`, `default`, `id` (defaults to `srcLang`), and `kind` (`"subtitles"` or `"captions"`, default `"subtitles"`).
1230
+ - `captions` — `true` / omit shows the default overlay; `false` hides the overlay and language menu; an object merges with defaults. Defaults: `color` `#ffffff`, `background` `rgba(0, 0, 0, 0.55)`, `fontSize` `16`, `fontFamily` Inter / system-ui, `fontWeight` `500`, `textShadow` `0 1px 2px rgba(0, 0, 0, 0.8)`, `padding` `"6px 10px"`, `borderRadius` `6`, `maxWidth` `"72%"`, `bottom` `72`, `lineHeight` `1.35`.
1231
+ - `controls` — `true` / omit mounts the default bar; `false` hides it; an object restyles the default instance. Appearance keys: `background` (default `rgba(16, 16, 16, 0.88)`), `color` (`#f4f4f4`), `accent` (`#75cbd3`), `borderRadius` (`8`), `fontSize` (`12`), `padding` (`"8px 10px"`). Render `PanoVideoControls` as a `PanoViewer` child to replace the bar (same `appearance` prop).
1232
+ - `yawOffset` — horizontal offset in degrees, default `0`.
1233
+ - `loop` — default `false`.
1234
+ - `muted` — default `true`.
1235
+ - `volume` — default `1`, range 0–1.
1236
+ - `autoPlay` — default `false`. When `true`, counts as media-activation intent.
1237
+ - `playsInline` — default `true`.
1238
+ - `preload` — `"none"` | `"metadata"` | `"auto"`, default `"metadata"`.
1239
+ - `crossOrigin` — `""` | `"anonymous"` | `"use-credentials"`; omit to leave unset.
1240
+ - `playbackRates` — speed menu list, default `[0.5, 1, 1.25, 1.5, 2]`.
1241
+ - `visible` — default `true`; `false` preloads without drawing.
1242
+ - `onLoad` — when the video becomes playable.
1243
+ - `onError` — `{ source: "poster" | "video" | "track", error }`.
1244
+ - `onPlaybackStateChange` — `"playing" | "paused" | "ended" | "blocked"`.
1245
+ - `onEnded` — when playback reaches the end without looping.
1246
+ - `onVariantChange` / `onTrackChange` — current variant id, or current track id (`null` when none).
1247
+
1028
1248
  ## Tile
1029
1249
 
1030
1250
  `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:
@@ -1136,6 +1356,24 @@ horizontal index and `%00v` produces a three-digit vertical index. The stereo
1136
1356
  `%t` and frame `%f` placeholders are not applicable to `Tile`, which represents
1137
1357
  a single non-stereo cube panorama.
1138
1358
 
1359
+ ### Props
1360
+
1361
+ - `baseUrl` / `multires` / `previewUrl` — required. `multires` may be a string or `{ tileSize, levels }`.
1362
+ - `previewFaceOrder` — default `["l", "f", "r", "b", "u", "d"]`; must list exactly six faces.
1363
+ - `urlTemplate` — package default when omitted; `resolveTileUrl` wins over the template.
1364
+ - `maxTextureMemoryMb` — standalone `Tile` default `128`.
1365
+ - `maxConcurrentLoads` — standalone `Tile` default `8` (do not confuse with `SphereTile` / `Scenes` concurrency of `4`).
1366
+ - `retryCount` — retries after the first failure, default `1`.
1367
+ - `loadMode` — `"full"` (default), `"preview"`, or `"base"`.
1368
+ - `visible` — default `true`; `false` preloads only.
1369
+ - `onReady` — when the preview is paintable (tiles may still report ready if the preview failed).
1370
+ - `onPreviewError` — preview load failure.
1371
+ - `onLoadProgress` — `{ requested, loaded, failed, active, queued }`.
1372
+ - `onTileError` — `{ address, url, error }`.
1373
+ - `onLevelChange` — current cube level, **1-based** (`SphereTile` logical levels are 0-based).
1374
+
1375
+ Inside `Scenes` with `type: "tile"`, the shared texture cache overrides per-scene `maxTextureMemoryMb`, `maxConcurrentLoads`, and `retryCount` (the shared provider fixes retry at `1`). Visibility, `loadMode`, and the callbacks above are owned by `Scenes` and should not be set on the scene object.
1376
+
1139
1377
  ## Scenes
1140
1378
 
1141
1379
  `Scenes` switches between single-image equirectangular, cube-tile, and
@@ -1211,6 +1449,234 @@ While a detail wait or transition runs, panorama drag/zoom input is locked and
1211
1449
  is still being prepared. `maxTextureMemoryMb` and `maxConcurrentTileLoads`
1212
1450
  apply to the whole `Scenes` viewer rather than to each tile scene.
1213
1451
 
1452
+ ### Props
1453
+
1454
+ - `scenes` — required. Supports `type: "sphere" | "tile" | "sphereTile"`.
1455
+ - `activeSceneId` — controlled current scene.
1456
+ - `defaultActiveSceneId` — uncontrolled initial scene. When both are omitted, uses `scenes[0].id`.
1457
+ - `onActiveSceneIdChange` — fired by imperative switches in controlled mode.
1458
+ - `transition` — defaults to `"crossfade"` for `1` second. Also accepts `{ preset, duration }`. Preset default seconds: `none` 0; `crossfade` / `whiteFlash` / most slides and opens / `pixelate` / `dissolve` / `clockWipe` / `zoomBlur` 1; `verticalOpen` 0.7; `glitch` 0.8; `hexDissolve` 1.1; the six `gridWipe*` / `shatter` / `swirl` / `ripple` 1.2; `filmBurn` 1.3; `particles` 1.4; `zoom` / `blackout` 2.
1459
+ - `transitionWaitForDetailMs` — default `0`.
1460
+ - `renderHotspots` — rendered only after a transition finishes; hidden while blending.
1461
+ - `onSceneReady` — first paintable scene, or incoming scene after snapshot takeover; not full detail.
1462
+ - `onTransitionEnd` — `{ previousSceneId, sceneId, preset }`.
1463
+ - `onTransitionError` — `{ sceneId, error }`.
1464
+ - `maxTextureMemoryMb` — shared budget, default `128`.
1465
+ - `maxConcurrentTileLoads` — shared concurrency, default `4`.
1466
+ - `snapshotMaxPixels` — max physical-pixel area of the outgoing GPU snapshot, default `3686400` (~2560×1440).
1467
+
1468
+ Scene `viewLimits` are covered in the FAQ; `yawOffset` matches the source component. Per-scene cube cache fields are ignored under the shared cache — see [Tile](#tile).
1469
+
1470
+ ## PanoSceneMenu
1471
+
1472
+ `PanoSceneMenu` is a scrollable thumbnail strip for switching scenes. Mount it
1473
+ from `PanoViewer.renderOverlay` so it stays inside the viewer (including
1474
+ fullscreen). Import `@ericchen1990/pano-view/styles.css` for the default layout.
1475
+
1476
+ The menu is host-driven: you pass `items`, `activeId`, and `onSelect`. It does
1477
+ not read `Scenes` state or scene `previewUrl`. Cube `previewUrl` values are
1478
+ often 1×6 preview strips and are not suitable thumbnails — use a dedicated
1479
+ thumbnail URL (for converter output, prefer something like `previews/200.webp`).
1480
+
1481
+ ### Basic wiring
1482
+
1483
+ Keep one controlled scene id and share it with `Scenes`:
1484
+
1485
+ ```tsx
1486
+ import { useState } from "react";
1487
+ import {
1488
+ PanoSceneMenu,
1489
+ PanoViewer,
1490
+ Scenes,
1491
+ type PanoSceneMenuItem,
1492
+ } from "@ericchen1990/pano-view";
1493
+ import "@ericchen1990/pano-view/styles.css";
1494
+
1495
+ const menuItems: PanoSceneMenuItem[] = [
1496
+ {
1497
+ id: "lobby",
1498
+ title: "Lobby",
1499
+ thumbnailUrl: "/panoramas/lobby/thumb.webp",
1500
+ },
1501
+ {
1502
+ id: "terrace",
1503
+ title: "Terrace",
1504
+ thumbnailUrl: "/panoramas/terrace/previews/200.webp",
1505
+ },
1506
+ ];
1507
+
1508
+ export function Tour() {
1509
+ const [activeSceneId, setActiveSceneId] = useState("lobby");
1510
+
1511
+ return (
1512
+ <PanoViewer
1513
+ style={{ height: 560 }}
1514
+ renderOverlay={() => (
1515
+ <PanoSceneMenu
1516
+ activeId={activeSceneId}
1517
+ items={menuItems}
1518
+ onSelect={setActiveSceneId}
1519
+ />
1520
+ )}
1521
+ >
1522
+ <Scenes
1523
+ scenes={scenes}
1524
+ activeSceneId={activeSceneId}
1525
+ onActiveSceneIdChange={setActiveSceneId}
1526
+ />
1527
+ </PanoViewer>
1528
+ );
1529
+ }
1530
+ ```
1531
+
1532
+ Each item needs `id` and `thumbnailUrl`. Optional `title` is shown on the
1533
+ thumbnail label and as the control tooltip; when omitted, `id` is used. The
1534
+ active item gets a selection ring above the thumbnail and `data-active="true"`.
1535
+ Scene names sit on a dark translucent bar at the bottom of each thumbnail;
1536
+ use `appearance.labelAlign` for `"center"` or `"end"` alignment.
1537
+
1538
+ ### Placement, inset, and size
1539
+
1540
+ - `placement` — `"top"` | `"bottom"` | `"left"` | `"right"` (default `"bottom"`).
1541
+ Top/bottom render a horizontal strip; left/right render a vertical strip.
1542
+ - `align` — `"start"` | `"center"` | `"end"` (default `"center"`). When the menu
1543
+ is smaller than the available dock space, aligns it along that edge. Short
1544
+ lists are also aligned inside the strip the same way.
1545
+ - `inset` — gap from the viewer edges (default `12`). A number is pixels; a
1546
+ string keeps units such as `"1rem"`; an object sets `top` / `right` /
1547
+ `bottom` / `left` independently.
1548
+ - `width` / `height` — menu size. A number is pixels; a string is used as CSS as-is,
1549
+ including `%`, `rem`, `calc()`, `min()`, `max()`, and `clamp()`.
1550
+ - `dragMotion` — scroll inertia and edge bounce. Omit for both on (custom pointer
1551
+ scrolling, including desktop drag). Set `false` for native overflow scroll.
1552
+ Pass `{ inertia?: boolean; bounce?: boolean }` to toggle either flag. A short
1553
+ press still selects; a drag past a few pixels suppresses the click.
1554
+
1555
+ ```tsx
1556
+ <PanoSceneMenu
1557
+ placement="bottom"
1558
+ align="center"
1559
+ inset={16}
1560
+ width="min(720px, 100%)"
1561
+ items={menuItems}
1562
+ activeId={activeSceneId}
1563
+ onSelect={setActiveSceneId}
1564
+ />
1565
+
1566
+ <PanoSceneMenu
1567
+ placement="right"
1568
+ align="start"
1569
+ inset={{ top: 24, right: 12, bottom: 24, left: 12 }}
1570
+ height="70%"
1571
+ width={160}
1572
+ items={menuItems}
1573
+ activeId={activeSceneId}
1574
+ onSelect={setActiveSceneId}
1575
+ />
1576
+ ```
1577
+
1578
+ If you omit `width` on a top/bottom menu, the strip stretches between the left
1579
+ and right insets. If you omit `height` on a left/right menu, it stretches between
1580
+ the top and bottom insets. The menu also applies a max size that subtracts
1581
+ `inset` and safe-area insets, so `100%` still stays inside the viewer.
1582
+
1583
+ Use `style` when you need one-off positioning overrides on top of these props.
1584
+
1585
+ ### Collapsible menu and arrows
1586
+
1587
+ By default the menu stays open. Set `collapsible` to show a toggle centered on
1588
+ the inner edge of the strip (above a bottom menu, below a top menu, toward the
1589
+ viewer for left/right). Collapsing slides the list off-screen along the dock
1590
+ edge and leaves only the expand button on the safe-area edge:
1591
+
1592
+ ```tsx
1593
+ <PanoSceneMenu
1594
+ collapsible
1595
+ defaultExpanded
1596
+ items={menuItems}
1597
+ activeId={activeSceneId}
1598
+ onSelect={setActiveSceneId}
1599
+ />
1600
+
1601
+ // Controlled
1602
+ <PanoSceneMenu
1603
+ collapsible
1604
+ expanded={menuOpen}
1605
+ onExpandedChange={setMenuOpen}
1606
+ items={menuItems}
1607
+ activeId={activeSceneId}
1608
+ onSelect={setActiveSceneId}
1609
+ />
1610
+ ```
1611
+
1612
+ Set `showArrows` to add previous/next buttons at both ends of the list. By
1613
+ default the buttons stop at the first and last items. Set `loop` so arrows and
1614
+ arrow keys wrap from the last item to the first (and vice versa). Arrows stay
1615
+ disabled when there are fewer than two scenes. Customize their accessible names
1616
+ with `previousLabel` / `nextLabel`, and the toggle with `expandLabel` /
1617
+ `collapseLabel`. Defaults are `"Previous scene"`, `"Next scene"`,
1618
+ `"Show scenes"`, and `"Hide scenes"`. The radiogroup label defaults to `"Scenes"`
1619
+ via `label`. When collapsible, `defaultExpanded` defaults to `true`.
1620
+
1621
+ ```tsx
1622
+ <PanoSceneMenu
1623
+ showArrows
1624
+ loop
1625
+ collapsible
1626
+ placement="bottom"
1627
+ width="min(720px, 100%)"
1628
+ items={menuItems}
1629
+ activeId={activeSceneId}
1630
+ onSelect={setActiveSceneId}
1631
+ previousLabel="Previous room"
1632
+ nextLabel="Next room"
1633
+ />
1634
+ ```
1635
+
1636
+ ### Appearance and CSS hooks
1637
+
1638
+ Pass `appearance` for common chrome tokens without writing CSS. The active-item
1639
+ ring sits above the thumbnail and is controlled by `selectionColor` /
1640
+ `selectionWidth` (`selectionWidth: 0` hides it). Scene titles default to start
1641
+ alignment; set `labelAlign: "center"` or `"end"` to change that.
1642
+
1643
+ ```tsx
1644
+ <PanoSceneMenu
1645
+ appearance={{
1646
+ background: "rgba(8, 12, 18, 0.88)",
1647
+ itemWidth: 144,
1648
+ itemHeight: 88,
1649
+ gap: 10,
1650
+ borderRadius: 10,
1651
+ selectionColor: "#7dd3fc",
1652
+ selectionWidth: 2,
1653
+ labelBackground: "rgba(0, 0, 0, 0.6)",
1654
+ labelColor: "#ffffff",
1655
+ labelAlign: "center",
1656
+ fontSize: 12,
1657
+ controlColor: "#f8fafc",
1658
+ controlBackground: "rgba(255, 255, 255, 0.14)",
1659
+ }}
1660
+ items={menuItems}
1661
+ activeId={activeSceneId}
1662
+ onSelect={setActiveSceneId}
1663
+ />
1664
+ ```
1665
+
1666
+ `DEFAULT_PANO_SCENE_MENU_APPEARANCE` exports the defaults (including
1667
+ `padding: "8px 10px"` and `labelAlign: "start"`). For deeper styling,
1668
+ use `className` / `style` against these stable classes:
1669
+
1670
+ - `pano-scene-menu` — root chrome
1671
+ - `pano-scene-menu-body` — arrows + list row/column (owns the strip background)
1672
+ - `pano-scene-menu-list` — scrollable item list
1673
+ - `pano-scene-menu-item` / `pano-scene-menu-thumb` / `pano-scene-menu-label`
1674
+ - `pano-scene-menu-control` / `pano-scene-menu-arrow` / `pano-scene-menu-toggle`
1675
+
1676
+ The root also exposes `data-placement`, `data-align`, `data-orientation`
1677
+ (`horizontal` | `vertical`), `data-expanded`, and `data-collapsible`. The active
1678
+ item has `data-active="true"`.
1679
+
1214
1680
  ## PanoFilter
1215
1681
 
1216
1682
  Render `PanoFilter` inside `PanoViewer` to color-grade or stylize the panorama
@@ -1257,7 +1723,7 @@ panorama when a filter is active.
1257
1723
 
1258
1724
  ## AutoRotate
1259
1725
 
1260
- 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.
1726
+ 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 (default `2`). While the user is dragging, or while drag inertia is still settling, rotation pauses and resumes from zero speed automatically. When the component is mounted and `enabled` is omitted, it is treated as `true`; omitting the component entirely means no auto-rotation.
1261
1727
 
1262
1728
  ```tsx
1263
1729
  <PanoViewer style={{ height: 560 }}>
@@ -1332,6 +1798,22 @@ every other scene plays `default.mp3`.
1332
1798
  For a sound attached to a point in the panorama that pans with the view, use
1333
1799
  `AudioHotspot`, not `BackgroundAudio`.
1334
1800
 
1801
+ ### Props
1802
+
1803
+ - `src` / `sources` — `src` is the shared track, or the fallback when a scene is missing from `sources`. Either value may be a string or a string array (Howler multi-format fallback). `sources` keys are `Scene.id` values.
1804
+ - `sceneId` — required when `sources` is set; omitting it throws.
1805
+ - `playing` / `defaultPlaying` — controlled playback, or uncontrolled initial value (default `false`).
1806
+ - `loop` — default `true`.
1807
+ - `muted` — default `false`.
1808
+ - `volume` — default `1`, range 0–1.
1809
+ - `pauseWhenHidden` — default `true`; pauses while the page is hidden.
1810
+ - `fadeMs` — crossfade milliseconds when the resolved track changes, default `400`; `0` is a hard cut.
1811
+ - `crossOrigin` — `""` | `"anonymous"` | `"use-credentials"`; omit to leave unset.
1812
+ - `onPlaybackStateChange` — `"playing" | "paused" | "ended" | "blocked"`.
1813
+ - `onPlaybackError` — `play()` and other playback failures.
1814
+ - `onEnded` — when the track finishes; uncontrolled mode also sets internal playing to `false`.
1815
+ - `onError` — resource load failure, `{ error }` (distinct from `onPlaybackError`).
1816
+
1335
1817
  ## Gyro
1336
1818
 
1337
1819
  `Gyro` is an opt-in device-orientation control and is disabled by default.
@@ -1405,6 +1887,22 @@ require HTTPS. A cross-origin iframe must be permitted by the host page, for
1405
1887
  example with `allow="gyroscope; accelerometer"` and a compatible
1406
1888
  `Permissions-Policy` header.
1407
1889
 
1890
+ ### Props
1891
+
1892
+ - `enabled` — default `false`. On iOS, call `requestPermission()` inside a user gesture before setting `true`.
1893
+ - `camroll` — default `true`.
1894
+ - `absolute` — default `false` (relative mode); `true` follows the compass.
1895
+ - `north` — panorama yaw that represents North, in degrees, default `0`.
1896
+ - `friction` — `0`–`0.99`, default `0`.
1897
+ - `touchMode` — default `"full"`; see values above.
1898
+ - `softstart` — blend seconds when enabling, default `0.5`.
1899
+ - `desktopSupport` — default `false`.
1900
+ - `onAvailable` / `onUnavailable` — when sensor capability detection changes.
1901
+ - `onEnable` / `onDisable` — when a session actually starts / stops.
1902
+ - `onDenied` — when permission is denied (examples often set `enabled` back to `false`).
1903
+
1904
+ These are props on `Gyro` itself. `PanoEvents` `onGyro*` callbacks subscribe to the same event bus for viewer-level listeners.
1905
+
1408
1906
  ## WebVR
1409
1907
 
1410
1908
  `WebVR` adds immersive viewing to the nearest `PanoViewer`. It prefers an
@@ -1462,6 +1960,22 @@ DOM overlays such as iframe hotspots, tooltips, and video controls are not
1462
1960
  visible inside an immersive headset session; mesh-based hotspots remain
1463
1961
  interactive through the default WebXR controller rays.
1464
1962
 
1963
+ ### Props
1964
+
1965
+ - `chrome` — default `true` mounts Enter / Exit / Setup UI. An appearance object may set `background` (default `rgba(16, 24, 32, 0.78)`), `color` (`#ffffff`), `accent` (`#7dd3fc`), `borderRadius` (`6`), `fontSize` (`14`), and optional `setupTitle`.
1966
+ - `mobileVr` — default `true`.
1967
+ - `desktopSupport` — default `false` (independent of the same-named `Gyro` prop).
1968
+ - `fakeSupport` — default `true` for mouse-driven desktop simulation.
1969
+ - `screensize` — `"auto"` (factory / omitted storage default) or inches; numbers are clamped to **3–20** with one decimal place.
1970
+ - `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?, label?, id? }`.
1971
+ - `cursor` — default `true`; `false` hides the center reticle.
1972
+ - `cursorDwellMs` — default `1500`; `0` disables auto-click.
1973
+ - `wakelock` / `mousePointerLock` — both default to `true`.
1974
+ - `onAvailable` / `onUnavailable` — VR capability changes.
1975
+ - `onEnterVR` / `onExitVR` — mode `"webxr" | "mobilevr" | "fake"`.
1976
+ - `onDenied` — permission or session denial, optional `error`.
1977
+ - `onUnknownDevice` — when screen / lens parameters cannot be identified.
1978
+
1465
1979
  ## Mouse, touch, and keyboard controls
1466
1980
 
1467
1981
  `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.
@@ -1485,15 +1999,36 @@ interactive through the default WebXR controller rays.
1485
1999
 
1486
2000
  ### MouseControls
1487
2001
 
1488
- See the defaults above. Override through `controls.mouse` or by rendering `<MouseControls />` as a child.
2002
+ Override the default channel through `controls.mouse` or by rendering `<MouseControls />` as a child. A child instance does **not** inherit `controls.fovSpeed`; pass `fovSpeed` on the component when you need the cap.
2003
+
2004
+ - `enabled` — default `true`.
2005
+ - `rotateSpeed` — default `0.35` (scales pointer displacement; `0` disables rotate).
2006
+ - `zoomSpeed` — default `0.08`; each wheel step is `deltaY * zoomSpeed`.
2007
+ - `wheel` — default `true`.
2008
+ - `invert` — default `false`; may override shared `controls.invert`.
2009
+ - `buttons` — default `["left"]`; values `"left" | "middle" | "right"`.
2010
+ - `fovSpeed` — omit for no cap; when set, clamps each wheel step to `±fovSpeed` (not a per-second rate).
1489
2011
 
1490
2012
  ### TouchControls
1491
2013
 
1492
- One-finger drag and optional two-finger pinch zoom (`pinchZoom`, default `true`). Override through `controls.touch` or by rendering `<TouchControls />` as a child.
2014
+ One-finger drag and optional two-finger pinch zoom. Override through `controls.touch` or by rendering `<TouchControls />`.
2015
+
2016
+ - `enabled` — default `true`.
2017
+ - `rotateSpeed` — default `0.35`, same meaning as mouse.
2018
+ - `invert` — default `false`.
2019
+ - `pinchZoom` — default `true`. Pinch changes FOV from the finger-distance ratio and is not limited by `fovSpeed`.
1493
2020
 
1494
2021
  ### KeyboardControls
1495
2022
 
1496
- Hold arrows (or custom bindings) for continuous look / FOV; `0` resets; optional scene bindings. Defaults: `rotateSpeed` `60`, `zoomSpeed` `30`, `shiftMultiplier` `3`. Set `invert` to flip up/down only. Override through `controls.keyboard` or by rendering `<KeyboardControls />` as a child.
2023
+ Hold arrows (or custom bindings) for continuous look / FOV. Override through `controls.keyboard` or by rendering `<KeyboardControls />`. Child instances likewise do not inherit `controls.fovSpeed`.
2024
+
2025
+ - `enabled` — default `true`.
2026
+ - `rotateSpeed` / `zoomSpeed` — default `60` / `30` (degrees per second).
2027
+ - `shiftMultiplier` — default `3`.
2028
+ - `invert` — default `false`; flips up/down only.
2029
+ - `fovSpeed` — omit for no cap; when set, limits with `min(zoom, fovSpeed * dt)`.
2030
+ - `keys` — replaces an action wholesale when that key is present; omitted actions keep defaults; an empty array unbinds the action. Matching compares both `event.key` and `event.code` (lowercased).
2031
+ - `onPreviousScene` / `onNextScene` — keys still `preventDefault` when bound; without a callback the scene does not change.
1497
2032
 
1498
2033
  Shared all-mode options on `controls`: `enabled`, `invert` (drag direction for mouse/touch), `bouncingLimits`, `fovSpeed`, `frictionStop` (default `0.01`), plus existing damping / inertia.
1499
2034
 
@@ -1546,7 +2081,7 @@ export function KeyboardExample({ scenes }: { scenes: Scene[] }) {
1546
2081
 
1547
2082
  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).
1548
2083
 
1549
- 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.
2084
+ Default bindings: arrows and WASD for look; zoom in with `+` and `=`, zoom out with `-` and `_`; `0` resets; `[` / `PageUp` and `]` / `PageDown` for previous/next scene (without callbacks the scene does not change, but the key may still be intercepted). 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.
1550
2085
 
1551
2086
  ## Panorama coordinate events
1552
2087
 
@@ -1622,25 +2157,28 @@ import {
1622
2157
 
1623
2158
  Supported callbacks (krpano names in parentheses where applicable):
1624
2159
 
1625
- - View: `onViewChange` (`onviewchange`), `onViewSettled`, `onViewInteractionStart` / `onViewInteractionEnd`
1626
- - Pointer on the panorama shell (not hotspots): `onClick`, `onDoubleClick`, `onPointerDown`, `onPointerUp`, `onPointerMove`, `onContextMenu`
1627
- - `onWheel` — mouse wheel only; touch pinch does not synthesize a wheel event
2160
+ - View: `onViewChange` (`onviewchange`), `onViewSettled` — both receive `{ yaw, pitch, fov }`. `viewchange` fires while the view moves; `viewsettled` fires when inertia/tween first stops or a commanded animation ends.
2161
+ - `onViewInteractionStart` / `onViewInteractionEnd` — `{ source }` with `source` `"mouse" | "touch" | "keyboard"`.
2162
+ - Pointer on the panorama shell (not hotspots): `onClick`, `onDoubleClick`, `onPointerDown`, `onPointerUp`, `onPointerMove`, `onContextMenu` — `PanoramaPointerEvent` with `position`, `nativeEvent`, `button`, `buttons`, `clientX`, `clientY`, and optional `pointerId`.
2163
+ - `onWheel` — mouse wheel only; touch pinch does not synthesize a wheel event. Payload `{ delta, nativeEvent }`; `delta` matches the sign of `WheelEvent.deltaY`.
1628
2164
  - Idle: `onIdle` / `onIdleEnd` with per-instance `idleTime` (ms, default `2000`)
1629
- - Fullscreen: `onEnterFullscreen` / `onExitFullscreen`
1630
- - `onResize` — canvas content-box size via `ResizeObserver`
1631
- - Auto-rotate: `onAutoRotateStart` / `onAutoRotateStop` / `onAutoRotateOneRound`
1632
- - Gyro: `onGyroAvailable` / `onGyroUnavailable` / `onGyroEnable` /
1633
- `onGyroDisable` / `onGyroDenied`
2165
+ - Fullscreen: `onEnterFullscreen` / `onExitFullscreen` (no payload)
2166
+ - `onResize` — `{ width, height }` from a content-box `ResizeObserver`
2167
+ - Auto-rotate: `onAutoRotateStart` / `onAutoRotateStop` / `onAutoRotateOneRound` (no payload)
2168
+ - Gyro: `onGyroAvailable` / `onGyroUnavailable` / `onGyroEnable` / `onGyroDisable` / `onGyroDenied` (no payload)
2169
+ - VR: `onVRAvailable` / `onVRUnavailable` / `onVREnter` / `onVRExit` / `onVRDenied` / `onVRUnknownDevice`. Enter/exit carry mode `"webxr" | "mobilevr" | "fake"`; `onVRDenied` may include an optional `error`.
1634
2170
 
1635
2171
  `PanoViewer`'s existing `onViewChange` / `onPanoramaClick` / `onPanoramaDoubleClick` /
1636
2172
  `onPanoramaPointerMove` props remain supported and share the same event bus.
2173
+ `usePanoEvents` accepts the same props as `<PanoEvents>` and returns nothing.
1637
2174
 
1638
2175
  Resource callbacks belong to each source: `Sphere` provides `onLoad`,
1639
2176
  `onError`, and `onPreviewError`; `Tile` / `SphereTile` provide `onLoadProgress`,
1640
2177
  `onTileError`, and `onPreviewError`. Sources report initial render readiness
1641
2178
  through `onReady`. Use `Scenes` callbacks `onSceneReady`, `onTransitionEnd`,
1642
- and `onTransitionError` for scene state. This package does not provide a
1643
- one-to-one mapping of krpano XML-loading, VR, or frame-render events.
2179
+ and `onTransitionError` for scene state. This package does not mirror krpano
2180
+ XML-loading or frame-render events one-to-one; use the `onVR*` callbacks above
2181
+ for VR lifecycle.
1644
2182
 
1645
2183
  ## PanoContextMenu
1646
2184
 
@@ -1734,18 +2272,32 @@ reimplementing fullscreen enter/exit state:
1734
2272
  </PanoViewer>
1735
2273
  ```
1736
2274
 
1737
- Override presentation on a preset with `{ preset: "fullscreen", label: "…" }`.
2275
+ Override presentation on a preset with `{ preset: "fullscreen", label: "…" }`;
2276
+ presets may also override `id`, `className`, `style`, `disabled`, and `hidden`.
1738
2277
  For fully manual assembly outside the prop (for example a custom
1739
2278
  `PanoContextMenu` child), call `createPanoContextMenuPresets({ reset,
1740
2279
  toggleFullscreen, isFullscreen })` and use `.resetView` / `.fullscreen` /
1741
2280
  `.defaults`.
1742
2281
 
1743
- Concrete items may use an `icon` React node or an `image` URL. Separators
1744
- always come from the `"separator"` preset. Appearance covers background, text
1745
- color, border, radius, shadow, background-only `opacity` (clamped from
1746
- `MIN_PANO_CONTEXT_MENU_BACKGROUND_OPACITY` / `0.4` to `1`; labels and icons
1747
- stay opaque), hover/disabled colors, and icon size. For an explicit translucent
1748
- fill without using `opacity`, you can still pass `background: "rgba(...)"`.
2282
+ Concrete items may use an `icon` React node or an `image` URL (optional
2283
+ `imageAlt`). Items may also set `disabled`, `hidden`, `className`, and `style`.
2284
+ `onSelect` receives `position`, `view`, `nativeEvent`, and `close()`. Separators
2285
+ always come from the `"separator"` preset.
2286
+
2287
+ When `items` is provided, `prepend` / `append` are ignored. `children` is a
2288
+ full custom render: `(props: { event, view, close }) => ReactNode`; it takes
2289
+ precedence over `items` while open/close/positioning stay with the component.
2290
+ `className`, `style`, `itemClassName`, and `separatorClassName` are also accepted.
2291
+
2292
+ Appearance fields and defaults: `background` `#161616`, `color` `#f4f4f4`,
2293
+ `border` `"1px solid #2e2e2e"`, `borderRadius` `8`,
2294
+ `shadow` `"0 8px 24px rgba(0,0,0,0.45)"`, `opacity` `1` (background only,
2295
+ clamped to `[0.4, 1]`; labels and icons stay opaque), `fontSize` `13`,
2296
+ `minWidth` `180`, `padding` `4`, `iconSize` `16`, `itemPadding` `"6px 10px"`,
2297
+ `itemHoverBackground` `#2a2a2a`, `itemDisabledColor` `#777777`,
2298
+ `separatorColor` `#333333`, `separatorMargin` `"4px 6px"`. For an explicit
2299
+ translucent fill without using `opacity`, you can still pass
2300
+ `background: "rgba(...)"`.
1749
2301
 
1750
2302
  For a fully custom DOM menu, set `contextMenu={false}` and render
1751
2303
  `PanoContextMenu` as a child. Do not mount both the default menu and a child
@@ -1754,15 +2306,34 @@ For a fully custom DOM menu, set `contextMenu={false}` and render
1754
2306
 
1755
2307
  ## Shared hotspot contract and saved definitions
1756
2308
 
1757
- Every hotspot has an `id`, optional `visible`, `interactive`, `pointerEvents`,
1758
- `renderOrder`, and semantic interaction callbacks. Point hotspots additionally
1759
- use a controlled `position`, angular `width`/`height`, hotspot mode, and the
1760
- zoom options described below. `interactive={false}` keeps a hotspot visible
1761
- while letting a drawing tool receive the panorama pointer events beneath it.
1762
- `pointerEvents="none"` ignores mouse, touch, and pen so those events pass
1763
- through to the panorama even when the hotspot stays keyboard-interactive.
1764
- Interactive hotspots set the canvas cursor to `pointer` while hovered, or to
1765
- the hotspot's `cursor` prop when that is set.
2309
+ Every hotspot has an `id`, optional shared fields, and semantic interaction
2310
+ callbacks. Point hotspots additionally use a controlled `position`, angular
2311
+ `width`/`height`, hotspot mode, and the zoom options described below.
2312
+ `interactive={false}` keeps a hotspot visible while letting a drawing tool
2313
+ receive the panorama pointer events beneath it. `pointerEvents="none"` ignores
2314
+ mouse, touch, and pen so those events pass through to the panorama even when
2315
+ the hotspot stays keyboard-interactive. Interactive hotspots set the canvas
2316
+ cursor to `pointer` while hovered, or to the hotspot's `cursor` prop when that
2317
+ is set.
2318
+
2319
+ Shared field defaults and usage:
2320
+
2321
+ - `visible` — default `true`.
2322
+ - `renderOrder` — default `0` for point hotspots; `10` for polygons / polylines.
2323
+ - `opacity` — default `1`; applies to point-hotspot textures / iframes.
2324
+ - `selected` — default `false`; when `true`, draws an ~3px selection outline (common in authoring).
2325
+ - `interactive` — default `true`; `pointerEvents` defaults to `"auto"`.
2326
+ - `onHoverChange(hovered, event)` — boolean hover state, then the interaction event.
2327
+ - `onClick` — position and input source (`"pointer"` or `"keyboard"`).
2328
+ - Point-hotspot drag: with `draggable`, `onDragStart` fires at the start,
2329
+ `onPositionChange` reports the new position while moving (the host must write
2330
+ it back), and `onDragEnd` fires on release. Polygon / polyline callbacks with
2331
+ the same names carry vertex sets — see those sections.
2332
+
2333
+ Default angular size when `width` / `height` are omitted: `ImageHotspot` /
2334
+ `SequenceHotspot` `12×8`; canvas `GraphicHotspot` `10×10` (`12×8` when
2335
+ `kind: "svg"` loads as an image); `VideoHotspot` `16×9`; `AudioHotspot` `8×8`;
2336
+ `TextHotspot` `16×6`; `IframeHotspot` `18×12`.
1766
2337
 
1767
2338
  ```tsx
1768
2339
  <TextHotspot
@@ -1773,11 +2344,10 @@ the hotspot's `cursor` prop when that is set.
1773
2344
  />
1774
2345
  ```
1775
2346
 
1776
- `onClick` and `onHoverChange` receive a position and input source (`"pointer"`
1777
- or `"keyboard"`). Point hotspots can use `draggable`, `onDragStart`,
1778
- `onPositionChange`, and `onDragEnd`; the host must write the reported position
1779
- back to its state. Polygon and polyline hotspots report the corresponding
1780
- controlled `vertices` through `onVerticesChange`.
2347
+ Point hotspots can use `draggable`, `onDragStart`, `onPositionChange`, and
2348
+ `onDragEnd`; the host must write the reported position back to its state.
2349
+ Polygon and polyline hotspots report controlled `vertices` through
2350
+ `onVerticesChange`.
1781
2351
 
1782
2352
  Every hotspot can show a DOM tooltip at its anchor. Pass a string or
1783
2353
  `{ text?, image?, imageAlt? }` as `tooltip`. The default `tooltipTrigger` is
@@ -1855,7 +2425,10 @@ const hotspots: HotspotDefinition[] = [
1855
2425
 
1856
2426
  Definitions intentionally contain data only. Render them with a switch in the
1857
2427
  host, where application-specific click actions, controlled media state, and
1858
- error reporting belong.
2428
+ error reporting belong. Project JSON `label` is not a component prop;
2429
+ `ProjectScenes` uses `hotspot.label || hotspot.id` as the accessible name
2430
+ (`ariaLabel`). `selected` exists only on runtime props and is not part of the
2431
+ saved definition.
1859
2432
 
1860
2433
  ## ImageHotspot
1861
2434
 
@@ -1885,15 +2458,19 @@ move a selected hotspot. Clickable hotspots need `ariaLabel`: PanoViewer creates
1885
2458
  an internal semantic control for Tab, Enter, and Space activation, with a
1886
2459
  visible WebGL focus outline.
1887
2460
 
1888
- `ImageHotspot` calls `onLoad(texture)` and `onError(error)`.
2461
+ `ImageHotspot` calls `onLoad(texture)` and `onError(error)`. When size is
2462
+ omitted, defaults are `width={12}` and `height={8}`.
1889
2463
 
1890
2464
  ## GraphicHotspot
1891
2465
 
1892
2466
  `GraphicHotspot` accepts built-in `circle`, `triangle`, `diamond`, `star`,
1893
2467
  `arrow`, `rectangle`, and `ring` graphics, an SVG URL, or safe SVG path data
1894
2468
  with an explicit viewBox. Built-in graphics support `fill`, `stroke`, and
1895
- `strokeWidth`; rectangles support a relative `cornerRadius` from `0` to `0.5`,
1896
- and rings support `innerRadius`.
2469
+ `strokeWidth`; rectangles support a relative `cornerRadius` from `0` to `0.5`;
2470
+ rings support `innerRadius` (fraction of the outer radius, default `0.58`).
2471
+ Canvas `strokeWidth` is line width on the **512** texture (default `8`), not
2472
+ the CSS-pixel stroke used by polygons. `kind: "path"` needs `path` and
2473
+ `viewBox: [x, y, width, height]`.
1897
2474
 
1898
2475
  ```tsx
1899
2476
  import { GraphicHotspot } from "@ericchen1990/pano-view";
@@ -1943,7 +2520,8 @@ relative proportion, not a texture-pixel measurement.
1943
2520
  `GraphicHotspot` uses the same `onLoad(texture)` and `onError(error)` callbacks
1944
2521
  as `ImageHotspot`. Built-in paths are rasterized locally; only an SVG URL
1945
2522
  or SVG `path` data plus an explicit `viewBox` is accepted, never arbitrary SVG
1946
- markup.
2523
+ markup. Canvas graphics default to angular size `10×10`; `kind: "svg"` image
2524
+ loads default to `12×8`.
1947
2525
 
1948
2526
  ## Hotspot mode and zoom behaviour
1949
2527
 
@@ -2018,6 +2596,13 @@ stroke. Dragging moves all vertices together while keeping yaw normalized and
2018
2596
  pitch clamped. Individual vertex editing is provided by the authoring
2019
2597
  workflow, not the runtime hotspot component.
2020
2598
 
2599
+ Defaults: `fill` `#df6b42`, `fillOpacity` `0.34`, `stroke` `#f5fbfc`,
2600
+ `strokeWidth` `2`, `strokeOpacity` `1`, `renderOrder` `10`. `onDragStart` /
2601
+ `onDragEnd` / `onVerticesChange` carry vertex events (`vertices`,
2602
+ `startVertices`, `position`, `startPosition`), not point-hotspot drag events.
2603
+ `onInvalid` codes are `too_few_vertices`, `self_intersection`,
2604
+ `contains_pole`, and `outside_hemisphere`.
2605
+
2021
2606
  Set `fillOpacity={0}` for a closed outline-only polygon. The fill and outline
2022
2607
  share the same spherical edge sampling so the outline follows the fill boundary.
2023
2608
 
@@ -2046,7 +2631,10 @@ import { PolylineHotspot } from "@ericchen1990/pano-view";
2046
2631
  />;
2047
2632
  ```
2048
2633
 
2049
- `onInvalid` reports the only invalid runtime shape: fewer than two vertices.
2634
+ `onInvalid` reports the only invalid runtime shape: fewer than two vertices
2635
+ (`code: "too_few_vertices"`). Defaults: `stroke` `#f5fbfc`, `strokeWidth` `2`,
2636
+ `strokeOpacity` `1`, `renderOrder` `10`. Drag callbacks likewise carry a
2637
+ vertex set (`PolylineVerticesChangeEvent`).
2050
2638
 
2051
2639
  ## SequenceHotspot
2052
2640
 
@@ -2054,7 +2642,8 @@ import { PolylineHotspot } from "@ericchen1990/pano-view";
2054
2642
  frames in a vertical or horizontal strip. This matches the common krpano
2055
2643
  animated-hotspot format. Give `width` and `height` the aspect ratio of a single
2056
2644
  frame, rather than the full strip. `playing` is controlled by the host; `fps`
2057
- defaults to `12` and `loop` defaults to `true`.
2645
+ defaults to `12`, `loop` to `true`, and `frameDirection` to `"vertical"`.
2646
+ Omitted size defaults to `12×8`.
2058
2647
 
2059
2648
  ```tsx
2060
2649
  import { SequenceHotspot } from "@ericchen1990/pano-view";
@@ -2076,7 +2665,10 @@ import { SequenceHotspot } from "@ericchen1990/pano-view";
2076
2665
  />;
2077
2666
  ```
2078
2667
 
2079
- Sequence loading uses `onLoadProgress` and `onError`. The component does not
2668
+ Sequence loading uses `onLoadProgress` (`{ id, total, loaded, failed }`) and
2669
+ `onError` (`{ id, src, error }`). Also available: `onFrameChange`
2670
+ (`{ id, index, src }`) and `onPlaybackStateChange`
2671
+ (`"playing" | "paused" | "ended"`, no `"blocked"`). The component does not
2080
2672
  change `playing` by itself: update that prop in response to your UI, click
2081
2673
  handler, `onEnded`, or playback error.
2082
2674
 
@@ -2088,7 +2680,9 @@ state. On source change or unmount, the element is paused and its texture is
2088
2680
  released. `playsInline` defaults to `true` and uses the same inline-playback
2089
2681
  attributes as `PanoVideo`. The element is mounted inside the viewer, hidden,
2090
2682
  so mobile browsers keep it in the page. `muted` defaults to `true`, `loop` to
2091
- `false`, and `preload` to `"metadata"`.
2683
+ `false`, `preload` to `"metadata"` (also `"none"` | `"auto"`), and `volume` to
2684
+ `1` (0–1). Omitted size defaults to `16×9`. Cross-origin media may set
2685
+ `crossOrigin` (`""` | `"anonymous"` | `"use-credentials"`).
2092
2686
 
2093
2687
  ```tsx
2094
2688
  import { VideoHotspot } from "@ericchen1990/pano-view";
@@ -2117,9 +2711,10 @@ Browsers can reject unmuted or otherwise non-gesture playback. In that case
2117
2711
  the browser error; retain control of `playing` in the host and offer an
2118
2712
  explicit user action.
2119
2713
 
2120
- Video reports media and poster failures through `onError`. Like
2121
- `SequenceHotspot`, it does not change `playing` by itself. This keeps source
2122
- changes, unmounts, and React StrictMode lifecycles deterministic.
2714
+ Video reports media and poster failures through `onError`, including
2715
+ `source: "poster" | "video"`. Like `SequenceHotspot`, it does not change
2716
+ `playing` by itself. This keeps source changes, unmounts, and React StrictMode
2717
+ lifecycles deterministic.
2123
2718
 
2124
2719
  ## AudioHotspot
2125
2720
 
@@ -2170,19 +2765,26 @@ import { AudioHotspot } from "@ericchen1990/pano-view";
2170
2765
  ```
2171
2766
 
2172
2767
  Like the other media hotspots, `AudioHotspot` does not change `playing` by
2173
- itself.
2768
+ itself. Omitted marker size defaults to `8×8`. `crossOrigin` is also accepted;
2769
+ load failures go to `onError({ id, error })`, distinct from `onPlaybackError`.
2174
2770
 
2175
2771
  ## TextHotspot
2176
2772
 
2177
2773
  `TextHotspot` rasterizes plain text onto a canvas texture. It does not accept
2178
2774
  HTML or Markdown. `\n` is a hard line break; `whiteSpace="normal"` (default)
2179
- wraps words to the hotspot's angular width. Dimensions stay in degrees like
2180
- the other point hotspots. Typography uses the same names as CSS
2181
- (`fontFamily`, `fontSize`, `fontWeight`, `fontStyle`) but is painted onto the
2182
- canvas, not applied as DOM style. `fontSize` is canvas pixels (default 96) and
2183
- does not scale with panel height. `fontWeight` defaults to `600` and
2184
- `fontStyle` to `"normal"`. `padding` and `borderRadius` remain fractions of
2185
- the texture's shorter side, so they still follow `width` / `height`.
2775
+ wraps words to the hotspot's angular width; `"nowrap"` is also accepted.
2776
+ Dimensions stay in degrees like the other point hotspots; the default panel is
2777
+ `16×6`. Typography uses the same names as CSS (`fontFamily`, `fontSize`,
2778
+ `fontWeight`, `fontStyle`) but is painted onto the canvas, not applied as DOM
2779
+ style. `fontSize` is canvas pixels (default 96) and does not scale with panel
2780
+ height. `fontWeight` defaults to `600` and `fontStyle` to `"normal"`. `align`
2781
+ defaults to `"center"` (`left` | `center` | `right`); `verticalAlign` defaults
2782
+ to `"middle"` (`top` | `middle` | `bottom`). `color` defaults to `#f8fafc`;
2783
+ `background` defaults to `#111827` (empty string skips the panel fill);
2784
+ `backgroundOpacity` defaults to `0.72`. `padding` defaults to `0.08` and
2785
+ `borderRadius` to `0.08` (fractions of the shorter side, `0`–`0.5`).
2786
+ `lineHeight` defaults to `1.25` (multiple of font size). `stroke` /
2787
+ `strokeWidth` draw an outline when `strokeWidth > 0` (default width `0`).
2186
2788
 
2187
2789
  ```tsx
2188
2790
  import { TextHotspot } from "@ericchen1990/pano-view";
@@ -2241,8 +2843,10 @@ interactive and blocks dragging through the iframe. A hotspot
2241
2843
  `pointerEvents="none"` also forces the iframe overlay not to capture pointer
2242
2844
  events. `sandbox` defaults to
2243
2845
  `allow-scripts allow-popups allow-forms` without `allow-same-origin`.
2244
- `referrerPolicy` defaults to `strict-origin-when-cross-origin`. Prefer a
2245
- same-origin document; many third-party sites send `X-Frame-Options` and will
2846
+ `referrerPolicy` defaults to `strict-origin-when-cross-origin`. Also accepted:
2847
+ `title` (falls back to `ariaLabel`), `allow`, `loading` (`"eager"` | `"lazy"`),
2848
+ `background`, and `onLoad` / `onError`. Omitted size defaults to `18×12`. Prefer
2849
+ a same-origin document; many third-party sites send `X-Frame-Options` and will
2246
2850
  not load.
2247
2851
 
2248
2852
  ## Next.js and SSR