Merge branch '4.1' into 4.2-beta

This commit is contained in:
Harald Csaszar 2023-06-27 15:17:23 +02:00
commit 102f3e044b
32 changed files with 11189 additions and 17 deletions

1
.gitignore vendored
View File

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

View File

@ -357,6 +357,11 @@ cp -f ../spineboy/export/spineboy-pro.skel "$ROOT/spine-ts/spine-player/example/
cp -f ../spineboy/export/spineboy-pma.atlas "$ROOT/spine-ts/spine-player/example/assets/"
cp -f ../spineboy/export/spineboy-pma.png "$ROOT/spine-ts/spine-player/example/assets/"
cp -f ../spineboy/export/spineboy-pro.json "$ROOT/spine-ts/spine-pixi/example/assets/"
cp -f ../spineboy/export/spineboy-pro.skel "$ROOT/spine-ts/spine-pixi/example/assets/"
cp -f ../spineboy/export/spineboy.atlas "$ROOT/spine-ts/spine-pixi/example/assets/"
cp -f ../spineboy/export/spineboy.png "$ROOT/spine-ts/spine-pixi/example/assets/"
rm "$ROOT/spine-ts/spine-phaser/example/assets/"*
cp -f ../raptor/export/raptor-pro.json "$ROOT/spine-ts/spine-phaser/example/assets/"
cp -f ../raptor/export/raptor-pma.atlas "$ROOT/spine-ts/spine-phaser/example/assets/"

View File

@ -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 "file=@spine-ts.zip" "$TS_UPDATE_URL$BRANCH"
else

View File

@ -14,6 +14,7 @@
"spine-phaser",
"spine-player",
"spine-threejs",
"spine-pixi",
"spine-webgl"
],
"devDependencies": {
@ -56,6 +57,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
@ -68,6 +73,178 @@
"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",
"dev": true,
@ -499,6 +676,12 @@
"dev": true,
"license": "MIT"
},
"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",
"dev": true,
@ -733,6 +916,12 @@
"dev": true,
"license": "MIT"
},
"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",
"dev": true,
@ -1407,6 +1596,12 @@
"dev": true,
"license": "MIT"
},
"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",
"dev": true,
@ -1823,6 +2018,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",
"dev": true,
@ -2586,6 +2797,16 @@
"dev": true,
"license": "MIT"
},
"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",
"dev": true,
@ -2729,6 +2950,21 @@
"@esotericsoftware/spine-webgl": "4.2.14"
}
},
"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.2.14",

View File

@ -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": {

View File

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

View File

@ -0,0 +1,117 @@
spineboy-polypack.png
size:2048,512
filter:Linear,Linear
pma:true
crosshair
bounds:1895,128,89,89
eye-indifferent
bounds:1863,217,93,89
rotate:90
eye-surprised
bounds:1696,48,93,89
front-bracer
bounds:1192,4,58,80
rotate:90
front-fist-closed
bounds:430,4,75,82
rotate:90
front-fist-open
bounds:1895,42,86,87
rotate:90
front-foot
bounds:1066,22,126,69
rotate:180
front-shin
bounds:0,46,82,184
rotate:90
front-thigh
bounds:1484,20,45,112
front-upper-arm
bounds:791,11,46,97
rotate:90
goggles
bounds:1320,116,261,166
gun
bounds:1653,137,210,203
head
bounds:1025,121,271,298
rotate:90
hoverboard-board
bounds:1297,240,492,152
rotate:180
hoverboard-thruster
bounds:1529,0,60,64
rotate:90
hoverglow-small
bounds:528,63,274,75
mouth-grind
bounds:698,4,93,59
mouth-oooo
bounds:605,4,93,59
mouth-smile
bounds:512,4,93,59
muzzle-glow
bounds:87,2,50,50
muzzle-ring
bounds:317,79,49,209
rotate:90
muzzle01
bounds:184,49,133,79
muzzle02
bounds:802,57,135,81
offsets:0,3,135,84
muzzle03
bounds:1536,60,160,98
offsets:4,5,166,106
muzzle04
bounds:1807,68,149,88
offsets:0,2,149,90
rotate:90
muzzle05
bounds:1351,43,133,73
offsets:0,1,135,75
neck
bounds:137,9,35,41
offsets:0,0,36,41
portal-bg
bounds:264,128,264,264
offsets:1,1,266,266
portal-flare1
bounds:1984,108,109,58
offsets:1,1,111,60
rotate:90
portal-flare2
bounds:1952,217,112,59
offsets:1,1,114,61
rotate:90
portal-flare3
bounds:317,22,113,57
offsets:1,1,115,59
portal-shade
bounds:0,128,264,264
offsets:1,1,266,266
portal-streaks1
bounds:528,138,250,254
offsets:1,1,252,256
portal-streaks2
bounds:778,138,248,247
offsets:1,1,250,249
rotate:90
rear-bracer
bounds:1272,7,55,70
offsets:0,2,56,72
rotate:90
rear-foot
bounds:1743,5,113,60
rotate:180
rear-shin
bounds:937,46,75,178
rotate:90
rear-thigh
bounds:1982,14,55,94
rear-upper-arm
bounds:0,7,40,87
rotate:90
torso
bounds:1171,62,98,180
rotate:270

Binary file not shown.

After

Width:  |  Height:  |  Size: 858 KiB

