@ericchen1990/pano-view 3.4.1 → 3.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.zh-CN.md CHANGED
@@ -1,4 +1,4 @@
1
- # @ericchen1990/pano-view
1
+ # @ericchen1990/pano-view · [![许可证](https://img.shields.io/badge/license-MIT-blue.svg)](./LICENSE) [![npm 版本](https://img.shields.io/npm/v/%40ericchen1990%2Fpano-view.svg?style=flat)](https://www.npmjs.com/package/@ericchen1990/pano-view) [![CI](https://github.com/Eric-Chen1990/pano-view/actions/workflows/ci.yml/badge.svg?branch=main)](https://github.com/Eric-Chen1990/pano-view/actions/workflows/ci.yml) [![欢迎 PR](https://img.shields.io/badge/PRs-welcome-brightgreen.svg)](https://github.com/Eric-Chen1990/pano-view/pulls)
2
2
 
3
3
  [English](./README.md) | **简体中文**
4
4
 
@@ -6,23 +6,296 @@
6
6
 
7
7
  在[在线 playground](https://pano-view-playground.vercel.app/?utm_source=github&utm_medium=readme&utm_campaign=package-readme&utm_content=playground) 中试用这些组件。
8
8
 
9
- ## 兼容 krpano 的 tile 输出
9
+ ## 目录
10
+
11
+ - [pano-converter](#pano-converter)
12
+ - [数据来源与选型](#数据来源与选型)
13
+ - [静态项目与 JSON 数据](#静态项目与-json-数据)
14
+ - [常见问题与故障排查](#常见问题与故障排查)
15
+ - [兼容 krpano 的瓦片输出](#兼容-krpano-的瓦片输出)
16
+ - [安装](#安装)
17
+ - [示例](#示例)
18
+ - [导出组件](#导出组件)
19
+ - [PanoViewer](#panoviewer)
20
+ - [Sphere](#sphere)
21
+ - [PanoVideo](#panovideo)
22
+ - [Tile](#tile)
23
+ - [Scenes](#scenes)
24
+ - [PanoFilter](#panofilter)
25
+ - [AutoRotate](#autorotate)
26
+ - [BackgroundAudio](#backgroundaudio)
27
+ - [Gyro](#gyro)
28
+ - [WebVR](#webvr)
29
+ - [鼠标、触控与键盘控件](#鼠标触控与键盘控件)
30
+ - [全景坐标事件](#全景坐标事件)
31
+ - [PanoEvents](#panoevents)
32
+ - [PanoContextMenu](#panocontextmenu)
33
+ - [共享热点约定与可序列化定义](#共享热点约定与可序列化定义)
34
+ - [ImageHotspot](#imagehotspot)
35
+ - [GraphicHotspot](#graphichotspot)
36
+ - [热点模式与缩放行为](#热点模式与缩放行为)
37
+ - [PolygonHotspot](#polygonhotspot)
38
+ - [PolylineHotspot](#polylinehotspot)
39
+ - [SequenceHotspot](#sequencehotspot)
40
+ - [VideoHotspot](#videohotspot)
41
+ - [AudioHotspot](#audiohotspot)
42
+ - [TextHotspot](#texthotspot)
43
+ - [IframeHotspot](#iframehotspot)
44
+ - [Next.js 与 SSR](#nextjs-与-ssr)
45
+ - [球面多分辨率瓦片](#球面多分辨率瓦片)
46
+ - [无障碍颜色矩阵与标签排版](#无障碍颜色矩阵与标签排版)
47
+
48
+ ## pano-converter
49
+
50
+ [@ericchen1990/pano-converter](../converter/README.zh-CN.md) 是本仓库中的另一个包。它在 Node.js 中,以及通过 `pano-convert` CLI,生成立方体与等距圆柱瓦片金字塔,并返回可供 [`Tile`](#tile)、[`SphereTile`](#球面多分辨率瓦片) 与 [`Scenes`](#scenes) 使用的 `viewerConfig`。转换器不依赖 React。
51
+
52
+ ## 数据来源与选型
53
+
54
+ 查看器接收配置对象,或通过 HTTP 加载 JSON,不直接连接数据库。数据库连接、查询、鉴权和响应映射由宿主后端负责。数据库 API 也可以返回 JSON;这里比较的是静态文件和动态提供的配置,而不是 JSON 格式本身。两种方式的预览图、瓦片和媒体都可以使用对象存储/CDN。
55
+
56
+ | 维度 | 静态 JSON + CDN | 数据库 + 后端 API |
57
+ | --- | --- | --- |
58
+ | 配置读取 | 无需查询数据库;仍有存储、请求和流量成本 | 到达后端的请求需要处理,可能查询数据库;缓存可以减少这些开销 |
59
+ | 更新 | 生成并发布文件,再管理入口引用和缓存 | 无需发布静态文件;生效时间仍取决于后端和客户端缓存 |
60
+ | 权限与个性化 | 公开文件适合公开内容;私有分发需要托管/CDN 访问控制 | 后端可按用户、组织鉴权并返回不同配置 |
61
+ | 可用性与维护 | 依赖静态托管/CDN;维护发布及缓存更新流程 | 依赖 API 及其数据库/缓存服务;维护查询、鉴权及缓存失效 |
62
+ | 适用场景 | 公开、已发布的导览;更新较少;读取流量大 | 编辑预览、私有内容、频繁更新和个性化 |
63
+
64
+ **公开、已发布的导览优先使用 JSON + CDN;编辑预览、私有和个性化内容优先使用数据库 + API。** 同时需要两者时,以数据库作为编辑来源,发布时生成 JSON 快照。这种混合方式需要应用自己的发布流程;包不会自动生成或同步快照。
65
+
66
+ 两条路径均不会自动轮询或订阅配置变化。更新已打开的页面时,宿主应用需要重新加载配置,并将新数据传给查看器。未命中缓存的 API 读取可以返回当前数据库值,但不会让已有查看器自动更新。
67
+
68
+ ### 数据库配置接入
69
+
70
+ 后端先查询导览并完成鉴权,再将需要公开给查看器的配置字段映射为项目。下例的 `readAuthorizedTour` 由宿主应用实现,负责数据库读取和访问检查,不是包提供的 API。它返回的场景记录包含转换时保存的精简 `viewerConfig`、资源目录、视角和热点定义:
10
71
 
11
- 当你需要围绕等距圆柱图像或现有 krpano 立方体 tile 金字塔构建 React 原生查看器时,请使用本包。将 krpano `<cube>` 的 `url` 与 `multires` 属性复制到 `Tile` 的 `urlTemplate` 与 `multires`,并将 `<preview url>` 复制到 `previewUrl`;krpano 的磁盘命名与本包默认不同,省略这些值会加载错误的 tile 或预览图。渲染、控件、热点与场景过渡仍由你的 React 应用负责。本包兼容该 tile 输出格式,但与 krpano 无隶属关系。
72
+ ```ts
73
+ import { loadPanoProject } from "@ericchen1990/pano-view/data";
74
+ import { readAuthorizedTour } from "./tour-repository";
75
+
76
+ export async function loadTourFromDatabase(tourId: string) {
77
+ const tour = await readAuthorizedTour(tourId);
78
+ return loadPanoProject({
79
+ schemaVersion: 1,
80
+ initialSceneId: tour.initialSceneId,
81
+ scenes: tour.scenes.map((scene) => ({
82
+ id: scene.id,
83
+ title: scene.title,
84
+ source: {
85
+ viewerConfig: scene.viewerConfig,
86
+ baseUrl: scene.assetBaseUrl,
87
+ },
88
+ initialView: scene.initialView,
89
+ hotspots: scene.hotspots,
90
+ })),
91
+ }, { baseUrl: tour.assetBaseUrl });
92
+ }
93
+ ```
94
+
95
+ 对象入口必须提供 `baseUrl`:这里的 `tour.assetBaseUrl` 是绝对目录 URL,例如 `https://cdn.example.com/tours/123/v12/`。项目中相对的热点地址基于该目录解析;每个场景可用 `source.baseUrl` 覆盖全景瓦片/预览图目录。全部场景内嵌 `source.viewerConfig` 时,该调用不会请求项目或 viewer JSON;渲染时仍会加载预览图、瓦片和媒体。服务端数据工具从 `@ericchen1990/pano-view/data` 导入。
96
+
97
+ 将返回的 `LoadedPanoProject` 传给查看器组件;在 Next.js 中,查看器需位于客户端边界:
98
+
99
+ ```tsx
100
+ "use client";
101
+
102
+ import {
103
+ PanoViewer, ProjectScenes, type LoadedPanoProject,
104
+ } from "@ericchen1990/pano-view";
105
+
106
+ export function DatabaseTour({ project }: { project: LoadedPanoProject }) {
107
+ return <PanoViewer style={{ height: 600 }}>
108
+ <ProjectScenes project={project} transition="dissolve" />
109
+ </PanoViewer>;
110
+ }
111
+ ```
112
+
113
+ 也可以由已鉴权的 JSON 接口返回 `PanoProjectDefinition`,再调用 `loadPanoProject("https://api.example.com/tours/123", { baseUrl: "https://cdn.example.com/tours/123/v12/" })`;地址不必以 `.json` 结尾。单场景可用 `sceneFromViewerConfig` 处理已有配置,或用 `loadSceneFromJson` 加载返回 `schemaVersion: 1` viewer 文档的 API,再通过 `Scenes` 渲染。请求选项与错误处理见 [静态项目与 JSON 数据](#静态项目与-json-数据)。私有图片/媒体需单独保护:JSON 请求的认证信息不会传到纹理请求。
114
+
115
+ ### 缓存与发布更新
116
+
117
+ `project.json`(场景组织、视角和热点)、各个 `viewer.json`(全景渲染配置)和预览图/瓦片/媒体是分别缓存的资源。只刷新项目文件,不会同步刷新地址不变的场景配置或图片。数据库 API 同样可能被后端、CDN 和客户端缓存,因此两种方式都需要缓存策略。
118
+
119
+ - **优先使用版本化目录:** 将完整版本发布到新路径,例如 `/tours/123/v12/`,包含 `project.json`、场景配置和资源;全部上传完成后,再切换页面或入口引用。变化的资源也要更新版本,并在旧配置仍可能被使用期间保留旧资源。内容固定的版本化文件可用长期缓存;选择版本的可变页面或入口需要短缓存或重新验证。AWS 也[建议使用版本化文件名或目录](https://docs.aws.amazon.com/AmazonCloudFront/latest/DeveloperGuide/UpdatingExistingObjects.html)。
120
+ - **覆盖相同 URL 时:** 等待设定的缓存有效期结束、要求重新验证,或上传后对受影响路径执行 CDN 失效。CDN 失效不会清除浏览器已有文件;新配置的响应头也无法追溯替换仍在有效期内的缓存响应。版本化路径可避免依赖这些副本过期。`?v=12` 等查询参数仅在 CDN 将其纳入缓存键时有效;JSON 的查询参数不会自动继承到引用的资源。
121
+ - **按资源选择响应头:** `Cache-Control: no-cache` 允许存储,但复用前必须验证;`no-store` 要求缓存不存储响应。可变的公开入口可配合 `ETag` 等验证器重新验证;私有/个性化 API 响应应明确配置策略,例如 `private, no-store`,避免在用户间共享。还需要配置托管/CDN 规则以遵循预期策略。参见 [MDN HTTP 缓存](https://developer.mozilla.org/en-US/docs/Web/HTTP/Guides/Caching)和 [Cache-Control](https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers/Cache-Control)。
122
+
123
+ 这些响应头和 CDN 操作由托管平台/后端配置。loader 的 `requestInit` 仅支持 `headers`、`credentials` 和 `signal`,不提供 Fetch 的 `cache` 选项,也不会使 CDN 缓存失效或自动刷新已加载的项目。
124
+
125
+ ## 静态项目与 JSON 数据
126
+
127
+ 无需数据库。转换器生成的 `manifest.json` 用于归档和还原;`viewer.json` 只保存渲染配置;应用编写 `project.json` 组织场景、视角和热点。不要把完整清单直接传给 `Scenes`。
128
+
129
+ 例如,将下面的 `project.json`、两个转换输出目录和你的 React 应用部署为静态网站:
130
+
131
+ ```json
132
+ {
133
+ "schemaVersion": 1,
134
+ "initialSceneId": "entrance",
135
+ "scenes": [
136
+ {
137
+ "id": "entrance",
138
+ "source": { "viewerUrl": "./panoramas/entrance/viewer.json" },
139
+ "initialView": { "yaw": 0, "pitch": 0, "fov": 75 },
140
+ "hotspots": [{
141
+ "id": "to-hall", "type": "graphic", "label": "Enter hall",
142
+ "position": { "yaw": 30, "pitch": -10 },
143
+ "graphic": { "kind": "arrow" },
144
+ "action": { "type": "scene", "sceneId": "hall" }
145
+ }]
146
+ },
147
+ {
148
+ "id": "hall",
149
+ "source": { "viewerUrl": "./panoramas/hall/viewer.json" },
150
+ "hotspots": [{
151
+ "id": "caption", "type": "text",
152
+ "position": { "yaw": 0, "pitch": 5 }, "text": "Welcome"
153
+ }]
154
+ }
155
+ ]
156
+ }
157
+ ```
158
+
159
+ `source` 必须在 `viewerUrl` 与内嵌 `viewerConfig` 中二选一,可加 `baseUrl` 覆盖该场景的全景资源根目录。`title`、`initialView`、`hotspots` 可省略。未指定 `initialSceneId` 时使用第一项。场景 ID 全项目唯一,热点 ID 在各场景内唯一,跳转目标必须存在。
160
+
161
+ `loadPanoProject` 读取项目及各场景的轻量配置,以最多四个并发请求加载元数据;任一失败会整体报错,不加载所有纹理。返回的 `LoadedPanoProject` 包含 `initialSceneId` 和 `scenes`;每项保存运行时 `scene`、标题、初始视角和已解析资源地址的热点。可直接传给 `ProjectScenes`:
162
+
163
+ ```tsx
164
+ import { useEffect, useState } from "react";
165
+ import {
166
+ PanoViewer, ProjectScenes, loadPanoProject, PanoDataError,
167
+ type LoadedPanoProject,
168
+ } from "@ericchen1990/pano-view";
169
+
170
+ export function Tour() {
171
+ const [project, setProject] = useState<LoadedPanoProject>();
172
+ const [error, setError] = useState<string>();
173
+ useEffect(() => {
174
+ const controller = new AbortController();
175
+ loadPanoProject("/tour/project.json", {
176
+ requestInit: { signal: controller.signal },
177
+ }).then((result) => {
178
+ if (!controller.signal.aborted) setProject(result);
179
+ }).catch((cause: unknown) => {
180
+ if (!controller.signal.aborted) {
181
+ setError(cause instanceof PanoDataError
182
+ ? `${cause.code}: ${cause.message}` : String(cause));
183
+ }
184
+ });
185
+ return () => controller.abort();
186
+ }, []);
187
+ if (error) return <p role="alert">{error}</p>;
188
+ if (!project) return <p role="status">Loading tour…</p>;
189
+ return <PanoViewer style={{ height: 600 }}>
190
+ <ProjectScenes project={project} transition="dissolve" />
191
+ </PanoViewer>;
192
+ }
193
+ ```
194
+
195
+ `ProjectScenes` 放在 `PanoViewer` 内,复用全部九类现有热点。`action: { type: "scene", sceneId }` 点击后切换场景;没有 action 的音频、视频、序列热点点击切换播放。离开场景时卸载媒体,再次进入恢复 JSON 初始播放配置。浏览器仍可能阻止自动播放。
196
+
197
+ 配置了 `initialView` 的场景在 `onSceneReady` 时应用视角,缺省字段为 yaw 0、pitch 0、fov 75;未配置时保持当前视角。过渡期间热点隐藏。使用 `activeSceneId` / `onActiveSceneIdChange` 可控制当前场景。`onHotspotClick(event)` 中调用 `event.preventDefault()` 可取消默认动作;`onHotspotError` 提供场景、热点和原始错误;媒体的原始错误可能是带有 `error` 字段的事件。`renderHotspot(hotspot, context)` 返回 `undefined` 使用默认渲染,返回 `null` 隐藏;context 包含 `sceneId`、`playing`、`onClick`、`togglePlayback`、`reportError` 和 `renderDefault`。
198
+
199
+ 单图和已有配置可用纯数据入口;Next.js 的查看器组件需位于客户端边界,服务端加载数据则使用 `/data`:
200
+
201
+ ```ts
202
+ import {
203
+ sceneFromViewerConfig, loadSceneFromJson, loadPanoProject,
204
+ } from "@ericchen1990/pano-view/data";
205
+
206
+ // A JSON API and a different asset/CDN origin.
207
+ const scene = await loadSceneFromJson(
208
+ "https://api.example.com/rooms/123/viewer",
209
+ { id: "room", baseUrl: "https://cdn.example.com/rooms/123/" },
210
+ );
211
+
212
+ // Existing Node.js conversion results need no extra JSON request.
213
+ const inlineScene = sceneFromViewerConfig(result.viewerConfig, {
214
+ id: "room", baseUrl: "https://cdn.example.com/rooms/123/",
215
+ });
216
+
217
+ // Use source.viewerConfig instead of source.viewerUrl to inline scene data.
218
+ const project = await loadPanoProject({
219
+ schemaVersion: 1,
220
+ scenes: [{ id: "room", source: { viewerConfig: result.viewerConfig } }],
221
+ }, { baseUrl: "https://cdn.example.com/rooms/123/" });
222
+ ```
223
+
224
+ ### 地址与上传映射
225
+
226
+ | 引用 | 默认基准 |
227
+ | --- | --- |
228
+ | 项目中的 `viewerUrl`、热点资源及 `source.baseUrl` | 项目根目录 |
229
+ | 外部 `viewer.json` 的瓦片与预览 | 该 JSON 的目录 |
230
+ | 内嵌配置的瓦片与预览 | 项目根目录 |
231
+ | 设置 `source.baseUrl` 的场景资源 | 指定目录 |
232
+
233
+ URL 入口以重定向后最终地址所在目录为默认根目录;对象入口必须提供 `baseUrl`,可用它覆盖项目根目录。目录末尾斜杠自动统一,域名和云前缀无需写回转换 JSON。浏览器接受相对入口地址,Node.js 需要绝对 HTTP(S) 地址。完整资源 URL 不重新拼接,`/` 开头的地址相对资源基准的 origin 解析。
234
+
235
+ 上传时若改名或打散目录,使用同步 `resolveAssetUrl(path, context)`。context 提供 `defaultUrl`、`kind`(`tile`、`preview` 或 `hotspot`)、`sceneId` 和可选 `hotspotId`。返回最终的完整 URL 或站点根路径;不会再次拼接。`uploadedUrls` 由业务提供:
236
+
237
+ ```ts
238
+ const project = await loadPanoProject("/tour/project.json", {
239
+ resolveAssetUrl(path, context) {
240
+ // Map keys must distinguish scenes and, for hotspot assets, hotspot IDs.
241
+ const key = `${context.sceneId}/${context.hotspotId ?? ""}/${path}`;
242
+ return uploadedUrls[key] ?? context.defaultUrl;
243
+ },
244
+ });
245
+ ```
246
+
247
+ 热点映射涵盖图片、SVG 图片、音频数组、视频 poster、播放图标和 tooltip 图片;文本、CSS 和 SVG path 不作为地址处理。临时签名应由业务预先取得并通过映射返回,不要在每个瓦片请求中异步签名。
248
+
249
+ `requestInit` 只接受 `headers`、`credentials`、`signal`,仅用于 JSON 请求。它们不会传到纹理请求,JSON 地址的查询参数也不会自动继承。`parsePanoProject(unknown)` 可单独校验并复制配置。`sceneFromViewerConfig` 接受未包装的精简配置;`loadSceneFromJson` 接受带 `schemaVersion: 1` 的 `viewer.json`。
250
+
251
+ `PanoDataError` 提供稳定 `code`:`INVALID_OPTIONS`、`FETCH_FAILED`、`HTTP_ERROR`、`INVALID_JSON`、`INVALID_CONFIG`、`UNSUPPORTED_SCHEMA_VERSION`、`UNSUPPORTED_PROJECTION`、`INVALID_PROJECT`、`INVALID_REFERENCE`、`ABORTED`;HTTP 错误带 `status`,项目错误尽可能带 `sceneId` 和 `field`。加载成功仅代表配置就绪,纹理就绪仍由 `onSceneReady` 等回调表示。
252
+
253
+ 完整静态示例见 playground 的 `/project` 页面和 `public/fixtures/projects/tour/project.json`。
254
+
255
+ ## 兼容 krpano 的瓦片输出
256
+
257
+ 当你需要围绕等距圆柱图像或现有 krpano 立方体瓦片金字塔构建 React 原生查看器时,请使用本包。将 krpano `<cube>` 的 `url` 与 `multires` 属性复制到 `Tile` 的 `urlTemplate` 与 `multires`,并将 `<preview url>` 复制到 `previewUrl`;krpano 的磁盘命名与本包默认不同,省略这些值会加载错误的瓦片或预览图。渲染、控件、热点与场景过渡仍由你的 React 应用负责。本包兼容该瓦片输出格式,但与 krpano 无隶属关系。
12
258
 
13
259
  ## 安装
14
260
 
15
261
  ### 包
16
262
 
263
+ <details name="pano-view-install" open>
264
+ <summary>npm</summary>
265
+
17
266
  ```bash
18
267
  npm install @ericchen1990/pano-view
19
268
  ```
20
269
 
21
- pnpm 与 Yarn 用法相同(`pnpm add` / `yarn add`)。
270
+ </details>
271
+ <details name="pano-view-install">
272
+ <summary>pnpm</summary>
273
+
274
+ ```bash
275
+ pnpm add @ericchen1990/pano-view
276
+ ```
277
+
278
+ </details>
279
+ <details name="pano-view-install">
280
+ <summary>yarn</summary>
281
+
282
+ ```bash
283
+ yarn add @ericchen1990/pano-view
284
+ ```
285
+
286
+ </details>
287
+ <details name="pano-view-install">
288
+ <summary>bun</summary>
289
+
290
+ ```bash
291
+ bun add @ericchen1990/pano-view
292
+ ```
293
+
294
+ </details>
22
295
 
23
296
  ### 对等依赖
24
297
 
25
- 这些包必须安装在**宿主应用**中,而不是嵌套在本库之下。npm 7+ 与 pnpm 会自动安装;Yarn Classic 与 npm 6 不会,需显式安装。
298
+ **宿主应用**必须提供这些包的兼容版本。对等依赖是否自动安装取决于包管理器及其配置;缺失的依赖应按下列版本范围显式安装。
26
299
 
27
300
  | 包 | 支持范围 |
28
301
  | --- | --- |
@@ -33,13 +306,13 @@ pnpm 与 Yarn 用法相同(`pnpm add` / `yarn add`)。
33
306
  | `@react-three/drei` | `^10.0.0` |
34
307
  | `@react-three/xr` | `^6.6.0` |
35
308
 
36
- 在宿主应用中保持 React、Three.js 与 React Three Fiber 相关包的**单一副本**。重复的 `three` 树是空白画布与上下文错误(`R3F: Hooks can only be used within the Canvas component`)的常见原因。若出现该问题,请检查 `npm ls three` / `pnpm why three` 并去重。
309
+ 在宿主应用中保持 React、Three.js 与 React Three Fiber 相关包的**单一副本**。重复的 `three` 树是空白画布与上下文错误(`R3F: Hooks can only be used within the Canvas component`)的常见原因。若出现该问题,请检查 `npm ls three`、`pnpm why three`、`yarn why three` 或 `bun pm ls` 并去重。
37
310
 
38
311
  ### 样式表(HTML 叠加层)
39
312
 
40
- 在宿主应用中导入一次随包样式表。WebGL 全景(`PanoViewer`、`Sphere`、`Tile`、3D 热点)无需该文件即可渲染。内置 **HTML** 叠加层则不行:它们使用带 `pano-` 前缀的类名做布局。
313
+ 在宿主应用中导入一次随包样式表。WebGL 全景(`PanoViewer`、`Sphere`、`Tile`、3D 热点)无需该文件即可渲染。内置 **HTML** 叠加层依赖该文件中带 `pano-` 前缀的类名完成布局。
41
314
 
42
- 这些叠加层包括视频控件、字幕、默认右键菜单、热点 tooltip、仅无障碍界面元素,以及 WebVR 会话 UI。
315
+ 这些叠加层包括视频控件、字幕、默认右键菜单、热点 tooltip、供辅助技术使用的界面元素,以及 WebVR 会话 UI。
43
316
 
44
317
  ```ts
45
318
  import "@ericchen1990/pano-view/styles.css";
@@ -53,10 +326,13 @@ import "@ericchen1990/pano-view/styles.css";
53
326
 
54
327
  仅当你不使用这些叠加层,或完全通过 `className`、`style` 与外观属性重设样式时,可跳过该导入。若不导入样式表,叠加层仍会挂载,但会无样式显示。
55
328
 
56
- ## 快速渲染一个全景图
329
+ ## 示例
57
330
 
58
331
  为 PanoViewer 提供明确尺寸,并在其内部渲染一个全景源。使用内置 HTML 叠加层
59
332
  (例如视频控件、字幕、tooltip、右键菜单或 WebVR UI)时,需导入一次样式表。
333
+ 下面是每种源的基本用法,完整 props 见对应章节。
334
+
335
+ ### 等距圆柱图像
60
336
 
61
337
  ~~~tsx
62
338
  import "@ericchen1990/pano-view/styles.css";
@@ -71,19 +347,87 @@ export function Panorama() {
71
347
  }
72
348
  ~~~
73
349
 
74
- 如需复用现有 krpano tile 金字塔,请在将 Sphere 换成 Tile 前阅读
75
- [迁移指南](../../docs/krpano-migration.zh-CN.md)。指南说明 XML 到 prop 的映射与
76
- 相对路径规则。
350
+ `previewUrl` 须为低分辨率 2:1 图像,用于在完整图像加载期间显示预览。详见 [Sphere](#sphere)。
351
+
352
+ ### 立方体瓦片
353
+
354
+ 从 krpano XML 复制 `url`、`multires` 与预览路径。把 Sphere 换成 Tile 之前,请阅读
355
+ [迁移指南](../../docs/krpano-migration.zh-CN.md)。
356
+
357
+ ~~~tsx
358
+ import { PanoViewer, Tile } from "@ericchen1990/pano-view";
359
+
360
+ export function TilePanorama() {
361
+ return (
362
+ <PanoViewer style={{ height: 560 }}>
363
+ <Tile
364
+ baseUrl="/panoramas/room"
365
+ urlTemplate="tiles/%s/l%l/%v/l%l_%s_%v_%h.jpg"
366
+ multires="512,1000,2000"
367
+ previewUrl="preview.jpg"
368
+ />
369
+ </PanoViewer>
370
+ );
371
+ }
372
+ ~~~
373
+
374
+ 占位符与预览面顺序见 [Tile](#tile)。
375
+
376
+ ### 360 视频
377
+
378
+ ~~~tsx
379
+ import { PanoViewer, PanoVideo } from "@ericchen1990/pano-view";
380
+
381
+ export function TourVideo() {
382
+ return (
383
+ <PanoViewer style={{ height: 560 }}>
384
+ <PanoVideo autoPlay muted src="/tour.mp4" />
385
+ </PanoViewer>
386
+ );
387
+ }
388
+ ~~~
389
+
390
+ 多档画质、字幕与自定义控件见 [PanoVideo](#panovideo)。
391
+
392
+ ### 多场景
393
+
394
+ ~~~tsx
395
+ import { useState } from "react";
396
+ import { PanoViewer, Scenes, type Scene } from "@ericchen1990/pano-view";
397
+
398
+ const scenes: Scene[] = [
399
+ { id: "lobby", type: "sphere", src: "/lobby.webp", previewUrl: "/lobby-preview.webp" },
400
+ { id: "roof", type: "sphere", src: "/roof.webp", previewUrl: "/roof-preview.webp" },
401
+ ];
402
+
403
+ export function Tour() {
404
+ const [activeSceneId, setActiveSceneId] = useState("lobby");
405
+
406
+ return (
407
+ <PanoViewer style={{ height: 560 }}>
408
+ <Scenes
409
+ activeSceneId={activeSceneId}
410
+ onActiveSceneIdChange={setActiveSceneId}
411
+ scenes={scenes}
412
+ transition="dissolve"
413
+ />
414
+ </PanoViewer>
415
+ );
416
+ }
417
+ ~~~
418
+
419
+ 过渡预设与热点渲染见 [Scenes](#scenes)。
77
420
 
78
421
  ## 导出组件
79
422
 
80
423
  ### 查看器
81
424
 
82
- - [`PanoViewer`](#panoviewer) — 画布外壳、相机、默认控件与命令式视图 API
425
+ - [`PanoViewer`](#panoviewer) — 画布容器、相机、默认控件与命令式视图 API
83
426
  - [`Sphere`](#sphere) — 单张 2:1 等距圆柱图像
427
+ - [`SphereTile`](#球面多分辨率瓦片) — 多分辨率等距圆柱瓦片
84
428
  - [`PanoVideo`](#panovideo) — 全景球面上的 2:1 等距圆柱视频
85
- - [`Tile`](#tile) — krpano 风格多分辨率立方体 tile
86
- - [`Scenes`](#scenes) — 受控多场景过渡
429
+ - [`Tile`](#tile) — krpano 风格多分辨率立方体瓦片
430
+ - [`Scenes`](#scenes) — 支持受控与非受控模式的多场景切换
87
431
  - [`BackgroundAudio`](#backgroundaudio) — 全部场景共用、或按场景切换的背景声
88
432
  - [`PanoFilter`](#panofilter) — 对全景源应用色彩与艺术滤镜
89
433
 
@@ -127,7 +471,7 @@ export function Panorama() {
127
471
 
128
472
  `PanoViewer` 负责画布、透视相机与控件。为其容器指定明确尺寸,并在内部放置一个全景源。
129
473
 
130
- 角度对外以度为单位。偏航(yaw)为正表示向右看,俯仰(pitch)为正表示向上看。
474
+ 公开 API 中的角度以度为单位。偏航(yaw)为正表示向右看,俯仰(pitch)为正表示向上看。
131
475
 
132
476
  ```tsx
133
477
  import "@ericchen1990/pano-view/styles.css";
@@ -170,13 +514,13 @@ export function ControlledExample() {
170
514
  }
171
515
  ```
172
516
 
173
- 句柄从单个 `ref` 暴露视图、全屏、场景、VR、视频与背景音乐控制,方便宿主自绘 UI:
517
+ 句柄通过单个 `ref` 提供视图、全屏、场景、VR、视频与背景音乐控制,供宿主构建自定义界面:
174
518
 
175
519
  **视图** — `getView`、`setView`、`reset`、`lookTo`、`moveTo`、`zoomTo`、`lookToHotspot`。
176
520
 
177
521
  `setView` 与 `reset` 仍立即生效。导航方法返回
178
522
  `Promise<{ status: "completed" | "cancelled" | "not-found" }>`;新的导航或用户操作会取消前一个导航。默认时长为 700ms,缓动为
179
- `easeInOutCubic`,YAW 默认走最短路径。`lookTo` 与 `lookToHotspot`
523
+ `easeInOutCubic`,yaw 默认沿最短路径变化。`lookTo` 与 `lookToHotspot`
180
524
  未传 `fov` 时保持当前 FOV。
181
525
 
182
526
  ```tsx
@@ -194,13 +538,13 @@ await viewer.zoomTo(48, { duration: 400, easing: "linear" });
194
538
 
195
539
  **全屏** — `enterFullscreen`、`exitFullscreen`、`toggleFullscreen`、`isFullscreen`。
196
540
 
197
- **场景**(需 `<Scenes />`)— `setScene(id)`、`nextScene`、`previousScene`、`getActiveSceneId`、`getSceneIds`、`isSceneTransitioning`。`Scenes` 现支持可选 `defaultActiveSceneId`(非受控)或继续传 `activeSceneId`(受控)。受控模式下命令式调用会触发 `onActiveSceneIdChange`。
541
+ **场景**(需 `<Scenes />`)— `setScene(id)`、`nextScene`、`previousScene`、`getActiveSceneId`、`getSceneIds`、`isSceneTransitioning`。`Scenes` 支持 `defaultActiveSceneId`(非受控)或 `activeSceneId`(受控)。受控模式下命令式调用会触发 `onActiveSceneIdChange`。
198
542
 
199
543
  **WebVR**(需 `<WebVR />`)— `enterVR`、`exitVR`、`toggleVR`、`isVRAvailable`、`isVREnabled`、`getVRMode`、`requestVRPermission`。
200
544
 
201
- **视频**(需 `<PanoVideo />`)— `getVideo` 返回 `PanoVideoController`,`subscribeVideo` 订阅宿主变更,另有快捷方法 `playVideo`、`pauseVideo`、`toggleVideo`、`seekVideo`、`setVideoVolume`、`setVideoMuted`、`toggleVideoMuted`。
545
+ **视频**(需 `<PanoVideo />`)— `getVideo` 返回 `PanoVideoController`,`subscribeVideo` 订阅视频控制器与播放状态变更,另有快捷方法 `playVideo`、`pauseVideo`、`toggleVideo`、`seekVideo`、`setVideoVolume`、`setVideoMuted`、`toggleVideoMuted`。
202
546
 
203
- **背景音乐**(需 `<BackgroundAudio />`)— `getBackgroundAudio` 返回 `BackgroundAudioController`,`subscribeBackgroundAudio` 订阅宿主变更,另有快捷方法 `playBackgroundAudio`、`pauseBackgroundAudio`、`toggleBackgroundAudio`、`setBackgroundAudioVolume`、`setBackgroundAudioMuted`、`toggleBackgroundAudioMuted`。`BackgroundAudio` 现支持可选 `playing`(受控)或 `defaultPlaying`(非受控)。
547
+ **背景音乐**(需 `<BackgroundAudio />`)— `getBackgroundAudio` 返回 `BackgroundAudioController`,`subscribeBackgroundAudio` 订阅背景音频控制器与播放状态变更,另有快捷方法 `playBackgroundAudio`、`pauseBackgroundAudio`、`toggleBackgroundAudio`、`setBackgroundAudioVolume`、`setBackgroundAudioMuted`、`toggleBackgroundAudioMuted`。`BackgroundAudio` 支持 `playing`(受控)或 `defaultPlaying`(非受控)。
204
548
 
205
549
  ### 移动端媒体激活
206
550
 
@@ -222,7 +566,7 @@ await viewer.zoomTo(48, { duration: 400, easing: "linear" });
222
566
 
223
567
  `onActivate` 的 `media` 提供 `resumeAudio()`、`playVideo({ unmute })` 和 `playBackgroundAudio()`。背景音乐若使用受控 `playing`,仍由宿主在回调内更新该状态;`viewer.activateMedia()` 可供自定义控件复用同一流程。
224
568
 
225
- 依赖未挂载子组件的方法(如无 `<WebVR />` 时调 `enterVR`)安全空操作,返回 `false`、`void` 或 `null`。
569
+ 依赖未挂载子组件的方法(如无 `<WebVR />` 时调 `enterVR`)不会执行相应操作;返回值按方法约定为 `false`、`undefined`、`null` 或对应的 Promise。
226
570
 
227
571
  ### 全屏
228
572
 
@@ -411,7 +755,7 @@ function BGMExample() {
411
755
  }
412
756
  ```
413
757
 
414
- ### 一站式示例
758
+ ### 综合示例
415
759
 
416
760
  ```tsx
417
761
  import {
@@ -447,7 +791,7 @@ function TourPlayer() {
447
791
 
448
792
  两个已废弃的自动旋转方法(`startAutoRotate`、`stopAutoRotate`)继续保留以兼容。鼠标、触控与键盘输入默认启用 — 普通浏览无需再渲染控件组件。仅当需要覆盖属性时渲染 `MouseControls`、`TouchControls` 或 `KeyboardControls`(子组件会替换该通道的默认实例)。通过 `controls` 调节共享行为(`inertia`、`invert`、`bouncingLimits`、`fovSpeed`、`frictionStop`、`rotateDamping`、`zoomDamping`,以及顶层的 `rotateSpeed` / `zoomSpeed`)。将 `controls.mouse` / `touch` / `keyboard` 设为 `false` 可关闭某通道,或传入选项对象(含 `enabled`)在不挂载子组件的情况下覆盖默认值。自动旋转在渲染 `AutoRotate` 之前处于关闭状态。默认右键菜单(重置视图 / 进入全屏)也会挂载 — 见 [`PanoContextMenu`](#panocontextmenu)。
449
793
 
450
- 拖拽与缩放会更新目标视图,相机平滑跟随。`rotateDamping` 与 `zoomDamping` 控制跟随速度,单位为秒⁻¹(默认分别为 `14` 与 `16`);数值越低手感越柔和,`0` 表示该轴不做平滑。两者必须为非负有限数。命令式 `setView()` 与 `reset()` 仍为即时生效。
794
+ 拖拽与缩放会更新目标视图,相机平滑跟随。`rotateDamping` 与 `zoomDamping` 控制跟随速度,单位为秒⁻¹(默认分别为 `14` 与 `16`);较小的正值使相机更缓慢地接近目标视角,`0` 表示该轴不做平滑。两者必须为非负有限数。命令式 `setView()` 与 `reset()` 仍为即时生效。
451
795
 
452
796
  画布光标默认为 `grab`,鼠标拖拽时为 `grabbing`,悬停在可交互热点上为 `pointer`,拖拽热点时为 `move`。可通过 `cursors`(`default`、`dragging`、`hotspot`、`hotspotDragging`)覆盖,或传 `cursors={false}` 保持画布光标不变。热点可设置 `cursor`,仅替换该热点的悬停光标。
453
797
 
@@ -469,7 +813,11 @@ function TourPlayer() {
469
813
 
470
814
  `Sphere` 需要 2:1 等距圆柱图像。当源图像的前向需要水平调整时,使用 `yawOffset`。
471
815
 
472
- `previewUrl` 为必填项,在 `src` 加载期间显示低分辨率 2:1 图像。相对路径相对于 `src` 所在目录解析;根绝对路径与 `http(s)` URL 原样使用。对于 krpano 球面场景,将 [`<preview url>`](https://krpano.com/docu/xml/#preview) 复制到 `previewUrl`。该预览必须是等距圆柱,而非立方体条带。
816
+ `Sphere` 将完整的 `src` 图像加载为单张纹理。高分辨率图像会增加下载、解码和 GPU 纹理内存开销;超出设备纹理尺寸或内存限制时,可能加载失败或导致 WebGL 上下文丢失,内存不足时也可能导致页面崩溃。当前球面渲染在顶部、底部极点附近仍可能出现可见瑕疵。
817
+
818
+ 高分辨率全景优先使用立方体瓦片 [`Tile`](#tile),通过 [@ericchen1990/pano-converter](../converter/README.zh-CN.md) 生成资源。查看器会按视野加载所需层级的瓦片,避免使用单张超大纹理。已有等距圆柱瓦片可使用 [`SphereTile`](#球面多分辨率瓦片)。
819
+
820
+ `previewUrl` 为必填项,在 `src` 加载期间显示低分辨率 2:1 图像。应提供尺寸较小的预览图,以减少首次显示的下载、解码和纹理内存开销。相对路径相对于 `src` 所在目录解析;根绝对路径与 `http(s)` URL 原样使用。对于 krpano 球面场景,将 [`<preview url>`](https://krpano.com/docu/xml/#preview) 复制到 `previewUrl`。该预览必须是等距圆柱,而非立方体条带。
473
821
 
474
822
  ```tsx
475
823
  "use client";
@@ -494,7 +842,7 @@ export function SphereExample() {
494
842
 
495
843
  默认播放栏挂载在查看器叠加层上:播放/暂停、进度、时间、音量、倍速、分辨率、字幕与全屏。当浏览器允许 `HTMLMediaElement.volume` 时,音量为悬停(或点击)垂直滑块;iOS Safari 仅暴露静音。倍速菜单列出当前元素接受的速率。字幕菜单还包含字幕字号与背景不透明度的设置面板。查看器较窄时,倍速、画质与字幕会收入「更多」菜单。传 `controls={false}` 可隐藏;传外观对象可重设默认实例样式;或将 `PanoVideoControls` 作为 `PanoViewer` 子组件以替换该实例。
496
844
 
497
- 字幕使用 WebVTT `tracks`。视频元素在屏幕外,因此 cue 文本以 HUD 叠加层渲染。`captions={false}` 会隐藏叠加层与语言菜单;对象会与默认外观合并(`color`、`background`、`fontSize`、`fontFamily`、`textShadow`、`padding`、`borderRadius`、`maxWidth`、`bottom`)。这些值作为字幕初始外观;观众仍可在运行时通过默认控制栏菜单调整字幕大小与背景。
845
+ 字幕使用 WebVTT `tracks`。视频元素以透明的 1×1 像素节点挂载在查看器内,字幕提示文本(cue)通过屏幕空间叠加层渲染。`captions={false}` 会隐藏叠加层与语言菜单;对象会与默认外观合并(`color`、`background`、`fontSize`、`fontFamily`、`textShadow`、`padding`、`borderRadius`、`maxWidth`、`bottom`)。这些值作为字幕初始外观;观众仍可在运行时通过默认控制栏菜单调整字幕大小与背景。
498
846
 
499
847
  ```tsx
500
848
  "use client";
@@ -538,11 +886,11 @@ export function VideoExample() {
538
886
  }
539
887
  ```
540
888
 
541
- `muted` 默认为 `true`,以便在浏览器自动播放规则下有机会自动播放;`playsInline` 也默认开启,并同时写入 `playsinline` / `webkit-playsinline`,避免 iPhone 将默认的 360 视频提取为原生全屏播放器。非静音播放仍需要用户手势;`play()` 被拒绝时 `onPlaybackStateChange` 会收到 `"blocked"`。非同源的远程视频与 VTT 文件应设置 `crossOrigin`(通常为 `"anonymous"`)。移动端请提供 H.264 + AAC 的 MP4 备选源、poster、CORS 与 Range 支持。
889
+ `muted` 默认为 `true`,以提高自动播放的兼容性;实际播放仍受浏览器策略约束。`playsInline` 也默认开启,并写入 `playsinline`、`webkit-playsinline` 以及 X5 的 `h5-page` 属性,向 iPhone Safari、微信、QQ 和 UC 请求页内播放;实际行为取决于浏览器及其版本。画中画和远程播放保持关闭,以便该元素持续作为球面纹理。元素会以 1×1 像素挂在查看器内。非静音播放可能需要用户手势;`play()` 被拒绝时 `onPlaybackStateChange` 会收到 `"blocked"`。非同源的远程视频与 VTT 文件应设置 `crossOrigin`(通常为 `"anonymous"`)。移动端请提供 H.264 + AAC 的 MP4 备选源、poster、CORS 与 Range 支持。
542
890
 
543
891
  ## Tile
544
892
 
545
- `Tile` 渲染六个朝内的立方体面,并仅加载当前视野附近的 tile。省略 `urlTemplate` 时,相对于 `baseUrl` 的路径使用本包默认:
893
+ `Tile` 渲染六个朝内的立方体面,并仅加载当前视野附近的瓦片。省略 `urlTemplate` 时,相对于 `baseUrl` 的路径使用本包默认:
546
894
 
547
895
  ```text
548
896
  tiles/%s/l%l/%v/l%l_%s_%h_%v.webp
@@ -566,7 +914,7 @@ export function TileExample() {
566
914
  }
567
915
  ```
568
916
 
569
- krpano 立方体 tile 输出不遵循该默认。从 krpano XML 读取 `url`、`multires` 与预览路径([`<cube>`](https://krpano.com/docu/xml/#image.cube)、[`<preview>`](https://krpano.com/docu/xml/#preview))并传入。若这些值缺失或与磁盘文件不匹配,tile 会解析到错误的 face、level 或行列,全景会看起来错乱。
917
+ krpano 立方体瓦片输出不遵循该默认。从 krpano XML 读取 `url`、`multires` 与预览路径([`<cube>`](https://krpano.com/docu/xml/#image.cube)、[`<preview>`](https://krpano.com/docu/xml/#preview))并传入。若这些值缺失或与磁盘文件不匹配,瓦片会解析到错误的立方体面、层级或行列,导致画面缺失或错位。
570
918
 
571
919
  | krpano | `Tile` / `TileScene` 属性 |
572
920
  |---|---|
@@ -599,9 +947,9 @@ krpano 文档中的简写语法:
599
947
  />
600
948
  ```
601
949
 
602
- MAKE PANO (MULTIRES) 文件夹布局常为 `tiles/%s/l%l/%v/l%l_%s_%v_%h.jpg`,场景根目录有 `preview.jpg`。该文件名为 `%v_%h`(先行后列)。本包默认 tile 模板为 `%h_%v`(先列后行)。请复制 XML 中的值,不要假设两种约定一致。
950
+ MAKE PANO (MULTIRES) 文件夹布局常为 `tiles/%s/l%l/%v/l%l_%s_%v_%h.jpg`,场景根目录有 `preview.jpg`。该文件名为 `%v_%h`(先行后列)。本包默认瓦片模板为 `%h_%v`(先列后行)。请复制 XML 中的值,不要假设两种约定一致。
603
951
 
604
- `multires` 的第一个值为 tile 尺寸。其余值为 `l1`、`l2` 及更高层级的递增立方体面尺寸。`previewUrl` 为必填。相对路径相对于 `baseUrl` 解析;根绝对路径与 `http(s)` URL 原样使用。典型的 krpano MAKE PANO (MULTIRES) 场景如下:
952
+ `multires` 的第一个值为瓦片尺寸。其余值按 `l1`、`l2` 及更高层级的顺序,指定递增的立方体面边长。`previewUrl` 为必填。相对路径相对于 `baseUrl` 解析;根绝对路径与 `http(s)` URL 原样使用。典型的 krpano MAKE PANO (MULTIRES) 场景如下:
605
953
 
606
954
  ```tsx
607
955
  <Tile
@@ -613,11 +961,11 @@ MAKE PANO (MULTIRES) 文件夹布局常为 `tiles/%s/l%l/%v/l%l_%s_%v_%h.jpg`,
613
961
  />
614
962
  ```
615
963
 
616
- `previewFaceOrder` 必须按自上而下 atlas 顺序列出六个面代码:`f`、`r`、`b`、`l`、`u`、`d`。预览图为竖向 1×6 立方体条带。krpano 的 `thumb.jpg` 是小缩略图,不是该 atlas。
964
+ `previewFaceOrder` 必须按预览图从上到下的实际顺序列出六个面代码。默认顺序为 `l`、`f`、`r`、`b`、`u`、`d`。预览图为竖向 1×6 立方体条带。krpano 的 `thumb.jpg` 通常是独立缩略图,不能替代该预览条带。
617
965
 
618
- 快速旋转或缩放时,已加载 tile 保持可见;新进入视野的 tile 会使用其父级或预览作为局部回退。
966
+ 快速旋转或缩放时,已加载瓦片保持可见;新进入视野的瓦片会使用其父级或预览作为局部回退。
619
967
 
620
- 对于非 krpano XML 的布局,可用占位符模板或解析器覆盖默认。两者均返回相对于 `baseUrl` 的路径:
968
+ 对于非 krpano XML 的布局,可用占位符模板或解析器覆盖默认。模板与解析器均可提供相对于 `baseUrl` 的路径或绝对 URL:
621
969
 
622
970
  ```tsx
623
971
  <Tile
@@ -633,11 +981,11 @@ MAKE PANO (MULTIRES) 文件夹布局常为 `tiles/%s/l%l/%v/l%l_%s_%v_%h.jpg`,
633
981
 
634
982
  `resolveTileUrl` 优先于 `urlTemplate`。
635
983
 
636
- 对于 krpano 立方体 tile 模板,`%s` 为立方体面,`%l` 为多分辨率层级。水平 tile 索引支持等价的 `%h`、`%x`、`%u`、`%c` 占位符;垂直索引支持 `%v`、`%y`、`%w`、`%r`。在索引占位符前加零可填充:例如 `%0h` 为两位水平索引,`%00v` 为三位垂直索引。立体 `%t` 与帧 `%f` 占位符不适用于表示单一非立体立方体全景的 `Tile`。
984
+ 对于 krpano 立方体瓦片模板,`%s` 为立方体面,`%l` 为多分辨率层级。水平瓦片索引支持等价的 `%h`、`%x`、`%u`、`%c` 占位符;垂直索引支持 `%v`、`%y`、`%w`、`%r`。在索引占位符前加零可填充:例如 `%0h` 为两位水平索引,`%00v` 为三位垂直索引。立体 `%t` 与帧 `%f` 占位符不适用于表示单一非立体立方体全景的 `Tile`。
637
985
 
638
986
  ## Scenes
639
987
 
640
- `Scenes` 在受控的普通球面、立方体瓦片与球面多级瓦片场景之间切换,采用纯 GPU 快照混合。目标场景在隐藏状态下预加载预览和基础层。预览可绘制后,当前帧缓冲成为临时 GPU 纹理,并释放旧场景的源纹理。目标随即进入 `full`:转场期间已经清晰的瓦片立刻盖住预览,尚未就绪的区域继续显示父层瓦片或预览。查看器不会同时保留两套高清瓦片,也不会预加载当前视野之外的瓦片。
988
+ `Scenes` 支持单张等距圆柱图像、立方体瓦片与等距圆柱瓦片场景之间的切换,并支持受控与非受控模式。过渡使用 GPU 帧缓冲快照与目标场景进行混合。目标场景在隐藏状态下加载预览;瓦片场景同时加载视野附近的最低分辨率层级。目标场景首次可绘制后,当前帧缓冲成为临时 GPU 纹理,并释放旧场景的源纹理。目标随即进入 `full`:转场期间已经清晰的瓦片立刻盖住预览,尚未就绪的区域继续显示父层瓦片或预览。旧场景释放后,目标场景才开始加载细节瓦片;瓦片选择范围包含当前视野及其周边的少量预留区域。
641
989
 
642
990
  ```tsx
643
991
  import {
@@ -669,19 +1017,19 @@ const scenes: Scene[] = [
669
1017
  </PanoViewer>;
670
1018
  ```
671
1019
 
672
- 可用的 KRpano 风格预设包括 `none`、`crossfade`、`zoom`、`blackout`、`whiteFlash`、`slideRightToLeft`、`slideTopToBottom`、`slideDiagonal`、`circleOpen`、`verticalOpen`、`horizontalOpen`、`ellipticZoomOpen`。网格类预设为 `pixelate`、`gridWipe`、`gridWipeUp`、`gridWipeRight`、`gridWipeDiagonal`、`gridWipeCenter`、`gridWipeChecker`、`hexDissolve`。电影感预设为 `dissolve`、`shatter`、`particles`、`glitch`、`swirl`、`clockWipe`、`ripple`、`zoomBlur`、`filmBurn`。它们均在入画全景之上混合 GPU 帧缓冲快照,不会混合两个完整 360 纹理。传 `{ preset: "crossfade", duration: 0.6 }` 可覆盖预设时长。
1020
+ 可用的 krpano 风格预设包括 `none`、`crossfade`、`zoom`、`blackout`、`whiteFlash`、`slideRightToLeft`、`slideTopToBottom`、`slideDiagonal`、`circleOpen`、`verticalOpen`、`horizontalOpen`、`ellipticZoomOpen`。网格类预设为 `pixelate`、`gridWipe`、`gridWipeUp`、`gridWipeRight`、`gridWipeDiagonal`、`gridWipeCenter`、`gridWipeChecker`、`hexDissolve`。电影感预设为 `dissolve`、`shatter`、`particles`、`glitch`、`swirl`、`clockWipe`、`ripple`、`zoomBlur`、`filmBurn`。它们均在目标全景之上混合 GPU 帧缓冲快照,不会混合两个完整 360 纹理。传 `{ preset: "crossfade", duration: 0.6 }` 可覆盖预设时长。
673
1021
 
674
- `transitionWaitForDetailMs` 默认为 `0`:目标一旦可绘制就开始转场,高清在转场期间继续加载。正数(例如 `200`)从旧画面快照完成、目标进入 `full` 之后开始计时;当前视野已经清晰就提前转场,否则最多再等待指定毫秒。该等待与转场动画时长分开计算。等待期间完整显示旧快照,目标在其下正常绘制并上传纹理。普通球面的“清晰”指完整图片可绘制;立方体和球面瓦片指按视口尺寸、设备像素比、FOV 和现有内存预算选出的当前视野目标层级全部可绘制,不要求视野外瓦片。负值或非有限值按 `0` 处理。`transition="none"` 使用同一条件,满足后直接切换。新的目标、中断、卸载或 WebGL 上下文丢失会取消尚未结束的等待,避免过期的就绪通知影响下一个场景。
1022
+ `transitionWaitForDetailMs` 默认为 `0`:目标一旦可绘制就开始转场,高分辨率细节在转场期间继续加载。正数(例如 `200`)从旧画面快照完成、目标进入 `full` 之后开始计时;当前视野的目标层级就绪后可提前转场,否则最多再等待指定毫秒。该等待与转场动画时长分开计算。等待期间完整显示旧快照,目标在其下正常绘制并上传纹理。单张图像场景的细节就绪指完整图片可绘制;立方体和球面瓦片指按视口尺寸、设备像素比、FOV 和现有内存预算选出的当前视野目标层级全部可绘制,不要求视野外瓦片。负值或非有限值按 `0` 处理。`transition="none"` 使用同一条件,满足后直接切换。新的目标、中断、卸载或 WebGL 上下文丢失会取消尚未结束的等待,避免过期的就绪通知影响下一个场景。
675
1023
 
676
- `onReady` 仍表示源第一次可绘制。`onSceneReady` 仍在首个场景可绘制,或旧画面快照完成且新场景接管时触发,并不表示当前视野的高清已经到齐。
1024
+ `onReady` 表示源第一次可绘制。`onSceneReady` 在首个场景可绘制,或旧画面快照完成且新场景接管时触发,并不表示当前视野的完整图像或细节瓦片已加载完成。
677
1025
 
678
- 清晰等待或转场进行时,全景拖拽/缩放输入被锁定,`renderHotspots` 被隐藏。新的 `activeSceneId` 会取代仍在准备中的目标。`maxTextureMemoryMb` 与 `maxConcurrentTileLoads` 作用于整个 `Scenes` 查看器,而非每个 tile 场景单独计算。
1026
+ 细节加载等待或转场进行时,全景拖拽/缩放输入被锁定,`renderHotspots` 被隐藏。新的 `activeSceneId` 会取代仍在准备中的目标。`maxTextureMemoryMb` 与 `maxConcurrentTileLoads` 作用于整个 `Scenes` 查看器,而非每个瓦片场景单独计算。
679
1027
 
680
1028
  ## PanoFilter
681
1029
 
682
- 在 `PanoViewer` 内渲染 `PanoFilter` 可对全景源(`Sphere`、`Tile`、`PanoVideo`)调色或风格化。滤镜仅作用于这些材质:3D 热点、tooltip、视频界面与 iframe 叠加层保持原色。WebXR / MobileVR 继承相同外观,因为修改的是全景着色器本身。
1030
+ 在 `PanoViewer` 内渲染 `PanoFilter` 可对全景源(`Sphere`、`Tile`、`SphereTile`、`PanoVideo`)调色或风格化。内置预设仅作用于全景材质;`colorMatrix` 默认仅作用于全景,设 `colorMatrixScope="all"` 可同时作用于热点纹理。tooltip、视频控件与 iframe 等 DOM 叠加层不受影响。WebXR / MobileVR 使用相同的全景着色器,因此继承相同的滤镜外观。
683
1031
 
684
- `intensity` 在滤镜结果与原图之间混合(`0`–`1`,默认 `1`)。省略组件、传 `preset="none"` 或设 `enabled={false}` 则保持源图无滤镜。
1032
+ `intensity` 在滤镜结果与原图之间混合(`0`–`1`,默认 `1`)。省略组件或设 `enabled={false}` 可关闭全部滤镜;`preset="none"` 仅关闭内置预设,已配置的 `colorMatrix` 仍会生效。
685
1033
 
686
1034
  ```tsx
687
1035
  <PanoViewer style={{ height: 560 }}>
@@ -709,7 +1057,7 @@ const scenes: Scene[] = [
709
1057
  | `cartoon` | 赛璐珞着色与描边 |
710
1058
  | `crosshatch` | 亮度交叉排线 |
711
1059
 
712
- 内置艺术效果使用屏幕空间边缘与世界空间颗粒,避免立方体 tile 出现滤镜接缝。有过滤器激活时,场景过渡快照会捕获带滤镜的全景。
1060
+ 内置艺术效果使用屏幕空间边缘与世界空间颗粒,避免立方体瓦片出现滤镜接缝。启用滤镜时,场景过渡快照会捕获带滤镜的全景。
713
1061
 
714
1062
  ## AutoRotate
715
1063
 
@@ -724,13 +1072,13 @@ const scenes: Scene[] = [
724
1072
 
725
1073
  ## BackgroundAudio
726
1074
 
727
- `BackgroundAudio` 在查看器里播放背景音乐或环境声。它不是热点:没有球面位置、没有标记、音量也不随视角变化。必须作为 `PanoViewer` 的子组件,与 `Scenes`、`Sphere` 或 `Tile` **并列**。不要写进 `Scenes` 的 `renderHotspots`:场景过渡时热点会卸载,背景声也会被停掉。
1075
+ `BackgroundAudio` 在查看器里播放背景音乐或环境声。它不是热点:没有球面位置、没有标记、音量也不随视角变化。必须作为 `PanoViewer` 的子组件,与 `Scenes`、`Sphere` 或 `Tile` **并列**。不要写进 `Scenes` 的 `renderHotspots`:场景过渡时热点会卸载,背景音频也会停止。
728
1076
 
729
- `playing` 由宿主控制,组件不会自行改这个值。`loop` 默认 `true`。浏览器可能拦住自动播放;此时 `onPlaybackStateChange` 收到 `"blocked"`,在 `playing` 仍为 true 时会于下一次点击或按键后重试。
1077
+ 传入 `playing` 时,播放状态由宿主控制;省略时使用内部状态,初始值由 `defaultPlaying` 指定(默认 `false`)。`loop` 默认 `true`。浏览器可能阻止自动播放;此时 `onPlaybackStateChange` 收到 `"blocked"`,播放意图仍为 `true` 时会在下一次点击或按键后重试。
730
1078
 
731
- 有两种用法,选一种即可。
1079
+ 可使用以下两种音轨配置方式。
732
1080
 
733
- **全部场景共用一条音轨** — 只传 `src`,不要传 `sources`。切换场景时音频继续播放,不会重头开始。
1081
+ **全部场景共用一条音轨** — 只传 `src`,不要传 `sources`。切换场景时音频继续播放,不会从头播放。
734
1082
 
735
1083
  ```tsx
736
1084
  import { BackgroundAudio, PanoViewer, Scenes } from "@ericchen1990/pano-view";
@@ -741,7 +1089,7 @@ import { BackgroundAudio, PanoViewer, Scenes } from "@ericchen1990/pano-view";
741
1089
  </PanoViewer>;
742
1090
  ```
743
1091
 
744
- **每个场景一条音轨** — 传 `sources`(键为 `Scene.id`)和当前的 `sceneId`。省略 `sceneId` 会抛错。切换场景时,若文件变了会按 `fadeMs` 交叉淡化(默认 400 毫秒;`0` 为硬切);若两个场景指向同一文件则继续播,不会重启。
1092
+ **每个场景一条音轨** — 传 `sources`(键为 `Scene.id`)和当前的 `sceneId`。省略 `sceneId` 会抛错。切换场景时,若文件变了会按 `fadeMs` 交叉淡化(默认 400 毫秒;`0` 表示立即切换);若两个场景指向同一文件则连续播放,不重新开始。
745
1093
 
746
1094
  ```tsx
747
1095
  <PanoViewer style={{ height: 560 }}>
@@ -768,9 +1116,9 @@ import { BackgroundAudio, PanoViewer, Scenes } from "@ericchen1990/pano-view";
768
1116
  />;
769
1117
  ```
770
1118
 
771
- 上例中:`lobby` 播院子声,`terrace` 静音,其它场景播 `default.mp3`。
1119
+ 上例中:`lobby` 播放庭院环境声,`terrace` 静音,其余场景播放 `default.mp3`。
772
1120
 
773
- 需要绑在全景某一点、随视角左右移动的声音,请用 `AudioHotspot`,不要用 `BackgroundAudio`。
1121
+ 需要将声源定位在全景某一点,并使立体声像随视角变化时,使用 `AudioHotspot`。
774
1122
 
775
1123
  ## Gyro
776
1124
 
@@ -820,7 +1168,7 @@ export function GyroExample() {
820
1168
  }
821
1169
  ```
822
1170
 
823
- `camroll`(默认 `true`)根据设备 roll 水平相机。`friction` 接受 `0` 到 `0.99`;越高越平滑但延迟更大。`softstart` 为启用时的混合时长(秒,默认 `0.5`)。`desktopSupport` 默认为 `false`,因桌面浏览器可能暴露 API 却无物理传感器。
1171
+ `camroll`(默认 `true`)根据设备的翻滚角(roll)调整相机倾斜。`friction` 接受 `0` 到 `0.99`;越高越平滑但延迟更大。`softstart` 为启用时的混合时长(秒,默认 `0.5`)。`desktopSupport` 默认为 `false`,因桌面浏览器可能暴露 API 却无物理传感器。
824
1172
 
825
1173
  `touchMode` 控制触控拖拽与设备运动的组合方式:
826
1174
 
@@ -1028,11 +1376,11 @@ import {
1028
1376
 
1029
1377
  `PanoViewer` 现有的 `onViewChange` / `onPanoramaClick` / `onPanoramaDoubleClick` / `onPanoramaPointerMove` props 仍受支持,共用同一事件总线。
1030
1378
 
1031
- 资源加载与场景混合仍由各自组件负责:使用 `Sphere` / `Tile` 的 `onLoad` / `onError` / `onLoadProgress`,以及 `Scenes` 的 `onTransitionEnd` / `onTransitionError`。本包不镜像 krpano 的 xml/VR/帧渲染事件。
1379
+ 资源加载回调由各组件提供:`Sphere` 使用 `onLoad`、`onError` 和 `onPreviewError`;`Tile` / `SphereTile` 使用 `onLoadProgress`、`onTileError` 和 `onPreviewError`。各源通过 `onReady` 报告首次可绘制状态;`Scenes` 通过 `onSceneReady`、`onTransitionEnd` 和 `onTransitionError` 报告场景状态。本包不逐一实现 krpano 的 XML 加载、VR 和逐帧渲染事件。
1032
1380
 
1033
1381
  ## PanoContextMenu
1034
1382
 
1035
- `PanoViewer` 会始终屏蔽浏览器原生右键菜单(画布、chrome 及其他 overlay 均生效),并挂载默认右键菜单。默认项:**Reset view** 与 **Enter fullscreen** / **Exit fullscreen**(文案与图标随全屏状态变化),中间有分隔线。
1383
+ `PanoViewer` 会始终屏蔽浏览器原生右键菜单(包括画布、内置界面与其他叠加层),并挂载默认右键菜单。默认项:**Reset view** 与 **Enter fullscreen** / **Exit fullscreen**(文案与图标随全屏状态变化),中间有分隔线。
1036
1384
 
1037
1385
  ```tsx
1038
1386
  <PanoViewer style={{ height: 560 }}>
@@ -1115,13 +1463,13 @@ import {
1115
1463
  </PanoViewer>
1116
1464
  ```
1117
1465
 
1118
- 在预设上用 `{ preset: "fullscreen", label: "…" }` 覆盖展示。在 prop 外完全手动组装时(例如自定义 `PanoContextMenu` 子组件),调用 `createPanoContextMenuPresets({ reset, toggleFullscreen, isFullscreen })` 并使用 `.resetView` / `.fullscreen` / `.defaults`。
1466
+ 使用 `{ preset: "fullscreen", label: "…" }` 可覆盖预设菜单项的显示文案。在 prop 外完全手动组装时(例如自定义 `PanoContextMenu` 子组件),调用 `createPanoContextMenuPresets({ reset, toggleFullscreen, isFullscreen })` 并使用 `.resetView` / `.fullscreen` / `.defaults`。
1119
1467
 
1120
1468
  具体项可使用 `icon` React 节点或 `image` URL。分隔线始终来自 `"separator"` 预设。外观涵盖背景、文字色、边框、圆角、阴影、仅背景的 `opacity`(从 `MIN_PANO_CONTEXT_MENU_BACKGROUND_OPACITY` / `0.4` 到 `1` 钳制;标签与图标保持不透明)、悬停/禁用色与图标尺寸。若需半透明填充而不使用 `opacity`,仍可传 `background: "rgba(...)"`。
1121
1469
 
1122
1470
  完全自定义 DOM 菜单时,设 `contextMenu={false}` 并渲染 `PanoContextMenu` 子组件。不要同时挂载默认菜单与子 `PanoContextMenu`。避免与 `controls.mouse.buttons: ["right"]` 同时使用 — 两者都会占用右键。
1123
1471
 
1124
- ## 共享热点约定与保存的定义
1472
+ ## 共享热点约定与可序列化定义
1125
1473
 
1126
1474
  每个热点有 `id`,以及可选的 `visible`、`interactive`、`pointerEvents`、`renderOrder` 与语义交互回调。点热点此外还使用受控 `position`、角度 `width`/`height`、热点 `mode` 与下文缩放选项。`interactive={false}` 保持热点可见,同时让绘制工具接收其下方的全景指针事件。`pointerEvents="none"` 忽略鼠标、触控与笔,事件穿透到全景,热点仍可键盘交互。可交互热点悬停时画布光标为 `pointer`;若设置了热点 `cursor` 则使用该值。
1127
1475
 
@@ -1161,6 +1509,11 @@ import {
1161
1509
  fontSize: 13,
1162
1510
  }}
1163
1511
  />
1512
+ ```
1513
+
1514
+ 按悬停触发的提示框示例:
1515
+
1516
+ ```tsx
1164
1517
  <GraphicHotspot
1165
1518
  id="marker"
1166
1519
  ariaLabel="Signal"
@@ -1172,7 +1525,7 @@ import {
1172
1525
  />
1173
1526
  ```
1174
1527
 
1175
- 持久化或宿主编辑器可使用导出的判别联合类型 `HotspotDefinition`。新增变体会破坏 exhaustive switch 的类型兼容;除现有点与路径类别外,还需处理 `audio`:
1528
+ 持久化或宿主编辑器可使用导出的判别联合类型 `HotspotDefinition`。渲染时按 `type` 区分各变体;使用穷尽检查的 `switch` 时,应覆盖联合类型中的全部热点类别:
1176
1529
 
1177
1530
  ```ts
1178
1531
  import type { HotspotDefinition } from "@ericchen1990/pano-view";
@@ -1261,16 +1614,16 @@ import { GraphicHotspot } from "@ericchen1990/pano-view";
1261
1614
  />
1262
1615
  ```
1263
1616
 
1264
- `cornerRadius` 相对于渲染图形较短边:`0` 为直角,`0.5` 为最大圆角。这取代了此前基于 Canvas 纹理像素的解释方式。
1617
+ `cornerRadius` 相对于渲染图形较短边:`0` 为直角,`0.5` 为最大圆角。该值为相对比例,不使用纹理像素作为单位。
1265
1618
 
1266
1619
  `GraphicHotspot` 与 `ImageHotspot` 相同,使用 `onLoad(texture)` 与 `onError(error)`。内置 path 本地栅格化;仅接受 SVG URL 或 SVG `path` 加显式 `viewBox`,不接受任意 SVG 标记。
1267
1620
 
1268
1621
  ## 热点模式与缩放行为
1269
1622
 
1270
- 点热点使用统一的 `mode`,而非分离的方向与 placement 属性:
1623
+ 点热点通过 `mode` 统一设置朝向与放置方式:
1271
1624
 
1272
1625
  - `mode="surface"` 将平面附着于局部全景表面。
1273
- - `mode="billboard"` 使平面始终朝向相机并浮于壳层前方。用 `distance`(世界单位,默认 `10`)拉近。内置全景壳层约在 `1000` 单位;距离会自动上限,避免热点角点穿过壳层。
1626
+ - `mode="billboard"` 使平面始终朝向相机并浮于壳层前方。`distance` 是热点到相机的距离(世界单位,默认 `10`),减小该值可将热点移近相机。内置全景壳层约在 `1000` 单位;距离会自动限制在允许范围内,避免热点角点穿过壳层。
1274
1627
  - `scaleMode="fov"`(默认)在用户放大时热点变大。`scaleMode="fixed"` 补偿 FOV 变化,屏幕尺寸接近 `referenceFov`(默认 `75`)时的大小。
1275
1628
  - `rotation` 绕自身法线旋转(度);正值顺时针,负值逆时针。`scale` 为角向 width/height 的整体正倍率,默认 `1`。
1276
1629
 
@@ -1321,7 +1674,7 @@ import { PolygonHotspot } from "@ericchen1990/pano-view";
1321
1674
 
1322
1675
  `strokeWidth` 为屏幕空间 CSS 像素,FOV 变化时视觉一致。`strokeDashSize` 与 `strokeGapSize` 同为 CSS 像素;省略或设 `strokeDashSize` 为 `0` 为实线。拖拽整体平移所有顶点,同时归一化 yaw、钳制 pitch。单顶点编辑由创作工作流提供,非运行时热点组件职责。
1323
1676
 
1324
- 设 `fillOpacity={0}` 可得仅轮廓闭合多边形。填充与轮廓共用相同球面边采样,无意图性接缝。
1677
+ 设 `fillOpacity={0}` 可得仅轮廓闭合多边形。填充与轮廓共用相同球面边界采样,使轮廓沿填充区域的边界绘制。
1325
1678
 
1326
1679
  ## PolylineHotspot
1327
1680
 
@@ -1374,7 +1727,7 @@ import { SequenceHotspot } from "@ericchen1990/pano-view";
1374
1727
 
1375
1728
  ## VideoHotspot
1376
1729
 
1377
- `VideoHotspot` 使用 `HTMLVideoElement` 与 `VideoTexture`。其 `playing` prop 同样受控:在点击处理或应用状态中切换。源变更或卸载时元素暂停并释放纹理。`playsInline` 默认 `true`,`muted` 默认 `true`,`loop` 默认 `false`,`preload` 默认 `"metadata"`。
1730
+ `VideoHotspot` 使用 `HTMLVideoElement` 与 `VideoTexture`。其 `playing` prop 同样受控:在点击处理或应用状态中切换。源变更或卸载时元素暂停并释放纹理。`playsInline` 默认 `true`,并使用与 `PanoVideo` 相同的内联播放属性。元素会隐藏挂在查看器内,以便移动端浏览器把它留在页面中。`muted` 默认 `true`,`loop` 默认 `false`,`preload` 默认 `"metadata"`。
1378
1731
 
1379
1732
  ```tsx
1380
1733
  import { VideoHotspot } from "@ericchen1990/pano-view";
@@ -1404,13 +1757,13 @@ import { VideoHotspot } from "@ericchen1990/pano-view";
1404
1757
 
1405
1758
  ## AudioHotspot
1406
1759
 
1407
- `AudioHotspot` 在球面位置播放定向声音。立体声像跟随相机朝向;`range` 为看离声源多少度后音量降到静音。默认 `360` 表示不因视角衰减(仅左右声像)。定点声源可使用较小值,例如 `90`。
1760
+ `AudioHotspot` 在球面位置播放定向声音。立体声像跟随相机朝向;`range` 指视线偏离声源达到该角度时,音量衰减至静音。默认 `360` 表示不因视角衰减(仅左右声像)。定点声源可使用较小值,例如 `90`。
1408
1761
 
1409
- 播放走 Howler.js 的 Web Audio(已打进本包,宿主不必安装 `howler`)。文件需先解码再起播,推荐 MP3。`src` 可以是字符串或回退 URL 数组。不要用 HTML5 流式播放,否则会失去立体声像。
1762
+ 音频通过 Howler.js 的 Web Audio 模式播放(Howler.js 已包含在本包构建产物中,宿主无需另行安装)。文件需完成解码后才能开始播放,可使用 MP3 格式。`src` 可以是字符串或回退 URL 数组。组件不使用 HTML5 流式音频模式,以保留立体声像控制。
1410
1763
 
1411
- `playing` 与 `VideoHotspot` 一样受控。`loop` 默认 `false`,`muted` 默认 `false`,`volume` 默认 `1`,`pauseWhenHidden` 默认 `true`(标签页隐藏时暂停,仍为 `playing` 则回来后恢复)。首次用户手势会解锁音频;若自动播放被拦,会报告 `"blocked"`,并在 `playing` 仍为 true 时于下一次指针或按键后重试 `play()`。
1764
+ `playing` 与 `VideoHotspot` 一样受控。`loop` 默认 `false`,`muted` 默认 `false`,`volume` 默认 `1`,`pauseWhenHidden` 默认 `true`(标签页隐藏时暂停,`playing` 仍为 `true` 时,在标签页恢复可见后继续播放)。首次用户手势会解锁音频;若自动播放被阻止,会报告 `"blocked"`,并在 `playing` 仍为 true 时于下一次指针或按键后重试 `play()`。
1412
1765
 
1413
- 默认标记在 **停止**(扬声器)与 **播放**(扬声器 + 声波)之间切换。可用 `icon` 自定义停止图标、`playingIcon` 自定义播放图标。只设 `icon` 时两种状态共用该图。`marker={false}` 则无视觉,仍作为定点声源。非空间的全 tour 或按场景背景声请使用 [`BackgroundAudio`](#backgroundaudio)。
1766
+ 默认标记在 **停止**(扬声器)与 **播放**(扬声器 + 声波)之间切换。可用 `icon` 自定义停止图标、`playingIcon` 自定义播放图标。只设 `icon` 时两种状态共用该图。`marker={false}` 隐藏标记,仍保留定点声源。整个导览共用或按场景切换的非空间背景音频请使用 [`BackgroundAudio`](#backgroundaudio)。
1414
1767
 
1415
1768
  ```tsx
1416
1769
  import { AudioHotspot } from "@ericchen1990/pano-view";
@@ -1486,7 +1839,7 @@ import { IframeHotspot } from "@ericchen1990/pano-view";
1486
1839
 
1487
1840
  ## Next.js 与 SSR
1488
1841
 
1489
- 分发入口标记为客户端模块。请在 Client Component 边界之下渲染。全景资源在浏览器加载;无需 API 密钥或服务器配置。
1842
+ 组件入口标记为客户端模块,应在 Client Component 边界内渲染。服务端加载配置时使用 `@ericchen1990/pano-view/data`。全景资源由浏览器加载;查看器本身无需 API 密钥,资源服务器仍需配置正确的访问权限与 CORS。
1490
1843
 
1491
1844
  在 Client Component 或全局 CSS 中导入一次样式表:
1492
1845
 
@@ -1498,7 +1851,11 @@ import "@ericchen1990/pano-view/styles.css";
1498
1851
 
1499
1852
  ## 球面多分辨率瓦片
1500
1853
 
1501
- `SphereTile` 直接渲染完整的 2:1 等距柱状全景瓦片,不拼接高分辨率整图:
1854
+ `SphereTile` 渲染覆盖完整 2:1 等距圆柱全景的多分辨率瓦片金字塔。组件根据视角、缩放和纹理预算选择瓦片,将每块瓦片作为独立纹理映射到对应的球面区域;尚未加载的区域使用较低分辨率瓦片或预览图显示。
1855
+
1856
+ 这一方式无需将整个全景加载为单张高分辨率纹理,可降低大图解码和纹理上传的峰值开销;实际内存占用仍取决于瓦片尺寸、可见层级和缓存预算。当前球面渲染在顶部、底部极点附近仍可能出现可见瑕疵。新生成的全景资源优先采用立方体瓦片 [`Tile`](#tile);已有等距圆柱瓦片可使用 `SphereTile`。
1857
+
1858
+ 基本用法:
1502
1859
 
1503
1860
  ```tsx
1504
1861
  import { PanoViewer, SphereTile } from "@ericchen1990/pano-view";
@@ -1558,8 +1915,8 @@ const scenes: Scene[] = [
1558
1915
 
1559
1916
  `onSceneReady(sceneId)` 在首个场景可绘制、或旧画面截图完成且新场景接管时触发。
1560
1917
  此时过渡可能仍在进行,可在该回调中设置新场景视角而不改变旧画面快照。
1561
- 高清在这次交接之后加载,转场期间也会继续加载。`transitionWaitForDetailMs`
1562
- 可以让旧快照多停留到当前视野清晰,但不会改变 `onSceneReady` 的触发时机。
1918
+ 高分辨率细节在这次交接之后加载,转场期间也会继续加载。`transitionWaitForDetailMs`
1919
+ 可以在设定时限内保留旧快照,等待当前视野的目标细节层级就绪,但不会改变 `onSceneReady` 的触发时机。
1563
1920
  `onTransitionEnd` 表示混合效果播放完成。
1564
1921
 
1565
1922
  `SphereTileScene` 保留 `SphereTile` 的资源选项,包括 `indexBase`、`resolveTileUrl`
@@ -1572,11 +1929,73 @@ const scenes: Scene[] = [
1572
1929
 
1573
1930
  `PanoFilter.colorMatrix` 接收只读的行优先 4×5 RGBA 矩阵(20 个有限数值,与 SVG
1574
1931
  `feColorMatrix` 一致),在预设滤镜之后、显示/sRGB 颜色空间中应用。`intensity`
1575
- 控制效果混合比例,`enabled={false}` 关闭预设和矩阵。`colorMatrixScope="all"`
1932
+ 控制各效果与其输入颜色的混合比例,`enabled={false}` 关闭预设和矩阵。`colorMatrixScope="all"`
1576
1933
  将矩阵同时应用于热点纹理;默认 `"panorama"` 保持原有行为。艺术预设始终仅作用于
1577
1934
  全景。HTML 标签、iframe 和其他 DOM 界面仍由宿主设置样式。
1578
1935
 
1579
- 热点的 `tooltipAppearance` 新增 `fontFamily`、`lineHeight`、`letterSpacing`,
1580
- 配合原有的 `fontSize` 使用。数字 `lineHeight` 为无单位倍数,数字 `letterSpacing` 为 CSS 像素。
1936
+ 热点的 `tooltipAppearance` 支持 `fontFamily`、`lineHeight`、`letterSpacing`,
1937
+ 配合 `fontSize` 使用。数字 `lineHeight` 为无单位倍数,数字 `letterSpacing` 为 CSS 像素。
1581
1938
 
1582
1939
  回归验证:`pnpm --filter @ericchen1990/pano-view test`(Node 22+)。
1940
+
1941
+ ## 常见问题与故障排查
1942
+
1943
+ ### 为什么推荐立方体瓦片?
1944
+
1945
+ [`Tile`](#tile) 根据视野和缩放加载立方体瓦片,可避免将完整全景作为单张超大纹理加载,并避开当前球面渲染在极点附近的瑕疵。已有等距圆柱瓦片可使用 [`SphereTile`](#球面多分辨率瓦片),同样支持按视野加载;瓦片化本身不会消除球面极点附近的渲染瑕疵。`Sphere` 适合尺寸可控的单张图像。预览图用于改善首次显示速度,仍应控制其尺寸。详见 [Sphere](#sphere)。
1946
+
1947
+ ### 没有数据库,怎样发布多个场景?
1948
+
1949
+ 保留转换目录结构,编写引用各 `viewer.json` 的 `project.json`,用 `loadPanoProject` 和 `ProjectScenes` 展示。将 React 构建产物和资源上传到静态托管即可。部署到 `/demo/` 等子目录时,项目入口也要带正确前缀;项目内部优先用相对地址。
1950
+
1951
+ ### JSON 或纹理跨域加载失败?
1952
+
1953
+ 先看浏览器 Network 与 Console,确认具体失败的是配置、预览还是瓦片。服务端/CDN 必须允许前端 origin 读取这些资源。公开资源可以使用 `Access-Control-Allow-Origin: *`;携带 Cookie 等凭据时必须使用明确允许的 origin 和 `Access-Control-Allow-Credentials: true`,动态响应 origin 时设置 `Vary: Origin`。浏览器可能只向 JavaScript 暴露 `Failed to fetch`,库不会据此断言原因。
1954
+
1955
+ `no-cors` 得到不可读取的 opaque 响应,不能解析 JSON,也不能用于需要 CORS 的 WebGL 纹理。应修改服务器配置,不能靠前端关闭安全检查。详见 [MDN CORS](https://developer.mozilla.org/en-US/docs/Web/HTTP/Guides/CORS) 和 [WebGL 纹理](https://developer.mozilla.org/en-US/docs/Web/API/WebGL_API/Tutorial/Using_textures_in_WebGL)。
1956
+
1957
+ S3 控制台的公开读取 CORS 示例(替换成实际网站 origin):
1958
+
1959
+ ```json
1960
+ [{
1961
+ "AllowedOrigins": ["https://app.example.com"],
1962
+ "AllowedMethods": ["GET", "HEAD"],
1963
+ "AllowedHeaders": ["Authorization"]
1964
+ }]
1965
+ ```
1966
+
1967
+ CORS 不授予对象读取权限。使用 CDN 时也需确保实际响应带有正确头。参阅 [S3 CORS 官方说明](https://docs.aws.amazon.com/AmazonS3/latest/userguide/cors.html)。
1968
+
1969
+ ### 404 或请求地址看起来不对?
1970
+
1971
+ 比较 Network 中的完整 URL 与云端对象 key,检查 `baseUrl`、多一层目录、大小写及文件名。只换域名或前缀时调整根地址;内部文件也改名时使用资源映射。保留转换器的模板,尤其注意 `%v_%h` 与 `%h_%v` 的区别。
1972
+
1973
+ ### 报 INVALID_JSON,响应却是 200?
1974
+
1975
+ 打开响应正文检查是否为 HTML。常见原因是 SPA fallback 把不存在的 JSON 路径返回为首页,或认证系统重定向到登录页。修正配置地址和静态路由;HTTP 200 不代表内容一定是 JSON。
1976
+
1977
+ ### JSON 正常,图片却是 401/403 或签名过期?
1978
+
1979
+ 配置认证与图片读取权限分别处理。`requestInit` 的头和凭据只用于 JSON;配置 URL 的 token 不会自动附加到瓦片。为图片提供可用的公开 URL、签名 URL 或适合应用的认证访问方案。异步取得签名后再加载/映射,签名过期后重新获取;不要把临时凭据永久写入转换 JSON。
1980
+
1981
+ ### 配置加载成功,为什么画面仍是空白?
1982
+
1983
+ 检查 `PanoViewer` 的容器是否有非零高度、纹理是否成功,以及 Console 中的 WebGL 错误。保持 React、Three.js 和 React Three Fiber 单一副本。纯数据加载成功不代表资源可绘制,应观察 `onSceneReady` 和 `onTransitionError`。
1984
+
1985
+ ### 预览正常,高清缺失或错位?
1986
+
1987
+ 检查瓦片是否全部上传,使用实际层级尺寸及转换输出的 `urlTemplate`、`previewFaceOrder`、`indexBase`。不要假定层级总能乘二或混淆从零开始的逻辑地址与存储编号;自定义 resolver 需要返回原文件对应的地址。
1988
+
1989
+ ### 热点不显示,或点击不跳转?
1990
+
1991
+ 检查所属场景、`visible`、`interactive`、`pointerEvents`、角度与资源请求。场景过渡时热点会暂时隐藏。场景 ID 和热点 ID 按各自范围唯一,`action.sceneId` 必须存在;`preventDefault()` 会取消默认动作。未配置 action 的文本只是文字,不会自动创建弹窗。
1992
+
1993
+ ### 音视频没有播放?
1994
+
1995
+ 使用点击/键盘等用户手势触发播放,检查格式和权限,并用 `onHotspotError` 查看失败。浏览器可阻止非静音自动播放;隐藏的标签页和设备规则也可能影响播放。组件不会绕过浏览器限制。
1996
+
1997
+ ### 双击本地 HTML、混合内容、缓存或版本问题?
1998
+
1999
+ 通过本地 HTTP(S) 服务预览,不使用 `file://`。HTTPS 页面使用 HTTPS 资源。采用带版本的资源目录,并协调项目 JSON 与图片的缓存,避免旧配置请求新目录。`UNSUPPORTED_SCHEMA_VERSION` 需要匹配支持的配置版本;平面图没有 `viewer.json`,需要平面查看器。
2000
+
2001
+ 转换阶段的问题另见 [converter 错误码与故障排查](../converter/README.zh-CN.md#错误码与故障排查)。