spine-canvaskit 运行时文档

Licensing

将官方的Spine运行时整合到你的应用程序之前, 请仔细阅读 Spine运行时许可页面.

简介

spine-canvaskit是一个基于TypeScript的运行时, 用于在浏览器和Node.js环境中使用 CanvasKit 加载、操作和渲染Spine skeleton. 因此spine-canvaskit既可以用在前端(渲染UI元素) 也可以用在后端 (无头化渲染skeleton).

spine-canvaskit要求使用CanvasKit 0.39.1及以上版本, 并支持除 双色tinting 外的全部Spine功能.

spine-canvaskit构建于spine-core之上, spine-core是Spine运行时核心API的TypeScript实现. 有关核心API的更多信息, 请参见 Spine运行时指南 一文.

安装

请注意: spine-canvaskit的 major.minor 版本与用来导出资产的Spine编辑器的 major.minor 版本必须一致. 更多详情请见 Spine 编辑器和运行时版本管理指南 一节.

NPM和Yarn

使用NPM或Yarn都能将spine-canvaskit添加到你的项目中:

npm install @esotericsoftware/spine-canvaskit@^4.2.0
yarn add @esotericsoftware/spine-canvaskit@^4.2.0

spine-canvaskit是一个 ECMAScript模块, 可原生地在Node.js和所有现代浏览器中使用, 也能通过webpack、rollup或esbuild等工具打包. 它包含了源码映射(source maps), 以便于开发调试.

请注意: 在用import导入spine-canvaskit模块中的类、枚举或函数后, 即可在你的代码中使用. 例如 import { loadTextureAtlas } from "@esotericsoftware/spine-canvaskit

原生JavaScript

添加引用了 unpkg CDN 的script标签(tag)便能在原生JavaScript项目中添加spine-canvas:

<script src="https://unpkg.com/canvaskit-wasm@latest/bin/canvaskit.js"></script>
<script src="https://unpkg.com/@esotericsoftware/spine-canvaskit@4.2.*/dist/iife/spine-canvaskit.js"></script>

启用源码映射便能在调试运行时的原始TypeScript源码.

我们还提供了spine-canvaskit的压缩版本, 将unpkg URL中的 .js 替换为 min.js 就能使用压缩版的spine-canvaskit.

<script src="https://unpkg.com/canvaskit-wasm@latest/bin/canvaskit.js"></script>
<script src="https://unpkg.com/@esotericsoftware/spine-canvaskit@4.2.*/dist/iife/spine-canvaskit.js"></script>

请注意: 如果你在原生JavaScript项目中引入了spine-canvaskit, 你必须通过全局的spine对象访问所有类、枚举和函数, 比如 spine.loadTextureAtlas()spine.SkeletonData. 下文中的代码示例均已省略 spine 对象.

示例

spine-canvaskit运行时内置了数个用于功能展示的示例.

你可按以下步骤运行示例项目:

  1. 安装 Node.js
  2. 克隆 spine-runtimes代码库
  3. 在终端中输入:
    cd path/to/spine-runtimes/spine-ts
    npm run dev

它会自动打开浏览器窗口并显示所有的spine-ts运行时示例. 在 CanvasKit 一节中能找到全部与spine-canvaskit相关的示例.

其中有如下几个示例:

更新spine-canvaskit运行时

在更新项目中的spine-canvaskit运行时前, 请先阅读 Spine 编辑器和运行时版本管理指南.

更新spine-haxe运行时只需修改 package.json 文件中 spine-canvaskit 包的版本号, 然后再次运行 npm install 即可. 使用原生JavaScript的话则请更新script标签中的unpkg URL.

请注意: 若更改了 spine-canvaskitmajor.minor 版本, 则必须使用同版本的Spine编辑器重新导出Spine skeleton!

使用spine-canvaskit

资产管理

导出适用于spine-canvaskit的Spine资产

请按Spine用户指南中的说明进行操作来:

  1. 导出skeleton和动画数据
  2. 导出包含skeleton图像的texture atlases

请注意: spine-canvaskit会在atlas图片中自动使用premultiplied alpha, 因此导出atlases时请勿使用premultiplied alpha!

