Spine Web Components

spine-webcomponents 包提供了一系列 Web Component(自定义 HTML 元素), 可直接将 Spine 动画嵌入网页中.

当在 HTML 页面中添加 <spine-skeleton> 标签后, 该库会创建一个共享的 WebGL 画布覆盖层, 用于渲染多个 Spine skeleton. 此设计突破了浏览器对 WebGL context 数量的限制.

Spine Player 不同, Web Components 不内置播放控制 UI. 相反, Web Components 通过 HTML 属性来提供广泛配置项以实现精细化控制.

导出

Spine Web Component使用与 Spine Player 相同的导出格式. 此外, 它还支持在同一个 JSON 文件中使用多个skeletons.

基本设置

<spine-skeleton> Web Components添加到网站仅需几个简单步骤, 具体如下.

添加JavaScript

spine-webcomponents 包含 JavaScript 文件 spine-webcomponents.js, 该文件定义了两个 HTML 自定义元素 <spine-skeleton><spine-overlay>, 以及一组相关的工具函数.

<script src="https://unpkg.com/@esotericsoftware/[email protected].*/dist/iife/spine-webcomponents.js"></script>

在上述示例中, 文件从 UNPKG (一个快速的 NPM CDN)加载. URL 中包含版本号(4.2), 该版本号必须与导出 skeleton 的 Spine 编辑器版本一致. 补丁版本中的星号(*)则会从 major.minor 版本加载最新的 JavaScript 代码.

如需从 UNPKG CDN 加载压缩后的文件, 请使用 .min.js 扩展名:

<script src="https://unpkg.com/@esotericsoftware/[email protected].*/dist/iife/spine-webcomponents.min.js"></script>

或者, 也可以通过从 UNPKG 下载文件, 或从 GitHub 仓库 中的源码构建来自托管该文件. 该仓库还包含如何使用 NPM 或 Yarn 集成 spine-webcomponents 的说明.

使用spine-skeleton

导入 JavaScript 文件后, 无需额外 JavaScript 即可在 HTML 中直接使用 Web Component:

html
<spine-skeleton
atlas="/files/spine-widget/assets/spineboy-pma.atlas"
skeleton="/files/spine-widget/assets/spineboy-pro.skel"
></spine-skeleton>

<spine-skeleton> 元素从 /files/spineboy/export/spineboy-pro.skel 加载 skeleton 数据, 从 /files/spineboy/export/spineboy.atlas 加载 atlas. Atlas 引用图像文件(spineboy.png), spine-webcomponents 会从 .atlas 文件所在目录中加载图像, 即 /files/spineboy/export/spineboy.png.

spine-webcomponents 会自动在 DOM 底部添加一个 <spine-overlay> 元素. 该组件创建一个覆盖整个页面的透明 WebGL 画布, 用于在各自父容器内正确定位渲染所有 <spine-skeleton> 组件. 在常见用例中无需关注此覆盖层.

Web Component 会按比例缩放 skeleton, 使其适应其父元素.

1 2
3 4
html
<table>
<tr>
<td>1</td>
<td>2
<spine-skeleton
   atlas="/files/spine-widget/assets/spineboy-pma.atlas"
   skeleton="/files/spine-widget/assets/spineboy-pro.skel">
</spine-skeleton>
</td>
</tr>
<tr>
<td>3
<spine-skeleton
   atlas="/files/spine-widget/assets/spineboy-pma.atlas"
   skeleton="/files/spine-widget/assets/spineboy-pro.skel">
</spine-skeleton>
</td>
<td>4</td>
</tr>
</table>!!

配置

<spine-skeleton> 元素提供众多配置属性, 可针对具体需求进行定制.

JSON, 二进制和atlas URL

skeletonatlas 是两个必填属性, 分别定义了 skeleton 的 .json 或二进制 .skel 文件及 .atlas 文件的源路径. 这些路径可以是相对 URL 也可以是绝对 URL.

html
<spine-skeleton
atlas="/files/spine-widget/assets/spineboy-pma.atlas"
skeleton="/files/spine-widget/assets/spineboy-pro.skel">
</spine-skeleton>

<spine-skeleton
atlas="https://esotericsoftware.com/assets/spineboy-pma.atlas"
skeleton="https://esotericsoftware.com/assets/spineboy-pro.skel">
</spine-skeleton>

当使用指向其他域的绝对 URL 时, 浏览器可能无法加载资源. 通过在资源托管服务器上启用 CORS 可以解决该问题.

内嵌数据

除了从 URL 加载数据外, 还可以用 raw-data 属性直接嵌入 .json/.skel.atlas.png 文件. 该属性接受一个字符串化的 JSON 对象, 键为资源名称, 值为 Base64 编码的内容. 此时, skeletonatlas 属性应引用此对象中对应的资源名称. raw-data 属性用于启用此设置.

