Преглед изворни кода

feat: add support for pixijs

eCode пре 2 година
родитељ
комит
4876dba946

+ 1 - 0
.gitignore

@@ -145,6 +145,7 @@ spine-ts/spine-canvas/dist
 spine-ts/spine-webgl/dist
 spine-ts/spine-player/dist
 spine-ts/spine-threejs/dist
+spine-ts/spine-pixi/dist
 spine-libgdx/gradle
 spine-libgdx/gradlew
 spine-libgdx/gradlew.bat

+ 1 - 0
spine-ts/build.sh

@@ -20,6 +20,7 @@ then
 		spine-webgl/dist/iife/* \
 		spine-player/dist/iife/* \
 		spine-threejs/dist/iife/* \
+		spine-pixi/dist/iife/* \
 		spine-player/css/spine-player.css
 	curl -f -F "[email protected]" "$TS_UPDATE_URL$BRANCH"
 else

+ 237 - 2
spine-ts/package-lock.json

@@ -14,6 +14,7 @@
         "spine-phaser",
         "spine-player",
         "spine-threejs",
+        "spine-pixi",
         "spine-webgl"
       ],
       "devDependencies": {
@@ -393,6 +394,10 @@
       "resolved": "spine-phaser",
       "link": true
     },
+    "node_modules/@esotericsoftware/spine-pixi": {
+      "resolved": "spine-pixi",
+      "link": true
+    },
     "node_modules/@esotericsoftware/spine-player": {
       "resolved": "spine-player",
       "link": true
@@ -405,11 +410,182 @@
       "resolved": "spine-webgl",
       "link": true
     },
+    "node_modules/@pixi/assets": {
+      "version": "7.2.4",
+      "resolved": "https://registry.npmjs.org/@pixi/assets/-/assets-7.2.4.tgz",
+      "integrity": "sha512-7199re3wvMAlVqXLaCyAr8IkJSXqkeVAxcYyB2rBu4Id5m2hhlGX1dQsdMBiCXLwu6/LLVqDvJggSNVQBzL6ZQ==",
+      "peer": true,
+      "dependencies": {
+        "@types/css-font-loading-module": "^0.0.7"
+      },
+      "peerDependencies": {
+        "@pixi/core": "7.2.4",
+        "@pixi/utils": "7.2.4"
+      }
+    },
+    "node_modules/@pixi/color": {
+      "version": "7.2.4",
+      "resolved": "https://registry.npmjs.org/@pixi/color/-/color-7.2.4.tgz",
+      "integrity": "sha512-B/+9JRcXe2uE8wQfsueFRPZVayF2VEMRB7XGeRAsWCryOX19nmWhv0Nt3nOU2rvzI0niz9XgugJXsB6vVmDFSg==",
+      "peer": true,
+      "dependencies": {
+        "colord": "^2.9.3"
+      }
+    },
+    "node_modules/@pixi/constants": {
+      "version": "7.2.4",
+      "resolved": "https://registry.npmjs.org/@pixi/constants/-/constants-7.2.4.tgz",
+      "integrity": "sha512-hKuHBWR6N4Q0Sf5MGF3/9l+POg/G5rqhueHfzofiuelnKg7aBs3BVjjZ+6hZbd6M++vOUmxYelEX/NEFBxrheA==",
+      "peer": true
+    },
+    "node_modules/@pixi/core": {
+      "version": "7.2.4",
+      "resolved": "https://registry.npmjs.org/@pixi/core/-/core-7.2.4.tgz",
+      "integrity": "sha512-0XtvrfxHlS2T+beBBSpo7GI8+QLyyTqMVQpNmPqB4woYxzrOEJ9JaUFBaBfCvycLeUkfVih1u6HAbtF+2d1EjQ==",
+      "peer": true,
+      "dependencies": {
+        "@pixi/color": "7.2.4",
+        "@pixi/constants": "7.2.4",
+        "@pixi/extensions": "7.2.4",
+        "@pixi/math": "7.2.4",
+        "@pixi/runner": "7.2.4",
+        "@pixi/settings": "7.2.4",
+        "@pixi/ticker": "7.2.4",
+        "@pixi/utils": "7.2.4",
+        "@types/offscreencanvas": "^2019.6.4"
+      },
+      "funding": {
+        "type": "opencollective",
+        "url": "https://opencollective.com/pixijs"
+      }
+    },
+    "node_modules/@pixi/display": {
+      "version": "7.2.4",
+      "resolved": "https://registry.npmjs.org/@pixi/display/-/display-7.2.4.tgz",
+      "integrity": "sha512-w5tqb8cWEO5qIDaO9GEqRvxYhL0iMk0Wsngw23bbLm1gLEQmrFkB2tpJlRAqd7H82C3DrDDeWvkrrxW6+m4apg==",
+      "peer": true,
+      "peerDependencies": {
+        "@pixi/core": "7.2.4"
+      }
+    },
+    "node_modules/@pixi/extensions": {
+      "version": "7.2.4",
+      "resolved": "https://registry.npmjs.org/@pixi/extensions/-/extensions-7.2.4.tgz",
+      "integrity": "sha512-Mnqv9scbL1ARD3QFKfOWs2aSVJJfP1dL8g5UiqGImYO3rZbz/9QCzXOeMVIZ5n3iaRyKMNhFFr84/zUja2H7Dw==",
+      "peer": true
+    },
+    "node_modules/@pixi/graphics": {
+      "version": "7.2.4",
+      "resolved": "https://registry.npmjs.org/@pixi/graphics/-/graphics-7.2.4.tgz",
+      "integrity": "sha512-3A2EumTjWJgXlDLOyuBrl9b6v1Za/E+/IjOGUIX843HH4NYaf1a2sfDfljx6r3oiDvy+VhuBFmgynRcV5IyA0Q==",
+      "peer": true,
+      "peerDependencies": {
+        "@pixi/core": "7.2.4",
+        "@pixi/display": "7.2.4",
+        "@pixi/sprite": "7.2.4"
+      }
+    },
+    "node_modules/@pixi/math": {
+      "version": "7.2.4",
+      "resolved": "https://registry.npmjs.org/@pixi/math/-/math-7.2.4.tgz",
+      "integrity": "sha512-LJB+mozyEPllxa0EssFZrKNfVwysfaBun4b2dJKQQInp0DafgbA0j7A+WVg0oe51KhFULTJMpDqbLn/ITFc41A==",
+      "peer": true
+    },
+    "node_modules/@pixi/mesh": {
+      "version": "7.2.4",
+      "resolved": "https://registry.npmjs.org/@pixi/mesh/-/mesh-7.2.4.tgz",
+      "integrity": "sha512-wiALIqcRKib2BqeH9kOA5fOKWN352nqAspgbDa8gA7OyWzmNwqIedIlElixd0oLFOrIN5jOZAdzeKnoYQlt9Aw==",
+      "peer": true,
+      "peerDependencies": {
+        "@pixi/core": "7.2.4",
+        "@pixi/display": "7.2.4"
+      }
+    },
+    "node_modules/@pixi/runner": {
+      "version": "7.2.4",
+      "resolved": "https://registry.npmjs.org/@pixi/runner/-/runner-7.2.4.tgz",
+      "integrity": "sha512-YtyqPk1LA+0guEFKSFx6t/YSvbEQwajFwi4Ft8iDhioa6VK2MmTir1GjWwy7JQYLcDmYSAcQjnmFtVTZohyYSw==",
+      "peer": true
+    },
+    "node_modules/@pixi/settings": {
+      "version": "7.2.4",
+      "resolved": "https://registry.npmjs.org/@pixi/settings/-/settings-7.2.4.tgz",
+      "integrity": "sha512-ZPKRar9EwibijGmH8EViu4Greq1I/O7V/xQx2rNqN23XA7g09Qo6yfaeQpufu5xl8+/lZrjuHtQSnuY7OgG1CA==",
+      "peer": true,
+      "dependencies": {
+        "@pixi/constants": "7.2.4",
+        "@types/css-font-loading-module": "^0.0.7",
+        "ismobilejs": "^1.1.0"
+      }
+    },
+    "node_modules/@pixi/sprite": {
+      "version": "7.2.4",
+      "resolved": "https://registry.npmjs.org/@pixi/sprite/-/sprite-7.2.4.tgz",
+      "integrity": "sha512-DhR1B+/d0eXpxHIesJMXcVPrKFwQ+zRA1LvEIFfzewqfaRN3X6PMIuoKX8SIb6tl+Hq8Ba9Pe28zI7d2rmRzrA==",
+      "peer": true,
+      "peerDependencies": {
+        "@pixi/core": "7.2.4",
+        "@pixi/display": "7.2.4"
+      }
+    },
+    "node_modules/@pixi/text": {
+      "version": "7.2.4",
+      "resolved": "https://registry.npmjs.org/@pixi/text/-/text-7.2.4.tgz",
+      "integrity": "sha512-DGu7ktpe+zHhqR2sG9NsJt4mgvSObv5EqXTtUxD4Z0li1gmqF7uktpLyn5I6vSg1TTEL4TECClRDClVDGiykWw==",
+      "peer": true,
+      "peerDependencies": {
+        "@pixi/core": "7.2.4",
+        "@pixi/sprite": "7.2.4"
+      }
+    },
+    "node_modules/@pixi/ticker": {
+      "version": "7.2.4",
+      "resolved": "https://registry.npmjs.org/@pixi/ticker/-/ticker-7.2.4.tgz",
+      "integrity": "sha512-hQQHIHvGeFsP4GNezZqjzuhUgNQEVgCH9+qU05UX1Mc5UHC9l6OJnY4VTVhhcHxZjA6RnyaY+1zBxCnoXuazpg==",
+      "peer": true,
+      "dependencies": {
+        "@pixi/extensions": "7.2.4",
+        "@pixi/settings": "7.2.4",
+        "@pixi/utils": "7.2.4"
+      }
+    },
+    "node_modules/@pixi/utils": {
+      "version": "7.2.4",
+      "resolved": "https://registry.npmjs.org/@pixi/utils/-/utils-7.2.4.tgz",
+      "integrity": "sha512-VUGQHBOINIS4ePzoqafwxaGPVRTa3oM/mEutIIHbNGI3b+QvSO+1Dnk40M0zcH6Bo+MxQZbOZK5X/wO9oU5+LQ==",
+      "peer": true,
+      "dependencies": {
+        "@pixi/color": "7.2.4",
+        "@pixi/constants": "7.2.4",
+        "@pixi/settings": "7.2.4",
+        "@types/earcut": "^2.1.0",
+        "earcut": "^2.2.4",
+        "eventemitter3": "^4.0.0",
+        "url": "^0.11.0"
+      }
+    },
+    "node_modules/@pixi/utils/node_modules/eventemitter3": {
+      "version": "4.0.7",
+      "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-4.0.7.tgz",
+      "integrity": "sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw==",
+      "peer": true
+    },
+    "node_modules/@types/css-font-loading-module": {
+      "version": "0.0.7",
+      "resolved": "https://registry.npmjs.org/@types/css-font-loading-module/-/css-font-loading-module-0.0.7.tgz",
+      "integrity": "sha512-nl09VhutdjINdWyXxHWN/w9zlNCfr60JUqJbd24YXUuCwgeL0TpFSdElCwb6cxfB6ybE19Gjj4g0jsgkXxKv1Q==",
+      "peer": true
+    },
+    "node_modules/@types/earcut": {
+      "version": "2.1.1",
+      "resolved": "https://registry.npmjs.org/@types/earcut/-/earcut-2.1.1.tgz",
+      "integrity": "sha512-w8oigUCDjElRHRRrMvn/spybSMyX8MTkKA5Dv+tS1IE/TgmNZPqUYtvYBXGY8cieSE66gm+szeK+bnbxC2xHTQ==",
+      "peer": true
+    },
     "node_modules/@types/offscreencanvas": {
       "version": "2019.7.0",
       "resolved": "https://registry.npmjs.org/@types/offscreencanvas/-/offscreencanvas-2019.7.0.tgz",
-      "integrity": "sha512-PGcyveRIpL1XIqK8eBsmRBt76eFgtzuPiSTyKHZxnGemp2yzGzWpjYKAfK3wIMiU7eH+851yEpiuP8JZerTmWg==",
-      "dev": true
+      "integrity": "sha512-PGcyveRIpL1XIqK8eBsmRBt76eFgtzuPiSTyKHZxnGemp2yzGzWpjYKAfK3wIMiU7eH+851yEpiuP8JZerTmWg=="
     },
     "node_modules/@types/three": {
       "version": "0.146.0",
@@ -875,6 +1051,12 @@
       "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
       "dev": true
     },
+    "node_modules/colord": {
+      "version": "2.9.3",
+      "resolved": "https://registry.npmjs.org/colord/-/colord-2.9.3.tgz",
+      "integrity": "sha512-jeC1axXpnb0/2nn/Y1LPuLdgXBLH7aDcHu4KEKfqw3CUhX7ZpfBSlPKyqXE6btIgEzfWtrX3/tyBCaCvXvMkOw==",
+      "peer": true
+    },
     "node_modules/colors": {
       "version": "1.4.0",
       "resolved": "https://registry.npmjs.org/colors/-/colors-1.4.0.tgz",
@@ -1092,6 +1274,12 @@
       "integrity": "sha512-jtD6YG370ZCIi/9GTaJKQxWTZD045+4R4hTk/x1UyoqadyJ9x9CgSi1RlVDQF8U2sxLLSnFkCaMihqljHIWgMg==",
       "dev": true
     },
+    "node_modules/earcut": {
+      "version": "2.2.4",
+      "resolved": "https://registry.npmjs.org/earcut/-/earcut-2.2.4.tgz",
+      "integrity": "sha512-/pjZsA1b4RPHbeWZQn66SWS8nZZWLQQ23oE3Eam7aroEFGEvwKAsJfZ9ytiEMycfzXWpca4FA9QIOehf7PocBQ==",
+      "peer": true
+    },
     "node_modules/ee-first": {
       "version": "1.1.1",
       "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz",
@@ -1774,6 +1962,12 @@
       "integrity": "sha512-D2S+3GLxWH+uhrNEcoh/fnmYeP8E8/zHl644d/jdA0g2uyXvy3sb0qxotE+ne0LtccHknQzWwZEzhak7oJ0COQ==",
       "dev": true
     },
+    "node_modules/ismobilejs": {
+      "version": "1.1.1",
+      "resolved": "https://registry.npmjs.org/ismobilejs/-/ismobilejs-1.1.1.tgz",
+      "integrity": "sha512-VaFW53yt8QO61k2WJui0dHf4SlL8lxBofUuUmwBo0ljPk0Drz2TiuDW4jo3wDcv41qy/SxrJ+VAzJ/qYqsmzRw==",
+      "peer": true
+    },
     "node_modules/isobject": {
       "version": "3.0.1",
       "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz",
@@ -2332,6 +2526,22 @@
         "node": ">=0.8.0"
       }
     },
+    "node_modules/punycode": {
+      "version": "1.3.2",
+      "resolved": "https://registry.npmjs.org/punycode/-/punycode-1.3.2.tgz",
+      "integrity": "sha512-RofWgt/7fL5wP1Y7fxE7/EmTLzQVnB0ycyibJ0OOHIlJqTNzglYFxVwETOcIoJqJmpDXJ9xImDv+Fq34F/d4Dw==",
+      "peer": true
+    },
+    "node_modules/querystring": {
+      "version": "0.2.0",
+      "resolved": "https://registry.npmjs.org/querystring/-/querystring-0.2.0.tgz",
+      "integrity": "sha512-X/xY82scca2tau62i9mDyU9K+I+djTMUsvwf7xnUX5GLvVzgJybOJf4Y6o9Zx3oJK/LSXg5tTZBjwzqVPaPO2g==",
+      "deprecated": "The querystring API is considered Legacy. new code should use the URLSearchParams API instead.",
+      "peer": true,
+      "engines": {
+        "node": ">=0.4.x"
+      }
+    },
     "node_modules/range-parser": {
       "version": "1.2.1",
       "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz",
@@ -3260,6 +3470,16 @@
       "deprecated": "Please see https://github.com/lydell/urix#deprecated",
       "dev": true
     },
+    "node_modules/url": {
+      "version": "0.11.0",
+      "resolved": "https://registry.npmjs.org/url/-/url-0.11.0.tgz",
+      "integrity": "sha512-kbailJa29QrtXnxgq+DdCEGlbTeYM2eJUxsz6vjZavrCYPMIFHMKQmSKYAIuUK2i7hgPm28a8piX5NTUtM/LKQ==",
+      "peer": true,
+      "dependencies": {
+        "punycode": "1.3.2",
+        "querystring": "0.2.0"
+      }
+    },
     "node_modules/use": {
       "version": "3.1.1",
       "resolved": "https://registry.npmjs.org/use/-/use-3.1.1.tgz",
@@ -3417,6 +3637,21 @@
         "@esotericsoftware/spine-webgl": "4.1.31"
       }
     },
+    "spine-pixi": {
+      "version": "4.1.31",
+      "license": "LicenseRef-LICENSE",
+      "dependencies": {
+        "@esotericsoftware/spine-core": "4.1.31"
+      },
+      "peerDependencies": {
+        "@pixi/assets": "^7.2.4",
+        "@pixi/core": "^7.2.4",
+        "@pixi/display": "^7.2.4",
+        "@pixi/graphics": "^7.2.4",
+        "@pixi/mesh": "^7.2.4",
+        "@pixi/text": "^7.2.4"
+      }
+    },
     "spine-player": {
       "name": "@esotericsoftware/spine-player",
       "version": "4.1.31",

+ 14 - 5
spine-ts/package.json

@@ -7,8 +7,8 @@
   ],
   "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",
-    "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\"",
+    "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\"",
     "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",
@@ -17,14 +17,22 @@
     "build:player": "npx copyfiles -f spine-player/css/spine-player.css spine-player/dist/ && npx 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",
-    "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",
-    "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\"",
+    
+    
+    
+    "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\"",
     "dev:modules": "npm run build:modules -- --watch",
     "dev:canvas": "npm run build:canvas -- --watch",
     "dev:webgl": "npm run build:webgl -- --watch",
     "dev:phaser": "npm run build:phaser -- --watch",
     "dev:player": "npm run build:player -- --watch",
-    "dev:threejs": "npm run build:threejs -- --watch"
+    "dev:threejs": "npm run build:threejs -- --watch",
+    "dev:pixi": "npm run build:pixi -- --watch"
   },
   "repository": {
     "type": "git",
@@ -51,6 +59,7 @@
     "spine-phaser",
     "spine-player",
     "spine-threejs",
+    "spine-pixi",
     "spine-webgl"
   ],
   "devDependencies": {

+ 3 - 0
spine-ts/spine-pixi/README.md

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

Разлика између датотеке није приказан због своје велике величине
+ 541 - 0
spine-ts/spine-pixi/example/assets/spineboy-pro.json


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


+ 101 - 0
spine-ts/spine-pixi/example/assets/spineboy.atlas

@@ -0,0 +1,101 @@
+spineboy.png
+	size: 1024, 256
+	filter: Linear, Linear
+	scale: 0.5
+crosshair
+	bounds: 813, 160, 45, 45
+eye-indifferent
+	bounds: 569, 2, 47, 45
+eye-surprised
+	bounds: 643, 7, 47, 45
+	rotate: 90
+front-bracer
+	bounds: 811, 51, 29, 40
+front-fist-closed
+	bounds: 807, 93, 38, 41
+front-fist-open
+	bounds: 815, 210, 43, 44
+front-foot
+	bounds: 706, 64, 63, 35
+	rotate: 90
+front-shin
+	bounds: 80, 11, 41, 92
+front-thigh
+	bounds: 754, 12, 23, 56
+front-upper-arm
+	bounds: 618, 5, 23, 49
+goggles
+	bounds: 214, 20, 131, 83
+gun
+	bounds: 347, 14, 105, 102
+	rotate: 90
+head
+	bounds: 80, 105, 136, 149
+hoverboard-board
+	bounds: 2, 8, 246, 76
+	rotate: 90
+hoverboard-thruster
+	bounds: 478, 2, 30, 32
+hoverglow-small
+	bounds: 218, 117, 137, 38
+	rotate: 90
+mouth-grind
+	bounds: 775, 80, 47, 30
+	rotate: 90
+mouth-oooo
+	bounds: 779, 31, 47, 30
+	rotate: 90
+mouth-smile
+	bounds: 783, 207, 47, 30
+	rotate: 90
+muzzle-glow
+	bounds: 779, 4, 25, 25
+muzzle-ring
+	bounds: 451, 14, 25, 105
+muzzle01
+	bounds: 664, 60, 67, 40
+	rotate: 90
+muzzle02
+	bounds: 580, 56, 68, 42
+	rotate: 90
+muzzle03
+	bounds: 478, 36, 83, 53
+	rotate: 90
+muzzle04
+	bounds: 533, 49, 75, 45
+	rotate: 90
+muzzle05
+	bounds: 624, 56, 68, 38
+	rotate: 90
+neck
+	bounds: 806, 8, 18, 21
+portal-bg
+	bounds: 258, 121, 133, 133
+portal-flare1
+	bounds: 690, 2, 56, 30
+	rotate: 90
+portal-flare2
+	bounds: 510, 3, 57, 31
+portal-flare3
+	bounds: 722, 4, 58, 30
+	rotate: 90
+portal-shade
+	bounds: 393, 121, 133, 133
+portal-streaks1
+	bounds: 528, 126, 126, 128
+portal-streaks2
+	bounds: 656, 129, 125, 125
+rear-bracer
+	bounds: 826, 13, 28, 36
+rear-foot
+	bounds: 743, 70, 57, 30
+	rotate: 90
+rear-shin
+	bounds: 174, 14, 38, 89
+rear-thigh
+	bounds: 783, 158, 28, 47
+rear-upper-arm
+	bounds: 783, 136, 20, 44
+	rotate: 90
+torso
+	bounds: 123, 13, 49, 90

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


+ 136 - 0
spine-ts/spine-pixi/example/index.html

@@ -0,0 +1,136 @@
+<html>
+
+<head>
+	<meta charset="UTF-8">
+	<title>spine-pixi</title>
+	<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/pixi.min.js"></script>
+	<script src="../dist/iife/spine-pixi.js"></script>
+	<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/tweakpane.min.js"></script>
+</head>
+<style>
+	* {
+		margin: 0;
+		padding: 0;
+	}
+
+	body,
+	html {
+		height: 100%
+	}
+
+	canvas {
+		position: absolute;
+		width: 100%;
+		height: 100%;
+	}
+</style>
+
+<body>
+	<script>
+		(async function () {
+			var app = new PIXI.Application({
+				width: window.innerWidth,
+				height: window.innerHeight,
+				resolution: window.devicePixelRatio || 1,
+				autoDensity: true,
+				resizeTo: window,
+				backgroundColor: 0x2c3e50,
+				hello:true
+			});
+			document.body.appendChild(app.view);
+
+			PIXI.Assets.add("spineboySkeletonJson", "./assets/spineboy-pro.json");
+			PIXI.Assets.add("spineboySkeletonBinary", "./assets/spineboy-pro.skel");
+			PIXI.Assets.add("spineboyAtlas", "./assets/spineboy.atlas");
+
+			await PIXI.Assets.load([
+				"spineboySkeletonJson",
+				"spineboySkeletonBinary",
+				"spineboyAtlas",
+			]);
+
+			// Create the spine display object
+			const spineBoy = spine.Spine.from("spineboySkeletonJson", "spineboyAtlas", { scale: 0.5 });
+
+			// .from(...) is a shortcut + cache for creating the skeleton data at a certain scale
+			// Here would be the "long way" of doing it (without cache):
+
+			// const skeletonAsset = Assets.get(skeletonAssetName);
+			// const atlasAsset = Assets.get(atlasAssetName);
+			// const attachmentLoader = new AtlasAttachmentLoader(atlasAsset);
+			// let parser; // You can skip this guessing step if you know the type of the skeleton asset
+			// if (skeletonAsset instanceof Uint8Array) {
+			// 	parser = new SkeletonBinary(attachmentLoader);
+			// } else {
+			// 	parser = new SkeletonJson(attachmentLoader);
+			// }
+			// parser.scale = options?.scale ?? 1;
+			// skeletonData = parser.readSkeletonData(skeletonAsset);
+			// onst spineBoy = new spine.Spine(skeletonData, options);
+
+
+			// Set the position
+			spineBoy.x = window.innerWidth / 2;
+			spineBoy.y = window.innerHeight * 0.9;
+
+			// start an animation. AutoUpdate is on by default, we don't need a manual rAF loop
+			spineBoy.state.setAnimation(0, "run", true);
+
+			// add to stage
+			app.stage.addChild(spineBoy);
+
+			// do we want debug? we can have debug!
+			const spinedebugger = new spine.SpineDebugRenderer();
+			spineBoy.debug = spinedebugger;
+
+			// End of spine setup. The rest is the tweakpane on the right to play with the spineboy
+
+			const pane = new Tweakpane.Pane({ title: 'spine pixi.js' });
+
+			// spineboy position on screen
+			pane.addInput(spineBoy, 'position', {
+				x: { min: 0, max: window.innerWidth },
+				y: { min: 0, max: window.innerHeight },
+			});
+
+			// Interesting example on how to get the pixi global position of a bone, and how to set a bone to a pixi global position
+			// spine's "global" position is local to the spine display object. It's not the same as pixi's global position
+			const aux = {aimPosition:spineBoy.toGlobal(spineBoy.getBonePosition("crosshair"))};
+			const aimControl = pane.addInput(aux, 'aimPosition', {
+				x: { min: 0, max: window.innerWidth },
+				y: { min: 0, max: window.innerHeight },
+			}).on("change", (e) => {
+				spineBoy.setBonePosition("crosshair", spineBoy.toLocal(e.value));
+			})
+			aimControl.hidden = true;
+
+			// anim changer
+			pane.addBlade({
+				view: 'list',
+				label: 'animation',
+				options: spineBoy.skeleton.data.animations.map(a => ({ text: a.name, value: a.name })),
+				value: 'run',
+			}).on("change", (e) => {
+				spineBoy.state.setAnimation(0, e.value, true);
+				aimControl.hidden = !(e.value == "aim")
+			})
+
+			// turn on or off debug draws
+			const debugFolder = pane.addFolder({
+				title: 'Debug options',
+			});
+
+			debugFolder.addInput(spinedebugger, 'drawMeshHull');
+			debugFolder.addInput(spinedebugger, 'drawMeshTriangles');
+			debugFolder.addInput(spinedebugger, 'drawBones');
+			debugFolder.addInput(spinedebugger, 'drawPaths');
+			debugFolder.addInput(spinedebugger, 'drawBoundingBoxes');
+			debugFolder.addInput(spinedebugger, 'drawClipping');
+			debugFolder.addInput(spinedebugger, 'drawRegionAttachments');
+			debugFolder.addInput(spinedebugger, 'drawEvents');
+
+		})();
+	</script>
+</body>
+
+</html>

+ 43 - 0
spine-ts/spine-pixi/package.json

@@ -0,0 +1,43 @@
+{
+  "name": "@esotericsoftware/spine-pixi",
+  "version": "4.1.31",
+  "description": "The official Spine Runtimes for the web.",
+  "main": "dist/index.js",
+  "types": "dist/index.d.ts",
+  "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.1.31"
+  },
+  "peerDependencies": {
+    "@pixi/core": "^7.2.4",
+    "@pixi/display": "^7.2.4",
+    "@pixi/graphics": "^7.2.4",
+    "@pixi/text": "^7.2.4",
+    "@pixi/assets": "^7.2.4",
+    "@pixi/mesh": "^7.2.4"
+  }
+}

+ 88 - 0
spine-ts/spine-pixi/src/DarkSlotMesh.ts

@@ -0,0 +1,88 @@
+import { SpineTexture } from "./SpineTexture";
+import type { BlendMode, NumberArrayLike } from "@esotericsoftware/spine-core";
+import { DarkTintMesh } from "./darkTintMesh/DarkTintMesh";
+import type { ISlotMesh } from "./Spine";
+
+export class DarkSlotMesh extends DarkTintMesh implements ISlotMesh {
+	public name: string = "";
+
+	private static auxColor = [0, 0, 0, 0];
+
+	constructor() {
+		super();
+	}
+	public updateFromSpineData(
+		slotTexture: SpineTexture,
+		slotBlendMode: BlendMode,
+		slotName: string,
+		finalVertices: NumberArrayLike,
+		finalVerticesLength: number,
+		finalIndices: NumberArrayLike,
+		finalIndicesLength: number,
+		darkTint: boolean
+	): void {
+		this.texture = slotTexture.texture;
+
+		const vertLenght = (finalVerticesLength / (darkTint ? 12 : 8)) * 2;
+
+		if (this.geometry.getBuffer("aTextureCoord").data?.length !== vertLenght) {
+			this.geometry.getBuffer("aTextureCoord").data = new Float32Array(vertLenght);
+		}
+
+		if (this.geometry.getBuffer("aVertexPosition").data?.length !== vertLenght) {
+			this.geometry.getBuffer("aVertexPosition").data = new Float32Array(vertLenght);
+		}
+
+		let vertIndex = 0;
+
+		for (let i = 0; i < finalVerticesLength; i += darkTint ? 12 : 8) {
+			let auxi = i;
+
+			this.geometry.getBuffer("aVertexPosition").data[vertIndex] = finalVertices[auxi++];
+			this.geometry.getBuffer("aVertexPosition").data[vertIndex + 1] = finalVertices[auxi++];
+
+			auxi += 4; // color
+
+			this.geometry.getBuffer("aTextureCoord").data[vertIndex] = finalVertices[auxi++];
+			this.geometry.getBuffer("aTextureCoord").data[vertIndex + 1] = finalVertices[auxi++];
+
+			vertIndex += 2;
+		}
+
+		if (darkTint) {
+			DarkSlotMesh.auxColor[0] = finalVertices[8];
+			DarkSlotMesh.auxColor[1] = finalVertices[9];
+			DarkSlotMesh.auxColor[2] = finalVertices[10];
+			DarkSlotMesh.auxColor[3] = finalVertices[11];
+			this.darkTint = DarkSlotMesh.auxColor;
+
+			DarkSlotMesh.auxColor[0] = finalVertices[2];
+			DarkSlotMesh.auxColor[1] = finalVertices[3];
+			DarkSlotMesh.auxColor[2] = finalVertices[4];
+			DarkSlotMesh.auxColor[3] = finalVertices[5];
+			this.tint = DarkSlotMesh.auxColor;
+		} else {
+			DarkSlotMesh.auxColor[0] = finalVertices[2];
+			DarkSlotMesh.auxColor[1] = finalVertices[3];
+			DarkSlotMesh.auxColor[2] = finalVertices[4];
+			DarkSlotMesh.auxColor[3] = finalVertices[5];
+
+			this.tint = DarkSlotMesh.auxColor;
+		}
+		this.blendMode = SpineTexture.toPixiBlending(slotBlendMode);
+
+		if (this.geometry.indexBuffer.data.length !== finalIndices.length) {
+			this.geometry.indexBuffer.data = new Uint32Array(finalIndices);
+		} else {
+			for (let i = 0; i < finalIndicesLength; i++) {
+				this.geometry.indexBuffer.data[i] = finalIndices[i];
+			}
+		}
+
+		this.name = slotName;
+
+		this.geometry.getBuffer("aVertexPosition").update();
+		this.geometry.getBuffer("aTextureCoord").update();
+		this.geometry.indexBuffer.update();
+	}
+}

+ 89 - 0
spine-ts/spine-pixi/src/SlotMesh.ts

@@ -0,0 +1,89 @@
+import { SpineTexture } from "./SpineTexture";
+import type { BlendMode, NumberArrayLike } from "@esotericsoftware/spine-core";
+import type { ISlotMesh } from "./Spine";
+import { Mesh, MeshGeometry, MeshMaterial } from "@pixi/mesh";
+import { Texture } from "@pixi/core";
+
+export class SlotMesh extends Mesh implements ISlotMesh {
+	public name: string = "";
+
+	private static readonly auxColor = [0, 0, 0, 0];
+	private warnedTwoTint: boolean = false;
+
+	constructor() {
+		const geometry = new MeshGeometry();
+
+		geometry.getBuffer("aVertexPosition").static = false;
+		geometry.getBuffer("aTextureCoord").static = false;
+
+		const meshMaterial = new MeshMaterial(Texture.EMPTY);
+		super(geometry, meshMaterial);
+	}
+	public updateFromSpineData(
+		slotTexture: SpineTexture,
+		slotBlendMode: BlendMode,
+		slotName: string,
+		finalVertices: NumberArrayLike,
+		finalVerticesLength: number,
+		finalIndices: NumberArrayLike,
+		finalIndicesLength: number,
+		darkTint: boolean
+	): void {
+		this.texture = slotTexture.texture;
+
+		const vertLenght = (finalVerticesLength / (darkTint ? 12 : 8)) * 2;
+
+		if (this.geometry.getBuffer("aTextureCoord").data?.length !== vertLenght) {
+			this.geometry.getBuffer("aTextureCoord").data = new Float32Array(vertLenght);
+		}
+
+		if (this.geometry.getBuffer("aVertexPosition").data?.length !== vertLenght) {
+			this.geometry.getBuffer("aVertexPosition").data = new Float32Array(vertLenght);
+		}
+
+		let vertIndex = 0;
+
+		for (let i = 0; i < finalVerticesLength; i += darkTint ? 12 : 8) {
+			let auxi = i;
+
+			this.geometry.getBuffer("aVertexPosition").data[vertIndex] = finalVertices[auxi++];
+			this.geometry.getBuffer("aVertexPosition").data[vertIndex + 1] = finalVertices[auxi++];
+
+			auxi += 4; // color
+
+			this.geometry.getBuffer("aTextureCoord").data[vertIndex] = finalVertices[auxi++];
+			this.geometry.getBuffer("aTextureCoord").data[vertIndex + 1] = finalVertices[auxi++];
+
+			vertIndex += 2;
+		}
+
+		// console.log(vertLenght, auxVert.length);
+
+		if (darkTint && !this.warnedTwoTint) {
+			console.warn("DarkTint is not enabled by default. To enable use a DarkSlotMesh factory while creating the Spine object.");
+			this.warnedTwoTint = true;
+		}
+
+		SlotMesh.auxColor[0] = finalVertices[2];
+		SlotMesh.auxColor[1] = finalVertices[3];
+		SlotMesh.auxColor[2] = finalVertices[4];
+		SlotMesh.auxColor[3] = finalVertices[5];
+
+		this.tint = SlotMesh.auxColor;
+		this.blendMode = SpineTexture.toPixiBlending(slotBlendMode);
+
+		if (this.geometry.indexBuffer.data.length !== finalIndices.length) {
+			this.geometry.indexBuffer.data = new Uint32Array(finalIndices);
+		} else {
+			for (let i = 0; i < finalIndicesLength; i++) {
+				this.geometry.indexBuffer.data[i] = finalIndices[i];
+			}
+		}
+
+		this.name = slotName;
+
+		this.geometry.getBuffer("aVertexPosition").update();
+		this.geometry.getBuffer("aTextureCoord").update();
+		this.geometry.indexBuffer.update();
+	}
+}

+ 407 - 0
spine-ts/spine-pixi/src/Spine.ts

@@ -0,0 +1,407 @@
+import type { BlendMode, Bone, Event, NumberArrayLike, SkeletonData, Slot, TextureAtlas, TrackEntry } from "@esotericsoftware/spine-core";
+import {
+	AnimationState,
+	AnimationStateData,
+	AtlasAttachmentLoader,
+	ClippingAttachment,
+	Color,
+	MeshAttachment,
+	RegionAttachment,
+	Skeleton,
+	SkeletonBinary,
+	SkeletonClipping,
+	SkeletonJson,
+	Utils,
+	Vector2,
+} from "@esotericsoftware/spine-core";
+import type { SpineTexture } from "./SpineTexture";
+import { SlotMesh } from "./SlotMesh";
+import type { ISpineDebugRenderer } from "./SpineDebugRenderer";
+import { Assets } from "@pixi/assets";
+import type { IPointData } from "@pixi/core";
+import { Ticker, utils } from "@pixi/core";
+import type { IDestroyOptions, DisplayObject } from "@pixi/display";
+import { Container } from "@pixi/display";
+
+export interface ISpineOptions {
+	removeUnusedSlots?: boolean;
+	autoUpdate?: boolean;
+	slotMeshFactory?: () => ISlotMesh;
+}
+
+export interface SpineEvents {
+	complete: [trackEntry: TrackEntry];
+	dispose: [trackEntry: TrackEntry];
+	end: [trackEntry: TrackEntry];
+	event: [trackEntry: TrackEntry, event: Event];
+	interrupt: [trackEntry: TrackEntry];
+	start: [trackEntry: TrackEntry];
+}
+
+export class Spine extends Container {
+	public skeleton: Skeleton;
+	public state: AnimationState;
+
+	private _debug?: ISpineDebugRenderer | undefined = undefined;
+	public get debug(): ISpineDebugRenderer | undefined {
+		return this._debug;
+	}
+	public set debug(value: ISpineDebugRenderer | undefined) {
+		if (this._debug) {
+			this._debug.unregisterSpine(this);
+		}
+		if (value) {
+			value.registerSpine(this);
+		}
+		this._debug = value;
+	}
+
+	// Each slot is a pixi mesh, by default we just visible=false the ones we don't need. This forces a removeChild and addChild every time we need to show a slot.
+	public removeUnusedSlots: boolean;
+	protected slotMeshFactory: () => ISlotMesh;
+
+	private autoUpdateWarned: boolean = false;
+	private _autoUpdate: boolean = true;
+	public get autoUpdate(): boolean {
+		return this._autoUpdate;
+	}
+	public set autoUpdate(value: boolean) {
+		if (value) {
+			Ticker.shared.add(this.internalUpdate, this);
+			this.autoUpdateWarned = false;
+		} else {
+			Ticker.shared.remove(this.internalUpdate, this);
+		}
+		this._autoUpdate = value;
+	}
+
+	private meshesCache = new Map<Slot, ISlotMesh>();
+
+	private static vectorAux: Vector2 = new Vector2();
+	private static clipper: SkeletonClipping = new SkeletonClipping();
+
+	private static QUAD_TRIANGLES = [0, 1, 2, 2, 3, 0];
+	private static VERTEX_SIZE = 2 + 2 + 4;
+	private static DARK_VERTEX_SIZE = 2 + 2 + 4 + 4;
+
+	private lightColor = new Color();
+	private darkColor = new Color();
+
+
+	constructor(skeletonData: SkeletonData, options?: ISpineOptions) {
+		super();
+
+		this.skeleton = new Skeleton(skeletonData);
+		const animData = new AnimationStateData(skeletonData);
+		this.state = new AnimationState(animData);
+		this.removeUnusedSlots = options?.removeUnusedSlots ?? false;
+		this.autoUpdate = options?.autoUpdate ?? true;
+		this.slotMeshFactory = options?.slotMeshFactory ?? ((): ISlotMesh => new SlotMesh());
+
+
+		/**
+		  * This is locked behind https://github.com/pixijs/pixijs/issues/8957
+		  * I don't want to make a custom event emitter and do `this.spineEvents.on` because that's just as "far" as `this.state.addListener`
+		  * So, until pixi fixes the custom event system, I'll stick to spine native events. - @miltoncandelero
+
+			this.spineListeners = {
+				complete: (trackEntry) => this.emit("complete", trackEntry),
+				dispose: (trackEntry) => this.emit("dispose", trackEntry),
+				end: (trackEntry) => this.emit("end", trackEntry),
+				event: (trackEntry, event) => this.emit("event", trackEntry, event),
+				interrupt: (trackEntry) => this.emit("interrupt", trackEntry),
+				start: (trackEntry) => this.emit("start", trackEntry),
+			};
+			this.state.addListener(this.spineListeners);
+		*/
+	}
+
+	public update(deltaSeconds: number): void {
+		if (this.autoUpdate && !this.autoUpdateWarned) {
+			console.warn("You are calling update on a Spine instance that has autoUpdate set to true. This is probably not what you want.");
+			this.autoUpdateWarned = true;
+		}
+		this.internalUpdate(0, deltaSeconds);
+	}
+
+	protected internalUpdate(_deltaFrame: number, deltaSeconds?: number): void {
+		// Because reasons, pixi uses deltaFrames at 60fps. We ignore the default deltaFrames and use the deltaSeconds from pixi ticker.
+		this.state.update(deltaSeconds ?? Ticker.shared.deltaMS / 1000);
+	}
+
+	public override updateTransform(): void {
+		this.updateSpineTransform();
+		this.debug?.renderDebug(this);
+		super.updateTransform();
+	}
+
+	protected updateSpineTransform(): void {
+		// if I ever create the linked spines, this will be useful.
+
+		this.state.apply(this.skeleton);
+		this.skeleton.updateWorldTransform();
+		this.updateGeometry();
+		this.sortChildren();
+	}
+
+	public override destroy(options?: boolean | IDestroyOptions | undefined): void {
+		for (const [, mesh] of this.meshesCache) {
+			mesh?.destroy();
+		}
+		this.state.clearListeners();
+		this.debug = undefined;
+		this.meshesCache.clear();
+		super.destroy(options);
+	}
+
+	private recycleMeshes(): void {
+		for (const [, mesh] of this.meshesCache) {
+			if (this.removeUnusedSlots) {
+				mesh.parent?.removeChild(mesh);
+			}
+			mesh.zIndex = -1;
+			mesh.visible = false;
+		}
+	}
+
+	/**
+	 * If you want to manually handle which meshes go on which slot and how you cache, overwrite this method.
+	 */
+	protected getMeshForSlot(slot: Slot): ISlotMesh {
+		if (!this.meshesCache.has(slot)) {
+			let mesh = this.slotMeshFactory();
+			this.addChild(mesh);
+			this.meshesCache.set(slot, mesh);
+			return mesh;
+		} else {
+			let mesh = this.meshesCache.get(slot)!;
+
+			if (this.removeUnusedSlots) {
+				this.addChild(mesh);
+			}
+			mesh.visible = true;
+			return mesh;
+		}
+	}
+
+	private verticesCache: NumberArrayLike = Utils.newFloatArray(1024);
+
+	private updateGeometry(): void {
+		this.recycleMeshes();
+
+		let triangles: Array<number> | null = null;
+		let uvs: NumberArrayLike | null = null;
+		const drawOrder = this.skeleton.drawOrder;
+
+		for (let i = 0, n = drawOrder.length; i < n; i++) {
+			const slot = drawOrder[i];
+			const useDarkColor = slot.darkColor != null;
+			const vertexSize = Spine.clipper.isClipping() ? 2 : useDarkColor ? Spine.DARK_VERTEX_SIZE : Spine.VERTEX_SIZE;
+			if (!slot.bone.active) {
+				Spine.clipper.clipEndWithSlot(slot);
+				continue;
+			}
+			const attachment = slot.getAttachment();
+			let attachmentColor: Color | null;
+			let texture: SpineTexture | null;
+			let numFloats = 0;
+			if (attachment instanceof RegionAttachment) {
+				const region = attachment;
+				attachmentColor = region.color;
+				numFloats = vertexSize * 4;
+				region.computeWorldVertices(slot, this.verticesCache, 0, vertexSize);
+				triangles = Spine.QUAD_TRIANGLES;
+				uvs = region.uvs;
+				texture = <SpineTexture>region.region?.texture;
+			} else if (attachment instanceof MeshAttachment) {
+				const mesh = attachment;
+				attachmentColor = mesh.color;
+				numFloats = (mesh.worldVerticesLength >> 1) * vertexSize;
+				if (numFloats > this.verticesCache.length) {
+					this.verticesCache = Utils.newFloatArray(numFloats);
+				}
+				mesh.computeWorldVertices(slot, 0, mesh.worldVerticesLength, this.verticesCache, 0, vertexSize);
+				triangles = mesh.triangles;
+				uvs = mesh.uvs;
+				texture = <SpineTexture>mesh.region?.texture;
+			} else if (attachment instanceof ClippingAttachment) {
+				Spine.clipper.clipStart(slot, attachment);
+				continue;
+			} else {
+				Spine.clipper.clipEndWithSlot(slot);
+				continue;
+			}
+			if (texture != null) {
+				const skeleton = slot.bone.skeleton;
+				const skeletonColor = skeleton.color;
+				const slotColor = slot.color;
+				const alpha = skeletonColor.a * slotColor.a * attachmentColor.a;
+				this.lightColor.set(
+					skeletonColor.r * slotColor.r * attachmentColor.r,
+					skeletonColor.g * slotColor.g * attachmentColor.g,
+					skeletonColor.b * slotColor.b * attachmentColor.b,
+					alpha
+				);
+				if (slot.darkColor != null) {
+					this.darkColor.setFromColor(slot.darkColor);
+				} else {
+					this.darkColor.set(0, 0, 0, 0);
+				}
+
+				let finalVertices: NumberArrayLike;
+				let finalVerticesLength: number;
+				let finalIndices: NumberArrayLike;
+				let finalIndicesLength: number;
+
+				if (Spine.clipper.isClipping()) {
+					Spine.clipper.clipTriangles(this.verticesCache, numFloats, triangles, triangles.length, uvs, this.lightColor, this.darkColor, useDarkColor);
+
+					finalVertices = Spine.clipper.clippedVertices;
+					finalVerticesLength = finalVertices.length;
+
+					finalIndices = Spine.clipper.clippedTriangles;
+					finalIndicesLength = finalIndices.length;
+				} else {
+					const verts = this.verticesCache;
+					for (let v = 2, u = 0, n = numFloats; v < n; v += vertexSize, u += 2) {
+						let tempV = v;
+						verts[tempV++] = this.lightColor.r;
+						verts[tempV++] = this.lightColor.g;
+						verts[tempV++] = this.lightColor.b;
+						verts[tempV++] = this.lightColor.a;
+
+						verts[tempV++] = uvs[u];
+						verts[tempV++] = uvs[u + 1];
+
+						if (useDarkColor) {
+							verts[tempV++] = this.darkColor.r;
+							verts[tempV++] = this.darkColor.g;
+							verts[tempV++] = this.darkColor.b;
+						}
+					}
+					finalVertices = this.verticesCache;
+					finalVerticesLength = numFloats;
+					finalIndices = triangles;
+					finalIndicesLength = triangles.length;
+				}
+
+				if (finalVerticesLength == 0 || finalIndicesLength == 0) {
+					Spine.clipper.clipEndWithSlot(slot);
+					continue;
+				}
+
+				const mesh = this.getMeshForSlot(slot);
+				mesh.zIndex = i;
+				mesh.updateFromSpineData(texture, slot.data.blendMode, slot.data.name, finalVertices, finalVerticesLength, finalIndices, finalIndicesLength, useDarkColor);
+			}
+
+			Spine.clipper.clipEndWithSlot(slot);
+		}
+		Spine.clipper.clipEnd();
+	}
+
+	public setBonePosition(bone: string | Bone, position: IPointData): void {
+		const boneAux = bone;
+		if (typeof bone === "string") {
+			bone = this.skeleton.findBone(bone)!;
+			this.skeleton.findBone;
+			this.skeleton.findIkConstraint;
+			this.skeleton.findPathConstraint;
+			this.skeleton.findSlot;
+			this.skeleton.findTransformConstraint;
+		}
+
+		if (!bone) {
+			console.error(`Cant set bone position! Bone ${String(boneAux)} not found`);
+			return;
+		}
+
+		Spine.vectorAux.set(position.x, position.y);
+
+		if (bone.parent)
+		{
+			const aux = bone.parent.worldToLocal(Spine.vectorAux);
+			bone.x = aux.x;
+			bone.y = aux.y;
+		}
+		else
+		{
+			bone.x = Spine.vectorAux.x;
+			bone.y = Spine.vectorAux.y;
+		}
+	}
+
+	public getBonePosition(bone: string | Bone, outPos?: IPointData): IPointData | undefined {
+		const boneAux = bone;
+		if (typeof bone === "string") {
+			bone = this.skeleton.findBone(bone)!;
+			this.skeleton.findBone;
+			this.skeleton.findIkConstraint;
+			this.skeleton.findPathConstraint;
+			this.skeleton.findSlot;
+			this.skeleton.findTransformConstraint;
+		}
+
+		if (!bone) {
+			console.error(`Cant set bone position! Bone ${String(boneAux)} not found`);
+			return outPos;
+		}
+
+		if (!outPos) {
+			outPos = { x: 0, y: 0 };
+		}
+
+		outPos.x = bone.worldX;
+		outPos.y = bone.worldY;
+		return outPos;
+	}
+
+	public static readonly skeletonCache: Record<string, SkeletonData> = Object.create(null);
+
+	public static from(skeletonAssetName: string, atlasAssetName: string, options?: ISpineOptions & { scale?: number }): Spine {
+		const cacheKey = `${skeletonAssetName}-${atlasAssetName}-${options?.scale ?? 1}`;
+
+		let skeletonData = Spine.skeletonCache[cacheKey];
+		if (skeletonData) {
+			return new Spine(skeletonData, options);
+		}
+
+		const skeletonAsset = Assets.get<any | Uint8Array>(skeletonAssetName);
+
+		const atlasAsset = Assets.get<TextureAtlas>(atlasAssetName);
+
+		// If you want a custom attachment laoder, you don't use .from(...)
+		const attachmentLoader = new AtlasAttachmentLoader(atlasAsset);
+
+		// What parser do we need?
+		let parser: SkeletonBinary | SkeletonJson;
+		if (skeletonAsset instanceof Uint8Array) {
+			parser = new SkeletonBinary(attachmentLoader);
+		} else {
+			parser = new SkeletonJson(attachmentLoader);
+		}
+		parser.scale = options?.scale ?? 1;
+
+		skeletonData = parser.readSkeletonData(skeletonAsset);
+
+		Spine.skeletonCache[cacheKey] = skeletonData;
+
+		return new this(skeletonData, options);
+	}
+}
+
+Skeleton.yDown = true;
+
+export interface ISlotMesh extends DisplayObject {
+	name: string;
+	updateFromSpineData(
+		slotTexture: SpineTexture,
+		slotBlendMode: BlendMode,
+		slotName: string,
+		finalVertices: NumberArrayLike,
+		finalVerticesLength: number,
+		finalIndices: NumberArrayLike,
+		finalIndicesLength: number,
+		darkTint: boolean
+	): void;
+}

