Merge branch '4.2' into spine-android

This commit is contained in:
Denis Andrasec 2024-07-10 11:00:36 +02:00
commit 88ea68a9bc
72 changed files with 6018 additions and 181 deletions

18
.github/workflows/spine-haxe.yml vendored Normal file
View File

@ -0,0 +1,18 @@
name: Build spine-haxe
on:
push:
paths:
- 'spine-haxe/**'
workflow_dispatch:
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Build spine-haxe
working-directory: spine-haxe
env:
HAXE_UPDATE_URL: ${{secrets.HAXE_UPDATE_URL}}
run: ./build.sh

2
.gitignore vendored
View File

@ -202,3 +202,5 @@ spine-haxe/export
spine-godot/example-v4-csharp/.godot
spine-flutter/src/spine-cpp-lite
spine-sdl/.vs
spine-ts/spine-canvaskit/dist
spine-ts/output.png

View File

@ -160,6 +160,7 @@
- All Spine Outline shaders, including the URP outline shader, now provide an additional parameter `Width in Screen Space`. Enable it to keep the outline width constant in screen space instead of texture space. Requires more expensive computations, so enable only where necessary. Defaults to `disabled` to maintain existing behaviour.
- Added support for BlendModeMaterials at runtime instantiation from files via an additional method `SkeletonDataAsset.SetupRuntimeBlendModeMaterials`. See example scene `Spine Examples/Other Examples/Instantiate from Script` for a usage example.
- SkeletonGraphic: You can now offset the skeleton mesh relative to the pivot via a newly added green circle handle. This allows you to e.g. frame only the face of a skeleton inside a masked frame. Previously offsetting the pivot downwards fails when `Layout Scale Mode` scales the mesh smaller and towards the pivot (e.g. the feet) and thus out of the frame. Now you can keep the pivot in the center of the `RectTransform` while offsetting only the mesh downwards, keeping the desired skeleton area (e.g. the face) centered while resizing. Moving the new larger green circle handle moves the mesh offset, while moving the blue pivot circle handle moves the pivot as usual.
- `Universal Render Pipeline/Spine/Skeleton` shader now performs proper alpha-testing when `Depth Write` is enabled, using the existing `Shadow alpha cutoff` parameter.
- **Breaking changes**
@ -173,7 +174,6 @@
- `SkeletonGraphicRenderTexture` example component: `protected RawImage quadRawImage` was changed to `protected SkeletonSubmeshGraphic quadMaskableGraphic` for a bugfix. This is only relevant for subclasses of `SkeletonGraphicRenderTexture` or when querying the `RawImage` component via e.g. `skeletonGraphicRenderTexture.quad.GetComponent<RawImage>()`.
- Fixed a bug where when Linear color space is used and `PMA vertex colors` enabled, additive slots add a too dark (too transparent) color value. If you want the old incorrect behaviour (darker additive slots) or are not using Linear but Gamma color space, you can comment-out the define `LINEAR_COLOR_SPACE_FIX_ADDITIVE_ALPHA` in `MeshGenerator.cs` to deactivate the fix or just to skip unnecessary instructions.
- **Changes of default values**
- **Deprecated**
@ -260,6 +260,10 @@
### Canvas backend
### CanvasKit backend
- Added spine-canvaskit runtime. See https://esotericsoftware.com/spine-canvaskit
### Three.js backend
- Added physics support
@ -418,6 +422,7 @@
- Made `SkeletonGraphic.unscaledTime` parameter protected, use the new property `UnscaledTime` instead.
- `SkeletonGraphic` `OnRebuild` callback delegate is now issued after the skeleton has been initialized, before the `AnimationState` component is initialized. This makes behaviour consistent with `SkeletonAnimation` and `SkeletonMecanim` component behaviour. Use the new callback `OnAnimationRebuild` if you want to receive a callback after the `SkeletonGraphic` `AnimationState` has been initialized.
- Changed name of prefab skeleton meshes stored at prefabs from `Skeleton Prefab Mesh "name"` to `Skeleton Prefab Mesh [name]` to avoid issues with quotes in mesh asset names (see [this issue](https://github.com/EsotericSoftware/spine-runtimes/issues/2572)). Likely this change poses no problems at all, however if you are parsing the prefab's mesh name for whatever reason, be sure to adjust the pattern accordingly.
- **Changes of default values**

View File

@ -448,10 +448,24 @@ cp -f ../sack/export/sack-pma.png "$ROOT/spine-ts/spine-webgl/example/assets/"
cp -f ../celestial-circus/export/* "$ROOT/spine-ts/spine-webgl/example/assets/"
rm "$ROOT/spine-ts/spine-canvas/example/assets/"*
cp -f ../spineboy/export/spineboy-pro.skel "$ROOT/spine-ts/spine-canvas/example/assets/"
cp -f ../spineboy/export/spineboy-ess.json "$ROOT/spine-ts/spine-canvas/example/assets/"
cp -f ../spineboy/export/spineboy.atlas "$ROOT/spine-ts/spine-canvas/example/assets/"
cp -f ../spineboy/export/spineboy.png "$ROOT/spine-ts/spine-canvas/example/assets/"
rm "$ROOT/spine-ts/spine-canvaskit/example/assets/"*
cp -f ../spineboy/export/spineboy-pro.skel "$ROOT/spine-ts/spine-canvaskit/example/assets/"
cp -f ../spineboy/export/spineboy.atlas "$ROOT/spine-ts/spine-canvaskit/example/assets/"
cp -f ../spineboy/export/spineboy.png "$ROOT/spine-ts/spine-canvaskit/example/assets/"
cp -f ../mix-and-match/export/mix-and-match-pro.skel "$ROOT/spine-ts/spine-canvaskit/example/assets/"
cp -f ../mix-and-match/export/mix-and-match.atlas "$ROOT/spine-ts/spine-canvaskit/example/assets/"
cp -f ../mix-and-match/export/mix-and-match.png "$ROOT/spine-ts/spine-canvaskit/example/assets/"
cp -f ../celestial-circus/export/celestial-circus-pro.json "$ROOT/spine-ts/spine-canvaskit/example/assets/"
cp -f ../celestial-circus/export/celestial-circus.atlas "$ROOT/spine-ts/spine-canvaskit/example/assets/"
cp -f ../celestial-circus/export/celestial-circus.png "$ROOT/spine-ts/spine-canvaskit/example/assets/"
rm "$ROOT/spine-ts/spine-threejs/example/assets/"*
cp -f ../raptor/export/raptor-pro.json "$ROOT/spine-ts/spine-threejs/example/assets/"
cp -f ../raptor/export/raptor.atlas "$ROOT/spine-ts/spine-threejs/example/assets/"

26
spine-haxe/LICENSE Normal file
View File

@ -0,0 +1,26 @@
Spine Runtimes License Agreement
Last updated July 28, 2023. Replaces all prior versions.
Copyright (c) 2013-2023, Esoteric Software LLC
Integration of the Spine Runtimes into software or otherwise creating
derivative works of the Spine Runtimes is permitted under the terms and
conditions of Section 2 of the Spine Editor License Agreement:
http://esotericsoftware.com/spine-editor-license
Otherwise, it is permitted to integrate the Spine Runtimes into software or
otherwise create derivative works of the Spine Runtimes (collectively,
"Products"), provided that each user of the Products must obtain their own
Spine Editor license and redistribution of the Products in any form must
include this license and copyright notice.
THE SPINE RUNTIMES ARE PROVIDED BY ESOTERIC SOFTWARE LLC "AS IS" AND ANY
EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
DISCLAIMED. IN NO EVENT SHALL ESOTERIC SOFTWARE LLC BE LIABLE FOR ANY
DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES,
BUSINESS INTERRUPTION, OR LOSS OF USE, DATA, OR PROFITS) HOWEVER CAUSED AND
ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THE
SPINE RUNTIMES, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

View File

@ -4,6 +4,10 @@
The spine-haxe runtime provides functionality to load, manipulate and render [Spine](http://esotericsoftware.com) skeletal animation data using [Haxe](https://haxe.org/) in combination with [OpenFL](https://www.openfl.org/) and [Lime](https://lime.openfl.org/).
For documentation of the core API in `spine-core`, please refer to our [Spine Runtimes Guide](http://esotericsoftware.com/spine-runtimes-guide).
For documentation of `spine-haxe`, please refer to our [spine-haxe Guide](https://esotericsoftware.com/spine-haxe).
## Licensing
You are welcome to evaluate the Spine Runtimes and the examples we provide in this repository free of charge.
@ -21,12 +25,31 @@ spine-haxe works with data exported from Spine 4.2.xx.
spine-haxe supports all Spine features except premultiplied alpha atlases and two color tinting.
## Setup
The core module of spine-haxe has zero dependencies. The rendering implementation through Starling has two dependencies: openfl and starling.
To use spine-haxe you have first to install all the necessary dependencies:
TBD
```
haxelib install openfl
haxelib install starling
```
Once you have installed the dependencies, you can [download the latest version of spine-haxe](https://esotericsoftware.com/files/spine-haxe/4.2/spine-haxe-latest.zip) and install it:
```
haxelib install spine-haxe-x.y.z.zip
```
Notice that the spine-haxe library is not available on [lib.haxe.org](https://lib.haxe.org/). This is why you need to download the library and install it through the zip archive.
## Example
TBD
The `example/` folder contains the spine-haxe examples. They demonstrates the usage of the API. Enter the the `spine-haxe` folder and run the following command:
```
lime test html5
```
This will compile the modules and start a server that serves the example pages at http://127.0.0.1:3000.
## Development

32
spine-haxe/build.sh Executable file
View File

@ -0,0 +1,32 @@
#!/bin/sh
set -e
if [ -z "$GITHUB_REF" ]; then
BRANCH=$(git symbolic-ref --short -q HEAD)
else
BRANCH=${GITHUB_REF#refs/heads/}
fi
# Get the latest commit message
COMMIT_MSG=$(git log -1 --pretty=%B)
# Public only if the commit message is in the correct format
if echo "$COMMIT_MSG" | grep -qE '^\[haxe\] Release [0-9]+\.[0-9]+\.[0-9]+$'; then
VERSION=$(echo "$COMMIT_MSG" | sed -E 's/^\[haxe\] Release ([0-9]+\.[0-9]+\.[0-9]+)$/\1/')
echo "Building spine-haxe $BRANCH artifacts (version $VERSION)"
if [ ! -z "$HAXE_UPDATE_URL" ] && [ ! -z "$BRANCH" ]; then
echo "Deploying spine-haxe $BRANCH artifacts (version $VERSION)"
zip -r "spine-haxe-$VERSION.zip" \
haxelib.json \
LICENSE \
README.md \
spine-haxe
curl -f -F "file=@spine-haxe-$VERSION.zip" "$HAXE_UPDATE_URL$BRANCH"
else
echo "Not deploying artifacts. HAXE_UPDATE_URL and/or BRANCH not set."
fi
else
echo "The commit is not a release - do not publish."
echo "To release the commit has to be in the for: \"[haxe] Release x.y.z\""
fi

View File

@ -17,8 +17,8 @@
"cpp"
],
"description": "The official Spine Runtime for Haxe",
"version": "4.1.0",
"releasenote": "Initial release",
"version": "4.2.0",
"releasenote": "Update to 4.2.0",
"contributors": [
"esotericsoftware"
],

29
spine-haxe/publish.sh Executable file
View File

@ -0,0 +1,29 @@
#!/bin/sh
set -e
currentVersion=$(grep -o '"version": "[^"]*' haxelib.json | grep -o '[^"]*$')
major=$(echo "$currentVersion" | cut -d. -f1)
minor=$(echo "$currentVersion" | cut -d. -f2)
patch=$(echo "$currentVersion" | cut -d. -f3)
newPatch=$((patch + 1))
newVersion="$major.$minor.$newPatch"
echo "current version: $currentVersion"
echo "new version: $newVersion"
sed -i '' "s/$currentVersion/$newVersion/" haxelib.json
echo "Write Y if you want to commit and push the new version $newVersion."
echo "This will trigger a pipeline that will publish the new version on esoteric software server."
echo "Do you want to proceed [y/n]?"
read answer
if [ "$answer" = "Y" ] || [ "$answer" = "y" ]; then
git add haxelib.json
git commit -m "[haxe] Release $newVersion"
git push origin haxe-ci
echo "Changes committed and pushed."
else
echo "Commit and push cancelled, but haxelib.json version updated."
fi

View File

@ -6,10 +6,11 @@ up into multiple modules:
1. `spine-core/`, the core classes to load and process Spine skeletons.
1. `spine-webgl/`, a self-contained WebGL backend, built on the core classes.
1. `spine-canvas/`, a self-contained Canvas backend, built on the core classes.
1. `spine-threejs/`, a self-contained THREE.JS backend, built on the core classes.
1. `spine-canvaskit/`, a self-contained [CanvasKit](https://skia.org/docs/user/modules/canvaskit/) backend, built on the core classes for CanvasKit, supporting both NodeJS for headless rendering, and browsers.
1. `spine-threejs/`, a self-contained [THREE.JS](https://threejs.org/) backend, built on the core classes.
1. `spine-player/`, a self-contained player to easily display Spine animations on your website, built on the core classes and WebGL backend.
1. `spine-phaser/`, a Phaser backend, built on the core classes.
1. `spine-pixi/`, a Pixi backend, built on the core classes.
1. `spine-phaser/`, a [Phaser](https://phaser.io/) backend, built on the core classes.
1. `spine-pixi/`, a [PixiJS](https://pixijs.com/) backend, built on the core classes.
In most cases, the `spine-player` module is best suited for your needs. Please refer to the [Spine Web Player documentation](https://esotericsoftware.com/spine-player) for more information.
@ -35,9 +36,11 @@ For the official legal terms governing the Spine Runtimes, please read the [Spin
spine-ts works with data exported from Spine 4.2.xx.
The spine-ts WebGL and Player backends support all Spine features.
spine-ts Canvas does not support mesh attachments, clipping attachments, or two-color tinting. Only the alpha channel from tint colors is applied. Experimental support for mesh attachments can be enabled by setting `spine.SkeletonRenderer.useTriangleRendering` to true. Note that this experimental mesh rendering is slow and render with artifacts on some browsers.
spine-ts Canvas does not support mesh attachments, clipping attachments, or color tinting. Only the alpha channel from tint colors is applied. Experimental support for mesh attachments can be enabled by setting `spine.SkeletonRenderer.useTriangleRendering` to true. Note that this experimental mesh rendering is slow and render with artifacts on some browsers.
spine-canvaskit supports all Spine features except two-color tinting.
The spine-webgl and spine-player support all Spine features.
spine-ts THREE.JS does not support two color tinting. The THREE.JS backend provides `SkeletonMesh.zOffset` to avoid z-fighting. Adjust to your near/far plane settings.
@ -50,20 +53,23 @@ All spine-ts modules are published to [npm](http://npmjs.com) for consumption vi
You can include a module in your project via a `<script>` tag from the [unpkg](https://unpkg.com/) CDN, specifying the version as part of the URL. In the examples below, the version is `4.0.*`, which fetches the latest patch release, and which will work with all exports from Spine Editor version `4.0.x`.
```
// spine-ts Core
// spine-core
<script src="https://unpkg.com/@esotericsoftware/spine-core@4.2.*/dist/iife/spine-core.js"></script>
// spine-ts Canvas
// spine-canvas
<script src="https://unpkg.com/@esotericsoftware/spine-canvas@4.2.*/dist/iife/spine-canvas.js"></script>
// spine-ts WebGL
// spine-canvaskit
<script src="https://unpkg.com/@esotericsoftware/spine-canvas@4.2.*/dist/iife/spine-canvaskit.js"></script>
// spine-webgl
<script src="https://unpkg.com/@esotericsoftware/spine-webgl@4.2.*/dist/iife/spine-webgl.js"></script>
// spine-ts Player, which requires a spine-player.css as well
// spine-player, which requires a spine-player.css as well
<script src="https://unpkg.com/@esotericsoftware/spine-player@4.2.*/dist/iife/spine-player.js"></script>
<link rel="stylesheet" href="https://unpkg.com/@esotericsoftware/spine-player@4.0.*/dist/spine-player.css">
// spine-ts ThreeJS
// spine-threejs
<script src="https://unpkg.com/@esotericsoftware/spine-threejs@4.2.*/dist/iife/spine-threejs.js"></script>
// spine-phaser
@ -84,6 +90,7 @@ If your project dependencies are managed through NPM or Yarn, you can add spine-
```
npm install @esotericsoftware/spine-core
npm install @esotericsoftware/spine-canvas
npm install @esotericsoftware/spine-canvaskit
npm install @esotericsoftware/spine-webgl
npm install @esotericsoftware/spine-player
npm install @esotericsoftware/spine-threejs

View File

@ -226,3 +226,7 @@ ol {
.p-4 {
padding: 1em;
}
.mb-4 {
margin-bottom: 1em;
}

View File

@ -20,6 +20,15 @@
<a href="/spine-canvas/example/mouse-click.html">Mouse click</a>
</li>
</ul>
<li>CanvasKit</li>
<ul>
<li><a href="/spine-canvaskit/example">Example</a></li>
<li><a href="/spine-canvaskit/example/animation-state-events.html">Animation State Events</a></li>
<li><a href="/spine-canvaskit/example/mix-and-match.html">Skins Mix &amp; Match</a></li>
<li><a href="/spine-canvaskit/example/ik-following.html">IK Following</a></li>
<li><a href="/spine-canvaskit/example/physics.html">Physics</a></li>
<li><a href="/spine-canvaskit/example/micro-benchmark.html">Micro Benchmark</a></li>
</ul>
<li>Pixi</li>
<ul>
<li><a href="/spine-pixi/example/index.html">Basic example</a></li>

View File

@ -1,12 +1,12 @@
{
"name": "@esotericsoftware/spine-ts",
"version": "4.2.48",
"version": "4.2.56",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "@esotericsoftware/spine-ts",
"version": "4.2.48",
"version": "4.2.56",
"license": "LicenseRef-LICENSE",
"workspaces": [
"spine-core",
@ -15,6 +15,7 @@
"spine-player",
"spine-threejs",
"spine-pixi",
"spine-canvaskit",
"spine-webgl"
],
"devDependencies": {
@ -45,6 +46,10 @@
"resolved": "spine-canvas",
"link": true
},
"node_modules/@esotericsoftware/spine-canvaskit": {
"resolved": "spine-canvaskit",
"link": true
},
"node_modules/@esotericsoftware/spine-core": {
"resolved": "spine-core",
"link": true
@ -69,6 +74,14 @@
"resolved": "spine-webgl",
"link": true
},
"node_modules/@pdf-lib/upng": {
"version": "1.0.1",
"dev": true,
"license": "MIT",
"dependencies": {
"pako": "^1.0.10"
}
},
"node_modules/@pixi/assets": {
"version": "7.4.2",
"license": "MIT",
@ -226,6 +239,14 @@
"license": "MIT",
"peer": true
},
"node_modules/@types/node": {
"version": "20.14.9",
"dev": true,
"license": "MIT",
"dependencies": {
"undici-types": "~5.26.4"
}
},
"node_modules/@types/offscreencanvas": {
"version": "2019.7.3",
"dev": true,
@ -244,6 +265,10 @@
"dev": true,
"license": "MIT"
},
"node_modules/@webgpu/types": {
"version": "0.1.21",
"license": "BSD-3-Clause"
},
"node_modules/accepts": {
"version": "1.3.8",
"dev": true,
@ -516,6 +541,13 @@
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/canvaskit-wasm": {
"version": "0.39.1",
"license": "BSD-3-Clause",
"dependencies": {
"@webgpu/types": "0.1.21"
}
},
"node_modules/chalk": {
"version": "4.1.2",
"dev": true,
@ -1954,6 +1986,11 @@
"node": ">=8"
}
},
"node_modules/pako": {
"version": "1.0.11",
"dev": true,
"license": "(MIT AND Zlib)"
},
"node_modules/parseurl": {
"version": "1.3.3",
"dev": true,
@ -2760,6 +2797,11 @@
"node": ">=4.2.0"
}
},
"node_modules/undici-types": {
"version": "5.26.5",
"dev": true,
"license": "MIT"
},
"node_modules/union-value": {
"version": "1.0.1",
"dev": true,
@ -2989,33 +3031,46 @@
},
"spine-canvas": {
"name": "@esotericsoftware/spine-canvas",
"version": "4.2.48",
"version": "4.2.56",
"license": "LicenseRef-LICENSE",
"dependencies": {
"@esotericsoftware/spine-core": "4.2.48"
"@esotericsoftware/spine-core": "4.2.56"
}
},
"spine-canvaskit": {
"name": "@esotericsoftware/spine-canvaskit",
"version": "4.2.56",
"license": "LicenseRef-LICENSE",
"dependencies": {
"@esotericsoftware/spine-core": "4.2.56",
"canvaskit-wasm": "0.39.1"
},
"devDependencies": {
"@pdf-lib/upng": "1.0.1",
"@types/node": "20.14.9"
}
},
"spine-core": {
"name": "@esotericsoftware/spine-core",
"version": "4.2.48",
"version": "4.2.56",
"license": "LicenseRef-LICENSE"
},
"spine-phaser": {
"name": "@esotericsoftware/spine-phaser",
"version": "4.2.48",
"version": "4.2.56",
"license": "LicenseRef-LICENSE",
"dependencies": {
"@esotericsoftware/spine-canvas": "4.2.48",
"@esotericsoftware/spine-core": "4.2.48",
"@esotericsoftware/spine-webgl": "4.2.48"
"@esotericsoftware/spine-canvas": "4.2.56",
"@esotericsoftware/spine-core": "4.2.56",
"@esotericsoftware/spine-webgl": "4.2.56"
}
},
"spine-pixi": {
"name": "@esotericsoftware/spine-pixi",
"version": "4.2.48",
"version": "4.2.56",
"license": "LicenseRef-LICENSE",
"dependencies": {
"@esotericsoftware/spine-core": "4.2.48"
"@esotericsoftware/spine-core": "4.2.56"
},
"peerDependencies": {
"@pixi/assets": "^7.2.4",
@ -3028,26 +3083,26 @@
},
"spine-player": {
"name": "@esotericsoftware/spine-player",
"version": "4.2.48",
"version": "4.2.56",
"license": "LicenseRef-LICENSE",
"dependencies": {
"@esotericsoftware/spine-webgl": "4.2.48"
"@esotericsoftware/spine-webgl": "4.2.56"
}
},
"spine-threejs": {
"name": "@esotericsoftware/spine-threejs",
"version": "4.2.48",
"version": "4.2.56",
"license": "LicenseRef-LICENSE",
"dependencies": {
"@esotericsoftware/spine-core": "4.2.48"
"@esotericsoftware/spine-core": "4.2.56"
}
},
"spine-webgl": {
"name": "@esotericsoftware/spine-webgl",
"version": "4.2.48",
"version": "4.2.56",
"license": "LicenseRef-LICENSE",
"dependencies": {
"@esotericsoftware/spine-core": "4.2.48"
"@esotericsoftware/spine-core": "4.2.56"
}
}
}

View File

@ -1,6 +1,6 @@
{
"name": "@esotericsoftware/spine-ts",
"version": "4.2.48",
"version": "4.2.56",
"description": "The official Spine Runtimes for the web.",
"type": "module",
"files": [
@ -8,21 +8,23 @@
],
"scripts": {
"prepublish": "npm run clean && npm run build",
"clean": "npx rimraf spine-core/dist spine-canvas/dist spine-webgl/dist spine-phaser/dist spine-player/dist spine-threejs/dist spine-pixi/dist",
"build": "npm run clean && npm run build:modules && concurrently \"npm run build:core\" \"npm run build:canvas\" \"npm run build:webgl\" \"npm run build:phaser\" \"npm run build:player\" \"npm run build:threejs\" \"npm run build:pixi\"",
"clean": "npx rimraf spine-core/dist spine-canvas/dist spine-canvaskit/dist spine-webgl/dist spine-phaser/dist spine-player/dist spine-threejs/dist spine-pixi/dist",
"build": "npm run clean && npm run build:modules && concurrently \"npm run build:core\" \"npm run build:canvas\" \"npm run build:canvaskit\" \"npm run build:webgl\" \"npm run build:phaser\" \"npm run build:player\" \"npm run build:threejs\" \"npm run build:pixi\"",
"postbuild": "npm run minify",
"build:modules": "npx tsc -b -clean && npx tsc -b",
"build:core": "npx esbuild --bundle spine-core/src/index.ts --tsconfig=spine-core/tsconfig.json --sourcemap --outfile=spine-core/dist/iife/spine-core.js --format=iife --global-name=spine",
"build:canvas": "npx esbuild --bundle spine-canvas/src/index.ts --tsconfig=spine-canvas/tsconfig.json --sourcemap --outfile=spine-canvas/dist/iife/spine-canvas.js --format=iife --global-name=spine",
"build:canvaskit": "npx esbuild --bundle spine-canvaskit/src/index.ts --tsconfig=spine-canvaskit/tsconfig.json --sourcemap --outfile=spine-canvaskit/dist/iife/spine-canvaskit.js --external:canvaskit-wasm --format=iife --global-name=spine",
"build:webgl": "npx esbuild --bundle spine-webgl/src/index.ts --tsconfig=spine-webgl/tsconfig.json --sourcemap --outfile=spine-webgl/dist/iife/spine-webgl.js --format=iife --global-name=spine",
"build:player": "npx copyfiles -f spine-player/css/spine-player.css spine-player/dist/ && npx esbuild spine-player/dist/spine-player.css --minify --outfile=spine-player/dist/spine-player.min.css && npx esbuild --bundle spine-player/src/index.ts --tsconfig=spine-player/tsconfig.json --sourcemap --outfile=spine-player/dist/iife/spine-player.js --format=iife --global-name=spine",
"build:phaser": "npx esbuild --bundle spine-phaser/src/index.ts --tsconfig=spine-phaser/tsconfig.json --sourcemap --outfile=spine-phaser/dist/iife/spine-phaser.js --external:Phaser --alias:phaser=Phaser --format=iife --global-name=spine",
"build:threejs": "npx esbuild --bundle spine-threejs/src/index.ts --tsconfig=spine-threejs/tsconfig.json --sourcemap --outfile=spine-threejs/dist/iife/spine-threejs.js --external:three --format=iife --global-name=spine",
"build:pixi": "npx esbuild --bundle spine-pixi/src/index.ts --tsconfig=spine-pixi/tsconfig.json --sourcemap --outfile=spine-pixi/dist/iife/spine-pixi.js --external:@pixi/* --format=iife --global-name=spine",
"minify": "npx esbuild --minify spine-core/dist/iife/spine-core.js --outfile=spine-core/dist/iife/spine-core.min.js && npx esbuild --minify spine-canvas/dist/iife/spine-canvas.js --outfile=spine-canvas/dist/iife/spine-canvas.min.js && npx esbuild --minify spine-player/dist/iife/spine-player.js --outfile=spine-player/dist/iife/spine-player.min.js && npx esbuild --minify spine-phaser/dist/iife/spine-phaser.js --outfile=spine-phaser/dist/iife/spine-phaser.min.js && npx esbuild --minify spine-webgl/dist/iife/spine-webgl.js --outfile=spine-webgl/dist/iife/spine-webgl.min.js && npx esbuild --minify spine-threejs/dist/iife/spine-threejs.js --outfile=spine-threejs/dist/iife/spine-threejs.min.js && npx esbuild --minify spine-pixi/dist/iife/spine-pixi.js --outfile=spine-pixi/dist/iife/spine-pixi.min.js",
"dev": "concurrently \"npx live-server\" \"npm run dev:canvas\" \"npm run dev:webgl\" \"npm run dev:phaser\" \"npm run dev:player\" \"npm run dev:threejs\" \"npm run dev:pixi\"",
"minify": "npx esbuild --minify spine-core/dist/iife/spine-core.js --outfile=spine-core/dist/iife/spine-core.min.js && npx esbuild --minify spine-canvas/dist/iife/spine-canvas.js --outfile=spine-canvas/dist/iife/spine-canvas.min.js && npx esbuild --minify spine-canvaskit/dist/iife/spine-canvaskit.js --outfile=spine-canvaskit/dist/iife/spine-canvaskit.min.js && npx esbuild --minify spine-player/dist/iife/spine-player.js --outfile=spine-player/dist/iife/spine-player.min.js && npx esbuild --minify spine-phaser/dist/iife/spine-phaser.js --outfile=spine-phaser/dist/iife/spine-phaser.min.js && npx esbuild --minify spine-webgl/dist/iife/spine-webgl.js --outfile=spine-webgl/dist/iife/spine-webgl.min.js && npx esbuild --minify spine-threejs/dist/iife/spine-threejs.js --outfile=spine-threejs/dist/iife/spine-threejs.min.js && npx esbuild --minify spine-pixi/dist/iife/spine-pixi.js --outfile=spine-pixi/dist/iife/spine-pixi.min.js",
"dev": "concurrently \"npx live-server\" \"npm run dev:canvas\" \"npm run dev:canvaskit\" \"npm run dev:webgl\" \"npm run dev:phaser\" \"npm run dev:player\" \"npm run dev:threejs\" \"npm run dev:pixi\" \"npm run dev:modules\"",
"dev:modules": "npm run build:modules -- --watch",
"dev:canvas": "npm run build:canvas -- --watch",
"dev:canvaskit": "npm run build:canvaskit -- --watch",
"dev:webgl": "npm run build:webgl -- --watch",
"dev:phaser": "npm run build:phaser -- --watch",
"dev:player": "npm run build:player -- --watch",
@ -55,18 +57,19 @@
"spine-player",
"spine-threejs",
"spine-pixi",
"spine-canvaskit",
"spine-webgl"
],
"devDependencies": {
"@types/offscreencanvas": "^2019.6.4",
"@types/three": "^0.146.0",
"concurrently": "^7.6.0",
"copyfiles": "^2.4.1",
"esbuild": "^0.16.4",
"live-server": "^1.2.2",
"phaser": "^3.60.0",
"rimraf": "^3.0.2",
"typescript": "^4.9.4",
"@types/three": "^0.146.0",
"three": "^0.146.0",
"phaser": "^3.60.0"
"typescript": "^4.9.4"
}
}

View File

@ -1,24 +1,25 @@
#!/bin/sh
set -e
if [ ! "$#" -eq 2 ]; then
echo "Usage: ./publish.sh <last-version> <new-version>"
exit
else
lastVersion=${1%/}
newVersion=${2%/}
echo "last version: $lastVersion"
echo "new version: $newVersion"
fi
currentVersion=$(grep -o '"version": "[^"]*' package.json | grep -o '[^"]*$')
major=$(echo "$currentVersion" | cut -d. -f1)
minor=$(echo "$currentVersion" | cut -d. -f2)
patch=$(echo "$currentVersion" | cut -d. -f3)
newPatch=$((patch + 1))
newVersion="$major.$minor.$newPatch"
sed -i '' "s/$lastVersion/$newVersion/" package.json
sed -i '' "s/$lastVersion/$newVersion/" spine-canvas/package.json
sed -i '' "s/$lastVersion/$newVersion/" spine-core/package.json
sed -i '' "s/$lastVersion/$newVersion/" spine-phaser/package.json
sed -i '' "s/$lastVersion/$newVersion/" spine-pixi/package.json
sed -i '' "s/$lastVersion/$newVersion/" spine-player/package.json
sed -i '' "s/$lastVersion/$newVersion/" spine-threejs/package.json
sed -i '' "s/$lastVersion/$newVersion/" spine-webgl/package.json
echo "current version: $currentVersion"
echo "new version: $newVersion"
sed -i '' "s/$currentVersion/$newVersion/" package.json
sed -i '' "s/$currentVersion/$newVersion/" spine-canvas/package.json
sed -i '' "s/$currentVersion/$newVersion/" spine-canvaskit/package.json
sed -i '' "s/$currentVersion/$newVersion/" spine-core/package.json
sed -i '' "s/$currentVersion/$newVersion/" spine-phaser/package.json
sed -i '' "s/$currentVersion/$newVersion/" spine-pixi/package.json
sed -i '' "s/$currentVersion/$newVersion/" spine-player/package.json
sed -i '' "s/$currentVersion/$newVersion/" spine-threejs/package.json
sed -i '' "s/$currentVersion/$newVersion/" spine-webgl/package.json
rm package-lock.json
rm -rf node_modules/@esotericsoftware

Binary file not shown.

View File

@ -21,19 +21,19 @@
canvas = document.getElementById("canvas");
context = canvas.getContext("2d");
skeletonRenderer = new spine.SkeletonRenderer(context);
// skeletonRenderer.triangleRendering = true;
skeletonRenderer.triangleRendering = true;
// Load the assets.
assetManager = new spine.AssetManager("https://esotericsoftware.com/files/examples/4.0/spineboy/export/");
assetManager.loadText("spineboy-ess.json");
assetManager = new spine.AssetManager("assets/");
assetManager.loadBinary("spineboy-pro.skel");
assetManager.loadTextureAtlas("spineboy.atlas");
await assetManager.loadAll();
// Create the texture atlas and skeleton data.
let atlas = assetManager.require("spineboy.atlas");
let atlasLoader = new spine.AtlasAttachmentLoader(atlas);
let skeletonJson = new spine.SkeletonJson(atlasLoader);
let skeletonData = skeletonJson.readSkeletonData(assetManager.require("spineboy-ess.json"));
let skeletonBinary = new spine.SkeletonBinary(atlasLoader);
let skeletonData = skeletonBinary.readSkeletonData(assetManager.require("spineboy-pro.skel"));
// Instantiate a new skeleton based on the atlas and skeleton data.
skeleton = new spine.Skeleton(skeletonData);

View File

@ -24,8 +24,8 @@
skeletonRenderer.triangleRendering = true;
// Load the assets.
assetManager = new spine.AssetManager("https://esotericsoftware.com/files/examples/4.0/spineboy/export/");
assetManager.loadText("spineboy-pro.json");
assetManager = new spine.AssetManager("assets/");
assetManager.loadText("spineboy-ess.json");
assetManager.loadTextureAtlas("spineboy.atlas");
await assetManager.loadAll();
@ -33,7 +33,7 @@
let atlas = assetManager.require("spineboy.atlas");
let atlasLoader = new spine.AtlasAttachmentLoader(atlas);
let skeletonJson = new spine.SkeletonJson(atlasLoader);
let skeletonData = skeletonJson.readSkeletonData(assetManager.require("spineboy-pro.json"));
let skeletonData = skeletonJson.readSkeletonData(assetManager.require("spineboy-ess.json"));
// Instantiate a new skeleton based on the atlas and skeleton data.
skeleton = new spine.Skeleton(skeletonData);

View File

@ -1,6 +1,6 @@
{
"name": "@esotericsoftware/spine-canvas",
"version": "4.2.48",
"version": "4.2.56",
"description": "The official Spine Runtimes for the web.",
"main": "dist/index.js",
"types": "dist/index.d.ts",
@ -31,6 +31,6 @@
},
"homepage": "https://github.com/esotericsoftware/spine-runtimes#readme",
"dependencies": {
"@esotericsoftware/spine-core": "4.2.48"
"@esotericsoftware/spine-core": "4.2.56"
}
}

View File

@ -178,12 +178,14 @@ export class SkeletonRenderer {
x2: number, y2: number, u2: number, v2: number) {
let ctx = this.ctx;
u0 *= img.width;
v0 *= img.height;
u1 *= img.width;
v1 *= img.height;
u2 *= img.width;
v2 *= img.height;
const width = img.width - 1;
const height = img.height - 1;
u0 *= width;
v0 *= height;
u1 *= width;
v1 *= height;
u2 *= width;
v2 *= height;
ctx.beginPath();
ctx.moveTo(x0, y0);
@ -201,17 +203,19 @@ export class SkeletonRenderer {
u2 -= u0;
v2 -= v0;
var det = 1 / (u1 * v2 - u2 * v1),
let det = u1 * v2 - u2 * v1;
if (det == 0) return;
det = 1 / det;
// linear transformation
a = (v2 * x1 - v1 * x2) * det,
b = (v2 * y1 - v1 * y2) * det,
c = (u1 * x2 - u2 * x1) * det,
d = (u1 * y2 - u2 * y1) * det,
// linear transformation
const a = (v2 * x1 - v1 * x2) * det;
const b = (v2 * y1 - v1 * y2) * det;
const c = (u1 * x2 - u2 * x1) * det;
const d = (u1 * y2 - u2 * y1) * det;
// translation
e = x0 - a * u0 - c * v0,
f = y0 - b * u0 - d * v0;
// translation
const e = x0 - a * u0 - c * v0;
const f = y0 - b * u0 - d * v0;
ctx.save();
ctx.transform(a, b, c, d, e, f);

View File

@ -0,0 +1,26 @@
Spine Runtimes License Agreement
Last updated May 1, 2019. Replaces all prior versions.
Copyright (c) 2013-2019, Esoteric Software LLC
Integration of the Spine Runtimes into software or otherwise creating
derivative works of the Spine Runtimes is permitted under the terms and
conditions of Section 2 of the Spine Editor License Agreement:
http://esotericsoftware.com/spine-editor-license
Otherwise, it is permitted to integrate the Spine Runtimes into software
or otherwise create derivative works of the Spine Runtimes (collectively,
"Products"), provided that each user of the Products must obtain their own
Spine Editor license and redistribution of the Products in any form must
include this license and copyright notice.
THIS SOFTWARE IS PROVIDED BY ESOTERIC SOFTWARE LLC "AS IS" AND ANY EXPRESS
OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN
NO EVENT SHALL ESOTERIC SOFTWARE LLC BE LIABLE FOR ANY DIRECT, INDIRECT,
INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES, BUSINESS
INTERRUPTION, OR LOSS OF USE, DATA, OR PROFITS) HOWEVER CAUSED AND ON ANY
THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE,
EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

View File

@ -0,0 +1,3 @@
# spine-ts CanvasKit
Please see the top-level [README.md](../README.md) for more information.

View File

@ -0,0 +1,79 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<link rel="stylesheet" href="../../index.css">
<script src="https://unpkg.com/canvaskit-wasm@latest/bin/canvaskit.js"></script>
<script src="../dist/iife/spine-canvaskit.js"></script>
<style>
* {
margin: 0;
padding: 0;
}
</style>
</head>
<body class="p-4 flex flex-col items-center">
<h1>Animation State Events</h1>
<p class="mb-4">Open the console in the developer tools to view events logs.</p>
<canvas id=foo style="margin: 0 auto; width: 600px; height: 400px;"></canvas>
</body>
<script type="module">
async function readFile(path) {
const response = await fetch(path);
if (!response.ok) throw new Error("Could not load file " + path);
return await response.arrayBuffer();
}
const canvasElement = document.querySelector("#foo");
const dpr = window.devicePixelRatio || 1;
canvasElement.width = canvasElement.clientWidth * dpr;
canvasElement.height = canvasElement.clientHeight * dpr;
const ck = await CanvasKitInit();
const surface = ck.MakeCanvasSurface('foo');
surface.getCanvas().scale(dpr, dpr);
const atlas = await spine.loadTextureAtlas(ck, "assets/spineboy.atlas", readFile);
const skeletonData = await spine.loadSkeletonData("assets/spineboy-pro.skel", atlas, readFile);
const drawable = new spine.SkeletonDrawable(skeletonData);
drawable.skeleton.scaleX = drawable.skeleton.scaleY = 0.4;
drawable.skeleton.x = 300;
drawable.skeleton.y = 380;
// Set the default mix to 0.2 seconds, queue animations, and set listeners
const animationState = drawable.animationState;
animationState.data.defaultMix = 0.2;
animationState.setAnimation(0, "walk", true).listener = {
start: (entry) => console.log("Walk animation started"),
end: (entry) => console.log("Walk animation ended"),
};
animationState.addAnimation(0, "jump", false, 2);
animationState.addAnimation(0, "run", true, 0).listener = {
event: (entry, event) => console.log(`Custom event "${event.data.name}"`)
};
animationState.addListener({
completed: (entry) => console.log(`Animation ${entry.animation.name} completed`)
});
const renderer = new spine.SkeletonRenderer(ck);
let lastTime = performance.now();
function drawFrame(canvas) {
canvas.clear(ck.Color(52, 52, 54, 1));
const now = performance.now();
const deltaTime = (now - lastTime) / 1000;
lastTime = now;
drawable.update(deltaTime);
renderer.render(canvas, drawable);
surface.requestAnimationFrame(drawFrame);
}
surface.requestAnimationFrame(drawFrame);
</script>
</html>

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,173 @@
celestial-circus.png
size: 1024, 1024
filter: Linear, Linear
scale: 0.4
arm-back-down
bounds: 324, 401, 38, 82
rotate: 90
arm-back-up
bounds: 290, 44, 83, 116
rotate: 90
arm-front-down
bounds: 706, 2, 36, 78
rotate: 90
arm-front-up
bounds: 860, 138, 77, 116
bench
bounds: 725, 256, 189, 48
body-bottom
bounds: 879, 868, 154, 124
rotate: 90
body-top
bounds: 725, 128, 126, 133
rotate: 90
chest
bounds: 408, 26, 104, 93
cloud-back
bounds: 752, 378, 202, 165
cloud-front
bounds: 2, 2, 325, 196
rotate: 90
collar
bounds: 786, 13, 47, 26
ear
bounds: 1002, 643, 20, 28
eye-back-shadow
bounds: 428, 395, 14, 10
eye-front-shadow
bounds: 704, 529, 24, 14
eye-reflex-back
bounds: 860, 128, 8, 7
rotate: 90
eye-reflex-front
bounds: 726, 386, 10, 7
eye-white-back
bounds: 835, 23, 13, 16
eye-white-front
bounds: 1005, 1000, 22, 17
rotate: 90
eyelashes-down-back
bounds: 232, 329, 11, 6
rotate: 90
eyelashes-down-front
bounds: 913, 851, 15, 6
rotate: 90
eyelashes-top-back
bounds: 408, 395, 18, 10
eyelashes-top-front
bounds: 702, 179, 30, 16
rotate: 90
face
bounds: 514, 26, 93, 102
rotate: 90
feathers-back
bounds: 954, 625, 46, 46
feathers-front
bounds: 706, 40, 72, 86
fringe-middle-back
bounds: 200, 6, 33, 52
rotate: 90
fringe-middle-front
bounds: 878, 76, 60, 50
rotate: 90
fringe-side-back
bounds: 780, 41, 27, 94
rotate: 90
fringe-side-front
bounds: 939, 161, 26, 93
glove-bottom-back
bounds: 954, 572, 51, 41
rotate: 90
glove-bottom-front
bounds: 916, 256, 47, 48
hair-back-1
bounds: 444, 395, 132, 306
rotate: 90
hair-back-2
bounds: 438, 211, 80, 285
rotate: 90
hair-back-3
bounds: 719, 306, 70, 268
rotate: 90
hair-back-4
bounds: 438, 121, 88, 262
rotate: 90
hair-back-5
bounds: 438, 293, 88, 279
rotate: 90
hair-back-6
bounds: 200, 41, 88, 286
hair-hat-shadow
bounds: 232, 398, 90, 41
hand-back
bounds: 954, 673, 60, 47
rotate: 90
hand-front
bounds: 967, 172, 53, 60
hat-back
bounds: 954, 802, 64, 45
rotate: 90
hat-front
bounds: 780, 70, 96, 56
head-back
bounds: 618, 17, 102, 86
rotate: 90
jabot
bounds: 967, 234, 70, 55
rotate: 90
leg-back
bounds: 232, 441, 210, 333
leg-front
bounds: 444, 529, 258, 320
logo-brooch
bounds: 954, 545, 16, 25
mouth
bounds: 408, 121, 22, 6
neck
bounds: 232, 342, 39, 56
rotate: 90
nose
bounds: 742, 529, 6, 7
rotate: 90
nose-highlight
bounds: 719, 300, 4, 4
nose-shadow
bounds: 869, 128, 7, 8
pupil-back
bounds: 730, 529, 10, 14
pupil-front
bounds: 254, 21, 12, 18
rope-back
bounds: 232, 383, 10, 492
rotate: 90
rope-front
bounds: 232, 383, 10, 492
rotate: 90
rope-front-bottom
bounds: 954, 735, 42, 65
skirt
bounds: 2, 776, 440, 246
sock-bow
bounds: 408, 407, 33, 32
spine-logo-body
bounds: 879, 853, 13, 32
rotate: 90
star-big
bounds: 939, 141, 18, 24
rotate: 90
star-medium
bounds: 742, 537, 6, 8
rotate: 90
star-small
bounds: 719, 378, 3, 4
rotate: 90
underskirt
bounds: 2, 329, 445, 228
rotate: 90
underskirt-back
bounds: 444, 851, 433, 171
wing-back
bounds: 290, 129, 146, 252
wing-front
bounds: 704, 545, 304, 248
rotate: 90

Binary file not shown.

After

Width:  |  Height:  |  Size: 790 KiB

View File

@ -0,0 +1,358 @@
mix-and-match.png
size: 1024, 512
filter: Linear, Linear
scale: 0.5
base-head
bounds: 118, 70, 95, 73
boy/arm-front
bounds: 831, 311, 36, 115
rotate: 90
boy/backpack
bounds: 249, 357, 119, 153
boy/backpack-pocket
bounds: 628, 193, 34, 62
rotate: 90
boy/backpack-strap-front
bounds: 330, 263, 38, 88
rotate: 90
boy/backpack-up
bounds: 482, 171, 21, 70
boy/body
bounds: 845, 413, 97, 132
rotate: 90
boy/boot-ribbon-front
bounds: 234, 304, 9, 11
boy/collar
bounds: 471, 243, 73, 29
rotate: 90
boy/ear
bounds: 991, 352, 19, 23
rotate: 90
boy/eye-back-low-eyelid
bounds: 66, 72, 17, 6
boy/eye-back-pupil
bounds: 694, 279, 8, 9
rotate: 90
boy/eye-back-up-eyelid
bounds: 460, 101, 23, 5
rotate: 90
boy/eye-back-up-eyelid-back
bounds: 979, 414, 19, 10
rotate: 90
boy/eye-front-low-eyelid
bounds: 1015, 203, 22, 7
rotate: 90
boy/eye-front-pupil
bounds: 309, 50, 9, 9
boy/eye-front-up-eyelid
bounds: 991, 373, 31, 6
boy/eye-front-up-eyelid-back
bounds: 107, 76, 26, 9
rotate: 90
boy/eye-iris-back
bounds: 810, 260, 17, 17
boy/eye-iris-front
bounds: 902, 230, 18, 18
boy/eye-white-back
bounds: 599, 179, 20, 12
boy/eye-white-front
bounds: 544, 183, 27, 13
boy/eyebrow-back
bounds: 1002, 225, 20, 11
rotate: 90
boy/eyebrow-front
bounds: 975, 234, 25, 11
boy/hair-back
bounds: 629, 289, 122, 81
rotate: 90
boy/hair-bangs
bounds: 505, 180, 70, 37
rotate: 90
boy/hair-side
bounds: 979, 435, 25, 43
rotate: 90
boy/hand-backfingers
bounds: 858, 183, 19, 21
boy/hand-front-fingers
bounds: 879, 183, 19, 21
boy/hat
bounds: 218, 121, 93, 56
boy/leg-front
bounds: 85, 104, 31, 158
boy/mouth-close
bounds: 467, 100, 21, 5
girl-blue-cape/mouth-close
bounds: 467, 100, 21, 5
girl-spring-dress/mouth-close
bounds: 467, 100, 21, 5
girl/mouth-close
bounds: 467, 100, 21, 5
boy/mouth-smile
bounds: 1015, 258, 29, 7
rotate: 90
boy/nose
bounds: 323, 79, 17, 10
boy/pompom
bounds: 979, 462, 48, 43
rotate: 90
boy/zip
bounds: 922, 231, 14, 23
rotate: 90
girl-blue-cape/back-eyebrow
bounds: 527, 106, 18, 12
rotate: 90
girl-blue-cape/body-dress
bounds: 2, 264, 109, 246
girl-blue-cape/body-ribbon
bounds: 576, 193, 50, 38
girl-blue-cape/cape-back
bounds: 113, 317, 134, 193
girl-blue-cape/cape-back-up
bounds: 504, 305, 123, 106
girl-blue-cape/cape-ribbon
bounds: 396, 118, 50, 18
rotate: 90
girl-blue-cape/cape-shoulder-back
bounds: 420, 243, 49, 59
girl-blue-cape/cape-shoulder-front
bounds: 2, 2, 62, 76
girl-blue-cape/cape-up-front
bounds: 118, 145, 98, 117
girl-blue-cape/ear
bounds: 837, 181, 19, 23
girl-spring-dress/ear
bounds: 837, 181, 19, 23
girl/ear
bounds: 837, 181, 19, 23
girl-blue-cape/eye-back-low-eyelid
bounds: 810, 252, 17, 6
girl-spring-dress/eye-back-low-eyelid
bounds: 810, 252, 17, 6
girl/eye-back-low-eyelid
bounds: 810, 252, 17, 6
girl-blue-cape/eye-back-pupil
bounds: 309, 40, 8, 9
rotate: 90
girl-spring-dress/eye-back-pupil
bounds: 309, 40, 8, 9
rotate: 90
girl/eye-back-pupil
bounds: 309, 40, 8, 9
rotate: 90
girl-blue-cape/eye-back-up-eyelid
bounds: 573, 179, 24, 12
girl-spring-dress/eye-back-up-eyelid
bounds: 573, 179, 24, 12
girl/eye-back-up-eyelid
bounds: 573, 179, 24, 12
girl-blue-cape/eye-back-up-eyelid-back
bounds: 380, 105, 17, 11
rotate: 90
girl-spring-dress/eye-back-up-eyelid-back
bounds: 380, 105, 17, 11
rotate: 90
girl/eye-back-up-eyelid-back
bounds: 380, 105, 17, 11
rotate: 90
girl-blue-cape/eye-front-low-eyelid
bounds: 1016, 353, 18, 6
rotate: 90
girl-spring-dress/eye-front-low-eyelid
bounds: 1016, 353, 18, 6
rotate: 90
girl/eye-front-low-eyelid
bounds: 1016, 353, 18, 6
rotate: 90
girl-blue-cape/eye-front-pupil
bounds: 363, 94, 9, 9
girl-spring-dress/eye-front-pupil
bounds: 363, 94, 9, 9
girl/eye-front-pupil
bounds: 363, 94, 9, 9
girl-blue-cape/eye-front-up-eyelid
bounds: 679, 413, 30, 14
rotate: 90
girl-spring-dress/eye-front-up-eyelid
bounds: 679, 413, 30, 14
rotate: 90
girl/eye-front-up-eyelid
bounds: 679, 413, 30, 14
rotate: 90
girl-blue-cape/eye-front-up-eyelid-back
bounds: 947, 234, 26, 11
girl-spring-dress/eye-front-up-eyelid-back
bounds: 947, 234, 26, 11
girl/eye-front-up-eyelid-back
bounds: 947, 234, 26, 11
girl-blue-cape/eye-iris-back
bounds: 323, 105, 17, 17
girl-blue-cape/eye-iris-front
bounds: 467, 107, 18, 18
girl-blue-cape/eye-white-back
bounds: 621, 175, 20, 16
girl-spring-dress/eye-white-back
bounds: 621, 175, 20, 16
girl-blue-cape/eye-white-front
bounds: 643, 175, 20, 16
girl-spring-dress/eye-white-front
bounds: 643, 175, 20, 16
girl/eye-white-front
bounds: 643, 175, 20, 16
girl-blue-cape/front-eyebrow
bounds: 309, 101, 18, 12
rotate: 90
girl-blue-cape/hair-back
bounds: 712, 317, 117, 98
girl-blue-cape/hair-bangs
bounds: 313, 170, 91, 40
rotate: 90
girl-blue-cape/hair-head-side-back
bounds: 544, 198, 30, 52
girl-blue-cape/hair-head-side-front
bounds: 466, 127, 41, 42
girl-blue-cape/hair-side
bounds: 175, 2, 36, 71
rotate: 90
girl-blue-cape/hand-front-fingers
bounds: 902, 207, 19, 21
girl-spring-dress/hand-front-fingers
bounds: 902, 207, 19, 21
girl-blue-cape/leg-front
bounds: 519, 413, 30, 158
rotate: 90
girl-blue-cape/mouth-smile
bounds: 1015, 227, 29, 7
rotate: 90
girl-spring-dress/mouth-smile
bounds: 1015, 227, 29, 7
rotate: 90
girl/mouth-smile
bounds: 1015, 227, 29, 7
rotate: 90
girl-blue-cape/nose
bounds: 342, 82, 11, 7
girl-spring-dress/nose
bounds: 342, 82, 11, 7
girl/nose
bounds: 342, 82, 11, 7
girl-blue-cape/sleeve-back
bounds: 416, 95, 42, 29
girl-blue-cape/sleeve-front
bounds: 249, 303, 52, 119
rotate: 90
girl-spring-dress/arm-front
bounds: 829, 292, 17, 111
rotate: 90
girl-spring-dress/back-eyebrow
bounds: 309, 81, 18, 12
rotate: 90
girl-spring-dress/body-up
bounds: 66, 2, 64, 66
girl-spring-dress/cloak-down
bounds: 758, 227, 50, 50
girl-spring-dress/cloak-up
bounds: 628, 229, 64, 58
girl-spring-dress/eye-iris-back
bounds: 342, 105, 17, 17
girl-spring-dress/eye-iris-front
bounds: 487, 107, 18, 18
girl-spring-dress/front-eyebrow
bounds: 323, 91, 18, 12
girl-spring-dress/hair-back
bounds: 370, 417, 147, 93
girl-spring-dress/hair-bangs
bounds: 829, 250, 91, 40
girl-spring-dress/hair-head-side-back
bounds: 509, 126, 30, 52
girl-spring-dress/hair-head-side-front
bounds: 816, 206, 41, 42
girl-spring-dress/hair-side
bounds: 248, 2, 36, 71
rotate: 90
girl-spring-dress/leg-front
bounds: 831, 381, 30, 158
rotate: 90
girl-spring-dress/neck
bounds: 85, 70, 20, 32
girl-spring-dress/shoulder-ribbon
bounds: 175, 44, 36, 24
girl-spring-dress/skirt
bounds: 2, 80, 182, 81
rotate: 90
girl-spring-dress/underskirt
bounds: 519, 445, 175, 65
girl/arm-front
bounds: 712, 279, 36, 115
rotate: 90
girl/back-eyebrow
bounds: 309, 61, 18, 12
rotate: 90
girl/bag-base
bounds: 694, 219, 62, 58
girl/bag-strap-front
bounds: 370, 304, 12, 96
rotate: 90
girl/bag-top
bounds: 765, 175, 49, 50
girl/body
bounds: 370, 318, 97, 132
rotate: 90
girl/boot-ribbon-front
bounds: 323, 64, 13, 13
girl/eye-iris-back
bounds: 361, 105, 17, 17
girl/eye-iris-front
bounds: 507, 106, 18, 18
girl/eye-white-back
bounds: 665, 175, 20, 16
girl/front-eyebrow
bounds: 343, 91, 18, 12
girl/hair-back
bounds: 696, 417, 147, 93
girl/hair-bangs
bounds: 922, 247, 91, 40
girl/hair-flap-down-front
bounds: 415, 171, 70, 65
rotate: 90
girl/hair-head-side-back
bounds: 991, 381, 30, 52
girl/hair-head-side-front
bounds: 859, 206, 41, 42
girl/hair-patch
bounds: 132, 2, 66, 41
rotate: 90
girl/hair-side
bounds: 692, 181, 36, 71
rotate: 90
girl/hair-strand-back-1
bounds: 948, 289, 58, 74
rotate: 90
girl/hair-strand-back-2
bounds: 355, 170, 91, 58
rotate: 90
girl/hair-strand-back-3
bounds: 215, 40, 92, 79
girl/hair-strand-front-1
bounds: 234, 263, 38, 94
rotate: 90
girl/hair-strand-front-2
bounds: 576, 233, 70, 50
rotate: 90
girl/hair-strand-front-3
bounds: 313, 124, 44, 81
rotate: 90
girl/hand-front-fingers
bounds: 923, 208, 19, 21
girl/hat
bounds: 218, 179, 93, 82
girl/leg-front
bounds: 831, 349, 30, 158
rotate: 90
girl/pompom
bounds: 416, 126, 48, 43
girl/scarf
bounds: 113, 264, 119, 51
girl/scarf-back
bounds: 502, 252, 72, 51
girl/zip
bounds: 816, 179, 19, 25

Binary file not shown.

After

Width:  |  Height:  |  Size: 341 KiB

View File

@ -0,0 +1,94 @@
spineboy.png
size: 1024, 256
filter: Linear, Linear
scale: 0.5
crosshair
bounds: 352, 7, 45, 45
eye-indifferent
bounds: 862, 105, 47, 45
eye-surprised
bounds: 505, 79, 47, 45
front-bracer
bounds: 826, 66, 29, 40
front-fist-closed
bounds: 786, 65, 38, 41
front-fist-open
bounds: 710, 51, 43, 44
rotate: 90
front-foot
bounds: 210, 6, 63, 35
front-shin
bounds: 665, 128, 41, 92
rotate: 90
front-thigh
bounds: 2, 2, 23, 56
rotate: 90
front-upper-arm
bounds: 250, 205, 23, 49
goggles
bounds: 665, 171, 131, 83
gun
bounds: 798, 152, 105, 102
head
bounds: 2, 27, 136, 149
hoverboard-board
bounds: 2, 178, 246, 76
hoverboard-thruster
bounds: 722, 96, 30, 32
rotate: 90
hoverglow-small
bounds: 275, 81, 137, 38
mouth-grind
bounds: 614, 97, 47, 30
mouth-oooo
bounds: 612, 65, 47, 30
mouth-smile
bounds: 661, 64, 47, 30
muzzle-glow
bounds: 382, 54, 25, 25
muzzle-ring
bounds: 275, 54, 25, 105
rotate: 90
muzzle01
bounds: 911, 95, 67, 40
rotate: 90
muzzle02
bounds: 792, 108, 68, 42
muzzle03
bounds: 956, 171, 83, 53
rotate: 90
muzzle04
bounds: 275, 7, 75, 45
muzzle05
bounds: 140, 3, 68, 38
neck
bounds: 250, 182, 18, 21
portal-bg
bounds: 140, 43, 133, 133
portal-flare1
bounds: 554, 65, 56, 30
portal-flare2
bounds: 759, 112, 57, 31
rotate: 90
portal-flare3
bounds: 554, 97, 58, 30
portal-shade
bounds: 275, 121, 133, 133
portal-streaks1
bounds: 410, 126, 126, 128
portal-streaks2
bounds: 538, 129, 125, 125
rear-bracer
bounds: 857, 67, 28, 36
rear-foot
bounds: 663, 96, 57, 30
rear-shin
bounds: 414, 86, 38, 89
rotate: 90
rear-thigh
bounds: 756, 63, 28, 47
rear-upper-arm
bounds: 60, 5, 20, 44
rotate: 90
torso
bounds: 905, 164, 49, 90

Binary file not shown.

After

Width:  |  Height:  |  Size: 240 KiB

View File

@ -0,0 +1,74 @@
import * as fs from "fs"
import { fileURLToPath } from 'url';
import path from 'path';
import CanvasKitInit from "canvaskit-wasm";
import UPNG from "@pdf-lib/upng"
import {loadTextureAtlas, SkeletonRenderer, Skeleton, SkeletonBinary, AnimationState, AnimationStateData, AtlasAttachmentLoader, Physics, loadSkeletonData, SkeletonDrawable} from "../dist/index.js"
// Get the current directory
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
// This app loads the Spineboy skeleton and its atlas, then renders Spineboy's "portal" animation
// at 30 fps to individual frames, which are then encoded as an animated PNG (APNG), which is
// written to "output.png"
async function main() {
// Initialize CanvasKit and create a surface and canvas.
const ck = await CanvasKitInit();
const surface = ck.MakeSurface(600, 400);
if (!surface) throw new Error();
// Load atlas
const atlas = await loadTextureAtlas(ck, __dirname + "/assets/spineboy.atlas", async (path) => fs.readFileSync(path));
// Load the skeleton data
const skeletonData = await loadSkeletonData(__dirname + "/assets/spineboy-pro.skel", atlas, async (path) => fs.readFileSync(path));
// Create a SkeletonDrawable
const drawable = new SkeletonDrawable(skeletonData);
// Scale and position the skeleton
drawable.skeleton.x = 300;
drawable.skeleton.y = 380;
drawable.skeleton.scaleX = drawable.skeleton.scaleY = 0.5;
// Set the "hoverboard" animation on track one
drawable.animationState.setAnimation(0, "hoverboard", true);
// Create a skeleton renderer to render the skeleton to the canvas with
const renderer = new SkeletonRenderer(ck);
// Render the full animation in 1/30 second steps (30fps) and save it to an APNG
const animationDuration = skeletonData.findAnimation("hoverboard")?.duration ?? 0;
const FRAME_TIME = 1 / 30; // 30 FPS
let deltaTime = 0;
const frames = [];
const imageInfo = { width: 600, height: 400, colorType: ck.ColorType.RGBA_8888, alphaType: ck.AlphaType.Unpremul, colorSpace: ck.ColorSpace.SRGB };
const pixelArray = ck.Malloc(Uint8Array, imageInfo.width * imageInfo.height * 4);
for (let time = 0; time <= animationDuration; time += deltaTime) {
// Get the canvas object to render to the surface to
const canvas = surface.getCanvas();
// Clear the canvas
canvas.clear(ck.WHITE);
// Update the drawable, which will advance the animation(s)
// apply them to the skeleton, and update the skeleton's pose.
drawable.update(deltaTime);
// Render the skeleton to the canvas
renderer.render(canvas, drawable)
// Read the pixels of the current frame and store it.
canvas.readPixels(0, 0, imageInfo, pixelArray);
frames.push(new Uint8Array(pixelArray.toTypedArray()).buffer.slice(0));
// First frame has deltaTime 0, subsequent use FRAME_TIME
deltaTime = FRAME_TIME;
}
const apng = UPNG.default.encode(frames, 600, 400, 0, frames.map(() => FRAME_TIME * 1000));
fs.writeFileSync('output.png', Buffer.from(apng));
}
main();

View File

@ -0,0 +1,105 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<link rel="stylesheet" href="../../index.css">
<script src="https://unpkg.com/canvaskit-wasm@latest/bin/canvaskit.js"></script>
<script src="../dist/iife/spine-canvaskit.js"></script>
<style>
* {
margin: 0;
padding: 0;
}
</style>
</head>
<body class="p-4 flex flex-col items-center">
<h1>IK Following</h1>
<p class="mb-4">Click/touch to set the aim</p>
<canvas id=foo style="margin: 0 auto; width: 600px; height: 400px;"></canvas>
</body>
<script type="module">
async function readFile(path) {
const response = await fetch(path);
if (!response.ok) throw new Error("Could not load file " + path);
return await response.arrayBuffer();
}
const canvasElement = document.querySelector("#foo");
const dpr = window.devicePixelRatio || 1;
canvasElement.width = canvasElement.clientWidth * dpr;
canvasElement.height = canvasElement.clientHeight * dpr;
const ck = await CanvasKitInit();
const surface = ck.MakeCanvasSurface('foo');
surface.getCanvas().scale(dpr, dpr);
const atlas = await spine.loadTextureAtlas(ck, "assets/spineboy.atlas", readFile);
const skeletonData = await spine.loadSkeletonData("assets/spineboy-pro.skel", atlas, readFile);
const drawable = new spine.SkeletonDrawable(skeletonData);
drawable.skeleton.scaleX = drawable.skeleton.scaleY = 0.5;
drawable.skeleton.x = 100;
drawable.skeleton.y = 380;
// Set the walk animation on track 0 and the aim animation on track 1
drawable.animationState.setAnimation(0, "walk", true);
drawable.animationState.setAnimation(1, "aim", true);
// Set up touch and mouse listeners on the canvas element
// and set the touch/mouse coordinate on the aim bone
const canvasElement = document.querySelector("#foo");
let mouseDown = false;
canvasElement.addEventListener("touchmove", (ev) => setCrosshairPosition(ev.changedTouches[0].clientX, ev.changedTouches[0].clientY));
canvasElement.addEventListener("mousedown", (ev) => {
mouseDown = true;
setCrosshairPosition(ev.clientX, ev.clientY);
});
canvasElement.addEventListener("mouseup", () => mouseDown = false)
canvasElement.addEventListener("mousemove", (ev) => {
if (mouseDown) setCrosshairPosition(ev.clientX, ev.clientY);
})
// Get the crosshair bone. We will adjust its position based on the
// touch/mouse coordinates
const crosshair = drawable.skeleton.findBone("crosshair");
// Sets the crosshair bone position based on the touch/mouse position
const setCrosshairPosition = (touchX, touchY) => {
const clientRect = canvasElement.getBoundingClientRect();
let x = touchX - clientRect.left;
let y = touchY - clientRect.top;
// Transform the touch/mouse position to the crosshair
// bone's parent bone coordinate system
const parent = crosshair.parent;
const parentPosition = new spine.Vector2(x, y);
parent.worldToLocal(parentPosition)
// Set the position on the bone (relative to its parent bone)
crosshair.x = parentPosition.x;
crosshair.y = parentPosition.y;
}
const renderer = new spine.SkeletonRenderer(ck);
let lastTime = performance.now();
function drawFrame(canvas) {
canvas.clear(ck.Color(52, 52, 54, 1));
const now = performance.now();
const deltaTime = (now - lastTime) / 1000;
lastTime = now;
drawable.update(deltaTime);
renderer.render(canvas, drawable);
surface.requestAnimationFrame(drawFrame);
}
surface.requestAnimationFrame(drawFrame);
</script>
</html>

View File

@ -0,0 +1,86 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<link rel="stylesheet" href="../../index.css">
<script src="https://unpkg.com/canvaskit-wasm@latest/bin/canvaskit.js"></script>
<script src="../dist/iife/spine-canvaskit.js"></script>
<style>
* {
margin: 0;
padding: 0;
}
</style>
</head>
<body class="p-4 flex flex-col items-center">
<h1>CanvasKit Example</h1>
<canvas id=foo style="margin: 0 auto; width: 600px; height: 400px;"></canvas>
</body>
<script type="module">
// Function to read file contents from a path, used to load texture atlas and skeleton file.
async function readFile(path) {
const response = await fetch(path);
if (!response.ok) throw new Error("Could not load file " + path);
return await response.arrayBuffer();
}
// Ensure we render at full DPI.
const canvasElement = document.querySelector("#foo");
const dpr = window.devicePixelRatio || 1;
canvasElement.width = canvasElement.clientWidth * dpr;
canvasElement.height = canvasElement.clientHeight * dpr;
// Initialize CanvasKit and create a surface from the Canvas element to draw to
const ck = await CanvasKitInit();
const surface = ck.MakeCanvasSurface('foo');
// Scale the CanvasKit coordinate system
surface.getCanvas().scale(dpr, dpr);
// Load the texture atlas
const atlas = await spine.loadTextureAtlas(ck, "assets/spineboy.atlas", readFile);
// Load skeleton data
const skeletonData = await spine.loadSkeletonData("assets/spineboy-pro.skel", atlas, readFile);
// Create a drawable and scale and position the skeleton
const drawable = new spine.SkeletonDrawable(skeletonData);
drawable.skeleton.scaleX = drawable.skeleton.scaleY = 0.4;
drawable.skeleton.x = 300;
drawable.skeleton.y = 380;
// Set the "hoverboard" animation on the first track of the animation state.
drawable.animationState.setAnimation(0, "hoverboard", true);
// Create a skeleton renderer to render the skeleton with to the canvas
const renderer = new spine.SkeletonRenderer(ck);
let lastTime = performance.now();
// Rendering loop
function drawFrame(canvas) {
// Clear the canvas
canvas.clear(ck.Color(52, 52, 54, 1));
// Calculate the time that's passed between now and the last frame
const now = performance.now();
const deltaTime = (now - lastTime) / 1000;
lastTime = now;
// Update the drawable, which will advance the animation(s)
// apply them to the skeleton, and update the skeleton's pose.
drawable.update(deltaTime);
// Render the skeleton to the canvas
renderer.render(canvas, drawable);
// Request the next frame
surface.requestAnimationFrame(drawFrame);
}
surface.requestAnimationFrame(drawFrame);
</script>
</html>

View File

@ -0,0 +1,78 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<link rel="stylesheet" href="../../index.css">
<script src="https://unpkg.com/canvaskit-wasm@latest/bin/canvaskit.js"></script>
<script src="../dist/iife/spine-canvaskit.js"></script>
<style>
* {
margin: 0;
padding: 0;
}
</style>
</head>
<body class="p-4 flex flex-col items-center">
<h1>Micro Benchmark</h1>
<div id="timing"></div>
<canvas id=foo style="margin: 0 auto; width: 600px; height: 400px;"></canvas>
</body>
<script type="module">
async function readFile(path) {
const response = await fetch(path);
if (!response.ok) throw new Error("Could not load file " + path);
return await response.arrayBuffer();
}
const canvasElement = document.querySelector("#foo");
const dpr = window.devicePixelRatio || 1;
canvasElement.width = canvasElement.clientWidth * dpr;
canvasElement.height = canvasElement.clientHeight * dpr;
const ck = await CanvasKitInit();
const surface = ck.MakeCanvasSurface('foo');
surface.getCanvas().scale(dpr, dpr);
const atlas = await spine.loadTextureAtlas(ck, "assets/spineboy.atlas", readFile);
const skeletonData = await spine.loadSkeletonData("assets/spineboy-pro.skel", atlas, readFile);
// Instantiate 100 drawables, randomly placed and scaled.
const drawables = [];
for (let i = 0; i < 100; i++) {
const drawable = new spine.SkeletonDrawable(skeletonData);
drawable.skeleton.scaleX = drawable.skeleton.scaleY = 0.1 + Math.random() * 0.4;
drawable.skeleton.x = Math.random() * 600;
drawable.skeleton.y = Math.random() * 400;
drawable.animationState.setAnimation(0, "walk", true);
drawables.push(drawable);
}
const timingElement = document.querySelector("#timing");
const renderer = new spine.SkeletonRenderer(ck);
let lastTime = performance.now();
function drawFrame(canvas) {
canvas.clear(ck.Color(52, 52, 54, 1));
const now = performance.now();
const deltaTime = (now - lastTime) / 1000;
lastTime = now;
// Render all drawables
for (let i = 0; i < drawables.length; i++) {
const drawable = drawables[i];
drawable.update(deltaTime);
renderer.render(canvas, drawable);
}
surface.requestAnimationFrame(drawFrame);
timingElement.textContent = (deltaTime * 1000).toFixed(0) + "ms/frame";
}
surface.requestAnimationFrame(drawFrame);
</script>
</html>

View File

@ -0,0 +1,84 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<link rel="stylesheet" href="../../index.css">
<script src="https://unpkg.com/canvaskit-wasm@latest/bin/canvaskit.js"></script>
<script src="../dist/iife/spine-canvaskit.js"></script>
<style>
* {
margin: 0;
padding: 0;
}
</style>
</head>
<body class="p-4 flex flex-col items-center">
<h1>Skins Mix &amp; Match Example</h1>
<canvas id=foo style="margin: 0 auto; width: 600px; height: 400px;"></canvas>
</body>
<script type="module">
async function readFile(path) {
const response = await fetch(path);
if (!response.ok) throw new Error("Could not load file " + path);
return await response.arrayBuffer();
}
const canvasElement = document.querySelector("#foo");
const dpr = window.devicePixelRatio || 1;
canvasElement.width = canvasElement.clientWidth * dpr;
canvasElement.height = canvasElement.clientHeight * dpr;
const ck = await CanvasKitInit();
const surface = ck.MakeCanvasSurface('foo');
surface.getCanvas().scale(dpr, dpr);
const atlas = await spine.loadTextureAtlas(ck, "assets/mix-and-match.atlas", readFile);
const skeletonData = await spine.loadSkeletonData("assets/mix-and-match-pro.skel", atlas, readFile);
const drawable = new spine.SkeletonDrawable(skeletonData);
drawable.skeleton.scaleX = drawable.skeleton.scaleY = 0.4;
drawable.skeleton.x = 300;
drawable.skeleton.y = 380;
drawable.animationState.setAnimation(0, "dance", true);
// 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"));
// Set the skin and the skeleton to the setup pose
drawable.skeleton.setSkin(skin);
drawable.skeleton.setSlotsToSetupPose();
const renderer = new spine.SkeletonRenderer(ck);
let lastTime = performance.now();
function drawFrame(canvas) {
canvas.clear(ck.Color(52, 52, 54, 1));
const now = performance.now();
const deltaTime = (now - lastTime) / 1000;
lastTime = now;
drawable.update(deltaTime);
renderer.render(canvas, drawable);
surface.requestAnimationFrame(drawFrame);
}
surface.requestAnimationFrame(drawFrame);
</script>
</html>

View File

@ -0,0 +1,103 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<link rel="stylesheet" href="../../index.css">
<script src="https://unpkg.com/canvaskit-wasm@latest/bin/canvaskit.js"></script>
<script src="../dist/iife/spine-canvaskit.js"></script>
<style>
* {
margin: 0;
padding: 0;
}
</style>
</head>
<body class="p-4 flex flex-col items-center">
<h1>IK Following</h1>
<p class="mb-4">Drag anywhere</p>
<canvas id=foo style="margin: 0 auto; width: 600px; height: 400px;"></canvas>
</body>
<script type="module">
async function readFile(path) {
const response = await fetch(path);
if (!response.ok) throw new Error("Could not load file " + path);
return await response.arrayBuffer();
}
const canvasElement = document.querySelector("#foo");
const dpr = window.devicePixelRatio || 1;
canvasElement.width = canvasElement.clientWidth * dpr;
canvasElement.height = canvasElement.clientHeight * dpr;
const ck = await CanvasKitInit();
const surface = ck.MakeCanvasSurface('foo');
surface.getCanvas().scale(dpr, dpr);
const atlas = await spine.loadTextureAtlas(ck, "assets/celestial-circus.atlas", readFile);
const skeletonData = await spine.loadSkeletonData("assets/celestial-circus-pro.json", atlas, readFile);
const drawable = new spine.SkeletonDrawable(skeletonData);
drawable.skeleton.scaleX = drawable.skeleton.scaleY = 0.15;
drawable.skeleton.x = 300;
drawable.skeleton.y = 300;
// Set the blink animation on track 0
drawable.animationState.setAnimation(0, "eyeblink-long", true);
// Set up touch and mouse listeners on the canvas element
// and set the touch/mouse coordinate on the aim bone
let mouseDown = false;
let lastX = -1, lastY = -1;
canvasElement.addEventListener("touchmove", (ev) => drag(ev.changedTouches[0].clientX, ev.changedTouches[0].clientY));
canvasElement.addEventListener("mousedown", (ev) => {
mouseDown = true;
drag(ev.clientX, ev.clientY);
});
canvasElement.addEventListener("mouseup", () => {
mouseDown = false;
lastX = -1; lastY = -1;
})
canvasElement.addEventListener("mousemove", (ev) => {
if (mouseDown) drag(ev.clientX, ev.clientY);
})
// Move the skeleton around based on the distance between
// the last touch/mouse location and the current touch/mouse location.
const drag = (touchX, touchY) => {
const clientRect = canvasElement.getBoundingClientRect();
let x = touchX - clientRect.left;
let y = touchY - clientRect.top;
if (lastX == -1 && lastY == -1) {
lastX = x;
lastY = y;
return;
}
drawable.skeleton.x += (x - lastX);
drawable.skeleton.y += (y - lastY);
lastX = x;
lastY = y;
}
const renderer = new spine.SkeletonRenderer(ck);
let lastTime = performance.now();
function drawFrame(canvas) {
canvas.clear(ck.Color(52, 52, 54, 1));
const now = performance.now();
const deltaTime = (now - lastTime) / 1000;
lastTime = now;
drawable.update(deltaTime);
renderer.render(canvas, drawable);
surface.requestAnimationFrame(drawFrame);
}
surface.requestAnimationFrame(drawFrame);
</script>
</html>

View File

@ -0,0 +1,41 @@
{
"name": "@esotericsoftware/spine-canvaskit",
"version": "4.2.56",
"description": "The official Spine Runtimes for CanvasKit for NodeJS",
"main": "dist/index.js",
"types": "dist/index.d.ts",
"type": "module",
"files": [
"dist/**/*",
"README.md",
"LICENSE"
],
"scripts": {},
"repository": {
"type": "git",
"url": "git+https://github.com/esotericsoftware/spine-runtimes.git"
},
"keywords": [
"gamedev",
"animations",
"2d",
"spine",
"game-dev",
"runtimes",
"skeletal"
],
"author": "Esoteric Software LLC",
"license": "LicenseRef-LICENSE",
"bugs": {
"url": "https://github.com/esotericsoftware/spine-runtimes/issues"
},
"homepage": "https://github.com/esotericsoftware/spine-runtimes#readme",
"dependencies": {
"@esotericsoftware/spine-core": "4.2.56",
"canvaskit-wasm": "0.39.1"
},
"devDependencies": {
"@pdf-lib/upng": "1.0.1",
"@types/node": "20.14.9"
}
}

View File

@ -0,0 +1,345 @@
export * from "@esotericsoftware/spine-core";
import {
AnimationState,
AnimationStateData,
AtlasAttachmentLoader,
BlendMode,
ClippingAttachment,
Color,
MeshAttachment,
NumberArrayLike,
Physics,
RegionAttachment,
Skeleton,
SkeletonBinary,
SkeletonClipping,
SkeletonData,
SkeletonJson,
Texture,
TextureAtlas,
TextureFilter,
TextureWrap,
Utils,
} from "@esotericsoftware/spine-core";
import {
Canvas,
Surface,
CanvasKit,
Image,
Paint,
Shader,
BlendMode as CanvasKitBlendMode,
} from "canvaskit-wasm";
Skeleton.yDown = true;
type CanvasKitImage = {
shaders: Shader[];
paintPerBlendMode: Map<BlendMode, Paint>;
image: Image;
};
// CanvasKit blend modes for premultiplied alpha
function toCkBlendMode (ck: CanvasKit, blendMode: BlendMode) {
switch (blendMode) {
case BlendMode.Normal:
return ck.BlendMode.SrcOver;
case BlendMode.Additive:
return ck.BlendMode.Plus;
case BlendMode.Multiply:
return ck.BlendMode.SrcOver;
case BlendMode.Screen:
return ck.BlendMode.Screen;
default:
return ck.BlendMode.SrcOver;
}
}
function bufferToUtf8String (buffer: any) {
if (typeof Buffer !== "undefined") {
return buffer.toString("utf-8");
} else if (typeof TextDecoder !== "undefined") {
return new TextDecoder("utf-8").decode(buffer);
} else {
throw new Error("Unsupported environment");
}
}
class CanvasKitTexture extends Texture {
getImage (): CanvasKitImage {
return this._image;
}
setFilters (minFilter: TextureFilter, magFilter: TextureFilter): void { }
setWraps (uWrap: TextureWrap, vWrap: TextureWrap): void { }
dispose (): void {
const data: CanvasKitImage = this._image;
for (const paint of data.paintPerBlendMode.values()) {
paint.delete();
}
for (const shader of data.shaders) {
shader.delete();
}
data.image.delete();
this._image = null;
}
static async fromFile (
ck: CanvasKit,
path: string,
readFile: (path: string) => Promise<any>
): Promise<CanvasKitTexture> {
const imgData = await readFile(path);
if (!imgData) throw new Error(`Could not load image ${path}`);
const image = ck.MakeImageFromEncoded(imgData);
if (!image) throw new Error(`Could not load image ${path}`);
const paintPerBlendMode = new Map<BlendMode, Paint>();
const shaders: Shader[] = [];
for (const blendMode of [
BlendMode.Normal,
BlendMode.Additive,
BlendMode.Multiply,
BlendMode.Screen,
]) {
const paint = new ck.Paint();
const shader = image.makeShaderOptions(
ck.TileMode.Clamp,
ck.TileMode.Clamp,
ck.FilterMode.Linear,
ck.MipmapMode.Linear
);
paint.setShader(shader);
paint.setBlendMode(toCkBlendMode(ck, blendMode));
paintPerBlendMode.set(blendMode, paint);
shaders.push(shader);
}
return new CanvasKitTexture({ shaders, paintPerBlendMode, image });
}
}
/**
* Loads a {@link TextureAtlas} and its atlas page images from the given file path using the `readFile(path: string): Promise<Buffer>` function.
* Throws an `Error` if the file or one of the atlas page images could not be loaded.
*/
export async function loadTextureAtlas (
ck: CanvasKit,
atlasFile: string,
readFile: (path: string) => Promise<Buffer>
): Promise<TextureAtlas> {
const atlas = new TextureAtlas(bufferToUtf8String(await readFile(atlasFile)));
const slashIndex = atlasFile.lastIndexOf("/");
const parentDir =
slashIndex >= 0 ? atlasFile.substring(0, slashIndex + 1) + "/" : "";
for (const page of atlas.pages) {
const texture = await CanvasKitTexture.fromFile(
ck,
parentDir + page.name,
readFile
);
page.setTexture(texture);
}
return atlas;
}
/**
* Loads a {@link SkeletonData} from the given file path (`.json` or `.skel`) using the `readFile(path: string): Promise<Buffer>` function.
* Attachments will be looked up in the provided atlas.
*/
export async function loadSkeletonData (
skeletonFile: string,
atlas: TextureAtlas,
readFile: (path: string) => Promise<Buffer>,
scale = 1
): Promise<SkeletonData> {
const attachmentLoader = new AtlasAttachmentLoader(atlas);
const loader = skeletonFile.endsWith(".json")
? new SkeletonJson(attachmentLoader)
: new SkeletonBinary(attachmentLoader);
loader.scale = scale;
let data = await readFile(skeletonFile);
if (skeletonFile.endsWith(".json")) {
data = bufferToUtf8String(data);
}
const skeletonData = loader.readSkeletonData(data);
return skeletonData;
}
/**
* Manages a {@link Skeleton} and its associated {@link AnimationState}. A drawable is constructed from a {@link SkeletonData}, which can
* be shared by any number of drawables.
*/
export class SkeletonDrawable {
public readonly skeleton: Skeleton;
public readonly animationState: AnimationState;
/**
* Constructs a new drawble from the skeleton data.
*/
constructor (skeletonData: SkeletonData) {
this.skeleton = new Skeleton(skeletonData);
this.animationState = new AnimationState(
new AnimationStateData(skeletonData)
);
}
/**
* Updates the animation state and skeleton time by the delta time. Applies the
* animations to the skeleton and calculates the final pose of the skeleton.
*
* @param deltaTime the time since the last update in seconds
* @param physicsUpdate optional {@link Physics} update mode.
*/
update (deltaTime: number, physicsUpdate: Physics = Physics.update) {
this.animationState.update(deltaTime);
this.skeleton.update(deltaTime);
this.animationState.apply(this.skeleton);
this.skeleton.updateWorldTransform(physicsUpdate);
}
}
/**
* Renders a {@link Skeleton} or {@link SkeletonDrawable} to a CanvasKit {@link Canvas}.
*/
export class SkeletonRenderer {
private clipper = new SkeletonClipping();
private tempColor = new Color();
private tempColor2 = new Color();
private static QUAD_TRIANGLES = [0, 1, 2, 2, 3, 0];
private scratchPositions = Utils.newFloatArray(100);
private scratchColors = Utils.newFloatArray(100);
private scratchUVs = Utils.newFloatArray(100);
/**
* Creates a new skeleton renderer.
* @param ck the {@link CanvasKit} instance returned by `CanvasKitInit()`.
*/
constructor (private ck: CanvasKit) { }
/**
* Renders a skeleton or skeleton drawable in its current pose to the canvas.
* @param canvas the canvas to render to.
* @param skeleton the skeleton or drawable to render.
*/
render (canvas: Canvas, skeleton: Skeleton | SkeletonDrawable) {
if (skeleton instanceof SkeletonDrawable) skeleton = skeleton.skeleton;
let clipper = this.clipper;
let drawOrder = skeleton.drawOrder;
let skeletonColor = skeleton.color;
for (let i = 0, n = drawOrder.length; i < n; i++) {
let slot = drawOrder[i];
if (!slot.bone.active) {
clipper.clipEndWithSlot(slot);
continue;
}
let attachment = slot.getAttachment();
let positions = this.scratchPositions;
let colors = this.scratchColors;
let uvs: NumberArrayLike;
let texture: CanvasKitTexture;
let triangles: Array<number>;
let attachmentColor: Color;
let numVertices = 0;
if (attachment instanceof RegionAttachment) {
let region = attachment as RegionAttachment;
positions = positions.length < 8 ? Utils.newFloatArray(8) : positions;
numVertices = 4;
region.computeWorldVertices(slot, positions, 0, 2);
triangles = SkeletonRenderer.QUAD_TRIANGLES;
uvs = region.uvs as Float32Array;
texture = region.region?.texture as CanvasKitTexture;
attachmentColor = region.color;
} else if (attachment instanceof MeshAttachment) {
let mesh = attachment as MeshAttachment;
positions =
positions.length < mesh.worldVerticesLength
? Utils.newFloatArray(mesh.worldVerticesLength)
: positions;
numVertices = mesh.worldVerticesLength >> 1;
mesh.computeWorldVertices(
slot,
0,
mesh.worldVerticesLength,
positions,
0,
2
);
triangles = mesh.triangles;
texture = mesh.region?.texture as CanvasKitTexture;
uvs = mesh.uvs as Float32Array;
attachmentColor = mesh.color;
} else if (attachment instanceof ClippingAttachment) {
let clip = attachment as ClippingAttachment;
clipper.clipStart(slot, clip);
continue;
} else {
clipper.clipEndWithSlot(slot);
continue;
}
if (texture) {
if (clipper.isClipping()) {
clipper.clipTrianglesUnpacked(
positions,
triangles,
triangles.length,
uvs
);
positions = clipper.clippedVertices;
uvs = clipper.clippedUVs;
triangles = clipper.clippedTriangles;
}
let slotColor = slot.color;
let finalColor = this.tempColor;
finalColor.r = skeletonColor.r * slotColor.r * attachmentColor.r;
finalColor.g = skeletonColor.g * slotColor.g * attachmentColor.g;
finalColor.b = skeletonColor.b * slotColor.b * attachmentColor.b;
finalColor.a = skeletonColor.a * slotColor.a * attachmentColor.a;
if (colors.length / 4 < numVertices)
colors = Utils.newFloatArray(numVertices * 4);
for (let i = 0, n = numVertices * 4; i < n; i += 4) {
colors[i] = finalColor.r;
colors[i + 1] = finalColor.g;
colors[i + 2] = finalColor.b;
colors[i + 3] = finalColor.a;
}
const scaledUvs =
this.scratchUVs.length < uvs.length
? Utils.newFloatArray(uvs.length)
: this.scratchUVs;
const width = texture.getImage().image.width();
const height = texture.getImage().image.height();
for (let i = 0; i < uvs.length; i += 2) {
scaledUvs[i] = uvs[i] * width;
scaledUvs[i + 1] = uvs[i + 1] * height;
}
const blendMode = slot.data.blendMode;
const vertices = this.ck.MakeVertices(
this.ck.VertexMode.Triangles,
positions,
scaledUvs,
colors,
triangles,
false
);
canvas.drawVertices(
vertices,
this.ck.BlendMode.Modulate,
texture.getImage().paintPerBlendMode.get(blendMode)!
);
vertices.delete();
}
clipper.clipEndWithSlot(slot);
}
clipper.clipEnd();
}
}

View File

@ -0,0 +1,18 @@
{
"extends": "../tsconfig.base.json",
"compilerOptions": {
"baseUrl": ".",
"rootDir": "./src",
"outDir": "./dist",
"paths": {
"@esotericsoftware/spine-core": ["../spine-core/src"]
}
},
"include": ["**/*.ts"],
"exclude": ["dist/**/*.d.ts"],
"references": [
{
"path": "../spine-core"
}
]
}

View File

@ -1,6 +1,6 @@
{
"name": "@esotericsoftware/spine-core",
"version": "4.2.48",
"version": "4.2.56",
"description": "The official Spine Runtimes for the web.",
"main": "dist/index.js",
"types": "dist/index.d.ts",

View File

@ -64,7 +64,7 @@ export class SkeletonBinary {
this.attachmentLoader = attachmentLoader;
}
readSkeletonData (binary: Uint8Array): SkeletonData {
readSkeletonData (binary: Uint8Array | ArrayBuffer): SkeletonData {
let scale = this.scale;
let skeletonData = new SkeletonData();
@ -1115,7 +1115,7 @@ export class SkeletonBinary {
}
export class BinaryInput {
constructor (data: Uint8Array, public strings = new Array<string>(), private index: number = 0, private buffer = new DataView(data.buffer)) {
constructor (data: Uint8Array | ArrayBuffer, public strings = new Array<string>(), private index: number = 0, private buffer = new DataView(data instanceof ArrayBuffer ? data : data.buffer)) {
}
readByte (): number {

View File

@ -37,6 +37,7 @@ export class SkeletonClipping {
private clippingPolygon = new Array<number>();
private clipOutput = new Array<number>();
clippedVertices = new Array<number>();
clippedUVs = new Array<number>();
clippedTriangles = new Array<number>();
private scratch = new Array<number>();
@ -303,6 +304,92 @@ export class SkeletonClipping {
}
}
public clipTrianglesUnpacked (vertices: NumberArrayLike, triangles: NumberArrayLike, trianglesLength: number, uvs: NumberArrayLike) {
let clipOutput = this.clipOutput, clippedVertices = this.clippedVertices, clippedUVs = this.clippedUVs;
let clippedTriangles = this.clippedTriangles;
let polygons = this.clippingPolygons!;
let polygonsCount = polygons.length;
let index = 0;
clippedVertices.length = 0;
clippedUVs.length = 0;
clippedTriangles.length = 0;
for (let i = 0; i < trianglesLength; i += 3) {
let vertexOffset = triangles[i] << 1;
let x1 = vertices[vertexOffset], y1 = vertices[vertexOffset + 1];
let u1 = uvs[vertexOffset], v1 = uvs[vertexOffset + 1];
vertexOffset = triangles[i + 1] << 1;
let x2 = vertices[vertexOffset], y2 = vertices[vertexOffset + 1];
let u2 = uvs[vertexOffset], v2 = uvs[vertexOffset + 1];
vertexOffset = triangles[i + 2] << 1;
let x3 = vertices[vertexOffset], y3 = vertices[vertexOffset + 1];
let u3 = uvs[vertexOffset], v3 = uvs[vertexOffset + 1];
for (let p = 0; p < polygonsCount; p++) {
let s = clippedVertices.length;
if (this.clip(x1, y1, x2, y2, x3, y3, polygons[p], clipOutput)) {
let clipOutputLength = clipOutput.length;
if (clipOutputLength == 0) continue;
let d0 = y2 - y3, d1 = x3 - x2, d2 = x1 - x3, d4 = y3 - y1;
let d = 1 / (d0 * d2 + d1 * (y1 - y3));
let clipOutputCount = clipOutputLength >> 1;
let clipOutputItems = this.clipOutput;
let clippedVerticesItems = Utils.setArraySize(clippedVertices, s + clipOutputCount * 2);
let clippedUVsItems = Utils.setArraySize(clippedUVs, s + clipOutputCount * 2);
for (let ii = 0; ii < clipOutputLength; ii += 2, s += 2) {
let x = clipOutputItems[ii], y = clipOutputItems[ii + 1];
clippedVerticesItems[s] = x;
clippedVerticesItems[s + 1] = y;
let c0 = x - x3, c1 = y - y3;
let a = (d0 * c0 + d1 * c1) * d;
let b = (d4 * c0 + d2 * c1) * d;
let c = 1 - a - b;
clippedUVsItems[s] = u1 * a + u2 * b + u3 * c;
clippedUVsItems[s + 1] = v1 * a + v2 * b + v3 * c;
}
s = clippedTriangles.length;
let clippedTrianglesItems = Utils.setArraySize(clippedTriangles, s + 3 * (clipOutputCount - 2));
clipOutputCount--;
for (let ii = 1; ii < clipOutputCount; ii++, s += 3) {
clippedTrianglesItems[s] = index;
clippedTrianglesItems[s + 1] = (index + ii);
clippedTrianglesItems[s + 2] = (index + ii + 1);
}
index += clipOutputCount + 1;
} else {
let clippedVerticesItems = Utils.setArraySize(clippedVertices, s + 3 * 2);
clippedVerticesItems[s] = x1;
clippedVerticesItems[s + 1] = y1;
clippedVerticesItems[s + 2] = x2;
clippedVerticesItems[s + 3] = y2;
clippedVerticesItems[s + 4] = x3;
clippedVerticesItems[s + 5] = y3;
let clippedUVSItems = Utils.setArraySize(clippedUVs, s + 3 * 2);
clippedUVSItems[s] = u1;
clippedUVSItems[s + 1] = v1;
clippedUVSItems[s + 2] = u2;
clippedUVSItems[s + 3] = v2;
clippedUVSItems[s + 4] = u3;
clippedUVSItems[s + 5] = v3;
s = clippedTriangles.length;
let clippedTrianglesItems = Utils.setArraySize(clippedTriangles, s + 3);
clippedTrianglesItems[s] = index;
clippedTrianglesItems[s + 1] = (index + 1);
clippedTrianglesItems[s + 2] = (index + 2);
index += 3;
break;
}
}
}
}
/** Clips the input triangle against the convex, clockwise clipping area. If the triangle lies entirely within the clipping
* area, false is returned. The clipping area must duplicate the first vertex at the end of the vertices list. */
clip (x1: number, y1: number, x2: number, y2: number, x3: number, y3: number, clippingArea: Array<number>, output: Array<number>) {

View File

@ -28,13 +28,13 @@
*****************************************************************************/
export abstract class Texture {
protected _image: HTMLImageElement | ImageBitmap;
protected _image: HTMLImageElement | ImageBitmap | any;
constructor (image: HTMLImageElement | ImageBitmap) {
constructor (image: HTMLImageElement | ImageBitmap | any) {
this._image = image;
}
getImage (): HTMLImageElement | ImageBitmap {
getImage (): HTMLImageElement | ImageBitmap | any {
return this._image;
}

View File

@ -87,10 +87,8 @@ export class StringSet {
}
}
export interface NumberArrayLike {
readonly length: number;
[n: number]: number;
}
export type NumberArrayLike = Array<number> | Float32Array;
export type IntArrayLike = Array<number> | Int16Array;
export interface Disposable {
dispose (): void;
@ -313,7 +311,7 @@ export class Utils {
}
}
static newShortArray (size: number): NumberArrayLike {
static newShortArray (size: number): IntArrayLike {
if (Utils.SUPPORTS_TYPED_ARRAYS)
return new Int16Array(size)
else {

View File

@ -1,6 +1,6 @@
{
"name": "@esotericsoftware/spine-phaser",
"version": "4.2.48",
"version": "4.2.56",
"description": "The official Spine Runtimes for the Phaser.",
"main": "dist/index.js",
"types": "dist/index.d.ts",
@ -31,8 +31,8 @@
},
"homepage": "https://github.com/esotericsoftware/spine-runtimes#readme",
"dependencies": {
"@esotericsoftware/spine-core": "4.2.48",
"@esotericsoftware/spine-webgl": "4.2.48",
"@esotericsoftware/spine-canvas": "4.2.48"
"@esotericsoftware/spine-core": "4.2.56",
"@esotericsoftware/spine-webgl": "4.2.56",
"@esotericsoftware/spine-canvas": "4.2.56"
}
}

View File

@ -1,6 +1,6 @@
{
"name": "@esotericsoftware/spine-pixi",
"version": "4.2.48",
"version": "4.2.56",
"description": "The official Spine Runtimes for the web.",
"main": "dist/index.js",
"types": "dist/index.d.ts",
@ -31,7 +31,7 @@
},
"homepage": "https://github.com/esotericsoftware/spine-runtimes#readme",
"dependencies": {
"@esotericsoftware/spine-core": "4.2.48"
"@esotericsoftware/spine-core": "4.2.56"
},
"peerDependencies": {
"@pixi/core": "^7.2.4",

View File

@ -54,26 +54,30 @@ export class DarkSlotMesh extends DarkTintMesh implements ISlotMesh {
const vertLenght = (finalVerticesLength / (darkTint ? 12 : 8)) * 2;
if (this.geometry.getBuffer("aTextureCoord").data?.length !== vertLenght) {
this.geometry.getBuffer("aTextureCoord").data = new Float32Array(vertLenght);
const textureCoord = this.geometry.getBuffer("aTextureCoord");
if (textureCoord.data?.length !== vertLenght) {
textureCoord.data = new Float32Array(vertLenght);
}
if (this.geometry.getBuffer("aVertexPosition").data?.length !== vertLenght) {
this.geometry.getBuffer("aVertexPosition").data = new Float32Array(vertLenght);
const vertexCoord = this.geometry.getBuffer("aVertexPosition");
if (vertexCoord.data?.length !== vertLenght) {
vertexCoord.data = new Float32Array(vertLenght);
}
let vertIndex = 0;
let textureCoordData = textureCoord.data;
let vertexCoordData = vertexCoord.data;
for (let i = 0; i < finalVerticesLength; i += darkTint ? 12 : 8) {
let auxi = i;
this.geometry.getBuffer("aVertexPosition").data[vertIndex] = finalVertices[auxi++];
this.geometry.getBuffer("aVertexPosition").data[vertIndex + 1] = finalVertices[auxi++];
vertexCoordData[vertIndex] = finalVertices[auxi++];
vertexCoordData[vertIndex + 1] = finalVertices[auxi++];
auxi += 4; // color
this.geometry.getBuffer("aTextureCoord").data[vertIndex] = finalVertices[auxi++];
this.geometry.getBuffer("aTextureCoord").data[vertIndex + 1] = finalVertices[auxi++];
textureCoordData[vertIndex] = finalVertices[auxi++];
textureCoordData[vertIndex + 1] = finalVertices[auxi++];
vertIndex += 2;
}
@ -101,18 +105,20 @@ export class DarkSlotMesh extends DarkTintMesh implements ISlotMesh {
this.blendMode = SpineTexture.toPixiBlending(slotBlendMode);
this.alpha = DarkSlotMesh.auxColor[3];
if (this.geometry.indexBuffer.data.length !== finalIndices.length) {
this.geometry.indexBuffer.data = new Uint32Array(finalIndices);
const indexBuffer = this.geometry.indexBuffer;
if (indexBuffer.data.length !== finalIndices.length) {
indexBuffer.data = new Uint32Array(finalIndices);
} else {
const indexBufferData = indexBuffer.data;
for (let i = 0; i < finalIndicesLength; i++) {
this.geometry.indexBuffer.data[i] = finalIndices[i];
indexBufferData[i] = finalIndices[i];
}
}
this.name = slotName;
this.geometry.getBuffer("aVertexPosition").update();
this.geometry.getBuffer("aTextureCoord").update();
this.geometry.indexBuffer.update();
textureCoord.update();
vertexCoord.update();
indexBuffer.update();
}
}

View File

@ -62,32 +62,34 @@ export class SlotMesh extends Mesh implements ISlotMesh {
const vertLenght = (finalVerticesLength / (darkTint ? 12 : 8)) * 2;
if (this.geometry.getBuffer("aTextureCoord").data?.length !== vertLenght) {
this.geometry.getBuffer("aTextureCoord").data = new Float32Array(vertLenght);
const textureCoord = this.geometry.getBuffer("aTextureCoord");
if (textureCoord.data?.length !== vertLenght) {
textureCoord.data = new Float32Array(vertLenght);
}
if (this.geometry.getBuffer("aVertexPosition").data?.length !== vertLenght) {
this.geometry.getBuffer("aVertexPosition").data = new Float32Array(vertLenght);
const vertexCoord = this.geometry.getBuffer("aVertexPosition");
if (vertexCoord.data?.length !== vertLenght) {
vertexCoord.data = new Float32Array(vertLenght);
}
let vertIndex = 0;
let textureCoordData = textureCoord.data;
let vertexCoordData = vertexCoord.data;
for (let i = 0; i < finalVerticesLength; i += darkTint ? 12 : 8) {
let auxi = i;
this.geometry.getBuffer("aVertexPosition").data[vertIndex] = finalVertices[auxi++];
this.geometry.getBuffer("aVertexPosition").data[vertIndex + 1] = finalVertices[auxi++];
vertexCoordData[vertIndex] = finalVertices[auxi++];
vertexCoordData[vertIndex + 1] = finalVertices[auxi++];
auxi += 4; // color
this.geometry.getBuffer("aTextureCoord").data[vertIndex] = finalVertices[auxi++];
this.geometry.getBuffer("aTextureCoord").data[vertIndex + 1] = finalVertices[auxi++];
textureCoordData[vertIndex] = finalVertices[auxi++];
textureCoordData[vertIndex + 1] = finalVertices[auxi++];
vertIndex += 2;
}
// console.log(vertLenght, auxVert.length);
if (darkTint && !this.warnedTwoTint) {
console.warn("DarkTint is not enabled by default. To enable use a DarkSlotMesh factory while creating the Spine object.");
this.warnedTwoTint = true;
@ -102,18 +104,20 @@ export class SlotMesh extends Mesh implements ISlotMesh {
this.alpha = SlotMesh.auxColor[3];
this.blendMode = SpineTexture.toPixiBlending(slotBlendMode);
if (this.geometry.indexBuffer.data.length !== finalIndices.length) {
this.geometry.indexBuffer.data = new Uint32Array(finalIndices);
const indexBuffer = this.geometry.indexBuffer;
if (indexBuffer.data.length !== finalIndices.length) {
indexBuffer.data = new Uint32Array(finalIndices);
} else {
const indexBufferData = indexBuffer.data;
for (let i = 0; i < finalIndicesLength; i++) {
this.geometry.indexBuffer.data[i] = finalIndices[i];
indexBufferData[i] = finalIndices[i];
}
}
this.name = slotName;
this.geometry.getBuffer("aVertexPosition").update();
this.geometry.getBuffer("aTextureCoord").update();
this.geometry.indexBuffer.update();
textureCoord.update();
vertexCoord.update();
indexBuffer.update();
}
}

View File

@ -199,6 +199,7 @@ export class Spine extends Container {
/** Destroy Spine game object elements, then call the {@link Container.destroy} with the given options */
public override destroy (options?: boolean | IDestroyOptions | undefined): void {
if (this.autoUpdate) this.autoUpdate = false;
for (const [, mesh] of this.meshesCache) {
mesh?.destroy();
}

View File

@ -1,6 +1,6 @@
{
"name": "@esotericsoftware/spine-player",
"version": "4.2.48",
"version": "4.2.56",
"description": "The official Spine Runtimes for the web.",
"main": "dist/index.js",
"types": "dist/index.d.ts",
@ -31,6 +31,6 @@
},
"homepage": "https://github.com/esotericsoftware/spine-runtimes#readme",
"dependencies": {
"@esotericsoftware/spine-webgl": "4.2.48"
"@esotericsoftware/spine-webgl": "4.2.56"
}
}

View File

@ -212,7 +212,7 @@ export class SpinePlayer implements Disposable {
private playTime = 0;
private selectedBones: (Bone | null)[] = [];
private cancelId = 0;
private cancelId: any = 0;
popup: Popup | null = null;
/* True if the player is unable to load or render the skeleton. */

View File

@ -128,7 +128,7 @@ body { margin: 0px; }
this.startPlayer();
}
private timerId = 0;
private timerId: any = 0;
startPlayer () {
clearTimeout(this.timerId);
this.timerId = setTimeout(() => {

View File

@ -1,6 +1,6 @@
{
"name": "@esotericsoftware/spine-threejs",
"version": "4.2.48",
"version": "4.2.56",
"description": "The official Spine Runtimes for the web.",
"main": "dist/index.js",
"types": "dist/index.d.ts",
@ -31,6 +31,6 @@
},
"homepage": "https://github.com/esotericsoftware/spine-runtimes#readme",
"dependencies": {
"@esotericsoftware/spine-core": "4.2.48"
"@esotericsoftware/spine-core": "4.2.56"
}
}

View File

@ -1,6 +1,6 @@
{
"name": "@esotericsoftware/spine-webgl",
"version": "4.2.48",
"version": "4.2.56",
"description": "The official Spine Runtimes for the web.",
"main": "dist/index.js",
"types": "dist/index.d.ts",
@ -31,6 +31,6 @@
},
"homepage": "https://github.com/esotericsoftware/spine-runtimes#readme",
"dependencies": {
"@esotericsoftware/spine-core": "4.2.48"
"@esotericsoftware/spine-core": "4.2.56"
}
}

View File

@ -1,26 +1,29 @@
{
"files": [],
"references": [
{
"path": "./spine-core"
},
{
"path": "./spine-canvas"
},
{
"path": "./spine-webgl"
},
{
"path": "./spine-phaser"
},
{
"path": "./spine-player"
},
{
"path": "./spine-threejs"
},
{
"path": "./spine-pixi"
}
]
}
"files": [],
"references": [
{
"path": "./spine-core"
},
{
"path": "./spine-canvas"
},
{
"path": "./spine-canvaskit"
},
{
"path": "./spine-webgl"
},
{
"path": "./spine-phaser"
},
{
"path": "./spine-player"
},
{
"path": "./spine-threejs"
},
{
"path": "./spine-pixi"
}
]
}

View File

@ -95,7 +95,7 @@ void USpineAtlasAsset::BeginDestroy() {
class UETextureLoader : public TextureLoader {
void load(AtlasPage &page, const String &path) {
page.texture = (void*)(uintptr_t)page.index;
page.texture = (void *) (uintptr_t) page.index;
}
void unload(void *texture) {

View File

@ -268,7 +268,7 @@ void USpineSkeletonRendererComponent::UpdateMesh(USpineSkeletonComponent *compon
// to the correct skeleton data yet, we won't find any regions.
// ignore regions for which we can't find a material
UMaterialInstanceDynamic *material = nullptr;
int foundPageIndex = (int)(intptr_t)attachmentAtlasRegion->rendererObject;
int foundPageIndex = (int) (intptr_t) attachmentAtlasRegion->rendererObject;
if (foundPageIndex == -1) {
clipper.clipEnd(*slot);
continue;

View File

@ -152,9 +152,24 @@ namespace Spine.Unity.Editor {
Texture2D image = Icon;
GUIStyle usedStyle = IsValueValid(property) ? EditorStyles.popup : ErrorPopupStyle;
string propertyStringValue = (property.hasMultipleDifferentValues) ? SpineInspectorUtility.EmDash : property.stringValue;
if (GUI.Button(position, string.IsNullOrEmpty(propertyStringValue) ? NoneLabel(image) :
SpineInspectorUtility.TempContent(propertyStringValue, image), usedStyle))
Selector(property);
if (!TargetAttribute.avoidGenericMenu) {
if (GUI.Button(position, string.IsNullOrEmpty(propertyStringValue) ? NoneLabel(image) :
SpineInspectorUtility.TempContent(propertyStringValue, image), usedStyle))
Selector(property);
} else {
SkeletonData skeletonData = skeletonDataAsset.GetSkeletonData(false);
List<GUIContent> contentList = new List<GUIContent>();
List<string> valueList = new List<string>();
PopulatePopupList(ref contentList, ref valueList, image, property, TargetAttribute, skeletonData);
int currentIndex = valueList.IndexOf(propertyStringValue);
int previousIndex = currentIndex;
currentIndex = EditorGUI.Popup(position, currentIndex, contentList.ToArray());
if (previousIndex != currentIndex) {
property.stringValue = valueList[currentIndex];
property.serializedObject.ApplyModifiedProperties();
}
}
}
public ISkeletonComponent GetTargetSkeletonComponent (SerializedProperty property) {
@ -192,6 +207,12 @@ namespace Spine.Unity.Editor {
serializedProperty.serializedObject.ApplyModifiedProperties();
}
protected virtual void PopulatePopupList (ref List<GUIContent> contentList, ref List<string> valueList,
Texture2D image, SerializedProperty property, T targetAttribute, SkeletonData data) {
contentList.Add(new GUIContent("Type Not Supported"));
valueList.Add(string.Empty);
}
public override float GetPropertyHeight (SerializedProperty property, GUIContent label) {
return 18;
}
@ -302,6 +323,25 @@ namespace Spine.Unity.Editor {
}
}
protected override void PopulatePopupList (ref List<GUIContent> contentList, ref List<string> valueList,
Texture2D image, SerializedProperty property, SpineSkin targetAttribute, SkeletonData data) {
if (targetAttribute.includeNone) {
contentList.Add(new GUIContent(NoneStringConstant, image));
valueList.Add(string.Empty);
}
for (int i = 0; i < data.Skins.Count; i++) {
string name = data.Skins.Items[i].Name;
if (name.StartsWith(targetAttribute.startsWith, StringComparison.Ordinal)) {
bool isDefault = string.Equals(name, DefaultSkinName, StringComparison.Ordinal);
string choiceValue = TargetAttribute.defaultAsEmptyString && isDefault ? string.Empty : name;
contentList.Add(new GUIContent(name, image));
valueList.Add(choiceValue);
}
}
}
}
[CustomPropertyDrawer(typeof(SpineAnimation))]
@ -348,6 +388,24 @@ namespace Spine.Unity.Editor {
}
}
protected override void PopulatePopupList (ref List<GUIContent> contentList, ref List<string> valueList,
Texture2D image, SerializedProperty property, SpineAnimation targetAttribute, SkeletonData data) {
ExposedList<Animation> animations = data.Animations;
if (targetAttribute.includeNone) {
contentList.Add(new GUIContent(NoneString, image));
valueList.Add(string.Empty);
}
for (int i = 0; i < animations.Count; i++) {
string name = animations.Items[i].Name;
if (name.StartsWith(targetAttribute.startsWith, StringComparison.Ordinal)) {
contentList.Add(new GUIContent(name, image));
valueList.Add(name);
}
}
}
}
[CustomPropertyDrawer(typeof(SpineEvent))]

View File

@ -128,10 +128,10 @@ namespace Spine.Unity.Editor {
Mesh mesh = meshFilter.sharedMesh;
if (mesh == null) continue;
string meshName = string.Format("Skeleton Prefab Mesh \"{0}\"", renderer.name);
string meshName = string.Format("Skeleton Prefab Mesh [{0}]", renderer.name);
if (nameUsageCount.ContainsKey(meshName)) {
nameUsageCount[meshName]++;
meshName = string.Format("Skeleton Prefab Mesh \"{0} ({1})\"", renderer.name, nameUsageCount[meshName]);
meshName = string.Format("Skeleton Prefab Mesh [{0} ({1})]", renderer.name, nameUsageCount[meshName]);
} else {
nameUsageCount.Add(meshName, 0);
}

View File

@ -89,6 +89,7 @@ namespace Spine.Unity {
public delegate void TextureLoadDelegate (OnDemandTextureLoader loader, Material material, int textureIndex);
protected event TextureLoadDelegate onTextureRequested;
protected event TextureLoadDelegate onTextureLoaded;
protected event TextureLoadDelegate onTextureLoadFailed;
protected event TextureLoadDelegate onTextureUnloaded;
public event TextureLoadDelegate TextureRequested {
@ -99,6 +100,10 @@ namespace Spine.Unity {
add { onTextureLoaded += value; }
remove { onTextureLoaded -= value; }
}
public event TextureLoadDelegate TextureLoadFailed {
add { onTextureLoadFailed += value; }
remove { onTextureLoadFailed -= value; }
}
public event TextureLoadDelegate TextureUnloaded {
add { onTextureUnloaded += value; }
remove { onTextureUnloaded -= value; }
@ -112,6 +117,10 @@ namespace Spine.Unity {
if (onTextureLoaded != null)
onTextureLoaded(this, material, textureIndex);
}
protected void OnTextureLoadFailed (Material material, int textureIndex) {
if (onTextureLoadFailed != null)
onTextureLoadFailed(this, material, textureIndex);
}
protected void OnTextureUnloaded (Material material, int textureIndex) {
if (onTextureUnloaded != null)
onTextureUnloaded(this, material, textureIndex);

View File

@ -33,12 +33,13 @@ using UnityEngine;
namespace Spine.Unity {
[AttributeUsage(AttributeTargets.Field, Inherited = true, AllowMultiple = false)]
[AttributeUsage(AttributeTargets.Property | AttributeTargets.Field, Inherited = true, AllowMultiple = false)]
public abstract class SpineAttributeBase : PropertyAttribute {
public string dataField = "";
public string startsWith = "";
public bool includeNone = true;
public bool fallbackToTextField = false;
public bool avoidGenericMenu = false;
}
public class SpineBone : SpineAttributeBase {
@ -103,11 +104,14 @@ namespace Spine.Unity {
/// Valid types are SkeletonDataAsset and SkeletonRenderer (and derivatives)
/// If left empty and the script the attribute is applied to is derived from Component, GetComponent<SkeletonRenderer>() will be called as a fallback.
/// </param>
public SpineAnimation (string startsWith = "", string dataField = "", bool includeNone = true, bool fallbackToTextField = false) {
public SpineAnimation (string startsWith = "", string dataField = "",
bool includeNone = true, bool fallbackToTextField = false, bool avoidGenericMenu = false) {
this.startsWith = startsWith;
this.dataField = dataField;
this.includeNone = includeNone;
this.fallbackToTextField = fallbackToTextField;
this.avoidGenericMenu = avoidGenericMenu;
}
}
@ -205,12 +209,15 @@ namespace Spine.Unity {
public bool defaultAsEmptyString = false;
public SpineSkin (string startsWith = "", string dataField = "", bool includeNone = false, bool fallbackToTextField = false, bool defaultAsEmptyString = false) {
public SpineSkin (string startsWith = "", string dataField = "", bool includeNone = false,
bool fallbackToTextField = false, bool defaultAsEmptyString = false, bool avoidGenericMenu = false) {
this.startsWith = startsWith;
this.dataField = dataField;
this.includeNone = includeNone;
this.fallbackToTextField = fallbackToTextField;
this.defaultAsEmptyString = defaultAsEmptyString;
this.avoidGenericMenu = avoidGenericMenu;
}
}

View File

@ -2,7 +2,7 @@
"name": "com.esotericsoftware.spine.spine-unity",
"displayName": "spine-unity Runtime",
"description": "This plugin provides the spine-unity runtime core.",
"version": "4.2.74",
"version": "4.2.76",
"unity": "2018.3",
"author": {
"name": "Esoteric Software",

View File

@ -87,6 +87,8 @@ namespace Spine.Unity {
materialToUpdate.mainTexture = loadedTexture;
OnTextureLoaded(materialToUpdate, textureIndex);
if (onTextureLoaded != null) onTextureLoaded(loadedTexture);
} else {
OnTextureLoadFailed(materialToUpdate, textureIndex);
}
};
}

View File

@ -2,7 +2,7 @@
"name": "com.esotericsoftware.spine.addressables",
"displayName": "Spine Addressables Extensions [Experimental]",
"description": "This experimental plugin provides integration of Addressables on-demand texture loading for the spine-unity runtime.\nPlease be sure to test this package first and create backups of your project before using.\n\nUsage: First declare your target Material textures as addressable. Then select the SpineAtlasAsset, right-click the SpineAtlasAsset Inspector heading and select 'Add Addressables Loader'. This generates an 'AddressableTextureLoader' asset providing configuration parameters and sets up low-resolution placeholder textures which are automatically assigned in a pre-build step when building your game executable.\n\nPrerequisites:\nIt requires a working installation of the spine-unity runtime (via the spine-unity unitypackage), version 4.2.\n(See http://esotericsoftware.com/git/spine-runtimes/spine-unity)",
"version": "4.2.0-preview.3",
"version": "4.2.0-preview.4",
"unity": "2018.3",
"author": {
"name": "Esoteric Software",

View File

@ -1,7 +1,7 @@
{
"name": "com.esotericsoftware.spine.ui-toolkit",
"displayName": "Spine UI Toolkit [Experimental]",
"description": "This plugin provides UI Toolkit integration for the spine-unity runtime.\n\nPrerequisites:\nIt requires a working installation of the spine-unity runtime, version 4.2.\n(See http://esotericsoftware.com/git/spine-runtimes/spine-unity)",
"description": "This plugin provides UI Toolkit integration for the spine-unity runtime.\n\nPrerequisites:\nIt requires a working installation of the spine-unity runtime, version 4.2.75 or newer.\n(See http://esotericsoftware.com/git/spine-runtimes/spine-unity)",
"version": "4.2.0-preview.1",
"unity": "2023.2",
"author": {
@ -11,7 +11,7 @@
},
"dependencies": {
"com.unity.modules.uielements": "1.0.0",
"com.esotericsoftware.spine.spine-unity": "4.2.21"
"com.esotericsoftware.spine.spine-unity": "4.2.75"
},
"keywords": [
"spine",

View File

@ -109,7 +109,8 @@ half4 CombinedShapeLightFragment(VertexOutputSpriteURP2D input) : SV_Target
#endif
half4 mask = SAMPLE_TEXTURE2D(_MaskTex, sampler_MaskTex, input.texcoord.xy);
main.rgb = main.a == 0 ? main.rgb : main.rgb / main.a; // un-premultiply for additive lights in CombinedShapeLightShared, reapply afterwards
// un-premultiply for additive lights in CombinedShapeLightShared, reapply afterwards
main.rgb = main.a < 0.001 ? main.rgb : main.rgb / main.a; // < epsilon prevents imprecision issues on some HW.
#if UNITY_VERSION < 202120
half4 pixel = half4(CombinedShapeLightShared(half4(main.rgb, 1), mask, input.lightingUV).rgb * main.a, main.a);
#else

View File

@ -139,7 +139,7 @@ Shader "Universal Render Pipeline/2D/Spine/Skeleton Lit" {
return main;
#endif
// un-premultiply for additive lights in CombinedShapeLightShared, reapply afterwards
main.rgb = main.a == 0 ? main.rgb : main.rgb / main.a;
main.rgb = main.a < 0.001 ? main.rgb : main.rgb / main.a;
#else
#if !defined(_LIGHT_AFFECTS_ADDITIVE)
if (i.color.a == 0) {
@ -149,10 +149,10 @@ Shader "Universal Render Pipeline/2D/Spine/Skeleton Lit" {
#if !defined(_STRAIGHT_ALPHA_INPUT)
// un-premultiply for additive lights in CombinedShapeLightShared, reapply afterwards
tex.rgb = tex.a == 0 ? tex.rgb : tex.rgb / tex.a;
tex.rgb = tex.a < 0.001 ? tex.rgb : tex.rgb / tex.a; // < epsilon prevents imprecision issues on some HW.
#endif
half4 main = tex * i.color;
#endif
#endif
half4 mask = SAMPLE_TEXTURE2D(_MaskTex, sampler_MaskTex, i.uv);
#if UNITY_VERSION < 202120
return half4(CombinedShapeLightShared(half4(main.rgb, 1), mask, i.lightingUV).rgb * main.a, main.a);

View File

@ -49,6 +49,9 @@ VertexOutput vert(appdata v) {
half4 frag(VertexOutput i) : SV_Target{
float4 texColor = tex2D(_MainTex, i.uv0);
#if defined(_ZWRITE)
clip(texColor.a * i.color.a - _Cutoff);
#endif
#if defined(_TINT_BLACK_ON)
return fragTintedColor(texColor, i.darkColor, i.color, _Color.a, _Black.a);
#else

View File

@ -51,6 +51,7 @@ Shader "Universal Render Pipeline/Spine/Skeleton" {
// Spine related keywords
#pragma shader_feature _ _STRAIGHT_ALPHA_INPUT
#pragma shader_feature _TINT_BLACK_ON
#pragma shader_feature _ZWRITE
#pragma vertex vert
#pragma fragment frag

View File

@ -2,7 +2,7 @@
"name": "com.esotericsoftware.spine.urp-shaders",
"displayName": "Spine Universal RP Shaders",
"description": "This plugin provides universal render pipeline (URP) shaders for the spine-unity runtime.\n\nPrerequisites:\nIt requires a working installation of the spine-unity runtime, version 4.2.\n(See http://esotericsoftware.com/git/spine-runtimes/spine-unity)",
"version": "4.2.33",
"version": "4.2.35",
"unity": "2019.3",
"author": {
"name": "Esoteric Software",