@taole/giftstage 0.1.27 → 0.1.28

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,636 +1,645 @@
1
- # GiftStage
2
-
3
- 统一的 Web 礼物播放框架,支持:
4
-
5
- - `SVGA`
6
- - `VAP / VAPX`
7
- - 透明视频 `AlphaVideo`
8
-
9
- 底层支持:
10
-
11
- - `WebGPU`
12
- - `WebGL2`
13
- - `WebGL1` 兼容兜底
14
-
15
- 适用场景:
16
-
17
- - 直播间礼物
18
- - 大批同屏动画
19
- - 需要统一接入 `SVGA / VAP / 透明视频` 的业务场景
20
-
21
- ## npm 包信息
22
-
23
- - 包名:`@taole/giftstage`
24
- - npm:`npm i @taole/giftstage`
25
- - ESM 导入:
26
-
27
- ```ts
28
- import { GiftStage } from '@taole/giftstage';
29
- ```
30
-
31
- - CommonJS 导入:
32
-
33
- ```js
34
- const { GiftStage } = require('@taole/giftstage');
35
- ```
36
-
37
- ## 特性
38
-
39
- - 统一 API:`addGift()` 即可播放三类礼物
40
- - `SVGA` 支持 Worker 解析、atlas 缓存、slot 替换
41
- - `VAP / AlphaVideo` 支持实验性的 `WebCodecs` 和稳定的 `HTMLVideoElement` 双路径
42
- - 同源视频播放实例、纹理、资源复用
43
- - 内存 LRU 缓存,默认 `128MB`
44
- - `IndexedDB` 磁盘 LRU 缓存,默认 `2048MB`
45
- - `x / y` 支持百分比定位
46
- - `width / height` 支持按礼物原始尺寸自动补全
47
-
48
- ## 安装
49
-
50
- ```bash
51
- npm install
52
- ```
53
-
54
- 安装发布包:
55
-
56
- ```bash
57
- npm i @taole/giftstage
58
- ```
59
-
60
- ## 开发
61
-
62
- ```bash
63
- npm run dev
64
- ```
65
-
66
- ## 构建
67
-
68
- ```bash
69
- npm run build
70
- ```
71
-
72
- Demo 构建:
73
-
74
- ```bash
75
- npm run build:demo
76
- ```
77
-
78
- ## 快速开始
79
-
80
- ```ts
81
- import { GiftStage } from '@taole/giftstage';
82
-
83
- const container = document.getElementById('app')!;
84
-
85
- const stage = new GiftStage({
86
- container,
87
- preferWebGPU: true,
88
- });
89
-
90
- await stage.ready;
91
-
92
- await stage.addGift({
93
- type: 'svga',
94
- source: 'https://example.com/demo.svga',
95
- x: 100,
96
- y: 100,
97
- loop: 0,
98
- });
99
- ```
100
-
101
- ## 核心 API
102
-
103
- ### `new GiftStage(options)`
104
-
105
- 创建礼物舞台。
106
-
107
- 常用配置:
108
-
109
- - `container: HTMLElement`
110
- 挂载容器。
111
-
112
- - `resolution?: number`
113
- 指定 canvas backing store 分辨率倍率。默认跟随 `devicePixelRatio`。
114
-
115
- - `antialias?: boolean`
116
- 是否开启抗锯齿。
117
-
118
- - `preferWebGPU?: boolean`
119
- 是否优先使用 `WebGPU`。
120
-
121
- - `forceWebGL1?: boolean`
122
- 强制走 `WebGL1`,用于兼容性验证。
123
-
124
- - `preferWebCodecs?: boolean`
125
- 实验性特性。默认关闭;显式传 `true` 时才会尝试用 `WebCodecs` 播放 `VAP / AlphaVideo`。
126
-
127
- - `shareIdenticalVideoPlayback?: boolean`
128
- 是否复用近同时起播的同源视频播放实例。默认开启。
129
-
130
- - `sharedVideoPlaybackWindowMs?: number`
131
- 同源视频共享播放窗口,默认 `100ms`。
132
-
133
- - `webCodecsVideoAtlas?: { width: number; height: number }`
134
- 实验性 `WebCodecs` 视频共享 atlas 配置。
135
-
136
- - `webCodecsDecodeInWorker?: boolean`
137
- 实验性 `WebCodecs` 选项,控制是否在 Worker 中执行解码。
138
-
139
- - `webCodecsMaxDecodedFrames?: number`
140
- 实验性 `WebCodecs` 选项,控制保留的最大解码帧数。
141
-
142
- - `webCodecsDecodeAheadFrames?: number`
143
- 实验性 `WebCodecs` 选项,控制前向解码帧数。
144
-
145
- - `svgaParseInWorker?: boolean`
146
- 是否在 Worker 中解析 SVGA。
147
-
148
- - `maxConcurrentParse?: number`
149
- 资源下载 / 解析 / 图像解码的并发数。
150
- 不传时自动跟随 `navigator.hardwareConcurrency`。
151
-
152
- - `svgaAtlasFrameBudgetMs?: number`
153
- SVGA atlas 在主线程组合或逐页上传时的单帧时间片上限,默认 `12ms`。
154
- 实际时间片会按当前刷新周期的一半动态调整,避免高刷新率设备上的加载任务挤占渲染帧。
155
-
156
- - `svgaLoadFrameBudgetMs?: number`
157
- SVGA 主线程加载任务的共享时间片上限,默认 `12ms`。图片 fallback、atlas、clip、音频、slot 纹理和二进制缓存共用同一帧预算。
158
-
159
- - `svgaWorkerFrameBudgetMs?: number`
160
- SVGA Worker 连续执行 JS 循环的时间片上限,默认 `4ms`,范围 `1-8ms`。单次 WASM protobuf 调用和单次原生 API 调用仍不可抢占。
161
-
162
- - `svgaWorkerTaskConcurrency?: number`
163
- parser、图片解码、atlas 和 clip 等 CPU 密集 Worker 的全局并发上限,默认 `1`。Atlas 首帧页面可提前返回,但后台 PNG 编码完成前仍持有该名额,避免与其他重载 Worker 叠加。
164
-
165
- - `svgaWorkerImageDecodeConcurrency?: number`
166
- 图片解码 Worker 内 `createImageBitmap` 并发上限,默认 `2`。运行时可按批次耗时收缩并发,负载恢复后回升,但不会超过该上限。
167
-
168
- - `svgaParseProfile?: boolean`
169
- 是否输出 `[GiftStage:SVGA:Profile]` 结构化解析日志,默认 `false`。日志包含 `DecompressionStream` 格式尝试、输入/输出 chunk、解压耗时以及 protobuf/payload 构建耗时;WASM payload 路径还会拆分 `prostDecodeMs`、`metadataAndImagesMs`、`frameTableMs`、`finalizeMs` 和 WASM 边界复制耗时。缓存命中不输出。
170
-
171
- SVGA 礼物在加载阶段被 `destroy()` / `removeGift()` 时,会取消排队任务并终止正在执行的 parser、图片解码、atlas 或 clip Worker。相同 URL 的共享加载按消费者计数,单个礼物取消不会中断其他仍在等待的礼物。
172
-
173
- - `onError?: (error: Error) => void`
174
- 统一错误回调。
175
-
176
- ### `await stage.ready`
177
-
178
- 等待底层后端和运行环境初始化完成。建议在第一次 `addGift()` 前等待。
179
-
180
- ### `stage.addGift(options)`
181
-
182
- 插入一个礼物,返回:
183
-
184
- ```ts
185
- Promise<GiftHandle>;
186
- ```
187
-
188
- ### `GiftHandle` API
189
-
190
- `addGift()` 返回的句柄可用于控制单个礼物:
191
-
192
- - `gift.id`
193
- - `gift.type`
194
- - `gift.pause()`
195
- - `gift.resume()`
196
- - `gift.destroy()`
197
- - 立即移除当前礼物
198
- - `gift.animate(stepOrSteps?)`
199
- - 创建链式动画并返回 `GiftAnimationChain`
200
-
201
- `GiftAnimationChain` 支持:
202
-
203
- - `.to(step)` / `.then(step)`
204
- - 追加动画步骤(两者等价)
205
- - `.delay(milliseconds)`
206
- - 在链中插入等待,不改变位置、缩放和透明度
207
- - `.onComplete((gift) => void)`
208
- - 整条链执行完成后的回调
209
- - `.start(mode?)`
210
- - `mode: 'immediate' | 'afterGiftComplete'`
211
- - `immediate`:立即开始(默认)
212
- - `afterGiftComplete`:等待礼物主播放结束后再执行链
213
- - `.cancel()`
214
- - 取消当前礼物正在执行的链式动画
215
-
216
- ### `stage.removeGift(id)`
217
-
218
- 按 ID 移除礼物。
219
-
220
- ### `stage.removeAll()`
221
-
222
- 移除全部礼物。
223
-
224
- ### `stage.pause()`
225
-
226
- 暂停舞台。
227
-
228
- ### `stage.resume()`
229
-
230
- 恢复舞台。
231
-
232
- ### `stage.destroy()`
233
-
234
- 销毁舞台并释放资源。
235
-
236
- ## `addGift()` 参数
237
-
238
- ### 通用参数
239
-
240
- - `type: 'svga' | 'vap' | 'alphaVideo'`
241
- - `source: string | ArrayBuffer`
242
- - `x: number | \`${number}%\``
243
- - `y: number | \`${number}%\``
244
- - `zIndex?: number`
245
- - `width?: number`
246
- - `height?: number`
247
- - `useOriginalSize?: boolean`
248
- - `objectFit?: 'contain' | 'cover' | 'fill'`
249
- - `loop?: number`
250
- - `0` 表示无限循环
1
+ # GiftStage
2
+
3
+ 统一的 Web 礼物播放框架,支持:
4
+
5
+ - `SVGA`
6
+ - `VAP / VAPX`
7
+ - 透明视频 `AlphaVideo`
8
+
9
+ 底层支持:
10
+
11
+ - `WebGPU`
12
+ - `WebGL2`
13
+ - `WebGL1` 兼容兜底
14
+
15
+ 适用场景:
16
+
17
+ - 直播间礼物
18
+ - 大批同屏动画
19
+ - 需要统一接入 `SVGA / VAP / 透明视频` 的业务场景
20
+
21
+ ## npm 包信息
22
+
23
+ - 包名:`@taole/giftstage`
24
+ - npm:`npm i @taole/giftstage`
25
+ - ESM 导入:
26
+
27
+ ```ts
28
+ import { GiftStage } from '@taole/giftstage';
29
+ ```
30
+
31
+ - CommonJS 导入:
32
+
33
+ ```js
34
+ const { GiftStage } = require('@taole/giftstage');
35
+ ```
36
+
37
+ ## 特性
38
+
39
+ - 统一 API:`addGift()` 即可播放三类礼物
40
+ - `SVGA` 支持 Worker 解析、atlas 缓存、slot 替换
41
+ - `VAP / AlphaVideo` 支持实验性的 `WebCodecs` 和稳定的 `HTMLVideoElement` 双路径
42
+ - 同源视频播放实例、纹理、资源复用
43
+ - 内存 LRU 缓存,默认 `128MB`
44
+ - `IndexedDB` 磁盘 LRU 缓存,默认 `2048MB`
45
+ - `x / y` 支持百分比定位
46
+ - `width / height` 支持按礼物原始尺寸自动补全
47
+
48
+ ## 安装
49
+
50
+ ```bash
51
+ npm install
52
+ ```
53
+
54
+ 安装发布包:
55
+
56
+ ```bash
57
+ npm i @taole/giftstage
58
+ ```
59
+
60
+ ## 开发
61
+
62
+ ```bash
63
+ npm run dev
64
+ ```
65
+
66
+ ## 构建
67
+
68
+ ```bash
69
+ npm run build
70
+ ```
71
+
72
+ Demo 构建:
73
+
74
+ ```bash
75
+ npm run build:demo
76
+ ```
77
+
78
+ ## 快速开始
79
+
80
+ ```ts
81
+ import { GiftStage } from '@taole/giftstage';
82
+
83
+ const container = document.getElementById('app')!;
84
+
85
+ const stage = new GiftStage({
86
+ container,
87
+ preferWebGPU: true,
88
+ });
89
+
90
+ await stage.ready;
91
+
92
+ await stage.addGift({
93
+ type: 'svga',
94
+ source: 'https://example.com/demo.svga',
95
+ x: 100,
96
+ y: 100,
97
+ loop: 0,
98
+ });
99
+ ```
100
+
101
+ ## 核心 API
102
+
103
+ ### `new GiftStage(options)`
104
+
105
+ 创建礼物舞台。
106
+
107
+ 常用配置:
108
+
109
+ - `container: HTMLElement`
110
+ 挂载容器。
111
+
112
+ - `resolution?: number`
113
+ 指定 canvas backing store 分辨率倍率。默认跟随 `devicePixelRatio`。
114
+
115
+ - `antialias?: boolean`
116
+ 是否开启抗锯齿。
117
+
118
+ - `preferWebGPU?: boolean`
119
+ 是否优先使用 `WebGPU`。
120
+
121
+ - `forceWebGL1?: boolean`
122
+ 强制走 `WebGL1`,用于兼容性验证。
123
+
124
+ - `preferWebCodecs?: boolean`
125
+ 实验性特性。默认关闭;显式传 `true` 时才会尝试用 `WebCodecs` 播放 `VAP / AlphaVideo`。
126
+
127
+ - `shareIdenticalVideoPlayback?: boolean`
128
+ 是否复用近同时起播的同源视频播放实例。默认开启。
129
+
130
+ - `sharedVideoPlaybackWindowMs?: number`
131
+ 同源视频共享播放窗口,默认 `100ms`。
132
+
133
+ - `webCodecsVideoAtlas?: { width: number; height: number }`
134
+ 实验性 `WebCodecs` 视频共享 atlas 配置。
135
+
136
+ - `webCodecsDecodeInWorker?: boolean`
137
+ 实验性 `WebCodecs` 选项,控制是否在 Worker 中执行解码。
138
+
139
+ - `webCodecsMaxDecodedFrames?: number`
140
+ 实验性 `WebCodecs` 选项,控制保留的最大解码帧数。
141
+
142
+ - `webCodecsDecodeAheadFrames?: number`
143
+ 实验性 `WebCodecs` 选项,控制前向解码帧数。
144
+
145
+ - `svgaParseInWorker?: boolean`
146
+ 是否在 Worker 中解析 SVGA。
147
+
148
+ - `maxConcurrentParse?: number`
149
+ 资源下载 / 解析 / 图像解码的并发数。
150
+ 不传时自动跟随 `navigator.hardwareConcurrency`。
151
+
152
+ - `svgaAtlasFrameBudgetMs?: number`
153
+ SVGA atlas 在主线程组合或逐页上传时的单帧时间片上限,默认 `12ms`。
154
+ 实际时间片会按当前刷新周期的一半动态调整,避免高刷新率设备上的加载任务挤占渲染帧。
155
+
156
+ - `svgaLoadFrameBudgetMs?: number`
157
+ SVGA 主线程加载任务的共享时间片上限,默认 `12ms`。图片 fallback、atlas、clip、音频、slot 纹理和二进制缓存共用同一帧预算。
158
+
159
+ - `svgaWorkerFrameBudgetMs?: number`
160
+ SVGA Worker 连续执行 JS 循环的时间片上限,默认 `4ms`,范围 `1-8ms`。单次 WASM protobuf 调用和单次原生 API 调用仍不可抢占。
161
+
162
+ - `svgaWorkerTaskConcurrency?: number`
163
+ parser、图片解码、atlas 和 clip 等 CPU 密集 Worker 的全局并发上限,默认 `1`。Atlas 首帧页面可提前返回,但后台 PNG 编码完成前仍持有该名额,避免与其他重载 Worker 叠加。
164
+
165
+ - `svgaWorkerImageDecodeConcurrency?: number`
166
+ 图片解码 Worker 内 `createImageBitmap` 并发上限,默认 `2`。运行时可按批次耗时收缩并发,负载恢复后回升,但不会超过该上限。
167
+
168
+ - `svgaParseProfile?: boolean`
169
+ 是否输出 `[GiftStage:SVGA:Profile]` 结构化解析日志,默认 `false`。日志包含 `DecompressionStream` 格式尝试、输入/输出 chunk、解压耗时以及 protobuf/payload 构建耗时;WASM payload 路径还会拆分 `prostDecodeMs`、`metadataAndImagesMs`、`frameTableMs`、`finalizeMs` 和 WASM 边界复制耗时。缓存命中不输出。
170
+
171
+ SVGA 礼物在加载阶段被 `destroy()` / `removeGift()` 时,会取消排队任务并终止正在执行的 parser、图片解码、atlas 或 clip Worker。相同 URL 的共享加载按消费者计数,单个礼物取消不会中断其他仍在等待的礼物。
172
+
173
+ - `onError?: (error: Error) => void`
174
+ 统一错误回调。
175
+
176
+ ### `await stage.ready`
177
+
178
+ 等待底层后端和运行环境初始化完成。建议在第一次 `addGift()` 前等待。
179
+
180
+ ### `stage.addGift(options)`
181
+
182
+ 插入一个礼物,返回:
183
+
184
+ ```ts
185
+ Promise<GiftHandle>;
186
+ ```
187
+
188
+ ### `GiftHandle` API
189
+
190
+ `addGift()` 返回的句柄可用于控制单个礼物:
191
+
192
+ - `gift.id`
193
+ - `gift.type`
194
+ - `gift.pause()`
195
+ - `gift.resume()`
196
+ - `gift.destroy()`
197
+ - 立即移除当前礼物
198
+ - `gift.animate(stepOrSteps?)`
199
+ - 创建链式动画并返回 `GiftAnimationChain`
200
+
201
+ `GiftAnimationChain` 支持:
202
+
203
+ - `.to(step)` / `.then(step)`
204
+ - 追加动画步骤(两者等价)
205
+ - `.delay(milliseconds)`
206
+ - 在链中插入等待,不改变位置、缩放和透明度
207
+ - `.onComplete((gift) => void)`
208
+ - 整条链执行完成后的回调
209
+ - `.start(mode?)`
210
+ - `mode: 'immediate' | 'afterGiftComplete'`
211
+ - `immediate`:立即开始(默认)
212
+ - `afterGiftComplete`:等待礼物主播放结束后再执行链
213
+ - `.cancel()`
214
+ - 取消当前礼物正在执行的链式动画
215
+
216
+ ### `stage.removeGift(id)`
217
+
218
+ 按 ID 移除礼物。
219
+
220
+ ### `stage.removeAll()`
221
+
222
+ 移除全部礼物。
223
+
224
+ ### `stage.pause()`
225
+
226
+ 暂停舞台。
227
+
228
+ ### `stage.resume()`
229
+
230
+ 恢复舞台。
231
+
232
+ ### `stage.destroy()`
233
+
234
+ 销毁舞台并释放资源。
235
+
236
+ ## `addGift()` 参数
237
+
238
+ ### 通用参数
239
+
240
+ - `type: 'svga' | 'vap' | 'alphaVideo'`
241
+ - `source: string | ArrayBuffer`
242
+ - `x: number | \`${number}%\``
243
+ - `y: number | \`${number}%\``
244
+ - `zIndex?: number`
245
+ - `width?: number`
246
+ - `height?: number`
247
+ - `useOriginalSize?: boolean`
248
+ - `objectFit?: 'contain' | 'cover' | 'fill'`
249
+ - `loop?: number`
250
+ - `0` 表示无限循环
251
251
  - `opacity?: number`
