[ts][canvaskit] Added CanvasKit runtime for NodeJS and browser environments

This commit is contained in:
Mario Zechner 2024-07-05 15:17:59 +02:00
parent 853dc33e19
commit a1f077d43c
19 changed files with 642 additions and 51 deletions

View File

@ -6,10 +6,11 @@ up into multiple modules:
1. `spine-core/`, the core classes to load and process Spine skeletons. 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-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-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-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-phaser/`, a [Phaser](https://phaser.io/) backend, built on the core classes.
1. `spine-pixi/`, a Pixi 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. 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. 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. 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`. 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> <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> <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> <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> <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"> <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> <script src="https://unpkg.com/@esotericsoftware/spine-threejs@4.2.*/dist/iife/spine-threejs.js"></script>
// spine-phaser // 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-core
npm install @esotericsoftware/spine-canvas npm install @esotericsoftware/spine-canvas
npm install @esotericsoftware/spine-canvaskit
npm install @esotericsoftware/spine-webgl npm install @esotericsoftware/spine-webgl
npm install @esotericsoftware/spine-player npm install @esotericsoftware/spine-player
npm install @esotericsoftware/spine-threejs npm install @esotericsoftware/spine-threejs

View File

@ -20,6 +20,10 @@
<a href="/spine-canvas/example/mouse-click.html">Mouse click</a> <a href="/spine-canvas/example/mouse-click.html">Mouse click</a>
</li> </li>
</ul> </ul>
<li>CanvasKit</li>
<ul>
<li><a href="/spine-canvaskit/example">Example</a></li>
</ul>
<li>Pixi</li> <li>Pixi</li>
<ul> <ul>
<li><a href="/spine-pixi/example/index.html">Basic example</a></li> <li><a href="/spine-pixi/example/index.html">Basic example</a></li>

View File