File diff suppressed because it is too large Load Diff

Binary file not shown.

View File

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 239 KiB

View File

@ -0,0 +1,140 @@
<html>
<head>
<meta charset="UTF-8">
<title>spine-pixi</title>
<script src="https://cdn.jsdelivr.net/npm/pixi.js@7.2.4/dist/pixi.min.js"></script>
<script src="../dist/iife/spine-pixi.js"></script>
<script src="https://cdn.jsdelivr.net/npm/tweakpane@3.1.9/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);
// Feel free to mix and match the binary skeleton, the json skeleton, the rect atlas and the polypack atlas
// You only need one skeleton and one atlas, the rest is just to show how to load different formats
PIXI.Assets.add("spineboySkeletonJson", "./assets/spineboy-pro.json");
PIXI.Assets.add("spineboySkeletonBinary", "./assets/spineboy-pro.skel");
PIXI.Assets.add("spineboyAtlas", "./assets/spineboy.atlas");
PIXI.Assets.add("spineboyAtlasPolypack", "./assets/spineboy-polypack.atlas");
await PIXI.Assets.load([
"spineboySkeletonJson",
"spineboySkeletonBinary",
"spineboyAtlas",
"spineboyAtlasPolypack"
]);
// 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>

View File

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

View File

@ -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();
}
}

View File

@ -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();
}
}

View File

@ -0,0 +1,365 @@
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 {
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;
}
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.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 resetMeshes (): void {
for (const [, mesh] of this.meshesCache) {
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)!;
mesh.visible = true;
return mesh;
}
}
private verticesCache: NumberArrayLike = Utils.newFloatArray(1024);
private updateGeometry (): void {
this.resetMeshes();
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)!;
}
if (!bone) throw Error(`Cant set bone position, bone ${String(boneAux)} not found`);
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)!;
}
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);
const attachmentLoader = new AtlasAttachmentLoader(atlasAsset);
let parser = skeletonAsset instanceof Uint8Array ? new SkeletonBinary(attachmentLoader) : 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;
}

View File

@ -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);
}
}

View File

@ -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)}`);
}
}
}

View File

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

View File

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

View File

@ -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);
}
}

View File

@ -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);
}
}

View File

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

View File

@ -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);
}
}

View File

@ -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];
}
}
}

View File

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

View File

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

View File

@ -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"
}
]
}

View File

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

View File

@ -664,7 +664,7 @@ namespace Spine.Unity.Editor {
Material material = (Material)AssetDatabase.LoadAssetAtPath(materialPath, typeof(Material));
if (material == null) {
Shader defaultShader = Shader.Find(SpineEditorUtilities.Preferences.DefaultShader);
Shader defaultShader = GetDefaultShader();
material = defaultShader != null ? new Material(defaultShader) : null;
if (material) {
ApplyPMAOrStraightAlphaSettings(material, SpineEditorUtilities.Preferences.textureSettingsReference);
@ -737,6 +737,13 @@ namespace Spine.Unity.Editor {
return loadedAtlas != null ? loadedAtlas : atlasAsset;
}
public static Shader GetDefaultShader () {
Shader shader = Shader.Find(SpineEditorUtilities.Preferences.DefaultShader);
if (shader == null) shader = Shader.Find("Spine/Skeleton");
if (shader == null) shader = Shader.Find("Standard");
return shader;
}
public static bool SpriteAtlasSettingsNeedAdjustment (UnityEngine.U2D.SpriteAtlas spriteAtlas) {
#if EXPOSES_SPRITE_ATLAS_UTILITIES
UnityEditor.U2D.SpriteAtlasPackingSettings packingSettings = UnityEditor.U2D.SpriteAtlasExtensions.GetPackingSettings(spriteAtlas);
@ -851,24 +858,24 @@ namespace Spine.Unity.Editor {
{
string pageName = "SpriteAtlas";
string materialPath = assetPath + "/" + primaryName + "_" + pageName + ".mat";
Material mat = AssetDatabase.LoadAssetAtPath<Material>(materialPath);
Material material = AssetDatabase.LoadAssetAtPath<Material>(materialPath);
if (mat == null) {
mat = new Material(Shader.Find(SpineEditorUtilities.Preferences.defaultShader));
ApplyPMAOrStraightAlphaSettings(mat, SpineEditorUtilities.Preferences.textureSettingsReference);
AssetDatabase.CreateAsset(mat, materialPath);
if (material == null) {
Shader defaultShader = GetDefaultShader();
material = defaultShader != null ? new Material(defaultShader) : null;
ApplyPMAOrStraightAlphaSettings(material, SpineEditorUtilities.Preferences.textureSettingsReference);
AssetDatabase.CreateAsset(material, materialPath);
} else {
vestigialMaterials.Remove(mat);
vestigialMaterials.Remove(material);
}
if (texture != null)
mat.mainTexture = texture;
material.mainTexture = texture;
EditorUtility.SetDirty(mat);
EditorUtility.SetDirty(material);
// note: don't call AssetDatabase.SaveAssets() since this would trigger OnPostprocessAllAssets() every time unnecessarily.
populatingMaterials.Add(mat); //atlasAsset.materials[i] = mat;
populatingMaterials.Add(material);
}
atlasAsset.materials = populatingMaterials.ToArray();

View File

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