@brmtech/spine
v0.1.7
Published
Framework-agnostic Spine WebGL renderer with shared canvas layers.
Maintainers
Readme
@brmtech/spine
Framework-agnostic Spine WebGL renderer with shared canvas layers.
中文文档
@brmtech/spine 是一个不绑定框架的 Spine WebGL 渲染器。原生 DOM、Vue、React 和其他前端框架都使用同一个核心 API:拿到宿主元素,然后调用 createSpine。
安装
pnpm add @brmtech/spine @esotericsoftware/spine-webgl快速开始
import { createSpine } from "@brmtech/spine";
const host = document.querySelector("#hero") as HTMLElement;
const spine = createSpine(host, "spine/hero/Hero", {
baseUrl: "https://cdn.example.com/assets/",
animation: "Idle",
});
await spine.ready;
spine.play("Attack", { loop: false });框架用法
| 场景 | 获取宿主元素 | 创建方式 |
| -------- | ------------------------------------------------ | ---------------------------------------------------------- |
| 原生 DOM | document.querySelector('#hero') as HTMLElement | createSpine(host, 'spine/hero/Hero', options) |
| Vue 3 | 模板 ref + onMounted | createSpine(hostRef.value, 'spine/hero/Hero', options) |
| React | useRef<HTMLDivElement>() + useEffect | createSpine(hostRef.current, 'spine/hero/Hero', options) |
Vue 3
<script setup lang="ts">
import { onBeforeUnmount, onMounted, ref } from "vue";
import { createSpine, type SpineInstance } from "@brmtech/spine";
const hostRef = ref<HTMLElement | null>(null);
const spineRef = ref<SpineInstance | null>(null);
onMounted(() => {
if (!hostRef.value) return;
spineRef.value = createSpine(hostRef.value, "spine/hero/Hero", {
animation: "Idle",
});
});
onBeforeUnmount(() => spineRef.value?.destroy());
</script>
<template>
<div ref="hostRef" class="hero" />
</template>React
import { useEffect, useRef } from "react";
import { createSpine, type SpineInstance } from "@brmtech/spine";
export function HeroSpine() {
const hostRef = useRef<HTMLDivElement | null>(null);
const spineRef = useRef<SpineInstance | null>(null);
useEffect(() => {
if (!hostRef.current) return;
const spine = createSpine(hostRef.current, "spine/hero/Hero", {
animation: "Idle",
});
spineRef.current = spine;
return () => spine.destroy();
}, []);
return <div ref={hostRef} style={{ width: 160, height: 160 }} />;
}资源地址规则
| 规则 | 说明 | 示例 |
| -------------- | -------------------------------------------------- | ----------------------------------------------------- |
| 推荐使用 src | src 是不带 .json / .atlas 的资源路径 | spine/hero/Hero |
| 默认文件推导 | 自动加载 {src}.json 和 {src}.atlas | spine/hero/Hero.json、spine/hero/Hero.atlas |
| baseUrl | 拼到相对资源路径前面 | https://cdn.example.com/assets/ + spine/hero/Hero |
| 绝对 URL | src 是绝对 URL 或以 / 开头时,不拼接 baseUrl | https://cdn.example.com/Hero |
| query/hash | 会保留 query 和 hash,并把扩展名加在 query 前 | Hero?v=1#main -> Hero.json?v=1#main |
| 完整地址 | jsonUrl / atlasUrl 可直接指定完整文件地址 | jsonUrl: 'hero.json' |
| 旧字段 | url 兼容旧代码,新代码建议用 src | url: 'spine/hero/Hero' |
| 自定义规则 | resolveUrl 只用于特殊命名规则 | (src, kind) => ... |
导出 API
| 名称 | 类型 | 说明 |
| ----------------------------------------------- | -------- | ---------------------------------------------------- |
| createSpine(host, src, options?) | 函数 | 创建 Spine 实例,推荐写法 |
| createSpine(host, options) | 函数 | 对象写法,适合把所有参数放在一个对象里 |
| createSpineManager(options?) | 函数 | 创建独立 manager,用于自定义共享层、根节点或像素比例 |
| getDefaultSpineManager(options?) | 函数 | 获取默认 manager;首次调用时会创建 |
| destroyDefaultSpineManager() | 函数 | 销毁默认 manager 和它管理的共享 canvas |
| SpineManager | 类 | manager 类,适合高级接入 |
| resolveSpineUrls(options) | 工具函数 | 根据资源参数解析出 jsonUrl / atlasUrl |
| defaultResolveUrl(src, kind, context) | 工具函数 | 默认资源地址解析函数 |
| computeBounds(skeleton) | 工具函数 | 计算当前骨骼 bounds |
| computeStableBounds(skeleton, animationState) | 工具函数 | 采样动画并计算稳定 bounds |
| updateWorldTransform(skeleton) | 工具函数 | 兼容不同 spine-webgl 版本的世界矩阵更新方法 |
createSpine 签名
| 签名 | 推荐度 | 说明 |
| ------------------------------------------------------ | ------ | ---------------------------------------------------- |
| createSpine(host, src, options?) | 推荐 | 最简洁的创建方式 |
| createSpine(host, options) | 推荐 | 所有参数都放在对象里 |
| createSpine(host, src, options, legacyManagerConfig) | 兼容 | 旧用法兼容;新代码把 manager 放进 options |
| createSpine(host, options, legacyManagerConfig) | 兼容 | 旧用法兼容;新代码把 managerOptions 放进 options |
createSpine 参数
| 参数 | 类型 | 必填 | 默认值 | 说明 |
| --------------------- | -------------------------- | -------------- | ------ | ----------------------------------------------- |
| host | HTMLElement | 是 | - | 承载动画的 DOM 元素,渲染器会跟随它的位置和尺寸 |
| src | string | 字符串签名必填 | - | 资源路径,不包含 .json 或 .atlas |
| options | SpineCreateOptions | 否 | {} | 创建参数 |
| legacyManagerConfig | CreateSpineManagerConfig | 否 | {} | 旧版 manager 参数,兼容保留 |
SpineCreateOptions
资源参数
| 参数 | 类型 | 必填 | 默认值 | 说明 |
| ------------ | ------------------ | ---- | ------------------- | ------------------------------------------------------ |
| src | string | 否 | - | 推荐资源路径,不包含 .json 或 .atlas |
| baseUrl | string | 否 | - | 相对资源路径的前缀,常用于 CDN 或静态资源目录 |
| jsonUrl | string | 否 | - | JSON 文件地址;提供后覆盖 src / url 的 JSON 推导 |
| atlasUrl | string | 否 | - | atlas 文件地址;提供后覆盖 src / url 的 atlas 推导 |
| url | string | 否 | - | 旧版资源基础地址;兼容保留,新代码建议用 src |
| resolveUrl | SpineUrlResolver | 否 | defaultResolveUrl | 自定义资源地址解析函数 |
动画参数
| 参数 | 类型 | 必填 | 默认值 | 说明 |
| ------------- | --------- | ---- | ------------------- | ----------------------------------------------------- |
| animation | string | 否 | skeleton 第一个动画 | 初始动画名;不存在时自动回退 |
| loop | boolean | 否 | true | 初始动画是否循环 |
| disableLoop | boolean | 否 | false | 旧配置兼容项;为 true 时关闭循环,优先级低于 loop |
| speed | number | 否 | 1 | 播放速度 |
| paused | boolean | 否 | false | 加载完成后是否暂停 |
外观参数
| 参数 | 类型 | 必填 | 默认值 | 说明 |
| ---------- | -------- | ---- | ------ | ------------------------------------------------ |
| skin | string | 否 | - | 初始皮肤名 |
| skins | string | 否 | - | skin 的兼容别名 |
| scale | number | 否 | 1 | 显示缩放;数值越大显示越大 |
| overflow | number | 否 | 0.5 | 宿主元素外额外渲染区域,用于避免动画超出后被裁掉 |
渲染参数
| 参数 | 类型 | 必填 | 默认值 | 说明 |
| ---------------- | --------------------- | ---- | ------------ | ------------------------------------------------------ |
| layer | SpineLayer | 否 | low | 共享渲染层名;内置 low 和 high,也可传自定义字符串 |
| exclusive | boolean | 否 | false | 是否在宿主元素内创建独立 canvas |
| manager | SpineManager | 否 | 默认 manager | 指定自定义 manager |
| managerOptions | SpineManagerOptions | 否 | - | 默认 manager 首次创建时使用的配置 |
回调参数
| 参数 | 类型 | 必填 | 默认值 | 说明 |
| ------------ | ----------------------------------- | ---- | ------ | ---------------------------- |
| onLoad | (instance: SpineInstance) => void | 否 | - | 资源和骨骼加载完成后触发 |
| onError | (error: unknown) => void | 否 | - | 资源加载或骨骼创建失败时触发 |
| onComplete | (entry: spine.TrackEntry) => void | 否 | - | 当前动画完成时触发 |
SpineInstance
属性
| 属性 | 类型 | 说明 |
| ---------------- | ------------------------------ | ------------------------------------------- |
| id | string | 实例 ID |
| host | HTMLElement | 传入的宿主元素 |
| ready | Promise<SpineInstance> | 加载完成后 resolve;加载失败会 reject |
| skeleton | spine.Skeleton \| null | 加载后的 skeleton,未完成时为 null |
| animationState | spine.AnimationState \| null | 加载后的 animation state,未完成时为 null |
方法
| 方法 | 参数 | 返回值 | 说明 |
| --------------- | -------------------------------------------------------------- | ---------- | -------------------------------------------------------------------- |
| play | animationName?: string, options?: SpinePlayOptions | void | 播放动画;不传动画名时重播当前动画 |
| pause | - | void | 暂停动画时间推进,但保留当前画面 |
| resume | - | void | 恢复播放 |
| setSpeed | speed: number | void | 设置播放速度 |
| setSkin | skinName: string | void | 切换皮肤,并重新计算 bounds |
| setNumberSkin | skinName: string, attachmentName?: string, slotName?: string | void | 兼容数字皮肤场景;默认 attachment 为 Number,slot 为 SmartNumber |
| getSkins | - | string[] | 返回 skeleton 中所有皮肤名 |
| destroy | - | void | 销毁实例,注销渲染条目并释放独立 canvas |
SpinePlayOptions
| 参数 | 类型 | 必填 | 默认值 | 说明 |
| ------------- | --------- | ---- | ------- | ---------------------- |
| loop | boolean | 否 | true | 本次播放是否循环 |
| pausedAfter | boolean | 否 | false | 设置动画后是否立即暂停 |
spine.play("Attack", { loop: false });
spine.play("Idle", { loop: true });
spine.play(undefined, { pausedAfter: true });SpineManagerOptions
| 参数 | 类型 | 必填 | 默认值 | 说明 |
| --------------------- | ---------------------------------- | ---- | --------------------- | ------------------------------- |
| root | HTMLElement | 否 | document.body | 共享 canvas 的父节点 |
| layers | Record<string, number \| string> | 否 | { low: 1, high: 5 } | 共享层 z-index 配置 |
| maxDevicePixelRatio | number | 否 | 2 | WebGL backing buffer 的最大 DPR |
| keepAlive | boolean | 否 | false | 没有实例时是否继续保持渲染循环 |
const manager = createSpineManager({
layers: {
background: 1,
foreground: 20,
},
maxDevicePixelRatio: 2,
});
createSpine(host, "spine/hero/Hero", {
layer: "foreground",
manager,
});类型
| 类型 | 定义 | 说明 |
| -------------------------- | ------------------------------------------------------------------ | ------------------------- |
| SpineLayer | 'low' \| 'high' \| string | 渲染层名称 |
| SpineUrlResolver | (src, kind, context) => string | 自定义资源解析函数类型 |
| SpineUrls | { jsonUrl: string; atlasUrl: string } | 解析后的资源地址 |
| SpineRenderLayer | 内部接口 | 内部渲染层结构 |
| SpineLoadedData | 内部接口 | 内部加载结果结构 |
| CreateSpineManagerConfig | { manager?: SpineManager; managerOptions?: SpineManagerOptions } | 兼容旧签名的 manager 配置 |
注意事项
| 项目 | 说明 |
| ------------ | ----------------------------------------------------------------- |
| Spine 运行时 | 本包不实现 Spine 运行时,底层使用 @esotericsoftware/spine-webgl |
| 框架入口 | 所有框架都通过核心入口 @brmtech/spine 使用,不提供框架专属入口 |
| 共享 canvas | 适合页面内多处 Spine 动画 |
| exclusive | 适合需要 canvas 跟随宿主元素隔离渲染的场景 |
English Guide
@brmtech/spine is a framework-agnostic Spine WebGL renderer. Native DOM, Vue, React, and other frontend frameworks all use the same core API: get a host element, then call createSpine.
Install
pnpm add @brmtech/spine @esotericsoftware/spine-webglQuick Start
import { createSpine } from "@brmtech/spine";
const host = document.querySelector("#hero") as HTMLElement;
const spine = createSpine(host, "spine/hero/Hero", {
baseUrl: "https://cdn.example.com/assets/",
animation: "Idle",
});
await spine.ready;
spine.play("Attack", { loop: false });Framework Usage
| Scenario | How to Get Host | Creation |
| ---------- | ------------------------------------------------ | ---------------------------------------------------------- |
| Native DOM | document.querySelector('#hero') as HTMLElement | createSpine(host, 'spine/hero/Hero', options) |
| Vue 3 | template ref + onMounted | createSpine(hostRef.value, 'spine/hero/Hero', options) |
| React | useRef<HTMLDivElement>() + useEffect | createSpine(hostRef.current, 'spine/hero/Hero', options) |
Vue 3
<script setup lang="ts">
import { onBeforeUnmount, onMounted, ref } from "vue";
import { createSpine, type SpineInstance } from "@brmtech/spine";
const hostRef = ref<HTMLElement | null>(null);
const spineRef = ref<SpineInstance | null>(null);
onMounted(() => {
if (!hostRef.value) return;
spineRef.value = createSpine(hostRef.value, "spine/hero/Hero", {
animation: "Idle",
});
});
onBeforeUnmount(() => spineRef.value?.destroy());
</script>
<template>
<div ref="hostRef" class="hero" />
</template>React
import { useEffect, useRef } from "react";
import { createSpine, type SpineInstance } from "@brmtech/spine";
export function HeroSpine() {
const hostRef = useRef<HTMLDivElement | null>(null);
const spineRef = useRef<SpineInstance | null>(null);
useEffect(() => {
if (!hostRef.current) return;
const spine = createSpine(hostRef.current, "spine/hero/Hero", {
animation: "Idle",
});
spineRef.current = spine;
return () => spine.destroy();
}, []);
return <div ref={hostRef} style={{ width: 160, height: 160 }} />;
}Resource URL Rules
| Rule | Description | Example |
| ----------------- | -------------------------------------------------------------------- | ----------------------------------------------------- |
| Prefer src | src is a resource path without .json / .atlas | spine/hero/Hero |
| Default inference | Loads {src}.json and {src}.atlas automatically | spine/hero/Hero.json, spine/hero/Hero.atlas |
| baseUrl | Prepended to relative resource paths | https://cdn.example.com/assets/ + spine/hero/Hero |
| Absolute URL | baseUrl is not prepended when src is absolute or starts with / | https://cdn.example.com/Hero |
| Query/hash | Query and hash are preserved, extension is inserted before query | Hero?v=1#main -> Hero.json?v=1#main |
| Full URLs | jsonUrl / atlasUrl can point to exact files | jsonUrl: 'hero.json' |
| Legacy field | url is kept for old code; prefer src | url: 'spine/hero/Hero' |
| Custom rule | resolveUrl is only for unusual naming rules | (src, kind) => ... |
Exports
| Name | Type | Description |
| ----------------------------------------------- | -------- | ----------------------------------------------------------------------- |
| createSpine(host, src, options?) | function | Creates a Spine instance. Recommended form |
| createSpine(host, options) | function | Object form for putting all options in one object |
| createSpineManager(options?) | function | Creates an independent manager for custom layers, roots, or pixel ratio |
| getDefaultSpineManager(options?) | function | Returns the default manager, creating it on first use |
| destroyDefaultSpineManager() | function | Destroys the default manager and its shared canvases |
| SpineManager | class | Manager class for advanced integrations |
| resolveSpineUrls(options) | utility | Resolves resource options to jsonUrl / atlasUrl |
| defaultResolveUrl(src, kind, context) | utility | Default resource resolver |
| computeBounds(skeleton) | utility | Computes current skeleton bounds |
| computeStableBounds(skeleton, animationState) | utility | Samples animation frames and computes stable bounds |
| updateWorldTransform(skeleton) | utility | Compatibility helper for different spine-webgl world-transform APIs |
createSpine Signatures
| Signature | Recommendation | Description |
| ------------------------------------------------------ | -------------- | ----------------------------------------------------------------------- |
| createSpine(host, src, options?) | Recommended | Shortest creation form |
| createSpine(host, options) | Recommended | Puts all options in one object |
| createSpine(host, src, options, legacyManagerConfig) | Compatibility | Old manager argument; new code should put manager in options |
| createSpine(host, options, legacyManagerConfig) | Compatibility | Old manager argument; new code should put managerOptions in options |
createSpine Parameters
| Parameter | Type | Required | Default | Description |
| --------------------- | -------------------------- | ---------------------------- | ------- | -------------------------------------------------------------------------------- |
| host | HTMLElement | Yes | - | DOM element that hosts the animation; the renderer follows its position and size |
| src | string | Required in string signature | - | Resource path without .json or .atlas |
| options | SpineCreateOptions | No | {} | Creation options |
| legacyManagerConfig | CreateSpineManagerConfig | No | {} | Legacy manager argument kept for compatibility |
SpineCreateOptions
Resource Options
| Option | Type | Required | Default | Description |
| ------------ | ------------------ | -------- | ------------------- | ---------------------------------------------------------------------- |
| src | string | No | - | Recommended resource path without .json or .atlas |
| baseUrl | string | No | - | Prefix for relative resource paths, usually a CDN or static asset root |
| jsonUrl | string | No | - | JSON file URL; overrides JSON inference from src / url |
| atlasUrl | string | No | - | atlas file URL; overrides atlas inference from src / url |
| url | string | No | - | Legacy base resource URL; kept for compatibility. Prefer src |
| resolveUrl | SpineUrlResolver | No | defaultResolveUrl | Custom resource resolver |
Animation Options
| Option | Type | Required | Default | Description |
| ------------- | --------- | -------- | ------------------------ | ------------------------------------------------------------------------- |
| animation | string | No | First skeleton animation | Initial animation name; falls back automatically |
| loop | boolean | No | true | Whether the initial animation loops |
| disableLoop | boolean | No | false | Compatibility flag; disables loop when true, lower priority than loop |
| speed | number | No | 1 | Playback speed |
| paused | boolean | No | false | Whether to pause after load |
Appearance Options
| Option | Type | Required | Default | Description |
| ---------- | -------- | -------- | ------- | --------------------------------------------------- |
| skin | string | No | - | Initial skin name |
| skins | string | No | - | Compatibility alias for skin |
| scale | number | No | 1 | Visual scale; larger values render larger |
| overflow | number | No | 0.5 | Extra render area around the host to avoid clipping |
Rendering Options
| Option | Type | Required | Default | Description |
| ---------------- | --------------------- | -------- | --------------- | -------------------------------------------------------------------------------- |
| layer | SpineLayer | No | low | Shared render layer name; built-ins are low and high, custom strings allowed |
| exclusive | boolean | No | false | Creates a dedicated canvas inside the host |
| manager | SpineManager | No | Default manager | Custom manager instance |
| managerOptions | SpineManagerOptions | No | - | Options used when the default manager is created for the first time |
Callbacks
| Option | Type | Required | Default | Description |
| ------------ | ----------------------------------- | -------- | ------- | -------------------------------------------- |
| onLoad | (instance: SpineInstance) => void | No | - | Called after assets and skeleton are ready |
| onError | (error: unknown) => void | No | - | Called when assets or skeleton creation fail |
| onComplete | (entry: spine.TrackEntry) => void | No | - | Called when the current animation completes |
SpineInstance
Properties
| Property | Type | Description |
| ---------------- | ------------------------------ | ------------------------------------------------------- |
| id | string | Instance ID |
| host | HTMLElement | Host element passed to createSpine |
| ready | Promise<SpineInstance> | Resolves after load, rejects on load failure |
| skeleton | spine.Skeleton \| null | Loaded skeleton, or null before load completes |
| animationState | spine.AnimationState \| null | Loaded animation state, or null before load completes |
Methods
| Method | Parameters | Return | Description |
| --------------- | -------------------------------------------------------------- | ---------- | ------------------------------------------------------------------------------------------------- |
| play | animationName?: string, options?: SpinePlayOptions | void | Plays an animation; without a name, replays the current animation |
| pause | - | void | Pauses animation time while keeping the current frame visible |
| resume | - | void | Resumes playback |
| setSpeed | speed: number | void | Sets playback speed |
| setSkin | skinName: string | void | Changes skin and recomputes bounds |
| setNumberSkin | skinName: string, attachmentName?: string, slotName?: string | void | Compatibility helper for number-skin use cases. Defaults: attachment Number, slot SmartNumber |
| getSkins | - | string[] | Returns all skin names from the skeleton |
| destroy | - | void | Destroys the instance, unregisters it, and releases its dedicated canvas if any |
SpinePlayOptions
| Option | Type | Required | Default | Description |
| ------------- | --------- | -------- | ------- | -------------------------------------------------------- |
| loop | boolean | No | true | Whether this playback loops |
| pausedAfter | boolean | No | false | Whether to pause immediately after setting the animation |
spine.play("Attack", { loop: false });
spine.play("Idle", { loop: true });
spine.play(undefined, { pausedAfter: true });SpineManagerOptions
| Option | Type | Required | Default | Description |
| --------------------- | ---------------------------------- | -------- | --------------------- | ------------------------------------------------------------ |
| root | HTMLElement | No | document.body | Parent element for shared canvases |
| layers | Record<string, number \| string> | No | { low: 1, high: 5 } | z-index map for shared layers |
| maxDevicePixelRatio | number | No | 2 | Maximum DPR for WebGL backing buffers |
| keepAlive | boolean | No | false | Keeps the render loop alive when no instances are registered |
const manager = createSpineManager({
layers: {
background: 1,
foreground: 20,
},
maxDevicePixelRatio: 2,
});
createSpine(host, "spine/hero/Hero", {
layer: "foreground",
manager,
});Types
| Type | Definition | Description |
| -------------------------- | ------------------------------------------------------------------ | ------------------------------------ |
| SpineLayer | 'low' \| 'high' \| string | Render layer name |
| SpineUrlResolver | (src, kind, context) => string | Custom resource resolver type |
| SpineUrls | { jsonUrl: string; atlasUrl: string } | Resolved resource URLs |
| SpineRenderLayer | internal interface | Internal render-layer structure |
| SpineLoadedData | internal interface | Internal loaded-data structure |
| CreateSpineManagerConfig | { manager?: SpineManager; managerOptions?: SpineManagerOptions } | Manager config for legacy signatures |
Notes
| Item | Description |
| --------------- | ------------------------------------------------------------------------------------------- |
| Spine runtime | This package does not implement Spine itself. It uses @esotericsoftware/spine-webgl |
| Framework entry | All frameworks use the core @brmtech/spine entry. No framework-specific entry is provided |
| Shared canvas | Useful for many animations on one page |
| exclusive | Useful when a canvas should stay isolated inside its host element |
