@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 CHANGED
@@ -164,7 +164,242 @@ export function ControlledExample() {
164
164
  }
165
165
  ```
166
166
 
167
- The handle exposes `getView`, `setView`, `reset`, `startAutoRotate`, `stopAutoRotate`, and `toggleFullscreen`. 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`. The two auto-rotation handle methods remain supported for compatibility. A default context menu (Reset view / Enter fullscreen) is also mounted — see [`PanoContextMenu`](#panocontextmenu).
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
- 句柄暴露 `getView`、`setView`、`reset`、`startAutoRotate`、`stopAutoRotate` 与 `toggleFullscreen`。鼠标、触控与键盘输入默认启用 — 普通浏览无需再渲染控件组件。仅当需要覆盖属性时渲染 `MouseControls`、`TouchControls` 或 `KeyboardControls`(子组件会替换该通道的默认实例)。通过 `controls` 调节共享行为(`inertia`、`invert`、`bouncingLimits`、`fovSpeed`、`frictionStop`、`rotateDamping`、`zoomDamping`,以及顶层的 `rotateSpeed` / `zoomSpeed`)。将 `controls.mouse` / `touch` / `keyboard` 设为 `false` 可关闭某通道,或传入选项对象(含 `enabled`)在不挂载子组件的情况下覆盖默认值。自动旋转在渲染 `AutoRotate` 之前处于关闭状态。两个自动旋转句柄方法仍保留以兼容旧用法。默认右键菜单(重置视图 / 进入全屏)也会挂载 — 见 [`PanoContextMenu`](#panocontextmenu)。
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