diff --git a/spine-ts/README.md b/spine-ts/README.md
index 6343767fc..1161689b6 100644
--- a/spine-ts/README.md
+++ b/spine-ts/README.md
@@ -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 `
-// spine-ts Canvas
+// spine-canvas
-// spine-ts WebGL
+// spine-canvaskit
+
+
+// spine-webgl
-// spine-ts Player, which requires a spine-player.css as well
+// spine-player, which requires a spine-player.css as well
-// spine-ts ThreeJS
+// spine-threejs
// 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
diff --git a/spine-ts/index.html b/spine-ts/index.html
index 60518ab10..343e4d3fb 100644
--- a/spine-ts/index.html
+++ b/spine-ts/index.html
@@ -20,6 +20,10 @@
Mouse click
+
CanvasKit
+
Pixi
Basic example
diff --git a/spine-ts/package-lock.json b/spine-ts/package-lock.json
index 194b9a5f3..a5a862600 100644
--- a/spine-ts/package-lock.json
+++ b/spine-ts/package-lock.json
@@ -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,15 @@
"resolved": "spine-webgl",
"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": {
"version": "7.4.2",
"license": "MIT",
@@ -226,6 +240,15 @@
"license": "MIT",
"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": {
"version": "2019.7.3",
"dev": true,
@@ -244,6 +267,11 @@
"dev": true,
"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": {
"version": "1.3.8",
"dev": true,
@@ -516,6 +544,14 @@
"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": {
"version": "4.1.2",
"dev": true,
@@ -1954,6 +1990,12 @@
"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": {
"version": "1.3.3",
"dev": true,
@@ -2760,6 +2802,12 @@
"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": {
"version": "1.0.1",
"dev": true,
@@ -2995,6 +3043,19 @@
"@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": {
"name": "@esotericsoftware/spine-core",
"version": "4.2.48",
diff --git a/spine-ts/package.json b/spine-ts/package.json
index 9f615570d..c872cc41c 100644
--- a/spine-ts/package.json
+++ b/spine-ts/package.json
@@ -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"
}
}
\ No newline at end of file
diff --git a/spine-ts/spine-canvaskit/LICENSE b/spine-ts/spine-canvaskit/LICENSE
new file mode 100644
index 000000000..4501a611f
--- /dev/null
+++ b/spine-ts/spine-canvaskit/LICENSE
@@ -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.
\ No newline at end of file
diff --git a/spine-ts/spine-canvaskit/README.md b/spine-ts/spine-canvaskit/README.md
new file mode 100644
index 000000000..65a181ec7
--- /dev/null
+++ b/spine-ts/spine-canvaskit/README.md
@@ -0,0 +1,3 @@
+# spine-ts CanvasKit
+
+Please see the top-level [README.md](../README.md) for more information.
diff --git a/spine-ts/spine-canvaskit/example/assets/spineboy-pro.skel b/spine-ts/spine-canvaskit/example/assets/spineboy-pro.skel
new file mode 100644
index 000000000..09e564b7e
Binary files /dev/null and b/spine-ts/spine-canvaskit/example/assets/spineboy-pro.skel differ
diff --git a/spine-ts/spine-canvaskit/example/assets/spineboy.atlas b/spine-ts/spine-canvaskit/example/assets/spineboy.atlas
new file mode 100644
index 000000000..eca542b71
--- /dev/null
+++ b/spine-ts/spine-canvaskit/example/assets/spineboy.atlas
@@ -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
diff --git a/spine-ts/spine-canvaskit/example/assets/spineboy.png b/spine-ts/spine-canvaskit/example/assets/spineboy.png
new file mode 100644
index 000000000..0ea9737f3
Binary files /dev/null and b/spine-ts/spine-canvaskit/example/assets/spineboy.png differ
diff --git a/spine-ts/spine-canvaskit/example/headless.js b/spine-ts/spine-canvaskit/example/headless.js
new file mode 100644
index 000000000..46c67afda
--- /dev/null
+++ b/spine-ts/spine-canvaskit/example/headless.js
@@ -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();
diff --git a/spine-ts/spine-canvaskit/example/index.html b/spine-ts/spine-canvaskit/example/index.html
new file mode 100644
index 000000000..ca87197da
--- /dev/null
+++ b/spine-ts/spine-canvaskit/example/index.html
@@ -0,0 +1,80 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+ CanvasKit Example
+
+
+
+
+
+
\ No newline at end of file
diff --git a/spine-ts/spine-canvaskit/package.json b/spine-ts/spine-canvaskit/package.json
new file mode 100644
index 000000000..e9e135ac0
--- /dev/null
+++ b/spine-ts/spine-canvaskit/package.json
@@ -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"
+ }
+}
\ No newline at end of file
diff --git a/spine-ts/spine-canvaskit/src/index.ts b/spine-ts/spine-canvaskit/src/index.ts
new file mode 100644
index 000000000..60f13988e
--- /dev/null
+++ b/spine-ts/spine-canvaskit/src/index.ts
@@ -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, 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): Promise {
+ 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();
+ 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): Promise {
+ 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;
+ 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(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();
+ }
+}
\ No newline at end of file
diff --git a/spine-ts/spine-canvaskit/tsconfig.json b/spine-ts/spine-canvaskit/tsconfig.json
new file mode 100644
index 000000000..4a16afef6
--- /dev/null
+++ b/spine-ts/spine-canvaskit/tsconfig.json
@@ -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"
+ }
+ ]
+}
diff --git a/spine-ts/spine-core/src/SkeletonBinary.ts b/spine-ts/spine-core/src/SkeletonBinary.ts
index f08847f9c..7e6621a84 100644
--- a/spine-ts/spine-core/src/SkeletonBinary.ts
+++ b/spine-ts/spine-core/src/SkeletonBinary.ts
@@ -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(), private index: number = 0, private buffer = new DataView(data.buffer)) {
+ constructor (data: Uint8Array | ArrayBuffer, public strings = new Array(), private index: number = 0, private buffer = new DataView(data instanceof ArrayBuffer ? data : data.buffer)) {
}
readByte (): number {
diff --git a/spine-ts/spine-core/src/Utils.ts b/spine-ts/spine-core/src/Utils.ts
index c821117d2..ddeb4f2c3 100644
--- a/spine-ts/spine-core/src/Utils.ts
+++ b/spine-ts/spine-core/src/Utils.ts
@@ -90,11 +90,6 @@ export class StringSet {
export type NumberArrayLike = Array | Float32Array;
export type IntArrayLike = Array | Int16Array;
-/*export interface NumberArrayLike {
- readonly length: number;
- [n: number]: number;
-}*/
-
export interface Disposable {
dispose (): void;
}
diff --git a/spine-ts/spine-player/src/Player.ts b/spine-ts/spine-player/src/Player.ts
index 611382653..27f9975a1 100644
--- a/spine-ts/spine-player/src/Player.ts
+++ b/spine-ts/spine-player/src/Player.ts
@@ -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. */
diff --git a/spine-ts/spine-player/src/PlayerEditor.ts b/spine-ts/spine-player/src/PlayerEditor.ts
index 329f0d2ab..955d7c72f 100644
--- a/spine-ts/spine-player/src/PlayerEditor.ts
+++ b/spine-ts/spine-player/src/PlayerEditor.ts
@@ -128,7 +128,7 @@ body { margin: 0px; }
this.startPlayer();
}
- private timerId = 0;
+ private timerId: any = 0;
startPlayer () {
clearTimeout(this.timerId);
this.timerId = setTimeout(() => {
diff --git a/spine-ts/tsconfig.json b/spine-ts/tsconfig.json
index d739fc105..b2f7b54cb 100644
--- a/spine-ts/tsconfig.json
+++ b/spine-ts/tsconfig.json
@@ -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"
- }
- ]
-}
\ No newline at end of file
+ "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"
+ }
+ ]
+}