@taole/giftstage 0.1.36 → 0.2.1

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.
Files changed (51) hide show
  1. package/README.md +770 -694
  2. package/dist/core/audio-manager.d.ts +10 -1
  3. package/dist/core/gift-stage.d.ts +63 -1
  4. package/dist/core/index.d.ts +4 -4
  5. package/dist/core/post-animator.d.ts +1 -1
  6. package/dist/core/render-manager.d.ts +55 -6
  7. package/dist/{gift-stage.cjs.js → gift-stage.cjs} +1401 -287
  8. package/dist/gift-stage.es.js +11496 -6099
  9. package/dist/gpu/backend-operation-result.d.ts +6 -0
  10. package/dist/gpu/detect.d.ts +1 -1
  11. package/dist/gpu/index.d.ts +3 -3
  12. package/dist/gpu/webgl1-backend.d.ts +8 -4
  13. package/dist/gpu/webgl2-backend.d.ts +154 -4
  14. package/dist/gpu/webgpu-backend.d.ts +130 -4
  15. package/dist/index.d.ts +36 -27
  16. package/dist/parsers/atlas-builder.d.ts +1 -1
  17. package/dist/parsers/index.d.ts +7 -7
  18. package/dist/parsers/svga-parser-worker-client.d.ts +1 -1
  19. package/dist/parsers/svga-parser.d.ts +1 -1
  20. package/dist/parsers/svga-proto-js.d.ts +1 -1
  21. package/dist/parsers/svga-sprite-table-binary.d.ts +1 -1
  22. package/dist/parsers/svga-sprite-table.d.ts +1 -1
  23. package/dist/parsers/svga-worker-payload-binary.d.ts +3 -3
  24. package/dist/parsers/vap-config-parser.d.ts +1 -1
  25. package/dist/plugin/motion-track.d.ts +19 -0
  26. package/dist/plugin/playback-scope.d.ts +42 -0
  27. package/dist/plugin/types.d.ts +183 -0
  28. package/dist/public.d.ts +2 -0
  29. package/dist/renderers/alpha-video-renderer.d.ts +1 -1
  30. package/dist/renderers/image-renderer.d.ts +1 -1
  31. package/dist/renderers/index.d.ts +4 -4
  32. package/dist/renderers/particle-batch-renderer.d.ts +19 -0
  33. package/dist/renderers/svga-batch-renderer.d.ts +140 -7
  34. package/dist/renderers/vap-renderer.d.ts +1 -1
  35. package/dist/shaders/index.d.ts +20 -0
  36. package/dist/types/index.d.ts +357 -6
  37. package/dist/utils/clip-mesh.d.ts +13 -0
  38. package/dist/utils/gift-object-fit.d.ts +2 -2
  39. package/dist/utils/gift-transform.d.ts +3 -1
  40. package/dist/utils/resource-cache.d.ts +1 -1
  41. package/dist/utils/slot-content.d.ts +1 -1
  42. package/dist/utils/svga-binary-cache.d.ts +2 -2
  43. package/dist/utils/svga-frame-timeline.d.ts +95 -0
  44. package/dist/utils/svga-hybrid-command-table.d.ts +12 -0
  45. package/dist/utils/svga-slot-state-schema.d.ts +28 -0
  46. package/dist/utils/vap-mix-composer.d.ts +1 -1
  47. package/dist/utils/wc-atlas-upload-size.d.ts +1 -1
  48. package/dist/wasm/wasm-bridge.d.ts +1 -1
  49. package/dist/workers/svga-atlas-worker-client.d.ts +1 -1
  50. package/dist/workers/svga-clip-worker-client.d.ts +1 -1
  51. package/package.json +81 -76