+ 543 - 0
spine-ts/spine-pixi/src/SpineDebugRenderer.ts

@@ -0,0 +1,543 @@
+import { Container } from "@pixi/display";
+import { Graphics } from "@pixi/graphics";
+import { Text } from "@pixi/text";
+import type { Spine } from "./Spine";
+import type { AnimationStateListener } from "@esotericsoftware/spine-core";
+import { ClippingAttachment, MeshAttachment, PathAttachment, RegionAttachment, SkeletonBounds } from "@esotericsoftware/spine-core";
+
+/**
+ * Make a class that extends from this interface to create your own debug renderer.
+ * @public
+ */
+export interface ISpineDebugRenderer {
+	/**
+	 * This will be called every frame, after the spine has been updated.
+	 */
+	renderDebug(spine: Spine): void;
+
+	/**
+	 *  This is called when the `spine.debug` object is set to null or when the spine is destroyed.
+	 */
+	unregisterSpine(spine: Spine): void;
+
+	/**
+	 * This is called when the `spine.debug` object is set to a new instance of a debug renderer.
+	 */
+	registerSpine(spine: Spine): void;
+}
+
+type DebugDisplayObjects = {
+	bones: Container;
+	skeletonXY: Graphics;
+	regionAttachmentsShape: Graphics;
+	meshTrianglesLine: Graphics;
+	meshHullLine: Graphics;
+	clippingPolygon: Graphics;
+	boundingBoxesRect: Graphics;
+	boundingBoxesCircle: Graphics;
+	boundingBoxesPolygon: Graphics;
+	pathsCurve: Graphics;
+	pathsLine: Graphics;
+	parentDebugContainer: Container;
+	eventText: Container;
+	eventCallback: AnimationStateListener;
+};
+
+/**
+ * This is a debug renderer that uses PixiJS Graphics under the hood.
+ * @public
+ */
+export class SpineDebugRenderer implements ISpineDebugRenderer {
+	private registeredSpines: Map<Spine, DebugDisplayObjects> = new Map();
+
+	public drawMeshHull = true;
+	public drawMeshTriangles = true;
+	public drawBones = true;
+	public drawPaths = true;
+	public drawBoundingBoxes = true;
+	public drawClipping = true;
+	public drawRegionAttachments = true;
+	public drawEvents = true;
+
+	public lineWidth = 1;
+	public regionAttachmentsColor = 0x0078ff;
+	public meshHullColor = 0x0078ff;
+	public meshTrianglesColor = 0xffcc00;
+	public clippingPolygonColor = 0xff00ff;
+	public boundingBoxesRectColor = 0x00ff00;
+	public boundingBoxesPolygonColor = 0x00ff00;
+	public boundingBoxesCircleColor = 0x00ff00;
+	public pathsCurveColor = 0xff0000;
+	public pathsLineColor = 0xff00ff;
+	public skeletonXYColor = 0xff0000;
+	public bonesColor = 0x00eecc;
+	public eventFontSize: number = 24;
+	public eventFontColor: number = 0x0;
+
+	/**
+	 * The debug is attached by force to each spine object. So we need to create it inside the spine when we get the first update
+	 */
+	public registerSpine(spine: Spine): void {
+		if (this.registeredSpines.has(spine)) {
+			console.warn("SpineDebugRenderer.registerSpine() - this spine is already registered!", spine);
+			return;
+		}
+		const debugDisplayObjects: DebugDisplayObjects = {
+			parentDebugContainer: new Container(),
+			bones: new Container(),
+			skeletonXY: new Graphics(),
+			regionAttachmentsShape: new Graphics(),
+			meshTrianglesLine: new Graphics(),
+			meshHullLine: new Graphics(),
+			clippingPolygon: new Graphics(),
+			boundingBoxesRect: new Graphics(),
+			boundingBoxesCircle: new Graphics(),
+			boundingBoxesPolygon: new Graphics(),
+			pathsCurve: new Graphics(),
+			pathsLine: new Graphics(),
+			eventText: new Container(),
+			eventCallback: {
+				event: (_, event) => {
+					if (this.drawEvents) {
+						const scale = Math.abs(spine.scale.x || spine.scale.y || 1);
+						const text = new Text(event.data.name, { fontSize: this.eventFontSize / scale, fill: this.eventFontColor, fontFamily: "monospace" });
+						text.scale.x = Math.sign(spine.scale.x);
+						text.anchor.set(0.5);
+						debugDisplayObjects.eventText.addChild(text);
+						setTimeout(() => {
+							if (!text.destroyed) {
+								text.destroy();
+							}
+						}, 250);
+					}
+				},
+			},
+		};
+
+		debugDisplayObjects.parentDebugContainer.addChild(debugDisplayObjects.bones);
+		debugDisplayObjects.parentDebugContainer.addChild(debugDisplayObjects.skeletonXY);
+		debugDisplayObjects.parentDebugContainer.addChild(debugDisplayObjects.regionAttachmentsShape);
+		debugDisplayObjects.parentDebugContainer.addChild(debugDisplayObjects.meshTrianglesLine);
+		debugDisplayObjects.parentDebugContainer.addChild(debugDisplayObjects.meshHullLine);
+		debugDisplayObjects.parentDebugContainer.addChild(debugDisplayObjects.clippingPolygon);
+		debugDisplayObjects.parentDebugContainer.addChild(debugDisplayObjects.boundingBoxesRect);
+		debugDisplayObjects.parentDebugContainer.addChild(debugDisplayObjects.boundingBoxesCircle);
+		debugDisplayObjects.parentDebugContainer.addChild(debugDisplayObjects.boundingBoxesPolygon);
+		debugDisplayObjects.parentDebugContainer.addChild(debugDisplayObjects.pathsCurve);
+		debugDisplayObjects.parentDebugContainer.addChild(debugDisplayObjects.pathsLine);
+		debugDisplayObjects.parentDebugContainer.addChild(debugDisplayObjects.eventText);
+
+		debugDisplayObjects.parentDebugContainer.zIndex = 9999999;
+		
+		// Disable screen reader and mouse input on debug objects.
+		(debugDisplayObjects.parentDebugContainer as any).accessibleChildren = false;
+		(debugDisplayObjects.parentDebugContainer as any).eventMode = "none";
+		(debugDisplayObjects.parentDebugContainer as any ).interactiveChildren = false;
+
+		spine.addChild(debugDisplayObjects.parentDebugContainer);
+
+		spine.state.addListener(debugDisplayObjects.eventCallback);
+
+		this.registeredSpines.set(spine, debugDisplayObjects);
+	}
+	public renderDebug(spine: Spine): void {
+		if (!this.registeredSpines.has(spine)) {
+			// This should never happen. Spines are registered when you assign spine.debug
+			this.registerSpine(spine);
+		}
+
+		const debugDisplayObjects = this.registeredSpines.get(spine);
+
+		if (!debugDisplayObjects) {
+			return;
+		}
+		spine.addChild(debugDisplayObjects.parentDebugContainer);
+
+		debugDisplayObjects.skeletonXY.clear();
+		debugDisplayObjects.regionAttachmentsShape.clear();
+		debugDisplayObjects.meshTrianglesLine.clear();
+		debugDisplayObjects.meshHullLine.clear();
+		debugDisplayObjects.clippingPolygon.clear();
+		debugDisplayObjects.boundingBoxesRect.clear();
+		debugDisplayObjects.boundingBoxesCircle.clear();
+		debugDisplayObjects.boundingBoxesPolygon.clear();
+		debugDisplayObjects.pathsCurve.clear();
+		debugDisplayObjects.pathsLine.clear();
+
+		for (let len = debugDisplayObjects.bones.children.length; len > 0; len--) {
+			debugDisplayObjects.bones.children[len - 1].destroy({ children: true, texture: true, baseTexture: true });
+		}
+
+		const scale = Math.abs(spine.scale.x || spine.scale.y || 1);
+		const lineWidth = this.lineWidth / scale;
+
+		if (this.drawBones) {
+			this.drawBonesFunc(spine, debugDisplayObjects, lineWidth, scale);
+		}
+
+		if (this.drawPaths) {
+			this.drawPathsFunc(spine, debugDisplayObjects, lineWidth);
+		}
+
+		if (this.drawBoundingBoxes) {
+			this.drawBoundingBoxesFunc(spine, debugDisplayObjects, lineWidth);
+		}
+
+		if (this.drawClipping) {
+			this.drawClippingFunc(spine, debugDisplayObjects, lineWidth);
+		}
+
+		if (this.drawMeshHull || this.drawMeshTriangles) {
+			this.drawMeshHullAndMeshTriangles(spine, debugDisplayObjects, lineWidth);
+		}
+
+		if (this.drawRegionAttachments) {
+			this.drawRegionAttachmentsFunc(spine, debugDisplayObjects, lineWidth);
+		}
+
+		if (this.drawEvents) {
+			for (const child of debugDisplayObjects.eventText.children) {
+				child.alpha -= 0.05;
+				child.y -= 2;
+			}
+		}
+	}
+
+	private drawBonesFunc(spine: Spine, debugDisplayObjects: DebugDisplayObjects, lineWidth: number, scale: number): void {
+		const skeleton = spine.skeleton;
+		const skeletonX = skeleton.x;
+		const skeletonY = skeleton.y;
+		const bones = skeleton.bones;
+
+		debugDisplayObjects.skeletonXY.lineStyle(lineWidth, this.skeletonXYColor, 1);
+
+		for (let i = 0, len = bones.length; i < len; i++) {
+			const bone = bones[i];
+			const boneLen = bone.data.length;
+			const starX = skeletonX + bone.worldX;
+			const starY = skeletonY + bone.worldY;
+			const endX = skeletonX + boneLen * bone.a + bone.worldX;
+			const endY = skeletonY + boneLen * bone.b + bone.worldY;
+
+			if (bone.data.name === "root" || bone.data.parent === null) {
+				continue;
+			}
+
+			const w = Math.abs(starX - endX);
+			const h = Math.abs(starY - endY);
+			// a = w, // side length a
+			const a2 = Math.pow(w, 2); // square root of side length a
+			const b = h; // side length b
+			const b2 = Math.pow(h, 2); // square root of side length b
+			const c = Math.sqrt(a2 + b2); // side length c
+			const c2 = Math.pow(c, 2); // square root of side length c
+			const rad = Math.PI / 180;
+			// A = Math.acos([a2 + c2 - b2] / [2 * a * c]) || 0, // Angle A
+			// C = Math.acos([a2 + b2 - c2] / [2 * a * b]) || 0, // C angle
+			const B = Math.acos((c2 + b2 - a2) / (2 * b * c)) || 0; // angle of corner B
+
+			if (c === 0) {
+				continue;
+			}
+
+			const gp = new Graphics();
+
+			debugDisplayObjects.bones.addChild(gp);
+
+			// draw bone
+			const refRation = c / 50 / scale;
+
+			gp.beginFill(this.bonesColor, 1);
+			gp.drawPolygon(0, 0, 0 - refRation, c - refRation * 3, 0, c - refRation, 0 + refRation, c - refRation * 3);
+			gp.endFill();
+			gp.x = starX;
+			gp.y = starY;
+			gp.pivot.y = c;
+
+			// Calculate bone rotation angle
+			let rotation = 0;
+
+			if (starX < endX && starY < endY) {
+				// bottom right
+				rotation = -B + 180 * rad;
+			} else if (starX > endX && starY < endY) {
+				// bottom left
+				rotation = 180 * rad + B;
+			} else if (starX > endX && starY > endY) {
+				// top left
+				rotation = -B;
+			} else if (starX < endX && starY > endY) {
+				// bottom left
+				rotation = B;
+			} else if (starY === endY && starX < endX) {
+				// To the right
+				rotation = 90 * rad;
+			} else if (starY === endY && starX > endX) {
+				// go left
+				rotation = -90 * rad;
+			} else if (starX === endX && starY < endY) {
+				// down
+				rotation = 180 * rad;
+			} else if (starX === endX && starY > endY) {
+				// up
+				rotation = 0;
+			}
+			gp.rotation = rotation;
+
+			// Draw the starting rotation point of the bone
+			gp.lineStyle(lineWidth + refRation / 2.4, this.bonesColor, 1);
+			gp.beginFill(0x000000, 0.6);
+			gp.drawCircle(0, c, refRation * 1.2);
+			gp.endFill();
+		}
+
+		// Draw the skeleton starting point "X" form
+		const startDotSize = lineWidth * 3;
+
+		debugDisplayObjects.skeletonXY.moveTo(skeletonX - startDotSize, skeletonY - startDotSize);
+		debugDisplayObjects.skeletonXY.lineTo(skeletonX + startDotSize, skeletonY + startDotSize);
+		debugDisplayObjects.skeletonXY.moveTo(skeletonX + startDotSize, skeletonY - startDotSize);
+		debugDisplayObjects.skeletonXY.lineTo(skeletonX - startDotSize, skeletonY + startDotSize);
+	}
+
+	private drawRegionAttachmentsFunc(spine: Spine, debugDisplayObjects: DebugDisplayObjects, lineWidth: number): void {
+		const skeleton = spine.skeleton;
+		const slots = skeleton.slots;
+
+		debugDisplayObjects.regionAttachmentsShape.lineStyle(lineWidth, this.regionAttachmentsColor, 1);
+
+		for (let i = 0, len = slots.length; i < len; i++) {
+			const slot = slots[i];
+			const attachment = slot.getAttachment();
+
+			if (attachment == null || !(attachment instanceof RegionAttachment)) {
+				continue;
+			}
+
+			const regionAttachment = attachment;
+
+			const vertices = new Float32Array(8);
+
+			regionAttachment.computeWorldVertices(slot, vertices, 0, 2);
+			debugDisplayObjects.regionAttachmentsShape.drawPolygon(Array.from(vertices.slice(0, 8)));
+		}
+	}
+
+	private drawMeshHullAndMeshTriangles(spine: Spine, debugDisplayObjects: DebugDisplayObjects, lineWidth: number): void {
+		const skeleton = spine.skeleton;
+		const slots = skeleton.slots;
+
+		debugDisplayObjects.meshHullLine.lineStyle(lineWidth, this.meshHullColor, 1);
+		debugDisplayObjects.meshTrianglesLine.lineStyle(lineWidth, this.meshTrianglesColor, 1);
+
+		for (let i = 0, len = slots.length; i < len; i++) {
+			const slot = slots[i];
+
+			if (!slot.bone.active) {
+				continue;
+			}
+			const attachment = slot.getAttachment();
+
+			if (attachment == null || !(attachment instanceof MeshAttachment)) {
+				continue;
+			}
+
+			const meshAttachment = attachment;
+
+			const vertices = new Float32Array(meshAttachment.worldVerticesLength);
+			const triangles = meshAttachment.triangles;
+			let hullLength = meshAttachment.hullLength;
+
+			meshAttachment.computeWorldVertices(slot, 0, meshAttachment.worldVerticesLength, vertices, 0, 2);
+			// draw the skinned mesh (triangle)
+			if (this.drawMeshTriangles) {
+				for (let i = 0, len = triangles.length; i < len; i += 3) {
+					const v1 = triangles[i] * 2;
+					const v2 = triangles[i + 1] * 2;
+					const v3 = triangles[i + 2] * 2;
+
+					debugDisplayObjects.meshTrianglesLine.moveTo(vertices[v1], vertices[v1 + 1]);
+					debugDisplayObjects.meshTrianglesLine.lineTo(vertices[v2], vertices[v2 + 1]);
+					debugDisplayObjects.meshTrianglesLine.lineTo(vertices[v3], vertices[v3 + 1]);
+				}
+			}
+
+			// draw skin border
+			if (this.drawMeshHull && hullLength > 0) {
+				hullLength = (hullLength >> 1) * 2;
+				let lastX = vertices[hullLength - 2];
+				let lastY = vertices[hullLength - 1];
+
+				for (let i = 0, len = hullLength; i < len; i += 2) {
+					const x = vertices[i];
+					const y = vertices[i + 1];
+
+					debugDisplayObjects.meshHullLine.moveTo(x, y);
+					debugDisplayObjects.meshHullLine.lineTo(lastX, lastY);
+					lastX = x;
+					lastY = y;
+				}
+			}
+		}
+	}
+
+	private drawClippingFunc(spine: Spine, debugDisplayObjects: DebugDisplayObjects, lineWidth: number): void {
+		const skeleton = spine.skeleton;
+		const slots = skeleton.slots;
+
+		debugDisplayObjects.clippingPolygon.lineStyle(lineWidth, this.clippingPolygonColor, 1);
+		for (let i = 0, len = slots.length; i < len; i++) {
+			const slot = slots[i];
+
+			if (!slot.bone.active) {
+				continue;
+			}
+			const attachment = slot.getAttachment();
+
+			if (attachment == null || !(attachment instanceof ClippingAttachment)) {
+				continue;
+			}
+
+			const clippingAttachment = attachment;
+
+			const nn = clippingAttachment.worldVerticesLength;
+			const world = new Float32Array(nn);
+
+			clippingAttachment.computeWorldVertices(slot, 0, nn, world, 0, 2);
+			debugDisplayObjects.clippingPolygon.drawPolygon(Array.from(world));
+		}
+	}
+
+	private drawBoundingBoxesFunc(spine: Spine, debugDisplayObjects: DebugDisplayObjects, lineWidth: number): void {
+		// draw the total outline of the bounding box
+		debugDisplayObjects.boundingBoxesRect.lineStyle(lineWidth, this.boundingBoxesRectColor, 5);
+
+		const bounds = new SkeletonBounds();
+
+		bounds.update(spine.skeleton, true);
+		debugDisplayObjects.boundingBoxesRect.drawRect(bounds.minX, bounds.minY, bounds.getWidth(), bounds.getHeight());
+
+		const polygons = bounds.polygons;
+		const drawPolygon = (polygonVertices: ArrayLike<number>, _offset: unknown, count: number): void => {
+			debugDisplayObjects.boundingBoxesPolygon.lineStyle(lineWidth, this.boundingBoxesPolygonColor, 1);
+			debugDisplayObjects.boundingBoxesPolygon.beginFill(this.boundingBoxesPolygonColor, 0.1);
+
+			if (count < 3) {
+				throw new Error("Polygon must contain at least 3 vertices");
+			}
+			const paths = [];
+			const dotSize = lineWidth * 2;
+
+			for (let i = 0, len = polygonVertices.length; i < len; i += 2) {
+				const x1 = polygonVertices[i];
+				const y1 = polygonVertices[i + 1];
+
+				// draw the bounding box node
+				debugDisplayObjects.boundingBoxesCircle.lineStyle(0);
+				debugDisplayObjects.boundingBoxesCircle.beginFill(this.boundingBoxesCircleColor);
+				debugDisplayObjects.boundingBoxesCircle.drawCircle(x1, y1, dotSize);
+				debugDisplayObjects.boundingBoxesCircle.endFill();
+
+				paths.push(x1, y1);
+			}
+
+			// draw the bounding box area
+			debugDisplayObjects.boundingBoxesPolygon.drawPolygon(paths);
+			debugDisplayObjects.boundingBoxesPolygon.endFill();
+		};
+
+		for (let i = 0, len = polygons.length; i < len; i++) {
+			const polygon = polygons[i];
+
+			drawPolygon(polygon, 0, polygon.length);
+		}
+	}
+
+	private drawPathsFunc(spine: Spine, debugDisplayObjects: DebugDisplayObjects, lineWidth: number): void {
+		const skeleton = spine.skeleton;
+		const slots = skeleton.slots;
+
+		debugDisplayObjects.pathsCurve.lineStyle(lineWidth, this.pathsCurveColor, 1);
+		debugDisplayObjects.pathsLine.lineStyle(lineWidth, this.pathsLineColor, 1);
+
+		for (let i = 0, len = slots.length; i < len; i++) {
+			const slot = slots[i];
+
+			if (!slot.bone.active) {
+				continue;
+			}
+			const attachment = slot.getAttachment();
+
+			if (attachment == null || !(attachment instanceof PathAttachment)) {
+				continue;
+			}
+
+			const pathAttachment = attachment;
+			let nn = pathAttachment.worldVerticesLength;
+			const world = new Float32Array(nn);
+
+			pathAttachment.computeWorldVertices(slot, 0, nn, world, 0, 2);
+			let x1 = world[2];
+			let y1 = world[3];
+			let x2 = 0;
+			let y2 = 0;
+
+			if (pathAttachment.closed) {
+				const cx1 = world[0];
+				const cy1 = world[1];
+				const cx2 = world[nn - 2];
+				const cy2 = world[nn - 1];
+
+				x2 = world[nn - 4];
+				y2 = world[nn - 3];
+
+				// curve
+				debugDisplayObjects.pathsCurve.moveTo(x1, y1);
+				debugDisplayObjects.pathsCurve.bezierCurveTo(cx1, cy1, cx2, cy2, x2, y2);
+
+				// handle
+				debugDisplayObjects.pathsLine.moveTo(x1, y1);
+				debugDisplayObjects.pathsLine.lineTo(cx1, cy1);
+				debugDisplayObjects.pathsLine.moveTo(x2, y2);
+				debugDisplayObjects.pathsLine.lineTo(cx2, cy2);
+			}
+			nn -= 4;
+			for (let ii = 4; ii < nn; ii += 6) {
+				const cx1 = world[ii];
+				const cy1 = world[ii + 1];
+				const cx2 = world[ii + 2];
+				const cy2 = world[ii + 3];
+
+				x2 = world[ii + 4];
+				y2 = world[ii + 5];
+				// curve
+				debugDisplayObjects.pathsCurve.moveTo(x1, y1);
+				debugDisplayObjects.pathsCurve.bezierCurveTo(cx1, cy1, cx2, cy2, x2, y2);
+
+				// handle
+				debugDisplayObjects.pathsLine.moveTo(x1, y1);
+				debugDisplayObjects.pathsLine.lineTo(cx1, cy1);
+				debugDisplayObjects.pathsLine.moveTo(x2, y2);
+				debugDisplayObjects.pathsLine.lineTo(cx2, cy2);
+				x1 = x2;
+				y1 = y2;
+			}
+		}
+	}
+
+	public unregisterSpine(spine: Spine): void {
+		if (!this.registeredSpines.has(spine)) {
+			console.warn("SpineDebugRenderer.unregisterSpine() - spine is not registered, can't unregister!", spine);
+		}
+		const debugDisplayObjects = this.registeredSpines.get(spine);
+
+		if (!debugDisplayObjects) {
+			return;
+		}
+
+		spine.state.removeListener(debugDisplayObjects.eventCallback);
+
+		debugDisplayObjects.parentDebugContainer.destroy({ baseTexture: true, children: true, texture: true });
+		this.registeredSpines.delete(spine);
+	}
+}

