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

custom-svga

v1.0.11

Published

A lightweight, tree-shakable SVGA animation player for Vue 3 & modern web. Supports dynamic imports, OffscreenCanvas + Web Worker, and in-flight sprite/text replacement. ~30KB gzipped, zero deps on UI frameworks.(面向 Vue3 与现代浏览器的轻量 SVGA 动画播放器。支持动态导入懒加载、Off

Readme

🎬 Custom SVGA

A lightweight, tree-shakable SVGA animation player for Vue 3 & modern web.

支持按需懒加载 · OffscreenCanvas 离屏渲染 · 动态精灵/文本替换 · TypeScript 全量类型

npm version npm downloads bundle size license TypeScript


✨ Features

| 特性 | 说明 | | --- | --- | | 📦 极致轻量 | gzip 后核心 ~30KB,无任何 UI 框架依赖 | | 🌳 Tree-shakable | 完整 ESM 支持,按需引入方法,未使用代码自动剔除 | | 🚀 按需懒加载 | Parser / ShapeRenderer / Loader 全部支持动态 import(),首屏最小化 | | ⚡ OffscreenCanvas 渲染 | 主流渲染管线 0 主线程占用,60fps 不卡顿 | | 👷 共享 Web Worker | 多 Player 实例复用同一 Worker,解析结果缓存共享 | | 🧠 LRU 智能缓存 | 自动管理解析后的 MovieData,避免重复解析 | | 🎨 动态形状渲染 | 内置 Path2D 矢量绘制,支持 rect / ellipse / SVG path | | 🖼️ 运行时图片替换 | 不重编 SVGA 即可替换图层(URL / Base64 / ImageBitmap) | | 📝 动态文本注入 | 在动画播放中实时替换文本图层 | | 🎯 TypeScript 一等公民 | 完整 .d.ts 类型定义,IDE 智能提示开箱即用 | | 🌐 多产物输出 | ESM / CJS / IIFE 全场景覆盖(npm、script 标签、CDN) |


📥 Installation

# npm
npm install custom-svga

# yarn
yarn add custom-svga

# pnpm
pnpm add custom-svga

浏览器 CDN(无构建工具)

<!-- unpkg -->
<script src="https://unpkg.com/custom-svga@latest/dist/index.global.js"></script>

<!-- jsDelivr -->
<script src="https://cdn.jsdelivr.net/npm/custom-svga@latest/dist/index.global.js"></script>

<script>
  const { Player, Parser } = CustomSvga;
</script>

🚀 Quick Start

Vue 3 组件用法

<template>
  <div ref="container" class="svga-stage"></div>
</template>

<script setup lang="ts">
import { ref, onMounted, onBeforeUnmount, shallowRef } from 'vue';
import { Player, Parser } from 'custom-svga';

const container = ref<HTMLDivElement>();
const player = shallowRef<Player>();

onMounted(async () => {
  player.value = new Player(container.value!, {
    useOffscreenCanvas: true,
  });

  player.value.onStart(() => console.log('播放开始'));
  player.value.onProcess((frame) => console.log('当前帧:', frame));
  player.value.onLoopEnd(() => console.log('循环结束'));

  const videoItem = await Parser.loadUrl('https://example.com/anim.svga');
  await player.value.setVideoItem(videoItem);
  player.value.play();
});

onBeforeUnmount(() => {
  player.value?.destroy();
});
</script>

<style scoped>
.svga-stage {
  width: 300px;
  height: 300px;
}
</style>

原生 JavaScript 用法

import { Player, Parser } from 'custom-svga';

const player = new Player(document.getElementById('stage'));
const item = await Parser.loadUrl('/assets/anim.svga');
await player.setVideoItem(item);
player.play();

直接传入 ArrayBuffer

import { Player, Parser } from 'custom-svga';

const buffer = await fetch('/anim.svga').then((r) => r.arrayBuffer());
const item = await Parser.parseBuffer(buffer);

const player = new Player(document.getElementById('stage'));
await player.setVideoItem(item);
player.play();

📚 API 文档

new Player(element, config?)

创建播放器实例。根据 config.useOffscreenCanvas 自动选择主线程渲染器或 Worker 离屏渲染器。

| 参数 | 类型 | 必填 | 说明 | | --- | --- | --- | --- | | element | HTMLCanvasElement \| HTMLDivElement | ✅ | 绘图容器节点,传入 div 时会自动创建 canvas | | config | PlayConfig | ❌ | 初始化配置 |

PlayConfig 配置项

| 字段 | 类型 | 默认值 | 说明 | | --- | --- | --- | --- | | loop | number | 0 | 循环次数(0 = 无限循环) | | fps | number | - | 覆盖 SVGA 自身帧率 | | fillMode | 'forwards' \| 'backwards' \| 'clear' | 'forwards' | 播放结束时的行为 | | startFrame | number | - | 起始播放帧 | | endFrame | number | - | 结束播放帧 | | direction | 'forward' \| 'backward' | 'forward' | 播放方向 | | useOffscreenCanvas | boolean | false | 启用 OffscreenCanvas 离屏渲染 | | useIntersectionObserver | boolean | true | 启用 IntersectionObserver 可见性优化 | | invisibleBehavior | 'pause' \| 'skip' | 'pause' | 不可见时的行为 | | useSharedRAF | boolean | true | 多实例共享 requestAnimationFrame | | cacheFrames | boolean | false | 缓存已绘制帧为位图 | | defaultPoster | string | - | 默认外链静态海报图 |

Player 实例方法

| 方法 | 说明 | | --- | --- | | setVideoItem(item) | 设置视频数据,返回 Promise | | play(config?) | 触发播放 | | playRange(start, end, config?) | 播放指定帧范围 | | pause() | 暂停 | | stop() | 停止并回到第一帧 | | stepToFrame(frame, andPlay?) | 跳转到指定帧 | | stepToPercentage(p, andPlay?) | 跳转到指定进度(0~1) | | setImage(image, key) | 动态替换图片图层 | | setText(text, key) | 动态替换文本图层 | | clearDynamicObject(key) | 清除单个动态对象 | | clearDynamicObjects() | 清除所有动态对象 | | renderPoster(imageUrl) | 渲染外链海报图 | | getMemoryInfo() | 获取内存占用信息 | | clear() | 清空画布和内存引用 | | destroy() | 彻底销毁实例 |

Player 事件回调

| 事件 | 签名 | 触发时机 | | --- | --- | --- | | onStart(cb) | () => void | 播放开始 | | onPause(cb) | () => void | 播放暂停 | | onStop(cb) | () => void | 播放停止 | | onLoopEnd(cb) | () => void | 单次循环结束 | | onProcess(cb) | (frame: number) => void | 每渲染一帧触发 |


Parser 静态方法

| 方法 | 说明 | 适用场景 | | --- | --- | --- | | Parser.loadUrl(url, options?) | 从网络 URL 加载并解析 | 传统主线程渲染 | | Parser.loadFile(file, options?) | 从本地 File / Blob 加载 | 用户上传文件 | | Parser.parseBuffer(buffer, options?) | 直接解析 ArrayBuffer | 已下载好的二进制 | | Parser.load(url, options?) | 专为 OffscreenCanvas 设计,返回轻量元数据 | 离屏渲染专用 | | Parser.getCachedVideoItem(url) | 同步获取已缓存的解析结果 | 命中缓存时 | | Parser.clearCache() | 清空主线程缓存 | 内存压力时 | | Parser.clearOffscreenCache() | 清空 Worker 缓存 | 内存压力时 | | Parser.destroy(url?) | 销毁指定/全部缓存 | 主动释放 |

Parser.loadUrl / Parser.load 选项

{
  useImageBitmap?: boolean; // 默认 false,true 时 GPU 加速
  cache?: boolean;          // 默认 true
  movieDataId?: string;     // 自定义缓存 key(默认用 URL)
  useWorker?: boolean;      // 默认 true,是否使用 Web Worker 解析
}

🎯 高级用法

1. 动态替换图片图层

// 替换为在线图片
await player.setImage('https://example.com/new-avatar.png', 'avatar');

// 替换为 ImageBitmap(最佳性能)
const bitmap = await createImageBitmap(blob);
await player.setImage(bitmap, 'avatar');

// 替换为 Canvas 内容
await player.setImage(myOffscreenCanvas, 'avatar');

2. 动态注入文本

// 简单字符串
player.setText('Hello, CustomSvga!', 'greeting');

// 完整样式配置
player.setText({
  text: 'VIP 用户',
  size: 28,
  color: '#FFD700',
  bold: true,
  family: 'Arial',
  offset: { x: 0, y: -10 },
}, 'username');

3. 帧范围播放与反向播放

// 只播放第 10~50 帧
player.playRange(10, 50, { loop: 3 });

// 跳转到 50% 进度后自动播放
player.stepToPercentage(0.5, true);

// 反向播放
player.play({ direction: 'backward' });

// 开启帧缓存(适合复杂矢量动画)
player.play({ cacheFrames: true });

4. 可见性优化

const player = new Player(container, {
  useIntersectionObserver: true,  // 默认开启
  invisibleBehavior: 'pause',     // 移出视口时暂停
  // invisibleBehavior: 'skip',   // 移出视口时跳过绘制但保持帧同步
});

5. 内存管理

// 查看内存占用
const info = player.getMemoryInfo();
console.log(info);
// {
//   fileSize: 102400,
//   imageMemory: 5242880,
//   dictionaryMemory: 204800,
//   version: '2.0',
//   fps: 30,
//   frames: 60,
//   width: 750,
//   height: 1334,
//   dynamicKeys: ['avatar'],
//   desc: '源文件大小: 100.00 KB | 预估显存: 5.20 MB'
// }

// 释放指定 URL 缓存
Parser.destroy('https://example.com/anim.svga');

// 清空所有缓存
Parser.destroy();

👷 Web Worker 使用场景与示例

场景一:离线离屏渲染(核心推荐)

将解析、解码、绘制全部移至 Worker 线程,主线程零压力。适用于对动画流畅度要求高的场景(如直播间礼物动画)。

import { Player, Parser } from 'custom-svga';

const player = new Player(canvasElement, {
  useOffscreenCanvas: true, // 开启离屏渲染
});

// 必须使用 Parser.load 而非 Parser.loadUrl
const metadata = await Parser.load('https://example.com/anim.svga');
// 返回轻量元数据:{ movieDataId, isOffscreen: true, params: { viewBoxWidth, viewBoxHeight, fps, frames } }

// 仅传入 movieDataId,Worker 内部从缓存获取完整数据
await player.setVideoItem(metadata);
player.play();

数据流示意:

主线程                          Web Worker
─────────                      ──────────
Parser.load(url) ──fetch──▶  解码 Protobuf
   │                         解码 ImageBitmap
   │                         存入 sharedMovieCache
   ◀──返回轻量 metadata ────
   │
Player.setVideoItem(meta) ──▶ 从 sharedMovieCache 读取
   │                         离屏 Canvas 渲染
   │                         requestAnimationFrame 循环
   ◀──事件通知 (onProcess等) ─

限制:开启 useOffscreenCanvas 后,setVideoItem 只能传入 Parser.load() 返回的轻量元数据或缓存 key 字符串,禁止传入 Parser.loadUrl() 返回的完整 MovieData。

场景二:多 Player 实例共享同一 Worker

多个播放器实例复用同一个共享 Web Worker,Worker 内部维护全局 LRU 缓存,相同 URL 的动画数据只解析一次。

import { Player, Parser } from 'custom-svga';

// 三个播放器共享同一个 Worker 和缓存
const player1 = new Player(container1, { useOffscreenCanvas: true });
const player2 = new Player(container2, { useOffscreenCanvas: true });
const player3 = new Player(container3, { useOffscreenCanvas: true });

// 同一 URL 只会在 Worker 内解析一次
const meta1 = await Parser.load('https://example.com/gift.svga');
const meta2 = await Parser.load('https://example.com/gift.svga');
const meta3 = await Parser.load('https://example.com/gift.svga');

await Promise.all([
  player1.setVideoItem(meta1),
  player2.setVideoItem(meta2),
  player3.setVideoItem(meta3),
]);

player1.play();
player2.play({ loop: 2 });
player3.play({ loop: 1 });

场景三:Worker 解析 + 主线程渲染

利用 Worker 进行高性能解析,解析完成后图片在主线程渲染。适用于需要精细控制 Canvas 绘制或需要兼容不支持 OffscreenCanvas 浏览器的场景。

import { Player, Parser } from 'custom-svga';

// 不开启 useOffscreenCanvas,走主线程渲染
const player = new Player(document.getElementById('stage'), {
  useOffscreenCanvas: false, // 默认值
});

// 使用 useWorker: true(默认)在 Worker 中解析
const videoItem = await Parser.loadUrl('https://example.com/anim.svga', {
  useWorker: true,   // Worker 解析,主线程渲染
  useImageBitmap: false, // 主线程使用 HTMLImageElement
  cache: true,
});

await player.setVideoItem(videoItem);
player.play();

场景四:禁用 Worker 回退到主线程解析

在不支持 Worker 的环境(如部分小程序 WebView)或调试时,可以强制降级。

import { Parser } from 'custom-svga';

// 全局关闭 Worker,所有解析在主线程进行
const videoItem = await Parser.loadUrl('https://example.com/anim.svga', {
  useWorker: false,
});

场景五:Worker 中的内存回收

Worker 内部维护 sharedMovieCache + 引用计数机制,当所有渲染器实例释放后自动回收。

// 手动释放 Worker 中缓存的指定动画数据
Parser.destroyOffscreenCache('https://example.com/anim.svga');

// 或使用通用 destroy 方法(同时清理主线程 + Worker 缓存)
Parser.destroy('https://example.com/anim.svga');

// 精细控制仅清理 Worker 离屏缓存
Parser.destroy({
  url: 'https://example.com/anim.svga',
  mainThread: false,
  offscreen: true,
});

// 清空 Worker 全部离屏缓存
Parser.clearOffscreenCache();

Worker 生命周期概览

┌─────────────────────────────────────────────────────────────┐
│                     主线程 (Main Thread)                     │
│                                                             │
│  Parser.loadUrl(url)  ───────┐                              │
│  Parser.load(url)     ───────┤                              │
│                              │ postMessage                  │
│  Player.setVideoItem()  ────┼──▶  WORKER_ACTION.SET_MOVIE   │
│  Player.play()          ────┼──▶  WORKER_ACTION.PLAY        │
│  Player.pause()         ────┼──▶  WORKER_ACTION.PAUSE       │
│  Player.destroy()       ────┼──▶  WORKER_ACTION.DESTROY     │
│                              │                              │
│  ◀── WORKER_RESPONSE_TYPE    │ postMessage                  │
│       .SUCCESS (解析结果)    │                              │
│       .EVENT (播放事件)      │                              │
└──────────────────────────────┼──────────────────────────────┘
                               │
┌──────────────────────────────┼──────────────────────────────┐
│                   Web Worker (共享子线程)                    │
│                                                             │
│  sharedMovieCache (Map<movieDataId, MovieData + refCount>)  │
│                                                             │
│  ┌──────────────────┐   ┌──────────────────┐               │
│  │ WorkerParser     │   │ WorkerRenderer[]  │               │
│  │  .parse()        │   │  .setMovie()      │               │
│  │  .parseOffscreen │   │  .play()          │               │
│  └──────────────────┘   │  .drawFrame()     │               │
│                          │  (OffscreenCanvas)│               │
│                          └──────────────────┘               │
│                                                             │
│  tickerManager (共享 RAF 调度器)                             │
└─────────────────────────────────────────────────────────────┘

Worker 模式自动降级机制

Custom SVGA 内置三层降级策略,无需开发者干预:

| 层级 | 条件 | 行为 | | --- | --- | --- | | 🥇 Blob Worker | 浏览器支持 Worker + Blob | 将 worker 代码通过 Blob URL 实例化,零网络请求 | | 🥈 Module Worker | 开发环境 / Blob 失败 | 通过 new URL('./svga.worker.ts', import.meta.url) 加载 | | 🥉 Mock Worker | Worker API 不可用 | 同构 setTimeout 宏任务模拟,功能完全等价 |


📊 性能对比

| 指标 | 官方 svga-web | Custom SVGA | | --- | --- | --- | | 核心包体积(gzip) | ~85KB | ~30KB ⬇️ 65% | | Tree-shaking 支持 | ❌ | ✅ | | 按需懒加载 | ❌ | ✅ | | OffscreenCanvas | 需手动配置 | ✅ 一键启用 | | Worker 缓存共享 | ❌ | ✅ | | 动态图片/文本替换 | 需重编 SVGA | ✅ 运行时热替换 | | Vue 3 集成 | 需包装 | ✅ 原生支持 | | TypeScript 类型 | 部分 | ✅ 100% 覆盖 |


🌐 浏览器兼容性

| 浏览器 | 最低版本 | OffscreenCanvas 支持 | | --- | --- | --- | | Chrome / Edge | 88+ | ✅ | | Firefox | 105+ | ✅ | | Safari | 16.4+ | ✅ | | 移动端 Chrome (Android) | 88+ | ✅ | | 移动端 Safari (iOS) | 16.4+ | ✅ |

在不支持 OffscreenCanvas 的浏览器中,播放器自动回退到主线程 Canvas 渲染,业务代码无需任何改动。


🔧 常见问题

开启 useOffscreenCanvas: true 时,setVideoItem 只能传 Parser.load() 返回的轻量元数据,不能传 Parser.loadUrl() 返回的完整 MovieData。

// ✅ 正确
const meta = await Parser.load(url);
await player.setVideoItem(meta);

// ❌ 错误
const item = await Parser.loadUrl(url);
await player.setVideoItem(item); // 抛错!
  • 关闭 useImageBitmap(iOS 兼容性差)
  • 检查是否启用了 useOffscreenCanvas(iOS 16.4+ 才支持)
  • 减少同屏 SVGA 实例数量
await player.setVideoItem(item);
player.stepToFrame(0, false); // 跳到第 0 帧,不自动播放

🤝 贡献

欢迎 PR 和 Issue!请确保:

  • 代码风格一致(TypeScript strict)
  • 提交前 npm run build:tsup 能成功
  • 重大变更请先开 Issue 讨论

📄 License

MIT © 2026 DreamLife(Zhou)

🙏 致谢