Merge branch '4.2' into 4.3-beta

# Conflicts:
#	spine-unity/Assets/Spine/package.json
This commit is contained in:
Nathan Sweet 2025-05-21 10:29:28 -04:00
commit 9b596c3856
53 changed files with 816 additions and 1823 deletions

1
.github/pull_request_template.md vendored Normal file
View File

@ -0,0 +1 @@
* [ ] I confirm this contribution is made under the Esoteric Software LLC [CLA](http://esotericsoftware.com/licenses/cla.txt).

View File

@ -49,7 +49,7 @@ jobs:
path: spine-godot/example-v4-extension/bin/windows/*.dll path: spine-godot/example-v4-extension/bin/windows/*.dll
build-linux-x86_64: build-linux-x86_64:
runs-on: ubuntu-latest runs-on: ubuntu-22.04
steps: steps:
- uses: actions/checkout@v3 - uses: actions/checkout@v3
with: with:

View File

@ -65,7 +65,7 @@ jobs:
path: spine-godot/godot/bin/**/* path: spine-godot/godot/bin/**/*
godot-editor-linux: godot-editor-linux:
runs-on: ubuntu-latest runs-on: ubuntu-22.04
steps: steps:
- uses: actions/checkout@v3 - uses: actions/checkout@v3
with: with:
@ -161,7 +161,7 @@ jobs:
path: spine-godot/godot/bin/macos.zip path: spine-godot/godot/bin/macos.zip
godot-template-linux: godot-template-linux:
runs-on: ubuntu-latest runs-on: ubuntu-22.04
steps: steps:
- uses: actions/checkout@v3 - uses: actions/checkout@v3
with: with:

1
.gitignore vendored
View File

@ -221,3 +221,4 @@ spine-godot/build/version.txt
spine-godot/vc140.pdb spine-godot/vc140.pdb
spine-godot/example-v4-extension/bin spine-godot/example-v4-extension/bin
spine-godot/example-v4-extension/MoltenVK.xcframework spine-godot/example-v4-extension/MoltenVK.xcframework
spine-flutter/example/android/app/.cxx

View File

@ -175,6 +175,7 @@
2. Add a `RenderExistingMeshGraphic` component. 2. Add a `RenderExistingMeshGraphic` component.
3. In the `RenderExistingMeshGraphic` component Inspector at `Reference Skeleton Graphic` assign the original `SkeletonGraphic` object. 3. In the `RenderExistingMeshGraphic` component Inspector at `Reference Skeleton Graphic` assign the original `SkeletonGraphic` object.
4. At `Replacement Material` assign e.g. the included _SkeletonGraphicDefaultOutline_ material to replace all materials with this material. Alternatively, if `Multiple CanvasRenderers` is enabled at the reference SkeletonGraphic, you can add entries to the `Replacement Materials` list and at each entry assign the original SkeletonGraphic material (e.g. _SkeletonGraphicDefault_) to be replaced and the respective `Replacement Material` (e.g. _SkeletonGraphicDefaultOutline_). 4. At `Replacement Material` assign e.g. the included _SkeletonGraphicDefaultOutline_ material to replace all materials with this material. Alternatively, if `Multiple CanvasRenderers` is enabled at the reference SkeletonGraphic, you can add entries to the `Replacement Materials` list and at each entry assign the original SkeletonGraphic material (e.g. _SkeletonGraphicDefault_) to be replaced and the respective `Replacement Material` (e.g. _SkeletonGraphicDefaultOutline_).
- Added option for unsafe direct data loading when loading skeleton binary data to avoid some allocations, enabled via build define `SPINE_ALLOW_UNSAFE`. This define can be set via Spine Preferences, setting `Unsafe Build Defines - Direct data access`. The define is disabled by default to maintain existing behaviour. Changed asmdef setting for spine-unity assembly to allow unsafe code, has no effect other than allowing setting the `SPINE_ALLOW_UNSAFE` define.
- **Breaking changes** - **Breaking changes**

View File

@ -1,3 +1,6 @@
# 4.2.36
- Support for 16KB page alignement on Android. You must specify the NDK version in the build.gradle file of your app's Android project. See https://github.com/EsotericSoftware/spine-runtimes/issues/2849
# 4.2.35 # 4.2.35
- Port of commit f1e0f0f: Fixed animation not being mixed out in some cases. - Port of commit f1e0f0f: Fixed animation not being mixed out in some cases.

View File

@ -11,7 +11,7 @@ buildscript {
dependencies { dependencies {
// The Android Gradle Plugin knows how to build native code with the NDK. // The Android Gradle Plugin knows how to build native code with the NDK.
classpath 'com.android.tools.build:gradle:7.1.2' classpath 'com.android.tools.build:gradle:8.5.1'
} }
} }
@ -33,7 +33,7 @@ android {
// Bumping the plugin ndkVersion requires all clients of this plugin to bump // Bumping the plugin ndkVersion requires all clients of this plugin to bump
// the version in their app and to download a newer version of the NDK. // the version in their app and to download a newer version of the NDK.
ndkVersion "21.1.6352462" ndkVersion "28.1.13356709"
// Invoke the shared CMake build with the Android Gradle Plugin. // Invoke the shared CMake build with the Android Gradle Plugin.
externalNativeBuild { externalNativeBuild {
@ -56,6 +56,6 @@ android {
} }
defaultConfig { defaultConfig {
minSdkVersion 16 minSdkVersion 21
} }
} }

View File

@ -5,9 +5,11 @@
*.swp *.swp
.DS_Store .DS_Store
.atom/ .atom/
.build/
.buildlog/ .buildlog/
.history .history
.svn/ .svn/
.swiftpm/
migrate_working_dir/ migrate_working_dir/
# IntelliJ related # IntelliJ related

View File