导出的skeleton数据和texture atlas将产生以下文件:

  1. skeleton-name.jsonskeleton-name.skel 文件, 包含了skeleton和动画数据.
  2. skeleton-name.atlas, 包含了texture atlas的相关信息.
  3. 单个或数个 .png 文件, texture atlas中的一页就对应着这样一个文件, 而texture atlas则保存着skeleton所需的图片包.

请注意: 你应该将skeleton优先导出为二进制格式而非JSON格式, 因为二进制格式尺寸更小且加载速度更快..

以上这些文件便是需要导入运行时呈现的资产文件.

更新Spine资产

在开发过程中, 你可能需要经常更新Spine skeleton数据和texture atlas文件. 只需从Spine编辑器重新导出并替换掉项目中的资产文件即可更新这些源文件(.json, .skel, .atlas, .png).

应确保spine-haxe的 major.minor 版本与用来导出资产的Spine编辑器的 major.minor 版本一致. 更多详情请见 同步版本号 一节.

初始化CanvasKit

spine-canvaskit使用CanvasKit来加载和渲染Spine skeletons. 在使用spine-canvaskit之前, 需要先初始化CanvasKit.

若是使用NodeJS或者启用了ES6的浏览器:

import CanvasKitInit from "canvaskit-wasm";

const ck = await CanvasKitInit();

若是在script标签中原生JavaScript的浏览器:

<script src="https://unpkg.com/canvaskit-wasm@latest/bin/canvaskit.js"></script>
<script type="module">
const ck = await CanvasKitInit();
</script>

下文中的代码示例均将初始化过的 CanvasKit 对象的引用存储于 ck 中.

核心类

spine-canvaskit API构建在基于TypeScript的通用 spine-core 运行时之上, 该运行时提供了平台无关的核心类和算法,用于加载、查询、修改和动画化Spine skeleton. 这些核心类也是spine-canvaskit的一部分.

本节将简要讨论日常使用spine-canvaskit时会遇到的关键核心类. 请同时查阅 Spine 运行时指南 详细了解Spine运行时的架构、核心类及API使用方法.

Atlas 类存储了从 .atlas 文件及其对应 .png 图片中加载的数据.

SkeletonData.json.skel skeleton文件中加载的数据. Skeleton数据包含关于骨骼层次结构、槽位、附件、约束、皮肤和动画的信息. 通常做法是通过 Atlas 来加载 SkeletonData 实例, 实例再从 Atlas 中获取Skelton所需的图片. 因此 Atlas 可视为创建 Skeleton 实例的蓝图. 同一套atlas和Skelton数据可以实例化出多个skeleton实例, 这些实例将共享被加载的数据, 由此最大限度地缩短运行时加载时间并减小内存开销.

Skeleton 类存储的是用 SkeletonData 实例创建的Skeleton实例. Skeleton实例会存储其当前pose, 即当前的骨骼位置和槽位、附件及活动皮肤的配置. 虽然手动修改骨骼变换(transform)可以更改当前pose, 但更常规的做法是应用 AnimationState 中的动画来更改当前pose.

AnimationState 类负责跟踪需应用于skeleton的动画, 根据上一帧和当前渲染帧之间的时间提前和mix这些动画, 并将动画应用于skeleton实例, 从而设置其当前pose. AnimationState 会查询 AnimationStateData 以获取动画间的mix时间, 如果一对动画之间没有设置mix时间, 则会使用默认mix时间.

spine-canvaskit扩展了spine-core的功能, 让加载、修改和渲染Spine skeleton更为便利.

加载资产

通过 loadTextureAtlas()loadSkeletonData() 函数分别用于加载Spine atlas和skeleton数据文件.

spine-canvaskit可在任何支持CanvasKit的JavaScript环境中运行, 例如NodeJS和浏览器. 因此这些加载函数是平台无关的, 只要给函数传入一个绝对或相对路径, 它就能返回一个包含文件原始二进制内容的 Buffer (NodeJS)或 ArrayBuffer (浏览器)对象.

加载器函数的TypeScript签名如下的:

readFile(path: string): Promise<any>

在NodeJS中其实现是这样:

import * as fs from "fs"

async function readFile(path) {
return fs.readFileSync(path)
}

而在浏览器环境中则可以这样实现:

async function readFile(path) {
const response = await fetch(path);
if (!response.ok) throw new Error("Could not load file " + path);
return await response.arrayBuffer();
}

