@taole/giftstage
v0.3.6
Published
High-performance WebGPU/WebGL gift animation player unifying SVGA, VAP, AlphaVideo, and images
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 个静态图片、SVGA Direct 的 1/2/10/50 个同资源实例、
复杂 Clip/Shape Hybrid 的 Auto/CPU 对照、20 个共享 VAP 和 20 个共享 AlphaVideo。
除首帧、P50/P95/P99、长任务、堆变化、draw/Stencil 外,还记录 SVGA CPU Sprite
展开数、GPU 帧表上传、每帧动态上传字节、路径晋升与回退。可用
npm run test:perf -- --project=chromium-webgl2 --scenarios=<逗号分隔场景> 做聚焦复测。
基线保存在 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,
});插件运行时(0.2)
聊天室只需要维护一个全屏 GiftStage。业务编排包通过 stage.mount(plugin) 接入,插件创建的礼物、轨道和粒子都归属于独立 PlaybackScope;销毁插件或作用域不会调用 removeAll(),也不会影响舞台上的其他礼物。
import createMultiGiftPlugin from '@taole/giftstage-plugin-multi-gift';
const mounted = await stage.mount(createMultiGiftPlugin({ playConfig }));
await mounted.api.preload();
const playback = await mounted.api.play({ target: null });
playback.pause();
playback.resume();
playback.seek(1200);
await playback.completion;
mounted.destroy();插件宿主能力包括:
preload():复用 GiftStage 的 SVGA/Image 缓存并返回引用计数资源租约。createPlaybackScope():使用 GiftStage 唯一 RAF、逻辑时钟和作用域清理;SVGA、WebCodecs、HTMLVideo 回退与定时图片均按 Scope 绝对时间定位,不叠加 RenderManager 的全局dt。SVGA 的可见帧、onFrame与嵌入音频由同一个绝对时间入口同步,跨帧、seek、循环和暂停恢复会重建正确音频偏移,且start()前不会触发首帧音频/回调。HTMLVideo 在 Scope 内保持暂停并使用独立实例,显式暂停和页面隐藏期间逻辑时间冻结。createMotionTrack():接收 8-float TypedArray/SoA 父级轨道。WebGPU 可把它与核心 SVGA 帧表在同一顶点着色器中合成;WebGL2 的 SVGA 内部帧仍可走 GPU Direct/Hybrid,但父级 MotionTrack 继续使用确定性 CPU reference sampler。图片与 WebGL1 同样使用 CPU reference。createParticleBatch():保留 16-floatballistic-v1ABI,WebGPU/WebGL2 使用持久实例缓冲和 GPU 弹道求值;缺省仍在媒体之后合成。host.createParticleAtlas()+scope.createParticleBatchGroup():可选sprite-v2能力。插件上传一次不可变预乘 RGBA8 atlas,再以原子 group 发布多个 24-float sprite batch。group 任一 draw 失败会 abort 整帧并以同一权威时间重放,不会显示 smoke-only 等局部结果。- 粒子
compositeLayer支持behind-media/above-media;正式帧顺序为begin → behind particles → media → above particles → end,zIndex只在同一 phase 内排序。WebGL1 明确不可用,不提供 Canvas2D 粒子后端。 stage.capabilities:插件可在播放前判断实际后端、GPU 轨道媒体范围和粒子能力。
ParticleBatch v2 迁移
stage.capabilities.particles.formats 包含 sprite-v2,且同时具备 spriteAtlas、compositeLayers、batchEnvelope、atomicBatchGroup 和 maxInstancesPerBatch 时,插件才能启用纹理粒子;不得只按 apiVersion 判断。旧核心缺少这些可选字段时继续使用原有 v1 或明确跳过效果。
sprite-v2 每实例为 24 floats:0–15 完全延续 birth/lifetime/弹道/scale/rotation/RGBA,16–17 是 CSS 逻辑宽高,18–19 是归一化 anchor,20–23 是 atlas UV。header 为 8 floats:秒制时间、已求值 opacity、已求值 size、fade mode、sx/sy 与两个零 padding。WebGL2 的实例 stride 为 96 bytes、header 为 32 bytes;WebGPU 从 8 + instanceIndex × 24 读取。非均匀 backing scale 只在最终顶点应用,不能再用 sqrt(sx × sy) 近似 v2 尺寸。
atlas descriptor 必须提供 width × height × 4 的不可变 Uint8Array,声明 colorSpace:'srgb' 与 alphaMode:'premultiplied'。透明像素 RGB 应为零;shader 不会再次乘采样 alpha。ParticleAtlasHandle.destroy() 只释放调用方 lease,存活 group 会保留内部引用;context/device loss 后旧 handle 的 lost 为 true,不能在新设备上复活。
await host.createParticleAtlas() 同时等待 sprite-v2 管线准备完成,建议放在插件预加载阶段、scope.start() 之前。WebGPU 使用异步编译并缓存普通/叠加混合及有/无 stencil 的四种管线;WebGL2 提前编译并链接粒子程序。预加载不会绘制舞台或推进时钟,重复创建 atlas 会复用管线;准备失败时 atlas 创建会拒绝,不发布半就绪资源。此优化移除粒子管线首次编译造成的播放期停顿,不改变动画时长,也不保证其他素材或驱动工作完全没有长帧。
envelope 使用绝对毫秒半开区间 [startMs,endMs),支持 linear / hold,边界右连续,因此可以表达中心闪光的瞬时跳变。setTime() 仅更新每 batch header,实例 buffer 创建后保持不变;pause 不产生写入,seek 直接求值绝对时间。
核心 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 纹理、二进制缓存和 Hybrid 命令表构建共用同一帧预算。 Hybrid 的路径解析、裁剪检查、逐帧计划和命令打包会分片执行;同一资源的重复裁剪路径复用分类和矩形结果,避免视频与复杂 SVGA 同播时被同步准备工作阻塞。资源释放或后端丢失后会取消未完成的构建。svgaCommandTableInWorker?: boolean默认true。后台 Worker 构建 GPU 命令表并打包帧数据;关闭时使用主线程共享帧预算分片。CPU 模式跳过 GPU 帧表准备。Worker 不可用、异常或结果校验失败时,每个资源最多回退一次,取消不触发回退。 输入按不超过 64 KiB 的片段复制,只转移专用副本;CPU 回退持有的原始数组保持有效。GPU 资源仍在主线程创建,并在发布前检查资源生命周期。onSVGAPreparationProfile?: (profile: SVGAPreparationProfile) => void可选的 GPU 资源准备汇总回调;未配置时不进行详细采样。每个准备任务结束时回调一次,包括实际executor、回退原因、完成/取消/失败状态和各阶段数据。mainThreadExecutionMs与workerExecutionMs是实际执行片段累计;elapsedMs包含调度等待,不能当作 CPU 计算时间。各阶段可能重叠,不能相加作为总准备时长。 阶段包括裁剪准备、Worker 排队、输入复制、命令构建、帧表打包、校验和 GPU 提交,另含最大连续片段、命中次数与字节数。GPU 提交时间是主线程调用耗时,并非 GPU 完成时间;网络/解析等待不属于此准备汇总。svgaWorkerFrameBudgetMs?: numberSVGA Worker 连续执行 JS 循环的时间片上限,默认4ms,范围1-8ms。单次 WASM protobuf 调用和单次原生 API 调用仍不可抢占。svgaWorkerTaskConcurrency?: numberparser、图片解码、atlas、clip 和命令表 Worker 的全局并发上限,默认1。命令任务每个时间片重新排队,前台解析/首帧优先,同级 FIFO;连续 8 次前台许可后允许一个后台片段。Atlas 首帧页面可提前返回,但后台 PNG 编码完成前仍持有该名额,避免与其他重载 Worker 叠加。svgaWorkerImageDecodeConcurrency?: number图片解码 Worker 内createImageBitmap并发上限,默认2。运行时可按批次耗时收缩并发,负载恢复后回升,但不会超过该上限。svgaFrameEvaluation?: 'auto' | 'cpu'SVGA 帧动画求值策略,默认auto。auto按后端能力、表大小和资源计划选择gpu-direct、gpu-hybrid或cpu-reference;cpu强制使用 CPU reference。普通单礼物和同资源多实例都经过同一策略。svgaGpuFrameTableBudgetBytes?: number所有活动 SVGA GPU 帧表、静态 Sprite metadata 和 Hybrid Command Table 共享的全局预算,默认32 MiB。预算不足时仅当前资源回退到cpu-reference,不会影响已存在的其他礼物。svgaParseProfile?: boolean是否输出[GiftStage:SVGA:Profile]结构化解析日志,默认false。日志包含DecompressionStream格式尝试、输入/输出 chunk、解压耗时以及 protobuf/payload 构建耗时;WASM payload 路径还会拆分prostDecodeMs、metadataAndImagesMs、frameTableMs、finalizeMs和 WASM 边界复制耗时。缓存命中不输出。
SVGA 礼物在加载阶段被 destroy() / removeGift() 时会取消相应订阅。相同 URL 的共享加载按消费者计数;裁剪生产和命令表属于资源,资源释放或 Stage 销毁才取消生产。命令任务使用 ID/代次隔离,取消一个资源不会终止其他资源的 Worker。
同一个 Stage 的裁剪预热、渲染和命令构建共用分类/矩形分析;重复路径只保留小型分析结果,网格缓存与磁盘格式不变。首帧只等待该帧裁剪与纹理,其余准备推迟到首帧绘制后。渲染循环不再同步解析未准备好的裁剪路径,继续按整帧等待处理。
播放资源的取消状态与缓存中的解析表分开管理。播放结束后可在同一 Stage 再次播放并复用 spriteTable;多个存活资源共享同一张表时,释放一个资源不会取消其他资源的裁剪准备。已释放资源的迟到任务不会恢复生产或覆盖新资源。
onError?: (error: Error) => void统一错误回调。onSVGARenderPathChange?: (info) => void当 SVGA 礼物完成资格分析、帧表上传、原子晋升或发生回退时通知路径变化。除资源与路径字段外,info还包含pathState、frameTableBytes、commandTableBytes、drawBatchMode和drawCompatibleInstanceCount。onRuntimeDiagnostic?: (event) => void接收 frame abort、replay failure、无时钟 retry、frame controller failure、运行期 fallback、Clip 协议/网格异常、Stencil 事务失败、Atlas 命令无效、Particle group 原子失败、Backend 操作拒绝/隔离、WebGPU uncaptured error、backend loss 和 observer error 等结构化事件。事件只在本地回调,不会由 GiftStage 上传;回调异常会被隔离,不能中断渲染降级事务。
await stage.ready
等待底层后端和运行环境初始化完成。建议在第一次 addGift() 前等待。
stage.addGift(options)
插入一个礼物,返回:
Promise<GiftHandle>;stage.getDiagnostics()
返回当前 Stage 生命周期内的累计运行诊断快照,包括 frame abort/replay/retry、FastPath fallback、native submitted/rejected draw、WebGPU uncaptured error、backend loss、observer error 以及最后一个结构化事件。该快照不会被性能测试采样窗口重置,返回对象可安全交给业务遥测系统;GiftStage 自身不上传数据。
GiftHandle API
addGift() 返回的句柄可用于控制单个礼物:
gift.idgift.typegift.pause()gift.resume()- 暂停期间礼物播放时钟、图片
duration和后置动画都会冻结
- 暂停期间礼物播放时钟、图片
gift.destroy()- 立即移除当前礼物
gift.getRenderInfo()- 返回 SVGA 的实时路径诊断;非 SVGA 或尚未形成资源计划时返回
null。pathState为preparing | settled | fallback,drawBatchMode为multi-slot | per-slot | hybrid-scheduled,可直接区分资源共享与实际 Draw 合批状态。
- 返回 SVGA 的实时路径诊断;非 SVGA 或尚未形成资源计划时返回
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()
销毁舞台并释放资源。
SVGA 帧动画路径
SVGA 资源的 9-float 帧行会在准备阶段打包为 3 个 vec4。WebGPU 使用持久 storage buffer,WebGL2 使用 RGBA32F + NEAREST + texelFetch 数据纹理。CPU 始终权威维护帧号、循环、暂停、Seek、音频与回调;GPU 根据该帧号读取子 Sprite 矩阵、透明度和可见性。
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,也不会触发资源回退。
gpu-direct:普通 atlas sprite 的帧变换、透明度和布局由 GPU 求值。gpu-hybrid:Shape/Clip 的精确 paint order 与 Draw/Stencil 提交由 CPU 按不可变命令表调度,Texture、Shape、Clip Mask 的动画属性都由 GPU 帧表求值。cpu-reference:WebGL1、动态svgaSlots、表校验/上传失败、预算不足或策略为cpu时使用确定性的 CPU 路径。
插件可通过 stage.capabilities.svgaFrameTimeline 判断 transport(storage-buffer、float-texture 或 unavailable)、正式 direct/hybrid 能力、supportedPaths 和 maxTableBytes,不需要启用 experimental policy。同一解析资源的多个实例共享帧表、静态 metadata 和 Hybrid Command Table;compatibleInstanceCount 表示资源共享数,实际本帧合批数量由 drawCompatibleInstanceCount 报告。
addGift() 不等待 GPU 帧表:非预加载礼物先显示 CPU reference 首帧,再在完整帧边界原子晋升。preload('svga') 会等待同一资源级帧表与调度准备完成。FastPath 提交若被后端拒绝,会丢弃未提交帧、同步降级该资源,并在同一 RAF 以原顺序完整重放一次;重放再次失败时 WebGL2 会清成透明帧,并在下一 RAF 不推进 CPU 权威时间轴地重试。不支持该帧事务的自定义 Backend 会直接使用 CPU Reference。WebGL1、动态 svgaSlots、能力/尺寸/预算校验失败和上传失败均按资源缓存回退,不逐帧重试;Stage 与单礼物都可用 svgaFrameEvaluation: 'cpu' 强制关闭。
性能诊断中的 submittedDrawCalls 仅表示命令已经到达 WebGL drawElements* 或 WebGPU drawIndexed 调用/编码;它不表示 GPU 已经执行完成。rejectedDrawCalls 表示在原生提交前被后端校验或容量检查拒绝。GPU error scope、pipeline validation 与 framebuffer 像素门禁用于补充验证执行结果。
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磁盘缓存会自动删除并重新生成
SVGA 半透明边缘与 Safari 首播
Worker 首次合成图集和缓存 PNG 解码都显式使用 premultiplyAlpha: 'premultiply',
上传时声明 sourcePremultiplied: true。不要改回直接使用 transferToImageBitmap():
WebKit 的该路径可能丢失预乘标记,而 WebGL 对 ImageBitmap 会忽略上传时的预乘开关,
导致无缓存首播的半透明边缘发白、过亮,缓存播放却正常。
运行 node scripts/verify-svga-atlas-alpha.mjs 可在 Chromium 中检查 WebGL1/2 的
Worker 首次合成、PNG 缓存解码及主线程回退像素;加 --webkit 可使用已安装的
Playwright WebKit。该检查不替代真实 iPhone / Mac Safari 的素材回归。
项目结构
主要目录:
src/核心源码docs/设计与汇总文档publish/发布脚本static/demo 依赖资源tests/测试
对外导出
入口文件:
主要导出:
GiftStagecreateBackendWebGPUBackendWebGL2BackendWebGL1BackendparseSVGAbuildAtlasparseVAPConfig- 相关类型定义
第三方 GPUBackend 迁移
GPUBackend 的帧与提交操作使用显式结果契约。第三方实现必须让 beginFrame()、draw(command)、endFrame() 和 abortFrame() 返回:
{ submitted: true }
// 或
{ submitted: false, reason, message? }稳定的 reason 为 backend-lost | invalid-command | missing-resource | capacity | validation-error | submission-error。只有真正到达原生 drawElements / drawIndexed 或成功完成对应帧操作时才能返回 submitted: true;不得再实现或依赖 wasLastDrawSubmitted(),也不得用性能计数差值推断提交结果。abortFrame() 拒绝表示无法保证失败帧被丢弃,Stage 会立即隔离该 Backend。
Demo
Demo 入口:
可用于验证:
SVGA / VAP / AlphaVideo播放- slot 替换
WebGPU / WebGL2 / WebGL1切换- 大批同屏压测