@ -1,3 +1,9 @@
plugins {
id "com.android.application"
id "kotlin-android"
id "dev.flutter.flutter-gradle-plugin"
}
def localProperties = new Properties() def localProperties = new Properties()
def localPropertiesFile = rootProject.file('local.properties') def localPropertiesFile = rootProject.file('local.properties')
if (localPropertiesFile.exists()) { if (localPropertiesFile.exists()) {
@ -6,11 +12,6 @@ if (localPropertiesFile.exists()) {
} }
} }
def flutterRoot = localProperties.getProperty('flutter.sdk')
if (flutterRoot == null) {
throw new GradleException("Flutter SDK not found. Define location with flutter.sdk in the local.properties file.")
}
def flutterVersionCode = localProperties.getProperty('flutter.versionCode') def flutterVersionCode = localProperties.getProperty('flutter.versionCode')
if (flutterVersionCode == null) { if (flutterVersionCode == null) {
flutterVersionCode = '1' flutterVersionCode = '1'
@ -21,13 +22,11 @@ if (flutterVersionName == null) {
flutterVersionName = '1.0' flutterVersionName = '1.0'
} }
apply plugin: 'com.android.application'
apply plugin: 'kotlin-android'
apply from: "$flutterRoot/packages/flutter_tools/gradle/flutter.gradle"
android { android {
compileSdkVersion flutter.compileSdkVersion compileSdkVersion flutter.compileSdkVersion
ndkVersion flutter.ndkVersion // ndkVersion flutter.ndkVersion
ndkVersion "28.1.13356709"
namespace "com.esotericsoftware.spine.android"
compileOptions { compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8 sourceCompatibility JavaVersion.VERSION_1_8
@ -44,7 +43,7 @@ android {
defaultConfig { defaultConfig {
// TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html). // TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html).
applicationId "com.example.spine_flutter_example" applicationId "com.esotericsoftware.spine.android"
// You can update the following values to match your application needs. // You can update the following values to match your application needs.
// For more information, see: https://docs.flutter.dev/deployment/android#reviewing-the-build-configuration. // For more information, see: https://docs.flutter.dev/deployment/android#reviewing-the-build-configuration.
minSdkVersion flutter.minSdkVersion minSdkVersion flutter.minSdkVersion
@ -65,7 +64,3 @@ android {
flutter { flutter {
source '../..' source '../..'
} }
dependencies {
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
}

View File

@ -1,5 +1,4 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android" <manifest xmlns:android="http://schemas.android.com/apk/res/android">
package="com.example.spine_flutter_example">
<!-- The INTERNET permission is required for development. Specifically, <!-- The INTERNET permission is required for development. Specifically,
the Flutter tool needs it to communicate with the running application the Flutter tool needs it to communicate with the running application
to allow setting breakpoints, to provide hot reload, etc. to allow setting breakpoints, to provide hot reload, etc.

View File

@ -1,5 +1,4 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android" <manifest xmlns:android="http://schemas.android.com/apk/res/android">
package="com.example.spine_flutter_example">
<application <application
android:label="spine_flutter_example" android:label="spine_flutter_example"
android:name="${applicationName}" android:name="${applicationName}"

View File

@ -1,4 +1,4 @@
package com.example.example package com.esotericsoftware.spine.android
import io.flutter.embedding.android.FlutterActivity import io.flutter.embedding.android.FlutterActivity

View File

@ -1,6 +0,0 @@
package com.example.spine_flutter_example
import io.flutter.embedding.android.FlutterActivity
class MainActivity: FlutterActivity() {
}

View File

@ -1,5 +1,4 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android" <manifest xmlns:android="http://schemas.android.com/apk/res/android">
package="com.example.spine_flutter_example">
<!-- The INTERNET permission is required for development. Specifically, <!-- The INTERNET permission is required for development. Specifically,
the Flutter tool needs it to communicate with the running application the Flutter tool needs it to communicate with the running application
to allow setting breakpoints, to provide hot reload, etc. to allow setting breakpoints, to provide hot reload, etc.

View File

@ -1,16 +1,3 @@
buildscript {
ext.kotlin_version = '1.6.10'
repositories {
google()
mavenCentral()
}
dependencies {
classpath 'com.android.tools.build:gradle:7.1.2'
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
}
}
allprojects { allprojects {
repositories { repositories {
google() google()

View File

@ -3,4 +3,4 @@ distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists distributionPath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists zipStorePath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-7.4-all.zip distributionUrl=https\://services.gradle.org/distributions/gradle-8.7-all.zip

View File

@ -1,11 +1,25 @@
include ':app' pluginManagement {
def flutterSdkPath = {
def properties = new Properties()
file("local.properties").withInputStream { properties.load(it) }
def flutterSdkPath = properties.getProperty("flutter.sdk")
assert flutterSdkPath != null, "flutter.sdk not set in local.properties"
return flutterSdkPath
}()
def localPropertiesFile = new File(rootProject.projectDir, "local.properties") includeBuild("$flutterSdkPath/packages/flutter_tools/gradle")
def properties = new Properties()
assert localPropertiesFile.exists() repositories {
localPropertiesFile.withReader("UTF-8") { reader -> properties.load(reader) } google()
mavenCentral()
gradlePluginPortal()
}
}
def flutterSdkPath = properties.getProperty("flutter.sdk") plugins {
assert flutterSdkPath != null, "flutter.sdk not set in local.properties" id "dev.flutter.flutter-plugin-loader" version "1.0.0" // apply true
apply from: "$flutterSdkPath/packages/flutter_tools/gradle/app_plugin_loader.gradle" id "com.android.application" version "8.5.1" apply false
id "org.jetbrains.kotlin.android" version "2.1.0" apply false
}
include ":app"

View File

@ -13,18 +13,18 @@ packages:
dependency: transitive dependency: transitive
description: description:
name: characters name: characters
sha256: "04a925763edad70e8443c99234dc3328f442e811f1d8fd1a72f1c8ad0f69a605" sha256: f71061c654a3380576a52b451dd5532377954cf9dbd272a78fc8479606670803
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "1.3.0" version: "1.4.0"
collection: collection:
dependency: transitive dependency: transitive
description: description:
name: collection name: collection
sha256: ee67cb0715911d28db6bf4af1026078bd6f0128b07a5f66fb2ed94ec6783c09a sha256: "2f5709ae4d3d59dd8f7cd309b4e023046b57d8a6c82130785d2b0e5868084e76"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "1.18.0" version: "1.19.1"
crypto: crypto:
dependency: transitive dependency: transitive
description: description:
@ -53,10 +53,10 @@ packages:
dependency: "direct main" dependency: "direct main"
description: description:
name: flame name: flame
sha256: "2a2352741500ce47823dcf212f06b23e9bdb622454eab90244ee6da58e23b488" sha256: f9e7a100c25f8d6bfd143bf325a9689c509216cd1c8133ce4684955c56770de7
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "1.15.0" version: "1.28.1"
flutter: flutter:
dependency: "direct main" dependency: "direct main"
description: flutter description: flutter
@ -114,26 +114,26 @@ packages:
dependency: transitive dependency: transitive
description: description:
name: material_color_utilities name: material_color_utilities
sha256: "0e0a020085b65b6083975e499759762399b4475f766c21668c4ecca34ea74e5a" sha256: f7142bb1154231d7ea5f96bc7bde4bda2a0945d2806bb11670e30b850d56bdec
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "0.8.0" version: "0.11.1"
meta: meta:
dependency: transitive dependency: transitive
description: description:
name: meta name: meta
sha256: "7687075e408b093f36e6bbf6c91878cc0d4cd10f409506f7bc996f68220b9136" sha256: e3641ec5d63ebf0d9b41bd43201a66e3fc79a65db5f61fc181f04cd27aab950c
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "1.12.0" version: "1.16.0"
ordered_set: ordered_set:
dependency: transitive dependency: transitive
description: description:
name: ordered_set name: ordered_set
sha256: "3fedcc9121b3ba24c0a84f32da2989c42e36c159b73feadbc2f402dc55966b81" sha256: dc68b8f1abc7115b81cf890bf7d2ece4ed1d95e0f3e486ab4b64ab3d16d2ea42
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "5.0.1" version: "7.0.0"
path: path:
dependency: transitive dependency: transitive
description: description:
@ -154,7 +154,7 @@ packages:
dependency: transitive dependency: transitive
description: flutter description: flutter
source: sdk source: sdk
version: "0.0.99" version: "0.0.0"
source_span: source_span:
dependency: transitive dependency: transitive
description: description:
@ -169,7 +169,7 @@ packages:
path: ".." path: ".."
relative: true relative: true
source: path source: path
version: "4.2.34" version: "4.2.35"
string_scanner: string_scanner:
dependency: transitive dependency: transitive
description: description:
@ -211,5 +211,5 @@ packages:
source: hosted source: hosted
version: "0.7.5" version: "0.7.5"
sdks: sdks:
dart: ">=3.3.0-0 <4.0.0" dart: ">=3.7.0-0 <4.0.0"
flutter: ">=3.16.0" flutter: ">=3.27.1"

View File

@ -14,7 +14,7 @@ dependencies:
spine_flutter: spine_flutter:
path: ../ path: ../
cupertino_icons: ^1.0.6 cupertino_icons: ^1.0.6
flame: ^1.10.1 flame: ^1.28.1
raw_image_provider: ^0.2.0 raw_image_provider: ^0.2.0
dev_dependencies: dev_dependencies:

View File

@ -1,6 +1,6 @@
name: spine_flutter name: spine_flutter
description: The official Spine Flutter Runtime to load, display and interact with Spine animations. description: The official Spine Flutter Runtime to load, display and interact with Spine animations.
version: 4.2.35 version: 4.2.36
homepage: https://esotericsoftware.com homepage: https://esotericsoftware.com
repository: https://github.com/esotericsoftware/spine-runtimes repository: https://github.com/esotericsoftware/spine-runtimes
issue_tracker: https://github.com/esotericsoftware/spine-runtimes/issues issue_tracker: https://github.com/esotericsoftware/spine-runtimes/issues

View File

@ -132,7 +132,6 @@ namespace Spine {
float attachmentColorR, attachmentColorG, attachmentColorB, attachmentColorA; float attachmentColorR, attachmentColorG, attachmentColorB, attachmentColorA;
object textureObject = null; object textureObject = null;
int verticesCount = 0; int verticesCount = 0;
float[] vertices = this.vertices;
int indicesCount = 0; int indicesCount = 0;
int[] indices = null; int[] indices = null;
float[] uvs = null; float[] uvs = null;
@ -211,9 +210,10 @@ namespace Spine {
darkColor.A = premultipliedAlpha ? (byte)255 : (byte)0; darkColor.A = premultipliedAlpha ? (byte)255 : (byte)0;
// clip // clip
float[] usedVertices = vertices;
if (clipper.IsClipping) { if (clipper.IsClipping) {
clipper.ClipTriangles(vertices, indices, indicesCount, uvs); clipper.ClipTriangles(usedVertices, indices, indicesCount, uvs);
vertices = clipper.ClippedVertices.Items; usedVertices = clipper.ClippedVertices.Items;
verticesCount = clipper.ClippedVertices.Count >> 1; verticesCount = clipper.ClippedVertices.Count >> 1;
indices = clipper.ClippedTriangles.Items; indices = clipper.ClippedTriangles.Items;
indicesCount = clipper.ClippedTriangles.Count; indicesCount = clipper.ClippedTriangles.Count;
@ -240,8 +240,8 @@ namespace Spine {
for (int ii = 0, v = 0, nn = verticesCount << 1; v < nn; ii++, v += 2) { for (int ii = 0, v = 0, nn = verticesCount << 1; v < nn; ii++, v += 2) {
itemVertices[ii].Color = color; itemVertices[ii].Color = color;
itemVertices[ii].Color2 = darkColor; itemVertices[ii].Color2 = darkColor;
itemVertices[ii].Position.X = vertices[v]; itemVertices[ii].Position.X = usedVertices[v];
itemVertices[ii].Position.Y = vertices[v + 1]; itemVertices[ii].Position.Y = usedVertices[v + 1];
itemVertices[ii].Position.Z = attachmentZOffset; itemVertices[ii].Position.Z = attachmentZOffset;
itemVertices[ii].TextureCoordinate.X = uvs[v]; itemVertices[ii].TextureCoordinate.X = uvs[v];
itemVertices[ii].TextureCoordinate.Y = uvs[v + 1]; itemVertices[ii].TextureCoordinate.Y = uvs[v + 1];

File diff suppressed because it is too large Load Diff

View File

@ -1,6 +1,6 @@
{ {
"name": "@esotericsoftware/spine-ts", "name": "@esotericsoftware/spine-ts",
"version": "4.2.81", "version": "4.2.82",
"description": "The official Spine Runtimes for the web.", "description": "The official Spine Runtimes for the web.",
"type": "module", "type": "module",
"files": [ "files": [
@ -85,9 +85,9 @@
"@types/offscreencanvas": "^2019.6.4", "@types/offscreencanvas": "^2019.6.4",
"concurrently": "^7.6.0", "concurrently": "^7.6.0",
"copyfiles": "^2.4.1", "copyfiles": "^2.4.1",
"esbuild": "^0.16.4", "esbuild": "^0.25.4",
"live-server": "^1.2.2", "alive-server": "^1.3.0",
"rimraf": "^3.0.2", "rimraf": "^3.0.2",
"typescript": "5.6.2" "typescript": "5.6.2"
} }
} }

View File

@ -22,6 +22,7 @@ sed -i '' "s/$currentVersion/$newVersion/" spine-pixi-v8/package.json
sed -i '' "s/$currentVersion/$newVersion/" spine-player/package.json sed -i '' "s/$currentVersion/$newVersion/" spine-player/package.json
sed -i '' "s/$currentVersion/$newVersion/" spine-threejs/package.json sed -i '' "s/$currentVersion/$newVersion/" spine-threejs/package.json
sed -i '' "s/$currentVersion/$newVersion/" spine-webgl/package.json sed -i '' "s/$currentVersion/$newVersion/" spine-webgl/package.json
sed -i '' "s/$currentVersion/$newVersion/" spine-webcomponents/package.json
rm package-lock.json rm package-lock.json
rm -rf node_modules/@esotericsoftware rm -rf node_modules/@esotericsoftware

View File

@ -1,6 +1,6 @@
{ {
"name": "@esotericsoftware/spine-canvas", "name": "@esotericsoftware/spine-canvas",
"version": "4.2.81", "version": "4.2.82",
"description": "The official Spine Runtimes for the web.", "description": "The official Spine Runtimes for the web.",
"main": "dist/index.js", "main": "dist/index.js",
"types": "dist/index.d.ts", "types": "dist/index.d.ts",
@ -31,6 +31,6 @@
}, },
"homepage": "https://github.com/esotericsoftware/spine-runtimes#readme", "homepage": "https://github.com/esotericsoftware/spine-runtimes#readme",
"dependencies": { "dependencies": {
"@esotericsoftware/spine-core": "4.2.81" "@esotericsoftware/spine-core": "4.2.82"
} }
} }

View File

@ -1,6 +1,6 @@
{ {
"name": "@esotericsoftware/spine-canvaskit", "name": "@esotericsoftware/spine-canvaskit",
"version": "4.2.81", "version": "4.2.82",
"description": "The official Spine Runtimes for CanvasKit for NodeJS", "description": "The official Spine Runtimes for CanvasKit for NodeJS",
"main": "dist/index.js", "main": "dist/index.js",
"types": "dist/index.d.ts", "types": "dist/index.d.ts",
@ -31,7 +31,7 @@
}, },
"homepage": "https://github.com/esotericsoftware/spine-runtimes#readme", "homepage": "https://github.com/esotericsoftware/spine-runtimes#readme",
"dependencies": { "dependencies": {
"@esotericsoftware/spine-core": "4.2.81", "@esotericsoftware/spine-core": "4.2.82",
"canvaskit-wasm": "0.39.1" "canvaskit-wasm": "0.39.1"
}, },
"devDependencies": { "devDependencies": {

View File

@ -1,6 +1,6 @@
{ {
"name": "@esotericsoftware/spine-core", "name": "@esotericsoftware/spine-core",
"version": "4.2.81", "version": "4.2.82",
"description": "The official Spine Runtimes for the web.", "description": "The official Spine Runtimes for the web.",
"main": "dist/index.js", "main": "dist/index.js",
"types": "dist/index.d.ts", "types": "dist/index.d.ts",

View File

@ -1,6 +1,6 @@
{ {
"name": "@esotericsoftware/spine-phaser-v3", "name": "@esotericsoftware/spine-phaser-v3",
"version": "4.2.81", "version": "4.2.82",
"description": "The official Spine Runtimes for the Phaser v3.", "description": "The official Spine Runtimes for the Phaser v3.",
"main": "dist/index.js", "main": "dist/index.js",
"types": "dist/index.d.ts", "types": "dist/index.d.ts",
@ -31,9 +31,9 @@
}, },
"homepage": "https://github.com/esotericsoftware/spine-runtimes#readme", "homepage": "https://github.com/esotericsoftware/spine-runtimes#readme",
"dependencies": { "dependencies": {
"@esotericsoftware/spine-canvas": "4.2.81", "@esotericsoftware/spine-canvas": "4.2.82",
"@esotericsoftware/spine-core": "4.2.81", "@esotericsoftware/spine-core": "4.2.82",
"@esotericsoftware/spine-webgl": "4.2.81" "@esotericsoftware/spine-webgl": "4.2.82"
}, },
"devDependencies": { "devDependencies": {
"phaser": "^3.60.0" "phaser": "^3.60.0"

View File

@ -370,7 +370,10 @@ class SpineAtlasFile extends Phaser.Loader.MultiFile {
} }
} }
let basePath = file.src.match(/^.*\//) ?? ""; let basePath = (file.src.match(/^.*\//) ?? "").toString();
if (this.loader.path && this.loader.path.length > 0 && basePath.startsWith(this.loader.path))
basePath = basePath.slice(this.loader.path.length);
for (var i = 0; i < textures.length; i++) { for (var i = 0; i < textures.length; i++) {
var url = basePath + textures[i]; var url = basePath + textures[i];
var key = file.key + "!" + textures[i]; var key = file.key + "!" + textures[i];

View File

@ -27,12 +27,9 @@
* SPINE RUNTIMES, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. * SPINE RUNTIMES, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*****************************************************************************/ *****************************************************************************/
declare global {
var require: any;
}
if (typeof window !== 'undefined' && window.Phaser) { if (typeof window !== 'undefined' && window.Phaser) {
let prevRequire = window.require; let prevRequire = window.require;
window.require = (x: string) => { (window as any).require = (x: string) => {
if (prevRequire) return prevRequire(x); if (prevRequire) return prevRequire(x);
else if (x === "Phaser") return window.Phaser; else if (x === "Phaser") return window.Phaser;
} }

View File

@ -1,6 +1,6 @@
{ {
"name": "@esotericsoftware/spine-phaser-v4", "name": "@esotericsoftware/spine-phaser-v4",
"version": "4.2.81", "version": "4.2.82",
"description": "The official Spine Runtimes for the Phaser v4.", "description": "The official Spine Runtimes for the Phaser v4.",
"main": "dist/index.js", "main": "dist/index.js",
"types": "dist/index.d.ts", "types": "dist/index.d.ts",
@ -31,9 +31,9 @@
}, },
"homepage": "https://github.com/esotericsoftware/spine-runtimes#readme", "homepage": "https://github.com/esotericsoftware/spine-runtimes#readme",
"dependencies": { "dependencies": {
"@esotericsoftware/spine-canvas": "4.2.81", "@esotericsoftware/spine-canvas": "4.2.82",
"@esotericsoftware/spine-core": "4.2.81", "@esotericsoftware/spine-core": "4.2.82",
"@esotericsoftware/spine-webgl": "4.2.81" "@esotericsoftware/spine-webgl": "4.2.82"
}, },
"devDependencies": { "devDependencies": {
"phaser": "^4.0.0-rc.1" "phaser": "^4.0.0-rc.1"

View File

@ -362,7 +362,10 @@ class SpineAtlasFile extends Phaser.Loader.MultiFile {
} }
} }
let basePath = file.src.match(/^.*\//) ?? ""; let basePath = (file.src.match(/^.*\//) ?? "").toString();
if (this.loader.path && this.loader.path.length > 0 && basePath.startsWith(this.loader.path))
basePath = basePath.slice(this.loader.path.length);
for (var i = 0; i < textures.length; i++) { for (var i = 0; i < textures.length; i++) {
var url = basePath + textures[i]; var url = basePath + textures[i];
var key = file.key + "!" + textures[i]; var key = file.key + "!" + textures[i];

View File

@ -27,12 +27,9 @@
* THE SPINE RUNTIMES, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. * THE SPINE RUNTIMES, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*****************************************************************************/ *****************************************************************************/
declare global {
var require: any;
}
if (typeof window !== 'undefined' && window.Phaser) { if (typeof window !== 'undefined' && window.Phaser) {
let prevRequire = window.require; let prevRequire = window.require;
window.require = (x: string) => { (window as any).require = (x: string) => {
if (prevRequire) return prevRequire(x); if (prevRequire) return prevRequire(x);
else if (x === "Phaser") return window.Phaser; else if (x === "Phaser") return window.Phaser;
} }

View File

@ -1,6 +1,6 @@
{ {
"name": "@esotericsoftware/spine-pixi-v7", "name": "@esotericsoftware/spine-pixi-v7",
"version": "4.2.81", "version": "4.2.82",
"description": "The official Spine Runtimes for the web PixiJS v7.", "description": "The official Spine Runtimes for the web PixiJS v7.",
"main": "dist/index.js", "main": "dist/index.js",
"types": "dist/index.d.ts", "types": "dist/index.d.ts",
@ -31,7 +31,7 @@
}, },
"homepage": "https://github.com/esotericsoftware/spine-runtimes#readme", "homepage": "https://github.com/esotericsoftware/spine-runtimes#readme",
"dependencies": { "dependencies": {
"@esotericsoftware/spine-core": "4.2.81" "@esotericsoftware/spine-core": "4.2.82"
}, },
"peerDependencies": { "peerDependencies": {
"@pixi/core": "^7.2.4", "@pixi/core": "^7.2.4",

View File

@ -27,16 +27,11 @@
* THE SPINE RUNTIMES, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. * THE SPINE RUNTIMES, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*****************************************************************************/ *****************************************************************************/
declare global { if (typeof window !== 'undefined' && (window as any).PIXI) {
var require: any;
var PIXI: any;
}
if (typeof window !== 'undefined' && window.PIXI) {
let prevRequire = window.require; let prevRequire = window.require;
window.require = (x: string) => { (window as any).require = (x: string) => {
if (prevRequire) return prevRequire(x); if (prevRequire) return prevRequire(x);
else if (x.startsWith("@pixi/")) return window.PIXI; else if (x.startsWith("@pixi/")) return (window as any).PIXI;
} }
} }

View File

@ -1,6 +1,6 @@
{ {
"name": "@esotericsoftware/spine-pixi-v8", "name": "@esotericsoftware/spine-pixi-v8",
"version": "4.2.81", "version": "4.2.82",
"description": "The official Spine Runtimes for PixiJS v8.", "description": "The official Spine Runtimes for PixiJS v8.",
"main": "dist/index.js", "main": "dist/index.js",
"types": "dist/index.d.ts", "types": "dist/index.d.ts",
@ -31,7 +31,7 @@
}, },
"homepage": "https://github.com/esotericsoftware/spine-runtimes#readme", "homepage": "https://github.com/esotericsoftware/spine-runtimes#readme",
"dependencies": { "dependencies": {
"@esotericsoftware/spine-core": "4.2.81" "@esotericsoftware/spine-core": "4.2.82"
}, },
"peerDependencies": { "peerDependencies": {
"pixi.js": "^8.4.0" "pixi.js": "^8.4.0"

View File

@ -27,16 +27,11 @@
* THE SPINE RUNTIMES, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. * THE SPINE RUNTIMES, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*****************************************************************************/ *****************************************************************************/
declare global { if (typeof window !== 'undefined' && (window as any).PIXI) {
var require: any;
var PIXI: any;
}
if (typeof window !== 'undefined' && window.PIXI) {
const prevRequire = window.require; const prevRequire = window.require;
(window as any).require = (x: string) => { (window as any).require = (x: string) => {
if (prevRequire) return prevRequire(x); if (prevRequire) return prevRequire(x);
else if (x.startsWith("@pixi/") || x.startsWith("pixi.js")) return window.PIXI; else if (x.startsWith("@pixi/") || x.startsWith("pixi.js")) return (window as any).PIXI;
}; };
} }

View File

@ -1,6 +1,6 @@
{ {
"name": "@esotericsoftware/spine-player", "name": "@esotericsoftware/spine-player",
"version": "4.2.81", "version": "4.2.82",
"description": "The official Spine Runtimes for the web.", "description": "The official Spine Runtimes for the web.",
"main": "dist/index.js", "main": "dist/index.js",
"types": "dist/index.d.ts", "types": "dist/index.d.ts",
@ -31,6 +31,6 @@
}, },
"homepage": "https://github.com/esotericsoftware/spine-runtimes#readme", "homepage": "https://github.com/esotericsoftware/spine-runtimes#readme",
"dependencies": { "dependencies": {
"@esotericsoftware/spine-webgl": "4.2.81" "@esotericsoftware/spine-webgl": "4.2.82"
} }
} }

View File

@ -151,6 +151,9 @@ export interface SpinePlayerConfig {
filter settings from the texture atlas are used. Default: true */ filter settings from the texture atlas are used. Default: true */
mipmaps?: boolean mipmaps?: boolean
/* Optional: Whether the player responds to user click/touch (play/pause, or control bones). Default: true */
interactive?: boolean
/* Optional: List of bone names that the user can drag to position. Default: none */ /* Optional: List of bone names that the user can drag to position. Default: none */
controlBones?: string[] controlBones?: string[]
@ -239,6 +242,7 @@ export class SpinePlayer implements Disposable {
private previousViewport: Viewport = {} as Viewport; private previousViewport: Viewport = {} as Viewport;
private viewportTransitionStart = 0; private viewportTransitionStart = 0;
private eventListeners: Array<{ target: any, event: any, func: any }> = []; private eventListeners: Array<{ target: any, event: any, func: any }> = [];
private input?: Input;
constructor (parent: HTMLElement | string, private config: SpinePlayerConfig) { constructor (parent: HTMLElement | string, private config: SpinePlayerConfig) {
let parentDom = typeof parent === "string" ? document.getElementById(parent) : parent; let parentDom = typeof parent === "string" ? document.getElementById(parent) : parent;
@ -286,6 +290,7 @@ export class SpinePlayer implements Disposable {
var eventListener = this.eventListeners[i]; var eventListener = this.eventListeners[i];
eventListener.target.removeEventListener(eventListener.event, eventListener.func); eventListener.target.removeEventListener(eventListener.event, eventListener.func);
} }
this.input?.dispose();
this.parent.removeChild(this.dom); this.parent.removeChild(this.dom);
this.disposed = true; this.disposed = true;
} }
@ -312,6 +317,7 @@ export class SpinePlayer implements Disposable {
if (config.premultipliedAlpha === void 0) config.premultipliedAlpha = true; if (config.premultipliedAlpha === void 0) config.premultipliedAlpha = true;
if (config.preserveDrawingBuffer === void 0) config.preserveDrawingBuffer = false; if (config.preserveDrawingBuffer === void 0) config.preserveDrawingBuffer = false;
if (config.mipmaps === void 0) config.mipmaps = true; if (config.mipmaps === void 0) config.mipmaps = true;
if (config.interactive === void 0) config.interactive = true;
if (!config.debug) config.debug = { if (!config.debug) config.debug = {
bones: false, bones: false,
clipping: false, clipping: false,
@ -592,57 +598,61 @@ export class SpinePlayer implements Disposable {
let skeleton = this.skeleton!; let skeleton = this.skeleton!;
let renderer = this.sceneRenderer!; let renderer = this.sceneRenderer!;
let closest = function (x: number, y: number): Bone | null { if (config.interactive) {
mouse.set(x, canvas.clientHeight - y, 0) let closest = function (x: number, y: number): Bone | null {
offset.x = offset.y = 0; mouse.set(x, canvas.clientHeight - y, 0)
let bestDistance = 24, index = 0; offset.x = offset.y = 0;
let best: Bone | null = null; let bestDistance = 24, index = 0;
for (let i = 0; i < controlBones.length; i++) { let best: Bone | null = null;
selectedBones[i] = null; for (let i = 0; i < controlBones.length; i++) {
let bone = skeleton.findBone(controlBones[i]); selectedBones[i] = null;
if (!bone) continue; let bone = skeleton.findBone(controlBones[i]);
let distance = renderer.camera.worldToScreen( if (!bone) continue;
coords.set(bone.worldX, bone.worldY, 0), let distance = renderer.camera.worldToScreen(
canvas.clientWidth, canvas.clientHeight).distance(mouse); coords.set(bone.worldX, bone.worldY, 0),
if (distance < bestDistance) { canvas.clientWidth, canvas.clientHeight).distance(mouse);
bestDistance = distance; if (distance < bestDistance) {
best = bone; bestDistance = distance;
index = i; best = bone;
offset.x = coords.x - mouse.x; index = i;
offset.y = coords.y - mouse.y; offset.x = coords.x - mouse.x;
} offset.y = coords.y - mouse.y;
}
if (best) selectedBones[index] = best;
return best;
};
new Input(canvas).addListener({
down: (x, y) => {
target = closest(x, y);
},
up: () => {
if (target)
target = null;
else if (config.showControls)
(this.paused ? this.play() : this.pause());
},
dragged: (x, y) => {
if (target) {
x = MathUtils.clamp(x + offset.x, 0, canvas.clientWidth)
y = MathUtils.clamp(y - offset.y, 0, canvas.clientHeight);
renderer.camera.screenToWorld(coords.set(x, y, 0), canvas.clientWidth, canvas.clientHeight);
if (target.parent) {
target.parent.worldToLocal(position.set(coords.x - skeleton.x, coords.y - skeleton.y));
target.x = position.x;
target.y = position.y;
} else {
target.x = coords.x - skeleton.x;
target.y = coords.y - skeleton.y;
} }
} }
}, if (best) selectedBones[index] = best;
moved: (x, y) => closest(x, y) return best;
}); };
this.input = new Input(canvas);
this.input.addListener({
down: (x, y) => {
target = closest(x, y);
},
up: () => {
if (target)
target = null;
else if (config.showControls)
(this.paused ? this.play() : this.pause());
},
dragged: (x, y) => {
if (target) {
x = MathUtils.clamp(x + offset.x, 0, canvas.clientWidth)
y = MathUtils.clamp(y - offset.y, 0, canvas.clientHeight);
renderer.camera.screenToWorld(coords.set(x, y, 0), canvas.clientWidth, canvas.clientHeight);
if (target.parent) {
target.parent.worldToLocal(position.set(coords.x - skeleton.x, coords.y - skeleton.y));
target.x = position.x;
target.y = position.y;
} else {
target.x = coords.x - skeleton.x;
target.y = coords.y - skeleton.y;
}
}
},
moved: (x, y) => closest(x, y)
});
}
if (config.showControls) { if (config.showControls) {
// For manual hover to work, we need to disable hidding controls if the mouse/touch entered the clickable area of a child of the controls. // For manual hover to work, we need to disable hidding controls if the mouse/touch entered the clickable area of a child of the controls.

View File

@ -1,6 +1,6 @@
{ {
"name": "@esotericsoftware/spine-threejs", "name": "@esotericsoftware/spine-threejs",
"version": "4.2.81", "version": "4.2.82",
"description": "The official Spine Runtimes for the web.", "description": "The official Spine Runtimes for the web.",
"main": "dist/index.js", "main": "dist/index.js",
"types": "dist/index.d.ts", "types": "dist/index.d.ts",
@ -31,7 +31,7 @@
}, },
"homepage": "https://github.com/esotericsoftware/spine-runtimes#readme", "homepage": "https://github.com/esotericsoftware/spine-runtimes#readme",
"dependencies": { "dependencies": {
"@esotericsoftware/spine-core": "4.2.81" "@esotericsoftware/spine-core": "4.2.82"
}, },
"devDependencies": { "devDependencies": {
"@types/three": "0.162.0" "@types/three": "0.162.0"

View File

@ -27,16 +27,11 @@
* THE SPINE RUNTIMES, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. * THE SPINE RUNTIMES, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*****************************************************************************/ *****************************************************************************/
declare global { if (typeof window !== 'undefined' && (window as any).THREE) {
var require: any;
var THREE: any;
}
if (typeof window !== 'undefined' && window.THREE) {
let prevRequire = window.require; let prevRequire = window.require;
window.require = (x: string) => { (window as any).require = (x: string) => {
if (prevRequire) return prevRequire(x); if (prevRequire) return prevRequire(x);
else if (x === "three") return window.THREE; else if (x === "three") return (window as any).THREE;
} }
} }

View File

@ -231,11 +231,11 @@
></spine-skeleton> ></spine-skeleton>
</div> </div>
<div class="split-right"> <div class="split-right">
If you want to preserve the original scale, you can use <code>fit="none"</code>. If you want to preserve the original scale, you can use <code>fit="none"</code> (center the bounds) or <code>fit="origin"</code> (center the skeleton origin).
In combination with that, you can use the <code>scale</code> attribute to set your desired scale. In combination with that, you can use the <code>scale</code> attribute to set your desired scale.
<br> <br>
<br> <br>
Other fit modes are <code>width</code>, <code>height</code>, <code>cover</code>, and <code>scaleDown</code>. Other fit modes are <code>width</code>, <code>height</code>, <code>cover</code>, <code>scaleDown</code>..
</div> </div>
</div> </div>
@ -280,12 +280,20 @@
<div class="split-top split"> <div class="split-top split">
<div class="split-left"> <div class="split-left">
<style>
.custom-class {
width: 150px;
height: 150px;
border: 1px solid green;
border-radius: 10px;
box-shadow: -5px 5px 3px rgba(255, 0, 0, 0.3);
}
</style>
<spine-skeleton <spine-skeleton
atlas="/assets/spineboy-pma.atlas" atlas="/assets/spineboy-pma.atlas"
skeleton="/assets/spineboy-pro.skel" skeleton="/assets/spineboy-pro.skel"
animation="walk" animation="walk"
height="150" class="custom-class"
width="150"
></spine-skeleton> ></spine-skeleton>
<spine-skeleton <spine-skeleton
atlas="/assets/spineboy-pma.atlas" atlas="/assets/spineboy-pma.atlas"
@ -301,10 +309,9 @@
></spine-skeleton> ></spine-skeleton>
</div> </div>
<div class="split-right"> <div class="split-right">
If you want to manually size the Spine widget, specify the <code>width</code> and <code>height</code> attributes in pixels (without the "px" unit). By default, the widget occupy zero width and height.
If you want to manually size the Spine widget, you can style the component using the <code>style</code> or <code>class</code> attribute, which provides more styling options.
<br> <br>
<br>
If you prefer, you can style the component using the <code>style</code> attribute, which provides more styling options.
</div> </div>
</div> </div>
@ -356,11 +363,6 @@
<div class="split-top split"> <div class="split-top split">
<div class="split-left"> <div class="split-left">
The <code>origin</code> mode centers the animation's world origin with the center of the HTML element.
<br>
You are responsible for scaling the skeleton when using this mode.
<br>
<br>
Move the origin by a percentage of the div's width and height using the <code>x-axis</code> and <code>y-axis</code> attributes, respectively. Move the origin by a percentage of the div's width and height using the <code>x-axis</code> and <code>y-axis</code> attributes, respectively.
</div> </div>
<div class="split-right"> <div class="split-right">
@ -368,7 +370,7 @@
atlas="/assets/vine-pma.atlas" atlas="/assets/vine-pma.atlas"
skeleton="/assets/vine-pro.skel" skeleton="/assets/vine-pro.skel"
animation="grow" animation="grow"
mode="origin" fit="origin"
scale=".5" scale=".5"
y-axis="-.5" y-axis="-.5"
></spine-skeleton> ></spine-skeleton>
@ -383,7 +385,7 @@
atlas="/assets/vine-pma.atlas" atlas="/assets/vine-pma.atlas"
skeleton="/assets/vine-pro.skel" skeleton="/assets/vine-pro.skel"
animation="grow" animation="grow"
mode="origin" fit="origin"
scale=".5" scale=".5"
y-axis="-.5" y-axis="-.5"
></spine-skeleton> ></spine-skeleton>
@ -765,10 +767,11 @@
<li><code>mixDuration</code>: the mix duration between this animation and the previous one (not used for the first animation on a track)</li> <li><code>mixDuration</code>: the mix duration between this animation and the previous one (not used for the first animation on a track)</li>
</ol> </ol>
<p>To loop a track once it reaches the end, add the special group <code>[loop, trackNumber]</code>, where:</p> <p>To loop a track once it reaches the end, add the special group <code>[loop, trackNumber, repeatDelay]</code>, where:</p>
<ul> <ul>
<li><code>loop</code>: identifies this as a loop instruction</li> <li><code>loop</code>: identifies this as a loop instruction</li>
<li><code>trackNumber</code>: the number of the track to loop</li> <li><code>trackNumber</code>: the number of the track to loop</li>
<li><code>repeatDelay</code>: the number of seconds to wait after the last animation is completed before repeating the loop</li>
</ul> </ul>
<p>The parameters of the first group on each track are passed to the <code>setAnimation</code> method, while the remaining groups use <code>addAnimation</code>.</p> <p>The parameters of the first group on each track are passed to the <code>setAnimation</code> method, while the remaining groups use <code>addAnimation</code>.</p>
@ -1346,7 +1349,7 @@ function removeDiv() {
Click the button below to toggle the spinner. Click the button below to toggle the spinner.
<br> <br>
<br> <br>
<input type="button" value="Spinner ON" onclick="toggleSpinner(this)"> <input type="button" value="Spinner OFF" onclick="toggleSpinner(this)">
</div> </div>
</div> </div>
@ -1355,7 +1358,6 @@ function removeDiv() {
async function reloadWidget(element) { async function reloadWidget(element) {
element.disabled = true; element.disabled = true;
await widget.whenReady; await widget.whenReady;
const skeleton = widget.skeleton;
widget.loading = true; widget.loading = true;
setTimeout(() => { setTimeout(() => {
element.disabled = false; element.disabled = false;
@ -1364,7 +1366,7 @@ function removeDiv() {
} }
function toggleSpinner(element) { function toggleSpinner(element) {
widget.noSpinner = !widget.noSpinner; widget.noSpinner = !widget.noSpinner;
element.value = widget.noSpinner ? "Spinner ON" : "Spinner OFF"; element.value = widget.noSpinner ? "Spinner OFF" : "Spinner ON";
} }
</script> </script>
@ -1420,6 +1422,8 @@ function toggleSpinner(element) {
<div class="split-left" style="width: 80%; box-sizing: border-box; min-height: 0;"> <div class="split-left" style="width: 80%; box-sizing: border-box; min-height: 0;">
It's very easy to display your different skins and animations. Simply create a table and use the <code>skin</code> and <code>animation</code> attributes. It's very easy to display your different skins and animations. Simply create a table and use the <code>skin</code> and <code>animation</code> attributes.
<br>
<code>skin</code> accepts a comma separated list of skin names. The skins will be combined in a new one, from the first to the last. If multiple skins set the same slot, the latest in the list will be used.
</div> </div>
<div class="skin-grid"> <div class="skin-grid">
@ -2042,19 +2046,16 @@ skins.forEach((skin, i) => {
<div class="split" style="width: 100%; flex-direction: column;"> <div class="split" style="width: 100%; flex-direction: column;">
<div class="split-left" style="width: 80%; box-sizing: border-box; min-height: 0;"> <div class="split-left" style="width: 80%; box-sizing: border-box; min-height: 0;">
When the widget (or the parent element) enters the viewport, the callback <code>onScreenFunction</code> is invoked. When the widget (or its parent element) enters the viewport, two things happen:<br>
<br> <ul>
<br> <li>the widget's <code>onScreenAtLeastOnce</code> property is set to <code>true</code></li>
By default, the callback does two things: <li>the widget's <code>onScreenFunction</code> callback is invoked</li>
<ul> </ul>
<li>sets <code>onScreenAtLeastOnce</code> to <code>true</code> when the widget enters the viewport for the first time</li> By default, <code>onScreenFunction</code> invokes the widget's <code>start</code> method if the widget has the <code>start-when-visible</code> attribute set, and this occurs only the first time it enters the viewport.<br>
<li>if <code>start-when-visible</code> is set, the widget's <code>start</code> method is invoked the first time the widget enters the viewport, and the assets are loaded at that moment.</li> <br>
</ul> The assets of the coin below are loaded only when the widget enters the viewport.<br>
<br> <br>
The assets of the coin below are loaded only when the widget enters the viewport. You can override the <code>onScreenFunction</code> behavior. For example, the raptor below changes its animation every time the widget enters the viewport.
<br>
<br>
You can overwrite the <code>onScreenFunction</code> behavior. For example, the raptor below changes its animation every time the widget enters the viewport.
</div> </div>
<div class="skin-grid"> <div class="skin-grid">
@ -2077,7 +2078,6 @@ skins.forEach((skin, i) => {
<script> <script>
(async () => { (async () => {
const raptorWidget = await spine.getSpineWidget("coin-with-raptor").whenReady; const raptorWidget = await spine.getSpineWidget("coin-with-raptor").whenReady;
let raptorWalking = true; let raptorWalking = true;
raptorWidget.onScreenFunction = widget => { raptorWidget.onScreenFunction = widget => {
raptorWalking = !raptorWalking; raptorWalking = !raptorWalking;
@ -2096,7 +2096,14 @@ skins.forEach((skin, i) => {
<script> <script>
escapeHTMLandInject(` escapeHTMLandInject(`
<spine-skeleton <spine-skeleton
identifier="coin" atlas="/assets/coin-pma.atlas"
skeleton="/assets/coin-pro.skel"
animation="animation"
start-when-visible
></spine-skeleton>
<spine-skeleton
identifier="coin-with-raptor"
atlas="/assets/raptor-pma.atlas" atlas="/assets/raptor-pma.atlas"
skeleton="/assets/raptor-pro.skel" skeleton="/assets/raptor-pro.skel"
animation="walk" animation="walk"
@ -2689,8 +2696,6 @@ tank.beforeUpdateWorldTransforms = (delta, skeleton, state) => {
<div class="split-left" style="overflow-y: auto; width: 100px; height: 200px; transform: translateZ(0);"> <div class="split-left" style="overflow-y: auto; width: 100px; height: 200px; transform: translateZ(0);">
<spine-overlay <spine-overlay
overlay-id="scroll" overlay-id="scroll"
scrollable
no-auto-parent-transform
overflow-top=".2" overflow-top=".2"
overflow-bottom=".2" overflow-bottom=".2"
overflow-left=".2" overflow-left=".2"
@ -3271,7 +3276,7 @@ const darkPicker = document.getElementById("dark-picker");
<li><code>followOpacity</code>: the element opacity is connected to the slot alpha</li> <li><code>followOpacity</code>: the element opacity is connected to the slot alpha</li>
<li><code>followScale</code>: the element scale is connected to the slot scale</li> <li><code>followScale</code>: the element scale is connected to the slot scale</li>
<li><code>followRotation</code>: the element rotation is connected to the slot rotation</li> <li><code>followRotation</code>: the element rotation is connected to the slot rotation</li>
<li><code>followAttachmentAttach</code>: the element is shown/hidden depending if the slot contains an attachment or not</li> <li><code>followVisibility</code>: the element is shown/hidden depending if the slot contains an attachment or not</li>
<li><code>hideAttachment</code>: the slot attachment is hidden as if the element replaced the attachment</li> <li><code>hideAttachment</code>: the slot attachment is hidden as if the element replaced the attachment</li>
</ul> </ul>
</li> </li>
@ -3296,10 +3301,10 @@ const darkPicker = document.getElementById("dark-picker");
<script> <script>
(async () => { (async () => {
const widget = await spine.getSpineWidget("potty").whenReady; const widget = await spine.getSpineWidget("potty").whenReady;
widget.followSlot("rain/rain-color", document.getElementById("rain/rain-color"), { followAttachmentAttach: false, hideAttachment: true }); widget.followSlot("rain/rain-color", document.getElementById("rain/rain-color"), { followVisibility: false, hideAttachment: true });
widget.followSlot("rain/rain-white", document.getElementById("rain/rain-white"), { followAttachmentAttach: false, hideAttachment: true }); widget.followSlot("rain/rain-white", document.getElementById("rain/rain-white"), { followVisibility: false, hideAttachment: true });
widget.followSlot("rain/rain-blue", document.getElementById("rain/rain-blue"), { followAttachmentAttach: false, hideAttachment: true }); widget.followSlot("rain/rain-blue", document.getElementById("rain/rain-blue"), { followVisibility: false, hideAttachment: true });
widget.followSlot("rain/rain-green", document.getElementById("rain/rain-green"), { followAttachmentAttach: false, hideAttachment: true }); widget.followSlot("rain/rain-green", document.getElementById("rain/rain-green"), { followVisibility: false, hideAttachment: true });
})(); })();
</script> </script>
@ -3323,10 +3328,10 @@ const darkPicker = document.getElementById("dark-picker");
(async () => { (async () => {
const widget = await spine.getSpineWidget("potty").whenReady; const widget = await spine.getSpineWidget("potty").whenReady;
widget.followSlot("rain/rain-color", document.getElementById("rain/rain-color"), { followAttachmentAttach: false, hideAttachment: true }); widget.followSlot("rain/rain-color", document.getElementById("rain/rain-color"), { followVisibility: false, hideAttachment: true });
widget.followSlot("rain/rain-white", document.getElementById("rain/rain-white"), { followAttachmentAttach: false, hideAttachment: true }); widget.followSlot("rain/rain-white", document.getElementById("rain/rain-white"), { followVisibility: false, hideAttachment: true });
widget.followSlot("rain/rain-blue", document.getElementById("rain/rain-blue"), { followAttachmentAttach: false, hideAttachment: true }); widget.followSlot("rain/rain-blue", document.getElementById("rain/rain-blue"), { followVisibility: false, hideAttachment: true });
widget.followSlot("rain/rain-green", document.getElementById("rain/rain-green"), { followAttachmentAttach: false, hideAttachment: true }); widget.followSlot("rain/rain-green", document.getElementById("rain/rain-green"), { followVisibility: false, hideAttachment: true });
})();`);</script> })();`);</script>
</code></pre> </code></pre>
</div> </div>
@ -3370,10 +3375,10 @@ const darkPicker = document.getElementById("dark-picker");
<script> <script>
(async () => { (async () => {
const widget = await spine.getSpineWidget("potty2").whenReady; const widget = await spine.getSpineWidget("potty2").whenReady;
widget.followSlot("rain/rain-color", spine.getSpineWidget("potty2-1"), { followAttachmentAttach: false, hideAttachment: true }); widget.followSlot("rain/rain-color", spine.getSpineWidget("potty2-1"), { followVisibility: false, hideAttachment: true });
widget.followSlot("rain/rain-white", spine.getSpineWidget("potty2-2"), { followAttachmentAttach: false, hideAttachment: true }); widget.followSlot("rain/rain-white", spine.getSpineWidget("potty2-2"), { followVisibility: false, hideAttachment: true });
widget.followSlot("rain/rain-blue", spine.getSpineWidget("potty2-3"), { followAttachmentAttach: false, hideAttachment: true }); widget.followSlot("rain/rain-blue", spine.getSpineWidget("potty2-3"), { followVisibility: false, hideAttachment: true });
widget.followSlot("rain/rain-green", spine.getSpineWidget("potty2-4"), { followAttachmentAttach: false, hideAttachment: true }); widget.followSlot("rain/rain-green", spine.getSpineWidget("potty2-4"), { followVisibility: false, hideAttachment: true });
})(); })();
</script> </script>
@ -3397,10 +3402,10 @@ const darkPicker = document.getElementById("dark-picker");
(async () => { (async () => {
const widget = await spine.getSpineWidget("potty2").whenReady; const widget = await spine.getSpineWidget("potty2").whenReady;
widget.followSlot("rain/rain-color", spine.getSpineWidget("potty2-1"), { followAttachmentAttach: false, hideAttachment: true }); widget.followSlot("rain/rain-color", spine.getSpineWidget("potty2-1"), { followVisibility: false, hideAttachment: true });
widget.followSlot("rain/rain-white", spine.getSpineWidget("potty2-2"), { followAttachmentAttach: false, hideAttachment: true }); widget.followSlot("rain/rain-white", spine.getSpineWidget("potty2-2"), { followVisibility: false, hideAttachment: true });
widget.followSlot("rain/rain-blue", spine.getSpineWidget("potty2-3"), { followAttachmentAttach: false, hideAttachment: true }); widget.followSlot("rain/rain-blue", spine.getSpineWidget("potty2-3"), { followVisibility: false, hideAttachment: true });
widget.followSlot("rain/rain-green", spine.getSpineWidget("potty2-4"), { followAttachmentAttach: false, hideAttachment: true }); widget.followSlot("rain/rain-green", spine.getSpineWidget("potty2-4"), { followVisibility: false, hideAttachment: true });
})();`);</script> })();`);</script>
</code></pre> </code></pre>
</div> </div>

View File

@ -1,6 +1,6 @@
{ {
"name": "@esotericsoftware/spine-webcomponents", "name": "@esotericsoftware/spine-webcomponents",
"version": "4.2.80", "version": "4.2.82",
"description": "The official Spine webcomponents.", "description": "The official Spine webcomponents.",
"main": "dist/index.js", "main": "dist/index.js",
"types": "dist/index.d.ts", "types": "dist/index.d.ts",
@ -31,6 +31,6 @@
}, },
"homepage": "https://github.com/esotericsoftware/spine-runtimes#readme", "homepage": "https://github.com/esotericsoftware/spine-runtimes#readme",
"dependencies": { "dependencies": {
"@esotericsoftware/spine-webgl": "4.2.80" "@esotericsoftware/spine-webgl": "4.2.82"
} }
} }

View File

@ -149,6 +149,8 @@ export class SpineWebComponentOverlay extends HTMLElement implements OverlayAttr
private lastCanvasBaseWidth = 0; private lastCanvasBaseWidth = 0;
private lastCanvasBaseHeight = 0; private lastCanvasBaseHeight = 0;
private zIndex?: number;
private disposed = false; private disposed = false;
private loaded = false; private loaded = false;
@ -164,13 +166,14 @@ export class SpineWebComponentOverlay extends HTMLElement implements OverlayAttr
* *
* In order to fix this behaviour, it is necessary to insert a dedicated `spine-overlay` webcomponent as a direct child of the container. * In order to fix this behaviour, it is necessary to insert a dedicated `spine-overlay` webcomponent as a direct child of the container.
* Moreover, it is necessary to perform the following actions: * Moreover, it is necessary to perform the following actions:
* 1) The scrollable container must have a `transform` css attribute. If it hasn't this attribute the `spine-overlay` will add it for you. * 1) The appendedToBody container must have a `transform` css attribute. If it hasn't this attribute the `spine-overlay` will add it for you.
* If your scrollable container has already this css attribute, or if you prefer to add it by yourself (example: `transform: translateZ(0);`), set the `no-auto-parent-transform` to the `spine-overlay`. * If your appendedToBody container has already this css attribute, or if you prefer to add it by yourself (example: `transform: translateZ(0);`), set the `no-auto-parent-transform` to the `spine-overlay`.
* 2) The `spine-overlay` must have an `overlay-id` attribute. Choose the value you prefer. * 2) The `spine-overlay` must have an `overlay-id` attribute. Choose the value you prefer.
* 3) Each `spine-skeleton` must have an `overlay-id` attribute. The same as the hosting `spine-overlay`. * 3) Each `spine-skeleton` must have an `overlay-id` attribute. The same as the hosting `spine-overlay`.
* Connected to `scrollable` attribute. * Connected to `appendedToBody` attribute.
*/ */
private appendedToBody = true; private appendedToBody = true;
private hasParentTransform = true;
readonly time = new TimeKeeper(); readonly time = new TimeKeeper();
@ -257,6 +260,7 @@ export class SpineWebComponentOverlay extends HTMLElement implements OverlayAttr
widget.onScreen = isIntersecting; widget.onScreen = isIntersecting;
if (isIntersecting) { if (isIntersecting) {
widget.onScreenFunction(widget); widget.onScreenFunction(widget);
widget.onScreenAtLeastOnce = true;
} }
} }
} }
@ -267,8 +271,10 @@ export class SpineWebComponentOverlay extends HTMLElement implements OverlayAttr
// Alternatively, we can store the body size, check the current body size in the loop (like the translateCanvas), and // Alternatively, we can store the body size, check the current body size in the loop (like the translateCanvas), and
// if they differs call the resizeCallback. I already tested it, and it works. ResizeObserver should be more efficient. // if they differs call the resizeCallback. I already tested it, and it works. ResizeObserver should be more efficient.
if (this.appendedToBody) { if (this.appendedToBody) {
// if the element is scrollable, the user does not disable translate tweak, and the parent did not have already a transform, add the tweak // if the element is appendedToBody, the user does not disable translate tweak, and the parent did not have already a transform, add the tweak
if (this.appendedToBody && !this.noAutoParentTransform && getComputedStyle(this.parentElement!).transform === "none") { if (this.hasCssTweakOff()) {
this.hasParentTransform = false;
} else {
this.parentElement!.style.transform = `translateZ(0)`; this.parentElement!.style.transform = `translateZ(0)`;
} }
this.resizeObserver = new ResizeObserver(this.resizedCallback); this.resizeObserver = new ResizeObserver(this.resizedCallback);
@ -376,6 +382,8 @@ export class SpineWebComponentOverlay extends HTMLElement implements OverlayAttr
this.parentElement!.appendChild(this); this.parentElement!.appendChild(this);
} }
} }
this.updateZIndexIfNecessary(widget);
} }
/** /**
@ -475,7 +483,7 @@ export class SpineWebComponentOverlay extends HTMLElement implements OverlayAttr
const tempVector = new Vector3(); const tempVector = new Vector3();
for (const widget of this.widgets) { for (const widget of this.widgets) {
const { skeleton, pma, bounds, mode, debug, offsetX, offsetY, xAxis, yAxis, dragX, dragY, fit, noSpinner, onScreen, loading, clip, isDraggable } = widget; const { skeleton, pma, bounds, debug, offsetX, offsetY, dragX, dragY, fit, noSpinner, loading, clip, isDraggable } = widget;
if (widget.isOffScreenAndWasMoved()) continue; if (widget.isOffScreenAndWasMoved()) continue;
const elementRef = widget.getHostElement(); const elementRef = widget.getHostElement();
@ -489,7 +497,7 @@ export class SpineWebComponentOverlay extends HTMLElement implements OverlayAttr
divBounds.y -= offsetTopForOverlay; divBounds.y -= offsetTopForOverlay;
} }
const { padLeft, padRight, padTop, padBottom } = widget const { padLeft, padRight, padTop, padBottom, xAxis, yAxis } = widget
const paddingShiftHorizontal = (padLeft - padRight) / 2; const paddingShiftHorizontal = (padLeft - padRight) / 2;
const paddingShiftVertical = (padTop - padBottom) / 2; const paddingShiftVertical = (padTop - padBottom) / 2;
@ -508,7 +516,7 @@ export class SpineWebComponentOverlay extends HTMLElement implements OverlayAttr
if (clip) startScissor(divBounds); if (clip) startScissor(divBounds);
if (loading) { if (loading) {
if (noSpinner) { if (!noSpinner) {
if (!widget.loadingScreen) widget.loadingScreen = new LoadingScreen(renderer); if (!widget.loadingScreen) widget.loadingScreen = new LoadingScreen(renderer);
widget.loadingScreen!.drawInCoordinates(divOriginX, divOriginY); widget.loadingScreen!.drawInCoordinates(divOriginX, divOriginY);
} }
@ -517,7 +525,7 @@ export class SpineWebComponentOverlay extends HTMLElement implements OverlayAttr
} }
if (skeleton) { if (skeleton) {
if (mode === "inside") { if (fit !== "origin") {
let { x: ax, y: ay, width: aw, height: ah } = bounds; let { x: ax, y: ay, width: aw, height: ah } = bounds;
if (aw <= 0 || ah <= 0) continue; if (aw <= 0 || ah <= 0) continue;
@ -583,8 +591,9 @@ export class SpineWebComponentOverlay extends HTMLElement implements OverlayAttr
} }
} }
const worldOffsetX = divOriginX + offsetX + dragX; // const worldOffsetX = divOriginX + offsetX + dragX;
const worldOffsetY = divOriginY + offsetY + dragY; const worldOffsetX = divOriginX + offsetX * window.devicePixelRatio + dragX;
const worldOffsetY = divOriginY + offsetY * window.devicePixelRatio + dragY;
widget.worldX = worldOffsetX; widget.worldX = worldOffsetX;
widget.worldY = worldOffsetY; widget.worldY = worldOffsetY;
@ -626,12 +635,10 @@ export class SpineWebComponentOverlay extends HTMLElement implements OverlayAttr
renderer.circle(true, root.x + worldOffsetX, root.y + worldOffsetY, 10, red); renderer.circle(true, root.x + worldOffsetX, root.y + worldOffsetY, 10, red);
// show shifted origin // show shifted origin
const originX = worldOffsetX - dragX - offsetX; renderer.circle(true, divOriginX, divOriginY, 10, green);
const originY = worldOffsetY - dragY - offsetY;
renderer.circle(true, originX, originY, 10, green);
// show line from origin to bounds center // show line from origin to bounds center
renderer.line(originX, originY, bbCenterX, bbCenterY, green); renderer.line(divOriginX, divOriginY, bbCenterX, bbCenterY, green);
} }
if (clip) endScissor(); if (clip) endScissor();
@ -646,7 +653,7 @@ export class SpineWebComponentOverlay extends HTMLElement implements OverlayAttr
if (widget.isOffScreenAndWasMoved() || !widget.skeleton) continue; if (widget.isOffScreenAndWasMoved() || !widget.skeleton) continue;
for (const boneFollower of widget.boneFollowerList) { for (const boneFollower of widget.boneFollowerList) {
const { slot, bone, element, followAttachmentAttach, followRotation, followOpacity, followScale } = boneFollower; const { slot, bone, element, followVisibility, followRotation, followOpacity, followScale } = boneFollower;
const { worldX, worldY } = widget; const { worldX, worldY } = widget;
this.worldToScreen(this.tempFollowBoneVector, bone.worldX + worldX, bone.worldY + worldY); this.worldToScreen(this.tempFollowBoneVector, bone.worldX + worldX, bone.worldY + worldY);
@ -667,7 +674,7 @@ export class SpineWebComponentOverlay extends HTMLElement implements OverlayAttr
element.style.display = "" element.style.display = ""
if (followAttachmentAttach && !slot.attachment) { if (followVisibility && !slot.attachment) {
element.style.opacity = "0"; element.style.opacity = "0";
} else if (followOpacity) { } else if (followOpacity) {
element.style.opacity = `${slot.color.a}`; element.style.opacity = `${slot.color.a}`;
@ -943,7 +950,7 @@ export class SpineWebComponentOverlay extends HTMLElement implements OverlayAttr
private updateWidgetScales () { private updateWidgetScales () {
for (const widget of this.widgets) { for (const widget of this.widgets) {
// inside mode scale automatically to fit the skeleton within its parent // inside mode scale automatically to fit the skeleton within its parent
if (widget.mode !== "origin" && widget.fit !== "none") continue; if (widget.fit !== "origin" && widget.fit !== "none") continue;
const skeleton = widget.skeleton; const skeleton = widget.skeleton;
if (!skeleton) continue; if (!skeleton) continue;
@ -958,6 +965,7 @@ export class SpineWebComponentOverlay extends HTMLElement implements OverlayAttr
} }
} }
// this function is invoked each frame - pay attention to what you add here
private translateCanvas () { private translateCanvas () {
let scrollPositionX = -this.overflowLeftSize; let scrollPositionX = -this.overflowLeftSize;
let scrollPositionY = -this.overflowTopSize; let scrollPositionY = -this.overflowTopSize;
@ -967,9 +975,9 @@ export class SpineWebComponentOverlay extends HTMLElement implements OverlayAttr
scrollPositionY += window.scrollY; scrollPositionY += window.scrollY;
} else { } else {
// Ideally this should be the only scrollable case (no-auto-parent-transform not enabled or at least an ancestor has transform) // Ideally this should be the only appendedToBody case (no-auto-parent-transform not enabled or at least an ancestor has transform)
// I'd like to get rid of the code below // I'd like to get rid of the else case
if (!this.hasCssTweakOff()) { if (this.hasParentTransform) {
scrollPositionX += this.parentElement!.scrollLeft; scrollPositionX += this.parentElement!.scrollLeft;
scrollPositionY += this.parentElement!.scrollTop; scrollPositionY += this.parentElement!.scrollTop;
} else { } else {
@ -979,7 +987,7 @@ export class SpineWebComponentOverlay extends HTMLElement implements OverlayAttr
let offsetParent = this.offsetParent; let offsetParent = this.offsetParent;
do { do {
if (offsetParent === document.body) break; if (offsetParent === null || offsetParent === document.body) break;
const htmlOffsetParentElement = offsetParent as HTMLElement; const htmlOffsetParentElement = offsetParent as HTMLElement;
if (htmlOffsetParentElement.style.position === "fixed" || htmlOffsetParentElement.style.position === "sticky" || htmlOffsetParentElement.style.position === "absolute") { if (htmlOffsetParentElement.style.position === "fixed" || htmlOffsetParentElement.style.position === "sticky" || htmlOffsetParentElement.style.position === "absolute") {
@ -1000,6 +1008,23 @@ export class SpineWebComponentOverlay extends HTMLElement implements OverlayAttr
this.canvas.style.transform = `translate(${scrollPositionX}px,${scrollPositionY}px)`; this.canvas.style.transform = `translate(${scrollPositionX}px,${scrollPositionY}px)`;
} }
private updateZIndexIfNecessary (element: HTMLElement) {
let parent: HTMLElement | null = element;
let zIndex: undefined | number;
do {
let currentZIndex = parseInt(getComputedStyle(parent).zIndex);
// searching the shallowest z-index
if (!isNaN(currentZIndex)) zIndex = currentZIndex;
parent = parent.parentElement;
} while (parent && parent !== document.body)
if (zIndex && (!this.zIndex || this.zIndex < zIndex)) {
this.zIndex = zIndex;
this.div.style.zIndex = `${this.zIndex}`;
}
}
/* /*
* Other utilities * Other utilities
*/ */

View File

@ -49,6 +49,7 @@ import {
RegionAttachment, RegionAttachment,
MeshAttachment, MeshAttachment,
Bone, Bone,
Skin,
} from "@esotericsoftware/spine-webgl"; } from "@esotericsoftware/spine-webgl";
import { AttributeTypes, castValue, isBase64, Rectangle } from "./wcUtils.js"; import { AttributeTypes, castValue, isBase64, Rectangle } from "./wcUtils.js";
import { SpineWebComponentOverlay } from "./SpineWebComponentOverlay.js"; import { SpineWebComponentOverlay } from "./SpineWebComponentOverlay.js";
@ -56,9 +57,12 @@ import { SpineWebComponentOverlay } from "./SpineWebComponentOverlay.js";
type UpdateSpineWidgetFunction = (delta: number, skeleton: Skeleton, state: AnimationState) => void; type UpdateSpineWidgetFunction = (delta: number, skeleton: Skeleton, state: AnimationState) => void;
export type OffScreenUpdateBehaviourType = "pause" | "update" | "pose"; export type OffScreenUpdateBehaviourType = "pause" | "update" | "pose";
export type ModeType = "inside" | "origin"; export type FitType = "fill" | "width" | "height" | "contain" | "cover" | "none" | "scaleDown" | "origin";
export type FitType = "fill" | "width" | "height" | "contain" | "cover" | "none" | "scaleDown"; export type AnimationsInfo = Record<string, {
export type AnimationsInfo = Record<string, { cycle?: boolean, animations: Array<AnimationsType> }>; cycle?: boolean,
repeatDelay?: number;
animations: Array<AnimationsType>
}>;
export type AnimationsType = { animationName: string | "#EMPTY#", loop?: boolean, delay?: number, mixDuration?: number }; export type AnimationsType = { animationName: string | "#EMPTY#", loop?: boolean, delay?: number, mixDuration?: number };
export type CursorEventType = "down" | "up" | "enter" | "leave" | "move" | "drag"; export type CursorEventType = "down" | "up" | "enter" | "leave" | "move" | "drag";
export type CursorEventTypesInput = Exclude<CursorEventType, "enter" | "leave">; export type CursorEventTypesInput = Exclude<CursorEventType, "enter" | "leave">;
@ -73,9 +77,8 @@ interface WidgetAttributes {
animation?: string animation?: string
animations?: AnimationsInfo animations?: AnimationsInfo
defaultMix?: number defaultMix?: number
skin?: string skin?: string[]
fit: FitType fit: FitType
mode: ModeType
xAxis: number xAxis: number
yAxis: number yAxis: number
offsetX: number offsetX: number
@ -217,14 +220,14 @@ export class SpineWebComponentSkeleton extends HTMLElement implements Disposable
* Optional: The name of the skin to be set * Optional: The name of the skin to be set
* Connected to `skin` attribute. * Connected to `skin` attribute.
*/ */
public get skin (): string | undefined { public get skin (): string[] | undefined {
return this._skin; return this._skin;
} }
public set skin (value: string | undefined) { public set skin (value: string[] | undefined) {
this._skin = value; this._skin = value;
this.initWidget(); this.initWidget();
} }
private _skin?: string private _skin?: string[]
/** /**
* Specify the way the skeleton is sized within the element automatically changing its `scaleX` and `scaleY`. * Specify the way the skeleton is sized within the element automatically changing its `scaleX` and `scaleY`.
@ -236,19 +239,11 @@ export class SpineWebComponentSkeleton extends HTMLElement implements Disposable
* - `cover`: as small as possible while still covering the entire element container. * - `cover`: as small as possible while still covering the entire element container.
* - `scaleDown`: scale the skeleton down to ensure that the skeleton fits within the element container. * - `scaleDown`: scale the skeleton down to ensure that the skeleton fits within the element container.
* - `none`: display the skeleton without autoscaling it. * - `none`: display the skeleton without autoscaling it.
* - `origin`: the skeleton origin is centered with the element container regardless of the bounds.
* Connected to `fit` attribute. * Connected to `fit` attribute.
*/ */
public fit: FitType = "contain"; public fit: FitType = "contain";
/**
* Specify the way the skeleton is centered within the element container:
* - `inside`: the skeleton bounds center is centered with the element container (Default)
* - `origin`: the skeleton origin is centered with the element container regardless of the bounds.
* Origin does not allow to specify any {@link fit} type and guarantee the skeleton to not be autoscaled.
* Connected to `mode` attribute.
*/
public mode: ModeType = "inside";
/** /**
* The x offset of the skeleton world origin x axis as a percentage of the element container width * The x offset of the skeleton world origin x axis as a percentage of the element container width
* Connected to `x-axis` attribute. * Connected to `x-axis` attribute.
@ -562,16 +557,11 @@ export class SpineWebComponentSkeleton extends HTMLElement implements Disposable
/** /**
* A callback invoked each time the element container enters the screen viewport. * A callback invoked each time the element container enters the screen viewport.
* By default, the callback call the {@link start} method the first time the widget * By default, the callback call the {@link start} method the first time the widget
* enters the screen viewport. * enters the screen viewport and {@link startWhenVisible} is `true`.
*/ */
public onScreenFunction: (widget: SpineWebComponentSkeleton) => void = async (widget) => { public onScreenFunction: (widget: SpineWebComponentSkeleton) => void = async (widget) => {
if (widget.loading && !widget.onScreenAtLeastOnce) { if (widget.loading && !widget.onScreenAtLeastOnce && widget.manualStart && widget.startWhenVisible)
widget.onScreenAtLeastOnce = true; widget.start()
if (widget.manualStart && widget.startWhenVisible) {
widget.start();
}
}
} }
/** /**
@ -706,7 +696,7 @@ export class SpineWebComponentSkeleton extends HTMLElement implements Disposable
animations: { propertyName: "animations", type: "animationsInfo", defaultValue: undefined }, animations: { propertyName: "animations", type: "animationsInfo", defaultValue: undefined },
"animation-bounds": { propertyName: "animationsBound", type: "array-string", defaultValue: undefined }, "animation-bounds": { propertyName: "animationsBound", type: "array-string", defaultValue: undefined },
"default-mix": { propertyName: "defaultMix", type: "number", defaultValue: 0 }, "default-mix": { propertyName: "defaultMix", type: "number", defaultValue: 0 },
skin: { propertyName: "skin", type: "string" }, skin: { propertyName: "skin", type: "array-string" },
width: { propertyName: "width", type: "number", defaultValue: -1 }, width: { propertyName: "width", type: "number", defaultValue: -1 },
height: { propertyName: "height", type: "number", defaultValue: -1 }, height: { propertyName: "height", type: "number", defaultValue: -1 },
isdraggable: { propertyName: "isDraggable", type: "boolean" }, isdraggable: { propertyName: "isDraggable", type: "boolean" },
@ -732,7 +722,6 @@ export class SpineWebComponentSkeleton extends HTMLElement implements Disposable
clip: { propertyName: "clip", type: "boolean" }, clip: { propertyName: "clip", type: "boolean" },
pages: { propertyName: "pages", type: "array-number" }, pages: { propertyName: "pages", type: "array-number" },
fit: { propertyName: "fit", type: "fitType", defaultValue: "contain" }, fit: { propertyName: "fit", type: "fitType", defaultValue: "contain" },
mode: { propertyName: "mode", type: "modeType", defaultValue: "inside" },
offscreen: { propertyName: "offScreenUpdateBehaviour", type: "offScreenUpdateBehaviourType", defaultValue: "pause" }, offscreen: { propertyName: "offScreenUpdateBehaviour", type: "offScreenUpdateBehaviourType", defaultValue: "pause" },
} }
@ -876,7 +865,7 @@ export class SpineWebComponentSkeleton extends HTMLElement implements Disposable
* @returns The `HTMLElement` where the widget is hosted. * @returns The `HTMLElement` where the widget is hosted.
*/ */
public getHostElement (): HTMLElement { public getHostElement (): HTMLElement {
return (this.width <= 0 || this.width <= 0) && !this.getAttribute("style") return (this.width <= 0 || this.width <= 0) && !this.getAttribute("style") && !this.getAttribute("class")
? this.parentElement! ? this.parentElement!
: this; : this;
} }
@ -1009,18 +998,28 @@ export class SpineWebComponentSkeleton extends HTMLElement implements Disposable
// skeleton.scaleX = this.dprScale; // skeleton.scaleX = this.dprScale;
// skeleton.scaleY = this.dprScale; // skeleton.scaleY = this.dprScale;
this.loading = false;
// the bounds are calculated the first time, if no custom bound is provided // the bounds are calculated the first time, if no custom bound is provided
this.initWidget(this.bounds.width <= 0 || this.bounds.height <= 0); this.initWidget(this.bounds.width <= 0 || this.bounds.height <= 0);
this.loading = false;
return this; return this;
} }
private initWidget (forceRecalculate = false) { private initWidget (forceRecalculate = false) {
if (this.loading) return;
const { skeleton, state, animation, animations: animationsInfo, skin, defaultMix } = this; const { skeleton, state, animation, animations: animationsInfo, skin, defaultMix } = this;
if (skin) { if (skin) {
skeleton?.setSkinByName(skin); if (skin.length === 1) {
skeleton?.setSkinByName(skin[0]);
} else {
const customSkin = new Skin("custom");
for (const s of skin) customSkin.addSkin(skeleton?.data.findSkin(s) as Skin);
skeleton?.setSkin(customSkin);
}
skeleton?.setSlotsToSetupPose(); skeleton?.setSlotsToSetupPose();
} }
@ -1028,7 +1027,7 @@ export class SpineWebComponentSkeleton extends HTMLElement implements Disposable
state.data.defaultMix = defaultMix; state.data.defaultMix = defaultMix;
if (animationsInfo) { if (animationsInfo) {
for (const [trackIndexString, { cycle, animations }] of Object.entries(animationsInfo)) { for (const [trackIndexString, { cycle, animations, repeatDelay }] of Object.entries(animationsInfo)) {
const cycleFn = () => { const cycleFn = () => {
const trackIndex = Number(trackIndexString); const trackIndex = Number(trackIndexString);
for (const [index, { animationName, delay, loop, mixDuration }] of animations.entries()) { for (const [index, { animationName, delay, loop, mixDuration }] of animations.entries()) {
@ -1050,7 +1049,15 @@ export class SpineWebComponentSkeleton extends HTMLElement implements Disposable
if (mixDuration) track.mixDuration = mixDuration; if (mixDuration) track.mixDuration = mixDuration;
if (cycle && index === animations.length - 1) { if (cycle && index === animations.length - 1) {
track.listener = { complete: () => cycleFn() }; track.listener = {
complete: () => {
if (repeatDelay)
setTimeout(() => cycleFn(), 1000 * repeatDelay);
else
cycleFn();
delete track.listener?.complete;
}
};
}; };
} }
} }
@ -1068,22 +1075,13 @@ export class SpineWebComponentSkeleton extends HTMLElement implements Disposable
} }
private render (): void { private render (): void {
let width; let noSize = (!this.getAttribute("style") && !this.getAttribute("class"));
let height;
if (this.width === -1 || this.height === -1) {
width = "0";
height = "0";
} else {
width = `${this.width}px`
height = `${this.height}px`
}
this.root.innerHTML = ` this.root.innerHTML = `
<style> <style>
:host { :host {
position: relative; position: relative;
display: inline-block; display: inline-block;
width: ${width}; ${noSize ? "width: 0; height: 0;" : ""}
height: ${height};
} }
</style> </style>
`; `;
@ -1233,10 +1231,10 @@ export class SpineWebComponentSkeleton extends HTMLElement implements Disposable
* Other utilities * Other utilities
*/ */
public boneFollowerList: Array<{ slot: Slot, bone: Bone, element: HTMLElement, followAttachmentAttach: boolean, followRotation: boolean, followOpacity: boolean, followScale: boolean, hideAttachment: boolean }> = []; public boneFollowerList: Array<{ slot: Slot, bone: Bone, element: HTMLElement, followVisibility: boolean, followRotation: boolean, followOpacity: boolean, followScale: boolean, hideAttachment: boolean }> = [];
public followSlot (slotName: string | Slot, element: HTMLElement, options: { followAttachmentAttach?: boolean, followRotation?: boolean, followOpacity?: boolean, followScale?: boolean, hideAttachment?: boolean } = {}) { public followSlot (slotName: string | Slot, element: HTMLElement, options: { followVisibility?: boolean, followRotation?: boolean, followOpacity?: boolean, followScale?: boolean, hideAttachment?: boolean } = {}) {
const { const {
followAttachmentAttach = false, followVisibility = false,
followRotation = true, followRotation = true,
followOpacity = true, followOpacity = true,
followScale = true, followScale = true,
@ -1255,7 +1253,7 @@ export class SpineWebComponentSkeleton extends HTMLElement implements Disposable
element.style.left = '0px'; element.style.left = '0px';
element.style.display = 'none'; element.style.display = 'none';
this.boneFollowerList.push({ slot, bone: slot.bone, element, followAttachmentAttach, followRotation, followOpacity, followScale, hideAttachment }); this.boneFollowerList.push({ slot, bone: slot.bone, element, followVisibility, followRotation, followOpacity, followScale, hideAttachment });
this.overlay.addSlotFollowerElement(element); this.overlay.addSlotFollowerElement(element);
} }
public unfollowSlot (element: HTMLElement): HTMLElement | undefined { public unfollowSlot (element: HTMLElement): HTMLElement | undefined {

View File

@ -27,10 +27,10 @@
* SPINE RUNTIMES, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. * SPINE RUNTIMES, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*****************************************************************************/ *****************************************************************************/
import { AnimationsInfo, FitType, ModeType, OffScreenUpdateBehaviourType } from "./SpineWebComponentSkeleton.js"; import { AnimationsInfo, FitType, OffScreenUpdateBehaviourType } from "./SpineWebComponentSkeleton.js";
const animatonTypeRegExp = /\[([^\]]+)\]/g; const animatonTypeRegExp = /\[([^\]]+)\]/g;
export type AttributeTypes = "string" | "number" | "boolean" | "array-number" | "array-string" | "object" | "fitType" | "modeType" | "offScreenUpdateBehaviourType" | "animationsInfo"; export type AttributeTypes = "string" | "number" | "boolean" | "array-number" | "array-string" | "object" | "fitType" | "offScreenUpdateBehaviourType" | "animationsInfo";
export function castValue (type: AttributeTypes, value: string | null, defaultValue?: any) { export function castValue (type: AttributeTypes, value: string | null, defaultValue?: any) {
switch (type) { switch (type) {
@ -48,8 +48,6 @@ export function castValue (type: AttributeTypes, value: string | null, defaultVa
return castObject(value, defaultValue); return castObject(value, defaultValue);
case "fitType": case "fitType":
return isFitType(value) ? value : defaultValue; return isFitType(value) ? value : defaultValue;
case "modeType":
return isModeType(value) ? value : defaultValue;
case "offScreenUpdateBehaviourType": case "offScreenUpdateBehaviourType":
return isOffScreenUpdateBehaviourType(value) ? value : defaultValue; return isOffScreenUpdateBehaviourType(value) ? value : defaultValue;
case "animationsInfo": case "animationsInfo":
@ -104,7 +102,7 @@ function castToAnimationsInfo (value: string | null): AnimationsInfo | undefined
if (!matches) return undefined; if (!matches) return undefined;
return matches.reduce((obj, group) => { return matches.reduce((obj, group) => {
const [trackIndexStringOrLoopDefinition, animationNameOrTrackIndexStringCycle, loop, delayString, mixDurationString] = group.slice(1, -1).split(',').map(v => v.trim()); const [trackIndexStringOrLoopDefinition, animationNameOrTrackIndexStringCycle, loopOrRepeatDelay, delayString, mixDurationString] = group.slice(1, -1).split(',').map(v => v.trim());
if (trackIndexStringOrLoopDefinition === "loop") { if (trackIndexStringOrLoopDefinition === "loop") {
if (!Number.isInteger(Number(animationNameOrTrackIndexStringCycle))) { if (!Number.isInteger(Number(animationNameOrTrackIndexStringCycle))) {
@ -112,6 +110,15 @@ function castToAnimationsInfo (value: string | null): AnimationsInfo | undefined
} }
const animationInfoObject = obj[animationNameOrTrackIndexStringCycle] ||= { animations: [] }; const animationInfoObject = obj[animationNameOrTrackIndexStringCycle] ||= { animations: [] };
animationInfoObject.cycle = true; animationInfoObject.cycle = true;
if (loopOrRepeatDelay !== undefined) {
const repeatDelay = Number(loopOrRepeatDelay);
if (Number.isNaN(repeatDelay)) {
throw new Error(`If present, duration of last animation of cycle in ${group} must be a positive integer number, instead it is ${loopOrRepeatDelay}. Original value: ${value}`);
}
animationInfoObject.repeatDelay = repeatDelay;
}
return obj; return obj;
} }
@ -139,7 +146,7 @@ function castToAnimationsInfo (value: string | null): AnimationsInfo | undefined
const animationInfoObject = obj[trackIndexStringOrLoopDefinition] ||= { animations: [] }; const animationInfoObject = obj[trackIndexStringOrLoopDefinition] ||= { animations: [] };
animationInfoObject.animations.push({ animationInfoObject.animations.push({
animationName: animationNameOrTrackIndexStringCycle, animationName: animationNameOrTrackIndexStringCycle,
loop: loop.trim().toLowerCase() === "true", loop: (loopOrRepeatDelay || "").trim().toLowerCase() === "true",
delay, delay,
mixDuration, mixDuration,
}); });
@ -155,7 +162,8 @@ function isFitType (value: string | null): value is FitType {
value === "contain" || value === "contain" ||
value === "cover" || value === "cover" ||
value === "none" || value === "none" ||
value === "scaleDown" value === "scaleDown" ||
value === "origin"
); );
} }
@ -167,12 +175,6 @@ function isOffScreenUpdateBehaviourType (value: string | null): value is OffScre
); );
} }
function isModeType (value: string | null): value is ModeType {
return (
value === "inside" ||
value === "origin"
);
}
const base64RegExp = /^(([A-Za-z0-9+/]{4})*([A-Za-z0-9+/]{4}|[A-Za-z0-9+/]{3}=|[A-Za-z0-9+/]{2}==))$/; const base64RegExp = /^(([A-Za-z0-9+/]{4})*([A-Za-z0-9+/]{4}|[A-Za-z0-9+/]{3}=|[A-Za-z0-9+/]{2}==))$/;
export function isBase64 (str: string) { export function isBase64 (str: string) {
return base64RegExp.test(str); return base64RegExp.test(str);

View File

@ -1,6 +1,6 @@
{ {
"name": "@esotericsoftware/spine-webgl", "name": "@esotericsoftware/spine-webgl",
"version": "4.2.81", "version": "4.2.82",
"description": "The official Spine Runtimes for the web.", "description": "The official Spine Runtimes for the web.",
"main": "dist/index.js", "main": "dist/index.js",
"types": "dist/index.d.ts", "types": "dist/index.d.ts",
@ -31,6 +31,6 @@
}, },
"homepage": "https://github.com/esotericsoftware/spine-runtimes#readme", "homepage": "https://github.com/esotericsoftware/spine-runtimes#readme",
"dependencies": { "dependencies": {
"@esotericsoftware/spine-core": "4.2.81" "@esotericsoftware/spine-core": "4.2.82"
} }
} }

View File

@ -83,6 +83,8 @@ namespace Spine.Unity.Editor {
} }
public static class SpineBuildEnvUtility { public static class SpineBuildEnvUtility {
public const string SPINE_ALLOW_UNSAFE_CODE = "SPINE_ALLOW_UNSAFE";
static bool IsInvalidGroup (BuildTargetGroup group) { static bool IsInvalidGroup (BuildTargetGroup group) {
int gi = (int)group; int gi = (int)group;
return return
@ -99,15 +101,18 @@ namespace Spine.Unity.Editor {
if (IsInvalidGroup(group)) if (IsInvalidGroup(group))
continue; continue;
string defines = PlayerSettings.GetScriptingDefineSymbolsForGroup(group); try {
if (!defines.Contains(define)) { string defines = PlayerSettings.GetScriptingDefineSymbolsForGroup(group);
wasDefineAdded = true; if (!defines.Contains(define)) {
if (defines.EndsWith(";", System.StringComparison.Ordinal)) wasDefineAdded = true;
defines += define; if (defines.EndsWith(";", System.StringComparison.Ordinal))
else defines += define;
defines += ";" + define; else
defines += ";" + define;
PlayerSettings.SetScriptingDefineSymbolsForGroup(group, defines); PlayerSettings.SetScriptingDefineSymbolsForGroup(group, defines);
}
} catch (System.Exception) {
} }
} }
Debug.LogWarning("Please ignore errors \"PlayerSettings Validation: Requested build target group doesn't exist\" above"); Debug.LogWarning("Please ignore errors \"PlayerSettings Validation: Requested build target group doesn't exist\" above");
@ -127,15 +132,18 @@ namespace Spine.Unity.Editor {
if (IsInvalidGroup(group)) if (IsInvalidGroup(group))
continue; continue;
string defines = PlayerSettings.GetScriptingDefineSymbolsForGroup(group); try {
if (defines.Contains(define)) { string defines = PlayerSettings.GetScriptingDefineSymbolsForGroup(group);
wasDefineRemoved = true; if (defines.Contains(define)) {
if (defines.Contains(define + ";")) wasDefineRemoved = true;
defines = defines.Replace(define + ";", ""); if (defines.Contains(define + ";"))
else defines = defines.Replace(define + ";", "");
defines = defines.Replace(define, ""); else
defines = defines.Replace(define, "");
PlayerSettings.SetScriptingDefineSymbolsForGroup(group, defines); PlayerSettings.SetScriptingDefineSymbolsForGroup(group, defines);
}
} catch (System.Exception) {
} }
} }

View File

@ -39,6 +39,14 @@
#define HAS_ON_POSTPROCESS_PREFAB #define HAS_ON_POSTPROCESS_PREFAB
#endif #endif
#if UNITY_2021_2_OR_NEWER
#define TEXT_ASSET_HAS_GET_DATA_BYTES
#endif
#if TEXT_ASSET_HAS_GET_DATA_BYTES
#define HAS_ANY_UNSAFE_OPTIONS
#endif
using System.Threading; using System.Threading;
using UnityEditor; using UnityEditor;
using UnityEngine; using UnityEngine;
@ -356,6 +364,18 @@ namespace Spine.Unity.Editor {
} }
#endif #endif
#if HAS_ANY_UNSAFE_OPTIONS
GUILayout.Space(20);
EditorGUILayout.LabelField("Unsafe Build Defines", EditorStyles.boldLabel);
using (new GUILayout.HorizontalScope()) {
EditorGUILayout.PrefixLabel(new GUIContent("Direct data access", "Allow unsafe direct data access. Currently affects reading .skel.bytes files, reading with fewer allocations."));
if (GUILayout.Button("Enable", GUILayout.Width(64)))
SpineBuildEnvUtility.EnableBuildDefine(SpineBuildEnvUtility.SPINE_ALLOW_UNSAFE_CODE);
if (GUILayout.Button("Disable", GUILayout.Width(64)))
SpineBuildEnvUtility.DisableBuildDefine(SpineBuildEnvUtility.SPINE_ALLOW_UNSAFE_CODE);
}
#endif
#if SPINE_TK2D_DEFINE #if SPINE_TK2D_DEFINE
bool isTK2DDefineSet = true; bool isTK2DDefineSet = true;
#else #else

View File

@ -1,4 +1,5 @@
{ {
"name": "spine-unity", "name": "spine-unity",
"references": [ "spine-csharp" ] "references": [ "spine-csharp" ],
"allowUnsafeCode": true
} }

View File

@ -27,14 +27,39 @@
* THE SPINE RUNTIMES, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. * THE SPINE RUNTIMES, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*****************************************************************************/ *****************************************************************************/
//#define SPINE_ALLOW_UNSAFE // note: this define can be set via Edit - Preferences - Spine.
#if UNITY_2021_2_OR_NEWER
#define TEXT_ASSET_HAS_GET_DATA_BYTES
#endif
#if SPINE_ALLOW_UNSAFE && TEXT_ASSET_HAS_GET_DATA_BYTES
#define UNSAFE_DIRECT_ACCESS_TEXT_ASSET_DATA
#endif
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.IO; using System.IO;
#if UNSAFE_DIRECT_ACCESS_TEXT_ASSET_DATA
using Unity.Collections;
#endif
using UnityEngine; using UnityEngine;
using CompatibilityProblemInfo = Spine.Unity.SkeletonDataCompatibility.CompatibilityProblemInfo; using CompatibilityProblemInfo = Spine.Unity.SkeletonDataCompatibility.CompatibilityProblemInfo;
namespace Spine.Unity { namespace Spine.Unity {
#if UNSAFE_DIRECT_ACCESS_TEXT_ASSET_DATA
public static class TextAssetExtensions {
public static Stream GetStreamUnsafe (this TextAsset textAsset) {
NativeArray<byte> dataNativeArray = textAsset.GetData<byte>();
return dataNativeArray.GetUnmanagedMemoryStream();
}
public static unsafe UnmanagedMemoryStream GetUnmanagedMemoryStream<T> (this NativeArray<T> nativeArray) where T : struct {
return new UnmanagedMemoryStream((byte*)global::Unity.Collections.LowLevel.Unsafe.
NativeArrayUnsafeUtility.GetUnsafeReadOnlyPtr(nativeArray), nativeArray.Length);
}
}
#endif
[CreateAssetMenu(fileName = "New SkeletonDataAsset", menuName = "Spine/SkeletonData Asset")] [CreateAssetMenu(fileName = "New SkeletonDataAsset", menuName = "Spine/SkeletonData Asset")]
public class SkeletonDataAsset : ScriptableObject { public class SkeletonDataAsset : ScriptableObject {
@ -188,9 +213,15 @@ namespace Spine.Unity {
SkeletonData loadedSkeletonData = null; SkeletonData loadedSkeletonData = null;
try { try {
if (hasBinaryExtension) if (hasBinaryExtension) {
#if UNSAFE_DIRECT_ACCESS_TEXT_ASSET_DATA
using (Stream stream = skeletonJSON.GetStreamUnsafe()) {
loadedSkeletonData = SkeletonDataAsset.ReadSkeletonData(stream, attachmentLoader, skeletonDataScale);
}
#else
loadedSkeletonData = SkeletonDataAsset.ReadSkeletonData(skeletonJSON.bytes, attachmentLoader, skeletonDataScale); loadedSkeletonData = SkeletonDataAsset.ReadSkeletonData(skeletonJSON.bytes, attachmentLoader, skeletonDataScale);
else #endif
} else
loadedSkeletonData = SkeletonDataAsset.ReadSkeletonData(skeletonJSON.text, attachmentLoader, skeletonDataScale); loadedSkeletonData = SkeletonDataAsset.ReadSkeletonData(skeletonJSON.text, attachmentLoader, skeletonDataScale);
} catch (Exception ex) { } catch (Exception ex) {
if (!quiet) if (!quiet)
@ -287,6 +318,13 @@ namespace Spine.Unity {
} }
} }
internal static SkeletonData ReadSkeletonData (Stream assetStream, AttachmentLoader attachmentLoader, float scale) {
SkeletonBinary binary = new SkeletonBinary(attachmentLoader) {
Scale = scale
};
return binary.ReadSkeletonData(assetStream);
}
internal static SkeletonData ReadSkeletonData (string text, AttachmentLoader attachmentLoader, float scale) { internal static SkeletonData ReadSkeletonData (string text, AttachmentLoader attachmentLoader, float scale) {
StringReader input = new StringReader(text); StringReader input = new StringReader(text);
SkeletonJson json = new SkeletonJson(attachmentLoader) { SkeletonJson json = new SkeletonJson(attachmentLoader) {

View File

@ -27,6 +27,16 @@
* THE SPINE RUNTIMES, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. * THE SPINE RUNTIMES, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*****************************************************************************/ *****************************************************************************/
//#define SPINE_ALLOW_UNSAFE // note: this define can be set via Edit - Preferences - Spine.
#if UNITY_2021_2_OR_NEWER
#define TEXT_ASSET_HAS_GET_DATA_BYTES
#endif
#if SPINE_ALLOW_UNSAFE && TEXT_ASSET_HAS_GET_DATA_BYTES
#define UNSAFE_DIRECT_ACCESS_TEXT_ASSET_DATA
#endif
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.IO; using System.IO;
@ -106,8 +116,12 @@ namespace Spine.Unity {
if (fileVersion.sourceType == SourceType.Binary) { if (fileVersion.sourceType == SourceType.Binary) {
try { try {
using (MemoryStream memStream = new MemoryStream(asset.bytes)) { #if UNSAFE_DIRECT_ACCESS_TEXT_ASSET_DATA
fileVersion.rawVersion = SkeletonBinary.GetVersionString(memStream); using (Stream stream = asset.GetStreamUnsafe()) {
#else
using (MemoryStream stream = new MemoryStream(asset.bytes)) {
#endif
fileVersion.rawVersion = SkeletonBinary.GetVersionString(stream);
} }
} catch (System.Exception e) { } catch (System.Exception e) {
problemDescription = string.Format("Failed to read '{0}'. It is likely not a binary Spine SkeletonData file.\n{1}", asset.name, e); problemDescription = string.Format("Failed to read '{0}'. It is likely not a binary Spine SkeletonData file.\n{1}", asset.name, e);
@ -162,8 +176,11 @@ namespace Spine.Unity {
} }
public static bool IsJsonFile (TextAsset file) { public static bool IsJsonFile (TextAsset file) {
#if TEXT_ASSET_HAS_GET_DATA_BYTES
var content = file.GetData<byte>();
#else
byte[] content = file.bytes; byte[] content = file.bytes;
#endif
// check for binary skeleton version number string, starts after 8 byte hash // check for binary skeleton version number string, starts after 8 byte hash
char majorVersionChar = compatibleBinaryVersions[0][0].ToString()[0]; char majorVersionChar = compatibleBinaryVersions[0][0].ToString()[0];
if (content.Length > 10 && content[9] == majorVersionChar && content[10] == '.') if (content.Length > 10 && content[9] == majorVersionChar && content[10] == '.')