+ 109 - 0
spine-ts/spine-pixi/src/SpineTexture.ts

@@ -0,0 +1,109 @@
+import { BlendMode, Texture, TextureFilter, TextureWrap } from "@esotericsoftware/spine-core";
+import type { BaseTexture as PixiBaseTexture, BaseImageResource } from "@pixi/core";
+import { Texture as PixiTexture, SCALE_MODES, MIPMAP_MODES, WRAP_MODES, BLEND_MODES } from "@pixi/core";
+
+export class SpineTexture extends Texture {
+	private static textureMap: Map<PixiBaseTexture, SpineTexture> = new Map<PixiBaseTexture, SpineTexture>();
+
+	public static from(texture: PixiBaseTexture): SpineTexture {
+		if (SpineTexture.textureMap.has(texture)) {
+			return SpineTexture.textureMap.get(texture)!;
+		}
+		return new SpineTexture(texture);
+	}
+
+	public readonly texture: PixiTexture;
+
+	private constructor(image: PixiBaseTexture) {
+		// Todo: maybe add error handling if you feed a video texture to spine?
+		super((image.resource as BaseImageResource).source as any);
+		this.texture = PixiTexture.from(image);
+	}
+
+	public setFilters(minFilter: TextureFilter, _magFilter: TextureFilter): void {
+		this.texture.baseTexture.scaleMode = SpineTexture.toPixiTextureFilter(minFilter);
+		this.texture.baseTexture.mipmap = SpineTexture.toPixiMipMap(minFilter);
+
+		// pixi only has one filter for both min and mag, too bad
+	}
+
+	public setWraps(uWrap: TextureWrap, _vWrap: TextureWrap): void {
+		this.texture.baseTexture.wrapMode = SpineTexture.toPixiTextureWrap(uWrap);
+
+		// Pixi only has one setting
+	}
+
+	public dispose(): void {
+		// I am not entirely sure about this...
+		this.texture.destroy();
+	}
+
+	private static toPixiTextureFilter(filter: TextureFilter): SCALE_MODES {
+		switch (filter) {
+			case TextureFilter.Nearest:
+			case TextureFilter.MipMapNearestLinear:
+			case TextureFilter.MipMapNearestNearest:
+				return SCALE_MODES.NEAREST;
+
+			case TextureFilter.Linear:
+			case TextureFilter.MipMapLinearLinear: // TextureFilter.MipMapLinearLinear == TextureFilter.MipMap
+			case TextureFilter.MipMapLinearNearest:
+				return SCALE_MODES.LINEAR;
+
+			default:
+				throw new Error(`Unknown texture filter: ${String(filter)}`);
+		}
+	}
+
+	private static toPixiMipMap(filter: TextureFilter): MIPMAP_MODES {
+		switch (filter) {
+			case TextureFilter.Nearest:
+			case TextureFilter.Linear:
+				return MIPMAP_MODES.OFF;
+
+			case TextureFilter.MipMapNearestLinear:
+			case TextureFilter.MipMapNearestNearest:
+			case TextureFilter.MipMapLinearLinear: // TextureFilter.MipMapLinearLinear == TextureFilter.MipMap
+			case TextureFilter.MipMapLinearNearest:
+				return MIPMAP_MODES.ON;
+
+			default:
+				throw new Error(`Unknown texture filter: ${String(filter)}`);
+		}
+	}
+
+	private static toPixiTextureWrap(wrap: TextureWrap): WRAP_MODES {
+		switch (wrap) {
+			case TextureWrap.ClampToEdge:
+				return WRAP_MODES.CLAMP;
+
+			case TextureWrap.MirroredRepeat:
+				return WRAP_MODES.MIRRORED_REPEAT;
+
+			case TextureWrap.Repeat:
+				return WRAP_MODES.REPEAT;
+
+			default:
+				throw new Error(`Unknown texture wrap: ${String(wrap)}`);
+		}
+	}
+
+	public static toPixiBlending(blend: BlendMode): BLEND_MODES {
+		switch (blend) {
+			case BlendMode.Normal:
+				return BLEND_MODES.NORMAL;
+
+			case BlendMode.Additive:
+				return BLEND_MODES.ADD;
+
+			case BlendMode.Multiply:
+				return BLEND_MODES.MULTIPLY;
+
+			case BlendMode.Screen:
+				return BLEND_MODES.SCREEN;
+
+			default:
+				throw new Error(`Unknown blendMode: ${String(blend)}`);
+		}
+	}
+}

