@ericchen1990/pano-view 2.3.0 → 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 +236 -1
- package/README.zh-CN.md +236 -1
- package/dist/index.cjs +908 -601
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.mts +269 -189
- package/dist/index.d.ts +269 -189
- package/dist/index.mjs +439 -132
- package/dist/index.mjs.map +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -164,7 +164,242 @@ export function ControlledExample() {
|
|
|
164
164
|
}
|
|
165
165
|
```
|
|
166
166
|
|
|
167
|
-
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).
|
|
168
403
|
|
|
169
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.
|
|
170
405
|
|
package/README.zh-CN.md
CHANGED
|
@@ -147,7 +147,242 @@ export function ControlledExample() {
|
|
|
147
147
|
}
|
|
148
148
|
```
|
|
149
149
|
|
|
150
|
-
|
|
150
|
+
句柄从单个 `ref` 暴露视图、全屏、场景、VR、视频与背景音乐控制,方便宿主自绘 UI:
|
|
151
|
+
|
|
152
|
+
**视图** — `getView`、`setView`、`reset`。
|
|
153
|
+
|
|
154
|
+
**全屏** — `enterFullscreen`、`exitFullscreen`、`toggleFullscreen`、`isFullscreen`。
|
|
155
|
+
|
|
156
|
+
**场景**(需 `<Scenes />`)— `setScene(id)`、`nextScene`、`previousScene`、`getActiveSceneId`、`getSceneIds`、`isSceneTransitioning`。`Scenes` 现支持可选 `defaultActiveSceneId`(非受控)或继续传 `activeSceneId`(受控)。受控模式下命令式调用会触发 `onActiveSceneIdChange`。
|
|
157
|
+
|
|
158
|
+
**WebVR**(需 `<WebVR />`)— `enterVR`、`exitVR`、`toggleVR`、`isVRAvailable`、`isVREnabled`、`getVRMode`、`requestVRPermission`。
|
|
159
|
+
|
|
160
|
+
**视频**(需 `<PanoVideo />`)— `getVideo` 返回 `PanoVideoController`,`subscribeVideo` 订阅宿主变更,另有快捷方法 `playVideo`、`pauseVideo`、`toggleVideo`、`seekVideo`、`setVideoVolume`、`setVideoMuted`、`toggleVideoMuted`。
|
|
161
|
+
|
|
162
|
+
**背景音乐**(需 `<BackgroundAudio />`)— `getBackgroundAudio` 返回 `BackgroundAudioController`,`subscribeBackgroundAudio` 订阅宿主变更,另有快捷方法 `playBackgroundAudio`、`pauseBackgroundAudio`、`toggleBackgroundAudio`、`setBackgroundAudioVolume`、`setBackgroundAudioMuted`、`toggleBackgroundAudioMuted`。`BackgroundAudio` 现支持可选 `playing`(受控)或 `defaultPlaying`(非受控)。
|
|
163
|
+
|
|
164
|
+
依赖未挂载子组件的方法(如无 `<WebVR />` 时调 `enterVR`)安全空操作,返回 `false`、`void` 或 `null`。
|
|
165
|
+
|
|
166
|
+
### 全屏
|
|
167
|
+
|
|
168
|
+
```tsx
|
|
169
|
+
import { useRef } from "react";
|
|
170
|
+
import { PanoViewer, Sphere, type PanoViewerHandle } from "@ericchen1990/pano-view";
|
|
171
|
+
|
|
172
|
+
function FullscreenExample() {
|
|
173
|
+
const ref = useRef<PanoViewerHandle>(null);
|
|
174
|
+
|
|
175
|
+
return (
|
|
176
|
+
<>
|
|
177
|
+
<button onClick={() => void ref.current?.enterFullscreen()}>进入全屏</button>
|
|
178
|
+
<button onClick={() => void ref.current?.exitFullscreen()}>退出全屏</button>
|
|
179
|
+
<button onClick={() => console.log(ref.current?.isFullscreen())}>是否全屏?</button>
|
|
180
|
+
<PanoViewer ref={ref} style={{ height: 560 }}>
|
|
181
|
+
<Sphere src="/panoramas/room.webp" previewUrl="preview.webp" />
|
|
182
|
+
</PanoViewer>
|
|
183
|
+
</>
|
|
184
|
+
);
|
|
185
|
+
}
|
|
186
|
+
```
|
|
187
|
+
|
|
188
|
+
### 命令式切换场景
|
|
189
|
+
|
|
190
|
+
使用 `defaultActiveSceneId` 为非受控模式 — 命令式调用直接更新内部状态。受控模式继续传 `activeSceneId`,命令式调用会触发 `onActiveSceneIdChange` 以便父组件更新 state。
|
|
191
|
+
|
|
192
|
+
```tsx
|
|
193
|
+
import { useRef } from "react";
|
|
194
|
+
import {
|
|
195
|
+
PanoViewer,
|
|
196
|
+
Scenes,
|
|
197
|
+
type PanoViewerHandle,
|
|
198
|
+
type Scene,
|
|
199
|
+
} from "@ericchen1990/pano-view";
|
|
200
|
+
|
|
201
|
+
const scenes: Scene[] = [
|
|
202
|
+
{ id: "lobby", type: "sphere", src: "/lobby.webp", previewUrl: "/lobby-preview.webp" },
|
|
203
|
+
{ id: "roof", type: "sphere", src: "/roof.webp", previewUrl: "/roof-preview.webp" },
|
|
204
|
+
];
|
|
205
|
+
|
|
206
|
+
function SceneExample() {
|
|
207
|
+
const ref = useRef<PanoViewerHandle>(null);
|
|
208
|
+
|
|
209
|
+
return (
|
|
210
|
+
<>
|
|
211
|
+
<button onClick={() => ref.current?.setScene("roof")}>去天台</button>
|
|
212
|
+
<button onClick={() => ref.current?.nextScene()}>下一场景</button>
|
|
213
|
+
<button onClick={() => ref.current?.previousScene()}>上一场景</button>
|
|
214
|
+
<p>当前: {ref.current?.getActiveSceneId()}</p>
|
|
215
|
+
<PanoViewer ref={ref} style={{ height: 560 }}>
|
|
216
|
+
<Scenes defaultActiveSceneId="lobby" scenes={scenes} transition="dissolve" />
|
|
217
|
+
</PanoViewer>
|
|
218
|
+
</>
|
|
219
|
+
);
|
|
220
|
+
}
|
|
221
|
+
```
|
|
222
|
+
|
|
223
|
+
### WebVR
|
|
224
|
+
|
|
225
|
+
`enterVR` 与 `requestVRPermission` 须在用户手势中调用。
|
|
226
|
+
|
|
227
|
+
```tsx
|
|
228
|
+
import { useRef } from "react";
|
|
229
|
+
import { PanoViewer, Sphere, WebVR, type PanoViewerHandle } from "@ericchen1990/pano-view";
|
|
230
|
+
|
|
231
|
+
function VRExample() {
|
|
232
|
+
const ref = useRef<PanoViewerHandle>(null);
|
|
233
|
+
|
|
234
|
+
return (
|
|
235
|
+
<>
|
|
236
|
+
<button
|
|
237
|
+
onClick={async () => {
|
|
238
|
+
await ref.current?.requestVRPermission();
|
|
239
|
+
await ref.current?.enterVR();
|
|
240
|
+
}}
|
|
241
|
+
>
|
|
242
|
+
进入 VR
|
|
243
|
+
</button>
|
|
244
|
+
<button onClick={() => void ref.current?.exitVR()}>退出 VR</button>
|
|
245
|
+
<button onClick={() => console.log(ref.current?.getVRMode())}>当前模式?</button>
|
|
246
|
+
<PanoViewer ref={ref} style={{ height: 560 }}>
|
|
247
|
+
<WebVR />
|
|
248
|
+
<Sphere src="/panoramas/room.webp" previewUrl="preview.webp" />
|
|
249
|
+
</PanoViewer>
|
|
250
|
+
</>
|
|
251
|
+
);
|
|
252
|
+
}
|
|
253
|
+
```
|
|
254
|
+
|
|
255
|
+
### 自定义视频控件
|
|
256
|
+
|
|
257
|
+
将 `PanoVideo` 的 `controls` 设为 `false`,从 viewer ref 驱动播放。配合 `useSyncExternalStore` 使用 `subscribeVideo` 获取响应式快照。
|
|
258
|
+
|
|
259
|
+
```tsx
|
|
260
|
+
import { useRef, useSyncExternalStore } from "react";
|
|
261
|
+
import {
|
|
262
|
+
PanoViewer,
|
|
263
|
+
PanoVideo,
|
|
264
|
+
type PanoViewerHandle,
|
|
265
|
+
} from "@ericchen1990/pano-view";
|
|
266
|
+
|
|
267
|
+
function VideoExample() {
|
|
268
|
+
const ref = useRef<PanoViewerHandle>(null);
|
|
269
|
+
|
|
270
|
+
const snapshot = useSyncExternalStore(
|
|
271
|
+
(cb) => ref.current?.subscribeVideo(cb) ?? (() => {}),
|
|
272
|
+
() => ref.current?.getVideo()?.getSnapshot() ?? null,
|
|
273
|
+
() => null,
|
|
274
|
+
);
|
|
275
|
+
|
|
276
|
+
return (
|
|
277
|
+
<>
|
|
278
|
+
<button onClick={() => void ref.current?.toggleVideo()}>
|
|
279
|
+
{snapshot?.playing ? "暂停" : "播放"}
|
|
280
|
+
</button>
|
|
281
|
+
<button onClick={() => ref.current?.toggleVideoMuted()}>
|
|
282
|
+
{snapshot?.muted ? "取消静音" : "静音"}
|
|
283
|
+
</button>
|
|
284
|
+
{snapshot && (
|
|
285
|
+
<input
|
|
286
|
+
type="range"
|
|
287
|
+
min={0}
|
|
288
|
+
max={snapshot.duration}
|
|
289
|
+
value={snapshot.currentTime}
|
|
290
|
+
onChange={(e) => ref.current?.seekVideo(Number(e.target.value))}
|
|
291
|
+
/>
|
|
292
|
+
)}
|
|
293
|
+
<PanoViewer ref={ref} style={{ height: 560 }}>
|
|
294
|
+
<PanoVideo controls={false} src="/tour.mp4" />
|
|
295
|
+
</PanoViewer>
|
|
296
|
+
</>
|
|
297
|
+
);
|
|
298
|
+
}
|
|
299
|
+
```
|
|
300
|
+
|
|
301
|
+
### 自定义背景音乐控件
|
|
302
|
+
|
|
303
|
+
`BackgroundAudio` 支持非受控模式 — 省略 `playing`,改用 `defaultPlaying`。通过 viewer ref 驱动。
|
|
304
|
+
|
|
305
|
+
```tsx
|
|
306
|
+
import { useRef, useSyncExternalStore } from "react";
|
|
307
|
+
import {
|
|
308
|
+
PanoViewer,
|
|
309
|
+
Sphere,
|
|
310
|
+
BackgroundAudio,
|
|
311
|
+
type PanoViewerHandle,
|
|
312
|
+
} from "@ericchen1990/pano-view";
|
|
313
|
+
|
|
314
|
+
function BGMExample() {
|
|
315
|
+
const ref = useRef<PanoViewerHandle>(null);
|
|
316
|
+
|
|
317
|
+
const snapshot = useSyncExternalStore(
|
|
318
|
+
(cb) => ref.current?.subscribeBackgroundAudio(cb) ?? (() => {}),
|
|
319
|
+
() => ref.current?.getBackgroundAudio()?.getSnapshot() ?? null,
|
|
320
|
+
() => null,
|
|
321
|
+
);
|
|
322
|
+
|
|
323
|
+
return (
|
|
324
|
+
<>
|
|
325
|
+
<button onClick={() => ref.current?.toggleBackgroundAudio()}>
|
|
326
|
+
{snapshot?.playing ? "暂停 BGM" : "播放 BGM"}
|
|
327
|
+
</button>
|
|
328
|
+
<button onClick={() => ref.current?.toggleBackgroundAudioMuted()}>
|
|
329
|
+
{snapshot?.muted ? "取消静音" : "静音"}
|
|
330
|
+
</button>
|
|
331
|
+
<input
|
|
332
|
+
type="range"
|
|
333
|
+
min={0}
|
|
334
|
+
max={1}
|
|
335
|
+
step={0.01}
|
|
336
|
+
value={snapshot?.volume ?? 1}
|
|
337
|
+
onChange={(e) =>
|
|
338
|
+
ref.current?.setBackgroundAudioVolume(Number(e.target.value))
|
|
339
|
+
}
|
|
340
|
+
/>
|
|
341
|
+
<PanoViewer ref={ref} style={{ height: 560 }}>
|
|
342
|
+
<BackgroundAudio defaultPlaying src="/bgm.mp3" />
|
|
343
|
+
<Sphere src="/panoramas/room.webp" previewUrl="preview.webp" />
|
|
344
|
+
</PanoViewer>
|
|
345
|
+
</>
|
|
346
|
+
);
|
|
347
|
+
}
|
|
348
|
+
```
|
|
349
|
+
|
|
350
|
+
### 一站式示例
|
|
351
|
+
|
|
352
|
+
```tsx
|
|
353
|
+
import { useRef } from "react";
|
|
354
|
+
import {
|
|
355
|
+
PanoViewer,
|
|
356
|
+
Scenes,
|
|
357
|
+
WebVR,
|
|
358
|
+
PanoVideo,
|
|
359
|
+
BackgroundAudio,
|
|
360
|
+
type PanoViewerHandle,
|
|
361
|
+
} from "@ericchen1990/pano-view";
|
|
362
|
+
|
|
363
|
+
function TourPlayer() {
|
|
364
|
+
const ref = useRef<PanoViewerHandle>(null);
|
|
365
|
+
|
|
366
|
+
return (
|
|
367
|
+
<>
|
|
368
|
+
<nav>
|
|
369
|
+
<button onClick={() => ref.current?.previousScene()}>← 上一场景</button>
|
|
370
|
+
<button onClick={() => ref.current?.nextScene()}>下一场景 →</button>
|
|
371
|
+
<button onClick={() => void ref.current?.toggleFullscreen()}>全屏</button>
|
|
372
|
+
<button onClick={() => void ref.current?.enterVR()}>进入 VR</button>
|
|
373
|
+
<button onClick={() => ref.current?.toggleBackgroundAudio()}>BGM</button>
|
|
374
|
+
</nav>
|
|
375
|
+
<PanoViewer ref={ref} style={{ height: "100vh" }}>
|
|
376
|
+
<WebVR />
|
|
377
|
+
<BackgroundAudio defaultPlaying src="/bgm.mp3" />
|
|
378
|
+
<Scenes defaultActiveSceneId="lobby" scenes={scenes} transition="dissolve" />
|
|
379
|
+
</PanoViewer>
|
|
380
|
+
</>
|
|
381
|
+
);
|
|
382
|
+
}
|
|
383
|
+
```
|
|
384
|
+
|
|
385
|
+
两个已废弃的自动旋转方法(`startAutoRotate`、`stopAutoRotate`)继续保留以兼容。鼠标、触控与键盘输入默认启用 — 普通浏览无需再渲染控件组件。仅当需要覆盖属性时渲染 `MouseControls`、`TouchControls` 或 `KeyboardControls`(子组件会替换该通道的默认实例)。通过 `controls` 调节共享行为(`inertia`、`invert`、`bouncingLimits`、`fovSpeed`、`frictionStop`、`rotateDamping`、`zoomDamping`,以及顶层的 `rotateSpeed` / `zoomSpeed`)。将 `controls.mouse` / `touch` / `keyboard` 设为 `false` 可关闭某通道,或传入选项对象(含 `enabled`)在不挂载子组件的情况下覆盖默认值。自动旋转在渲染 `AutoRotate` 之前处于关闭状态。默认右键菜单(重置视图 / 进入全屏)也会挂载 — 见 [`PanoContextMenu`](#panocontextmenu)。
|
|
151
386
|
|
|
152
387
|
拖拽与缩放会更新目标视图,相机平滑跟随。`rotateDamping` 与 `zoomDamping` 控制跟随速度,单位为秒⁻¹(默认分别为 `14` 与 `16`);数值越低手感越柔和,`0` 表示该轴不做平滑。两者必须为非负有限数。命令式 `setView()` 与 `reset()` 仍为即时生效。
|
|
153
388
|
|