@ericchen1990/pano-view 2.0.0 → 2.2.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
@@ -1,5 +1,7 @@
1
1
  # @ericchen1990/pano-view
2
2
 
3
+ **English** | [简体中文](./README.zh-CN.md)
4
+
3
5
  Composable React components for equirectangular and krpano-style multiresolution panorama viewers.
4
6
 
5
7
  Try the components in the [live playground](https://pano-view-playground.vercel.app/).
@@ -7,36 +9,68 @@ Try the components in the [live playground](https://pano-view-playground.vercel.
7
9
  ## krpano-compatible tile output
8
10
 
9
11
  Use this package when you want a React-native viewer around equirectangular
10
- images or an existing krpano cube-tile pyramid. `Tile` understands the common
11
- krpano-style multires directory layout by default, while keeping rendering,
12
- controls, hotspots, and scene transitions in your React application. It is
13
- compatible with that tile output format but is not affiliated with krpano.
12
+ images or an existing krpano cube-tile pyramid. Copy the krpano `<cube>` `url`
13
+ and `multires` attributes into `Tile`'s `urlTemplate` and `multires`, and copy
14
+ `<preview url>` into `previewUrl`; krpano's on-disk naming is not the same as
15
+ this package's defaults, so omitting them loads the wrong tiles or preview.
16
+ Rendering, controls, hotspots, and scene transitions stay in your React
17
+ application. This package is compatible with that tile output format but is
18
+ not affiliated with krpano.
14
19
 
15
20
  ## Install
16
21
 
22
+ ### Package
23
+
17
24
  ```bash
18
25
  npm install @ericchen1990/pano-view
19
26
  ```
20
27
 
21
- React 19, React DOM 19, Three.js, `@react-three/fiber` 9,
22
- `@react-three/drei` 10, and `@react-three/xr` 6 are peer dependencies. npm 7+
23
- and pnpm install them automatically; they must be a single copy in the host
24
- app.
28
+ pnpm and Yarn work the same way (`pnpm add` / `yarn add`).
29
+
30
+ ### Peer dependencies
31
+
32
+ These packages must be installed in the **host application**, not nested under
33
+ this library. npm 7+ and pnpm install them automatically; Yarn Classic and npm
34
+ 6 do not, so install them explicitly.
35
+
36
+ | Package | Supported range |
37
+ | --- | --- |
38
+ | `react` | `>=19.0.0 <19.3.0` |
39
+ | `react-dom` | `>=19.0.0 <19.3.0` |
40
+ | `three` | `^0.185.1` |
41
+ | `@react-three/fiber` | `^9.7.0` |
42
+ | `@react-three/drei` | `^10.0.0` |
43
+ | `@react-three/xr` | `^6.6.0` |
44
+
45
+ Keep a **single copy** of React, Three.js, and the React Three Fiber packages
46
+ in the host app. Duplicate `three` trees are a common cause of blank canvases
47
+ and context errors (`R3F: Hooks can only be used within the Canvas component`).
48
+ If that happens, check `npm ls three` / `pnpm why three` and dedupe.
49
+
50
+ React 19.3 and later are outside the supported range.
51
+
52
+ ### Stylesheet (HTML chrome)
53
+
54
+ Import the shipped stylesheet once in the host app. The WebGL panorama
55
+ (`PanoViewer`, `Sphere`, `Tile`, 3D hotspots) renders without it. Built-in
56
+ **HTML** overlays do not: they use prefixed `pano-*` classes for layout.
25
57
 
26
- Tailwind CSS v4 is now required for the built-in HTML chrome: video controls,
27
- captions, context menus, tooltips, and accessibility helpers render with
28
- Tailwind utility classes instead of a bundled stylesheet. Add a source entry in
29
- your app so Tailwind can scan this package:
58
+ Those overlays include video controls, captions, the default context menu,
59
+ hotspot tooltips, accessibility-only chrome, and WebVR session UI.
60
+
61
+ ```ts
62
+ import "@ericchen1990/pano-view/styles.css";
63
+ ```
64
+
65
+ Or from CSS:
30
66
 
31
67
  ```css
32
- @import "tailwindcss";
33
- @source "../node_modules/@ericchen1990/pano-view";
68
+ @import "@ericchen1990/pano-view/styles.css";
34
69
  ```
35
70
 
36
- This package does not ship a separate CSS file. If your app does not scan
37
- `@ericchen1990/pano-view`, those built-in overlays will render without their
38
- default styling. Customization still works through `className`, `style`, and
39
- component appearance props.
71
+ Skip this import only if you do not use those overlays, or if you restyle
72
+ them entirely through `className`, `style`, and appearance props. Without
73
+ the stylesheet, the overlays still mount, but they render unstyled.
40
74
 
41
75
  ## Exported components
42
76
 
@@ -91,6 +125,7 @@ Angles are public degrees. Positive yaw looks right and positive pitch looks up.
91
125
 
92
126
  ```tsx
93
127
  import { useRef } from "react";
128
+ import "@ericchen1990/pano-view/styles.css";
94
129
  import {
95
130
  PanoViewer,
96
131
  Sphere,
@@ -120,7 +155,7 @@ export function ControlledExample() {
120
155
  style={{ height: 560 }}
121
156
  >
122
157
  <AutoRotate enabled speed={18} acceleration={18} startDelay={1_000} />
123
- <Sphere src="/panoramas/room.webp" />
158
+ <Sphere src="/panoramas/room.webp" previewUrl="preview.webp" />
124
159
  </PanoViewer>
125
160
  </>
126
161
  );
@@ -151,6 +186,12 @@ The canvas cursor defaults to `grab`, becomes `grabbing` while dragging with the
151
186
 
152
187
  `Sphere` expects a 2:1 equirectangular image. Use `yawOffset` when the source's forward direction needs horizontal adjustment.
153
188
 
189
+ `previewUrl` is required and shows a low-resolution 2:1 image while `src` loads.
190
+ Relative paths resolve against the directory of `src`; root-absolute and
191
+ `http(s)` URLs are used as-is. For krpano sphere scenes, copy
192
+ [`<preview url>`](https://krpano.com/docu/xml/#preview) into `previewUrl`. That
193
+ preview must be equirectangular, not a cube strip.
194
+
154
195
  ```tsx
155
196
  "use client";
156
197
 
@@ -159,7 +200,10 @@ import { PanoViewer, Sphere } from "@ericchen1990/pano-view";
159
200
  export function SphereExample() {
160
201
  return (
161
202
  <PanoViewer style={{ width: "100%", height: 560 }}>
162
- <Sphere src="/panoramas/room.webp" />
203
+ <Sphere
204
+ src="/panoramas/room.webp"
205
+ previewUrl="preview.webp"
206
+ />
163
207
  </PanoViewer>
164
208
  );
165
209
  }
@@ -240,13 +284,13 @@ that are not same-origin should set `crossOrigin` (usually `"anonymous"`).
240
284
 
241
285
  ## Tile
242
286
 
243
- `Tile` renders six inward-facing cube faces and loads only tiles around the current view. The default layout matches krpano-style output:
287
+ `Tile` renders six inward-facing cube faces and loads only tiles around the current view. When `urlTemplate` is omitted, paths relative to `baseUrl` use this package default:
244
288
 
245
289
  ```text
246
- tiles/{face}/l{level}/{row}/l{level}_{face}_{col}_{row}.webp
290
+ tiles/%s/l%l/%v/l%l_%s_%h_%v.webp
247
291
  ```
248
292
 
249
- Faces are `f`, `r`, `b`, `l`, `u`, and `d`; rows and columns are 1-based.
293
+ which expands to `tiles/{face}/l{level}/{row}/l{level}_{face}_{col}_{row}.webp`. Faces are `f`, `r`, `b`, `l`, `u`, and `d`; rows and columns are 1-based.
250
294
 
251
295
  ```tsx
252
296
  import { PanoViewer, Tile } from "@ericchen1990/pano-view";
@@ -257,33 +301,81 @@ export function TileExample() {
257
301
  <Tile
258
302
  baseUrl="https://cdn.example.com/panoramas/room"
259
303
  multires="512,500,1000,2000"
304
+ previewUrl="previews/cube-vertical.webp"
260
305
  />
261
306
  </PanoViewer>
262
307
  );
263
308
  }
264
309
  ```
265
310
 
266
- The first `multires` value is the tile size. Remaining values are ascending cube-face sizes for `l1`, `l2`, and later levels. The default preview is `${baseUrl}/previews/cube-vertical.webp`, with six square faces stacked from top to bottom as `l/f/r/b/u/d`. Use `previewFaceOrder` when an atlas uses a different order:
311
+ krpano cube-tile output does not follow that default. Read `url`, `multires`,
312
+ and the preview path from the krpano XML
313
+ ([`<cube>`](https://krpano.com/docu/xml/#image.cube),
314
+ [`<preview>`](https://krpano.com/docu/xml/#preview)) and pass them through. If
315
+ those values are missing or do not match the files on disk, tiles resolve to
316
+ the wrong face, level, or row/column and the panorama looks scrambled.
317
+
318
+ | krpano | `Tile` / `TileScene` prop |
319
+ |---|---|
320
+ | `<cube url>` | `urlTemplate` |
321
+ | `<cube multires>` | `multires` |
322
+ | `<preview url>` | `previewUrl` |
323
+ | `<preview striporder>` | `previewFaceOrder` |
324
+
325
+ `Sphere` uses the same `<preview url>` → `previewUrl` mapping; that file must be a 2:1 equirectangular image, not a cube strip.
326
+
327
+ krpano's documented short syntax:
328
+
329
+ ```xml
330
+ <preview url="preview.jpg" />
331
+ <image>
332
+ <cube url="pano_%s_%l_%v_%h.jpg"
333
+ multires="512,1024,2048,4096"
334
+ />
335
+ </image>
336
+ ```
337
+
338
+ maps to:
339
+
340
+ ```tsx
341
+ <Tile
342
+ baseUrl="/panoramas/room"
343
+ urlTemplate="pano_%s_%l_%v_%h.jpg"
344
+ multires="512,1024,2048,4096"
345
+ previewUrl="preview.jpg"
346
+ />
347
+ ```
348
+
349
+ A MAKE PANO (MULTIRES) folder layout is often
350
+ `tiles/%s/l%l/%v/l%l_%s_%v_%h.jpg` with `preview.jpg` at the scene root. That
351
+ filename is `%v_%h` (row then column). This package's default tile template is
352
+ `%h_%v` (column then row). Copy the XML values instead of assuming the two
353
+ conventions match.
354
+
355
+ The first `multires` value is the tile size. Remaining values are ascending cube-face sizes for `l1`, `l2`, and later levels. `previewUrl` is required. Relative paths resolve against `baseUrl`; root-absolute and `http(s)` URLs are used as-is. A typical krpano MAKE PANO (MULTIRES) scene looks like this:
267
356
 
268
357
  ```tsx
269
358
  <Tile
270
359
  baseUrl="/panoramas/room"
360
+ urlTemplate="tiles/%s/l%l/%v/l%l_%s_%v_%h.jpg"
271
361
  multires="512,1000,2000"
272
- previewUrl="/panoramas/room/previews/cube-vertical.webp"
362
+ previewUrl="preview.jpg"
273
363
  previewFaceOrder={["l", "f", "r", "b", "u", "d"]}
274
364
  />
275
365
  ```
276
366
 
277
- `previewFaceOrder` must list the six face codes in their top-to-bottom atlas order: `f`, `r`, `b`, `l`, `u`, and `d`.
367
+ `previewFaceOrder` must list the six face codes in their top-to-bottom atlas order: `f`, `r`, `b`, `l`, `u`, and `d`. The preview image is a vertical 1×6 cubestrip. krpano `thumb.jpg` is a small thumbnail and is not this atlas.
278
368
 
279
369
  During rapid rotation or zoom, loaded tiles remain visible while newly visible tiles use their parent level or the preview as a local fallback.
280
370
 
281
- Override storage conventions with either a krpano placeholder template or a resolver. Both return paths relative to `baseUrl`:
371
+ For layouts that are not krpano XML, override the default with a placeholder
372
+ template or a resolver. Both return paths relative to `baseUrl`:
282
373
 
283
374
  ```tsx
284
375
  <Tile
285
376
  baseUrl="/panoramas/room"
286
377
  multires={{ tileSize: 512, levels: [500, 1000, 2000] }}
378
+ previewUrl="previews/cube-vertical.webp"
287
379
  urlTemplate="assets/%s/%l/%v_%h.webp"
288
380
  resolveTileUrl={({ face, level, row, col }) =>
289
381
  `api/tile/${face}/${level}/${row}/${col}`
@@ -304,7 +396,7 @@ a single non-stereo cube panorama.
304
396
  ## Scenes
305
397
 
306
398
  `Scenes` switches controlled sphere and cube-tile scenes with GPU-only
307
- snapshot blending. The target scene loads its sphere image or tile preview first;
399
+ snapshot blending. The target scene loads its sphere or tile preview first;
308
400
  once ready, the current framebuffer becomes a temporary GPU texture, its source
309
401
  textures are released, and the target scene blends in. This avoids holding two
310
402
  high-resolution tile scenes in WebGL memory at once.
@@ -317,12 +409,14 @@ import {
317
409
  } from "@ericchen1990/pano-view";
318
410
 
319
411
  const scenes: Scene[] = [
320
- { id: "lobby", type: "sphere", src: "/panoramas/lobby.webp" },
412
+ { id: "lobby", type: "sphere", src: "/panoramas/lobby.webp", previewUrl: "preview.webp" },
321
413
  {
322
414
  id: "terrace",
323
415
  type: "tile",
324
416
  baseUrl: "/panoramas/terrace",
325
417
  multires: "512,1000,2000",
418
+ urlTemplate: "tiles/%s/l%l/%v/l%l_%s_%v_%h.webp",
419
+ previewUrl: "previews/cube-vertical.webp",
326
420
  },
327
421
  ];
328
422
 
@@ -368,7 +462,7 @@ to leave the source unfiltered.
368
462
  ```tsx
369
463
  <PanoViewer style={{ height: 560 }}>
370
464
  <PanoFilter preset="pencil" intensity={0.85} />
371
- <Sphere src="/panoramas/room.webp" />
465
+ <Sphere src="/panoramas/room.webp" previewUrl="preview.webp" />
372
466
  <ImageHotspot
373
467
  id="door"
374
468
  position={{ yaw: 24, pitch: -6 }}
@@ -402,7 +496,7 @@ Auto-rotation is off by default. Render `AutoRotate` inside `PanoViewer` to enab
402
496
  ```tsx
403
497
  <PanoViewer style={{ height: 560 }}>
404
498
  <AutoRotate enabled speed={12} acceleration={6} startDelay={2_000} />
405
- <Sphere src="/panoramas/room.webp" />
499
+ <Sphere src="/panoramas/room.webp" previewUrl="preview.webp" />
406
500
  </PanoViewer>
407
501
  ```
408
502
 
@@ -451,7 +545,7 @@ export function GyroExample() {
451
545
  touchMode="full"
452
546
  onDenied={() => setEnabled(false)}
453
547
  />
454
- <Sphere src="/panoramas/room.webp" />
548
+ <Sphere src="/panoramas/room.webp" previewUrl="preview.webp" />
455
549
  </PanoViewer>
456
550
  </>
457
551
  );
@@ -508,7 +602,7 @@ export function WebVRExample() {
508
602
  mobileVr
509
603
  onEnterVR={(mode) => console.log("Entered", mode)}
510
604
  />
511
- <Sphere src="/panoramas/room.webp" />
605
+ <Sphere src="/panoramas/room.webp" previewUrl="preview.webp" />
512
606
  </PanoViewer>
513
607
  );
514
608
  }
@@ -551,7 +645,7 @@ interactive through the default WebXR controller rays.
551
645
  }}
552
646
  style={{ height: 560 }}
553
647
  >
554
- <Sphere src="/panoramas/room.webp" />
648
+ <Sphere src="/panoramas/room.webp" previewUrl="preview.webp" />
555
649
  </PanoViewer>
556
650
  ```
557
651
 
@@ -649,7 +743,7 @@ export function PlacementExample() {
649
743
  onPanoramaPointerMove={({ position }) => console.log("move", position)}
650
744
  style={{ height: 560 }}
651
745
  >
652
- <Sphere src="/panoramas/room.webp" />
746
+ <Sphere src="/panoramas/room.webp" previewUrl="preview.webp" />
653
747
  </PanoViewer>
654
748
  );
655
749
  }
@@ -690,7 +784,7 @@ import {
690
784
  onAutoRotateOneRound={() => console.log("full turn")}
691
785
  />
692
786
  <AutoRotate enabled />
693
- <Sphere src="/panoramas/room.webp" />
787
+ <Sphere src="/panoramas/room.webp" previewUrl="preview.webp" />
694
788
  </PanoViewer>;
695
789
  ```
696
790
 
@@ -723,7 +817,7 @@ separator between them.
723
817
 
724
818
  ```tsx
725
819
  <PanoViewer style={{ height: 560 }}>
726
- <Sphere src="/panoramas/room.webp" />
820
+ <Sphere src="/panoramas/room.webp" previewUrl="preview.webp" />
727
821
  </PanoViewer>
728
822
  ```
729
823
 
@@ -731,7 +825,7 @@ Disable the default menu (restore the browser menu, or mount your own):
731
825
 
732
826
  ```tsx
733
827
  <PanoViewer contextMenu={false}>
734
- <Sphere src="/panoramas/room.webp" />
828
+ <Sphere src="/panoramas/room.webp" previewUrl="preview.webp" />
735
829
  </PanoViewer>
736
830
  ```
737
831
 
@@ -739,7 +833,7 @@ Tune appearance while keeping the default items:
739
833
 
740
834
  ```tsx
741
835
  <PanoViewer contextMenu={{ appearance: { opacity: 0.92, borderRadius: 8 } }}>
742
- <Sphere src="/panoramas/room.webp" />
836
+ <Sphere src="/panoramas/room.webp" previewUrl="preview.webp" />
743
837
  </PanoViewer>
744
838
  ```
745
839
 
@@ -766,7 +860,7 @@ only add your own:
766
860
  }}
