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>, 以及一组相关的工具函数.
在上述示例中, 文件从 UNPKG (一个快速的 NPM CDN)加载. URL 中包含版本号(4.2), 该版本号必须与导出 skeleton 的 Spine 编辑器版本一致. 补丁版本中的星号(*)则会从 major.minor 版本加载最新的 JavaScript 代码.
如需从 UNPKG CDN 加载压缩后的文件, 请使用 .min.js 扩展名:
或者, 也可以通过从 UNPKG 下载文件, 或从 GitHub 仓库 中的源码构建来自托管该文件. 该仓库还包含如何使用 NPM 或 Yarn 集成 spine-webcomponents 的说明.
使用spine-skeleton
导入 JavaScript 文件后, 无需额外 JavaScript 即可在 HTML 中直接使用 Web Component:
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 |
<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
skeleton 和 atlas 是两个必填属性, 分别定义了 skeleton 的 .json 或二进制 .skel 文件及 .atlas 文件的源路径. 这些路径可以是相对 URL 也可以是绝对 URL.
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 编码的内容. 此时, skeleton 和 atlas 属性应引用此对象中对应的资源名称. raw-data 属性用于启用此设置.
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的尺寸, 请使用标准的 style 或 class 属性. 这些属性可以实现你需要的任意样式.
.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 资产管理器仅会将每个资产加载一次, 即使在页面中多次使用.
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 属性来为其设置动画:
atlas="/files/spine-widget/assets/spineboy-pma.atlas"
skeleton="/files/spine-widget/assets/spineboy-pro.skel"
animation="walk"
></spine-skeleton>
默认皮肤
默认情况下, Web Component 使用 skeleton 的默认皮肤. 可通过 skin 属性显式设置当前皮肤:
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 接受以逗号分隔的皮肤名称列表. 皮肤将依序合并为一套新皮肤. 若多个皮肤使用了同一槽位, 则以列表中最后一个皮肤为准.
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 的 scaleX 和 scaleY. 这意味着这些属性无法手动更改. 若需直接控制 scaleX 和 scaleY, 请设置 fit="none" 或 fit="origin", 并按下文所述通过 JavaScript 访问 skeleton 对象.
缩放
通过 scale 属性可以设置 Skeleton 加载器的缩放. 有关缩放的更多信息, 请参阅 Spine 运行时指南.
本示例中我们将 fit 模式设为 none, 以便观察缩放比例的变化(否则默认的填充模式会自动修改 scaleX 和 scaleY).
scale="0.3"scale="0.2"scale="0.1"轴偏移
x-axis 和 y-axis 属性用于按容器元素宽高的百分比水平或垂直地平移 skeleton.
fit="none"scale=".2"x-axis=".25"fit="origin"scale=".2"fit="origin"scale=".2"y-axis="-.5"像素偏移
使用 offset-x 和 offset-y 属性可按指定像素数水平或垂直平移 skeleton.
offset-x="0" offset-y="0" offset-x="-100" offset-y="50" 内边距
pad-left、pad-right、pad-top 和 pad-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 代码中访问 Skeleton 和 AnimationState 对象.
<spine-skeleton> 执行异步操作以获取资产. whenReady 方法可用于确定何时 Skeleton 和 AnimationState 对象到达访问就绪的状态.
atlas="/files/spine-widget/assets/raptor-pma.atlas"
skeleton="/files/spine-widget/assets/raptor-pro.skel"
identifier="raptor"
></spine-skeleton>
raptor.skeleton.color.set(1, 0, 0, 1);
剪裁
clip 属性会将容器元素外部的所有内容隐藏.
要注意这样做会将破坏 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-x、bounds-y、bounds-width 和 bounds-height 属性便可定义自定义边界.
该示例会聚焦 Celeste 的面部. 为防止 skeleton 溢出容器, 因此也设置了 clip 属性.
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 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>
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" 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>
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 属性便能复现上文示例的能力:
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.
若要使用 setEmptyAnimation 或 addEmptyAnimation, 则必须将动画名称指定为 #EMPTY#. 此时 loop 参数将被忽略.
请参考以下两个示例.
Spineboy 的 animations 属性值如下:
[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 属性值如下::
[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 属性.
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)" />
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.apply 和 skeleton.updateWorldTransform 函数. 这便是 offscreen=pause 的效果.
为确保即使离屏也调用更新函数, 请设置为 offscreen=update.
而希望无论可见性如何都调用所有函数时, 则需设置 offscreen=pose.
pause update pose 当刷新此页面且三个 skeleton 均在视口内可见时, 将同步地开始播放这些动画. 但第一个 skeleton 设置为了 offscreen="pause", 其状态会在页面滚动出视图时暂停. 当重新进入视图时, 其动画将从暂停处恢复, 此时它会与其他两个 skeleton 动画失去同步. 此时其他两个 skeleton 仍保持同步, 但可能因物理效果等原因出现细微差异.
为防止在离屏时暂停 skeleton 动画, 建议使用 update 行为. 这样做能避免调用 updateWorldTransform, 因为该函数是典型的 CPU 密集函数.
自定义更新
Web Component 中 skeleton 和 state 的更新和应用与其他运行时的行为一致.
beforeUpdateWorldTransforms 和 afterUpdateWorldTransforms 属性可分别在调用 updateWorldTransform 前后加入自定义逻辑.
如需完全替换默认更新行为, 可以将函数分配给 update 属性. 这将替换状态更新和 skeleton 更新行为, 包括离屏行为设置. 此时必须由开发者来管理 update、apply 和 updateWorldTransform 的调用. onScreen 属性在组件可见时值为 true, 这一特性在你手动管理调用时能提供些许便利.
update、apply 和 updateWorldTransform 这三个函数的函数签名完全相同: (delta: number, skeleton: Skeleton, state: AnimationState) => void
拖放
设置 drag 属性可启用 Web Component 的拖放功能. 不过由于可能增加 CPU 使用率, 因此建议仅在需要可拖拽行为时启用该属性.
鼠标指针位置
以下属性可在不同坐标空间中确定鼠标指针位置:
对于 spine-skeleton:
pointerWorldX和pointerWorldY: 这是指针相对于 skeleton 原点的 XY 坐标(Spine 世界坐标).worldX和worldY: 这是相对于画布/WebGL context 原点的 skeleton 原点 XY 坐标(Spine 世界坐标).
对于 spine-overlay:
pointerCanvasX和pointerCanvasY: 表示的是指针相对于画布左上角的 XY 坐标(屏幕坐标).pointerWorldX和pointerWorldY: 指针相对于画布/WebGL context 原点的 XY 坐标(Spine 世界坐标).
通过以上属性可实现与 Web Component 的交互. 例如在以下示例中, 猫头鹰的眼睛会一直看向鼠标指针.
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>
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)包括 down、up、enter、leave、move 和 drag.
添加回调有两种方法:
- 设置
pointerEventCallback: (event: PointerEventType) => void以处理 Web Component 边界内的指针动作. - 调用
addPointerSlotEventCallback (slotRef: number | string | Slot, slotFunction: (slot: Slot, event: PointerEventType) => void)来处理某个槽位其附件边界内的指针操作.
在以下示例中:
pointerEventCallback在enter时触发jump动画, 在leave时触发wave动画.addPointerSlotEventCallback为head-base槽位(面部)添加回调. 当附件收到down事件时, 根据两个调色板中的颜色更新 Normal 和 Dark tint 色.
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;"/>
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 设为可拖拽, 则会显示可拖拽区域(半透明红色).
在本例中, 我们偏移了根节点位置来避免与原点重叠.
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>
.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.
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)" />
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在视觉上将其替代
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>
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 一起使用也有效! 它即使处于拖放状态时也能正常工作!
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>
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 属性的驼峰命名版本.
<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 末尾.
<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() 即可开始资产加载. 请注意, 任何对 Skeleton 或 AnimationState 的操作必须推迟到 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 分离和附加操作, 建议将覆盖层放在目标容器的最末元素位置.
手动创建覆盖层适用于以下用例:
- 可滚动(Scrollable)容器
- skeleton 可能在容器完全可见前溢出容器.
- skeleton 在滚动时出现显示延迟, 这一问题在低刷新率显示器上尤其明显.
- 固定/粘性(Fixed/sticky)定位容器
- skeleton 在滚动时可能出现卡顿或忽快忽慢的问题.
- 自定义覆盖层定位
- 适用于需要更小尺寸的覆盖层, 或覆盖层不直接附加到
<body>而应附加到某个容器节点的情况.
- 适用于需要更小尺寸的覆盖层, 或覆盖层不直接附加到
以上情况在以下示例中均有展示. 第一个可滚动列表使用默认覆盖层, 第二个列表则是位于可滚动 <div> 内的专用覆盖层. 点击按钮将把容器 <div> 设为固定位置, 以此演示滚动时动画卡顿的情况.
<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>
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 数量.