loadTextureAtlas() 函数加载 .atlas 文件和其对应 .png 页图片文件的写法是:

const atlas = await loadTextureAtlas(ck, "myatlas.atlas", readFile);

运行时会解析 .atlas 文件所在目录下的Atlas页 .png 文件.

类似地, 用 loadSkeletonData() 函数加载 .json.skel 格式的skeleton数据文件则应该:

const skeletonData = await loadSkeletonData("myskeleton.skel", atlas, readFile);

loadSkeletonData() 既可以加载 .json 格式也能加载 .skel 格式文件, 它用文件名后缀来区分加载逻辑的. atlas 中引用着渲染skeleton所需的图片.

SkeletonDrawable

SkeletonDrawable 是对 SkeletonAnimationState 的封装, 前者存储了skeleton的当前pose和皮肤, 后者负责跟踪和应用动画.

加载完skeleton的atlas和skeleton数据文件后, 就便可据此实例化一个乃至数个 SkeletonDrawable 实例. 可以发现, SkeletonData 也隐式地引用了 TextureAtlas.

调用 SkeletonDrawable 构造函数便可新建一个实例:

const drawable = new SkeletonDrawable(skeletonData);

通过各个字段访问可以 SkeletonDrawable 中的 SkeletonAnimationState 对象:

// Position and scale the skeleton
const skeleton = drawable.skeleton
skeleton.x = 300;
skeleton.y = 380;
skeleton.scaleX = skeleton.scaleY = 0.5;

// Queue an animation on the animation state
const animationState = drawable.animationState;
animationState.setAnimation(0, "walk", true);

有关 SkeletonAnimationState API 的详情, 参见 Spine运行时指南 一文.

应用动画

请注意: 请参阅Spine运行时指南中的 应用动画 一节了解更多信息.

AnimationState 可以在多条轨道上队列单个或数段动画. 轨道索引从0开始. 高索引号轨道中的动画会覆盖低索引号轨道动画中key入的全部属性. 轨道这个概念使得同时播放和同时mix多个动画成为可能.

要在0号轨道上设置某个动画, 可以调用 AnimationState.setAnimation():

drawable.animationState.setAnimation(0, "walk", true);

第一个参数指定了轨道号, 第二个参数则是动画名称, 第三个参数表示是否应循环播放动画.

也可以队列多个动画:

const animationState = drawable.animationState;
animationState.setAnimation(0, "walk", true);
animationState.addAnimation(0, "jump", false, 2);
animationState.addAnimation(0, "run", true, 0);

addAnimation() 的第一个参数是轨道号. 第二个参数是动画的名称. 第三个参数指的是延迟时间(以秒为单位), 延迟后该动画就会替换轨道上的前一个动画. 最后一个参数定义是否应循环播放动画.

在上例中, 首先播放的是 "walk" 动画. 2 秒后, 播放一次 "jump" 动画, 然后过渡到 "run" 动画, 最后将循环播放这个 "run" 动画.

从一个动画过渡到另一个动画时, AnimationState 会在某段时间内mix(混合)动画, 该时间称为mix时长. 这些mix时长定义在 AnimationStateData 实例中, 而 AnimationState 会从中获取mix时长.

也可通过 AnimationState 获取 AnimationStateData 实例. 你既可以设置全局的默认mix时长, 也可以单独设置某对动画间的mix时长:

animationState.data.defaultMix = 0.2;
animationState.data.setMix("walk", "jump", 0.1);

设置或添加动画时会返回一个 TrackEntry 对象, 通过该对象可以进一步定制动画的播放细节. 例如, 你可以设置轨道条目来反向播放动画:

const entry = drawable.animationState.setAnimation(0, "walk", true);
entry.reverse = true;

更多选项请参阅 TrackEntry 类文档.

请注意: 小心不要函数作用域外保留 TrackEntry 实例. 轨道条目会在内部重复使用, 因此一旦发生轨道条目的 销毁(dispose)事件, 轨道条目就会失效.

在动画轨道上设置空动画或队列空动画, 就可将skeleton重置为setup pose:

dart
controller.animationState.setEmptyAnimation(0, 0.5);
controller.animationState.addEmptyAnimation(0, 0.5, 0.5);

setEmptyAnimation()的第一个参数指定轨道号. 第二个参数指定mix持续时间(以秒为单位), 这段时间可以mix掉之前的动画并过渡到空动画.