+ 93 - 0
spine-ts/spine-pixi/src/assets/atlasLoader.ts

@@ -0,0 +1,93 @@
+import { TextureAtlas } from "@esotericsoftware/spine-core";
+import { SpineTexture } from "../SpineTexture";
+import type { AssetExtension, LoadAsset, Loader } from "@pixi/assets";
+import { LoaderParserPriority, checkExtension } from "@pixi/assets";
+import type { Texture } from "@pixi/core";
+import { ExtensionType, settings, utils, BaseTexture, extensions } from "@pixi/core";
+
+type RawAtlas = string;
+
+const spineTextureAtlasLoader: AssetExtension<RawAtlas | TextureAtlas, ISpineAtlasMetadata> = {
+	extension: ExtensionType.Asset,
+
+	loader: {
+		extension: {
+			type: ExtensionType.LoadParser,
+			priority: LoaderParserPriority.Normal,
+			name: "spineTextureAtlasLoader",
+		},
+
+		test(url: string): boolean {
+			return checkExtension(url, ".atlas");
+		},
+
+		async load(url: string): Promise<RawAtlas> {
+			const response = await settings.ADAPTER.fetch(url);
+
+			const txt = await response.text();
+
+			return txt;
+		},
+
+		testParse(asset: unknown, options: LoadAsset): Promise<boolean> {
+			const isExtensionRight = checkExtension(options.src, ".atlas");
+			const isString = typeof asset === "string";
+
+			return Promise.resolve(isExtensionRight && isString);
+		},
+
+		unload(atlas: TextureAtlas) {
+			atlas.dispose();
+		},
+
+		async parse(asset: RawAtlas, options: LoadAsset, loader: Loader): Promise<TextureAtlas> {
+			const metadata: ISpineAtlasMetadata = options.data || {};
+			let basePath = utils.path.dirname(options.src);
+
+			if (basePath && basePath.lastIndexOf("/") !== basePath.length - 1) {
+				basePath += "/";
+			}
+
+			// Retval is going to be a texture atlas. However we need to wait for it's callback to resolve this promise.
+			const retval = new TextureAtlas(asset);
+
+			// If the user gave me only one texture, that one is assumed to be the "first" texture in the atlas
+			if (metadata.images instanceof BaseTexture || typeof metadata.images === "string") {
+				const pixiTexture = metadata.images;
+				metadata.images = {} as Record<string, BaseTexture | string>;
+				metadata.images[retval.pages[0].name] = pixiTexture;
+			}
+
+			// we will wait for all promises for the textures at the same time at the end.
+			const textureLoadingPromises = [];
+
+			// fill the pages
+			for (const page of retval.pages) {
+				const pageName = page.name;
+				const providedPage = metadata?.images ? metadata.images[pageName] : undefined;
+				if (providedPage instanceof BaseTexture) {
+					page.setTexture(SpineTexture.from(providedPage));
+				} else {
+					const url: string = providedPage ?? utils.path.normalize([...basePath.split(utils.path.sep), pageName].join(utils.path.sep));
+					const pixiPromise = loader.load<Texture>({ src: url, data: metadata.imageMetadata }).then((texture) => {
+						page.setTexture(SpineTexture.from(texture.baseTexture));
+					});
+					textureLoadingPromises.push(pixiPromise);
+				}
+			}
+
+			await Promise.all(textureLoadingPromises);
+
+			return retval;
+		},
+	},
+} as AssetExtension<RawAtlas | TextureAtlas, ISpineAtlasMetadata>;
+
+extensions.add(spineTextureAtlasLoader);
+
+export interface ISpineAtlasMetadata {
+	// If you are downloading an .atlas file, this metadata will go to the Texture loader
+	imageMetadata?: any;
+	// If you already have atlas pages loaded as pixi textures and want to use that to create the atlas, you can pass them here
+	images?: BaseTexture | string | Record<string, BaseTexture | string>;
+}

