npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@taole/giftstage

v0.3.6

Published

High-performance WebGPU/WebGL gift animation player unifying SVGA, VAP, AlphaVideo, and images

Readme

GiftStage

统一的 Web 礼物播放框架,支持:

  • SVGA
  • VAP / VAPX
  • 透明视频 AlphaVideo
  • 静态图片 Image

底层支持:

  • WebGPU
  • WebGL2
  • WebGL1 兼容兜底

适用场景:

  • 直播间礼物
  • 大批同屏动画
  • 需要统一接入 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 缓存,默认 2048MB
  • x / y 支持百分比定位
  • width / height 支持按礼物原始尺寸自动补全
  • 四类礼物共用位移、缩放、透明度、旋转、层级和销毁生命周期

安装

npm install

安装发布包:

npm i @taole/giftstage

开发

npm run dev

构建

npm run build

Demo 构建:

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-float ballistic-v1 ABI,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?: number SVGA atlas 在主线程组合或逐页上传时的单帧时间片上限,默认 12ms。 实际时间片会按当前刷新周期的一半动态调整,避免高刷新率设备上的加载任务挤占渲染帧。

  • svgaLoadFrameBudgetMs?: number SVGA 主线程加载任务的共享时间片上限,默认 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?: number SVGA Worker 连续执行 JS 循环的时间片上限,默认 4ms,范围 1-8ms。单次 WASM protobuf 调用和单次原生 API 调用仍不可抢占。

  • svgaWorkerTaskConcurrency?: number parser、图片解码、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.id
  • gift.type
  • gift.pause()
  • gift.resume()
    • 暂停期间礼物播放时钟、图片 duration 和后置动画都会冻结
  • gift.destroy()
    • 立即移除当前礼物
  • gift.getRenderInfo()
    • 返回 SVGA 的实时路径诊断;非 SVGA 或尚未形成资源计划时返回 null。pathState 为 preparing | settled | fallback,drawBatchMode 为 multi-slot | per-slot | hybrid-scheduled,可直接区分资源共享与实际 Draw 合批状态。
  • 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 | ArrayBuffer
  • x: number | \${number}%``
  • y: number | \${number}%``
  • zIndex?: number
  • width?: number
  • height?: number
  • useOriginalSize?: boolean
  • objectFit?: 'contain' | 'cover'
  • loop?: number
    • 0 表示无限循环
    • 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?: boolean
    • VAP / 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
  • 结构化文本 / 图片对象

后端策略

默认回退顺序:

  1. WebGPU
  2. WebGL2
  3. WebGL1

说明:

  • WebGPU 仅在浏览器支持相关必要能力时启用
  • WebGL1 主要用于兼容兜底,不以性能最优为目标
  • demo 中可手动强制切换到 WebGL1

缓存策略

内存缓存

  • 默认 128MB
  • LRU 淘汰

缓存内容包括:

  • 原始资源字节
  • 文本 / JSON
  • SVGA worker payload
  • SVGA atlas 资产

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/ 测试

对外导出

入口文件:

主要导出:

  • GiftStage
  • createBackend
  • WebGPUBackend
  • WebGL2Backend
  • WebGL1Backend
  • parseSVGA
  • buildAtlas
  • parseVAPConfig
  • 相关类型定义

第三方 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 切换
  • 大批同屏压测

相关文档