252
252
  - 全局透明度,范围 `0 ~ 1`,默认 `1`
253
+ - `videoRGBAlphaMode?: 'straight' | 'premultiplied'`
254
+ - 仅用于 `vap` / `alphaVideo` 分离 Alpha 视频源,默认 `premultiplied`
255
+ - `straight`:RGB 区域尚未乘独立 Alpha,播放器负责预乘
256
+ - `premultiplied`:RGB 区域在制作阶段已经乘过独立 Alpha,播放器不会再次相乘
257
+ - 该参数只描述输入视频;最终画布仍统一输出预乘 Alpha
253
258
  - `clearsAfterStop?: boolean`
254
- - 播放结束后,是否自动移除礼物
255
- - 默认 `true`,传 `false` 时会停留最后一帧,方便后续继续调用 `gift.animate(...).start()`
256
- - `mute?: boolean`
257
- - `onComplete?: (gift) => void`
258
-
259
- 层级规则:
260
-
261
- - `zIndex` 在 `SVGA / VAP / AlphaVideo` 三种礼物间通用
262
- - 数值越大,渲染越靠上
263
- - 同层级下,后 `addGift()` 的礼物会覆盖先添加的礼物
264
-
265
- ### 宽高默认行为
266
-
267
- `width / height` 现在是可选参数,规则如下:
268
-
269
- - 两个都传:按传入值显示
270
- - 只传 `width`:`height` 按礼物原始宽高比自动补全
271
- - 只传 `height`:`width` 按礼物原始宽高比自动补全
272
- - 两个都不传:默认按当前画布尺寸做等比 `contain`
273
- - 如果 `objectFit: 'cover'` 且两个都不传:按整个画布作为显示区域做等比 `cover`
274
- - 也就是会在画布内尽可能放大或缩小,并保持礼物原始宽高比
275
- - 如果 `useOriginalSize: true`,且礼物原始尺寸本身小于画布,则优先使用礼物原始尺寸
276
- - 如果 `useOriginalSize: true`,但礼物原始尺寸超出画布,则仍会按画布尺寸等比缩小
277
-
278
- 例如:
279
-
280
- ```ts
281
- await stage.addGift({
282
- type: 'svga',
283
- source: 'https://example.com/demo.svga',
284
- x: '50%',
285
- y: '50%',
286
- loop: 0,
287
- });
288
- ```
289
-
290
- 这时会按舞台尺寸做等比适配,并保持礼物原始宽高比。
291
-
292
- 如果希望礼物铺满整个画布,并按中心裁剪超出的部分,可以使用 `cover`:
293
-
294
- ```ts
295
- await stage.addGift({
296
- type: 'svga',
297
- source: 'https://example.com/demo.svga',
298
- x: '50%',
299
- y: '50%',
300
- objectFit: 'cover',
301
- loop: 0,
302
- });
303
- ```
304
-
305
- 当 `x: '50%'`、`y: '50%'` 且未传 `width / height` 时,显示区域会居中放在整个画布上;`cover` 会保持礼物原始宽高比铺满该区域,并从中心裁剪溢出的部分。
306
-
307
- 如果你希望“小礼物保持原始尺寸,大礼物再缩小”,可以这样:
308
-
309
- ```ts
310
- await stage.addGift({
311
- type: 'svga',
312
- source: 'https://example.com/demo.svga',
313
- x: '50%',
314
- y: '50%',
315
- useOriginalSize: true,
316
- loop: 0,
317
- });
318
- ```
319
-
320
- ### 百分比坐标
321
-
322
- `x / y` 支持百分比,例如:
323
-
324
- ```ts
325
- await stage.addGift({
326
- type: 'svga',
327
- source: 'https://example.com/demo.svga',
328
- x: '50%',
329
- y: '50%',
330
- width: 300,
331
- height: 300,
332
- loop: 0,
333
- });
334
- ```
335
-
336
- 语义是:
337
-
338
- - `x: '50%'` 按容器宽度的 `50%` 定位,并减去自身一半宽度
339
- - `y: '50%'` 按容器高度的 `50%` 定位,并减去自身一半高度
340
-
341
- 也就是接近:
342
-
343
- ```css
344
- left: 50%;
345
- top: 50%;
346
- transform: translate(-50%, -50%);
347
- ```
348
-
349
- 如果是数值,则继续按原来的像素坐标语义处理。
350
-
351
- ## 三类礼物示例
352
-
353
- ### 播放 SVGA
354
-
355
- ```ts
356
- await stage.addGift({
357
- type: 'svga',
358
- source: 'https://example.com/demo.svga',
359
- x: 20,
360
- y: 20,
361
- width: 300,
362
- height: 300,
363
- loop: 1,
364
- });
365
- ```
366
-
367
- ### 播放 VAP
368
-
369
- ```ts
370
- await stage.addGift({
371
- type: 'vap',
372
- source: 'https://example.com/demo.mp4',
373
- config: 'https://example.com/demo.json',
374
- x: 20,
375
- y: 20,
259
+ - 播放结束后,是否自动移除礼物
260
+ - 默认 `true`,传 `false` 时会停留最后一帧,方便后续继续调用 `gift.animate(...).start()`
261
+ - `mute?: boolean`
262
+ - `onComplete?: (gift) => void`
263
+
264
+ 层级规则:
265
+
266
+ - `zIndex` 在 `SVGA / VAP / AlphaVideo` 三种礼物间通用
267
+ - 数值越大,渲染越靠上
268
+ - 同层级下,后 `addGift()` 的礼物会覆盖先添加的礼物
269
+
270
+ ### 宽高默认行为
271
+
272
+ `width / height` 现在是可选参数,规则如下:
273
+
274
+ - 两个都传:按传入值显示
275
+ - 只传 `width`:`height` 按礼物原始宽高比自动补全
276
+ - 只传 `height`:`width` 按礼物原始宽高比自动补全
277
+ - 两个都不传:默认按当前画布尺寸做等比 `contain`
278
+ - 如果 `objectFit: 'cover'` 且两个都不传:按整个画布作为显示区域做等比 `cover`
279
+ - 也就是会在画布内尽可能放大或缩小,并保持礼物原始宽高比
280
+ - 如果 `useOriginalSize: true`,且礼物原始尺寸本身小于画布,则优先使用礼物原始尺寸
281
+ - 如果 `useOriginalSize: true`,但礼物原始尺寸超出画布,则仍会按画布尺寸等比缩小
282
+
283
+ 例如:
284
+
285
+ ```ts
286
+ await stage.addGift({
287
+ type: 'svga',
288
+ source: 'https://example.com/demo.svga',
289
+ x: '50%',
290
+ y: '50%',
291
+ loop: 0,
292
+ });
293
+ ```
294
+
295
+ 这时会按舞台尺寸做等比适配,并保持礼物原始宽高比。
296
+
297
+ 如果希望礼物铺满整个画布,并按中心裁剪超出的部分,可以使用 `cover`:
298
+
299
+ ```ts
300
+ await stage.addGift({
301
+ type: 'svga',
302
+ source: 'https://example.com/demo.svga',
303
+ x: '50%',
304
+ y: '50%',
305
+ objectFit: 'cover',
306
+ loop: 0,
307
+ });
308
+ ```
309
+
310
+ 当 `x: '50%'`、`y: '50%'` 且未传 `width / height` 时,显示区域会居中放在整个画布上;`cover` 会保持礼物原始宽高比铺满该区域,并从中心裁剪溢出的部分。
311
+
312
+ 如果你希望“小礼物保持原始尺寸,大礼物再缩小”,可以这样:
313
+
314
+ ```ts
315
+ await stage.addGift({
316
+ type: 'svga',
317
+ source: 'https://example.com/demo.svga',
318
+ x: '50%',
319
+ y: '50%',
320
+ useOriginalSize: true,
321
+ loop: 0,
322
+ });
323
+ ```
324
+
325
+ ### 百分比坐标
326
+
327
+ `x / y` 支持百分比,例如:
328
+
329
+ ```ts
330
+ await stage.addGift({
331
+ type: 'svga',
332
+ source: 'https://example.com/demo.svga',
333
+ x: '50%',
334
+ y: '50%',
335
+ width: 300,
336
+ height: 300,
337
+ loop: 0,
338
+ });
339
+ ```
340
+
341
+ 语义是:
342
+
343
+ - `x: '50%'` 按容器宽度的 `50%` 定位,并减去自身一半宽度
344
+ - `y: '50%'` 按容器高度的 `50%` 定位,并减去自身一半高度
345
+
346
+ 也就是接近:
347
+
348
+ ```css
349
+ left: 50%;
350
+ top: 50%;
351
+ transform: translate(-50%, -50%);
352
+ ```
353
+
354
+ 如果是数值,则继续按原来的像素坐标语义处理。
355
+
356
+ ## 三类礼物示例
357
+
358
+ ### 播放 SVGA
359
+
360
+ ```ts
361
+ await stage.addGift({
362
+ type: 'svga',
363
+ source: 'https://example.com/demo.svga',
364
+ x: 20,
365
+ y: 20,
366
+ width: 300,
367
+ height: 300,
368
+ loop: 1,
369
+ });
370
+ ```
371
+
372
+ ### 播放 VAP
373
+
374
+ ```ts
375
+ await stage.addGift({
376
+ type: 'vap',
377
+ source: 'https://example.com/demo.mp4',
378
+ config: 'https://example.com/demo.json',
379
+ x: 20,
380
+ y: 20,
376
381
  width: 400,
377
382
  height: 220,
383
+ // 默认 premultiplied;RGB 尚未乘独立 Alpha 的视频源需显式传 straight
384
+ videoRGBAlphaMode: 'premultiplied',
378
385
  loop: 0,
379
386
  });