package/README.md CHANGED
@@ -1,719 +1,795 @@
1
- # GiftStage
2
-
3
- 统一的 Web 礼物播放框架,支持:
4
-
5
- - `SVGA`
6
- - `VAP / VAPX`
7
- - 透明视频 `AlphaVideo`
8
- - 静态图片 `Image`
9
-
10
- 底层支持:
11
-
12
- - `WebGPU`
13
- - `WebGL2`
14
- - `WebGL1` 兼容兜底
15
-
16
- 适用场景:
17
-
18
- - 直播间礼物
19
- - 大批同屏动画
20
- - 需要统一接入 `SVGA / VAP / 透明视频 / 图片` 的业务场景
21
-
22
- ## npm 包信息
23
-
24
- - 包名:`@taole/giftstage`
25
- - npm:`npm i @taole/giftstage`
26
- - ESM 导入:
27
-
28
- ```ts
29
- import { GiftStage } from '@taole/giftstage';
30
- ```
31
-
32
- - CommonJS 导入:
33
-
34
- ```js
35
- const { GiftStage } = require('@taole/giftstage');
36
- ```
37
-
38
- ## 特性
39
-
40
- - 统一 API:`addGift()` 即可播放四类礼物
41
- - `SVGA` 支持 Worker 解析、atlas 缓存、slot 替换
42
- - `VAP / AlphaVideo` 支持实验性的 `WebCodecs` 和稳定的 `HTMLVideoElement` 双路径
43
- - 同源视频播放实例、纹理、资源复用
44
- - 内存 LRU 缓存,默认 `128MB`
45
- - `IndexedDB` 磁盘 LRU 缓存,默认 `2048MB`
46
- - `x / y` 支持百分比定位
47
- - `width / height` 支持按礼物原始尺寸自动补全
48
- - 四类礼物共用位移、缩放、透明度、旋转、层级和销毁生命周期
49
-
50
- ## 安装
51
-
52
- ```bash
53
- npm install
54
- ```
55
-
56
- 安装发布包:
57
-
58
- ```bash
59
- npm i @taole/giftstage
60
- ```
61
-
62
- ## 开发
63
-
64
- ```bash
65
- npm run dev
66
- ```
67
-
68
- ## 构建
69
-
70
- ```bash
71
- npm run build
72
- ```
73
-
74
- Demo 构建:
75
-
76
- ```bash
77
- npm run build:demo
78
- ```
79
-
80
- ## 性能基线
81
-
82
- ```bash
83
- # 在当前机器记录 WebGL2 / WebGPU 基线
84
- npm run perf:record
85
-
86
- # 与同浏览器主版本、硬件并发数、DPR 和视口的基线比较
87
- npm run test:perf
88
- ```
89
-
90
- 基线覆盖冷/热启动、100 个静态图片、50 个共享 SVGA、20 个共享 VAP 20 个共享 AlphaVideo,
91
- 记录首帧、P50/P95 帧间隔、长任务、堆变化、提交帧、draw、纹理上传与 stencil 帧数。
92
- 基线保存在 `tests/performance/baselines/`;Windows 会自动探测系统 Chrome,也可通过
93
- `PLAYWRIGHT_CHROME_EXECUTABLE_PATH` 指定浏览器。
94
-
95
- ## 快速开始
96
-
97
- ```ts
98
- import { GiftStage } from '@taole/giftstage';
99
-
100
- const container = document.getElementById('app')!;
101
-
102
- const stage = new GiftStage({
103
- container,
104
- preferWebGPU: true,
105
- });
106
-
107
- await stage.ready;
108
-
109
- await stage.addGift({
110
- type: 'svga',
111
- source: 'https://example.com/demo.svga',
112
- x: 100,
113
- y: 100,
114
- loop: 0,
115
- });
116
- ```
117
-
118
- ## 核心 API
119
-
120
- ### `new GiftStage(options)`
121
-
122
- 创建礼物舞台。
123
-
124
- 常用配置:
125
-
126
- - `container: HTMLElement`
127
- 挂载容器。
128
-
129
- - `resolution?: number`
130
- 指定 canvas backing store 分辨率倍率。默认跟随 `devicePixelRatio`。
131
-
132
- - `antialias?: boolean`
133
- 是否开启抗锯齿,WebGL 和 WebGPU 均默认开启;WebGPU 使用 4× MSAA。大量透明视频同时播放时可显式设为 `false`。
134
-
135
- - `preferWebGPU?: boolean`
136
- 是否优先使用 `WebGPU`。
137
-
138
- - `forceWebGL1?: boolean`
139
- 强制走 `WebGL1`,用于兼容性验证。
140
-
141
- - `preferWebCodecs?: boolean`
142
- 实验性特性。默认关闭;显式传 `true` 时才会尝试用 `WebCodecs` 播放 `VAP / AlphaVideo`。
143
-
144
- - `shareIdenticalVideoPlayback?: boolean`
145
- 是否复用近同时起播的同源视频播放实例。默认开启。
146
-
147
- - `sharedVideoPlaybackWindowMs?: number`
148
- 同源视频共享播放窗口,默认 `100ms`。
149
-
150
- - `webCodecsVideoAtlas?: { width: number; height: number }`
151
- 实验性 `WebCodecs` 视频共享 atlas 配置。
152
-
153
- - `webCodecsDecodeInWorker?: boolean`
154
- 实验性 `WebCodecs` 选项,控制是否在 Worker 中执行解码。
155
-
156
- - `webCodecsMaxDecodedFrames?: number`
157
- 实验性 `WebCodecs` 选项,控制保留的最大解码帧数。
158
-
159
- - `webCodecsDecodeAheadFrames?: number`
160
- 实验性 `WebCodecs` 选项,控制前向解码帧数。
161
-
162
- - `svgaParseInWorker?: boolean`
163
- 是否在 Worker 中解析 SVGA。
164
-
165
- - `maxConcurrentParse?: number`
166
- 资源下载 / 解析 / 图像解码的并发数。
167
- 不传时自动跟随 `navigator.hardwareConcurrency`。
168
-
169
- - `svgaAtlasFrameBudgetMs?: number`
170
- SVGA atlas 在主线程组合或逐页上传时的单帧时间片上限,默认 `12ms`。
171
- 实际时间片会按当前刷新周期的一半动态调整,避免高刷新率设备上的加载任务挤占渲染帧。
172
-
173
- - `svgaLoadFrameBudgetMs?: number`
174
- SVGA 主线程加载任务的共享时间片上限,默认 `12ms`。图片 fallback、atlas、clip、音频、slot 纹理和二进制缓存共用同一帧预算。
175
-
176
- - `svgaWorkerFrameBudgetMs?: number`
177
- SVGA Worker 连续执行 JS 循环的时间片上限,默认 `4ms`,范围 `1-8ms`。单次 WASM protobuf 调用和单次原生 API 调用仍不可抢占。
178
-
179
- - `svgaWorkerTaskConcurrency?: number`
180
- parser、图片解码、atlas clip CPU 密集 Worker 的全局并发上限,默认 `1`。Atlas 首帧页面可提前返回,但后台 PNG 编码完成前仍持有该名额,避免与其他重载 Worker 叠加。
181
-
1
+ # GiftStage
2
+
3
+ 统一的 Web 礼物播放框架,支持:
4
+
5
+ - `SVGA`
6
+ - `VAP / VAPX`
7
+ - 透明视频 `AlphaVideo`
8
+ - 静态图片 `Image`
9
+
10
+ 底层支持:
11
+
12
+ - `WebGPU`
13
+ - `WebGL2`
14
+ - `WebGL1` 兼容兜底
15
+
16
+ 适用场景:
17
+
18
+ - 直播间礼物
19
+ - 大批同屏动画
20
+ - 需要统一接入 `SVGA / VAP / 透明视频 / 图片` 的业务场景
21
+
22
+ ## npm 包信息
23
+
24
+ - 包名:`@taole/giftstage`
25
+ - npm:`npm i @taole/giftstage`
26
+ - ESM 导入:
27
+
28
+ ```ts
29
+ import { GiftStage } from '@taole/giftstage';
30
+ ```
31
+
32
+ - CommonJS 导入:
33
+
34
+ ```js
35
+ const { GiftStage } = require('@taole/giftstage');
36
+ ```
37
+
38
+ ## 特性
39
+
40
+ - 统一 API:`addGift()` 即可播放四类礼物
41
+ - `SVGA` 支持 Worker 解析、atlas 缓存、slot 替换
42
+ - `VAP / AlphaVideo` 支持实验性的 `WebCodecs` 和稳定的 `HTMLVideoElement` 双路径
43
+ - 同源视频播放实例、纹理、资源复用
44
+ - 内存 LRU 缓存,默认 `128MB`
45
+ - `IndexedDB` 磁盘 LRU 缓存,默认 `2048MB`
46
+ - `x / y` 支持百分比定位
47
+ - `width / height` 支持按礼物原始尺寸自动补全
48
+ - 四类礼物共用位移、缩放、透明度、旋转、层级和销毁生命周期
49
+
50
+ ## 安装
51
+
52
+ ```bash
53
+ npm install
54
+ ```
55
+
56
+ 安装发布包:
57
+
58
+ ```bash
59
+ npm i @taole/giftstage
60
+ ```
61
+
62
+ ## 开发
63
+
64
+ ```bash
65
+ npm run dev
66
+ ```
67
+
68
+ ## 构建
69
+
70
+ ```bash
71
+ npm run build
72
+ ```
73
+
74
+ Demo 构建:
75
+
76
+ ```bash
77
+ npm run build:demo
78
+ ```
79
+
80
+ ## 性能基线
81
+
82
+ ```bash
83
+ # 在当前机器记录 WebGL2 / WebGPU 基线
84
+ npm run perf:record
85
+
86
+ # 与同浏览器主版本、硬件并发数、DPR 和视口的基线比较
87
+ npm run test:perf
88
+ ```
89
+
90
+ 基线覆盖冷/热启动、100 个静态图片、SVGA Direct 1/2/10/50 个同资源实例、
91
+ 复杂 Clip/Shape Hybrid Auto/CPU 对照、20 个共享 VAP 和 20 个共享 AlphaVideo。
92
+ 除首帧、P50/P95/P99、长任务、堆变化、draw/Stencil 外,还记录 SVGA CPU Sprite
93
+ 展开数、GPU 帧表上传、每帧动态上传字节、路径晋升与回退。可用
94
+ `npm run test:perf -- --project=chromium-webgl2 --scenarios=<逗号分隔场景>` 做聚焦复测。
95
+ 基线保存在 `tests/performance/baselines/`;Windows 会自动探测系统 Chrome,也可通过
96
+ `PLAYWRIGHT_CHROME_EXECUTABLE_PATH` 指定浏览器。
97
+
98
+ ## 快速开始
99
+
100
+ ```ts
101
+ import { GiftStage } from '@taole/giftstage';
102
+
103
+ const container = document.getElementById('app')!;
104
+
105
+ const stage = new GiftStage({
106
+ container,
107
+ preferWebGPU: true,
108
+ });
109
+
110
+ await stage.ready;
111
+
112
+ await stage.addGift({
113
+ type: 'svga',
114
+ source: 'https://example.com/demo.svga',
115
+ x: 100,
116
+ y: 100,
117
+ loop: 0,
118
+ });
119
+ ```
120
+
121
+ ## 插件运行时(0.2)
122
+
123
+ 聊天室只需要维护一个全屏 `GiftStage`。业务编排包通过 `stage.mount(plugin)` 接入,插件创建的礼物、轨道和粒子都归属于独立 `PlaybackScope`;销毁插件或作用域不会调用 `removeAll()`,也不会影响舞台上的其他礼物。
124
+
125
+ ```ts
126
+ import createMultiGiftPlugin from '@taole/giftstage-plugin-multi-gift';
127
+
128
+ const mounted = await stage.mount(createMultiGiftPlugin({ playConfig }));
129
+ await mounted.api.preload();
130
+ const playback = await mounted.api.play({ target: null });
131
+
132
+ playback.pause();
133
+ playback.resume();
134
+ playback.seek(1200);
135
+ await playback.completion;
136
+
137
+ mounted.destroy();
138
+ ```
139
+
140
+ 插件宿主能力包括:
141
+
142
+ - `preload()`:复用 GiftStage SVGA/Image 缓存并返回引用计数资源租约。
143
+ - `createPlaybackScope()`:使用 GiftStage 唯一 RAF、逻辑时钟和作用域清理;SVGA、WebCodecs、HTMLVideo 回退与定时图片均按 Scope 绝对时间定位,不叠加 RenderManager 的全局 `dt`。SVGA 的可见帧、`onFrame` 与嵌入音频由同一个绝对时间入口同步,跨帧、seek、循环和暂停恢复会重建正确音频偏移,且 `start()` 前不会触发首帧音频/回调。HTMLVideo 在 Scope 内保持暂停并使用独立实例,显式暂停和页面隐藏期间逻辑时间冻结。
144
+ - `createMotionTrack()`:接收 8-float TypedArray/SoA 父级轨道。WebGPU 可把它与核心 SVGA 帧表在同一顶点着色器中合成;WebGL2 的 SVGA 内部帧仍可走 GPU Direct/Hybrid,但父级 MotionTrack 继续使用确定性 CPU reference sampler。图片与 WebGL1 同样使用 CPU reference。
145
+ - `createParticleBatch()`:WebGPU/WebGL2 使用持久实例缓冲和 GPU 弹道求值;WebGL1 明确不可用,不提供 Canvas2D 粒子后端。粒子统一在媒体礼物之后合成,`zIndex` 只控制粒子批次之间的顺序。
146
+ - `stage.capabilities`:插件可在播放前判断实际后端、GPU 轨道媒体范围和粒子能力。
147
+
148
+ ## 核心 API
149
+
150
+ ### `new GiftStage(options)`
151
+
152
+ 创建礼物舞台。
153
+
154
+ 常用配置:
155
+
156
+ - `container: HTMLElement`
157
+ 挂载容器。
158
+
159
+ - `resolution?: number`
160
+ 指定 canvas backing store 分辨率倍率。默认跟随 `devicePixelRatio`。
161
+
162
+ - `antialias?: boolean`
163
+ 是否开启抗锯齿,WebGL WebGPU 均默认开启;WebGPU 使用 4× MSAA。大量透明视频同时播放时可显式设为 `false`。
164
+
165
+ - `preferWebGPU?: boolean`
166
+ 是否优先使用 `WebGPU`。
167
+
168
+ - `forceWebGL1?: boolean`
169
+ 强制走 `WebGL1`,用于兼容性验证。
170
+
171
+ - `preferWebCodecs?: boolean`
172
+ 实验性特性。默认关闭;显式传 `true` 时才会尝试用 `WebCodecs` 播放 `VAP / AlphaVideo`。
173
+
174
+ - `shareIdenticalVideoPlayback?: boolean`
175
+ 是否复用近同时起播的同源视频播放实例。默认开启。
176
+
177
+ - `sharedVideoPlaybackWindowMs?: number`
178
+ 同源视频共享播放窗口,默认 `100ms`。
179
+
180
+ - `webCodecsVideoAtlas?: { width: number; height: number }`
181
+ 实验性 `WebCodecs` 视频共享 atlas 配置。
182
+
183
+ - `webCodecsDecodeInWorker?: boolean`
184
+ 实验性 `WebCodecs` 选项,控制是否在 Worker 中执行解码。
185
+
186
+ - `webCodecsMaxDecodedFrames?: number`
187
+ 实验性 `WebCodecs` 选项,控制保留的最大解码帧数。
188
+
189
+ - `webCodecsDecodeAheadFrames?: number`
190
+ 实验性 `WebCodecs` 选项,控制前向解码帧数。
191
+
192
+ - `svgaParseInWorker?: boolean`
193
+ 是否在 Worker 中解析 SVGA。
194
+
195
+ - `maxConcurrentParse?: number`
196
+ 资源下载 / 解析 / 图像解码的并发数。
197
+ 不传时自动跟随 `navigator.hardwareConcurrency`。
198
+
199
+ - `svgaAtlasFrameBudgetMs?: number`
200
+ SVGA atlas 在主线程组合或逐页上传时的单帧时间片上限,默认 `12ms`。
201
+ 实际时间片会按当前刷新周期的一半动态调整,避免高刷新率设备上的加载任务挤占渲染帧。
202
+
203
+ - `svgaLoadFrameBudgetMs?: number`
204
+ SVGA 主线程加载任务的共享时间片上限,默认 `12ms`。图片 fallback、atlas、clip、音频、slot 纹理和二进制缓存共用同一帧预算。
205
+
206
+ - `svgaWorkerFrameBudgetMs?: number`
207
+ SVGA Worker 连续执行 JS 循环的时间片上限,默认 `4ms`,范围 `1-8ms`。单次 WASM protobuf 调用和单次原生 API 调用仍不可抢占。
208
+
209
+ - `svgaWorkerTaskConcurrency?: number`
210
+ parser、图片解码、atlas 和 clip 等 CPU 密集 Worker 的全局并发上限,默认 `1`。Atlas 首帧页面可提前返回,但后台 PNG 编码完成前仍持有该名额,避免与其他重载 Worker 叠加。
211
+
182
212
  - `svgaWorkerImageDecodeConcurrency?: number`