767
861
  style={{ height: 560 }}
768
862
  >
769
- <Sphere src="/panoramas/room.webp" />
863
+ <Sphere src="/panoramas/room.webp" previewUrl="preview.webp" />
770
864
  </PanoViewer>
771
865
  ```
772
866
 
@@ -800,7 +894,7 @@ reimplementing fullscreen enter/exit state:
800
894
  }}
801
895
  style={{ height: 560 }}
802
896
  >
803
- <Sphere src="/panoramas/room.webp" />
897
+ <Sphere src="/panoramas/room.webp" previewUrl="preview.webp" />
804
898
  </PanoViewer>
805
899
  ```
806
900
 
@@ -931,7 +1025,7 @@ are angular degrees, so they remain independent of the canvas resolution.
931
1025
  import { ImageHotspot, PanoViewer, Sphere } from "@ericchen1990/pano-view";
932
1026
 
933
1027
  <PanoViewer style={{ height: 560 }}>
934
- <Sphere src="/panoramas/room.webp" />
1028
+ <Sphere src="/panoramas/room.webp" previewUrl="preview.webp" />
935
1029
  <ImageHotspot
936
1030
  id="gallery"
937
1031
  ariaLabel="Open gallery"
@@ -1260,4 +1354,14 @@ not load.
1260
1354
 
1261
1355
  ## Next.js and SSR
1262
1356
 
1263
- The distributed entry is marked as a client module. Render it below a Client Component boundary. Panorama resources are loaded in the browser; no API keys or server configuration are required.
1357
+ The distributed entry is marked as a client module. Render it below a Client
1358
+ Component boundary. Panorama resources are loaded in the browser; no API keys
1359
+ or server configuration are required.
1360
+
1361
+ Import the stylesheet once from a Client Component or a global CSS file:
1362
+
1363
+ ```ts
1364
+ import "@ericchen1990/pano-view/styles.css";
1365
+ ```
1366
+
1367
+ See [Stylesheet (HTML chrome)](#stylesheet-html-chrome).