380
- ```
381
-
382
- ### 播放透明视频
383
-
384
- ```ts
385
- await stage.addGift({
386
- type: 'alphaVideo',
387
- source: 'https://example.com/demo.mp4',
388
- x: 20,
389
- y: 20,
387
+ ```
388
+
389
+ ### 播放透明视频
390
+
391
+ ```ts
392
+ await stage.addGift({
393
+ type: 'alphaVideo',
394
+ source: 'https://example.com/demo.mp4',
395
+ x: 20,
396
+ y: 20,
390
397
  width: 400,
391
398
  height: 220,
399
+ // 默认 premultiplied;未预乘的视频源改为 straight
400
+ videoRGBAlphaMode: 'premultiplied',
392
401
  loop: 0,
393
402
  });
394
- ```
395
-
396
- ## 动画控制
397
-
398
- ### 链式动画
399
-
400
- `addGift()` 返回的 `GiftHandle` 支持 `animate()`。一次 `to()` 是组合动画,同一步里可以同时移动、缩放和改变透明度;多个 `to()` / `then()` 会按顺序播放:
401
-
402
- ```ts
403
- const gift = await stage.addGift({
404
- type: 'svga',
405
- source: 'https://example.com/demo.svga',
406
- x: '50%',
407
- y: '50%',
408
- width: 300,
409
- height: 300,
410
- loop: 0,
411
- });
412
-
413
- await gift
414
- .animate()
415
- .to({
416
- flyTo: { x: 200, y: 240 },
417
- scaleTo: 0.8,
418
- opacity: 0.6,
419
- duration: 500,
420
- })
421
- .then({
422
- flyTo: { x: 600, y: 320 },
423
- scaleTo: 1.1,
424
- opacity: 1,
425
- duration: 700,
426
- })
427
- .delay(300)
428
- .then({
429
- opacity: 0,
430
- duration: 400,
431
- })
432
- .onComplete((g) => {
433
- console.log('chain complete', g.id);
434
- })
435
- .start();
436
- ```
437
-
438
- 也可以传入单步或数组:
439
-
440
- ```ts
441
- gift.animate({ flyTo: { x: 300, y: 300 }, scaleTo: 0.5, opacity: 0.3 }).start();
442
-
443
- gift
444
- .animate([
445
- { flyTo: { x: 300, y: 300 }, duration: 400 },
446
- { scaleTo: 1, opacity: 1, duration: 300 },
447
- ])
448
- .start();
449
- ```
450
-
451
- ## Slot 替换
452
-
453
- ### SVGA
454
-
455
- 使用 `svgaSlots`:
456
-
457
- ```ts
458
- await stage.addGift({
459
- type: 'svga',
460
- source: 'https://example.com/demo.svga',
461
- x: 0,
462
- y: 0,
463
- width: 400,
464
- height: 400,
465
- svgaSlots: {
466
- avatar: { image: avatarImage },
467
- title: {
468
- text: 'Hello',
469
- color: '#ff0000',
470
- fontSize: 28,
471
- mode: 'dynamic',
472
- },
473
- },
474
- });
475
- ```
476
-
477
- 支持:
478
-
479
- - `TexImageSource`
480
- - 文本配置
481
- - 图片配置
482
-
483
- SVGA 文本配置会按目标 frame 自动适配字号,避免文字超出槽位:
484
-
485
- - `mode: 'dynamic'`:默认行为。生成文字自身尺寸的纹理,渲染时按自身逻辑尺寸居中到 frame 内,不会被拉伸。
486
- - `mode: 'replace'`:生成和 frame 一样大的纹理,按替换图逻辑铺满 frame。
487
- - `fontSize?: number`:期望字号;如果文字放不下,会自动缩小。
488
- - `minFontSize?: number` / `maxFontSize?: number`:限制自适应字号范围。
489
- - `padding?: number`:文字纹理内边距,默认 `2`。
490
- - `scale?: number`:文字纹理栅格倍率,SVGA 默认 `3`,用于保持清晰度。
491
- - `fontStyle?: string | { font?: string; color?: string }`:字体样式,SVGA / VAP 都支持。
492
-
493
- `fontStyle` 可以直接传 canvas font 字符串:
494
-
495
- ```ts
496
- svgaSlots: {
497
- title: {
498
- text: 'Hello',
499
- fontStyle: 'bold 40px Arial',
500
- },
501
- }
502
- ```
503
-
504
- 也可以传对象:
505
-
506
- ```ts
507
- vapSlots: {
508
- welcome01: {
509
- text: '欢迎进入房间',
510
- fontStyle: {
511
- font: 'bold 40px Arial',
512
- color: '#ffffff',
513
- },
514
- },
515
- }
516
- ```
517
-
518
- 对象形式目前生效字段是 `font` 和 `color`。如果同时传了外层 `color` 和 `fontStyle.color`,以 `fontStyle.color` 为准。VAP 文本如果没有传 `fontStyle`,会回退使用 VAP 配置里的 `src.fontStyle`。
519
-
520
- ### VAP / VAPX
521
-
522
- 使用 `vapSlots`:
523
-
524
- ```ts
525
- await stage.addGift({
526
- type: 'vap',
527
- source: 'https://example.com/demo.mp4',
528
- config: 'https://example.com/demo.json',
529
- x: 0,
530
- y: 0,
531
- width: 400,
532
- height: 400,
533
- vapSlots: {
534
- welcome01: '欢迎进入房间',
535
- avatar_left: 'https://example.com/avatar.png',
536
- },
537
- });
538
- ```
539
-
540
- 支持:
541
-
542
- - 文本字符串
543
- - 图片 URL
544
- - `TexImageSource`
545
- - 结构化文本 / 图片对象
546
-
547
- ## 后端策略
548
-
549
- 默认回退顺序:
550
-
551
- 1. `WebGPU`
552
- 2. `WebGL2`
553
- 3. `WebGL1`
554
-
555
- 说明:
556
-
557
- - `WebGPU` 仅在浏览器支持相关必要能力时启用
558
- - `WebGL1` 主要用于兼容兜底,不以性能最优为目标
559
- - demo 中可手动强制切换到 `WebGL1`
560
-
561
- ## 缓存策略
562
-
563
- ### 内存缓存
564
-
565
- - 默认 `128MB`
566
- - LRU 淘汰
567
-
568
- 缓存内容包括:
569
-
570
- - 原始资源字节
571
- - 文本 / JSON
572
- - `SVGA` worker payload
573
- - `SVGA` atlas 资产
574
-
575
- ### `IndexedDB` 磁盘缓存
576
-
577
- - 默认 `2048MB`
578
- - LRU 淘汰
579
- - 读取命中会刷新最近访问时间
580
- - 超出预算时按最久未使用记录淘汰
581
-
582
- 异常情况处理:
583
-
584
- - `IndexedDB` 不可用、事务失败、写入失败、容量不足时,会自动降级
585
- - 损坏的 `SVGA` 磁盘缓存会自动删除并重新生成
586
-
587
- ## 项目结构
588
-
589
- 主要目录:
590
-
591
- - `src/`
592
- 核心源码
593
- - `docs/`
594
- 设计与汇总文档
595
- - `publish/`
596
- 发布脚本
597
- - `static/`
598
- demo 依赖资源
599
- - `tests/`
600
- 测试
601
-
602
- ## 对外导出
603
-
604
- 入口文件:
605
-
606
- - [src/index.ts](/D:/MyDocuments/UnityProjects/SVGAPlayer-Unity/gift-stage/src/index.ts)
607
-
608
- 主要导出:
609
-
610
- - `GiftStage`
611
- - `createBackend`
612
- - `WebGPUBackend`
613
- - `WebGL2Backend`
614
- - `WebGL1Backend`
615
- - `parseSVGA`
616
- - `buildAtlas`
617
- - `parseVAPConfig`
618
- - 相关类型定义
619
-
620
- ## Demo
621
-
622
- Demo 入口:
623
-
624
- - [index.html](/D:/MyDocuments/UnityProjects/SVGAPlayer-Unity/gift-stage/index.html)
625
-
626
- 可用于验证:
627
-
628
- - `SVGA / VAP / AlphaVideo` 播放
629
- - slot 替换
630
- - `WebGPU / WebGL2 / WebGL1` 切换
631
- - 大批同屏压测
632
-
633
- ## 相关文档
634
-
635
- - 优化汇总:
636
- [docs/giftstage-optimization-summary.md](/D:/MyDocuments/UnityProjects/SVGAPlayer-Unity/gift-stage/docs/giftstage-optimization-summary.md)
403
+ ```
404
+
405
+ ## 动画控制
406
+
407
+ ### 链式动画
408
+
409
+ `addGift()` 返回的 `GiftHandle` 支持 `animate()`。一次 `to()` 是组合动画,同一步里可以同时移动、缩放和改变透明度;多个 `to()` / `then()` 会按顺序播放:
410
+
411
+ ```ts
412
+ const gift = await stage.addGift({
413
+ type: 'svga',
414
+ source: 'https://example.com/demo.svga',
415
+ x: '50%',
416
+ y: '50%',
417
+ width: 300,
418
+ height: 300,
419
+ loop: 0,
420
+ });
421
+
422
+ await gift
423
+ .animate()
424
+ .to({
425
+ flyTo: { x: 200, y: 240 },
426
+ scaleTo: 0.8,
427
+ opacity: 0.6,
428
+ duration: 500,
429
+ })
430
+ .then({
431
+ flyTo: { x: 600, y: 320 },
432
+ scaleTo: 1.1,
433
+ opacity: 1,
434
+ duration: 700,
435
+ })
436
+ .delay(300)
437
+ .then({
438
+ opacity: 0,
439
+ duration: 400,
440
+ })
441
+ .onComplete((g) => {
442
+ console.log('chain complete', g.id);
443
+ })
444
+ .start();
445
+ ```
446
+
447
+ 也可以传入单步或数组:
448
+
449
+ ```ts
450
+ gift.animate({ flyTo: { x: 300, y: 300 }, scaleTo: 0.5, opacity: 0.3 }).start();
451
+
452
+ gift
453
+ .animate([
454
+ { flyTo: { x: 300, y: 300 }, duration: 400 },
455
+ { scaleTo: 1, opacity: 1, duration: 300 },
456
+ ])
457
+ .start();
458
+ ```
459
+
460
+ ## Slot 替换
461
+
462
+ ### SVGA
463
+
464
+ 使用 `svgaSlots`:
465
+
466
+ ```ts
467
+ await stage.addGift({
468
+ type: 'svga',
469
+ source: 'https://example.com/demo.svga',
470
+ x: 0,
471
+ y: 0,
472
+ width: 400,
473
+ height: 400,
474
+ svgaSlots: {
475
+ avatar: { image: avatarImage },
476
+ title: {
477
+ text: 'Hello',
478
+ color: '#ff0000',
479
+ fontSize: 28,
480
+ mode: 'dynamic',
481
+ },
482
+ },
483
+ });
484
+ ```
485
+
486
+ 支持:
487
+
488
+ - `TexImageSource`
489
+ - 文本配置
490
+ - 图片配置
491
+
492
+ SVGA 文本配置会按目标 frame 自动适配字号,避免文字超出槽位:
493
+
494
+ - `mode: 'dynamic'`:默认行为。生成文字自身尺寸的纹理,渲染时按自身逻辑尺寸居中到 frame 内,不会被拉伸。
495
+ - `mode: 'replace'`:生成和 frame 一样大的纹理,按替换图逻辑铺满 frame。
496
+ - `fontSize?: number`:期望字号;如果文字放不下,会自动缩小。
497
+ - `minFontSize?: number` / `maxFontSize?: number`:限制自适应字号范围。
498
+ - `padding?: number`:文字纹理内边距,默认 `2`。
499
+ - `scale?: number`:文字纹理栅格倍率,SVGA 默认 `3`,用于保持清晰度。
500
+ - `fontStyle?: string | { font?: string; color?: string }`:字体样式,SVGA / VAP 都支持。
501
+
502
+ `fontStyle` 可以直接传 canvas font 字符串:
503
+
504
+ ```ts
505
+ svgaSlots: {
506
+ title: {
507
+ text: 'Hello',
508
+ fontStyle: 'bold 40px Arial',
509
+ },
510
+ }
511
+ ```
512
+
513
+ 也可以传对象:
514
+
515
+ ```ts
516
+ vapSlots: {
517
+ welcome01: {
518
+ text: '欢迎进入房间',
519
+ fontStyle: {
520
+ font: 'bold 40px Arial',
521
+ color: '#ffffff',
522
+ },
523
+ },
524
+ }
525
+ ```
526
+
527
+ 对象形式目前生效字段是 `font` 和 `color`。如果同时传了外层 `color` 和 `fontStyle.color`,以 `fontStyle.color` 为准。VAP 文本如果没有传 `fontStyle`,会回退使用 VAP 配置里的 `src.fontStyle`。
528
+
529
+ ### VAP / VAPX
530
+
531
+ 使用 `vapSlots`:
532
+
533
+ ```ts
534
+ await stage.addGift({
535
+ type: 'vap',
536
+ source: 'https://example.com/demo.mp4',
537
+ config: 'https://example.com/demo.json',
538
+ x: 0,
539
+ y: 0,
540
+ width: 400,
541
+ height: 400,
542
+ vapSlots: {
543
+ welcome01: '欢迎进入房间',
544
+ avatar_left: 'https://example.com/avatar.png',
545
+ },
546
+ });
547
+ ```
548
+
549
+ 支持:
550
+
551
+ - 文本字符串
552
+ - 图片 URL
553
+ - `TexImageSource`
554
+ - 结构化文本 / 图片对象
555
+
556
+ ## 后端策略
557
+
558
+ 默认回退顺序:
559
+
560
+ 1. `WebGPU`
561
+ 2. `WebGL2`
562
+ 3. `WebGL1`
563
+
564
+ 说明:
565
+
566
+ - `WebGPU` 仅在浏览器支持相关必要能力时启用
567
+ - `WebGL1` 主要用于兼容兜底,不以性能最优为目标
568
+ - demo 中可手动强制切换到 `WebGL1`
569
+
570
+ ## 缓存策略
571
+
572
+ ### 内存缓存
573
+
574
+ - 默认 `128MB`
575
+ - LRU 淘汰
576
+
577
+ 缓存内容包括:
578
+
579
+ - 原始资源字节
580
+ - 文本 / JSON
581
+ - `SVGA` worker payload
582
+ - `SVGA` atlas 资产
583
+
584
+ ### `IndexedDB` 磁盘缓存
585
+
586
+ - 默认 `2048MB`
587
+ - LRU 淘汰
588
+ - 读取命中会刷新最近访问时间
589
+ - 超出预算时按最久未使用记录淘汰
590
+
591
+ 异常情况处理:
592
+
593
+ - `IndexedDB` 不可用、事务失败、写入失败、容量不足时,会自动降级
594
+ - 损坏的 `SVGA` 磁盘缓存会自动删除并重新生成
595
+
596
+ ## 项目结构
597
+
598
+ 主要目录:
599
+
600
+ - `src/`
601
+ 核心源码
602
+ - `docs/`
603
+ 设计与汇总文档
604
+ - `publish/`
605
+ 发布脚本
606
+ - `static/`
607
+ demo 依赖资源
608
+ - `tests/`
609
+ 测试
610
+
611
+ ## 对外导出
612
+
613
+ 入口文件:
614
+
615
+ - [src/index.ts](/D:/MyDocuments/UnityProjects/SVGAPlayer-Unity/gift-stage/src/index.ts)
616
+
617
+ 主要导出:
618
+
619
+ - `GiftStage`
620
+ - `createBackend`
621
+ - `WebGPUBackend`
622
+ - `WebGL2Backend`
623
+ - `WebGL1Backend`
624
+ - `parseSVGA`
625
+ - `buildAtlas`
626
+ - `parseVAPConfig`
627
+ - 相关类型定义
628
+
629
+ ## Demo
630
+
631
+ Demo 入口:
632
+
633
+ - [index.html](/D:/MyDocuments/UnityProjects/SVGAPlayer-Unity/gift-stage/index.html)
634
+
635
+ 可用于验证:
636
+
637
+ - `SVGA / VAP / AlphaVideo` 播放
638
+ - slot 替换
639
+ - `WebGPU / WebGL2 / WebGL1` 切换
640
+ - 大批同屏压测
641
+
642
+ ## 相关文档
643
+
644
+ - 优化汇总:
645
+ [docs/giftstage-optimization-summary.md](/D:/MyDocuments/UnityProjects/SVGAPlayer-Unity/gift-stage/docs/giftstage-optimization-summary.md)