+ 45 - 0
spine-ts/spine-pixi/src/assets/skeletonLoader.ts

@@ -0,0 +1,45 @@
+import type { AssetExtension, LoadAsset } from "@pixi/assets";
+import { LoaderParserPriority, checkExtension } from "@pixi/assets";
+import { ExtensionType, settings, extensions } from "@pixi/core";
+
+type SkeletonJsonAsset = any;
+type SkeletonBinaryAsset = Uint8Array;
+
+function isJson(resource: any): resource is SkeletonJsonAsset {
+	return resource.hasOwnProperty("bones");
+}
+
+function isBuffer(resource: any): resource is SkeletonBinaryAsset {
+	return resource instanceof Uint8Array;
+}
+
+const spineLoaderExtension: AssetExtension<SkeletonJsonAsset | SkeletonBinaryAsset> = {
+	extension: ExtensionType.Asset,
+
+	loader: {
+		extension: {
+			type: ExtensionType.LoadParser,
+			priority: LoaderParserPriority.Normal,
+		},
+
+		test(url) {
+			return checkExtension(url, ".skel");
+		},
+
+		async load(url: string): Promise<SkeletonBinaryAsset> {
+			const response = await settings.ADAPTER.fetch(url);
+
+			const buffer = new Uint8Array(await response.arrayBuffer());
+
+			return buffer;
+		},
+		testParse(asset: unknown, options: LoadAsset): Promise<boolean> {
+			const isJsonSpineModel = checkExtension(options.src, ".json") && isJson(asset);
+			const isBinarySpineModel = checkExtension(options.src, ".skel") && isBuffer(asset);
+
+			return Promise.resolve(isJsonSpineModel || isBinarySpineModel);
+		},
+	},
+} as AssetExtension<SkeletonJsonAsset | SkeletonBinaryAsset>;
+
+extensions.add(spineLoaderExtension);

