@ericchen1990/pano-view 2.3.0 → 2.5.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 +243 -6
- package/README.zh-CN.md +243 -6
- package/dist/index.cjs +993 -601
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.mts +275 -190
- package/dist/index.d.ts +275 -190
- package/dist/index.mjs +618 -227
- package/dist/index.mjs.map +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -113,6 +113,7 @@ the stylesheet, the overlays still mount, but they render unstyled.
|
|
|
113
113
|
|
|
114
114
|
### Hooks and helpers
|
|
115
115
|
|
|
116
|
+
- `usePanoViewer` — wrap the viewer ref in reusable parent-side commands
|
|
116
117
|
- `usePanoEvents` — subscribe to viewer events inside a custom child ([`PanoEvents`](#panoevents))
|
|
117
118
|
- Coordinate helpers — `normalizePanoPosition`, `normalizePanoYaw`, `clampPanoPitch`, `panoPositionToVector3`, `vector3ToPanoPosition` ([Panorama coordinate events](#panorama-coordinate-events))
|
|
118
119
|
- `cycleSceneId` — wrap previous/next scene ids ([`KeyboardControls`](#keyboardcontrols))
|
|
@@ -126,25 +127,24 @@ the stylesheet, the overlays still mount, but they render unstyled.
|
|
|
126
127
|
Angles are public degrees. Positive yaw looks right and positive pitch looks up.
|
|
127
128
|
|
|
128
129
|
```tsx
|
|
129
|
-
import { useRef } from "react";
|
|
130
130
|
import "@ericchen1990/pano-view/styles.css";
|
|
131
131
|
import {
|
|
132
132
|
PanoViewer,
|
|
133
133
|
Sphere,
|
|
134
134
|
AutoRotate,
|
|
135
|
-
|
|
135
|
+
usePanoViewer,
|
|
136
136
|
} from "@ericchen1990/pano-view";
|
|
137
137
|
|
|
138
138
|
export function ControlledExample() {
|
|
139
|
-
const
|
|
139
|
+
const viewer = usePanoViewer();
|
|
140
140
|
|
|
141
141
|
return (
|
|
142
142
|
<>
|
|
143
|
-
<button onClick={() =>
|
|
143
|
+
<button onClick={() => viewer.setView({ yaw: 90, fov: 55 })}>
|
|
144
144
|
Look right
|
|
145
145
|
</button>
|
|
146
146
|
<PanoViewer
|
|
147
|
-
ref={ref}
|
|
147
|
+
ref={viewer.ref}
|
|
148
148
|
controls={{
|
|
149
149
|
inertia: true,
|
|
150
150
|
rotateDamping: 14,
|
|
@@ -164,7 +164,244 @@ 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 { PanoViewer, Sphere, usePanoViewer } from "@ericchen1990/pano-view";
|
|
187
|
+
|
|
188
|
+
function FullscreenExample() {
|
|
189
|
+
const viewer = usePanoViewer();
|
|
190
|
+
|
|
191
|
+
return (
|
|
192
|
+
<>
|
|
193
|
+
<button onClick={() => void viewer.enterFullscreen()}>Enter fullscreen</button>
|
|
194
|
+
<button onClick={() => void viewer.exitFullscreen()}>Exit fullscreen</button>
|
|
195
|
+
<button onClick={() => console.log(viewer.isFullscreen())}>Is fullscreen?</button>
|
|
196
|
+
<PanoViewer ref={viewer.ref} style={{ height: 560 }}>
|
|
197
|
+
<Sphere src="/panoramas/room.webp" previewUrl="preview.webp" />
|
|
198
|
+
</PanoViewer>
|
|
199
|
+
</>
|
|
200
|
+
);
|
|
201
|
+
}
|
|
202
|
+
```
|
|
203
|
+
|
|
204
|
+
### Imperative scene switching
|
|
205
|
+
|
|
206
|
+
Use `usePanoViewer()` to shorten parent-side commands to `viewer.setScene("roof")`. Keep using controlled `activeSceneId` when the host UI needs to render the current scene id reactively. For uncontrolled mode, `defaultActiveSceneId` still lets imperative calls update the internal state directly.
|
|
207
|
+
|
|
208
|
+
```tsx
|
|
209
|
+
import { useState } from "react";
|
|
210
|
+
import {
|
|
211
|
+
PanoViewer,
|
|
212
|
+
Scenes,
|
|
213
|
+
usePanoViewer,
|
|
214
|
+
type Scene,
|
|
215
|
+
} from "@ericchen1990/pano-view";
|
|
216
|
+
|
|
217
|
+
const scenes: Scene[] = [
|
|
218
|
+
{ id: "lobby", type: "sphere", src: "/lobby.webp", previewUrl: "/lobby-preview.webp" },
|
|
219
|
+
{ id: "roof", type: "sphere", src: "/roof.webp", previewUrl: "/roof-preview.webp" },
|
|
220
|
+
];
|
|
221
|
+
|
|
222
|
+
function SceneExample() {
|
|
223
|
+
const viewer = usePanoViewer();
|
|
224
|
+
const [activeSceneId, setActiveSceneId] = useState("lobby");
|
|
225
|
+
|
|
226
|
+
return (
|
|
227
|
+
<>
|
|
228
|
+
<button onClick={() => setActiveSceneId("roof")}>Go to roof</button>
|
|
229
|
+
<button onClick={() => viewer.nextScene()}>Next</button>
|
|
230
|
+
<button onClick={() => viewer.previousScene()}>Previous</button>
|
|
231
|
+
<button onClick={() => viewer.setScene("roof")}>Jump via hook</button>
|
|
232
|
+
<p>Current: {activeSceneId}</p>
|
|
233
|
+
<PanoViewer ref={viewer.ref} style={{ height: 560 }}>
|
|
234
|
+
<Scenes
|
|
235
|
+
activeSceneId={activeSceneId}
|
|
236
|
+
onActiveSceneIdChange={setActiveSceneId}
|
|
237
|
+
scenes={scenes}
|
|
238
|
+
transition="dissolve"
|
|
239
|
+
/>
|
|
240
|
+
</PanoViewer>
|
|
241
|
+
</>
|
|
242
|
+
);
|
|
243
|
+
}
|
|
244
|
+
```
|
|
245
|
+
|
|
246
|
+
### WebVR
|
|
247
|
+
|
|
248
|
+
`enterVR` and `requestVRPermission` must be called from a user gesture.
|
|
249
|
+
|
|
250
|
+
```tsx
|
|
251
|
+
import { PanoViewer, Sphere, WebVR, usePanoViewer } from "@ericchen1990/pano-view";
|
|
252
|
+
|
|
253
|
+
function VRExample() {
|
|
254
|
+
const viewer = usePanoViewer();
|
|
255
|
+
|
|
256
|
+
return (
|
|
257
|
+
<>
|
|
258
|
+
<button
|
|
259
|
+
onClick={async () => {
|
|
260
|
+
await viewer.requestVRPermission();
|
|
261
|
+
await viewer.enterVR();
|
|
262
|
+
}}
|
|
263
|
+
>
|
|
264
|
+
Enter VR
|
|
265
|
+
</button>
|
|
266
|
+
<button onClick={() => void viewer.exitVR()}>Exit VR</button>
|
|
267
|
+
<button onClick={() => console.log(viewer.getVRMode())}>Mode?</button>
|
|
268
|
+
<PanoViewer ref={viewer.ref} style={{ height: 560 }}>
|
|
269
|
+
<WebVR />
|
|
270
|
+
<Sphere src="/panoramas/room.webp" previewUrl="preview.webp" />
|
|
271
|
+
</PanoViewer>
|
|
272
|
+
</>
|
|
273
|
+
);
|
|
274
|
+
}
|
|
275
|
+
```
|
|
276
|
+
|
|
277
|
+
### Custom video controls
|
|
278
|
+
|
|
279
|
+
Set `controls={false}` on `PanoVideo` and drive playback from `usePanoViewer()`. Use `subscribeVideo` with `useSyncExternalStore` for reactive snapshots.
|
|
280
|
+
|
|
281
|
+
```tsx
|
|
282
|
+
import { useSyncExternalStore } from "react";
|
|
283
|
+
import {
|
|
284
|
+
PanoViewer,
|
|
285
|
+
PanoVideo,
|
|
286
|
+
usePanoViewer,
|
|
287
|
+
} from "@ericchen1990/pano-view";
|
|
288
|
+
|
|
289
|
+
function VideoExample() {
|
|
290
|
+
const viewer = usePanoViewer();
|
|
291
|
+
|
|
292
|
+
const snapshot = useSyncExternalStore(
|
|
293
|
+
viewer.subscribeVideo,
|
|
294
|
+
() => viewer.getVideo()?.getSnapshot() ?? null,
|
|
295
|
+
() => null,
|
|
296
|
+
);
|
|
297
|
+
|
|
298
|
+
return (
|
|
299
|
+
<>
|
|
300
|
+
<button onClick={() => void viewer.toggleVideo()}>
|
|
301
|
+
{snapshot?.playing ? "Pause" : "Play"}
|
|
302
|
+
</button>
|
|
303
|
+
<button onClick={() => viewer.toggleVideoMuted()}>
|
|
304
|
+
{snapshot?.muted ? "Unmute" : "Mute"}
|
|
305
|
+
</button>
|
|
306
|
+
{snapshot && (
|
|
307
|
+
<input
|
|
308
|
+
type="range"
|
|
309
|
+
min={0}
|
|
310
|
+
max={snapshot.duration}
|
|
311
|
+
value={snapshot.currentTime}
|
|
312
|
+
onChange={(e) => viewer.seekVideo(Number(e.target.value))}
|
|
313
|
+
/>
|
|
314
|
+
)}
|
|
315
|
+
<PanoViewer ref={viewer.ref} style={{ height: 560 }}>
|
|
316
|
+
<PanoVideo controls={false} src="/tour.mp4" />
|
|
317
|
+
</PanoViewer>
|
|
318
|
+
</>
|
|
319
|
+
);
|
|
320
|
+
}
|
|
321
|
+
```
|
|
322
|
+
|
|
323
|
+
### Custom background audio controls
|
|
324
|
+
|
|
325
|
+
`BackgroundAudio` supports uncontrolled mode — omit `playing` and use `defaultPlaying` instead. Drive it from `usePanoViewer()`.
|
|
326
|
+
|
|
327
|
+
```tsx
|
|
328
|
+
import { useSyncExternalStore } from "react";
|
|
329
|
+
import {
|
|
330
|
+
PanoViewer,
|
|
331
|
+
Sphere,
|
|
332
|
+
BackgroundAudio,
|
|
333
|
+
usePanoViewer,
|
|
334
|
+
} from "@ericchen1990/pano-view";
|
|
335
|
+
|
|
336
|
+
function BGMExample() {
|
|
337
|
+
const viewer = usePanoViewer();
|
|
338
|
+
|
|
339
|
+
const snapshot = useSyncExternalStore(
|
|
340
|
+
viewer.subscribeBackgroundAudio,
|
|
341
|
+
() => viewer.getBackgroundAudio()?.getSnapshot() ?? null,
|
|
342
|
+
() => null,
|
|
343
|
+
);
|
|
344
|
+
|
|
345
|
+
return (
|
|
346
|
+
<>
|
|
347
|
+
<button onClick={() => viewer.toggleBackgroundAudio()}>
|
|
348
|
+
{snapshot?.playing ? "Pause BGM" : "Play BGM"}
|
|
349
|
+
</button>
|
|
350
|
+
<button onClick={() => viewer.toggleBackgroundAudioMuted()}>
|
|
351
|
+
{snapshot?.muted ? "Unmute" : "Mute"}
|
|
352
|
+
</button>
|
|
353
|
+
<input
|
|
354
|
+
type="range"
|
|
355
|
+
min={0}
|
|
356
|
+
max={1}
|
|
357
|
+
step={0.01}
|
|
358
|
+
value={snapshot?.volume ?? 1}
|
|
359
|
+
onChange={(e) => viewer.setBackgroundAudioVolume(Number(e.target.value))}
|
|
360
|
+
/>
|
|
361
|
+
<PanoViewer ref={viewer.ref} style={{ height: 560 }}>
|
|
362
|
+
<BackgroundAudio defaultPlaying src="/bgm.mp3" />
|
|
363
|
+
<Sphere src="/panoramas/room.webp" previewUrl="preview.webp" />
|
|
364
|
+
</PanoViewer>
|
|
365
|
+
</>
|
|
366
|
+
);
|
|
367
|
+
}
|
|
368
|
+
```
|
|
369
|
+
|
|
370
|
+
### All-in-one example
|
|
371
|
+
|
|
372
|
+
```tsx
|
|
373
|
+
import {
|
|
374
|
+
PanoViewer,
|
|
375
|
+
Scenes,
|
|
376
|
+
WebVR,
|
|
377
|
+
PanoVideo,
|
|
378
|
+
BackgroundAudio,
|
|
379
|
+
usePanoViewer,
|
|
380
|
+
} from "@ericchen1990/pano-view";
|
|
381
|
+
|
|
382
|
+
function TourPlayer() {
|
|
383
|
+
const viewer = usePanoViewer();
|
|
384
|
+
|
|
385
|
+
return (
|
|
386
|
+
<>
|
|
387
|
+
<nav>
|
|
388
|
+
<button onClick={() => viewer.previousScene()}>← Prev</button>
|
|
389
|
+
<button onClick={() => viewer.nextScene()}>Next →</button>
|
|
390
|
+
<button onClick={() => void viewer.toggleFullscreen()}>Fullscreen</button>
|
|
391
|
+
<button onClick={() => void viewer.enterVR()}>Enter VR</button>
|
|
392
|
+
<button onClick={() => viewer.toggleBackgroundAudio()}>BGM</button>
|
|
393
|
+
</nav>
|
|
394
|
+
<PanoViewer ref={viewer.ref} style={{ height: "100vh" }}>
|
|
395
|
+
<WebVR />
|
|
396
|
+
<BackgroundAudio defaultPlaying src="/bgm.mp3" />
|
|
397
|
+
<Scenes defaultActiveSceneId="lobby" scenes={scenes} transition="dissolve" />
|
|
398
|
+
</PanoViewer>
|
|
399
|
+
</>
|
|
400
|
+
);
|
|
401
|
+
}
|
|
402
|
+
```
|
|
403
|
+
|
|
404
|
+
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
405
|
|
|
169
406
|
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
407
|
|
package/README.zh-CN.md
CHANGED
|
@@ -96,6 +96,7 @@ import "@ericchen1990/pano-view/styles.css";
|
|
|
96
96
|
|
|
97
97
|
### Hook 与辅助工具
|
|
98
98
|
|
|
99
|
+
- `usePanoViewer` — 将 viewer ref 封装成可复用的父组件侧命令
|
|
99
100
|
- `usePanoEvents` — 在自定义子组件内订阅查看器事件([`PanoEvents`](#panoevents))
|
|
100
101
|
- 坐标辅助函数 — `normalizePanoPosition`、`normalizePanoYaw`、`clampPanoPitch`、`panoPositionToVector3`、`vector3ToPanoPosition`([全景坐标事件](#全景坐标事件))
|
|
101
102
|
- `cycleSceneId` — 循环上一/下一场景 id([`KeyboardControls`](#keyboardcontrols))
|
|
@@ -109,25 +110,24 @@ import "@ericchen1990/pano-view/styles.css";
|
|
|
109
110
|
角度对外以度为单位。偏航(yaw)为正表示向右看,俯仰(pitch)为正表示向上看。
|
|
110
111
|
|
|
111
112
|
```tsx
|
|
112
|
-
import { useRef } from "react";
|
|
113
113
|
import "@ericchen1990/pano-view/styles.css";
|
|
114
114
|
import {
|
|
115
115
|
PanoViewer,
|
|
116
116
|
Sphere,
|
|
117
117
|
AutoRotate,
|
|
118
|
-
|
|
118
|
+
usePanoViewer,
|
|
119
119
|
} from "@ericchen1990/pano-view";
|
|
120
120
|
|
|
121
121
|
export function ControlledExample() {
|
|
122
|
-
const
|
|
122
|
+
const viewer = usePanoViewer();
|
|
123
123
|
|
|
124
124
|
return (
|
|
125
125
|
<>
|
|
126
|
-
<button onClick={() =>
|
|
126
|
+
<button onClick={() => viewer.setView({ yaw: 90, fov: 55 })}>
|
|
127
127
|
Look right
|
|
128
128
|
</button>
|
|
129
129
|
<PanoViewer
|
|
130
|
-
ref={ref}
|
|
130
|
+
ref={viewer.ref}
|
|
131
131
|
controls={{
|
|
132
132
|
inertia: true,
|
|
133
133
|
rotateDamping: 14,
|
|
@@ -147,7 +147,244 @@ 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 { PanoViewer, Sphere, usePanoViewer } from "@ericchen1990/pano-view";
|
|
170
|
+
|
|
171
|
+
function FullscreenExample() {
|
|
172
|
+
const viewer = usePanoViewer();
|
|
173
|
+
|
|
174
|
+
return (
|
|
175
|
+
<>
|
|
176
|
+
<button onClick={() => void viewer.enterFullscreen()}>进入全屏</button>
|
|
177
|
+
<button onClick={() => void viewer.exitFullscreen()}>退出全屏</button>
|
|
178
|
+
<button onClick={() => console.log(viewer.isFullscreen())}>是否全屏?</button>
|
|
179
|
+
<PanoViewer ref={viewer.ref} style={{ height: 560 }}>
|
|
180
|
+
<Sphere src="/panoramas/room.webp" previewUrl="preview.webp" />
|
|
181
|
+
</PanoViewer>
|
|
182
|
+
</>
|
|
183
|
+
);
|
|
184
|
+
}
|
|
185
|
+
```
|
|
186
|
+
|
|
187
|
+
### 命令式切换场景
|
|
188
|
+
|
|
189
|
+
使用 `usePanoViewer()` 可将父组件侧命令缩短为 `viewer.setScene("roof")`。当宿主 UI 需要响应式显示当前场景 id 时,仍建议继续使用受控 `activeSceneId`。若使用非受控模式,`defaultActiveSceneId` 依然允许命令式调用直接更新内部状态。
|
|
190
|
+
|
|
191
|
+
```tsx
|
|
192
|
+
import { useState } from "react";
|
|
193
|
+
import {
|
|
194
|
+
PanoViewer,
|
|
195
|
+
Scenes,
|
|
196
|
+
usePanoViewer,
|
|
197
|
+
type Scene,
|
|
198
|
+
} from "@ericchen1990/pano-view";
|
|
199
|
+
|
|
200
|
+
const scenes: Scene[] = [
|
|
201
|
+
{ id: "lobby", type: "sphere", src: "/lobby.webp", previewUrl: "/lobby-preview.webp" },
|
|
202
|
+
{ id: "roof", type: "sphere", src: "/roof.webp", previewUrl: "/roof-preview.webp" },
|
|
203
|
+
];
|
|
204
|
+
|
|
205
|
+
function SceneExample() {
|
|
206
|
+
const viewer = usePanoViewer();
|
|
207
|
+
const [activeSceneId, setActiveSceneId] = useState("lobby");
|
|
208
|
+
|
|
209
|
+
return (
|
|
210
|
+
<>
|
|
211
|
+
<button onClick={() => setActiveSceneId("roof")}>去天台</button>
|
|
212
|
+
<button onClick={() => viewer.nextScene()}>下一场景</button>
|
|
213
|
+
<button onClick={() => viewer.previousScene()}>上一场景</button>
|
|
214
|
+
<button onClick={() => viewer.setScene("roof")}>通过 hook 跳转</button>
|
|
215
|
+
<p>当前: {activeSceneId}</p>
|
|
216
|
+
<PanoViewer ref={viewer.ref} style={{ height: 560 }}>
|
|
217
|
+
<Scenes
|
|
218
|
+
activeSceneId={activeSceneId}
|
|
219
|
+
onActiveSceneIdChange={setActiveSceneId}
|
|
220
|
+
scenes={scenes}
|
|
221
|
+
transition="dissolve"
|
|
222
|
+
/>
|
|
223
|
+
</PanoViewer>
|
|
224
|
+
</>
|
|
225
|
+
);
|
|
226
|
+
}
|
|
227
|
+
```
|
|
228
|
+
|
|
229
|
+
### WebVR
|
|
230
|
+
|
|
231
|
+
`enterVR` 与 `requestVRPermission` 须在用户手势中调用。
|
|
232
|
+
|
|
233
|
+
```tsx
|
|
234
|
+
import { PanoViewer, Sphere, WebVR, usePanoViewer } from "@ericchen1990/pano-view";
|
|
235
|
+
|
|
236
|
+
function VRExample() {
|
|
237
|
+
const viewer = usePanoViewer();
|
|
238
|
+
|
|
239
|
+
return (
|
|
240
|
+
<>
|
|
241
|
+
<button
|
|
242
|
+
onClick={async () => {
|
|
243
|
+
await viewer.requestVRPermission();
|
|
244
|
+
await viewer.enterVR();
|
|
245
|
+
}}
|
|
246
|
+
>
|
|
247
|
+
进入 VR
|
|
248
|
+
</button>
|
|
249
|
+
<button onClick={() => void viewer.exitVR()}>退出 VR</button>
|
|
250
|
+
<button onClick={() => console.log(viewer.getVRMode())}>当前模式?</button>
|
|
251
|
+
<PanoViewer ref={viewer.ref} style={{ height: 560 }}>
|
|
252
|
+
<WebVR />
|
|
253
|
+
<Sphere src="/panoramas/room.webp" previewUrl="preview.webp" />
|
|
254
|
+
</PanoViewer>
|
|
255
|
+
</>
|
|
256
|
+
);
|
|
257
|
+
}
|
|
258
|
+
```
|
|
259
|
+
|
|
260
|
+
### 自定义视频控件
|
|
261
|
+
|
|
262
|
+
将 `PanoVideo` 的 `controls` 设为 `false`,通过 `usePanoViewer()` 驱动播放。配合 `useSyncExternalStore` 使用 `subscribeVideo` 获取响应式快照。
|
|
263
|
+
|
|
264
|
+
```tsx
|
|
265
|
+
import { useSyncExternalStore } from "react";
|
|
266
|
+
import {
|
|
267
|
+
PanoViewer,
|
|
268
|
+
PanoVideo,
|
|
269
|
+
usePanoViewer,
|
|
270
|
+
} from "@ericchen1990/pano-view";
|
|
271
|
+
|
|
272
|
+
function VideoExample() {
|
|
273
|
+
const viewer = usePanoViewer();
|
|
274
|
+
|
|
275
|
+
const snapshot = useSyncExternalStore(
|
|
276
|
+
viewer.subscribeVideo,
|
|
277
|
+
() => viewer.getVideo()?.getSnapshot() ?? null,
|
|
278
|
+
() => null,
|
|
279
|
+
);
|
|
280
|
+
|
|
281
|
+
return (
|
|
282
|
+
<>
|
|
283
|
+
<button onClick={() => void viewer.toggleVideo()}>
|
|
284
|
+
{snapshot?.playing ? "暂停" : "播放"}
|
|
285
|
+
</button>
|
|
286
|
+
<button onClick={() => viewer.toggleVideoMuted()}>
|
|
287
|
+
{snapshot?.muted ? "取消静音" : "静音"}
|
|
288
|
+
</button>
|
|
289
|
+
{snapshot && (
|
|
290
|
+
<input
|
|
291
|
+
type="range"
|
|
292
|
+
min={0}
|
|
293
|
+
max={snapshot.duration}
|
|
294
|
+
value={snapshot.currentTime}
|
|
295
|
+
onChange={(e) => viewer.seekVideo(Number(e.target.value))}
|
|
296
|
+
/>
|
|
297
|
+
)}
|
|
298
|
+
<PanoViewer ref={viewer.ref} style={{ height: 560 }}>
|
|
299
|
+
<PanoVideo controls={false} src="/tour.mp4" />
|
|
300
|
+
</PanoViewer>
|
|
301
|
+
</>
|
|
302
|
+
);
|
|
303
|
+
}
|
|
304
|
+
```
|
|
305
|
+
|
|
306
|
+
### 自定义背景音乐控件
|
|
307
|
+
|
|
308
|
+
`BackgroundAudio` 支持非受控模式 — 省略 `playing`,改用 `defaultPlaying`。通过 `usePanoViewer()` 驱动。
|
|
309
|
+
|
|
310
|
+
```tsx
|
|
311
|
+
import { useSyncExternalStore } from "react";
|
|
312
|
+
import {
|
|
313
|
+
PanoViewer,
|
|
314
|
+
Sphere,
|
|
315
|
+
BackgroundAudio,
|
|
316
|
+
usePanoViewer,
|
|
317
|
+
} from "@ericchen1990/pano-view";
|
|
318
|
+
|
|
319
|
+
function BGMExample() {
|
|
320
|
+
const viewer = usePanoViewer();
|
|
321
|
+
|
|
322
|
+
const snapshot = useSyncExternalStore(
|
|
323
|
+
viewer.subscribeBackgroundAudio,
|
|
324
|
+
() => viewer.getBackgroundAudio()?.getSnapshot() ?? null,
|
|
325
|
+
() => null,
|
|
326
|
+
);
|
|
327
|
+
|
|
328
|
+
return (
|
|
329
|
+
<>
|
|
330
|
+
<button onClick={() => viewer.toggleBackgroundAudio()}>
|
|
331
|
+
{snapshot?.playing ? "暂停 BGM" : "播放 BGM"}
|
|
332
|
+
</button>
|
|
333
|
+
<button onClick={() => viewer.toggleBackgroundAudioMuted()}>
|
|
334
|
+
{snapshot?.muted ? "取消静音" : "静音"}
|
|
335
|
+
</button>
|
|
336
|
+
<input
|
|
337
|
+
type="range"
|
|
338
|
+
min={0}
|
|
339
|
+
max={1}
|
|
340
|
+
step={0.01}
|
|
341
|
+
value={snapshot?.volume ?? 1}
|
|
342
|
+
onChange={(e) => viewer.setBackgroundAudioVolume(Number(e.target.value))}
|
|
343
|
+
/>
|
|
344
|
+
<PanoViewer ref={viewer.ref} style={{ height: 560 }}>
|
|
345
|
+
<BackgroundAudio defaultPlaying src="/bgm.mp3" />
|
|
346
|
+
<Sphere src="/panoramas/room.webp" previewUrl="preview.webp" />
|
|
347
|
+
</PanoViewer>
|
|
348
|
+
</>
|
|
349
|
+
);
|
|
350
|
+
}
|
|
351
|
+
```
|
|
352
|
+
|
|
353
|
+
### 一站式示例
|
|
354
|
+
|
|
355
|
+
```tsx
|
|
356
|
+
import {
|
|
357
|
+
PanoViewer,
|
|
358
|
+
Scenes,
|
|
359
|
+
WebVR,
|
|
360
|
+
PanoVideo,
|
|
361
|
+
BackgroundAudio,
|
|
362
|
+
usePanoViewer,
|
|
363
|
+
} from "@ericchen1990/pano-view";
|
|
364
|
+
|
|
365
|
+
function TourPlayer() {
|
|
366
|
+
const viewer = usePanoViewer();
|
|
367
|
+
|
|
368
|
+
return (
|
|
369
|
+
<>
|
|
370
|
+
<nav>
|
|
371
|
+
<button onClick={() => viewer.previousScene()}>← 上一场景</button>
|
|
372
|
+
<button onClick={() => viewer.nextScene()}>下一场景 →</button>
|
|
373
|
+
<button onClick={() => void viewer.toggleFullscreen()}>全屏</button>
|
|
374
|
+
<button onClick={() => void viewer.enterVR()}>进入 VR</button>
|
|
375
|
+
<button onClick={() => viewer.toggleBackgroundAudio()}>BGM</button>
|
|
376
|
+
</nav>
|
|
377
|
+
<PanoViewer ref={viewer.ref} style={{ height: "100vh" }}>
|
|
378
|
+
<WebVR />
|
|
379
|
+
<BackgroundAudio defaultPlaying src="/bgm.mp3" />
|
|
380
|
+
<Scenes defaultActiveSceneId="lobby" scenes={scenes} transition="dissolve" />
|
|
381
|
+
</PanoViewer>
|
|
382
|
+
</>
|
|
383
|
+
);
|
|
384
|
+
}
|
|
385
|
+
```
|
|
386
|
+
|
|
387
|
+
两个已废弃的自动旋转方法(`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
388
|
|
|
152
389
|
拖拽与缩放会更新目标视图,相机平滑跟随。`rotateDamping` 与 `zoomDamping` 控制跟随速度,单位为秒⁻¹(默认分别为 `14` 与 `16`);数值越低手感越柔和,`0` 表示该轴不做平滑。两者必须为非负有限数。命令式 `setView()` 与 `reset()` 仍为即时生效。
|
|
153
390
|
|