@ericchen1990/pano-view 2.4.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 +63 -61
- package/README.zh-CN.md +63 -61
- package/dist/index.cjs +356 -271
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.mts +7 -2
- package/dist/index.d.ts +7 -2
- package/dist/index.mjs +230 -146
- 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,
|
|
@@ -183,18 +183,17 @@ Methods that depend on an unmounted child component (e.g. `enterVR` without `<We
|
|
|
183
183
|
### Fullscreen
|
|
184
184
|
|
|
185
185
|
```tsx
|
|
186
|
-
import {
|
|
187
|
-
import { PanoViewer, Sphere, type PanoViewerHandle } from "@ericchen1990/pano-view";
|
|
186
|
+
import { PanoViewer, Sphere, usePanoViewer } from "@ericchen1990/pano-view";
|
|
188
187
|
|
|
189
188
|
function FullscreenExample() {
|
|
190
|
-
const
|
|
189
|
+
const viewer = usePanoViewer();
|
|
191
190
|
|
|
192
191
|
return (
|
|
193
192
|
<>
|
|
194
|
-
<button onClick={() => void
|
|
195
|
-
<button onClick={() => void
|
|
196
|
-
<button onClick={() => console.log(
|
|
197
|
-
<PanoViewer ref={ref} style={{ height: 560 }}>
|
|
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 }}>
|
|
198
197
|
<Sphere src="/panoramas/room.webp" previewUrl="preview.webp" />
|
|
199
198
|
</PanoViewer>
|
|
200
199
|
</>
|
|
@@ -204,14 +203,14 @@ function FullscreenExample() {
|
|
|
204
203
|
|
|
205
204
|
### Imperative scene switching
|
|
206
205
|
|
|
207
|
-
Use `
|
|
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.
|
|
208
207
|
|
|
209
208
|
```tsx
|
|
210
|
-
import {
|
|
209
|
+
import { useState } from "react";
|
|
211
210
|
import {
|
|
212
211
|
PanoViewer,
|
|
213
212
|
Scenes,
|
|
214
|
-
|
|
213
|
+
usePanoViewer,
|
|
215
214
|
type Scene,
|
|
216
215
|
} from "@ericchen1990/pano-view";
|
|
217
216
|
|
|
@@ -221,16 +220,23 @@ const scenes: Scene[] = [
|
|
|
221
220
|
];
|
|
222
221
|
|
|
223
222
|
function SceneExample() {
|
|
224
|
-
const
|
|
223
|
+
const viewer = usePanoViewer();
|
|
224
|
+
const [activeSceneId, setActiveSceneId] = useState("lobby");
|
|
225
225
|
|
|
226
226
|
return (
|
|
227
227
|
<>
|
|
228
|
-
<button onClick={() =>
|
|
229
|
-
<button onClick={() =>
|
|
230
|
-
<button onClick={() =>
|
|
231
|
-
<
|
|
232
|
-
<
|
|
233
|
-
|
|
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
|
+
/>
|
|
234
240
|
</PanoViewer>
|
|
235
241
|
</>
|
|
236
242
|
);
|
|
@@ -242,25 +248,24 @@ function SceneExample() {
|
|
|
242
248
|
`enterVR` and `requestVRPermission` must be called from a user gesture.
|
|
243
249
|
|
|
244
250
|
```tsx
|
|
245
|
-
import {
|
|
246
|
-
import { PanoViewer, Sphere, WebVR, type PanoViewerHandle } from "@ericchen1990/pano-view";
|
|
251
|
+
import { PanoViewer, Sphere, WebVR, usePanoViewer } from "@ericchen1990/pano-view";
|
|
247
252
|
|
|
248
253
|
function VRExample() {
|
|
249
|
-
const
|
|
254
|
+
const viewer = usePanoViewer();
|
|
250
255
|
|
|
251
256
|
return (
|
|
252
257
|
<>
|
|
253
258
|
<button
|
|
254
259
|
onClick={async () => {
|
|
255
|
-
await
|
|
256
|
-
await
|
|
260
|
+
await viewer.requestVRPermission();
|
|
261
|
+
await viewer.enterVR();
|
|
257
262
|
}}
|
|
258
263
|
>
|
|
259
264
|
Enter VR
|
|
260
265
|
</button>
|
|
261
|
-
<button onClick={() => void
|
|
262
|
-
<button onClick={() => console.log(
|
|
263
|
-
<PanoViewer ref={ref} style={{ height: 560 }}>
|
|
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 }}>
|
|
264
269
|
<WebVR />
|
|
265
270
|
<Sphere src="/panoramas/room.webp" previewUrl="preview.webp" />
|
|
266
271
|
</PanoViewer>
|
|
@@ -271,31 +276,31 @@ function VRExample() {
|
|
|
271
276
|
|
|
272
277
|
### Custom video controls
|
|
273
278
|
|
|
274
|
-
Set `controls={false}` on `PanoVideo` and drive playback from
|
|
279
|
+
Set `controls={false}` on `PanoVideo` and drive playback from `usePanoViewer()`. Use `subscribeVideo` with `useSyncExternalStore` for reactive snapshots.
|
|
275
280
|
|
|
276
281
|
```tsx
|
|
277
|
-
import {
|
|
282
|
+
import { useSyncExternalStore } from "react";
|
|
278
283
|
import {
|
|
279
284
|
PanoViewer,
|
|
280
285
|
PanoVideo,
|
|
281
|
-
|
|
286
|
+
usePanoViewer,
|
|
282
287
|
} from "@ericchen1990/pano-view";
|
|
283
288
|
|
|
284
289
|
function VideoExample() {
|
|
285
|
-
const
|
|
290
|
+
const viewer = usePanoViewer();
|
|
286
291
|
|
|
287
292
|
const snapshot = useSyncExternalStore(
|
|
288
|
-
|
|
289
|
-
() =>
|
|
293
|
+
viewer.subscribeVideo,
|
|
294
|
+
() => viewer.getVideo()?.getSnapshot() ?? null,
|
|
290
295
|
() => null,
|
|
291
296
|
);
|
|
292
297
|
|
|
293
298
|
return (
|
|
294
299
|
<>
|
|
295
|
-
<button onClick={() => void
|
|
300
|
+
<button onClick={() => void viewer.toggleVideo()}>
|
|
296
301
|
{snapshot?.playing ? "Pause" : "Play"}
|
|
297
302
|
</button>
|
|
298
|
-
<button onClick={() =>
|
|
303
|
+
<button onClick={() => viewer.toggleVideoMuted()}>
|
|
299
304
|
{snapshot?.muted ? "Unmute" : "Mute"}
|
|
300
305
|
</button>
|
|
301
306
|
{snapshot && (
|
|
@@ -304,10 +309,10 @@ function VideoExample() {
|
|
|
304
309
|
min={0}
|
|
305
310
|
max={snapshot.duration}
|
|
306
311
|
value={snapshot.currentTime}
|
|
307
|
-
onChange={(e) =>
|
|
312
|
+
onChange={(e) => viewer.seekVideo(Number(e.target.value))}
|
|
308
313
|
/>
|
|
309
314
|
)}
|
|
310
|
-
<PanoViewer ref={ref} style={{ height: 560 }}>
|
|
315
|
+
<PanoViewer ref={viewer.ref} style={{ height: 560 }}>
|
|
311
316
|
<PanoVideo controls={false} src="/tour.mp4" />
|
|
312
317
|
</PanoViewer>
|
|
313
318
|
</>
|
|
@@ -317,32 +322,32 @@ function VideoExample() {
|
|
|
317
322
|
|
|
318
323
|
### Custom background audio controls
|
|
319
324
|
|
|
320
|
-
`BackgroundAudio` supports uncontrolled mode — omit `playing` and use `defaultPlaying` instead. Drive it from
|
|
325
|
+
`BackgroundAudio` supports uncontrolled mode — omit `playing` and use `defaultPlaying` instead. Drive it from `usePanoViewer()`.
|
|
321
326
|
|
|
322
327
|
```tsx
|
|
323
|
-
import {
|
|
328
|
+
import { useSyncExternalStore } from "react";
|
|
324
329
|
import {
|
|
325
330
|
PanoViewer,
|
|
326
331
|
Sphere,
|
|
327
332
|
BackgroundAudio,
|
|
328
|
-
|
|
333
|
+
usePanoViewer,
|
|
329
334
|
} from "@ericchen1990/pano-view";
|
|
330
335
|
|
|
331
336
|
function BGMExample() {
|
|
332
|
-
const
|
|
337
|
+
const viewer = usePanoViewer();
|
|
333
338
|
|
|
334
339
|
const snapshot = useSyncExternalStore(
|
|
335
|
-
|
|
336
|
-
() =>
|
|
340
|
+
viewer.subscribeBackgroundAudio,
|
|
341
|
+
() => viewer.getBackgroundAudio()?.getSnapshot() ?? null,
|
|
337
342
|
() => null,
|
|
338
343
|
);
|
|
339
344
|
|
|
340
345
|
return (
|
|
341
346
|
<>
|
|
342
|
-
<button onClick={() =>
|
|
347
|
+
<button onClick={() => viewer.toggleBackgroundAudio()}>
|
|
343
348
|
{snapshot?.playing ? "Pause BGM" : "Play BGM"}
|
|
344
349
|
</button>
|
|
345
|
-
<button onClick={() =>
|
|
350
|
+
<button onClick={() => viewer.toggleBackgroundAudioMuted()}>
|
|
346
351
|
{snapshot?.muted ? "Unmute" : "Mute"}
|
|
347
352
|
</button>
|
|
348
353
|
<input
|
|
@@ -351,11 +356,9 @@ function BGMExample() {
|
|
|
351
356
|
max={1}
|
|
352
357
|
step={0.01}
|
|
353
358
|
value={snapshot?.volume ?? 1}
|
|
354
|
-
onChange={(e) =>
|
|
355
|
-
ref.current?.setBackgroundAudioVolume(Number(e.target.value))
|
|
356
|
-
}
|
|
359
|
+
onChange={(e) => viewer.setBackgroundAudioVolume(Number(e.target.value))}
|
|
357
360
|
/>
|
|
358
|
-
<PanoViewer ref={ref} style={{ height: 560 }}>
|
|
361
|
+
<PanoViewer ref={viewer.ref} style={{ height: 560 }}>
|
|
359
362
|
<BackgroundAudio defaultPlaying src="/bgm.mp3" />
|
|
360
363
|
<Sphere src="/panoramas/room.webp" previewUrl="preview.webp" />
|
|
361
364
|
</PanoViewer>
|
|
@@ -367,29 +370,28 @@ function BGMExample() {
|
|
|
367
370
|
### All-in-one example
|
|
368
371
|
|
|
369
372
|
```tsx
|
|
370
|
-
import { useRef } from "react";
|
|
371
373
|
import {
|
|
372
374
|
PanoViewer,
|
|
373
375
|
Scenes,
|
|
374
376
|
WebVR,
|
|
375
377
|
PanoVideo,
|
|
376
378
|
BackgroundAudio,
|
|
377
|
-
|
|
379
|
+
usePanoViewer,
|
|
378
380
|
} from "@ericchen1990/pano-view";
|
|
379
381
|
|
|
380
382
|
function TourPlayer() {
|
|
381
|
-
const
|
|
383
|
+
const viewer = usePanoViewer();
|
|
382
384
|
|
|
383
385
|
return (
|
|
384
386
|
<>
|
|
385
387
|
<nav>
|
|
386
|
-
<button onClick={() =>
|
|
387
|
-
<button onClick={() =>
|
|
388
|
-
<button onClick={() => void
|
|
389
|
-
<button onClick={() => void
|
|
390
|
-
<button onClick={() =>
|
|
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>
|
|
391
393
|
</nav>
|
|
392
|
-
<PanoViewer ref={ref} style={{ height: "100vh" }}>
|
|
394
|
+
<PanoViewer ref={viewer.ref} style={{ height: "100vh" }}>
|
|
393
395
|
<WebVR />
|
|
394
396
|
<BackgroundAudio defaultPlaying src="/bgm.mp3" />
|
|
395
397
|
<Scenes defaultActiveSceneId="lobby" scenes={scenes} transition="dissolve" />
|
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,
|
|
@@ -166,18 +166,17 @@ export function ControlledExample() {
|
|
|
166
166
|
### 全屏
|
|
167
167
|
|
|
168
168
|
```tsx
|
|
169
|
-
import {
|
|
170
|
-
import { PanoViewer, Sphere, type PanoViewerHandle } from "@ericchen1990/pano-view";
|
|
169
|
+
import { PanoViewer, Sphere, usePanoViewer } from "@ericchen1990/pano-view";
|
|
171
170
|
|
|
172
171
|
function FullscreenExample() {
|
|
173
|
-
const
|
|
172
|
+
const viewer = usePanoViewer();
|
|
174
173
|
|
|
175
174
|
return (
|
|
176
175
|
<>
|
|
177
|
-
<button onClick={() => void
|
|
178
|
-
<button onClick={() => void
|
|
179
|
-
<button onClick={() => console.log(
|
|
180
|
-
<PanoViewer ref={ref} style={{ height: 560 }}>
|
|
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 }}>
|
|
181
180
|
<Sphere src="/panoramas/room.webp" previewUrl="preview.webp" />
|
|
182
181
|
</PanoViewer>
|
|
183
182
|
</>
|
|
@@ -187,14 +186,14 @@ function FullscreenExample() {
|
|
|
187
186
|
|
|
188
187
|
### 命令式切换场景
|
|
189
188
|
|
|
190
|
-
使用 `
|
|
189
|
+
使用 `usePanoViewer()` 可将父组件侧命令缩短为 `viewer.setScene("roof")`。当宿主 UI 需要响应式显示当前场景 id 时,仍建议继续使用受控 `activeSceneId`。若使用非受控模式,`defaultActiveSceneId` 依然允许命令式调用直接更新内部状态。
|
|
191
190
|
|
|
192
191
|
```tsx
|
|
193
|
-
import {
|
|
192
|
+
import { useState } from "react";
|
|
194
193
|
import {
|
|
195
194
|
PanoViewer,
|
|
196
195
|
Scenes,
|
|
197
|
-
|
|
196
|
+
usePanoViewer,
|
|
198
197
|
type Scene,
|
|
199
198
|
} from "@ericchen1990/pano-view";
|
|
200
199
|
|
|
@@ -204,16 +203,23 @@ const scenes: Scene[] = [
|
|
|
204
203
|
];
|
|
205
204
|
|
|
206
205
|
function SceneExample() {
|
|
207
|
-
const
|
|
206
|
+
const viewer = usePanoViewer();
|
|
207
|
+
const [activeSceneId, setActiveSceneId] = useState("lobby");
|
|
208
208
|
|
|
209
209
|
return (
|
|
210
210
|
<>
|
|
211
|
-
<button onClick={() =>
|
|
212
|
-
<button onClick={() =>
|
|
213
|
-
<button onClick={() =>
|
|
214
|
-
<
|
|
215
|
-
<
|
|
216
|
-
|
|
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
|
+
/>
|
|
217
223
|
</PanoViewer>
|
|
218
224
|
</>
|
|
219
225
|
);
|
|
@@ -225,25 +231,24 @@ function SceneExample() {
|
|
|
225
231
|
`enterVR` 与 `requestVRPermission` 须在用户手势中调用。
|
|
226
232
|
|
|
227
233
|
```tsx
|
|
228
|
-
import {
|
|
229
|
-
import { PanoViewer, Sphere, WebVR, type PanoViewerHandle } from "@ericchen1990/pano-view";
|
|
234
|
+
import { PanoViewer, Sphere, WebVR, usePanoViewer } from "@ericchen1990/pano-view";
|
|
230
235
|
|
|
231
236
|
function VRExample() {
|
|
232
|
-
const
|
|
237
|
+
const viewer = usePanoViewer();
|
|
233
238
|
|
|
234
239
|
return (
|
|
235
240
|
<>
|
|
236
241
|
<button
|
|
237
242
|
onClick={async () => {
|
|
238
|
-
await
|
|
239
|
-
await
|
|
243
|
+
await viewer.requestVRPermission();
|
|
244
|
+
await viewer.enterVR();
|
|
240
245
|
}}
|
|
241
246
|
>
|
|
242
247
|
进入 VR
|
|
243
248
|
</button>
|
|
244
|
-
<button onClick={() => void
|
|
245
|
-
<button onClick={() => console.log(
|
|
246
|
-
<PanoViewer ref={ref} style={{ height: 560 }}>
|
|
249
|
+
<button onClick={() => void viewer.exitVR()}>退出 VR</button>
|
|
250
|
+
<button onClick={() => console.log(viewer.getVRMode())}>当前模式?</button>
|
|
251
|
+
<PanoViewer ref={viewer.ref} style={{ height: 560 }}>
|
|
247
252
|
<WebVR />
|
|
248
253
|
<Sphere src="/panoramas/room.webp" previewUrl="preview.webp" />
|
|
249
254
|
</PanoViewer>
|
|
@@ -254,31 +259,31 @@ function VRExample() {
|
|
|
254
259
|
|
|
255
260
|
### 自定义视频控件
|
|
256
261
|
|
|
257
|
-
将 `PanoVideo` 的 `controls` 设为 `false
|
|
262
|
+
将 `PanoVideo` 的 `controls` 设为 `false`,通过 `usePanoViewer()` 驱动播放。配合 `useSyncExternalStore` 使用 `subscribeVideo` 获取响应式快照。
|
|
258
263
|
|
|
259
264
|
```tsx
|
|
260
|
-
import {
|
|
265
|
+
import { useSyncExternalStore } from "react";
|
|
261
266
|
import {
|
|
262
267
|
PanoViewer,
|
|
263
268
|
PanoVideo,
|
|
264
|
-
|
|
269
|
+
usePanoViewer,
|
|
265
270
|
} from "@ericchen1990/pano-view";
|
|
266
271
|
|
|
267
272
|
function VideoExample() {
|
|
268
|
-
const
|
|
273
|
+
const viewer = usePanoViewer();
|
|
269
274
|
|
|
270
275
|
const snapshot = useSyncExternalStore(
|
|
271
|
-
|
|
272
|
-
() =>
|
|
276
|
+
viewer.subscribeVideo,
|
|
277
|
+
() => viewer.getVideo()?.getSnapshot() ?? null,
|
|
273
278
|
() => null,
|
|
274
279
|
);
|
|
275
280
|
|
|
276
281
|
return (
|
|
277
282
|
<>
|
|
278
|
-
<button onClick={() => void
|
|
283
|
+
<button onClick={() => void viewer.toggleVideo()}>
|
|
279
284
|
{snapshot?.playing ? "暂停" : "播放"}
|
|
280
285
|
</button>
|
|
281
|
-
<button onClick={() =>
|
|
286
|
+
<button onClick={() => viewer.toggleVideoMuted()}>
|
|
282
287
|
{snapshot?.muted ? "取消静音" : "静音"}
|
|
283
288
|
</button>
|
|
284
289
|
{snapshot && (
|
|
@@ -287,10 +292,10 @@ function VideoExample() {
|
|
|
287
292
|
min={0}
|
|
288
293
|
max={snapshot.duration}
|
|
289
294
|
value={snapshot.currentTime}
|
|
290
|
-
onChange={(e) =>
|
|
295
|
+
onChange={(e) => viewer.seekVideo(Number(e.target.value))}
|
|
291
296
|
/>
|
|
292
297
|
)}
|
|
293
|
-
<PanoViewer ref={ref} style={{ height: 560 }}>
|
|
298
|
+
<PanoViewer ref={viewer.ref} style={{ height: 560 }}>
|
|
294
299
|
<PanoVideo controls={false} src="/tour.mp4" />
|
|
295
300
|
</PanoViewer>
|
|
296
301
|
</>
|
|
@@ -300,32 +305,32 @@ function VideoExample() {
|
|
|
300
305
|
|
|
301
306
|
### 自定义背景音乐控件
|
|
302
307
|
|
|
303
|
-
`BackgroundAudio` 支持非受控模式 — 省略 `playing`,改用 `defaultPlaying`。通过
|
|
308
|
+
`BackgroundAudio` 支持非受控模式 — 省略 `playing`,改用 `defaultPlaying`。通过 `usePanoViewer()` 驱动。
|
|
304
309
|
|
|
305
310
|
```tsx
|
|
306
|
-
import {
|
|
311
|
+
import { useSyncExternalStore } from "react";
|
|
307
312
|
import {
|
|
308
313
|
PanoViewer,
|
|
309
314
|
Sphere,
|
|
310
315
|
BackgroundAudio,
|
|
311
|
-
|
|
316
|
+
usePanoViewer,
|
|
312
317
|
} from "@ericchen1990/pano-view";
|
|
313
318
|
|
|
314
319
|
function BGMExample() {
|
|
315
|
-
const
|
|
320
|
+
const viewer = usePanoViewer();
|
|
316
321
|
|
|
317
322
|
const snapshot = useSyncExternalStore(
|
|
318
|
-
|
|
319
|
-
() =>
|
|
323
|
+
viewer.subscribeBackgroundAudio,
|
|
324
|
+
() => viewer.getBackgroundAudio()?.getSnapshot() ?? null,
|
|
320
325
|
() => null,
|
|
321
326
|
);
|
|
322
327
|
|
|
323
328
|
return (
|
|
324
329
|
<>
|
|
325
|
-
<button onClick={() =>
|
|
330
|
+
<button onClick={() => viewer.toggleBackgroundAudio()}>
|
|
326
331
|
{snapshot?.playing ? "暂停 BGM" : "播放 BGM"}
|
|
327
332
|
</button>
|
|
328
|
-
<button onClick={() =>
|
|
333
|
+
<button onClick={() => viewer.toggleBackgroundAudioMuted()}>
|
|
329
334
|
{snapshot?.muted ? "取消静音" : "静音"}
|
|
330
335
|
</button>
|
|
331
336
|
<input
|
|
@@ -334,11 +339,9 @@ function BGMExample() {
|
|
|
334
339
|
max={1}
|
|
335
340
|
step={0.01}
|
|
336
341
|
value={snapshot?.volume ?? 1}
|
|
337
|
-
onChange={(e) =>
|
|
338
|
-
ref.current?.setBackgroundAudioVolume(Number(e.target.value))
|
|
339
|
-
}
|
|
342
|
+
onChange={(e) => viewer.setBackgroundAudioVolume(Number(e.target.value))}
|
|
340
343
|
/>
|
|
341
|
-
<PanoViewer ref={ref} style={{ height: 560 }}>
|
|
344
|
+
<PanoViewer ref={viewer.ref} style={{ height: 560 }}>
|
|
342
345
|
<BackgroundAudio defaultPlaying src="/bgm.mp3" />
|
|
343
346
|
<Sphere src="/panoramas/room.webp" previewUrl="preview.webp" />
|
|
344
347
|
</PanoViewer>
|
|
@@ -350,29 +353,28 @@ function BGMExample() {
|
|
|
350
353
|
### 一站式示例
|
|
351
354
|
|
|
352
355
|
```tsx
|
|
353
|
-
import { useRef } from "react";
|
|
354
356
|
import {
|
|
355
357
|
PanoViewer,
|
|
356
358
|
Scenes,
|
|
357
359
|
WebVR,
|
|
358
360
|
PanoVideo,
|
|
359
361
|
BackgroundAudio,
|
|
360
|
-
|
|
362
|
+
usePanoViewer,
|
|
361
363
|
} from "@ericchen1990/pano-view";
|
|
362
364
|
|
|
363
365
|
function TourPlayer() {
|
|
364
|
-
const
|
|
366
|
+
const viewer = usePanoViewer();
|
|
365
367
|
|
|
366
368
|
return (
|
|
367
369
|
<>
|
|
368
370
|
<nav>
|
|
369
|
-
<button onClick={() =>
|
|
370
|
-
<button onClick={() =>
|
|
371
|
-
<button onClick={() => void
|
|
372
|
-
<button onClick={() => void
|
|
373
|
-
<button onClick={() =>
|
|
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>
|
|
374
376
|
</nav>
|
|
375
|
-
<PanoViewer ref={ref} style={{ height: "100vh" }}>
|
|
377
|
+
<PanoViewer ref={viewer.ref} style={{ height: "100vh" }}>
|
|
376
378
|
<WebVR />
|
|
377
379
|
<BackgroundAudio defaultPlaying src="/bgm.mp3" />
|
|
378
380
|
<Scenes defaultActiveSceneId="lobby" scenes={scenes} transition="dissolve" />
|