@ -15,6 +15,7 @@
"spine-player", "spine-player",
"spine-threejs", "spine-threejs",
"spine-pixi", "spine-pixi",
"spine-canvaskit",
"spine-webgl" "spine-webgl"
], ],
"devDependencies": { "devDependencies": {
@ -45,6 +46,10 @@
"resolved": "spine-canvas", "resolved": "spine-canvas",
"link": true "link": true
}, },
"node_modules/@esotericsoftware/spine-canvaskit": {
"resolved": "spine-canvaskit",
"link": true
},
"node_modules/@esotericsoftware/spine-core": { "node_modules/@esotericsoftware/spine-core": {
"resolved": "spine-core", "resolved": "spine-core",
"link": true "link": true
@ -69,6 +74,15 @@
"resolved": "spine-webgl", "resolved": "spine-webgl",
"link": true "link": true
}, },
"node_modules/@pdf-lib/upng": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/@pdf-lib/upng/-/upng-1.0.1.tgz",
"integrity": "sha512-dQK2FUMQtowVP00mtIksrlZhdFXQZPC+taih1q4CvPZ5vqdxR/LKBaFg0oAfzd1GlHZXXSPdQfzQnt+ViGvEIQ==",
"dev": true,
"dependencies": {
"pako": "^1.0.10"
}
},
"node_modules/@pixi/assets": { "node_modules/@pixi/assets": {
"version": "7.4.2", "version": "7.4.2",
"license": "MIT", "license": "MIT",
@ -226,6 +240,15 @@
"license": "MIT", "license": "MIT",
"peer": true "peer": true
}, },
"node_modules/@types/node": {
"version": "20.14.9",
"resolved": "https://registry.npmjs.org/@types/node/-/node-20.14.9.tgz",
"integrity": "sha512-06OCtnTXtWOZBJlRApleWndH4JsRVs1pDCc8dLSQp+7PpUpX3ePdHyeNSFTeSe7FtKyQkrlPvHwJOW3SLd8Oyg==",
"dev": true,
"dependencies": {
"undici-types": "~5.26.4"
}
},
"node_modules/@types/offscreencanvas": { "node_modules/@types/offscreencanvas": {
"version": "2019.7.3", "version": "2019.7.3",
"dev": true, "dev": true,
@ -244,6 +267,11 @@
"dev": true, "dev": true,
"license": "MIT" "license": "MIT"
}, },
"node_modules/@webgpu/types": {
"version": "0.1.21",
"resolved": "https://registry.npmjs.org/@webgpu/types/-/types-0.1.21.tgz",
"integrity": "sha512-pUrWq3V5PiSGFLeLxoGqReTZmiiXwY3jRkIG5sLLKjyqNxrwm/04b4nw7LSmGWJcKk59XOM/YRTUwOzo4MMlow=="
},
"node_modules/accepts": { "node_modules/accepts": {
"version": "1.3.8", "version": "1.3.8",
"dev": true, "dev": true,
@ -516,6 +544,14 @@
"url": "https://github.com/sponsors/ljharb" "url": "https://github.com/sponsors/ljharb"
} }
}, },
"node_modules/canvaskit-wasm": {
"version": "0.39.1",
"resolved": "https://registry.npmjs.org/canvaskit-wasm/-/canvaskit-wasm-0.39.1.tgz",
"integrity": "sha512-Gy3lCmhUdKq+8bvDrs9t8+qf7RvcjuQn+we7vTVVyqgOVO1UVfHpsnBxkTZw+R4ApEJ3D5fKySl9TU11hmjl/A==",
"dependencies": {
"@webgpu/types": "0.1.21"
}
},
"node_modules/chalk": { "node_modules/chalk": {
"version": "4.1.2", "version": "4.1.2",
"dev": true, "dev": true,
@ -1954,6 +1990,12 @@
"node": ">=8" "node": ">=8"
} }
}, },
"node_modules/pako": {
"version": "1.0.11",
"resolved": "https://registry.npmjs.org/pako/-/pako-1.0.11.tgz",
"integrity": "sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==",
"dev": true
},
"node_modules/parseurl": { "node_modules/parseurl": {
"version": "1.3.3", "version": "1.3.3",
"dev": true, "dev": true,
@ -2760,6 +2802,12 @@
"node": ">=4.2.0" "node": ">=4.2.0"
} }
}, },
"node_modules/undici-types": {
"version": "5.26.5",
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz",
"integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==",
"dev": true
},
"node_modules/union-value": { "node_modules/union-value": {
"version": "1.0.1", "version": "1.0.1",
"dev": true, "dev": true,
@ -2995,6 +3043,19 @@
"@esotericsoftware/spine-core": "4.2.48" "@esotericsoftware/spine-core": "4.2.48"
} }
}, },
"spine-canvaskit": {
"name": "@esotericsoftware/spine-canvaskit",
"version": "4.2.48",
"license": "LicenseRef-LICENSE",
"dependencies": {
"@esotericsoftware/spine-core": "4.2.48",
"canvaskit-wasm": "0.39.1"
},
"devDependencies": {
"@pdf-lib/upng": "1.0.1",
"@types/node": "20.14.9"
}
},
"spine-core": { "spine-core": {
"name": "@esotericsoftware/spine-core", "name": "@esotericsoftware/spine-core",
"version": "4.2.48", "version": "4.2.48",

View File

@ -8,21 +8,23 @@
], ],
"scripts": { "scripts": {
"prepublish": "npm run clean && npm run build", "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", "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:webgl\" \"npm run build:phaser\" \"npm run build:player\" \"npm run build:threejs\" \"npm run build:pixi\"", "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", "postbuild": "npm run minify",
"build:modules": "npx tsc -b -clean && npx tsc -b", "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: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: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: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: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: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: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", "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", "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:webgl\" \"npm run dev:phaser\" \"npm run dev:player\" \"npm run dev:threejs\" \"npm run dev:pixi\"", "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:modules": "npm run build:modules -- --watch",
"dev:canvas": "npm run build:canvas -- --watch", "dev:canvas": "npm run build:canvas -- --watch",
"dev:canvaskit": "npm run build:canvaskit -- --watch",
"dev:webgl": "npm run build:webgl -- --watch", "dev:webgl": "npm run build:webgl -- --watch",
"dev:phaser": "npm run build:phaser -- --watch", "dev:phaser": "npm run build:phaser -- --watch",
"dev:player": "npm run build:player -- --watch", "dev:player": "npm run build:player -- --watch",
@ -55,18 +57,19 @@
"spine-player", "spine-player",
"spine-threejs", "spine-threejs",
"spine-pixi", "spine-pixi",
"spine-canvaskit",
"spine-webgl" "spine-webgl"
], ],
"devDependencies": { "devDependencies": {
"@types/offscreencanvas": "^2019.6.4", "@types/offscreencanvas": "^2019.6.4",
"@types/three": "^0.146.0",
"concurrently": "^7.6.0", "concurrently": "^7.6.0",
"copyfiles": "^2.4.1", "copyfiles": "^2.4.1",
"esbuild": "^0.16.4", "esbuild": "^0.16.4",
"live-server": "^1.2.2", "live-server": "^1.2.2",
"phaser": "^3.60.0",
"rimraf": "^3.0.2", "rimraf": "^3.0.2",
"typescript": "^4.9.4",
"@types/three": "^0.146.0",
"three": "^0.146.0", "three": "^0.146.0",
"phaser": "^3.60.0" "typescript": "^4.9.4"
} }
} }

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,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,76 @@
import * as fs from "fs"
import { fileURLToPath } from 'url';
import path from 'path';
import CanvasKitInit from "canvaskit-wasm/bin/canvaskit.js";
import UPNG from "@pdf-lib/upng"
import {loadTextureAtlas, SkeletonRenderer, Skeleton, SkeletonBinary, AnimationState, AnimationStateData, AtlasAttachmentLoader, Physics} 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();
const canvas = surface.getCanvas();
// Load atlas
const atlas = await loadTextureAtlas(ck, __dirname + "/assets/spineboy.atlas", async (path) => fs.readFileSync(path));
// Load skeleton data
const binary = new SkeletonBinary(new AtlasAttachmentLoader(atlas));
const skeletonData = binary.readSkeletonData(fs.readFileSync(__dirname + "/assets/spineboy-pro.skel"));
// Create a skeleton and scale and position it.
const skeleton = new Skeleton(skeletonData);
skeleton.scaleX = skeleton.scaleY = 0.5;
skeleton.x = 300;
skeleton.y = 380;
// Create an animation state to apply and mix one or more animations
const animationState = new AnimationState(new AnimationStateData(skeletonData));
animationState.setAnimation(0, "hoverboard", true);
// Create a skeleton renderer to render the skeleton with to the canvas
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) {
// Clear the canvas
canvas.clear(ck.WHITE);
// Update and apply the animations to the skeleton
animationState.update(deltaTime);
animationState.apply(skeleton);
// Update the skeleton time for physics, and its world transforms
skeleton.update(deltaTime);
skeleton.updateWorldTransform(Physics.update);
// Render the skeleton to the canvas
renderer.render(canvas, skeleton)
// 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,80 @@
<!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 width=600 height=400 style="margin: 0 auto;"></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();
}
// Initialize CanvasKit and create a surface from the Canvas element to draw to
const ck = await CanvasKitInit();
const surface = ck.MakeCanvasSurface('foo');
// Load the texture atlas
const atlas = await spine.loadTextureAtlas(ck, "assets/spineboy.atlas", readFile);
// Load skeleton data
const binary = new spine.SkeletonBinary(new spine.AtlasAttachmentLoader(atlas));
const skeletonData = binary.readSkeletonData(await readFile("assets/spineboy-pro.skel"));
// Create a skeleton and scale and position it.
const skeleton = new spine.Skeleton(skeletonData);
skeleton.scaleX = skeleton.scaleY = 0.4;
skeleton.x = 300;
skeleton.y = 380;
skeleton.setToSetupPose();
// Create an animation state to apply and mix one or more animations
const animationState = new spine.AnimationState(new spine.AnimationStateData(skeletonData));
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) {
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 and apply the animations to the skeleton
animationState.update(deltaTime);
animationState.apply(skeleton);
// Update the skeleton time for physics, and its world transforms
skeleton.update(deltaTime);
skeleton.updateWorldTransform(spine.Physics.update);
renderer.render(canvas, skeleton);
surface.requestAnimationFrame(drawFrame);
}
surface.requestAnimationFrame(drawFrame);
</script>
</html>