html
<spine-skeleton
atlas="/assets/inline.atlas"
skeleton="/assets/inline.skel"
animation="animation"
raw-data='{
"/assets/inline.atlas":"aW5saW5lLnBuZwpzaXplOjE2LDE2CmZpbHRlcjpMaW5lYXIsTGluZWFyCnBtYTp0cnVlCmRvdApib3VuZHM6MCwwLDEsMQo=",
"/assets/inline.skel":"/B8S/IqaXgYHNC4yLjM5wkgAAMJIAABCyAAAQsgAAELIAAAAAQRkb3QCBXJvb3QAAAAAAAAAAAAAAAA/gAAAP4AAAAAAAAAAAAAAAAAAAAAABGRvdAAAAAAAAAAAAAAAAABCyAAAQsgAAAAAAAAAAAAAAAAAAAAAAQRkb3QB//////////8BAAAAAAABAAEBACWwfdcAAAAAP4AAAD+AAAA/gAAAP4AAAAAAAQphbmltYXRpb24BAQABAQMAAAAAAP////8/gAAA/wAA/wBAAAAA/////wAAAAAAAAAAAA==",
"/assets/inline.png":"iVBORw0KGgoAAAANSUhEUgAAAAEAAAABAQMAAAAl21bKAAAAAXNSR0IB2cksfwAAAAlwSFlzAAALEwAACxMBAJqcGAAAAANQTFRF////p8QbyAAAAApJREFUeJxjZAAAAAQAAiFkrWoAAAAASUVORK5CYII="
}'

></spine-skeleton>

样式, 宽度和高度

默认情况下, Web Component 在渲染 skeleton 时会缩放 skeleton 来适应其父元素的尺寸, 但请注意实际上 skeleton 的宽高为零. 若需手动设置 Web Component的尺寸, 请使用标准的 styleclass 属性. 这些属性可以实现你需要的任意样式.

html
<style>
.custom-class {
width: 150px;
height: 150px;
border: 1px solid green;
border-radius: 10px;
box-shadow: -5px 5px 3px rgba(0, 255, 0, 0.3);
margin-right: 10px;
}
</style>

<spine-skeleton
atlas="/files/spine-widget/assets/spineboy-pma.atlas"
skeleton="/files/spine-widget/assets/spineboy-pro.skel"
animation="walk"
class="custom-class"
></spine-skeleton>

<spine-skeleton
atlas="/files/spine-widget/assets/spineboy-pma.atlas"
skeleton="/files/spine-widget/assets/spineboy-pro.skel"
animation="walk"
style="
width: 150px;
height: 150px;
border: 1px solid red;
border-radius: 10px;
box-shadow: -5px 5px 3px rgba(255, 0, 0, 0.3);
"

></spine-skeleton>

JSON skeleton键

为减少资源请求, 可将多个 skeleton 嵌入单个 JSON 文件. 如此使用 JSON 时, 需在 Web Component上设置 json-skeleton-key 属性来指定需要显示的 skeleton. spine-webcomponents 资产管理器仅会将每个资产加载一次, 即使在页面中多次使用.

html
<spine-skeleton
atlas="/files/spine-widget/assets/atlas2.atlas"
skeleton="/files/spine-widget/assets/demos.json"
json-skeleton-key="armorgirl"
animation="animation"
></spine-skeleton>

<spine-skeleton
atlas="/files/spine-widget/assets/atlas2.atlas"
skeleton="/files/spine-widget/assets/demos.json"
json-skeleton-key="greengirl"
animation="animation"
></spine-skeleton>

动画

默认情况下, Web Component 会显示 setup pose. 需要使用 animation 属性来为其设置动画:

html
<spine-skeleton
atlas="/files/spine-widget/assets/spineboy-pma.atlas"
skeleton="/files/spine-widget/assets/spineboy-pro.skel"
animation="walk"
></spine-skeleton>

默认皮肤

默认情况下, Web Component 使用 skeleton 的默认皮肤. 可通过 skin 属性显式设置当前皮肤:

html
<spine-skeleton
atlas="/files/spine-widget/assets/mix-and-match-pma.atlas"
skeleton="/files/spine-widget/assets/mix-and-match-pro.skel"
animation="dance"
skin="full-skins/girl-spring-dress"
></spine-skeleton>

skin 接受以逗号分隔的皮肤名称列表. 皮肤将依序合并为一套新皮肤. 若多个皮肤使用了同一槽位, 则以列表中最后一个皮肤为准.

html
<spine-skeleton
atlas="/files/spine-widget/assets/mix-and-match-pma.atlas"
skeleton="/files/spine-widget/assets/mix-and-match-pro.skel"
animation="dance"
skin="nose/short,skin-base,eyes/violet,hair/brown,clothes/hoodie-orange,legs/pants-jeans,accessories/bag,accessories/hat-red-yellow,eyelids/girly"
></spine-skeleton>

填充模式

Web Component 会根据 fit 属性尝试将 skeleton 动画填充到其所在的容器元素中. 以下为一些示例:

contain: 尽可能地扩大 skeleton, 但仍完全包含在容器元素内(此为默认值)
fill: 通过拉伸 skeleton 来完全填充容器元素
scaleDown: 缩小 skeleton 以确保其完全适应容器元素
none: 显示 skeleton 时无视容器元素尺寸 (此处 [scale](#scale) 属性置为 `0.05`)

其余 fit 模式包括:

  • width: 填充容器元素的宽度, 无视 skeleton 是否在垂直方向溢出容器.
  • height: 填充容器元素的高度, 无视 skeleton 是否在水平方向溢出容器.
  • cover: 尽可能小, 但仍完全覆盖整个容器元素.
  • origin: 无论边界如何, 按照 skeleton 原生尺寸并居中容器元素显示.

边界

Web Component 按照 skeleton 边界来适配容器元素. Skeleton 边界是动画(或多个动画)的边界框(AABB), 或若未指定动画则为 setup pose 的边界框. 为确保边界根据指定的 fit 模式适配父元素, Web Component 会设置 skeleton 的 scaleXscaleY. 这意味着这些属性无法手动更改. 若需直接控制 scaleXscaleY, 请设置 fit="none"fit="origin", 并按下文所述通过 JavaScript 访问 skeleton 对象.

缩放

通过 scale 属性可以设置 Skeleton 加载器的缩放. 有关缩放的更多信息, 请参阅 Spine 运行时指南.

本示例中我们将 fit 模式设为 none, 以便观察缩放比例的变化(否则默认的填充模式会自动修改 scaleXscaleY).

scale="0.3"
scale="0.2"
scale="0.1"

轴偏移

x-axisy-axis 属性用于按容器元素宽高的百分比水平或垂直地平移 skeleton.

fit="none"
scale=".2"
x-axis=".25"
fit="origin"
scale=".2"
fit="origin"
scale=".2"
y-axis="-.5"

像素偏移

使用 offset-xoffset-y 属性可按指定像素数水平或垂直平移 skeleton.

offset-x="0"
offset-y="0"
offset-x="-100"
offset-y="50"

内边距

pad-leftpad-rightpad-toppad-bottom 用于为容器元素设置虚拟内边距. 左右值为容器宽度的百分比, 上下值则为容器高度的百分比.

pad-left="0" pad-right="0" pad-top="0" pad-top="0"
pad-left=".25" pad-right=".25" pad-top=".25" pad-top=".25"

标识符

为 Web Component 分配了标识符后便能通过 spine.getSkeleton 函数检索. 这便于在 JavaScript 代码中访问 SkeletonAnimationState 对象.

<spine-skeleton> 执行异步操作以获取资产. whenReady 方法可用于确定何时 SkeletonAnimationState 对象到达访问就绪的状态.

html
<spine-skeleton
atlas="/files/spine-widget/assets/raptor-pma.atlas"
skeleton="/files/spine-widget/assets/raptor-pro.skel"
identifier="raptor"
></spine-skeleton>
js
const raptor = await spine.getSkeleton("raptor").whenReady;
raptor.skeleton.color.set(1, 0, 0, 1);

剪裁

clip 属性会将容器元素外部的所有内容隐藏.

要注意这样做会将破坏 skeleton 间的合批处理.

html
<spine-skeleton
atlas="/files/spine-widget/assets/tank-pma.atlas"
skeleton="/files/spine-widget/assets/tank-pro.skel"
animation="drive"
fit="height"
pad-top="0.3"
pad-bottom="0.3"
></spine-skeleton>

<spine-skeleton
atlas="/files/spine-widget/assets/tank-pma.atlas"
skeleton="/files/spine-widget/assets/tank-pro.skel"
animation="drive"
fit="height"
pad-top="0.3"
pad-bottom="0.3"
clip
></spine-skeleton>

自定义边界

自定义边界用于聚焦动画的特定细节、放大动画或模拟摄像机移动等用例.

使用 bounds-xbounds-ybounds-widthbounds-height 属性便可定义自定义边界.

该示例会聚焦 Celeste 的面部. 为防止 skeleton 溢出容器, 因此也设置了 clip 属性.

html
<spine-skeleton
atlas="/files/spine-widget/assets/celestial-circus-pma.atlas"
skeleton="/files/spine-widget/assets/celestial-circus-pro.skel"
animation="wings-and-feet"
bounds-x="-155"
bounds-y="650"
bounds-width="300"
bounds-height="350"
clip
></spine-skeleton>

边界自动计算

修改 animation 属性便可更改动画, 此时 Web Component 会像新建画布一样切换到新动画. 但 Web Component 不会重新计算新的边界, 除非设置了 auto-calculate-bounds 属性. 这一默认行为有助于在不同动画间保持一致的 skeleton 尺寸. 不妨将其和 动画边界 属性结合起来使用.

未设置 auto-calculate-bounds
设置了 auto-calculate-bounds
html
<spine-skeleton
identifier="spineboy-auto-bounds-1"
atlas="/files/spine-widget/assets/spineboy-pma.atlas"
skeleton="/files/spine-widget/assets/spineboy-pro.skel"
animation="jump"
></spine-skeleton>

<spine-skeleton
identifier="spineboy-auto-bounds-2"
atlas="/files/spine-widget/assets/spineboy-pma.atlas"
skeleton="/files/spine-widget/assets/spineboy-pro.skel"
animation="jump"
auto-calculate-bounds
></spine-skeleton>
js
const [wc1, wc2] = await Promise.all([
spine.getSkeleton("spineboy-auto-bounds-1").whenReady,
spine.getSkeleton("spineboy-auto-bounds-2").whenReady,
]);
let toogleAnimation = false;
setInterval(() => {
const newAnimation = toogleAnimation ? "jump" : "death";
wc1.setAttribute("animation", newAnimation)
wc2.setAttribute("animation", newAnimation)
toogleAnimation = !toogleAnimation;
}, 4000);

默认mix

default-mix 属性控制的是 AnimationState 的默认mix时长. 其含义为在动画切换时 (无论是通过 animations属性切换还是通过 AnimationState 对象使用 JavaScript 代码来切换) 从一个动画过渡到另一个动画的默认时长(单位为秒).

default-mix="0" (default)
default-mix="1"
html
<spine-skeleton
identifier="spineboy-default-mix-1"
atlas="/files/spine-widget/assets/spineboy-pma.atlas"
skeleton="/files/spine-widget/assets/spineboy-pro.skel"
animation="idle"
default-mix="0"
></spine-skeleton>
<spine-skeleton
identifier="spineboy-default-mix-2"
atlas="/files/spine-widget/assets/spineboy-pma.atlas"
skeleton="/files/spine-widget/assets/spineboy-pro.skel"
animation="idle"
default-mix="1"
></spine-skeleton>
js
const [wc1, wc2] = await Promise.all([
spine.getSkeleton("spineboy-default-mix-1").whenReady,
spine.getSkeleton("spineboy-default-mix-2").whenReady,
]);
let toogleAnimation = false;
setInterval(() => {
const newAnimation = toogleAnimation ? "idle" : "run";
wc1.setAttribute("animation", newAnimation)
wc2.setAttribute("animation", newAnimation)
toogleAnimation = !toogleAnimation;
}, 4000);

动画

使用 animations 属性, 无需编写代码就可以显示动画序列. 以下示例展示了仅使用 Web Component 属性便能复现上文示例的能力:

html
<spine-skeleton
atlas="/files/spine-widget/assets/spineboy-pma.atlas"
skeleton="/files/spine-widget/assets/spineboy-pro.skel"
animation-bounds="walk,run"
default-mix="1"
animations="
[loop, 0, 3.5]
[0, idle, true]
[0, run, true, 4]
"

></spine-skeleton>

可向 animations 属性传入方括号括起的字符串组, 形如:[...][...][...].

组内每个元素表示一段要播放的动画, 其参数以逗号分隔:

  • track: 播放动画所用的轨道号
  • animation name: 动画名称
  • loop: 置为 true 表示循环播放动画 (省略该参数时默认为 false)
  • delay: 在前一段动画开始后等待的秒数, 置为 0 表示一直等待直到前一段动画播放结束(省略该参数时默认为 0)
  • mixDuration: 从前一段动画过渡到该动画的 mix 时长 (省略该参数时默认为 default-mix, 但不适用于轨道上的第一段动画)

如需在播放完最后一段动画后循环播放整条轨道, 可添加一个特殊元素 [loop, trackNumber, repeatDelay], 其中:

  • loop: 表示需要循环播放
  • trackNumber: 需要循环播放的轨道号
  • repeatDelay: 最后一个动画完成到开始新一轮循环前需等待的秒数 (省略该参数时默认为 0)

每根轨道的首个元素将被传入 setAnimation 方法, 而后续元素则传入 addAnimation.

若要使用 setEmptyAnimationaddEmptyAnimation, 则必须将动画名称指定为 #EMPTY#. 此时 loop 参数将被忽略.

请参考以下两个示例.

Spineboy 的 animations 属性值如下:

[loop, 0]
[0, idle, true]
[0, run, false, 2, 0.25]
[0, run]
[0, run]
[0, run-to-idle, false, 0, 0.15]
[0, idle, true]
[0, jump, false, 0, 0.15]
[0, walk, false, 0, 0.05]
[0, death, false, 0, 0.05]

所有动画均位于单条轨道上播放. 动画序列分解如下:

  • [loop, 0]: 表示轨道 0 在播到末尾时返回开头继续循环播放
  • [0, idle, true]: 循环播放 idle 动画
  • [0, run, false, 2, 0.25]: 队列 run 动画, 延迟 2 秒后开始播放, mix时长置为 0.25 秒
  • [0, run]: 队列第二段 run 动画, 不循环
  • [0, run]: 队列第三段 run 动画
  • [0, run-to-idle, false, 0, 0.15]: 队列 run-to-idle 动画, 过渡无延迟, mix时长为 0.15 秒
  • [0, idle, true]: 再队列一段循环的 idle 动画
  • [0, jump, false, 0, 0.15]: 队列 jump 动画, 无延迟, mix时长为 0.15 秒
  • [0, walk, false, 0, 0.05]: 队列 walk 动画, 无延迟, mix时长为 0.05 秒
  • [0, death, false, 0, 0.05]: 队列 death 动画, 无延迟, mix时长为 0.05 秒

Celeste 的 animations 属性值如下::

[0, wings-and-feet, true]
[loop, 1]
[1, #EMPTY#]
[1, eyeblink, false, 2]

本示例使用了两条轨道. 轨道 0 播放 wings-and-feet 动画. 轨道 1 循环播放空动画后接 eyeblink 动画, 延迟 2 秒.

不妨试试修改上方的文本输入框内容, 再点击 Update animation (更新动画)看看效果. 比如将延迟从 2 改为 0.5 会使角色眨眼更频繁. 如需在 5 秒后于轨道 0 上播放 swing 动画并将 mix 时长置为 0.5 秒, 可以追加一行: [0, swing, true, 5, 0.5]

动画边界

animation-bounds 属性用来为多个动画设置边界. 该属性接受动画列表, 并计算可容纳所有动画的边界.

此方法有助于在多个动画间保持缩放比例一致, 防止过渡到边界更大的动画时 skeleton 溢出容器.

未设置 animation-bounds
animation-bounds="walk,jump"

旋转加载动画

spinner 属性用于在加载资源时显示圆形加载动画. 默认情况下加载过程中不会显示任何内容. 下方的按钮模拟的是延迟 1 秒加载并切换 spinner 属性.

html
<spine-skeleton
identifier="spineboy-loading"
atlas="/files/spine-widget/assets/spineboy-pma.atlas"
skeleton="/files/spine-widget/assets/spineboy-pro.skel"
spinner
></spine-skeleton>

<input type="button" value="切换加载动画: 关" onclick="toggleSpinner(this)" />
<input type="
button" value="刷新" onclick="reloadWidget(this)" />
js
const wcLoading = spine.getSkeleton("spineboy-loading");
async function reloadWidget(element) {
element.disabled = true;
await wcLoading.whenReady;
wcLoading.loading = true;
setTimeout(() => {
element.disabled = false;
wcLoading.loading = false;
}, 1000)
}
function toggleSpinner(element) {
wcLoading.spinner = !wcLoading.spinner;
element.value = wcLoading.spinner ? "切换加载动画: 关" : "切换加载动画: 开";
}

离屏行为

离屏的 Web Component不会被渲染. 默认情况下, 离屏时不会调用 AnimationState 的 update、Skeleton 的 update、skeleton.applyskeleton.updateWorldTransform 函数. 这便是 offscreen=pause 的效果.

为确保即使离屏也调用更新函数, 请设置为 offscreen=update.

而希望无论可见性如何都调用所有函数时, 则需设置 offscreen=pose.

pause
update
pose

当刷新此页面且三个 skeleton 均在视口内可见时, 将同步地开始播放这些动画. 但第一个 skeleton 设置为了 offscreen="pause", 其状态会在页面滚动出视图时暂停. 当重新进入视图时, 其动画将从暂停处恢复, 此时它会与其他两个 skeleton 动画失去同步. 此时其他两个 skeleton 仍保持同步, 但可能因物理效果等原因出现细微差异.

为防止在离屏时暂停 skeleton 动画, 建议使用 update 行为. 这样做能避免调用 updateWorldTransform, 因为该函数是典型的 CPU 密集函数.

自定义更新

Web Component 中 skeletonstate 的更新和应用与其他运行时的行为一致.

beforeUpdateWorldTransformsafterUpdateWorldTransforms 属性可分别在调用 updateWorldTransform 前后加入自定义逻辑.

如需完全替换默认更新行为, 可以将函数分配给 update 属性. 这将替换状态更新和 skeleton 更新行为, 包括离屏行为设置. 此时必须由开发者来管理 updateapplyupdateWorldTransform 的调用. onScreen 属性在组件可见时值为 true, 这一特性在你手动管理调用时能提供些许便利.

updateapplyupdateWorldTransform 这三个函数的函数签名完全相同: (delta: number, skeleton: Skeleton, state: AnimationState) => void

拖放

设置 drag 属性可启用 Web Component 的拖放功能. 不过由于可能增加 CPU 使用率, 因此建议仅在需要可拖拽行为时启用该属性.

鼠标指针位置

以下属性可在不同坐标空间中确定鼠标指针位置:

对于 spine-skeleton:

  • pointerWorldXpointerWorldY: 这是指针相对于 skeleton 原点的 XY 坐标(Spine 世界坐标).
  • worldXworldY: 这是相对于画布/WebGL context 原点的 skeleton 原点 XY 坐标(Spine 世界坐标).

对于 spine-overlay:

  • pointerCanvasXpointerCanvasY: 表示的是指针相对于画布左上角的 XY 坐标(屏幕坐标).
  • pointerWorldXpointerWorldY: 指针相对于画布/WebGL context 原点的 XY 坐标(Spine 世界坐标).

通过以上属性可实现与 Web Component 的交互. 例如在以下示例中, 猫头鹰的眼睛会一直看向鼠标指针.

html
<spine-skeleton
identifier="owl-pointer"
atlas="/files/spine-widget/assets/owl-pma.atlas"
skeleton="/files/spine-widget/assets/owl-pro.skel"
animations="[0, idle, true][1, blink, true]"
></spine-skeleton>
js
const wc = await spine.getSkeleton("owl-pointer").whenReady;
const controlBone = wc.skeleton.findBone("control");
const tempVector = new spine.Vector3();
wc.afterUpdateWorldTransforms = () => {
controlBone.parent.worldToLocal(tempVector.set(wc.pointerWorldX, wc.pointerWorldY));
controlBone.x = controlBone.data.x + tempVector.x / wc.overlay.canvas.width * 30;
controlBone.y = controlBone.data.y + tempVector.y / wc.overlay.canvas.height * 30;
}

交互回调

设置 interactive 属性可为 Web Component 附加回调函数, 以便处理指针交互.

回调可响应 Web Component 边界 内的交互, 或特定槽位的交互. 支持的事件(PointerEventType)包括 downupenterleavemovedrag.

添加回调有两种方法:

  • 设置 pointerEventCallback: (event: PointerEventType) => void 以处理 Web Component 边界内的指针动作.
  • 调用 addPointerSlotEventCallback (slotRef: number | string | Slot, slotFunction: (slot: Slot, event: PointerEventType) => void) 来处理某个槽位其附件边界内的指针操作.

在以下示例中:

  • pointerEventCallbackenter 时触发 jump 动画, 在 leave 时触发 wave 动画.
  • addPointerSlotEventCallbackhead-base 槽位(面部)添加回调. 当附件收到 down 事件时, 根据两个调色板中的颜色更新 Normal 和 Dark tint 色.

Tint normal:
Tint black:
html
<spine-skeleton
identifier="interactive0"
atlas="/files/spine-widget/assets/chibi-stickers-pma.atlas"
skeleton="/files/spine-widget/assets/chibi-stickers.skel"
skin="mario"
animation="emotes/wave"
animation-bounds="emotes/wave,emotes/hooray"
pages="0,4"
interactive
></spine-skeleton>

<spine-skeleton
identifier="interactive1"
atlas="/files/spine-widget/assets/chibi-stickers-pma.atlas"
skeleton="/files/spine-widget/assets/chibi-stickers.skel"
skin="nate"
animation="emotes/wave"
animation-bounds="emotes/wave,emotes/hooray"
pages="0,6"
interactive
></spine-skeleton>

Tint normal: <input type="color" id="color-picker" value="#ff0000" style="margin: 0;" />
Tint black: <input type="color" id="dark-picker" value="#000000" style="margin: 0;"/>
js
const colorPicker = document.getElementById("color-picker");
const darkPicker = document.getElementById("dark-picker");
[0, 1].forEach(async (i) => {
const wc = await spine.getSkeleton(`interactive${i}`).whenReady;
wc.pointerEventCallback = (event) => {
if (event === "enter") wc.state.setAnimation(0, "emotes/hooray", true).mixDuration = .15;
if (event === "leave") wc.state.setAnimation(0, "emotes/wave", true).mixDuration = .25;
}
const tempColor = new spine.Color();
const slot = wc.skeleton.findSlot("head-base");
slot.darkColor = new spine.Color(0, 0, 0, 1);
wc.addPointerSlotEventCallback(slot, (slot, event) => {
if (event === "down") {
slot.darkColor.setFromColor(spine.Color.fromString(darkPicker.value, tempColor));
slot.color.setFromColor(spine.Color.fromString(colorPicker.value, tempColor));
}
});
})

调试模式

通过设置 debug 属性可启用调试模式. 此模式下会显示以下视觉标记:

  • skeleton 的世界原点(绿色)
  • 根骨骼位置(红色)
  • 边界 矩形及其中心(蓝色)
  • 若将 Web Component 设为可拖拽, 则会显示可拖拽区域(半透明红色).

在本例中, 我们偏移了根节点位置来避免与原点重叠.

html
<spine-skeleton
style="width: 200px; height: 200px;"
identifier="sack-debug"
atlas="/files/spine-widget/assets/sack-pma.atlas"
skeleton="/files/spine-widget/assets/sack-pro.skel"
animation="cape-follow-example"
drag
offscreen="pose"
debug
></spine-skeleton>
js
spine.getSkeleton("sack-debug").whenReady
.then(({ skeleton }) => skeleton.getRootBone().x += 50);

Atlas页

当使用多个 Atlas 页(例如每个皮肤一个页)且仅需显示部分页面时, 可使用 pages 属性指定要加载的 Atlas 页. 该属性接受所需页的索引号, 应以逗号分隔的列表形式提供.

pages="0,6"
pages="0,4"
pages="0,1"

如需程序化加载texture, 请将 pages 属性置空: pages="". 这样做会只加载 skeleton 和 atals 数据但不加载任何texture, 你可以稍后手动按需加载 texture.

html
<spine-skeleton
identifier="dragon"
style="flex: 0.8; height: 100%;"
atlas="/files/spine-widget/assets/dragon-pma.atlas"
skeleton="/files/spine-widget/assets/dragon-ess.skel"
animation="flying"
pages=""
></spine-skeleton>

<input type="button" value="加载 page 0" onclick="loadPageDragon(0)" />
<input type="
button" value="加载 page 1" onclick="loadPageDragon(1)" />
<input type="
button" value="加载 page 2" onclick="loadPageDragon(2)" />
<input type="
button" value="加载 page 3" onclick="loadPageDragon(3)" />
<input type="
button" value="加载 page 4" onclick="loadPageDragon(4)" />
js
async function loadPageDragon(pageIndex) {
const dragon = await spine.getSkeleton("dragon").whenReady;
if (!dragon.pages.includes(pageIndex)) {
dragon.pages.push(pageIndex);
dragon.loadTexturesInPagesAttribute();
}
}

跟随槽位

该属性可使一个 HTMLElement 跟随某个槽位. 一般用于将动态内容(例如文本对话框)集成到动画中.

调用 followSlot 函数并传入以下参数:

  • 要跟随的 Slot 实例或槽位名称

  • 将跟随槽位的 HTMLElement

  • 包含以下属性的 "options" 对象:

    • followOpacity: 联动的槽位 Alpha 的 HTMLElement 不透明度
    • followScale: 联动的槽位缩放值的 HTMLElement 缩放值
    • followRotation: 联动的槽位旋转值的 HTMLElement 旋转值
    • followVisibility: 根槽位槽附件的可见性来显示或隐藏 HTMLElement 元素
    • hideAttachment: 隐藏槽位附件, 仿佛 HTMLElement 在视觉上将其替代

html
<spine-skeleton
style="width: 200px; height: 200px;"
identifier="potty"
atlas="/files/spine-widget/assets/cloud-pot-pma.atlas"
skeleton="/files/spine-widget/assets/cloud-pot.skel"
animation="playing-in-the-rain"
></spine-skeleton>

<div id="rain/rain-color" style="font-size: 50px; display: none;">A</div>
<div id="rain/rain-white" style="font-size: 50px; display: none;">B</div>
<div id="rain/rain-blue" style="font-size: 50px; display: none;">C</div>
<div id="rain/rain-green" style="font-size: 50px; display: none;">D</div>
js
const wc = await spine.getSkeleton("potty").whenReady;
const options = { followVisibility: false, hideAttachment: true };
wc.followSlot("rain/rain-color", document.getElementById("rain/rain-color"), options);
wc.followSlot("rain/rain-white", document.getElementById("rain/rain-white"), options);
wc.followSlot("rain/rain-blue", document.getElementById("rain/rain-blue"), options);
wc.followSlot("rain/rain-green", document.getElementById("rain/rain-green"), options);

followSlot 即使与其他 Spine Web Component 一起使用也有效! 它即使处于拖放状态时也能正常工作!

html
<spine-skeleton
style="width: 200px; height: 200px;"
identifier="potty2"
atlas="/files/spine-widget/assets/cloud-pot-pma.atlas"
skeleton="/files/spine-widget/assets/cloud-pot.skel"
animation="rain"
drag
offscreen="pose"
></spine-skeleton>

<spine-skeleton identifier="potty2-1" atlas="/files/spine-widget/assets/raptor-pma.atlas" skeleton="/files/spine-widget/assets/raptor-pro.skel" animation="walk" style="height:200px; width: 200px;"></spine-skeleton>
<spine-skeleton identifier="potty2-2" atlas="/files/spine-widget/assets/spineboy-pma.atlas" skeleton="/files/spine-widget/assets/spineboy-pro.skel" animation="walk" style="height:200px; width: 200px;"></spine-skeleton>
<spine-skeleton identifier="potty2-3" atlas="/files/spine-widget/assets/celestial-circus-pma.atlas" skeleton="/files/spine-widget/assets/celestial-circus-pro.skel" animation="wings-and-feet" style="height:200px; width: 200px;"></spine-skeleton>
<spine-skeleton identifier="potty2-4" atlas="/files/spine-widget/assets/goblins-pma.atlas" skeleton="/files/spine-widget/assets/goblins-pro.skel" skin="goblingirl" animation="walk" style="height:200px; width: 200px;"></spine-skeleton>
js
const wc = await spine.getSkeleton("potty2").whenReady;
const options = { followVisibility: false, hideAttachment: true };
wc.followSlot("rain/rain-color", spine.getSkeleton("potty2-1"), options);
wc.followSlot("rain/rain-white", spine.getSkeleton("potty2-2"), options);
wc.followSlot("rain/rain-blue", spine.getSkeleton("potty2-3"), options);
wc.followSlot("rain/rain-green", spine.getSkeleton("potty2-4"), options);

进阶用例

程序化创建

可使用 createSkeleton 函数程序化地创建 <spine-skeleton> 元素. 该函数接受一个对象, 其中每个属性对应 Web Component 属性的驼峰命名版本.

html
<div style="display: flex; flex-wrap: wrap; justify-content: space-evenly; align-items: center; width: 100%;">
<script>
["soeren", "sinisa", "luke"].forEach(skin => {
["emotes/wave", "movement/trot-left", "emotes/idea", "emotes/hooray"].forEach(animation => {
const wc = spine.createSkeleton({
   atlasPath: "/files/spine-widget/assets/chibi-stickers-pma.atlas",
   skeletonPath: "/files/spine-widget/assets/chibi-stickers.skel",
   animation,
   skin,
   pages: [0, 3, 7, 8],
});
wc.style.width = "25%";
wc.style.height = "100px";
document.currentScript.parentElement.appendChild(wc);
})
})
</script>
</div>

另一种方案是使用标准 DOM 操作方法, 直接将 Web Component 作为 HTML 置于 DOM 末尾.

html
<div style="display: flex; flex-wrap: wrap; justify-content: space-evenly; align-items: center; width: 100%;">
<script>
["harri", "misaki", "spineboy"].forEach(skin => {
["emotes/wave", "movement/trot-left", "emotes/idea", "emotes/hooray"].forEach(animation => {
document.currentScript.parentElement.insertAdjacentHTML('beforeend', `<spine-skeleton
   style="width: 25%; height: 100px;"
   atlas="/files/spine-widget/assets/chibi-stickers-pma.atlas"
   skeleton="/files/spine-widget/assets/chibi-stickers.skel"
   animation="${animation}"
   skin="${skin}"
   pages="0,2,5,9"
></spine-skeleton>
`);
});
});
</script>
</div>

默认情况下, 资产会立即加载.

如需延迟资产加载, 请设置 manualStart: "false". 此时在 <spine-skeleton> 元素创建后, 将通过异步方法 appendTo 将元素追加到 DOM 末尾. 最后在时机恰当时, 可调用元素中的 start() 即可开始资产加载. 请注意, 任何对 SkeletonAnimationState 的操作必须推迟到 whenReady 判断为真之后.

销毁

从 DOM 中移除 Web Component 不会触发 Web Component 的自动销毁, 这样设计的原因是因为可能在其他地方仍需要重用它. 因此请调用其 dispose() 方法来进行显式销毁. 此操作是安全的, 不会释放仍被其他 Web Component 使用的资源.

如需销毁全部 spine-webcomponents 资源, 请在覆盖层(overlay)实例上调用 dispose().

dispose.html 示例展示了如何使用 dispose 函数.

手动创建覆盖层

当在页面中添加 <spine-skeleton> 时, Web Component 会自动追加一个 <spine-overlay> 来容纳渲染 Skeleton 的 WebGL 画布. 此覆盖层(overlay)覆盖整个浏览器视口.

不过你也可以手动将 <spine-overlay> 追加到某个 HTML 元素中. 此时它将继承其父元素的尺寸. 如需在手动添加的覆盖层中渲染 <spine-skeleton>, 则 skeleton 和覆盖层需要拥有相同的 overlay-id.

无论覆盖层在 HTML 元素中哪个位置, 它始终都会被重定位为最后一个子元素. 这样才能确保覆盖层始终位于其他元素之上. 为减少不必要的 DOM 分离和附加操作, 建议将覆盖层放在目标容器的最末元素位置.

手动创建覆盖层适用于以下用例:

  1. 可滚动(Scrollable)容器
    • skeleton 可能在容器完全可见前溢出容器.
    • skeleton 在滚动时出现显示延迟, 这一问题在低刷新率显示器上尤其明显.

  2. 固定/粘性(Fixed/sticky)定位容器
    • skeleton 在滚动时可能出现卡顿或忽快忽慢的问题.

  3. 自定义覆盖层定位
    • 适用于需要更小尺寸的覆盖层, 或覆盖层不直接附加到 <body> 而应附加到某个容器节点的情况.

以上情况在以下示例中均有展示. 第一个可滚动列表使用默认覆盖层, 第二个列表则是位于可滚动 <div> 内的专用覆盖层. 点击按钮将把容器 <div> 设为固定位置, 以此演示滚动时动画卡顿的情况.

html
<div style="display: flex; flex-direction: column;">
<button id="popup-overlay-button-open">设置固定位置</button>
<div style="height: 250px; display: flex;">
<div id="fixed" style="display: flex;">
<div style="overflow-y: auto; width: 150px; height: 250px; border: 1px solid black; padding: 1px; background: white;">
<script>
   for (let i = 0; i < 6; i++)
    document.currentScript.parentElement.insertAdjacentHTML('beforeend', `
    <spine-skeleton style="height:80px; width: 120px; border: 1px solid black;"
    atlas="/files/spine-widget/assets/spineboy-pma.atlas"
    skeleton="/files/spine-widget/assets/spineboy-pro.skel"
    animation="walk"
    ></spine-skeleton>`);
</script>
</div>
<div style="overflow-y: auto; width: 150px; height: 250px; border: 1px solid black; padding: 1px; background: white;">
<spine-overlay overlay-id="scroll"></spine-overlay>
<script>
   for (let i = 0; i < 6; i++)
    document.currentScript.parentElement.insertAdjacentHTML('beforeend', `
    <spine-skeleton style="height:80px; width: 120px; border: 1px solid black;"
    overlay-id="scroll"
    atlas="/files/spine-widget/assets/spineboy-pma.atlas"
    skeleton="/files/spine-widget/assets/spineboy-pro.skel"
    animation="walk"
    ></spine-skeleton>`);
</script>
</div>
</div>
</div>
</div>
js
let positionFixed = false;
const openPopupButton = document.getElementById('popup-overlay-button-open');
const popupOverlay = document.getElementById('fixed');
openPopupButton.addEventListener('click', function() {
if (positionFixed) {
popupOverlay.style.position = "";
popupOverlay.style.top = "";
popupOverlay.style.background = "";
openPopupButton.innerText = "设置固定位置";
} else {
popupOverlay.style.position = 'fixed';
popupOverlay.style.top = 'calc(50% - 125px)';
popupOverlay.style.background = 'white';
openPopupButton.innerText = "取消固定位置";
}
positionFixed = !positionFixed;
});

请注意每个覆盖层都会创建其专用的 WebGL context, 这会占用可用的 WebGL context 数量.