183
213
  图片解码 Worker 内 `createImageBitmap` 并发上限,默认 `2`。运行时可按批次耗时收缩并发,负载恢复后回升,但不会超过该上限。
184
214
 
185
- - `svgaParseProfile?: boolean`
186
- 是否输出 `[GiftStage:SVGA:Profile]` 结构化解析日志,默认 `false`。日志包含 `DecompressionStream` 格式尝试、输入/输出 chunk、解压耗时以及 protobuf/payload 构建耗时;WASM payload 路径还会拆分 `prostDecodeMs`、`metadataAndImagesMs`、`frameTableMs`、`finalizeMs` WASM 边界复制耗时。缓存命中不输出。
187
-
188
- SVGA 礼物在加载阶段被 `destroy()` / `removeGift()` 时,会取消排队任务并终止正在执行的 parser、图片解码、atlas 或 clip Worker。相同 URL 的共享加载按消费者计数,单个礼物取消不会中断其他仍在等待的礼物。
189
-
215
+ - `svgaFrameEvaluation?: 'auto' | 'cpu'`
216
+ SVGA 帧动画求值策略,默认 `auto`。`auto` 按后端能力、表大小和资源计划选择 `gpu-direct`、`gpu-hybrid` `cpu-reference`;`cpu` 强制使用 CPU reference。普通单礼物和同资源多实例都经过同一策略。
217
+
218
+ - `svgaGpuFrameTableBudgetBytes?: number`
219
+ 所有活动 SVGA GPU 帧表、静态 Sprite metadata 和 Hybrid Command Table 共享的全局预算,默认 `32 MiB`。预算不足时仅当前资源回退到 `cpu-reference`,不会影响已存在的其他礼物。
220
+
221
+ - `svgaParseProfile?: boolean`
222
+ 是否输出 `[GiftStage:SVGA:Profile]` 结构化解析日志,默认 `false`。日志包含 `DecompressionStream` 格式尝试、输入/输出 chunk、解压耗时以及 protobuf/payload 构建耗时;WASM payload 路径还会拆分 `prostDecodeMs`、`metadataAndImagesMs`、`frameTableMs`、`finalizeMs` 和 WASM 边界复制耗时。缓存命中不输出。
223
+
224
+ SVGA 礼物在加载阶段被 `destroy()` / `removeGift()` 时,会取消排队任务并终止正在执行的 parser、图片解码、atlas 或 clip Worker。相同 URL 的共享加载按消费者计数,单个礼物取消不会中断其他仍在等待的礼物。
225
+
190
226
  - `onError?: (error: Error) => void`
191
227
  统一错误回调。
192
228
 
193
- ### `await stage.ready`
194
-
195
- 等待底层后端和运行环境初始化完成。建议在第一次 `addGift()` 前等待。
229
+ - `onSVGARenderPathChange?: (info) => void`
230
+ 当 SVGA 礼物完成资格分析、帧表上传、原子晋升或发生回退时通知路径变化。除资源与路径字段外,`info` 还包含 `pathState`、`frameTableBytes`、`commandTableBytes`、`drawBatchMode` 和 `drawCompatibleInstanceCount`。
196
231
 
232
+ - `onRuntimeDiagnostic?: (event) => void`
233
+ 接收 frame abort、replay failure、无时钟 retry、运行期 fallback、Clip 协议/网格异常、Stencil 事务失败、Atlas 命令无效、Backend 操作拒绝/隔离、WebGPU uncaptured error、backend loss 和 observer error 等结构化事件。事件只在本地回调,不会由 GiftStage 上传;回调异常会被隔离,不能中断渲染降级事务。
234
+
235
+ ### `await stage.ready`
236
+
237
+ 等待底层后端和运行环境初始化完成。建议在第一次 `addGift()` 前等待。
238
+
197
239
  ### `stage.addGift(options)`
198
-
199
- 插入一个礼物,返回:
200
-
201
- ```ts
240
+
241
+ 插入一个礼物,返回:
242
+
243
+ ```ts
202
244
  Promise<GiftHandle>;