View File

@ -0,0 +1,41 @@
{
"name": "@esotericsoftware/spine-canvaskit",
"version": "4.2.48",
"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.48",
"canvaskit-wasm": "0.39.1"
},
"devDependencies": {
"@pdf-lib/upng": "1.0.1",
"@types/node": "20.14.9"
}
}

View File

@ -0,0 +1,180 @@
export * from "@esotericsoftware/spine-core";
import { BlendMode, ClippingAttachment, Color, MeshAttachment, NumberArrayLike, RegionAttachment, Skeleton, SkeletonClipping, Texture, TextureAtlas, TextureFilter, TextureWrap, Utils } from "@esotericsoftware/spine-core";
import { Canvas, 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.Modulate;
case BlendMode.Screen: return ck.BlendMode.Screen;
default: return ck.BlendMode.SrcOver;
}
}
export 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<Buffer>): 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 });
}
}
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');
}
}
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;
}
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);
constructor(private ck: CanvasKit) {}
render(canvas: Canvas, 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 = new Array<number>(uvs.length);
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)!);
}
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

@ -64,7 +64,7 @@ export class SkeletonBinary {
this.attachmentLoader = attachmentLoader; this.attachmentLoader = attachmentLoader;
} }
readSkeletonData (binary: Uint8Array): SkeletonData { readSkeletonData (binary: Uint8Array | ArrayBuffer): SkeletonData {
let scale = this.scale; let scale = this.scale;
let skeletonData = new SkeletonData(); let skeletonData = new SkeletonData();
@ -1115,7 +1115,7 @@ export class SkeletonBinary {
} }
export class BinaryInput { 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 { readByte (): number {

View File

@ -90,11 +90,6 @@ export class StringSet {
export type NumberArrayLike = Array<number> | Float32Array; export type NumberArrayLike = Array<number> | Float32Array;
export type IntArrayLike = Array<number> | Int16Array; export type IntArrayLike = Array<number> | Int16Array;
/*export interface NumberArrayLike {
readonly length: number;
[n: number]: number;
}*/
export interface Disposable { export interface Disposable {
dispose (): void; dispose (): void;
} }

View File

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

View File

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

View File

@ -7,6 +7,9 @@
{ {
"path": "./spine-canvas" "path": "./spine-canvas"
}, },
{
"path": "./spine-canvaskit"
},
{ {
"path": "./spine-webgl" "path": "./spine-webgl"
}, },