@taole/giftstage
v0.1.36
Published
High-performance WebGPU/WebGL gift animation player unifying SVGA, VAP, AlphaVideo, and images
Downloads
2,415
Readme
GiftStage
统一的 Web 礼物播放框架,支持:
SVGAVAP / VAPX- 透明视频
AlphaVideo - 静态图片
Image
底层支持:
WebGPUWebGL2WebGL1兼容兜底
适用场景:
- 直播间礼物
- 大批同屏动画
- 需要统一接入
SVGA / VAP / 透明视频 / 图片的业务场景
npm 包信息
- 包名:
@taole/giftstage - npm:
npm i @taole/giftstage - ESM 导入:
import { GiftStage } from '@taole/giftstage';- CommonJS 导入:
const { GiftStage } = require('@taole/giftstage');特性
- 统一 API:
addGift()即可播放四类礼物 SVGA支持 Worker 解析、atlas 缓存、slot 替换VAP / AlphaVideo支持实验性的WebCodecs和稳定的HTMLVideoElement双路径- 同源视频播放实例、纹理、资源复用
- 内存 LRU 缓存,默认
128MB IndexedDB磁盘 LRU 缓存,默认2048MBx / y支持百分比定位width / height支持按礼物原始尺寸自动补全- 四类礼物共用位移、缩放、透明度、旋转、层级和销毁生命周期
安装
npm install安装发布包:
npm i @taole/giftstage开发
npm run dev构建
npm run buildDemo 构建:
npm run build:demo性能基线
# 在当前机器记录 WebGL2 / WebGPU 基线
npm run perf:record
# 与同浏览器主版本、硬件并发数、DPR 和视口的基线比较
npm run test:perf基线覆盖冷/热启动、100 个静态图片、50 个共享 SVGA、20 个共享 VAP 和 20 个共享 AlphaVideo,
记录首帧、P50/P95 帧间隔、长任务、堆变化、提交帧、draw、纹理上传与 stencil 帧数。
基线保存在 tests/performance/baselines/;Windows 会自动探测系统 Chrome,也可通过
PLAYWRIGHT_CHROME_EXECUTABLE_PATH 指定浏览器。
快速开始
import { GiftStage } from '@taole/giftstage';
const container = document.getElementById('app')!;
const stage = new GiftStage({
container,
preferWebGPU: true,
});
await stage.ready;
await stage.addGift({
type: 'svga',
source: 'https://example.com/demo.svga',
x: 100,
y: 100,
loop: 0,
});核心 API
new GiftStage(options)
创建礼物舞台。
常用配置:
container: HTMLElement挂载容器。resolution?: number指定 canvas backing store 分辨率倍率。默认跟随devicePixelRatio。antialias?: boolean是否开启抗锯齿,WebGL 和 WebGPU 均默认开启;WebGPU 使用 4× MSAA。大量透明视频同时播放时可显式设为false。preferWebGPU?: boolean是否优先使用WebGPU。forceWebGL1?: boolean强制走WebGL1,用于兼容性验证。preferWebCodecs?: boolean实验性特性。默认关闭;显式传true时才会尝试用WebCodecs播放VAP / AlphaVideo。shareIdenticalVideoPlayback?: boolean是否复用近同时起播的同源视频播放实例。默认开启。sharedVideoPlaybackWindowMs?: number同源视频共享播放窗口,默认100ms。webCodecsVideoAtlas?: { width: number; height: number }实验性WebCodecs视频共享 atlas 配置。webCodecsDecodeInWorker?: boolean实验性WebCodecs选项,控制是否在 Worker 中执行解码。webCodecsMaxDecodedFrames?: number实验性WebCodecs选项,控制保留的最大解码帧数。webCodecsDecodeAheadFrames?: number实验性WebCodecs选项,控制前向解码帧数。svgaParseInWorker?: boolean是否在 Worker 中解析 SVGA。maxConcurrentParse?: number资源下载 / 解析 / 图像解码的并发数。 不传时自动跟随navigator.hardwareConcurrency。svgaAtlasFrameBudgetMs?: numberSVGA atlas 在主线程组合或逐页上传时的单帧时间片上限,默认12ms。 实际时间片会按当前刷新周期的一半动态调整,避免高刷新率设备上的加载任务挤占渲染帧。svgaLoadFrameBudgetMs?: numberSVGA 主线程加载任务的共享时间片上限,默认12ms。图片 fallback、atlas、clip、音频、slot 纹理和二进制缓存共用同一帧预算。svgaWorkerFrameBudgetMs?: numberSVGA Worker 连续执行 JS 循环的时间片上限,默认4ms,范围1-8ms。单次 WASM protobuf 调用和单次原生 API 调用仍不可抢占。svgaWorkerTaskConcurrency?: numberparser、图片解码、atlas 和 clip 等 CPU 密集 Worker 的全局并发上限,默认1。Atlas 首帧页面可提前返回,但后台 PNG 编码完成前仍持有该名额,避免与其他重载 Worker 叠加。svgaWorkerImageDecodeConcurrency?: number图片解码 Worker 内createImageBitmap并发上限,默认2。运行时可按批次耗时收缩并发,负载恢复后回升,但不会超过该上限。svgaParseProfile?: boolean是否输出[GiftStage:SVGA:Profile]结构化解析日志,默认false。日志包含DecompressionStream格式尝试、输入/输出 chunk、解压耗时以及 protobuf/payload 构建耗时;WASM payload 路径还会拆分prostDecodeMs、metadataAndImagesMs、frameTableMs、finalizeMs和 WASM 边界复制耗时。缓存命中不输出。
SVGA 礼物在加载阶段被 destroy() / removeGift() 时,会取消排队任务并终止正在执行的 parser、图片解码、atlas 或 clip Worker。相同 URL 的共享加载按消费者计数,单个礼物取消不会中断其他仍在等待的礼物。
onError?: (error: Error) => void统一错误回调。
await stage.ready
等待底层后端和运行环境初始化完成。建议在第一次 addGift() 前等待。
stage.addGift(options)
插入一个礼物,返回:
Promise<GiftHandle>;GiftHandle API
addGift() 返回的句柄可用于控制单个礼物:
gift.idgift.typegift.pause()gift.resume()- 暂停期间礼物播放时钟、图片
duration和后置动画都会冻结
- 暂停期间礼物播放时钟、图片
gift.destroy()- 立即移除当前礼物
gift.animate(stepOrSteps?)- 创建链式动画并返回
GiftAnimationChain
- 创建链式动画并返回
GiftAnimationChain 支持:
.to(step)/.then(step)- 追加动画步骤(两者等价)
.delay(milliseconds)- 在链中插入等待,不改变位置、缩放和透明度
.onComplete((gift) => void)- 整条链执行完成后的回调
.start(mode?)mode: 'immediate' | 'afterGiftComplete'immediate:立即开始(默认)afterGiftComplete:等待礼物主播放结束后再执行链
.cancel()- 取消当前礼物正在执行的链式动画
stage.removeGift(id)
按 ID 移除礼物。
stage.removeAll()
移除全部礼物。
stage.pause()
暂停舞台。
stage.resume()
恢复舞台。
stage.destroy()
销毁舞台并释放资源。
addGift() 参数
通用参数
type: 'svga' | 'vap' | 'alphaVideo' | 'image'source: string | ArrayBufferx: number | \${number}%``y: number | \${number}%``zIndex?: numberwidth?: numberheight?: numberuseOriginalSize?: booleanobjectFit?: 'contain' | 'cover'loop?: number0表示无限循环- VAP / AlphaVideo 的 HTMLVideo 回退路径会为有限循环设置“媒体总时长 + 容错窗口”的超时保护;即使浏览器未触发
ended,也会进入正常完成和清理流程
opacity?: number- 全局透明度,范围
0 ~ 1,默认1
- 全局透明度,范围
rotation?: number- 初始旋转弧度;屏幕坐标中正值为顺时针
transformOrigin?: { x: number | \${number}%`; y: number | `${number}%` }`- 相对未缩放礼物框左上角,默认
50% / 50% - 数值使用 CSS 像素;允许负值或超过礼物宽高,用于绕框外点公转
- 相对未缩放礼物框左上角,默认
videoRGBAlphaMode?: 'straight' | 'premultiplied'- 仅用于
vap/alphaVideo分离 Alpha 视频源,默认premultiplied straight:RGB 区域尚未乘独立 Alpha,播放器负责预乘premultiplied:RGB 区域在制作阶段已经乘过独立 Alpha,播放器不会再次相乘- 该参数只描述输入视频;最终画布仍统一输出预乘 Alpha
- 仅用于
clearsAfterStop?: boolean- 播放结束后,是否自动移除礼物
- 默认
true,传false时会停留最后一帧,方便后续继续调用gift.animate(...).start()
mute?: booleanVAP / AlphaVideo传false时使用HTMLVideoElement播放声音;该礼物不会进入仅解码视频帧的 WebCodecs 路径- 如果浏览器阻止有声自动播放,会自动回退静音播放,保证画面正常完成
onFrame?: (frame, gift) => void- 仅用于 SVGA;画面帧推进时触发
- 性能不足时可能跨帧,业务应使用
frame >= target的阈值判断
onComplete?: (gift) => void
图片专用参数:
duration?: number- 单位毫秒;正数到期后触发
onComplete,并遵循clearsAfterStop - 不传或传非正数时持续显示到
destroy();加载完成后afterGiftComplete可立即继续,但不会自动触发onComplete image忽略loop和mute
- 单位毫秒;正数到期后触发
层级规则:
zIndex在SVGA / VAP / AlphaVideo / Image四种礼物间通用- 数值越大,渲染越靠上
- 同层级下,后
addGift()的礼物会覆盖先添加的礼物
宽高默认行为
width / height 现在是可选参数,规则如下:
- 两个都传:按传入值显示
- 只传
width:height按礼物原始宽高比自动补全 - 只传
height:width按礼物原始宽高比自动补全 - 两个都不传:默认按当前画布尺寸做等比
contain - 如果
objectFit: 'cover'且两个都不传:按整个画布作为显示区域做等比cover - 也就是会在画布内尽可能放大或缩小,并保持礼物原始宽高比
- 如果
useOriginalSize: true,且礼物原始尺寸本身小于画布,则优先使用礼物原始尺寸 - 如果
useOriginalSize: true,但礼物原始尺寸超出画布,则仍会按画布尺寸等比缩小
例如:
await stage.addGift({
type: 'svga',
source: 'https://example.com/demo.svga',
x: '50%',
y: '50%',
loop: 0,
});这时会按舞台尺寸做等比适配,并保持礼物原始宽高比。
如果希望礼物铺满整个画布,并按中心裁剪超出的部分,可以使用 cover:
await stage.addGift({
type: 'svga',
source: 'https://example.com/demo.svga',
x: '50%',
y: '50%',
objectFit: 'cover',
loop: 0,
});当 x: '50%'、y: '50%' 且未传 width / height 时,显示区域会居中放在整个画布上;cover 会保持礼物原始宽高比铺满该区域,并从中心裁剪溢出的部分。
如果你希望“小礼物保持原始尺寸,大礼物再缩小”,可以这样:
await stage.addGift({
type: 'svga',
source: 'https://example.com/demo.svga',
x: '50%',
y: '50%',
useOriginalSize: true,
loop: 0,
});百分比坐标
x / y 支持百分比,例如:
await stage.addGift({
type: 'svga',
source: 'https://example.com/demo.svga',
x: '50%',
y: '50%',
width: 300,
height: 300,
loop: 0,
});语义是:
x: '50%'按容器宽度的50%定位,并减去自身一半宽度y: '50%'按容器高度的50%定位,并减去自身一半高度
也就是接近:
left: 50%;
top: 50%;
transform: translate(-50%, -50%);如果是数值,则继续按原来的像素坐标语义处理。
四类礼物示例
播放 SVGA
await stage.addGift({
type: 'svga',
source: 'https://example.com/demo.svga',
x: 20,
y: 20,
width: 300,
height: 300,
loop: 1,
});播放 VAP
await stage.addGift({
type: 'vap',
source: 'https://example.com/demo.mp4',
config: 'https://example.com/demo.json',
x: 20,
y: 20,
width: 400,
height: 220,
// 默认 premultiplied;RGB 尚未乘独立 Alpha 的视频源需显式传 straight
videoRGBAlphaMode: 'premultiplied',
loop: 0,
});播放透明视频
await stage.addGift({
type: 'alphaVideo',
source: 'https://example.com/demo.mp4',
x: 20,
y: 20,
width: 400,
height: 220,
// 默认 premultiplied;未预乘的视频源改为 straight
videoRGBAlphaMode: 'premultiplied',
loop: 0,
});播放图片
source 首版支持 URL 和 ArrayBuffer。URL 图片会按地址共享 GPU 纹理并引用计数;图片按静态帧处理,不保证 GIF / WebP 动画播放。跨域 URL 必须允许匿名 CORS 读取。
const avatar = await stage.addGift({
type: 'image',
source: 'https://example.com/avatar.png',
x: '50%',
y: '50%',
width: 100,
height: 100,
objectFit: 'cover',
duration: 5000,
});动画控制
链式动画
addGift() 返回的 GiftHandle 支持 animate()。一次 to() 是组合动画,同一步里可以同时移动、缩放、改变透明度和旋转;多个 to() / then() 会按顺序播放。rotationTo 直接按数值线性插值,不做最短角度归一化,因此可以明确表达多圈旋转:
const gift = await stage.addGift({
type: 'svga',
source: 'https://example.com/demo.svga',
x: '50%',
y: '50%',
width: 300,
height: 300,
loop: 0,
});
await gift
.animate()
.to({
flyTo: { x: 200, y: 240 },
scaleTo: 0.8,
opacity: 0.6,
rotationTo: Math.PI * 4,
duration: 500,
})
.then({
flyTo: { x: 600, y: 320 },
scaleTo: 1.1,
opacity: 1,
duration: 700,
})
.delay(300)
.then({
opacity: 0,
duration: 400,
})
.onComplete((g) => {
console.log('chain complete', g.id);
})
.start();绕外部中心旋转
下面的礼物初始位于舞台中心右侧 200px,旋转中心通过框外坐标指回舞台中心:
const orbitGift = await stage.addGift({
type: 'image',
source: 'https://example.com/gift.png',
x: container.clientWidth / 2 + 150,
y: '50%',
width: 100,
height: 100,
transformOrigin: { x: -150, y: '50%' },
});
await orbitGift
.animate({ rotationTo: Math.PI * 2, duration: 1200 })
.start();也可以传入单步或数组:
gift.animate({ flyTo: { x: 300, y: 300 }, scaleTo: 0.5, opacity: 0.3 }).start();
gift
.animate([
{ flyTo: { x: 300, y: 300 }, duration: 400 },
{ scaleTo: 1, opacity: 1, duration: 300 },
])
.start();Slot 替换
SVGA
使用 svgaSlots:
await stage.addGift({
type: 'svga',
source: 'https://example.com/demo.svga',
x: 0,
y: 0,
width: 400,
height: 400,
svgaSlots: {
avatar: { image: avatarImage },
title: {
text: 'Hello',
color: '#ff0000',
fontSize: 28,
mode: 'dynamic',
},
},
});支持:
TexImageSource- 文本配置
- 图片配置
SVGA 文本配置会按目标 frame 自动适配字号,避免文字超出槽位:
mode: 'dynamic':默认行为。生成文字自身尺寸的纹理,渲染时按自身逻辑尺寸居中到 frame 内,不会被拉伸。mode: 'replace':生成和 frame 一样大的纹理,按替换图逻辑铺满 frame。fontSize?: number:期望字号;如果文字放不下,会自动缩小。minFontSize?: number/maxFontSize?: number:限制自适应字号范围。padding?: number:文字纹理内边距,默认2。scale?: number:文字纹理栅格倍率,SVGA 默认3,用于保持清晰度。fontStyle?: string | { font?: string; color?: string }:字体样式,SVGA / VAP 都支持。
fontStyle 可以直接传 canvas font 字符串:
svgaSlots: {
title: {
text: 'Hello',
fontStyle: 'bold 40px Arial',
},
}也可以传对象:
vapSlots: {
welcome01: {
text: '欢迎进入房间',
fontStyle: {
font: 'bold 40px Arial',
color: '#ffffff',
},
},
}对象形式目前生效字段是 font 和 color。如果同时传了外层 color 和 fontStyle.color,以 fontStyle.color 为准。VAP 文本如果没有传 fontStyle,会回退使用 VAP 配置里的 src.fontStyle。
VAP / VAPX
使用 vapSlots:
await stage.addGift({
type: 'vap',
source: 'https://example.com/demo.mp4',
config: 'https://example.com/demo.json',
x: 0,
y: 0,
width: 400,
height: 400,
vapSlots: {
welcome01: '欢迎进入房间',
avatar_left: 'https://example.com/avatar.png',
},
});支持:
- 文本字符串
- 图片 URL
TexImageSource- 结构化文本 / 图片对象
后端策略
默认回退顺序:
WebGPUWebGL2WebGL1
说明:
WebGPU仅在浏览器支持相关必要能力时启用WebGL1主要用于兼容兜底,不以性能最优为目标- demo 中可手动强制切换到
WebGL1
缓存策略
内存缓存
- 默认
128MB - LRU 淘汰
缓存内容包括:
- 原始资源字节
- 文本 / JSON
SVGAworker payloadSVGAatlas 资产
IndexedDB 磁盘缓存
- 默认
2048MB - LRU 淘汰
- 读取命中会刷新最近访问时间
- 超出预算时按最久未使用记录淘汰
异常情况处理:
IndexedDB不可用、事务失败、写入失败、容量不足时,会自动降级- 损坏的
SVGA磁盘缓存会自动删除并重新生成
项目结构
主要目录:
src/核心源码docs/设计与汇总文档publish/发布脚本static/demo 依赖资源tests/测试
对外导出
入口文件:
主要导出:
GiftStagecreateBackendWebGPUBackendWebGL2BackendWebGL1BackendparseSVGAbuildAtlasparseVAPConfig- 相关类型定义
Demo
Demo 入口:
可用于验证:
SVGA / VAP / AlphaVideo播放- slot 替换
WebGPU / WebGL2 / WebGL1切换- 大批同屏压测
