Ver Fonte

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

Mario Zechner há 1 ano atrás
pai
commit
a1f077d43c

+ 17 - 10
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 `<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/[email protected].*/dist/iife/spine-core.js"></script>
 
-// spine-ts Canvas
+// spine-canvas
 <script src="https://unpkg.com/@esotericsoftware/[email protected].*/dist/iife/spine-canvas.js"></script>
 
-// spine-ts WebGL
+// spine-canvaskit
+<script src="https://unpkg.com/@esotericsoftware/[email protected].*/dist/iife/spine-canvaskit.js"></script>
+
+// spine-webgl
 <script src="https://unpkg.com/@esotericsoftware/[email protected].*/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/[email protected].*/dist/iife/spine-player.js"></script>
 <link rel="stylesheet" href="https://unpkg.com/@esotericsoftware/[email protected].*/dist/spine-player.css">
 
-// spine-ts ThreeJS
+// spine-threejs
 <script src="https://unpkg.com/@esotericsoftware/[email protected].*/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

+ 4 - 0
spine-ts/index.html

@@ -20,6 +20,10 @@
           <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>
+      </ul>
       <li>Pixi</li>
       <ul>
         <li><a href="/spine-pixi/example/index.html">Basic example</a></li>

+ 61 - 0
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",

+ 10 - 7
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"
   }
 }

+ 26 - 0
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.

+ 3 - 0
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.

BIN
spine-ts/spine-canvaskit/example/assets/spineboy-pro.skel


+ 94 - 0
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

BIN
spine-ts/spine-canvaskit/example/assets/spineboy.png


+ 76 - 0
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();

+ 80 - 0
spine-ts/spine-canvaskit/example/index.html

@@ -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>

+ 41 - 0
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"
+  }
+}

+ 180 - 0
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<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();
+    }
+}

+ 18 - 0
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"
+    }
+  ]
+}

+ 2 - 2
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<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 {

+ 0 - 5
spine-ts/spine-core/src/Utils.ts

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

+ 1 - 1
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. */

+ 1 - 1
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(() => {

+ 28 - 25
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"
-		}
-	]
-}
+  "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"
+    }
+  ]
+}