addEmptyAnimation()的第一个参数指定轨道号. 第二个参数指定mix持续时间(以秒为单位). 第三个参数则是延迟时间(以秒为单位), 轨道上的上一个动画在延迟后将mix到空动画.

通过 AnimationState.clearTrack() 可以立即清空轨道上的所有动画. 要一次性清空全部轨道, 可使用 AnimationState.clearTracks(). 这将使skeleton保持中断时的pose.

要将skeleton的pose重置为setup pose, 请使用 Skeleton.setToSetupPose():

drawable.skeleton.setToSetupPose();

这将把skeleton和槽位都重置为setup pose. 使用 Skeleton.setSlotsToSetupPose() 则仅将槽位重置为setup pose设置.

AnimationState事件

AnimationState 将在动画播放的生命周期中触发事件. 你可以监听这些事件来按需响应. Spine Runtimes API 定义了以下几种 事件类型:

  • Start: 动画开始时触发.
  • Interrupted: 清空了某条动画轨道或设置了某个新动画后触发.
  • Completed: 当动画完成一次循环后触发.
  • Ended: 当不再应用某个动画后触发.
  • Disposed: 当销毁了某个动画的轨道条目后触发.
  • Event: 当用户定义的 事件 发生后触发.

要接收事件, 你可以在 AnimationState 上注册一个 AnimationStateListener 回调函数来接收所有动画的事件, 也可以注册到 TrackEntry 上以便监听队列中某个动画的事件:

const entry = drawable.animationState.setAnimation(0, "walk", true);
entry.listener = {
event: (entry, event) => console.log(`User defined event: ${event.data.name}`),
complete: (entry) => console.log(`Animation loop completed.`)
}

drawable.animationState.setListener({
end: (entry) => console.log(`Animation ${entry.data.name} has ended and will not be applied again.`
});

参见 example/animation-state-events.html 了解更多示例.

皮肤

许多应用程序和游戏都允许用户用头发、眼睛、裤子或耳环、包包等配饰等部件来创建自定义形象. 而用Spine可以通过 皮肤混搭 来实现这一功能.

这样写可以从其他皮肤中创建自定义皮肤:

// Create a custom, empty skin
const skin = new spine.Skin("custom");

// Add other skins to the custom skin
skin.addSkin(skeletonData.findSkin("skin-base"));
skin.addSkin(skeletonData.findSkin("nose/short"));
skin.addSkin(skeletonData.findSkin("eyelids/girly"));
skin.addSkin(skeletonData.findSkin("eyes/violet"));
skin.addSkin(skeletonData.findSkin("hair/brown"));
skin.addSkin(skeletonData.findSkin("clothes/hoodie-orange"));
skin.addSkin(skeletonData.findSkin("legs/pants-jeans"));
skin.addSkin(skeletonData.findSkin("accessories/bag"));
skin.addSkin(skeletonData.findSkin("accessories/hat-red-yellow"));
skeleton.setSkin(skin);
skeleton.setSlotsToSetupPose();

使用 Skin() 构造函数将创建自定义皮肤.

从skeleton中可获取 SkeletonData. 使用 SkeletonData.findSkin() 则可以按名称查找皮肤.

通过 Skin.addSkin() 能将所有需要组合的皮肤组件添加到新建的自定义皮肤中.

最后, 在 Skeleton 上设置新皮肤, 并调用 Skeleton.setSlotsToSetupPose() 来确保先前的皮肤和/或动画附件没有残留.

完整示例代码请参见 mix-and-match-example.html.

设置骨骼变换

在Spine Editor中创建skeleton时, skeleton定义在skeleton世界坐标系——即所谓的"skeleton坐标系"下. 该坐标系与Phaser中的坐标系大概率不一致. 使用 Bone.worldToLocal() 方法可以将画布坐标系下的触摸或鼠标坐标转换为到skeleton坐标.

当需要根据用户输入来更改skeleton位置时会非常有用.

详见示例 example/ik-following.html.

性能

spine-canvaskit使用 CavansKit.MakeVertices()Canvas.drawVertices() 来绘制各个skeleton附件的网格. 虽然Skia会在后台对这些网格合批, 但在spine-canvaskit中对附件网格合批, 更有可能提高性能..

详情参见 example/micro-benchmark.html 示例.