+ 32 - 0
spine-ts/spine-pixi/src/darkTintMesh/DarkTintBatchGeom.ts

@@ -0,0 +1,32 @@
+import { Geometry, Buffer, TYPES } from "@pixi/core";
+
+/**
+ * Geometry used to batch standard PIXI content (e.g. Mesh, Sprite, Graphics objects).
+ * @memberof PIXI
+ */
+export class DarkTintBatchGeometry extends Geometry {
+	// eslint-disable-next-line @typescript-eslint/naming-convention
+	public _buffer: Buffer;
+
+	// eslint-disable-next-line @typescript-eslint/naming-convention
+	public _indexBuffer: Buffer;
+
+	/**
+	 * @param {boolean} [_static=false] - Optimization flag, where `false`
+	 *        is updated every frame, `true` doesn't change frame-to-frame.
+	 */
+	constructor(_static = false) {
+		super();
+
+		this._buffer = new Buffer(undefined, _static, false);
+
+		this._indexBuffer = new Buffer(undefined, _static, true);
+
+		this.addAttribute("aVertexPosition", this._buffer, 2, false, TYPES.FLOAT)
+			.addAttribute("aTextureCoord", this._buffer, 2, false, TYPES.FLOAT)
+			.addAttribute("aColor", this._buffer, 4, true, TYPES.UNSIGNED_BYTE)
+			.addAttribute("aDarkColor", this._buffer, 4, true, TYPES.UNSIGNED_BYTE)
+			.addAttribute("aTextureId", this._buffer, 1, true, TYPES.FLOAT)
+			.addIndex(this._indexBuffer);
+	}
+}