203
245
  ```
204
246
 
205
- ### `GiftHandle` API
206
-
207
- `addGift()` 返回的句柄可用于控制单个礼物:
247
+ ### `stage.getDiagnostics()`
208
248
 
209
- - `gift.id`
210
- - `gift.type`
211
- - `gift.pause()`
212
- - `gift.resume()`
213
- - 暂停期间礼物播放时钟、图片 `duration` 和后置动画都会冻结
249
+ 返回当前 Stage 生命周期内的累计运行诊断快照,包括 frame abort/replay/retry、FastPath fallback、native submitted/rejected draw、WebGPU uncaptured error、backend loss、observer error 以及最后一个结构化事件。该快照不会被性能测试采样窗口重置,返回对象可安全交给业务遥测系统;GiftStage 自身不上传数据。
250
+
251
+ ### `GiftHandle` API
252
+
253
+ `addGift()` 返回的句柄可用于控制单个礼物:
254
+
255
+ - `gift.id`
256
+ - `gift.type`
257
+ - `gift.pause()`
258
+ - `gift.resume()`
259
+ - 暂停期间礼物播放时钟、图片 `duration` 和后置动画都会冻结
214
260
  - `gift.destroy()`
215
261
  - 立即移除当前礼物
262
+ - `gift.getRenderInfo()`
263
+ - 返回 SVGA 的实时路径诊断;非 SVGA 或尚未形成资源计划时返回 `null`。`pathState` 为 `preparing | settled | fallback`,`drawBatchMode` 为 `multi-slot | per-slot | hybrid-scheduled`,可直接区分资源共享与实际 Draw 合批状态。
216
264
  - `gift.animate(stepOrSteps?)`
217
- - 创建链式动画并返回 `GiftAnimationChain`
218
-
219
- `GiftAnimationChain` 支持:
220
-
221
- - `.to(step)` / `.then(step)`
222
- - 追加动画步骤(两者等价)
223
- - `.delay(milliseconds)`
224
- - 在链中插入等待,不改变位置、缩放和透明度
225
- - `.onComplete((gift) => void)`
226
- - 整条链执行完成后的回调
227
- - `.start(mode?)`
228
- - `mode: 'immediate' | 'afterGiftComplete'`
229
- - `immediate`:立即开始(默认)
230
- - `afterGiftComplete`:等待礼物主播放结束后再执行链
231
- - `.cancel()`
232
- - 取消当前礼物正在执行的链式动画
233
-
234
- ### `stage.removeGift(id)`
235
-
236
- 按 ID 移除礼物。
237
-
238
- ### `stage.removeAll()`
239
-
240
- 移除全部礼物。
241
-
242
- ### `stage.pause()`
243
-
244
- 暂停舞台。
245
-
246
- ### `stage.resume()`
247
-
248
- 恢复舞台。
249
-
265
+ - 创建链式动画并返回 `GiftAnimationChain`
266
+
267
+ `GiftAnimationChain` 支持:
268
+
269
+ - `.to(step)` / `.then(step)`
270
+ - 追加动画步骤(两者等价)
271
+ - `.delay(milliseconds)`
272
+ - 在链中插入等待,不改变位置、缩放和透明度
273
+ - `.onComplete((gift) => void)`
274
+ - 整条链执行完成后的回调
275
+ - `.start(mode?)`
276
+ - `mode: 'immediate' | 'afterGiftComplete'`
277
+ - `immediate`:立即开始(默认)
278
+ - `afterGiftComplete`:等待礼物主播放结束后再执行链
279
+ - `.cancel()`
280
+ - 取消当前礼物正在执行的链式动画
281
+
282
+ ### `stage.removeGift(id)`
283
+
284
+ 按 ID 移除礼物。
285
+
286
+ ### `stage.removeAll()`
287
+
288
+ 移除全部礼物。
289
+
290
+ ### `stage.pause()`
291
+
292
+ 暂停舞台。
293
+
294
+ ### `stage.resume()`
295
+
296
+ 恢复舞台。
297
+
250
298
  ### `stage.destroy()`
251
299
 
252
300
  销毁舞台并释放资源。
253
301
 
254
- ## `addGift()` 参数
255
-
256
- ### 通用参数
257
-
258
- - `type: 'svga' | 'vap' | 'alphaVideo' | 'image'`
259
- - `source: string | ArrayBuffer`
260
- - `x: number | \`${number}%\``
261
- - `y: number | \`${number}%\``
262
- - `zIndex?: number`
263
- - `width?: number`
264
- - `height?: number`
265
- - `useOriginalSize?: boolean`
266
- - `objectFit?: 'contain' | 'cover'`
267
- - `loop?: number`
268
- - `0` 表示无限循环
269
- - VAP / AlphaVideo 的 HTMLVideo 回退路径会为有限循环设置“媒体总时长 + 容错窗口”的超时保护;即使浏览器未触发 `ended`,也会进入正常完成和清理流程
270
- - `opacity?: number`
271
- - 全局透明度,范围 `0 ~ 1`,默认 `1`
272
- - `rotation?: number`
273
- - 初始旋转弧度;屏幕坐标中正值为顺时针
274
- - `transformOrigin?: { x: number | \`${number}%\`; y: number | \`${number}%\` }`
275
- - 相对未缩放礼物框左上角,默认 `50% / 50%`
276
- - 数值使用 CSS 像素;允许负值或超过礼物宽高,用于绕框外点公转
277
- - `videoRGBAlphaMode?: 'straight' | 'premultiplied'`
278
- - 仅用于 `vap` / `alphaVideo` 分离 Alpha 视频源,默认 `premultiplied`
279
- - `straight`:RGB 区域尚未乘独立 Alpha,播放器负责预乘
280
- - `premultiplied`:RGB 区域在制作阶段已经乘过独立 Alpha,播放器不会再次相乘
281
- - 该参数只描述输入视频;最终画布仍统一输出预乘 Alpha
282
- - `clearsAfterStop?: boolean`
283
- - 播放结束后,是否自动移除礼物
284
- - 默认 `true`,传 `false` 时会停留最后一帧,方便后续继续调用 `gift.animate(...).start()`
285
- - `mute?: boolean`
286
- - `VAP / AlphaVideo` 传 `false` 时使用 `HTMLVideoElement` 播放声音;该礼物不会进入仅解码视频帧的 WebCodecs 路径
287
- - 如果浏览器阻止有声自动播放,会自动回退静音播放,保证画面正常完成
288
- - `onFrame?: (frame, gift) => void`
289
- - 仅用于 SVGA;画面帧推进时触发
290
- - 性能不足时可能跨帧,业务应使用 `frame >= target` 的阈值判断
291
- - `onComplete?: (gift) => void`
292
-
293
- 图片专用参数:
294
-
295
- - `duration?: number`
296
- - 单位毫秒;正数到期后触发 `onComplete`,并遵循 `clearsAfterStop`
297
- - 不传或传非正数时持续显示到 `destroy()`;加载完成后 `afterGiftComplete` 可立即继续,但不会自动触发 `onComplete`
298
- - `image` 忽略 `loop` 和 `mute`
299
-
300
- 层级规则:
301
-
302
- - `zIndex` `SVGA / VAP / AlphaVideo / Image` 四种礼物间通用
303
- - 数值越大,渲染越靠上
304
- - 同层级下,后 `addGift()` 的礼物会覆盖先添加的礼物
305
-
306
- ### 宽高默认行为
307
-
308
- `width / height` 现在是可选参数,规则如下:
309
-
310
- - 两个都传:按传入值显示
311
- - 只传 `width`:`height` 按礼物原始宽高比自动补全
312
- - 只传 `height`:`width` 按礼物原始宽高比自动补全
313
- - 两个都不传:默认按当前画布尺寸做等比 `contain`
314
- - 如果 `objectFit: 'cover'` 且两个都不传:按整个画布作为显示区域做等比 `cover`
315
- - 也就是会在画布内尽可能放大或缩小,并保持礼物原始宽高比
316
- - 如果 `useOriginalSize: true`,且礼物原始尺寸本身小于画布,则优先使用礼物原始尺寸
317
- - 如果 `useOriginalSize: true`,但礼物原始尺寸超出画布,则仍会按画布尺寸等比缩小
318
-
319
- 例如:
320
-
321
- ```ts
322
- await stage.addGift({
323
- type: 'svga',
324
- source: 'https://example.com/demo.svga',
325
- x: '50%',
326
- y: '50%',
327
- loop: 0,
328
- });
329
- ```
330
-
331
- 这时会按舞台尺寸做等比适配,并保持礼物原始宽高比。
332
-
333
- 如果希望礼物铺满整个画布,并按中心裁剪超出的部分,可以使用 `cover`:
334
-
335
- ```ts
336
- await stage.addGift({
337
- type: 'svga',
338
- source: 'https://example.com/demo.svga',
339
- x: '50%',
340
- y: '50%',
341
- objectFit: 'cover',
342
- loop: 0,
343
- });
344
- ```
345
-
346
- 当 `x: '50%'`、`y: '50%'` 且未传 `width / height` 时,显示区域会居中放在整个画布上;`cover` 会保持礼物原始宽高比铺满该区域,并从中心裁剪溢出的部分。
347
-
348
- 如果你希望“小礼物保持原始尺寸,大礼物再缩小”,可以这样:
349
-
350
- ```ts
351
- await stage.addGift({
352
- type: 'svga',
353
- source: 'https://example.com/demo.svga',
354
- x: '50%',
355
- y: '50%',
356
- useOriginalSize: true,
357
- loop: 0,
358
- });
359
- ```
360
-
361
- ### 百分比坐标
362
-
363
- `x / y` 支持百分比,例如:
364
-
365
- ```ts
366
- await stage.addGift({
367
- type: 'svga',
368
- source: 'https://example.com/demo.svga',
369
- x: '50%',
370
- y: '50%',
371
- width: 300,
372
- height: 300,
373
- loop: 0,
374
- });
375
- ```
376
-
377
- 语义是:
378
-
379
- - `x: '50%'` 按容器宽度的 `50%` 定位,并减去自身一半宽度
380
- - `y: '50%'` 按容器高度的 `50%` 定位,并减去自身一半高度
381
-
382
- 也就是接近:
383
-
384
- ```css
385
- left: 50%;
386
- top: 50%;
387
- transform: translate(-50%, -50%);
388
- ```
389
-
390
- 如果是数值,则继续按原来的像素坐标语义处理。
391
-
392
- ## 四类礼物示例
393
-
394
- ### 播放 SVGA
395
-
396
- ```ts
397
- await stage.addGift({
398
- type: 'svga',
399
- source: 'https://example.com/demo.svga',
400
- x: 20,
401
- y: 20,
402
- width: 300,
403
- height: 300,
404
- loop: 1,
405
- });
406
- ```
407
-
408
- ### 播放 VAP
409
-
410
- ```ts
411
- await stage.addGift({
412
- type: 'vap',
413
- source: 'https://example.com/demo.mp4',
414
- config: 'https://example.com/demo.json',
415
- x: 20,
416
- y: 20,
417
- width: 400,
418
- height: 220,
419
- // 默认 premultiplied;RGB 尚未乘独立 Alpha 的视频源需显式传 straight
420
- videoRGBAlphaMode: 'premultiplied',
421
- loop: 0,
422
- });
423
- ```
424
-
425
- ### 播放透明视频
426
-
427
- ```ts
428
- await stage.addGift({
429
- type: 'alphaVideo',
430
- source: 'https://example.com/demo.mp4',
431
- x: 20,
432
- y: 20,
433
- width: 400,
434
- height: 220,
435
- // 默认 premultiplied;未预乘的视频源改为 straight
436
- videoRGBAlphaMode: 'premultiplied',
437
- loop: 0,
438
- });
439
- ```
440
-
441
- ### 播放图片
442
-
443
- `source` 首版支持 URL 和 `ArrayBuffer`。URL 图片会按地址共享 GPU 纹理并引用计数;图片按静态帧处理,不保证 GIF / WebP 动画播放。跨域 URL 必须允许匿名 CORS 读取。
444
-
445
- ```ts
446
- const avatar = await stage.addGift({
447
- type: 'image',
448
- source: 'https://example.com/avatar.png',
449
- x: '50%',
450
- y: '50%',
451
- width: 100,
452
- height: 100,
453
- objectFit: 'cover',
454
- duration: 5000,
455
- });
456
- ```
457
-
458
- ## 动画控制
459
-
460
- ### 链式动画
461
-
462
- `addGift()` 返回的 `GiftHandle` 支持 `animate()`。一次 `to()` 是组合动画,同一步里可以同时移动、缩放、改变透明度和旋转;多个 `to()` / `then()` 会按顺序播放。`rotationTo` 直接按数值线性插值,不做最短角度归一化,因此可以明确表达多圈旋转:
463
-
464
- ```ts
465
- const gift = await stage.addGift({
466
- type: 'svga',
467
- source: 'https://example.com/demo.svga',
468
- x: '50%',
469
- y: '50%',
470
- width: 300,
471
- height: 300,
472
- loop: 0,
473
- });
474
-
475
- await gift
476
- .animate()
477
- .to({
478
- flyTo: { x: 200, y: 240 },
479
- scaleTo: 0.8,
480
- opacity: 0.6,
481
- rotationTo: Math.PI * 4,
482
- duration: 500,
483
- })
484
- .then({
485
- flyTo: { x: 600, y: 320 },
486
- scaleTo: 1.1,
487
- opacity: 1,
488
- duration: 700,
489
- })
490
- .delay(300)
491
- .then({
492
- opacity: 0,
493
- duration: 400,
494
- })
495
- .onComplete((g) => {
496
- console.log('chain complete', g.id);
497
- })
498
- .start();
499
- ```
500
-
501
- ### 绕外部中心旋转
502
-
503
- 下面的礼物初始位于舞台中心右侧 200px,旋转中心通过框外坐标指回舞台中心:
504
-
505
- ```ts
506
- const orbitGift = await stage.addGift({
507
- type: 'image',
508
- source: 'https://example.com/gift.png',
509
- x: container.clientWidth / 2 + 150,
510
- y: '50%',
511
- width: 100,
512
- height: 100,
513
- transformOrigin: { x: -150, y: '50%' },
514
- });
515
-
516
- await orbitGift
517
- .animate({ rotationTo: Math.PI * 2, duration: 1200 })
518
- .start();
519
- ```
520
-
521
- 也可以传入单步或数组:
522
-
523
- ```ts
524
- gift.animate({ flyTo: { x: 300, y: 300 }, scaleTo: 0.5, opacity: 0.3 }).start();
525
-
526
- gift
527
- .animate([
528
- { flyTo: { x: 300, y: 300 }, duration: 400 },
529
- { scaleTo: 1, opacity: 1, duration: 300 },
530
- ])
531
- .start();
532
- ```
533
-
534
- ## Slot 替换
535
-
536
- ### SVGA
537
-
538
- 使用 `svgaSlots`:
539
-
540
- ```ts
541
- await stage.addGift({
542
- type: 'svga',
543
- source: 'https://example.com/demo.svga',
544
- x: 0,
545
- y: 0,
546
- width: 400,
547
- height: 400,
548
- svgaSlots: {
549
- avatar: { image: avatarImage },
550
- title: {
551
- text: 'Hello',
552
- color: '#ff0000',
553
- fontSize: 28,
554
- mode: 'dynamic',
555
- },
556
- },
557
- });
558
- ```
559
-
560
- 支持:
561
-
562
- - `TexImageSource`
563
- - 文本配置
564
- - 图片配置
565
-
566
- SVGA 文本配置会按目标 frame 自动适配字号,避免文字超出槽位:
567
-
568
- - `mode: 'dynamic'`:默认行为。生成文字自身尺寸的纹理,渲染时按自身逻辑尺寸居中到 frame 内,不会被拉伸。
569
- - `mode: 'replace'`:生成和 frame 一样大的纹理,按替换图逻辑铺满 frame。
570
- - `fontSize?: number`:期望字号;如果文字放不下,会自动缩小。
571
- - `minFontSize?: number` / `maxFontSize?: number`:限制自适应字号范围。
572
- - `padding?: number`:文字纹理内边距,默认 `2`。
573
- - `scale?: number`:文字纹理栅格倍率,SVGA 默认 `3`,用于保持清晰度。
574
- - `fontStyle?: string | { font?: string; color?: string }`:字体样式,SVGA / VAP 都支持。
575
-
576
- `fontStyle` 可以直接传 canvas font 字符串:
577
-
578
- ```ts
579
- svgaSlots: {
580
- title: {
581
- text: 'Hello',
582
- fontStyle: 'bold 40px Arial',
583
- },
584
- }
585
- ```
586
-
587
- 也可以传对象:
588
-
589
- ```ts
590
- vapSlots: {
591
- welcome01: {
592
- text: '欢迎进入房间',
593
- fontStyle: {
594
- font: 'bold 40px Arial',
595
- color: '#ffffff',
596
- },
597
- },
598
- }
599
- ```
600
-
601
- 对象形式目前生效字段是 `font` 和 `color`。如果同时传了外层 `color` 和 `fontStyle.color`,以 `fontStyle.color` 为准。VAP 文本如果没有传 `fontStyle`,会回退使用 VAP 配置里的 `src.fontStyle`。
602
-
603
- ### VAP / VAPX
604
-
605
- 使用 `vapSlots`:
606
-
607
- ```ts
608
- await stage.addGift({
609
- type: 'vap',
610
- source: 'https://example.com/demo.mp4',
611
- config: 'https://example.com/demo.json',
612
- x: 0,
613
- y: 0,
614
- width: 400,
615
- height: 400,
616
- vapSlots: {
617
- welcome01: '欢迎进入房间',
618
- avatar_left: 'https://example.com/avatar.png',
619
- },
620
- });
621
- ```
622
-
623
- 支持:
624
-
625
- - 文本字符串
626
- - 图片 URL
627
- - `TexImageSource`
628
- - 结构化文本 / 图片对象
629
-
630
- ## 后端策略
631
-
632
- 默认回退顺序:
633
-
634
- 1. `WebGPU`
635
- 2. `WebGL2`
636
- 3. `WebGL1`
637
-
638
- 说明:
639
-
640
- - `WebGPU` 仅在浏览器支持相关必要能力时启用
641
- - `WebGL1` 主要用于兼容兜底,不以性能最优为目标
642
- - demo 中可手动强制切换到 `WebGL1`
643
-
644
- ## 缓存策略
645
-
646
- ### 内存缓存
647
-
648
- - 默认 `128MB`
649
- - LRU 淘汰
650
-
651
- 缓存内容包括:
652
-
653
- - 原始资源字节
654
- - 文本 / JSON
655
- - `SVGA` worker payload
656
- - `SVGA` atlas 资产
657
-
658
- ### `IndexedDB` 磁盘缓存
659
-
660
- - 默认 `2048MB`
661
- - LRU 淘汰
662
- - 读取命中会刷新最近访问时间
663
- - 超出预算时按最久未使用记录淘汰
664
-
665
- 异常情况处理:
666
-
667
- - `IndexedDB` 不可用、事务失败、写入失败、容量不足时,会自动降级
668
- - 损坏的 `SVGA` 磁盘缓存会自动删除并重新生成
669
-
670
- ## 项目结构
671
-
672
- 主要目录:
673
-
674
- - `src/`
675
- 核心源码
676
- - `docs/`
677
- 设计与汇总文档
678
- - `publish/`
679
- 发布脚本
680
- - `static/`
681
- demo 依赖资源
682
- - `tests/`
683
- 测试
684
-
685
- ## 对外导出
686
-
687
- 入口文件:
688
-
689
- - [src/index.ts](/D:/MyDocuments/UnityProjects/SVGAPlayer-Unity/gift-stage/src/index.ts)
690
-
302
+ ### SVGA 帧动画路径
303
+
304
+ SVGA 资源的 9-float 帧行会在准备阶段打包为 3 个 `vec4`。WebGPU 使用持久 storage buffer,WebGL2 使用 `RGBA32F + NEAREST + texelFetch` 数据纹理。CPU 始终权威维护帧号、循环、暂停、Seek、音频与回调;GPU 根据该帧号读取子 Sprite 矩阵、透明度和可见性。
305
+
306
+ Direct 对连续且兼容的同资源 slot 使用 4 个 `vec4`/slot 的父级状态并进行 slot-major 实例化合批;多 Atlas、不同 MotionTrack binding 或无法证明重排安全的区间继续使用 per-slot Direct,不回退 CPU。Hybrid 在资源准备期生成 `frameOffsets + operations + spriteIndices + meshReferences` 不可变命令表,并把连续同 Atlas 的纹理操作进一步预编译为 execution run;运行时只读取当前帧 run slice。Texture、Shape 和 Clip Mask Shader 读取同一核心帧表,每帧动态上传仍只有 slot 父级状态。结构相同的不同权威帧可通过 `frameScheduleIds` 共用一次合批;矩形纹理 Clip 使用静态本地裁剪 metadata,不增加 Stencil pass。MoveTo-only、线段、重复点或零面积 Clip 按 SVG/Canvas 的空裁剪语义直接隐藏对应 Sprite,不创建 Mesh/Stencil,也不会触发资源回退。
307
+
308
+ - `gpu-direct`:普通 atlas sprite 的帧变换、透明度和布局由 GPU 求值。
309
+ - `gpu-hybrid`:Shape/Clip 的精确 paint order 与 Draw/Stencil 提交由 CPU 按不可变命令表调度,Texture、Shape、Clip Mask 的动画属性都由 GPU 帧表求值。
310
+ - `cpu-reference`:WebGL1、动态 `svgaSlots`、表校验/上传失败、预算不足或策略为 `cpu` 时使用确定性的 CPU 路径。
311
+
312
+ 插件可通过 `stage.capabilities.svgaFrameTimeline` 判断 `transport`(`storage-buffer`、`float-texture` `unavailable`)、正式 `direct`/`hybrid` 能力、`supportedPaths` 和 `maxTableBytes`,不需要启用 experimental policy。同一解析资源的多个实例共享帧表、静态 metadata 和 Hybrid Command Table;`compatibleInstanceCount` 表示资源共享数,实际本帧合批数量由 `drawCompatibleInstanceCount` 报告。
313
+
314
+ `addGift()` 不等待 GPU 帧表:非预加载礼物先显示 CPU reference 首帧,再在完整帧边界原子晋升。`preload('svga')` 会等待同一资源级帧表与调度准备完成。FastPath 提交若被后端拒绝,会丢弃未提交帧、同步降级该资源,并在同一 RAF 以原顺序完整重放一次;重放再次失败时 WebGL2 会清成透明帧,并在下一 RAF 不推进 CPU 权威时间轴地重试。不支持该帧事务的自定义 Backend 会直接使用 CPU Reference。WebGL1、动态 `svgaSlots`、能力/尺寸/预算校验失败和上传失败均按资源缓存回退,不逐帧重试;Stage 与单礼物都可用 `svgaFrameEvaluation: 'cpu'` 强制关闭。
315
+
316
+ 性能诊断中的 `submittedDrawCalls` 仅表示命令已经到达 WebGL `drawElements*` 或 WebGPU `drawIndexed` 调用/编码;它不表示 GPU 已经执行完成。`rejectedDrawCalls` 表示在原生提交前被后端校验或容量检查拒绝。GPU error scope、pipeline validation 与 framebuffer 像素门禁用于补充验证执行结果。
317
+
318
+ ## `addGift()` 参数
319
+
320
+ ### 通用参数
321
+
322
+ - `type: 'svga' | 'vap' | 'alphaVideo' | 'image'`
323
+ - `source: string | ArrayBuffer`
324
+ - `x: number | \`${number}%\``
325
+ - `y: number | \`${number}%\``
326
+ - `zIndex?: number`
327
+ - `width?: number`
328
+ - `height?: number`
329
+ - `useOriginalSize?: boolean`
330
+ - `objectFit?: 'contain' | 'cover'`
331
+ - `loop?: number`
332
+ - `0` 表示无限循环
333
+ - VAP / AlphaVideo 的 HTMLVideo 回退路径会为有限循环设置“媒体总时长 + 容错窗口”的超时保护;即使浏览器未触发 `ended`,也会进入正常完成和清理流程
334
+ - `opacity?: number`
335
+ - 全局透明度,范围 `0 ~ 1`,默认 `1`
336
+ - `rotation?: number`
337
+ - 初始旋转弧度;屏幕坐标中正值为顺时针
338
+ - `transformOrigin?: { x: number | \`${number}%\`; y: number | \`${number}%\` }`
339
+ - 相对未缩放礼物框左上角,默认 `50% / 50%`
340
+ - 数值使用 CSS 像素;允许负值或超过礼物宽高,用于绕框外点公转
341
+ - `videoRGBAlphaMode?: 'straight' | 'premultiplied'`
342
+ - 仅用于 `vap` / `alphaVideo` 分离 Alpha 视频源,默认 `premultiplied`
343
+ - `straight`:RGB 区域尚未乘独立 Alpha,播放器负责预乘
344
+ - `premultiplied`:RGB 区域在制作阶段已经乘过独立 Alpha,播放器不会再次相乘
345
+ - 该参数只描述输入视频;最终画布仍统一输出预乘 Alpha
346
+ - `clearsAfterStop?: boolean`
347
+ - 播放结束后,是否自动移除礼物
348
+ - 默认 `true`,传 `false` 时会停留最后一帧,方便后续继续调用 `gift.animate(...).start()`
349
+ - `mute?: boolean`
350
+ - `VAP / AlphaVideo` `false` 时使用 `HTMLVideoElement` 播放声音;该礼物不会进入仅解码视频帧的 WebCodecs 路径
351
+ - 如果浏览器阻止有声自动播放,会自动回退静音播放,保证画面正常完成
352
+ - `onFrame?: (frame, gift) => void`
353
+ - 仅用于 SVGA;画面帧推进时触发
354
+ - 性能不足时可能跨帧,业务应使用 `frame >= target` 的阈值判断
355
+ - `onComplete?: (gift) => void`
356
+
357
+ 图片专用参数:
358
+
359
+ - `duration?: number`
360
+ - 单位毫秒;正数到期后触发 `onComplete`,并遵循 `clearsAfterStop`
361
+ - 不传或传非正数时持续显示到 `destroy()`;加载完成后 `afterGiftComplete` 可立即继续,但不会自动触发 `onComplete`
362
+ - `image` 忽略 `loop` `mute`
363
+
364
+ 层级规则:
365
+
366
+ - `zIndex` 在 `SVGA / VAP / AlphaVideo / Image` 四种礼物间通用
367
+ - 数值越大,渲染越靠上
368
+ - 同层级下,后 `addGift()` 的礼物会覆盖先添加的礼物
369
+
370
+ ### 宽高默认行为
371
+
372
+ `width / height` 现在是可选参数,规则如下:
373
+
374
+ - 两个都传:按传入值显示
375
+ - 只传 `width`:`height` 按礼物原始宽高比自动补全
376
+ - 只传 `height`:`width` 按礼物原始宽高比自动补全
377
+ - 两个都不传:默认按当前画布尺寸做等比 `contain`
378
+ - 如果 `objectFit: 'cover'` 且两个都不传:按整个画布作为显示区域做等比 `cover`
379
+ - 也就是会在画布内尽可能放大或缩小,并保持礼物原始宽高比
380
+ - 如果 `useOriginalSize: true`,且礼物原始尺寸本身小于画布,则优先使用礼物原始尺寸
381
+ - 如果 `useOriginalSize: true`,但礼物原始尺寸超出画布,则仍会按画布尺寸等比缩小
382
+
383
+ 例如:
384
+
385
+ ```ts
386
+ await stage.addGift({
387
+ type: 'svga',
388
+ source: 'https://example.com/demo.svga',
389
+ x: '50%',
390
+ y: '50%',
391
+ loop: 0,
392
+ });
393
+ ```
394
+
395
+ 这时会按舞台尺寸做等比适配,并保持礼物原始宽高比。
396
+
397
+ 如果希望礼物铺满整个画布,并按中心裁剪超出的部分,可以使用 `cover`:
398
+
399
+ ```ts
400
+ await stage.addGift({
401
+ type: 'svga',
402
+ source: 'https://example.com/demo.svga',
403
+ x: '50%',
404
+ y: '50%',
405
+ objectFit: 'cover',
406
+ loop: 0,
407
+ });
408
+ ```
409
+
410
+ 当 `x: '50%'`、`y: '50%'` 且未传 `width / height` 时,显示区域会居中放在整个画布上;`cover` 会保持礼物原始宽高比铺满该区域,并从中心裁剪溢出的部分。
411
+
412
+ 如果你希望“小礼物保持原始尺寸,大礼物再缩小”,可以这样:
413
+
414
+ ```ts
415
+ await stage.addGift({
416
+ type: 'svga',
417
+ source: 'https://example.com/demo.svga',
418
+ x: '50%',
419
+ y: '50%',
420
+ useOriginalSize: true,
421
+ loop: 0,
422
+ });
423
+ ```
424
+
425
+ ### 百分比坐标
426
+
427
+ `x / y` 支持百分比,例如:
428
+
429
+ ```ts
430
+ await stage.addGift({
431
+ type: 'svga',
432
+ source: 'https://example.com/demo.svga',
433
+ x: '50%',
434
+ y: '50%',
435
+ width: 300,
436
+ height: 300,
437
+ loop: 0,
438
+ });
439
+ ```
440
+
441
+ 语义是:
442
+
443
+ - `x: '50%'` 按容器宽度的 `50%` 定位,并减去自身一半宽度
444
+ - `y: '50%'` 按容器高度的 `50%` 定位,并减去自身一半高度
445
+
446
+ 也就是接近:
447
+
448
+ ```css
449
+ left: 50%;
450
+ top: 50%;
451
+ transform: translate(-50%, -50%);
452
+ ```
453
+
454
+ 如果是数值,则继续按原来的像素坐标语义处理。
455
+
456
+ ## 四类礼物示例
457
+
458
+ ### 播放 SVGA
459
+
460
+ ```ts
461
+ await stage.addGift({
462
+ type: 'svga',
463
+ source: 'https://example.com/demo.svga',
464
+ x: 20,
465
+ y: 20,
466
+ width: 300,
467
+ height: 300,
468
+ loop: 1,
469
+ });
470
+ ```
471
+
472
+ ### 播放 VAP
473
+
474
+ ```ts
475
+ await stage.addGift({
476
+ type: 'vap',
477
+ source: 'https://example.com/demo.mp4',
478
+ config: 'https://example.com/demo.json',
479
+ x: 20,
480
+ y: 20,
481
+ width: 400,
482
+ height: 220,
483
+ // 默认 premultiplied;RGB 尚未乘独立 Alpha 的视频源需显式传 straight
484
+ videoRGBAlphaMode: 'premultiplied',
485
+ loop: 0,
486
+ });
487
+ ```
488
+
489
+ ### 播放透明视频
490
+
491
+ ```ts
492
+ await stage.addGift({
493
+ type: 'alphaVideo',
494
+ source: 'https://example.com/demo.mp4',
495
+ x: 20,
496
+ y: 20,
497
+ width: 400,
498
+ height: 220,
499
+ // 默认 premultiplied;未预乘的视频源改为 straight
500
+ videoRGBAlphaMode: 'premultiplied',
501
+ loop: 0,
502
+ });
503
+ ```
504
+
505
+ ### 播放图片
506
+
507
+ `source` 首版支持 URL 和 `ArrayBuffer`。URL 图片会按地址共享 GPU 纹理并引用计数;图片按静态帧处理,不保证 GIF / WebP 动画播放。跨域 URL 必须允许匿名 CORS 读取。
508
+
509
+ ```ts
510
+ const avatar = await stage.addGift({
511
+ type: 'image',
512
+ source: 'https://example.com/avatar.png',
513
+ x: '50%',
514
+ y: '50%',
515
+ width: 100,
516
+ height: 100,
517
+ objectFit: 'cover',
518
+ duration: 5000,
519
+ });
520
+ ```
521
+
522
+ ## 动画控制
523
+
524
+ ### 链式动画
525
+
526
+ `addGift()` 返回的 `GiftHandle` 支持 `animate()`。一次 `to()` 是组合动画,同一步里可以同时移动、缩放、改变透明度和旋转;多个 `to()` / `then()` 会按顺序播放。`rotationTo` 直接按数值线性插值,不做最短角度归一化,因此可以明确表达多圈旋转:
527
+
528
+ ```ts
529
+ const gift = await stage.addGift({
530
+ type: 'svga',
531
+ source: 'https://example.com/demo.svga',
532
+ x: '50%',
533
+ y: '50%',
534
+ width: 300,
535
+ height: 300,
536
+ loop: 0,
537
+ });
538
+
539
+ await gift
540
+ .animate()
541
+ .to({
542
+ flyTo: { x: 200, y: 240 },
543
+ scaleTo: 0.8,
544
+ opacity: 0.6,
545
+ rotationTo: Math.PI * 4,
546
+ duration: 500,
547
+ })
548
+ .then({
549
+ flyTo: { x: 600, y: 320 },
550
+ scaleTo: 1.1,
551
+ opacity: 1,
552
+ duration: 700,
553
+ })
554
+ .delay(300)
555
+ .then({
556
+ opacity: 0,
557
+ duration: 400,
558
+ })
559
+ .onComplete((g) => {
560
+ console.log('chain complete', g.id);
561
+ })
562
+ .start();
563
+ ```
564
+
565
+ ### 绕外部中心旋转
566
+
567
+ 下面的礼物初始位于舞台中心右侧 200px,旋转中心通过框外坐标指回舞台中心:
568
+
569
+ ```ts
570
+ const orbitGift = await stage.addGift({
571
+ type: 'image',
572
+ source: 'https://example.com/gift.png',
573
+ x: container.clientWidth / 2 + 150,
574
+ y: '50%',
575
+ width: 100,
576
+ height: 100,
577
+ transformOrigin: { x: -150, y: '50%' },
578
+ });
579
+
580
+ await orbitGift
581
+ .animate({ rotationTo: Math.PI * 2, duration: 1200 })
582
+ .start();
583
+ ```
584
+
585
+ 也可以传入单步或数组:
586
+
587
+ ```ts
588
+ gift.animate({ flyTo: { x: 300, y: 300 }, scaleTo: 0.5, opacity: 0.3 }).start();
589
+
590
+ gift
591
+ .animate([
592
+ { flyTo: { x: 300, y: 300 }, duration: 400 },
593
+ { scaleTo: 1, opacity: 1, duration: 300 },
594
+ ])
595
+ .start();
596
+ ```
597
+
598
+ ## Slot 替换
599
+
600
+ ### SVGA
601
+
602
+ 使用 `svgaSlots`:
603
+
604
+ ```ts
605
+ await stage.addGift({
606
+ type: 'svga',
607
+ source: 'https://example.com/demo.svga',
608
+ x: 0,
609
+ y: 0,
610
+ width: 400,
611
+ height: 400,
612
+ svgaSlots: {
613
+ avatar: { image: avatarImage },
614
+ title: {
615
+ text: 'Hello',
616
+ color: '#ff0000',
617
+ fontSize: 28,
618
+ mode: 'dynamic',
619
+ },
620
+ },
621
+ });
622
+ ```
623
+
624
+ 支持:
625
+
626
+ - `TexImageSource`
627
+ - 文本配置
628
+ - 图片配置
629
+
630
+ SVGA 文本配置会按目标 frame 自动适配字号,避免文字超出槽位:
631
+
632
+ - `mode: 'dynamic'`:默认行为。生成文字自身尺寸的纹理,渲染时按自身逻辑尺寸居中到 frame 内,不会被拉伸。
633
+ - `mode: 'replace'`:生成和 frame 一样大的纹理,按替换图逻辑铺满 frame。
634
+ - `fontSize?: number`:期望字号;如果文字放不下,会自动缩小。
635
+ - `minFontSize?: number` / `maxFontSize?: number`:限制自适应字号范围。
636
+ - `padding?: number`:文字纹理内边距,默认 `2`。
637
+ - `scale?: number`:文字纹理栅格倍率,SVGA 默认 `3`,用于保持清晰度。
638
+ - `fontStyle?: string | { font?: string; color?: string }`:字体样式,SVGA / VAP 都支持。
639
+
640
+ `fontStyle` 可以直接传 canvas font 字符串:
641
+
642
+ ```ts
643
+ svgaSlots: {
644
+ title: {
645
+ text: 'Hello',
646
+ fontStyle: 'bold 40px Arial',
647
+ },
648
+ }
649
+ ```
650
+
651
+ 也可以传对象:
652
+
653
+ ```ts
654
+ vapSlots: {
655
+ welcome01: {
656
+ text: '欢迎进入房间',
657
+ fontStyle: {
658
+ font: 'bold 40px Arial',
659
+ color: '#ffffff',
660
+ },
661
+ },
662
+ }
663
+ ```
664
+
665
+ 对象形式目前生效字段是 `font` 和 `color`。如果同时传了外层 `color` 和 `fontStyle.color`,以 `fontStyle.color` 为准。VAP 文本如果没有传 `fontStyle`,会回退使用 VAP 配置里的 `src.fontStyle`。
666
+
667
+ ### VAP / VAPX
668
+
669
+ 使用 `vapSlots`:
670
+
671
+ ```ts
672
+ await stage.addGift({
673
+ type: 'vap',
674
+ source: 'https://example.com/demo.mp4',
675
+ config: 'https://example.com/demo.json',
676
+ x: 0,
677
+ y: 0,
678
+ width: 400,
679
+ height: 400,
680
+ vapSlots: {
681
+ welcome01: '欢迎进入房间',
682
+ avatar_left: 'https://example.com/avatar.png',
683
+ },
684
+ });
685
+ ```
686
+
687
+ 支持:
688
+
689
+ - 文本字符串
690
+ - 图片 URL
691
+ - `TexImageSource`
692
+ - 结构化文本 / 图片对象
693
+
694
+ ## 后端策略
695
+
696
+ 默认回退顺序:
697
+
698
+ 1. `WebGPU`
699
+ 2. `WebGL2`
700
+ 3. `WebGL1`
701
+
702
+ 说明:
703
+
704
+ - `WebGPU` 仅在浏览器支持相关必要能力时启用
705
+ - `WebGL1` 主要用于兼容兜底,不以性能最优为目标
706
+ - demo 中可手动强制切换到 `WebGL1`
707
+
708
+ ## 缓存策略
709
+
710
+ ### 内存缓存
711
+
712
+ - 默认 `128MB`
713
+ - LRU 淘汰
714
+
715
+ 缓存内容包括:
716
+
717
+ - 原始资源字节
718
+ - 文本 / JSON
719
+ - `SVGA` worker payload
720
+ - `SVGA` atlas 资产
721
+
722
+ ### `IndexedDB` 磁盘缓存
723
+
724
+ - 默认 `2048MB`
725
+ - LRU 淘汰
726
+ - 读取命中会刷新最近访问时间
727
+ - 超出预算时按最久未使用记录淘汰
728
+
729
+ 异常情况处理:
730
+
731
+ - `IndexedDB` 不可用、事务失败、写入失败、容量不足时,会自动降级
732
+ - 损坏的 `SVGA` 磁盘缓存会自动删除并重新生成
733
+
734
+ ## 项目结构
735
+
736
+ 主要目录:
737
+
738
+ - `src/`
739
+ 核心源码
740
+ - `docs/`
741
+ 设计与汇总文档
742
+ - `publish/`
743
+ 发布脚本
744
+ - `static/`
745
+ demo 依赖资源
746
+ - `tests/`
747
+ 测试
748
+
749
+ ## 对外导出
750
+
751
+ 入口文件:
752
+
753
+ - [src/index.ts](/D:/MyDocuments/UnityProjects/SVGAPlayer-Unity/gift-stage/src/index.ts)
754
+
691
755
  主要导出:
692
-
693
- - `GiftStage`
694
- - `createBackend`
695
- - `WebGPUBackend`
696
- - `WebGL2Backend`
697
- - `WebGL1Backend`
698
- - `parseSVGA`
699
- - `buildAtlas`
700
- - `parseVAPConfig`
756
+
757
+ - `GiftStage`
758
+ - `createBackend`
759
+ - `WebGPUBackend`
760
+ - `WebGL2Backend`
761
+ - `WebGL1Backend`
762
+ - `parseSVGA`
763
+ - `buildAtlas`
764
+ - `parseVAPConfig`
701
765
  - 相关类型定义
702
766
 
703
- ## Demo
704
-
705
- Demo 入口:
706
-
707
- - [index.html](/D:/MyDocuments/UnityProjects/SVGAPlayer-Unity/gift-stage/index.html)
767
+ ### 第三方 `GPUBackend` 迁移
708
768
 
709
- 可用于验证:
769
+ `GPUBackend` 的帧与提交操作使用显式结果契约。第三方实现必须让 `beginFrame()`、`draw(command)`、`endFrame()` 和 `abortFrame()` 返回:
710
770
 
711
- - `SVGA / VAP / AlphaVideo` 播放
712
- - slot 替换
713
- - `WebGPU / WebGL2 / WebGL1` 切换
714
- - 大批同屏压测
715
-
716
- ## 相关文档
771
+ ```ts
772
+ { submitted: true }
773
+ //
774
+ { submitted: false, reason, message? }
775
+ ```
717
776
 
