@ericchen1990/pano-view 2.2.2 → 2.4.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 +355 -3
- package/README.zh-CN.md +321 -2
- package/dist/index.cjs +4021 -798
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.mts +342 -188
- package/dist/index.d.ts +342 -188
- package/dist/index.mjs +3700 -460
- package/dist/index.mjs.map +1 -1
- package/package.json +4 -2
package/README.md
CHANGED
|
@@ -81,6 +81,7 @@ the stylesheet, the overlays still mount, but they render unstyled.
|
|
|
81
81
|
- [`PanoVideo`](#panovideo) — 2:1 equirectangular video on the panorama sphere
|
|
82
82
|
- [`Tile`](#tile) — krpano-style multires cube tiles
|
|
83
83
|
- [`Scenes`](#scenes) — controlled multi-scene transitions
|
|
84
|
+
- [`BackgroundAudio`](#backgroundaudio) — one shared track, or a different track per scene
|
|
84
85
|
- [`PanoFilter`](#panofilter) — color and artistic filters on the panorama source
|
|
85
86
|
|
|
86
87
|
### Controls
|
|
@@ -104,6 +105,7 @@ the stylesheet, the overlays still mount, but they render unstyled.
|
|
|
104
105
|
- [`GraphicHotspot`](#graphichotspot) — built-in shapes or SVG at a position
|
|
105
106
|
- [`SequenceHotspot`](#sequencehotspot) — sprite-sheet animation
|
|
106
107
|
- [`VideoHotspot`](#videohotspot) — HTML video texture
|
|
108
|
+
- [`AudioHotspot`](#audiohotspot) — directional sound at a spherical position
|
|
107
109
|
- [`TextHotspot`](#texthotspot) — rasterized plain text at a position
|
|
108
110
|
- [`IframeHotspot`](#iframehotspot) — embedded document overlay at a position
|
|
109
111
|
- [`PolygonHotspot`](#polygonhotspot) — closed spherical area
|
|
@@ -162,7 +164,242 @@ export function ControlledExample() {
|
|
|
162
164
|
}
|
|
163
165
|
```
|
|
164
166
|
|
|
165
|
-
The handle exposes
|
|
167
|
+
The handle exposes view, fullscreen, scene, VR, video, and background-audio controls so custom UIs can drive the viewer from a single ref:
|
|
168
|
+
|
|
169
|
+
**View** — `getView`, `setView`, `reset`.
|
|
170
|
+
|
|
171
|
+
**Fullscreen** — `enterFullscreen`, `exitFullscreen`, `toggleFullscreen`, `isFullscreen`.
|
|
172
|
+
|
|
173
|
+
**Scenes** (requires `<Scenes />`) — `setScene(id)`, `nextScene`, `previousScene`, `getActiveSceneId`, `getSceneIds`, `isSceneTransitioning`. `Scenes` now accepts an optional `defaultActiveSceneId` (uncontrolled) or keeps the existing `activeSceneId` (controlled). In controlled mode imperative calls invoke `onActiveSceneIdChange`.
|
|
174
|
+
|
|
175
|
+
**WebVR** (requires `<WebVR />`) — `enterVR`, `exitVR`, `toggleVR`, `isVRAvailable`, `isVREnabled`, `getVRMode`, `requestVRPermission`.
|
|
176
|
+
|
|
177
|
+
**Video** (requires `<PanoVideo />`) — `getVideo` returns the `PanoVideoController`, `subscribeVideo` subscribes to host changes, plus convenience aliases `playVideo`, `pauseVideo`, `toggleVideo`, `seekVideo`, `setVideoVolume`, `setVideoMuted`, `toggleVideoMuted`.
|
|
178
|
+
|
|
179
|
+
**Background Audio** (requires `<BackgroundAudio />`) — `getBackgroundAudio` returns the `BackgroundAudioController`, `subscribeBackgroundAudio` subscribes to host changes, plus convenience aliases `playBackgroundAudio`, `pauseBackgroundAudio`, `toggleBackgroundAudio`, `setBackgroundAudioVolume`, `setBackgroundAudioMuted`, `toggleBackgroundAudioMuted`. `BackgroundAudio` now accepts an optional `playing` prop (controlled) or uses `defaultPlaying` (uncontrolled).
|
|
180
|
+
|
|
181
|
+
Methods that depend on an unmounted child component (e.g. `enterVR` without `<WebVR />`) are safe no-ops — they return `false`, `void`, or `null`.
|
|
182
|
+
|
|
183
|
+
### Fullscreen
|
|
184
|
+
|
|
185
|
+
```tsx
|
|
186
|
+
import { useRef } from "react";
|
|
187
|
+
import { PanoViewer, Sphere, type PanoViewerHandle } from "@ericchen1990/pano-view";
|
|
188
|
+
|
|
189
|
+
function FullscreenExample() {
|
|
190
|
+
const ref = useRef<PanoViewerHandle>(null);
|
|
191
|
+
|
|
192
|
+
return (
|
|
193
|
+
<>
|
|
194
|
+
<button onClick={() => void ref.current?.enterFullscreen()}>Enter fullscreen</button>
|
|
195
|
+
<button onClick={() => void ref.current?.exitFullscreen()}>Exit fullscreen</button>
|
|
196
|
+
<button onClick={() => console.log(ref.current?.isFullscreen())}>Is fullscreen?</button>
|
|
197
|
+
<PanoViewer ref={ref} style={{ height: 560 }}>
|
|
198
|
+
<Sphere src="/panoramas/room.webp" previewUrl="preview.webp" />
|
|
199
|
+
</PanoViewer>
|
|
200
|
+
</>
|
|
201
|
+
);
|
|
202
|
+
}
|
|
203
|
+
```
|
|
204
|
+
|
|
205
|
+
### Imperative scene switching
|
|
206
|
+
|
|
207
|
+
Use `defaultActiveSceneId` for uncontrolled mode — imperative calls update the internal state directly. For controlled mode keep passing `activeSceneId`; imperative calls invoke `onActiveSceneIdChange` so the parent can update its state.
|
|
208
|
+
|
|
209
|
+
```tsx
|
|
210
|
+
import { useRef } from "react";
|
|
211
|
+
import {
|
|
212
|
+
PanoViewer,
|
|
213
|
+
Scenes,
|
|
214
|
+
type PanoViewerHandle,
|
|
215
|
+
type Scene,
|
|
216
|
+
} from "@ericchen1990/pano-view";
|
|
217
|
+
|
|
218
|
+
const scenes: Scene[] = [
|
|
219
|
+
{ id: "lobby", type: "sphere", src: "/lobby.webp", previewUrl: "/lobby-preview.webp" },
|
|
220
|
+
{ id: "roof", type: "sphere", src: "/roof.webp", previewUrl: "/roof-preview.webp" },
|
|
221
|
+
];
|
|
222
|
+
|
|
223
|
+
function SceneExample() {
|
|
224
|
+
const ref = useRef<PanoViewerHandle>(null);
|
|
225
|
+
|
|
226
|
+
return (
|
|
227
|
+
<>
|
|
228
|
+
<button onClick={() => ref.current?.setScene("roof")}>Go to roof</button>
|
|
229
|
+
<button onClick={() => ref.current?.nextScene()}>Next</button>
|
|
230
|
+
<button onClick={() => ref.current?.previousScene()}>Previous</button>
|
|
231
|
+
<p>Current: {ref.current?.getActiveSceneId()}</p>
|
|
232
|
+
<PanoViewer ref={ref} style={{ height: 560 }}>
|
|
233
|
+
<Scenes defaultActiveSceneId="lobby" scenes={scenes} transition="dissolve" />
|
|
234
|
+
</PanoViewer>
|
|
235
|
+
</>
|
|
236
|
+
);
|
|
237
|
+
}
|
|
238
|
+
```
|
|
239
|
+
|
|
240
|
+
### WebVR
|
|
241
|
+
|
|
242
|
+
`enterVR` and `requestVRPermission` must be called from a user gesture.
|
|
243
|
+
|
|
244
|
+
```tsx
|
|
245
|
+
import { useRef } from "react";
|
|
246
|
+
import { PanoViewer, Sphere, WebVR, type PanoViewerHandle } from "@ericchen1990/pano-view";
|
|
247
|
+
|
|
248
|
+
function VRExample() {
|
|
249
|
+
const ref = useRef<PanoViewerHandle>(null);
|
|
250
|
+
|
|
251
|
+
return (
|
|
252
|
+
<>
|
|
253
|
+
<button
|
|
254
|
+
onClick={async () => {
|
|
255
|
+
await ref.current?.requestVRPermission();
|
|
256
|
+
await ref.current?.enterVR();
|
|
257
|
+
}}
|
|
258
|
+
>
|
|
259
|
+
Enter VR
|
|
260
|
+
</button>
|
|
261
|
+
<button onClick={() => void ref.current?.exitVR()}>Exit VR</button>
|
|
262
|
+
<button onClick={() => console.log(ref.current?.getVRMode())}>Mode?</button>
|
|
263
|
+
<PanoViewer ref={ref} style={{ height: 560 }}>
|
|
264
|
+
<WebVR />
|
|
265
|
+
<Sphere src="/panoramas/room.webp" previewUrl="preview.webp" />
|
|
266
|
+
</PanoViewer>
|
|
267
|
+
</>
|
|
268
|
+
);
|
|
269
|
+
}
|
|
270
|
+
```
|
|
271
|
+
|
|
272
|
+
### Custom video controls
|
|
273
|
+
|
|
274
|
+
Set `controls={false}` on `PanoVideo` and drive playback from the viewer ref. Use `subscribeVideo` with `useSyncExternalStore` for reactive snapshots.
|
|
275
|
+
|
|
276
|
+
```tsx
|
|
277
|
+
import { useRef, useSyncExternalStore } from "react";
|
|
278
|
+
import {
|
|
279
|
+
PanoViewer,
|
|
280
|
+
PanoVideo,
|
|
281
|
+
type PanoViewerHandle,
|
|
282
|
+
} from "@ericchen1990/pano-view";
|
|
283
|
+
|
|
284
|
+
function VideoExample() {
|
|
285
|
+
const ref = useRef<PanoViewerHandle>(null);
|
|
286
|
+
|
|
287
|
+
const snapshot = useSyncExternalStore(
|
|
288
|
+
(cb) => ref.current?.subscribeVideo(cb) ?? (() => {}),
|
|
289
|
+
() => ref.current?.getVideo()?.getSnapshot() ?? null,
|
|
290
|
+
() => null,
|
|
291
|
+
);
|
|
292
|
+
|
|
293
|
+
return (
|
|
294
|
+
<>
|
|
295
|
+
<button onClick={() => void ref.current?.toggleVideo()}>
|
|
296
|
+
{snapshot?.playing ? "Pause" : "Play"}
|
|
297
|
+
</button>
|
|
298
|
+
<button onClick={() => ref.current?.toggleVideoMuted()}>
|
|
299
|
+
{snapshot?.muted ? "Unmute" : "Mute"}
|
|
300
|
+
</button>
|
|
301
|
+
{snapshot && (
|
|
302
|
+
<input
|
|
303
|
+
type="range"
|
|
304
|
+
min={0}
|
|
305
|
+
max={snapshot.duration}
|
|
306
|
+
value={snapshot.currentTime}
|
|
307
|
+
onChange={(e) => ref.current?.seekVideo(Number(e.target.value))}
|
|
308
|
+
/>
|
|
309
|
+
)}
|
|
310
|
+
<PanoViewer ref={ref} style={{ height: 560 }}>
|
|
311
|
+
<PanoVideo controls={false} src="/tour.mp4" />
|
|
312
|
+
</PanoViewer>
|
|
313
|
+
</>
|
|
314
|
+
);
|
|
315
|
+
}
|
|
316
|
+
```
|
|
317
|
+
|
|
318
|
+
### Custom background audio controls
|
|
319
|
+
|
|
320
|
+
`BackgroundAudio` supports uncontrolled mode — omit `playing` and use `defaultPlaying` instead. Drive it from the viewer ref.
|
|
321
|
+
|
|
322
|
+
```tsx
|
|
323
|
+
import { useRef, useSyncExternalStore } from "react";
|
|
324
|
+
import {
|
|
325
|
+
PanoViewer,
|
|
326
|
+
Sphere,
|
|
327
|
+
BackgroundAudio,
|
|
328
|
+
type PanoViewerHandle,
|
|
329
|
+
} from "@ericchen1990/pano-view";
|
|
330
|
+
|
|
331
|
+
function BGMExample() {
|
|
332
|
+
const ref = useRef<PanoViewerHandle>(null);
|
|
333
|
+
|
|
334
|
+
const snapshot = useSyncExternalStore(
|
|
335
|
+
(cb) => ref.current?.subscribeBackgroundAudio(cb) ?? (() => {}),
|
|
336
|
+
() => ref.current?.getBackgroundAudio()?.getSnapshot() ?? null,
|
|
337
|
+
() => null,
|
|
338
|
+
);
|
|
339
|
+
|
|
340
|
+
return (
|
|
341
|
+
<>
|
|
342
|
+
<button onClick={() => ref.current?.toggleBackgroundAudio()}>
|
|
343
|
+
{snapshot?.playing ? "Pause BGM" : "Play BGM"}
|
|
344
|
+
</button>
|
|
345
|
+
<button onClick={() => ref.current?.toggleBackgroundAudioMuted()}>
|
|
346
|
+
{snapshot?.muted ? "Unmute" : "Mute"}
|
|
347
|
+
</button>
|
|
348
|
+
<input
|
|
349
|
+
type="range"
|
|
350
|
+
min={0}
|
|
351
|
+
max={1}
|
|
352
|
+
step={0.01}
|
|
353
|
+
value={snapshot?.volume ?? 1}
|
|
354
|
+
onChange={(e) =>
|
|
355
|
+
ref.current?.setBackgroundAudioVolume(Number(e.target.value))
|
|
356
|
+
}
|
|
357
|
+
/>
|
|
358
|
+
<PanoViewer ref={ref} style={{ height: 560 }}>
|
|
359
|
+
<BackgroundAudio defaultPlaying src="/bgm.mp3" />
|
|
360
|
+
<Sphere src="/panoramas/room.webp" previewUrl="preview.webp" />
|
|
361
|
+
</PanoViewer>
|
|
362
|
+
</>
|
|
363
|
+
);
|
|
364
|
+
}
|
|
365
|
+
```
|
|
366
|
+
|
|
367
|
+
### All-in-one example
|
|
368
|
+
|
|
369
|
+
```tsx
|
|
370
|
+
import { useRef } from "react";
|
|
371
|
+
import {
|
|
372
|
+
PanoViewer,
|
|
373
|
+
Scenes,
|
|
374
|
+
WebVR,
|
|
375
|
+
PanoVideo,
|
|
376
|
+
BackgroundAudio,
|
|
377
|
+
type PanoViewerHandle,
|
|
378
|
+
} from "@ericchen1990/pano-view";
|
|
379
|
+
|
|
380
|
+
function TourPlayer() {
|
|
381
|
+
const ref = useRef<PanoViewerHandle>(null);
|
|
382
|
+
|
|
383
|
+
return (
|
|
384
|
+
<>
|
|
385
|
+
<nav>
|
|
386
|
+
<button onClick={() => ref.current?.previousScene()}>← Prev</button>
|
|
387
|
+
<button onClick={() => ref.current?.nextScene()}>Next →</button>
|
|
388
|
+
<button onClick={() => void ref.current?.toggleFullscreen()}>Fullscreen</button>
|
|
389
|
+
<button onClick={() => void ref.current?.enterVR()}>Enter VR</button>
|
|
390
|
+
<button onClick={() => ref.current?.toggleBackgroundAudio()}>BGM</button>
|
|
391
|
+
</nav>
|
|
392
|
+
<PanoViewer ref={ref} style={{ height: "100vh" }}>
|
|
393
|
+
<WebVR />
|
|
394
|
+
<BackgroundAudio defaultPlaying src="/bgm.mp3" />
|
|
395
|
+
<Scenes defaultActiveSceneId="lobby" scenes={scenes} transition="dissolve" />
|
|
396
|
+
</PanoViewer>
|
|
397
|
+
</>
|
|
398
|
+
);
|
|
399
|
+
}
|
|
400
|
+
```
|
|
401
|
+
|
|
402
|
+
The two deprecated auto-rotation handle methods (`startAutoRotate`, `stopAutoRotate`) remain for compatibility. Mouse, touch, and keyboard input are enabled by default — you do not need to render control components for ordinary viewing. Render `MouseControls`, `TouchControls`, or `KeyboardControls` only to override properties (the child replaces that channel's default instance). Tune shared behaviour through `controls` (`inertia`, `invert`, `bouncingLimits`, `fovSpeed`, `frictionStop`, `rotateDamping`, `zoomDamping`, and top-level `rotateSpeed` / `zoomSpeed`). Disable a channel with `controls.mouse` / `touch` / `keyboard` set to `false`, or pass an options object (including `enabled`) to override defaults without mounting a child. Auto-rotation is off until you render `AutoRotate`. A default context menu (Reset view / Enter fullscreen) is also mounted — see [`PanoContextMenu`](#panocontextmenu).
|
|
166
403
|
|
|
167
404
|
Drag and zoom update a target view that the camera follows smoothly. `rotateDamping` and `zoomDamping` control that following speed in seconds^-1 (defaults: `14` and `16` respectively); lower values feel softer, while `0` disables smoothing for that axis. Both values must be non-negative finite numbers. Imperative `setView()` and `reset()` remain immediate.
|
|
168
405
|
|
|
@@ -500,6 +737,71 @@ Auto-rotation is off by default. Render `AutoRotate` inside `PanoViewer` to enab
|
|
|
500
737
|
</PanoViewer>
|
|
501
738
|
```
|
|
502
739
|
|
|
740
|
+
## BackgroundAudio
|
|
741
|
+
|
|
742
|
+
`BackgroundAudio` plays background music or ambience for the viewer. It is not
|
|
743
|
+
a hotspot: there is no spherical position, no marker, and volume does not
|
|
744
|
+
follow the camera. Render it as a child of `PanoViewer`, **alongside** `Scenes`,
|
|
745
|
+
`Sphere`, or `Tile`. Do not put it in `Scenes`' `renderHotspots` — hotspots
|
|
746
|
+
unmount during scene transitions and would stop the audio.
|
|
747
|
+
|
|
748
|
+
`playing` is controlled by the host; the component does not change it. `loop`
|
|
749
|
+
defaults to `true`. Browsers may block autoplay; `onPlaybackStateChange` then
|
|
750
|
+
receives `"blocked"`, and the component retries `play()` after the next click
|
|
751
|
+
or keypress while `playing` remains true.
|
|
752
|
+
|
|
753
|
+
Pick one of the two patterns.
|
|
754
|
+
|
|
755
|
+
**One track for every scene** — pass `src` only; omit `sources`. Scene changes
|
|
756
|
+
do not restart the audio.
|
|
757
|
+
|
|
758
|
+
```tsx
|
|
759
|
+
import { BackgroundAudio, PanoViewer, Scenes } from "@ericchen1990/pano-view";
|
|
760
|
+
|
|
761
|
+
<PanoViewer style={{ height: 560 }}>
|
|
762
|
+
<BackgroundAudio src="/bgm/tour.mp3" playing={isBgmPlaying} />
|
|
763
|
+
<Scenes scenes={scenes} activeSceneId={activeSceneId} />
|
|
764
|
+
</PanoViewer>;
|
|
765
|
+
```
|
|
766
|
+
|
|
767
|
+
**A different track per scene** — pass `sources` (keys are `Scene.id`) and the
|
|
768
|
+
current `sceneId`. Omitting `sceneId` throws. When the file changes, playback
|
|
769
|
+
crossfades over `fadeMs` (default 400 ms; `0` is a hard cut). When two scenes
|
|
770
|
+
point at the same file, playback continues without restarting.
|
|
771
|
+
|
|
772
|
+
```tsx
|
|
773
|
+
<PanoViewer style={{ height: 560 }}>
|
|
774
|
+
<BackgroundAudio
|
|
775
|
+
sources={{
|
|
776
|
+
lobby: "/bgm/lobby.mp3",
|
|
777
|
+
terrace: "/bgm/terrace.mp3",
|
|
778
|
+
}}
|
|
779
|
+
sceneId={activeSceneId}
|
|
780
|
+
playing={isBgmPlaying}
|
|
781
|
+
/>
|
|
782
|
+
<Scenes scenes={scenes} activeSceneId={activeSceneId} />
|
|
783
|
+
</PanoViewer>;
|
|
784
|
+
```
|
|
785
|
+
|
|
786
|
+
Optional: also pass `src` as the default for scenes missing from `sources`. To
|
|
787
|
+
silence a scene, set that id to `""` (this overrides `src`; it does not fall
|
|
788
|
+
back to the default track).
|
|
789
|
+
|
|
790
|
+
```tsx
|
|
791
|
+
<BackgroundAudio
|
|
792
|
+
src="/bgm/default.mp3"
|
|
793
|
+
sources={{ lobby: "/bgm/courtyard.mp3", terrace: "" }}
|
|
794
|
+
sceneId={activeSceneId}
|
|
795
|
+
playing={isBgmPlaying}
|
|
796
|
+
/>;
|
|
797
|
+
```
|
|
798
|
+
|
|
799
|
+
In that example, `lobby` plays the courtyard clip, `terrace` is silent, and
|
|
800
|
+
every other scene plays `default.mp3`.
|
|
801
|
+
|
|
802
|
+
For a sound attached to a point in the panorama that pans with the view, use
|
|
803
|
+
`AudioHotspot`, not `BackgroundAudio`.
|
|
804
|
+
|
|
503
805
|
## Gyro
|
|
504
806
|
|
|
505
807
|
`Gyro` is an opt-in device-orientation control and is disabled by default.
|
|
@@ -996,8 +1298,8 @@ color, border, corner radius, shadow, padding, and font size.
|
|
|
996
1298
|
|
|
997
1299
|
For persistence or a host-owned editor, use the exported discriminated
|
|
998
1300
|
`HotspotDefinition` union. Adding a variant is a TypeScript breaking change for
|
|
999
|
-
exhaustive switches; handle `
|
|
1000
|
-
|
|
1301
|
+
exhaustive switches; handle `audio` alongside the existing point and path
|
|
1302
|
+
categories:
|
|
1001
1303
|
|
|
1002
1304
|
```ts
|
|
1003
1305
|
import type { HotspotDefinition } from "@ericchen1990/pano-view";
|
|
@@ -1007,6 +1309,7 @@ const hotspots: HotspotDefinition[] = [
|
|
|
1007
1309
|
{ type: "graphic", id: "marker", position: { yaw: -18, pitch: 9 }, graphic: { kind: "ring" } },
|
|
1008
1310
|
{ type: "sequence", id: "pulse", position: { yaw: -42, pitch: -7 }, src: "/hotspots/pulse.png", frameCount: 20 },
|
|
1009
1311
|
{ type: "video", id: "clip", position: { yaw: 48, pitch: 6 }, src: "/hotspots/clip.webm" },
|
|
1312
|
+
{ type: "audio", id: "fountain", position: { yaw: -30, pitch: -8 }, src: "/hotspots/fountain.mp3", range: 90 },
|
|
1010
1313
|
{ type: "text", id: "caption", position: { yaw: 0, pitch: -16 }, text: "Courtyard overlook" },
|
|
1011
1314
|
{ type: "iframe", id: "guide", position: { yaw: -62, pitch: 4 }, src: "/hotspots/embed.html" },
|
|
1012
1315
|
{ type: "polygon", id: "zone", vertices: [{ yaw: 12, pitch: 4 }, { yaw: 22, pitch: 4 }, { yaw: 18, pitch: 14 }] },
|
|
@@ -1281,6 +1584,55 @@ Video reports media and poster failures through `onError`. Like
|
|
|
1281
1584
|
`SequenceHotspot`, it does not change `playing` by itself. This keeps source
|
|
1282
1585
|
changes, unmounts, and React StrictMode lifecycles deterministic.
|
|
1283
1586
|
|
|
1587
|
+
## AudioHotspot
|
|
1588
|
+
|
|
1589
|
+
`AudioHotspot` plays a directional sound at a spherical position. Stereo
|
|
1590
|
+
panning follows the camera look direction; `range` is the look-away angle in
|
|
1591
|
+
degrees at which volume reaches silence. The default `360` disables look
|
|
1592
|
+
attenuation (stereo pan only). Use a smaller `range` such as `90` for a
|
|
1593
|
+
spot source.
|
|
1594
|
+
|
|
1595
|
+
Playback uses Web Audio via Howler.js (bundled; hosts do not install
|
|
1596
|
+
`howler`). The file must decode before play starts — MP3 is the recommended
|
|
1597
|
+
format. `src` may be a string or an array of fallback URLs. Do not use HTML5
|
|
1598
|
+
streaming (`html5`); it drops stereo panning.
|
|
1599
|
+
|
|
1600
|
+
`playing` is controlled like `VideoHotspot`. `loop` defaults to `false`,
|
|
1601
|
+
`muted` to `false`, `volume` to `1`, and `pauseWhenHidden` to `true` (pause
|
|
1602
|
+
when the tab is hidden, resume if `playing` is still true). A first user
|
|
1603
|
+
gesture unlocks audio; if autoplay is blocked, the hotspot reports
|
|
1604
|
+
`"blocked"` and retries `play()` after the next pointer or key event while
|
|
1605
|
+
`playing` remains true.
|
|
1606
|
+
|
|
1607
|
+
The default marker switches between a built-in **stopped** icon (speaker)
|
|
1608
|
+
and a **playing** icon (speaker plus sound waves). Pass `icon`
|
|
1609
|
+
for a custom stopped image and `playingIcon` for a custom playing image.
|
|
1610
|
+
If only `icon` is set, it is used for both states. `marker={false}` hides
|
|
1611
|
+
the visual for an invisible positional source. For a
|
|
1612
|
+
non-spatial tour or per-scene soundtrack, use [`BackgroundAudio`](#backgroundaudio).
|
|
1613
|
+
|
|
1614
|
+
```tsx
|
|
1615
|
+
import { AudioHotspot } from "@ericchen1990/pano-view";
|
|
1616
|
+
|
|
1617
|
+
<AudioHotspot
|
|
1618
|
+
id="fountain"
|
|
1619
|
+
ariaLabel="Fountain"
|
|
1620
|
+
position={{ yaw: -30, pitch: -8 }}
|
|
1621
|
+
src="/hotspots/fountain.mp3"
|
|
1622
|
+
playing={isFountainPlaying}
|
|
1623
|
+
loop
|
|
1624
|
+
volume={0.8}
|
|
1625
|
+
range={90}
|
|
1626
|
+
onClick={() => setFountainPlaying((playing) => !playing)}
|
|
1627
|
+
onEnded={() => setFountainPlaying(false)}
|
|
1628
|
+
onPlaybackStateChange={(state) => console.log(state)}
|
|
1629
|
+
onPlaybackError={(error) => console.error(error)}
|
|
1630
|
+
/>;
|
|
1631
|
+
```
|
|
1632
|
+
|
|
1633
|
+
Like the other media hotspots, `AudioHotspot` does not change `playing` by
|
|
1634
|
+
itself.
|
|
1635
|
+
|
|
1284
1636
|
## TextHotspot
|
|
1285
1637
|
|
|
1286
1638
|
`TextHotspot` rasterizes plain text onto a canvas texture. It does not accept
|