+ 23 - 0
spine-ts/spine-pixi/src/darkTintMesh/DarkTintGeom.ts

@@ -0,0 +1,23 @@
+import { Geometry, Buffer, TYPES } from "@pixi/core";
+
+/**
+ * Geometry used to batch standard PIXI content (e.g. Mesh, Sprite, Graphics objects).
+ * @memberof PIXI
+ */
+export class DarkTintGeometry extends Geometry {
+	/**
+	 * @param {boolean} [_static=false] - Optimization flag, where `false`
+	 *        is updated every frame, `true` doesn't change frame-to-frame.
+	 */
+	constructor(_static = false) {
+		super();
+
+		const verticesBuffer = new Buffer(undefined);
+		const uvsBuffer = new Buffer(undefined, true);
+		const indexBuffer = new Buffer(undefined, true, true);
+
+		this.addAttribute("aVertexPosition", verticesBuffer, 2, false, TYPES.FLOAT);
+		this.addAttribute("aTextureCoord", uvsBuffer, 2, false, TYPES.FLOAT);
+		this.addIndex(indexBuffer);
+	}
+}

+ 175 - 0
spine-ts/spine-pixi/src/darkTintMesh/DarkTintMaterial.ts

@@ -0,0 +1,175 @@
+import type { ColorSource } from "@pixi/core";
+import { Shader, TextureMatrix, Color, Texture, Matrix, Program } from "@pixi/core";
+
+const vertex = `
+attribute vec2 aVertexPosition;
+attribute vec2 aTextureCoord;
+
+uniform mat3 projectionMatrix;
+uniform mat3 translationMatrix;
+uniform mat3 uTextureMatrix;
+
+varying vec2 vTextureCoord;
+
+void main(void)
+{
+    gl_Position = vec4((projectionMatrix * translationMatrix * vec3(aVertexPosition, 1.0)).xy, 0.0, 1.0);
+
+    vTextureCoord = (uTextureMatrix * vec3(aTextureCoord, 1.0)).xy;
+}
+`;
+
+const fragment = `
+varying vec2 vTextureCoord;
+uniform vec4 uColor;
+uniform vec4 uDarkColor;
+
+uniform sampler2D uSampler;
+
+void main(void)
+{
+	vec4 texColor = texture2D(uSampler, vTextureCoord);
+    gl_FragColor.a = texColor.a * uColor.a;
+    gl_FragColor.rgb = ((texColor.a - 1.0) * uDarkColor.a + 1.0 - texColor.rgb) * uDarkColor.rgb + texColor.rgb * uColor.rgb;
+}
+`;
+
+export interface IDarkTintMaterialOptions {
+	alpha?: number;
+	tint?: ColorSource;
+	darkTint?: ColorSource;
+	pluginName?: string;
+	uniforms?: Record<string, unknown>;
+}
+
+export class DarkTintMaterial extends Shader {
+	public readonly uvMatrix: TextureMatrix;
+
+	public batchable: boolean;
+
+	public pluginName: string;
+
+	// eslint-disable-next-line @typescript-eslint/naming-convention
+	public _tintRGB: number;
+	// eslint-disable-next-line @typescript-eslint/naming-convention
+	public _darkTintRGB: number;
+
+	/**
+	 * Only do update if tint or alpha changes.
+	 * @private
+	 * @default false
+	 */
+	private _colorDirty: boolean;
+	private _alpha: number;
+
+	private _tintColor: Color;
+	private _darkTintColor: Color;
+
+	constructor(texture?: Texture) {
+		const uniforms = {
+			uSampler: texture ?? Texture.EMPTY,
+			alpha: 1,
+			uTextureMatrix: Matrix.IDENTITY,
+			uColor: new Float32Array([1, 1, 1, 1]),
+			uDarkColor: new Float32Array([0, 0, 0, 0]),
+		};
+
+		// Set defaults
+		const options = {
+			tint: 0xffffff,
+			darkTint: 0x0,
+			alpha: 1,
+			pluginName: "darkTintBatch",
+		};
+
+		super(Program.from(vertex, fragment), uniforms);
+
+		this._colorDirty = false;
+
+		this.uvMatrix = new TextureMatrix(uniforms.uSampler);
+		this.batchable = true;
+		this.pluginName = options.pluginName;
+
+		this._tintColor = new Color(options.tint);
+		this._darkTintColor = new Color(options.darkTint);
+		this._tintRGB = this._tintColor.toLittleEndianNumber();
+		this._darkTintRGB = this._darkTintColor.toLittleEndianNumber();
+		this._alpha = options.alpha;
+		this._colorDirty = true;
+	}
+
+	public get texture(): Texture {
+		return this.uniforms.uSampler;
+	}
+	public set texture(value: Texture) {
+		if (this.uniforms.uSampler !== value) {
+			if (!this.uniforms.uSampler.baseTexture.alphaMode !== !value.baseTexture.alphaMode) {
+				this._colorDirty = true;
+			}
+
+			this.uniforms.uSampler = value;
+			this.uvMatrix.texture = value;
+		}
+	}
+
+	public set alpha(value: number) {
+		if (value === this._alpha) {
+			return;
+		}
+
+		this._alpha = value;
+		this._colorDirty = true;
+	}
+	public get alpha(): number {
+		return this._alpha;
+	}
+
+	public set tint(value: ColorSource) {
+		if (value === this.tint) {
+			return;
+		}
+
+		this._tintColor.setValue(value);
+		this._tintRGB = this._tintColor.toLittleEndianNumber();
+		this._colorDirty = true;
+	}
+	public get tint(): ColorSource {
+		return this._tintColor.value!;
+	}
+
+	public set darkTint(value: ColorSource) {
+		if (value === this.darkTint) {
+			return;
+		}
+
+		this._darkTintColor.setValue(value);
+		this._darkTintRGB = this._darkTintColor.toLittleEndianNumber();
+		this._colorDirty = true;
+	}
+	public get darkTint(): ColorSource {
+		return this._darkTintColor.value!;
+	}
+
+	public get tintValue(): number {
+		return this._tintColor.toNumber();
+	}
+
+	public get darkTintValue(): number {
+		return this._darkTintColor.toNumber();
+	}
+
+	/** Gets called automatically by the Mesh. Intended to be overridden for custom {@link PIXI.MeshMaterial} objects. */
+	public update(): void {
+		if (this._colorDirty) {
+			this._colorDirty = false;
+			const baseTexture = this.texture.baseTexture;
+			const applyToChannels = baseTexture.alphaMode as unknown as boolean;
+
+			Color.shared.setValue(this._tintColor).premultiply(this._alpha, applyToChannels).toArray(this.uniforms.uColor);
+			Color.shared.setValue(this._darkTintColor).premultiply(this._alpha, applyToChannels).toArray(this.uniforms.uDarkColor);
+		}
+		if (this.uvMatrix.update()) {
+			this.uniforms.uTextureMatrix = this.uvMatrix.mapCoord;
+		}
+	}
+}