718
- - 优化汇总:
719
- [docs/giftstage-optimization-summary.md](/D:/MyDocuments/UnityProjects/SVGAPlayer-Unity/gift-stage/docs/giftstage-optimization-summary.md)
777
+ 稳定的 `reason` 为 `backend-lost | invalid-command | missing-resource | capacity | validation-error | submission-error`。只有真正到达原生 `drawElements` / `drawIndexed` 或成功完成对应帧操作时才能返回 `submitted: true`;不得再实现或依赖 `wasLastDrawSubmitted()`,也不得用性能计数差值推断提交结果。`abortFrame()` 拒绝表示无法保证失败帧被丢弃,Stage 会立即隔离该 Backend。
778
+
779
+ ## Demo
780
+
781
+ Demo 入口:
782
+
783
+ - [index.html](/D:/MyDocuments/UnityProjects/SVGAPlayer-Unity/gift-stage/index.html)
784
+
785
+ 可用于验证:
786
+
787
+ - `SVGA / VAP / AlphaVideo` 播放
788
+ - slot 替换
789
+ - `WebGPU / WebGL2 / WebGL1` 切换
790
+ - 大批同屏压测
791
+
792
+ ## 相关文档
793
+
794
+ - 优化汇总:
795
+ [docs/giftstage-optimization-summary.md](/D:/MyDocuments/UnityProjects/SVGAPlayer-Unity/gift-stage/docs/giftstage-optimization-summary.md)