+ 62 - 0
spine-ts/spine-pixi/src/darkTintMesh/DarkTintMesh.ts

@@ -0,0 +1,62 @@
+import type { Texture, ColorSource, Renderer, BLEND_MODES } from "@pixi/core";
+import { Mesh } from "@pixi/mesh";
+import { DarkTintGeometry } from "./DarkTintGeom";
+import { DarkTintMaterial } from "./DarkTintMaterial";
+
+export interface IDarkTintElement {
+	// eslint-disable-next-line @typescript-eslint/naming-convention
+	_texture: Texture;
+	vertexData: Float32Array;
+	indices: Uint16Array | Uint32Array | Array<number>;
+	uvs: Float32Array;
+	worldAlpha: number;
+	// eslint-disable-next-line @typescript-eslint/naming-convention
+	_tintRGB: number;
+	// eslint-disable-next-line @typescript-eslint/naming-convention
+	_darkTintRGB: number;
+	blendMode: BLEND_MODES;
+}
+
+export class DarkTintMesh extends Mesh<DarkTintMaterial> {
+	// eslint-disable-next-line @typescript-eslint/naming-convention
+	public _darkTintRGB: number = 0;
+
+	constructor(texture?: Texture) {
+		super(new DarkTintGeometry(), new DarkTintMaterial(texture), undefined, undefined);
+	}
+
+	public get darkTint(): ColorSource | null {
+		return "darkTint" in this.shader ? (this.shader as unknown as DarkTintMaterial).darkTint : null;
+	}
+
+	public set darkTint(value: ColorSource | null) {
+		(this.shader as unknown as DarkTintMaterial).darkTint = value!;
+	}
+
+	public get darkTintValue(): number {
+		return (this.shader as unknown as DarkTintMaterial).darkTintValue;
+	}
+
+	// eslint-disable-next-line @typescript-eslint/naming-convention
+	protected override _renderToBatch(renderer: Renderer): void {
+		const geometry = this.geometry;
+		const shader = this.shader;
+
+		if (shader.uvMatrix) {
+			shader.uvMatrix.update();
+			this.calculateUvs();
+		}
+
+		// set properties for batching..
+		this.calculateVertices();
+		this.indices = geometry.indexBuffer.data as Uint16Array;
+		this._tintRGB = shader._tintRGB;
+		this._darkTintRGB = shader._darkTintRGB;
+		this._texture = shader.texture;
+
+		const pluginName = this.material.pluginName;
+
+		renderer.batch.setObjectRenderer(renderer.plugins[pluginName]);
+		renderer.plugins[pluginName].render(this);
+	}
+}

+ 90 - 0
spine-ts/spine-pixi/src/darkTintMesh/DarkTintRenderer.ts

@@ -0,0 +1,90 @@
+import type { IDarkTintElement } from "./DarkTintMesh";
+import { DarkTintBatchGeometry } from "./DarkTintBatchGeom";
+import type { ExtensionMetadata, Renderer, ViewableBuffer } from "@pixi/core";
+import { BatchRenderer, ExtensionType, BatchShaderGenerator, Color } from "@pixi/core";
+
+const vertex = `
+precision highp float;
+attribute vec2 aVertexPosition;
+attribute vec2 aTextureCoord;
+attribute vec4 aColor;
+attribute vec4 aDarkColor;
+attribute float aTextureId;
+
+uniform mat3 projectionMatrix;
+uniform mat3 translationMatrix;
+uniform vec4 tint;
+
+varying vec2 vTextureCoord;
+varying vec4 vColor;
+varying vec4 vDarkColor;
+varying float vTextureId;
+
+void main(void){
+    gl_Position = vec4((projectionMatrix * translationMatrix * vec3(aVertexPosition, 1.0)).xy, 0.0, 1.0);
+
+    vTextureCoord = aTextureCoord;
+    vTextureId = aTextureId;
+    vColor = aColor * tint;
+    vDarkColor = aDarkColor * tint;
+
+}
+`;
+
+const fragment = `
+varying vec2 vTextureCoord;
+varying vec4 vColor;
+varying vec4 vDarkColor;
+varying float vTextureId;
+uniform sampler2D uSamplers[%count%];
+
+void main(void){
+    vec4 color;
+    %forloop%
+
+
+    gl_FragColor.a = color.a * vColor.a;
+    gl_FragColor.rgb = ((color.a - 1.0) * vDarkColor.a + 1.0 - color.rgb) * vDarkColor.rgb + color.rgb * vColor.rgb;
+}
+`;
+
+export class DarkTintRenderer extends BatchRenderer {
+	public static override extension: ExtensionMetadata = {
+		name: "darkTintBatch",
+		type: ExtensionType.RendererPlugin,
+	};
+
+	constructor(renderer: Renderer) {
+		super(renderer);
+		this.shaderGenerator = new BatchShaderGenerator(vertex, fragment);
+		this.geometryClass = DarkTintBatchGeometry;
+		// Pixi's default 6 + 1 for uDarkTint. (this is size in _floats_. color is 4 bytes which roughly equals one float :P )
+		this.vertexSize = 7;
+	}
+
+	public override packInterleavedGeometry(element: IDarkTintElement, attributeBuffer: ViewableBuffer, indexBuffer: Uint16Array, aIndex: number, iIndex: number): void {
+		const { uint32View, float32View } = attributeBuffer;
+		const packedVertices = aIndex / this.vertexSize;
+		const uvs = element.uvs;
+		const indicies = element.indices;
+		const vertexData = element.vertexData;
+		const textureId = element._texture.baseTexture._batchLocation;
+		const alpha = Math.min(element.worldAlpha, 1.0);
+		const argb = Color.shared.setValue(element._tintRGB).toPremultiplied(alpha, (element._texture.baseTexture.alphaMode ?? 0) > 0);
+		const darkargb = Color.shared.setValue(element._darkTintRGB).toPremultiplied(alpha, (element._texture.baseTexture.alphaMode ?? 0) > 0);
+
+		// lets not worry about tint! for now..
+		for (let i = 0; i < vertexData.length; i += 2) {
+			float32View[aIndex++] = vertexData[i];
+			float32View[aIndex++] = vertexData[i + 1];
+			float32View[aIndex++] = uvs[i];
+			float32View[aIndex++] = uvs[i + 1];
+			uint32View[aIndex++] = argb;
+			uint32View[aIndex++] = darkargb;
+			float32View[aIndex++] = textureId;
+		}
+		for (let i = 0; i < indicies.length; i++) {
+			indexBuffer[iIndex++] = packedVertices + indicies[i];
+		}
+	}
+}

+ 18 - 0
spine-ts/spine-pixi/src/index.ts

@@ -0,0 +1,18 @@
+export * from './require-shim';
+export * from './Spine';
+export * from './SpineDebugRenderer';
+export * from './SpineTexture';
+export * from './SlotMesh';
+export * from './DarkSlotMesh';
+export * from './assets/atlasLoader';
+export * from './assets/skeletonLoader';
+export * from './darkTintMesh/DarkTintBatchGeom';
+export * from './darkTintMesh/DarkTintGeom';
+export * from './darkTintMesh/DarkTintMaterial';
+export * from './darkTintMesh/DarkTintMesh';
+export * from './darkTintMesh/DarkTintRenderer';
+export * from "@esotericsoftware/spine-core";
+
+
+import './assets/atlasLoader'; // Side effects install the loaders into pixi
+import './assets/skeletonLoader'; // Side effects install the loaders into pixi

+ 43 - 0
spine-ts/spine-pixi/src/require-shim.ts

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

+ 24 - 0
spine-ts/spine-pixi/tsconfig.json

@@ -0,0 +1,24 @@
+{
+	"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"
+		}
+	]
+}

+ 3 - 0
spine-ts/tsconfig.json

@@ -18,6 +18,9 @@
 		},
 		{
 			"path": "./spine-threejs"
+		},
+		{
+			"path": "./spine-pixi"
 		}
 	]
 }

Неке датотеке нису приказане због велике количине промена