Forráskód Böngészése

[libgdx] Fix line endings, CRLF -> LF

badlogic 4 éve
szülő
commit
c2b1ecb195
52 módosított fájl, 16479 hozzáadás és 16477 törlés
  1. 2 0
      build.gradle
  2. 1011 1011
      spine-libgdx/spine-libgdx-tests/src/com/esotericsoftware/spine/AnimationStateTests.java
  3. 109 109
      spine-libgdx/spine-libgdx-tests/src/com/esotericsoftware/spine/AttachmentTimelineTests.java
  4. 87 87
      spine-libgdx/spine-libgdx-tests/src/com/esotericsoftware/spine/BonePlotting.java
  5. 249 249
      spine-libgdx/spine-libgdx-tests/src/com/esotericsoftware/spine/Box2DExample.java
  6. 228 228
      spine-libgdx/spine-libgdx-tests/src/com/esotericsoftware/spine/EventTimelineTests.java
  7. 145 145
      spine-libgdx/spine-libgdx-tests/src/com/esotericsoftware/spine/MixTest.java
  8. 384 384
      spine-libgdx/spine-libgdx-tests/src/com/esotericsoftware/spine/NormalMapTest.java
  9. 111 111
      spine-libgdx/spine-libgdx-tests/src/com/esotericsoftware/spine/SimpleTest1.java
  10. 173 173
      spine-libgdx/spine-libgdx-tests/src/com/esotericsoftware/spine/SimpleTest2.java
  11. 111 111
      spine-libgdx/spine-libgdx-tests/src/com/esotericsoftware/spine/SimpleTest3.java
  12. 117 117
      spine-libgdx/spine-libgdx-tests/src/com/esotericsoftware/spine/SkeletonAttachmentTest.java
  13. 2430 2430
      spine-libgdx/spine-libgdx/src/com/esotericsoftware/spine/Animation.java
  14. 1376 1376
      spine-libgdx/spine-libgdx/src/com/esotericsoftware/spine/AnimationState.java
  15. 119 119
      spine-libgdx/spine-libgdx/src/com/esotericsoftware/spine/AnimationStateData.java
  16. 57 57
      spine-libgdx/spine-libgdx/src/com/esotericsoftware/spine/BlendMode.java
  17. 578 578
      spine-libgdx/spine-libgdx/src/com/esotericsoftware/spine/Bone.java
  18. 206 206
      spine-libgdx/spine-libgdx/src/com/esotericsoftware/spine/BoneData.java
  19. 109 109
      spine-libgdx/spine-libgdx/src/com/esotericsoftware/spine/Event.java
  20. 105 105
      spine-libgdx/spine-libgdx/src/com/esotericsoftware/spine/EventData.java
  21. 375 375
      spine-libgdx/spine-libgdx/src/com/esotericsoftware/spine/IkConstraint.java
  22. 122 122
      spine-libgdx/spine-libgdx/src/com/esotericsoftware/spine/IkConstraintData.java
  23. 564 564
      spine-libgdx/spine-libgdx/src/com/esotericsoftware/spine/PathConstraint.java
  24. 175 175
      spine-libgdx/spine-libgdx/src/com/esotericsoftware/spine/PathConstraintData.java
  25. 768 768
      spine-libgdx/spine-libgdx/src/com/esotericsoftware/spine/Skeleton.java
  26. 1110 1110
      spine-libgdx/spine-libgdx/src/com/esotericsoftware/spine/SkeletonBinary.java
  27. 254 254
      spine-libgdx/spine-libgdx/src/com/esotericsoftware/spine/SkeletonBounds.java
  28. 314 314
      spine-libgdx/spine-libgdx/src/com/esotericsoftware/spine/SkeletonData.java
  29. 1062 1062
      spine-libgdx/spine-libgdx/src/com/esotericsoftware/spine/SkeletonJson.java
  30. 494 494
      spine-libgdx/spine-libgdx/src/com/esotericsoftware/spine/SkeletonRenderer.java
  31. 310 310
      spine-libgdx/spine-libgdx/src/com/esotericsoftware/spine/SkeletonRendererDebug.java
  32. 207 207
      spine-libgdx/spine-libgdx/src/com/esotericsoftware/spine/Skin.java
  33. 159 159
      spine-libgdx/spine-libgdx/src/com/esotericsoftware/spine/Slot.java
  34. 107 107
      spine-libgdx/spine-libgdx/src/com/esotericsoftware/spine/SlotData.java
  35. 370 370
      spine-libgdx/spine-libgdx/src/com/esotericsoftware/spine/TransformConstraint.java
  36. 186 186
      spine-libgdx/spine-libgdx/src/com/esotericsoftware/spine/TransformConstraintData.java
  37. 41 41
      spine-libgdx/spine-libgdx/src/com/esotericsoftware/spine/Updatable.java
  38. 81 81
      spine-libgdx/spine-libgdx/src/com/esotericsoftware/spine/attachments/AtlasAttachmentLoader.java
  39. 52 52
      spine-libgdx/spine-libgdx/src/com/esotericsoftware/spine/attachments/Attachment.java
  40. 58 58
      spine-libgdx/spine-libgdx/src/com/esotericsoftware/spine/attachments/AttachmentLoader.java
  41. 36 36
      spine-libgdx/spine-libgdx/src/com/esotericsoftware/spine/attachments/AttachmentType.java
  42. 61 61
      spine-libgdx/spine-libgdx/src/com/esotericsoftware/spine/attachments/BoundingBoxAttachment.java
  43. 275 275
      spine-libgdx/spine-libgdx/src/com/esotericsoftware/spine/attachments/MeshAttachment.java
  44. 96 96
      spine-libgdx/spine-libgdx/src/com/esotericsoftware/spine/attachments/PathAttachment.java
  45. 285 285
      spine-libgdx/spine-libgdx/src/com/esotericsoftware/spine/attachments/RegionAttachment.java
  46. 58 58
      spine-libgdx/spine-libgdx/src/com/esotericsoftware/spine/attachments/SkeletonAttachment.java
  47. 193 193
      spine-libgdx/spine-libgdx/src/com/esotericsoftware/spine/attachments/VertexAttachment.java
  48. 113 113
      spine-libgdx/spine-libgdx/src/com/esotericsoftware/spine/utils/SkeletonActor.java
  49. 131 131
      spine-libgdx/spine-libgdx/src/com/esotericsoftware/spine/utils/SkeletonActorPool.java
  50. 57 57
      spine-libgdx/spine-libgdx/src/com/esotericsoftware/spine/utils/SkeletonPool.java
  51. 281 281
      spine-libgdx/spine-skeletonviewer/src/com/esotericsoftware/spine/JsonRollback.java
  52. 377 377
      spine-libgdx/spine-skeletonviewer/src/com/esotericsoftware/spine/SkeletonViewer.java

+ 2 - 0
build.gradle

@@ -5,6 +5,8 @@ plugins {
 }
 
 spotless {
+    lineEndings 'UNIX'
+
     java {
         target 'spine-libgdx/**/*.java'
         eclipse().configFile('formatters/eclipse-formatter.xml')

+ 1011 - 1011
spine-libgdx/spine-libgdx-tests/src/com/esotericsoftware/spine/AnimationStateTests.java

@@ -1,1011 +1,1011 @@
-/******************************************************************************
- * Spine Runtimes License Agreement
- * Last updated January 1, 2020. Replaces all prior versions.
- *
- * Copyright (c) 2013-2020, Esoteric Software LLC
- *
- * Integration of the Spine Runtimes into software or otherwise creating
- * derivative works of the Spine Runtimes is permitted under the terms and
- * conditions of Section 2 of the Spine Editor License Agreement:
- * http://esotericsoftware.com/spine-editor-license
- *
- * Otherwise, it is permitted to integrate the Spine Runtimes into software
- * or otherwise create derivative works of the Spine Runtimes (collectively,
- * "Products"), provided that each user of the Products must obtain their own
- * Spine Editor license and redistribution of the Products in any form must
- * include this license and copyright notice.
- *
- * THE SPINE RUNTIMES ARE PROVIDED BY ESOTERIC SOFTWARE LLC "AS IS" AND ANY
- * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
- * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
- * DISCLAIMED. IN NO EVENT SHALL ESOTERIC SOFTWARE LLC BE LIABLE FOR ANY
- * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
- * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES,
- * BUSINESS INTERRUPTION, OR LOSS OF USE, DATA, OR PROFITS) HOWEVER CAUSED AND
- * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
- * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
- * THE SPINE RUNTIMES, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
- *****************************************************************************/
-
-package com.esotericsoftware.spine;
-
-import java.lang.reflect.Field;
-import java.util.concurrent.atomic.AtomicInteger;
-
-import com.badlogic.gdx.Files.FileType;
-import com.badlogic.gdx.backends.lwjgl.LwjglFileHandle;
-import com.badlogic.gdx.math.MathUtils;
-import com.badlogic.gdx.utils.Array;
-import com.badlogic.gdx.utils.Pool;
-
-import com.esotericsoftware.spine.AnimationState.AnimationStateListener;
-import com.esotericsoftware.spine.AnimationState.TrackEntry;
-import com.esotericsoftware.spine.attachments.AttachmentLoader;
-import com.esotericsoftware.spine.attachments.BoundingBoxAttachment;
-import com.esotericsoftware.spine.attachments.ClippingAttachment;
-import com.esotericsoftware.spine.attachments.MeshAttachment;
-import com.esotericsoftware.spine.attachments.PathAttachment;
-import com.esotericsoftware.spine.attachments.PointAttachment;
-import com.esotericsoftware.spine.attachments.RegionAttachment;
-
-public class AnimationStateTests {
-	final SkeletonJson json = new SkeletonJson(new AttachmentLoader() {
-		public RegionAttachment newRegionAttachment (Skin skin, String name, String path) {
-			return null;
-		}
-
-		public MeshAttachment newMeshAttachment (Skin skin, String name, String path) {
-			return null;
-		}
-
-		public BoundingBoxAttachment newBoundingBoxAttachment (Skin skin, String name) {
-			return null;
-		}
-
-		public ClippingAttachment newClippingAttachment (Skin skin, String name) {
-			return null;
-		}
-
-		public PathAttachment newPathAttachment (Skin skin, String name) {
-			return null;
-		}
-
-		public PointAttachment newPointAttachment (Skin skin, String name) {
-			return null;
-		}
-	});
-
-	final AnimationStateListener stateListener = new AnimationStateListener() {
-		public void start (TrackEntry entry) {
-			add(actual("start", entry));
-		}
-
-		public void interrupt (TrackEntry entry) {
-			add(actual("interrupt", entry));
-		}
-
-		public void end (TrackEntry entry) {
-			add(actual("end", entry));
-		}
-
-		public void dispose (TrackEntry entry) {
-			add(actual("dispose", entry));
-		}
-
-		public void complete (TrackEntry entry) {
-			add(actual("complete", entry));
-		}
-
-		public void event (TrackEntry entry, Event event) {
-			add(actual("event " + event.getString(), entry));
-		}
-
-		private void add (Result result) {
-			while (expected.size > actual.size) {
-				Result note = expected.get(actual.size);
-				if (!note.note) break;
-				actual.add(note);
-				log(note.name);
-			}
-
-			String message = result.toString();
-			if (actual.size >= expected.size) {
-				message += "FAIL: <none>";
-				fail = true;
-			} else if (!expected.get(actual.size).equals(result)) {
-				message += "FAIL: " + expected.get(actual.size);
-				fail = true;
-			} else
-				message += "PASS";
-			log(message);
-			actual.add(result);
-		}
-	};
-
-	final SkeletonData skeletonData;
-	final Array<Result> actual = new Array();
-	final Array<Result> expected = new Array();
-
-	AnimationStateData stateData;
-	AnimationState state;
-	int entryCount;
-	float time = 0;
-	boolean fail;
-	int test;
-
-	AnimationStateTests () {
-		skeletonData = json.readSkeletonData(new LwjglFileHandle("test/test.json", FileType.Internal));
-
-		TrackEntry entry;
-
-		setup("0.1 time step", // 1
-			expect(0, "start", 0, 0), //
-			expect(0, "event 0", 0, 0), //
-			expect(0, "event 14", 0.5f, 0.5f), //
-			expect(0, "event 30", 1, 1), //
-			expect(0, "complete", 1, 1), //
-			expect(0, "end", 1, 1.1f), //
-			expect(0, "dispose", 1, 1.1f) //
-		);
-		state.setAnimation(0, "events0", false).setTrackEnd(1);
-		run(0.1f, 1000, null);
-
-		setup("1/60 time step, dispose queued", // 2
-			expect(0, "start", 0, 0), //
-			expect(0, "interrupt", 0, 0), //
-			expect(0, "end", 0, 0), //
-			expect(0, "dispose", 0, 0), //
-			expect(1, "dispose", 0, 0), //
-			expect(0, "dispose", 0, 0), //
-			expect(1, "dispose", 0, 0), //
-
-			note("First 2 set/addAnimation calls are done."),
-
-			expect(0, "start", 0, 0), //
-			expect(0, "event 0", 0, 0), //
-			expect(0, "event 14", 0.483f, 0.483f), //
-			expect(0, "event 30", 1, 1), //
-			expect(0, "complete", 1, 1), //
-			expect(0, "end", 1, 1.017f), //
-			expect(0, "dispose", 1, 1.017f) //
-		);
-		state.setAnimation(0, "events0", false);
-		state.addAnimation(0, "events1", false, 0);
-		state.addAnimation(0, "events0", false, 0);
-		state.addAnimation(0, "events1", false, 0);
-		state.setAnimation(0, "events0", false).setTrackEnd(1);
-		run(1 / 60f, 1000, null);
-
-		setup("30 time step", // 3
-			expect(0, "start", 0, 0), //
-			expect(0, "event 0", 0, 0), //
-			expect(0, "event 14", 30, 30), //
-			expect(0, "event 30", 30, 30), //
-			expect(0, "complete", 30, 30), //
-			expect(0, "end", 30, 60), //
-			expect(0, "dispose", 30, 60) //
-		);
-		state.setAnimation(0, "events0", false).setTrackEnd(1);
-		run(30, 1000, null);
-
-		setup("1 time step", // 4
-			expect(0, "start", 0, 0), //
-			expect(0, "event 0", 0, 0), //
-			expect(0, "event 14", 1, 1), //
-			expect(0, "event 30", 1, 1), //
-			expect(0, "complete", 1, 1), //
-			expect(0, "end", 1, 2), //
-			expect(0, "dispose", 1, 2) //
-		);
-		state.setAnimation(0, "events0", false).setTrackEnd(1);
-		run(1, 1.01f, null);
-
-		setup("interrupt", // 5
-			expect(0, "start", 0, 0), //
-			expect(0, "event 0", 0, 0), //
-			expect(0, "event 14", 0.5f, 0.5f), //
-			expect(0, "event 30", 1, 1), //
-			expect(0, "complete", 1, 1), //
-			expect(0, "interrupt", 1.1f, 1.1f), //
-
-			expect(1, "start", 0.1f, 1.1f), //
-			expect(1, "event 0", 0.1f, 1.1f), //
-
-			expect(0, "end", 1.1f, 1.2f), //
-			expect(0, "dispose", 1.1f, 1.2f), //
-
-			expect(1, "event 14", 0.5f, 1.5f), //
-			expect(1, "event 30", 1, 2), //
-			expect(1, "complete", 1, 2), //
-			expect(1, "interrupt", 1.1f, 2.1f), //
-
-			expect(0, "start", 0.1f, 2.1f), //
-			expect(0, "event 0", 0.1f, 2.1f), //
-
-			expect(1, "end", 1.1f, 2.2f), //
-			expect(1, "dispose", 1.1f, 2.2f), //
-
-			expect(0, "event 14", 0.5f, 2.5f), //
-			expect(0, "event 30", 1, 3), //
-			expect(0, "complete", 1, 3), //
-			expect(0, "end", 1, 3.1f), //
-			expect(0, "dispose", 1, 3.1f) //
-		);
-		state.setAnimation(0, "events0", false);
-		state.addAnimation(0, "events1", false, 0);
-		state.addAnimation(0, "events0", false, 0).setTrackEnd(1);
-		run(0.1f, 4f, null);
-
-		setup("interrupt with delay", // 6
-			expect(0, "start", 0, 0), //
-			expect(0, "event 0", 0, 0), //
-			expect(0, "event 14", 0.5f, 0.5f), //
-			expect(0, "interrupt", 0.6f, 0.6f), //
-
-			expect(1, "start", 0.1f, 0.6f), //
-			expect(1, "event 0", 0.1f, 0.6f), //
-
-			expect(0, "end", 0.6f, 0.7f), //
-			expect(0, "dispose", 0.6f, 0.7f), //
-
-			expect(1, "event 14", 0.5f, 1.0f), //
-			expect(1, "event 30", 1, 1.5f), //
-			expect(1, "complete", 1, 1.5f), //
-			expect(1, "end", 1, 1.6f), //
-			expect(1, "dispose", 1, 1.6f) //
-		);
-		state.setAnimation(0, "events0", false);
-		state.addAnimation(0, "events1", false, 0.5f).setTrackEnd(1);
-		run(0.1f, 1000, null);
-
-		setup("interrupt with delay and mix time", // 7
-			expect(0, "start", 0, 0), //
-			expect(0, "event 0", 0, 0), //
-			expect(0, "event 14", 0.5f, 0.5f), //
-			expect(0, "interrupt", 1, 1), //
-
-			expect(1, "start", 0.1f, 1), //
-
-			expect(0, "complete", 1, 1), //
-
-			expect(1, "event 0", 0.1f, 1), //
-			expect(1, "event 14", 0.5f, 1.4f), //
-
-			expect(0, "end", 1.6f, 1.7f), //
-			expect(0, "dispose", 1.6f, 1.7f), //
-
-			expect(1, "event 30", 1, 1.9f), //
-			expect(1, "complete", 1, 1.9f), //
-			expect(1, "end", 1, 2), //
-			expect(1, "dispose", 1, 2) //
-		);
-		stateData.setMix("events0", "events1", 0.7f);
-		state.setAnimation(0, "events0", true);
-		state.addAnimation(0, "events1", false, 0.9f).setTrackEnd(1);
-		run(0.1f, 1000, null);
-
-		setup("animation 0 events do not fire during mix", // 8
-			expect(0, "start", 0, 0), //
-			expect(0, "event 0", 0, 0), //
-			expect(0, "interrupt", 0.5f, 0.5f), //
-
-			expect(1, "start", 0.1f, 0.5f), //
-			expect(1, "event 0", 0.1f, 0.5f), //
-			expect(1, "event 14", 0.5f, 0.9f), //
-
-			expect(0, "complete", 1, 1), //
-			expect(0, "end", 1.1f, 1.2f), //
-			expect(0, "dispose", 1.1f, 1.2f), //
-
-			expect(1, "event 30", 1, 1.4f), //
-			expect(1, "complete", 1, 1.4f), //
-			expect(1, "end", 1, 1.5f), //
-			expect(1, "dispose", 1, 1.5f) //
-		);
-		stateData.setDefaultMix(0.7f);
-		state.setAnimation(0, "events0", false);
-		state.addAnimation(0, "events1", false, 0.4f).setTrackEnd(1);
-		run(0.1f, 1000, null);
-
-		setup("event threshold, some animation 0 events fire during mix", // 9
-			expect(0, "start", 0, 0), //
-			expect(0, "event 0", 0, 0), //
-			expect(0, "interrupt", 0.5f, 0.5f), //
-
-			expect(1, "start", 0.1f, 0.5f), //
-
-			expect(0, "event 14", 0.5f, 0.5f), //
-
-			expect(1, "event 0", 0.1f, 0.5f), //
-			expect(1, "event 14", 0.5f, 0.9f), //
-
-			expect(0, "complete", 1, 1), //
-			expect(0, "end", 1.1f, 1.2f), //
-			expect(0, "dispose", 1.1f, 1.2f), //
-
-			expect(1, "event 30", 1, 1.4f), //
-			expect(1, "complete", 1, 1.4f), //
-			expect(1, "end", 1, 1.5f), //
-			expect(1, "dispose", 1, 1.5f) //
-		);
-		stateData.setMix("events0", "events1", 0.7f);
-		state.setAnimation(0, "events0", false).setEventThreshold(0.5f);
-		state.addAnimation(0, "events1", false, 0.4f).setTrackEnd(1);
-		run(0.1f, 1000, null);
-
-		setup("event threshold, all animation 0 events fire during mix", // 10
-			expect(0, "start", 0, 0), //
-			expect(0, "event 0", 0, 0), //
-			expect(0, "event 14", 0.5f, 0.5f), //
-			expect(0, "interrupt", 0.9f, 0.9f), //
-
-			expect(1, "start", 0.1f, 0.9f), //
-			expect(1, "event 0", 0.1f, 0.9f), //
-
-			expect(0, "event 30", 1, 1), //
-			expect(0, "complete", 1, 1), //
-			expect(0, "event 0", 1, 1), //
-
-			expect(1, "event 14", 0.5f, 1.3f), //
-
-			expect(0, "end", 1.5f, 1.6f), //
-			expect(0, "dispose", 1.5f, 1.6f), //
-
-			expect(1, "event 30", 1, 1.8f), //
-			expect(1, "complete", 1, 1.8f), //
-			expect(1, "end", 1, 1.9f), //
-			expect(1, "dispose", 1, 1.9f) //
-		);
-		state.setAnimation(0, "events0", true).setEventThreshold(1);
-		entry = state.addAnimation(0, "events1", false, 0.8f);
-		entry.setMixDuration(0.7f);
-		entry.setTrackEnd(1);
-		run(0.1f, 1000, null);
-
-		setup("looping", // 11
-			expect(0, "start", 0, 0), //
-			expect(0, "event 0", 0, 0), //
-			expect(0, "event 14", 0.5f, 0.5f), //
-			expect(0, "event 30", 1, 1), //
-			expect(0, "complete", 1, 1), //
-			expect(0, "event 0", 1, 1), //
-			expect(0, "event 14", 1.5f, 1.5f), //
-			expect(0, "event 30", 2, 2), //
-			expect(0, "complete", 2, 2), //
-			expect(0, "event 0", 2, 2), //
-			expect(0, "event 14", 2.5f, 2.5f), //
-			expect(0, "event 30", 3, 3), //
-			expect(0, "complete", 3, 3), //
-			expect(0, "event 0", 3, 3), //
-			expect(0, "event 14", 3.5f, 3.5f), //
-			expect(0, "event 30", 4, 4), //
-			expect(0, "complete", 4, 4), //
-			expect(0, "event 0", 4, 4), //
-			expect(0, "end", 4.1f, 4.1f), //
-			expect(0, "dispose", 4.1f, 4.1f) //
-		);
-		state.setAnimation(0, "events0", true);
-		run(0.1f, 4, null);
-
-		setup("not looping, track end past animation 0 duration", // 12
-			expect(0, "start", 0, 0), //
-			expect(0, "event 0", 0, 0), //
-			expect(0, "event 14", 0.5f, 0.5f), //
-			expect(0, "event 30", 1, 1), //
-			expect(0, "complete", 1, 1), //
-			expect(0, "interrupt", 2.1f, 2.1f), //
-
-			expect(1, "start", 0.1f, 2.1f), //
-			expect(1, "event 0", 0.1f, 2.1f), //
-
-			expect(0, "end", 2.1f, 2.2f), //
-			expect(0, "dispose", 2.1f, 2.2f), //
-
-			expect(1, "event 14", 0.5f, 2.5f), //
-			expect(1, "event 30", 1, 3), //
-			expect(1, "complete", 1, 3), //
-			expect(1, "end", 1, 3.1f), //
-			expect(1, "dispose", 1, 3.1f) //
-		);
-		state.setAnimation(0, "events0", false);
-		state.addAnimation(0, "events1", false, 2).setTrackEnd(1);
-		run(0.1f, 4f, null);
-
-		setup("interrupt animation after first loop complete", // 13
-			expect(0, "start", 0, 0), //
-			expect(0, "event 0", 0, 0), //
-			expect(0, "event 14", 0.5f, 0.5f), //
-			expect(0, "event 30", 1, 1), //
-			expect(0, "complete", 1, 1), //
-			expect(0, "event 0", 1, 1), //
-			expect(0, "event 14", 1.5f, 1.5f), //
-			expect(0, "event 30", 2, 2), //
-			expect(0, "complete", 2, 2), //
-			expect(0, "event 0", 2, 2), //
-			expect(0, "interrupt", 2.1f, 2.1f), //
-
-			expect(1, "start", 0.1f, 2.1f), //
-			expect(1, "event 0", 0.1f, 2.1f), //
-
-			expect(0, "end", 2.1f, 2.2f), //
-			expect(0, "dispose", 2.1f, 2.2f), //
-
-			expect(1, "event 14", 0.5f, 2.5f), //
-			expect(1, "event 30", 1, 3), //
-			expect(1, "complete", 1, 3), //
-			expect(1, "end", 1, 3.1f), //
-			expect(1, "dispose", 1, 3.1f) //
-		);
-		state.setAnimation(0, "events0", true);
-		run(0.1f, 6, new TestListener() {
-			public void frame (float time) {
-				if (MathUtils.isEqual(time, 1.4f)) state.addAnimation(0, "events1", false, 0).setTrackEnd(1);
-			}
-		});
-
-		setup("add animation on empty track", // 14
-			expect(0, "start", 0, 0), //
-			expect(0, "event 0", 0, 0), //
-			expect(0, "event 14", 0.5f, 0.5f), //
-			expect(0, "event 30", 1, 1), //
-			expect(0, "complete", 1, 1), //
-			expect(0, "end", 1, 1.1f), //
-			expect(0, "dispose", 1, 1.1f) //
-		);
-		state.addAnimation(0, "events0", false, 0).setTrackEnd(1);
-		run(0.1f, 1.9f, null);
-
-		setup("end time beyond non-looping animation duration", // 15
-			expect(0, "start", 0, 0), //
-			expect(0, "event 0", 0, 0), //
-			expect(0, "event 14", 0.5f, 0.5f), //
-			expect(0, "event 30", 1, 1), //
-			expect(0, "complete", 1, 1), //
-			expect(0, "end", 9f, 9.1f), //
-			expect(0, "dispose", 9f, 9.1f) //
-		);
-		state.setAnimation(0, "events0", false).setTrackEnd(9);
-		run(0.1f, 10, null);
-
-		setup("looping with animation start", // 16
-			expect(0, "start", 0, 0), //
-			expect(0, "event 30", 0.4f, 0.4f), //
-			expect(0, "complete", 0.4f, 0.4f), //
-			expect(0, "event 30", 0.8f, 0.8f), //
-			expect(0, "complete", 0.8f, 0.8f), //
-			expect(0, "event 30", 1.2f, 1.2f), //
-			expect(0, "complete", 1.2f, 1.2f), //
-			expect(0, "end", 1.4f, 1.4f), //
-			expect(0, "dispose", 1.4f, 1.4f) //
-		);
-		entry = state.setAnimation(0, "events0", true);
-		entry.setAnimationLast(0.6f);
-		entry.setAnimationStart(0.6f);
-		run(0.1f, 1.4f, null);
-
-		setup("looping with animation start and end", // 17
-			expect(0, "start", 0, 0), //
-			expect(0, "event 14", 0.3f, 0.3f), //
-			expect(0, "complete", 0.6f, 0.6f), //
-			expect(0, "event 14", 0.9f, 0.9f), //
-			expect(0, "complete", 1.2f, 1.2f), //
-			expect(0, "event 14", 1.5f, 1.5f), //
-			expect(0, "end", 1.8f, 1.8f), //
-			expect(0, "dispose", 1.8f, 1.8f) //
-		);
-		entry = state.setAnimation(0, "events0", true);
-		entry.setAnimationStart(0.2f);
-		entry.setAnimationLast(0.2f);
-		entry.setAnimationEnd(0.8f);
-		run(0.1f, 1.8f, null);
-
-		setup("non-looping with animation start and end", // 18
-			expect(0, "start", 0, 0), //
-			expect(0, "event 14", 0.3f, 0.3f), //
-			expect(0, "complete", 0.6f, 0.6f), //
-			expect(0, "end", 1, 1.1f), //
-			expect(0, "dispose", 1, 1.1f) //
-		);
-		entry = state.setAnimation(0, "events0", false);
-		entry.setAnimationStart(0.2f);
-		entry.setAnimationLast(0.2f);
-		entry.setAnimationEnd(0.8f);
-		entry.setTrackEnd(1);
-		run(0.1f, 1.8f, null);
-
-		setup("mix out looping with animation start and end", // 19
-			expect(0, "start", 0, 0), //
-			expect(0, "event 14", 0.3f, 0.3f), //
-			expect(0, "complete", 0.6f, 0.6f), //
-			expect(0, "interrupt", 0.8f, 0.8f), //
-
-			expect(1, "start", 0.1f, 0.8f), //
-			expect(1, "event 0", 0.1f, 0.8f), //
-
-			expect(0, "event 14", 0.9f, 0.9f), //
-			expect(0, "complete", 1.2f, 1.2f), //
-
-			expect(1, "event 14", 0.5f, 1.2f), //
-
-			expect(0, "end", 1.4f, 1.5f), //
-			expect(0, "dispose", 1.4f, 1.5f), //
-
-			expect(1, "event 30", 1, 1.7f), //
-			expect(1, "complete", 1, 1.7f), //
-			expect(1, "end", 1, 1.8f), //
-			expect(1, "dispose", 1, 1.8f) //
-		);
-		entry = state.setAnimation(0, "events0", true);
-		entry.setAnimationStart(0.2f);
-		entry.setAnimationLast(0.2f);
-		entry.setAnimationEnd(0.8f);
-		entry.setEventThreshold(1);
-		entry = state.addAnimation(0, "events1", false, 0.7f);
-		entry.setMixDuration(0.7f);
-		entry.setTrackEnd(1);
-		run(0.1f, 20, null);
-
-		setup("setAnimation with track entry mix", // 20
-			expect(0, "start", 0, 0), //
-			expect(0, "event 0", 0, 0), //
-			expect(0, "event 14", 0.5f, 0.5f), //
-			expect(0, "event 30", 1, 1), //
-			expect(0, "complete", 1, 1), //
-			expect(0, "event 0", 1, 1), //
-			expect(0, "interrupt", 1, 1), //
-
-			expect(1, "start", 0, 1), //
-
-			expect(1, "event 0", 0.1f, 1.1f), //
-			expect(1, "event 14", 0.5f, 1.5f), //
-
-			expect(0, "end", 1.7f, 1.8f), //
-			expect(0, "dispose", 1.7f, 1.8f), //
-
-			expect(1, "event 30", 1, 2), //
-			expect(1, "complete", 1, 2), //
-			expect(1, "end", 1, 2.1f), //
-			expect(1, "dispose", 1, 2.1f) //
-		);
-		state.setAnimation(0, "events0", true);
-		run(0.1f, 1000, new TestListener() {
-			public void frame (float time) {
-				if (MathUtils.isEqual(time, 1f)) {
-					TrackEntry entry = state.setAnimation(0, "events1", false);
-					entry.setMixDuration(0.7f);
-					entry.setTrackEnd(1);
-				}
-			}
-		});
-
-		setup("setAnimation twice", // 21
-			expect(0, "start", 0, 0), //
-			expect(0, "interrupt", 0, 0), //
-			expect(0, "end", 0, 0), //
-			expect(0, "dispose", 0, 0), //
-
-			expect(1, "start", 0, 0), //
-			expect(1, "event 0", 0, 0), //
-			expect(1, "event 14", 0.5f, 0.5f), //
-
-			note("First 2 setAnimation calls are done."),
-
-			expect(1, "interrupt", 0.8f, 0.8f), //
-
-			expect(0, "start", 0, 0.8f), //
-			expect(0, "interrupt", 0, 0.8f), //
-			expect(0, "end", 0, 0.8f), //
-			expect(0, "dispose", 0, 0.8f), //
-
-			expect(2, "start", 0, 0.8f), //
-			expect(2, "event 0", 0.1f, 0.9f), //
-
-			expect(1, "end", 0.9f, 1), //
-			expect(1, "dispose", 0.9f, 1), //
-
-			expect(2, "event 14", 0.5f, 1.3f), //
-			expect(2, "event 30", 1, 1.8f), //
-			expect(2, "complete", 1, 1.8f), //
-			expect(2, "end", 1, 1.9f), //
-			expect(2, "dispose", 1, 1.9f) //
-		);
-		state.setAnimation(0, "events0", false); // First should be ignored.
-		state.setAnimation(0, "events1", false);
-		run(0.1f, 1000, new TestListener() {
-			public void frame (float time) {
-				if (MathUtils.isEqual(time, 0.8f)) {
-					state.setAnimation(0, "events0", false); // First should be ignored.
-					state.setAnimation(0, "events2", false).setTrackEnd(1);
-				}
-			}
-		});
-
-		setup("setAnimation twice with multiple mixing", // 22
-			expect(0, "start", 0, 0), //
-			expect(0, "interrupt", 0, 0), //
-			expect(0, "end", 0, 0), //
-			expect(0, "dispose", 0, 0), //
-
-			expect(1, "start", 0, 0), //
-			expect(1, "event 0", 0, 0), //
-
-			note("First 2 setAnimation calls are done."),
-
-			expect(1, "interrupt", 0.2f, 0.2f), //
-
-			expect(0, "start", 0, 0.2f), //
-			expect(0, "interrupt", 0, 0.2f), //
-			expect(0, "end", 0, 0.2f), //
-			expect(0, "dispose", 0, 0.2f), //
-
-			expect(2, "start", 0, 0.2f), //
-			expect(2, "event 0", 0.1f, 0.3f), //
-
-			note("Second 2 setAnimation calls are done."),
-
-			expect(2, "interrupt", 0.2f, 0.4f), //
-
-			expect(1, "start", 0, 0.4f), //
-			expect(1, "interrupt", 0, 0.4f), //
-			expect(1, "end", 0, 0.4f), //
-			expect(1, "dispose", 0, 0.4f), //
-
-			expect(0, "start", 0, 0.4f), //
-			expect(0, "event 0", 0.1f, 0.5f), //
-
-			expect(1, "end", 0.8f, 0.9f), //
-			expect(1, "dispose", 0.8f, 0.9f), //
-
-			expect(0, "event 14", 0.5f, 0.9f), //
-
-			expect(2, "end", 0.8f, 1.1f), //
-			expect(2, "dispose", 0.8f, 1.1f), //
-
-			expect(0, "event 30", 1, 1.4f), //
-			expect(0, "complete", 1, 1.4f), //
-			expect(0, "end", 1, 1.5f), //
-			expect(0, "dispose", 1, 1.5f) //
-		);
-		stateData.setDefaultMix(0.6f);
-		state.setAnimation(0, "events0", false); // First should be ignored.
-		state.setAnimation(0, "events1", false);
-		run(0.1f, 1000, new TestListener() {
-			public void frame (float time) {
-				if (MathUtils.isEqual(time, 0.2f)) {
-					state.setAnimation(0, "events0", false); // First should be ignored.
-					state.setAnimation(0, "events2", false);
-				}
-				if (MathUtils.isEqual(time, 0.4f)) {
-					state.setAnimation(0, "events1", false); // First should be ignored.
-					state.setAnimation(0, "events0", false).setTrackEnd(1);
-				}
-			}
-		});
-
-		setup("addAnimation with delay on empty track", // 23
-			expect(0, "start", 0, 0), //
-			expect(0, "event 0", 0, 5), //
-			expect(0, "event 14", 0.5f, 5.5f), //
-			expect(0, "event 30", 1, 6), //
-			expect(0, "complete", 1, 6), //
-			expect(0, "end", 1, 6.1f), //
-			expect(0, "dispose", 1, 6.1f) //
-		);
-		state.addAnimation(0, "events0", false, 5).setTrackEnd(1);
-		run(0.1f, 10, null);
-
-		setup("setAnimation during AnimationStateListener"); // 24
-		state.addListener(new AnimationStateListener() {
-			public void start (TrackEntry entry) {
-				if (entry.getAnimation().getName().equals("events0")) state.setAnimation(1, "events1", false);
-			}
-
-			public void interrupt (TrackEntry entry) {
-				state.addAnimation(3, "events1", false, 0);
-			}
-
-			public void end (TrackEntry entry) {
-				if (entry.getAnimation().getName().equals("events0")) state.setAnimation(0, "events1", false);
-			}
-
-			public void dispose (TrackEntry entry) {
-				if (entry.getAnimation().getName().equals("events0")) state.setAnimation(1, "events1", false);
-			}
-
-			public void complete (TrackEntry entry) {
-				if (entry.getAnimation().getName().equals("events0")) state.setAnimation(1, "events1", false);
-			}
-
-			public void event (TrackEntry entry, Event event) {
-				if (entry.getTrackIndex() != 2) state.setAnimation(2, "events1", false);
-			}
-		});
-		state.addAnimation(0, "events0", false, 0);
-		state.addAnimation(0, "events1", false, 0);
-		state.setAnimation(1, "events1", false).setTrackEnd(1);
-		run(0.1f, 10, null);
-
-		setup("clearTrack", // 25
-			expect(0, "start", 0, 0), //
-			expect(0, "event 0", 0, 0), //
-			expect(0, "event 14", 0.5f, 0.5f), //
-			expect(0, "end", 0.7f, 0.7f), //
-			expect(0, "dispose", 0.7f, 0.7f) //
-		);
-		state.addAnimation(0, "events0", false, 0).setTrackEnd(1);
-		run(0.1f, 10, new TestListener() {
-			public void frame (float time) {
-				if (MathUtils.isEqual(time, 0.7f)) state.clearTrack(0);
-			}
-		});
-
-		setup("setEmptyAnimation", // 26
-			expect(0, "start", 0, 0), //
-			expect(0, "event 0", 0, 0), //
-			expect(0, "event 14", 0.5f, 0.5f), //
-			expect(0, "interrupt", 0.7f, 0.7f), //
-
-			expect(-1, "start", 0, 0.7f), //
-			expect(-1, "complete", 0.1f, 0.8f), //
-
-			expect(0, "end", 0.8f, 0.9f), //
-			expect(0, "dispose", 0.8f, 0.9f), //
-
-			expect(-1, "end", 0.2f, 1), //
-			expect(-1, "dispose", 0.2f, 1) //
-		);
-		state.addAnimation(0, "events0", false, 0).setTrackEnd(1);
-		run(0.1f, 10, new TestListener() {
-			public void frame (float time) {
-				if (MathUtils.isEqual(time, 0.7f)) state.setEmptyAnimation(0, 0);
-			}
-		});
-
-		setup("TrackEntry listener"); // 27
-		final AtomicInteger counter = new AtomicInteger();
-		state.addAnimation(0, "events0", false, 0).setListener(new AnimationStateListener() {
-			public void start (TrackEntry entry) {
-				counter.addAndGet(1 << 1);
-			}
-
-			public void interrupt (TrackEntry entry) {
-				counter.addAndGet(1 << 5);
-			}
-
-			public void end (TrackEntry entry) {
-				counter.addAndGet(1 << 9);
-			}
-
-			public void dispose (TrackEntry entry) {
-				counter.addAndGet(1 << 13);
-			}
-
-			public void complete (TrackEntry entry) {
-				counter.addAndGet(1 << 17);
-			}
-
-			public void event (TrackEntry entry, Event event) {
-				counter.addAndGet(1 << 21);
-			}
-		});
-		state.addAnimation(0, "events0", false, 0);
-		state.addAnimation(0, "events1", false, 0);
-		state.setAnimation(1, "events1", false).setTrackEnd(1);
-		run(0.1f, 10, null);
-		if (counter.get() != 15082016) {
-			log("TEST 28 FAILED! " + counter);
-			System.exit(0);
-		}
-
-		setup("looping", // 28
-			expect(0, "start", 0, 0), //
-			expect(0, "event 0", 0, 0), //
-			expect(0, "event 14", 0.5f, 0.5f), //
-			expect(0, "event 30", 1, 1), //
-			expect(0, "complete", 1, 1), //
-			expect(0, "event 0", 1, 1), //
-			expect(0, "event 14", 1.5f, 1.5f), //
-			expect(0, "event 30", 2, 2), //
-			expect(0, "complete", 2, 2), //
-			expect(0, "event 0", 2, 2), //
-			expect(0, "event 14", 2.5f, 2.5f), //
-			expect(0, "end", 2.6f, 2.7f), //
-			expect(0, "dispose", 2.6f, 2.7f) //
-		);
-		state.setAnimation(0, "events0", true).setTrackEnd(2.6f);
-		run(0.1f, 1000, null);
-
-		setup("set next", // 29
-			expect(0, "start", 0, 0), //
-			expect(0, "event 0", 0, 0), //
-			expect(0, "event 14", 0.5f, 0.5f), //
-			expect(0, "event 30", 1, 1), //
-			expect(0, "complete", 1, 1), //
-			expect(0, "interrupt", 1.1f, 1.1f), //
-
-			expect(1, "start", 0.1f, 1.1f), //
-			expect(1, "event 0", 0.1f, 1.1f), //
-
-			expect(0, "end", 1.1f, 1.2f), //
-			expect(0, "dispose", 1.1f, 1.2f), //
-
-			expect(1, "event 14", 0.5f, 1.5f), //
-			expect(1, "event 30", 1, 2), //
-			expect(1, "complete", 1, 2), //
-			expect(1, "end", 1, 2.1f), //
-			expect(1, "dispose", 1, 2.1f) //
-		);
-		state.setAnimation(0, "events0", false);
-		state.addAnimation(0, "events1", false, 0).setTrackEnd(1);
-		run(0.1f, 1000, null);
-
-		System.out.println("AnimationState tests passed.");
-	}
-
-	void setup (String description, Result... expectedArray) {
-		test++;
-		expected.addAll(expectedArray);
-		stateData = new AnimationStateData(skeletonData);
-		state = new AnimationState(stateData);
-
-		Pool trackEntryPool = new Pool<TrackEntry>() {
-			public TrackEntry obtain () {
-				TrackEntry entry = super.obtain();
-				entryCount++;
-				// System.out.println("+1: " + entryCount + " " + entry.hashCode());
-				return entry;
-			}
-
-			protected TrackEntry newObject () {
-				return new TrackEntry();
-			}
-
-			public void free (TrackEntry entry) {
-				entryCount--;
-				// System.out.println("-1: " + entryCount + " " + entry.hashCode());
-				super.free(entry);
-			}
-		};
-		try {
-			Field field = state.getClass().getDeclaredField("trackEntryPool");
-			field.setAccessible(true);
-			field.set(state, trackEntryPool);
-		} catch (Exception ex) {
-			throw new RuntimeException(ex);
-		}
-
-		time = 0;
-		fail = false;
-		log(test + ": " + description);
-		if (expectedArray.length > 0) {
-			state.addListener(stateListener);
-			log(String.format("%-3s%-12s%-7s%-7s%-7s", "#", "EVENT", "TRACK", "TOTAL", "RESULT"));
-		}
-	}
-
-	void run (float incr, float endTime, TestListener listener) {
-		Skeleton skeleton = new Skeleton(skeletonData);
-		state.apply(skeleton);
-		while (time < endTime) {
-			time += incr;
-			skeleton.update(incr);
-			state.update(incr);
-
-			// Reduce float discrepancies for tests.
-			for (TrackEntry entry : state.getTracks()) {
-				if (entry == null) continue;
-				entry.trackTime = round(entry.trackTime, 6);
-				entry.delay = round(entry.delay, 3);
-				if (entry.mixingFrom != null) entry.mixingFrom.trackTime = round(entry.mixingFrom.trackTime, 6);
-			}
-
-			state.apply(skeleton);
-
-			// Apply multiple times to ensure no side effects.
-			if (expected.size > 0) state.removeListener(stateListener);
-			state.apply(skeleton);
-			state.apply(skeleton);
-			if (expected.size > 0) state.addListener(stateListener);
-
-			if (listener != null) listener.frame(time);
-		}
-		state.clearTracks();
-
-		// Expecting more than actual is a failure.
-		for (int i = actual.size, n = expected.size; i < n; i++) {
-			log(String.format("%-29s", "<none>") + "FAIL: " + expected.get(i));
-			fail = true;
-		}
-
-		// Check all allocated entries were freed.
-		if (!fail) {
-			if (entryCount != 0) {
-				log("FAIL: Pool balance: " + entryCount);
-				fail = true;
-			}
-		}
-
-		actual.clear();
-		expected.clear();
-		log("");
-		if (fail) {
-			log("TEST " + test + " FAILED!");
-			System.exit(0);
-		}
-	}
-
-	Result expect (int animationIndex, String name, float trackTime, float totalTime) {
-		Result result = new Result();
-		result.name = name;
-		result.animationIndex = animationIndex;
-		result.trackTime = trackTime;
-		result.totalTime = totalTime;
-		return result;
-	}
-
-	Result actual (String name, TrackEntry entry) {
-		Result result = new Result();
-		result.name = name;
-		result.animationIndex = skeletonData.getAnimations().indexOf(entry.animation, true);
-		result.trackTime = Math.round(entry.trackTime * 1000) / 1000f;
-		result.totalTime = Math.round(time * 1000) / 1000f;
-		return result;
-	}
-
-	Result note (String message) {
-		Result result = new Result();
-		result.name = message;
-		result.note = true;
-		return result;
-	}
-
-	void log (String message) {
-		System.out.println(message);
-	}
-
-	class Result {
-		String name;
-		int animationIndex;
-		float trackTime, totalTime;
-		boolean note;
-
-		public int hashCode () {
-			int result = 31 + animationIndex;
-			result = 31 * result + name.hashCode();
-			result = 31 * result + Float.floatToIntBits(totalTime);
-			result = 31 * result + Float.floatToIntBits(trackTime);
-			return result;
-		}
-
-		public boolean equals (Object obj) {
-			Result other = (Result)obj;
-			if (animationIndex != other.animationIndex) return false;
-			if (!name.equals(other.name)) return false;
-			if (!MathUtils.isEqual(totalTime, other.totalTime)) return false;
-			if (!MathUtils.isEqual(trackTime, other.trackTime)) return false;
-			return true;
-		}
-
-		public String toString () {
-			return String.format("%-3s%-12s%-7s%-7s", "" + animationIndex, name, roundTime(trackTime), roundTime(totalTime));
-		}
-	}
-
-	static float round (float value, int decimals) {
-		float shift = (float)Math.pow(10, decimals);
-		return Math.round(value * shift) / shift;
-	}
-
-	static String roundTime (float value) {
-		String text = Float.toString(round(value, 3));
-		return text.endsWith(".0") ? text.substring(0, text.length() - 2) : text;
-	}
-
-	static interface TestListener {
-		void frame (float time);
-	}
-
-	static public void main (String[] args) throws Exception {
-		new AnimationStateTests();
-	}
-}
+/******************************************************************************
+ * Spine Runtimes License Agreement
+ * Last updated January 1, 2020. Replaces all prior versions.
+ *
+ * Copyright (c) 2013-2020, Esoteric Software LLC
+ *
+ * Integration of the Spine Runtimes into software or otherwise creating
+ * derivative works of the Spine Runtimes is permitted under the terms and
+ * conditions of Section 2 of the Spine Editor License Agreement:
+ * http://esotericsoftware.com/spine-editor-license
+ *
+ * Otherwise, it is permitted to integrate the Spine Runtimes into software
+ * or otherwise create derivative works of the Spine Runtimes (collectively,
+ * "Products"), provided that each user of the Products must obtain their own
+ * Spine Editor license and redistribution of the Products in any form must
+ * include this license and copyright notice.
+ *
+ * THE SPINE RUNTIMES ARE PROVIDED BY ESOTERIC SOFTWARE LLC "AS IS" AND ANY
+ * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL ESOTERIC SOFTWARE LLC BE LIABLE FOR ANY
+ * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES,
+ * BUSINESS INTERRUPTION, OR LOSS OF USE, DATA, OR PROFITS) HOWEVER CAUSED AND
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
+ * THE SPINE RUNTIMES, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ *****************************************************************************/
+
+package com.esotericsoftware.spine;
+
+import java.lang.reflect.Field;
+import java.util.concurrent.atomic.AtomicInteger;
+
+import com.badlogic.gdx.Files.FileType;
+import com.badlogic.gdx.backends.lwjgl.LwjglFileHandle;
+import com.badlogic.gdx.math.MathUtils;
+import com.badlogic.gdx.utils.Array;
+import com.badlogic.gdx.utils.Pool;
+
+import com.esotericsoftware.spine.AnimationState.AnimationStateListener;
+import com.esotericsoftware.spine.AnimationState.TrackEntry;
+import com.esotericsoftware.spine.attachments.AttachmentLoader;
+import com.esotericsoftware.spine.attachments.BoundingBoxAttachment;
+import com.esotericsoftware.spine.attachments.ClippingAttachment;
+import com.esotericsoftware.spine.attachments.MeshAttachment;
+import com.esotericsoftware.spine.attachments.PathAttachment;
+import com.esotericsoftware.spine.attachments.PointAttachment;
+import com.esotericsoftware.spine.attachments.RegionAttachment;
+
+public class AnimationStateTests {
+	final SkeletonJson json = new SkeletonJson(new AttachmentLoader() {
+		public RegionAttachment newRegionAttachment (Skin skin, String name, String path) {
+			return null;
+		}
+
+		public MeshAttachment newMeshAttachment (Skin skin, String name, String path) {
+			return null;
+		}
+
+		public BoundingBoxAttachment newBoundingBoxAttachment (Skin skin, String name) {
+			return null;
+		}
+
+		public ClippingAttachment newClippingAttachment (Skin skin, String name) {
+			return null;
+		}
+
+		public PathAttachment newPathAttachment (Skin skin, String name) {
+			return null;
+		}
+
+		public PointAttachment newPointAttachment (Skin skin, String name) {
+			return null;
+		}
+	});
+
+	final AnimationStateListener stateListener = new AnimationStateListener() {
+		public void start (TrackEntry entry) {
+			add(actual("start", entry));
+		}
+
+		public void interrupt (TrackEntry entry) {
+			add(actual("interrupt", entry));
+		}
+
+		public void end (TrackEntry entry) {
+			add(actual("end", entry));
+		}
+
+		public void dispose (TrackEntry entry) {
+			add(actual("dispose", entry));
+		}
+
+		public void complete (TrackEntry entry) {
+			add(actual("complete", entry));
+		}
+
+		public void event (TrackEntry entry, Event event) {
+			add(actual("event " + event.getString(), entry));
+		}
+
+		private void add (Result result) {
+			while (expected.size > actual.size) {
+				Result note = expected.get(actual.size);
+				if (!note.note) break;
+				actual.add(note);
+				log(note.name);
+			}
+
+			String message = result.toString();
+			if (actual.size >= expected.size) {
+				message += "FAIL: <none>";
+				fail = true;
+			} else if (!expected.get(actual.size).equals(result)) {
+				message += "FAIL: " + expected.get(actual.size);
+				fail = true;
+			} else
+				message += "PASS";
+			log(message);
+			actual.add(result);
+		}
+	};
+
+	final SkeletonData skeletonData;
+	final Array<Result> actual = new Array();
+	final Array<Result> expected = new Array();
+
+	AnimationStateData stateData;
+	AnimationState state;
+	int entryCount;
+	float time = 0;
+	boolean fail;
+	int test;
+
+	AnimationStateTests () {
+		skeletonData = json.readSkeletonData(new LwjglFileHandle("test/test.json", FileType.Internal));
+
+		TrackEntry entry;
+
+		setup("0.1 time step", // 1
+			expect(0, "start", 0, 0), //
+			expect(0, "event 0", 0, 0), //
+			expect(0, "event 14", 0.5f, 0.5f), //
+			expect(0, "event 30", 1, 1), //
+			expect(0, "complete", 1, 1), //
+			expect(0, "end", 1, 1.1f), //
+			expect(0, "dispose", 1, 1.1f) //
+		);
+		state.setAnimation(0, "events0", false).setTrackEnd(1);
+		run(0.1f, 1000, null);
+
+		setup("1/60 time step, dispose queued", // 2
+			expect(0, "start", 0, 0), //
+			expect(0, "interrupt", 0, 0), //
+			expect(0, "end", 0, 0), //
+			expect(0, "dispose", 0, 0), //
+			expect(1, "dispose", 0, 0), //
+			expect(0, "dispose", 0, 0), //
+			expect(1, "dispose", 0, 0), //
+
+			note("First 2 set/addAnimation calls are done."),
+
+			expect(0, "start", 0, 0), //
+			expect(0, "event 0", 0, 0), //
+			expect(0, "event 14", 0.483f, 0.483f), //
+			expect(0, "event 30", 1, 1), //
+			expect(0, "complete", 1, 1), //
+			expect(0, "end", 1, 1.017f), //
+			expect(0, "dispose", 1, 1.017f) //
+		);
+		state.setAnimation(0, "events0", false);
+		state.addAnimation(0, "events1", false, 0);
+		state.addAnimation(0, "events0", false, 0);
+		state.addAnimation(0, "events1", false, 0);
+		state.setAnimation(0, "events0", false).setTrackEnd(1);
+		run(1 / 60f, 1000, null);
+
+		setup("30 time step", // 3
+			expect(0, "start", 0, 0), //
+			expect(0, "event 0", 0, 0), //
+			expect(0, "event 14", 30, 30), //
+			expect(0, "event 30", 30, 30), //
+			expect(0, "complete", 30, 30), //
+			expect(0, "end", 30, 60), //
+			expect(0, "dispose", 30, 60) //
+		);
+		state.setAnimation(0, "events0", false).setTrackEnd(1);
+		run(30, 1000, null);
+
+		setup("1 time step", // 4
+			expect(0, "start", 0, 0), //
+			expect(0, "event 0", 0, 0), //
+			expect(0, "event 14", 1, 1), //
+			expect(0, "event 30", 1, 1), //
+			expect(0, "complete", 1, 1), //
+			expect(0, "end", 1, 2), //
+			expect(0, "dispose", 1, 2) //
+		);
+		state.setAnimation(0, "events0", false).setTrackEnd(1);
+		run(1, 1.01f, null);
+
+		setup("interrupt", // 5
+			expect(0, "start", 0, 0), //
+			expect(0, "event 0", 0, 0), //
+			expect(0, "event 14", 0.5f, 0.5f), //
+			expect(0, "event 30", 1, 1), //
+			expect(0, "complete", 1, 1), //
+			expect(0, "interrupt", 1.1f, 1.1f), //
+
+			expect(1, "start", 0.1f, 1.1f), //
+			expect(1, "event 0", 0.1f, 1.1f), //
+
+			expect(0, "end", 1.1f, 1.2f), //
+			expect(0, "dispose", 1.1f, 1.2f), //
+
+			expect(1, "event 14", 0.5f, 1.5f), //
+			expect(1, "event 30", 1, 2), //
+			expect(1, "complete", 1, 2), //
+			expect(1, "interrupt", 1.1f, 2.1f), //
+
+			expect(0, "start", 0.1f, 2.1f), //
+			expect(0, "event 0", 0.1f, 2.1f), //
+
+			expect(1, "end", 1.1f, 2.2f), //
+			expect(1, "dispose", 1.1f, 2.2f), //
+
+			expect(0, "event 14", 0.5f, 2.5f), //
+			expect(0, "event 30", 1, 3), //
+			expect(0, "complete", 1, 3), //
+			expect(0, "end", 1, 3.1f), //
+			expect(0, "dispose", 1, 3.1f) //
+		);
+		state.setAnimation(0, "events0", false);
+		state.addAnimation(0, "events1", false, 0);
+		state.addAnimation(0, "events0", false, 0).setTrackEnd(1);
+		run(0.1f, 4f, null);
+
+		setup("interrupt with delay", // 6
+			expect(0, "start", 0, 0), //
+			expect(0, "event 0", 0, 0), //
+			expect(0, "event 14", 0.5f, 0.5f), //
+			expect(0, "interrupt", 0.6f, 0.6f), //
+
+			expect(1, "start", 0.1f, 0.6f), //
+			expect(1, "event 0", 0.1f, 0.6f), //
+
+			expect(0, "end", 0.6f, 0.7f), //
+			expect(0, "dispose", 0.6f, 0.7f), //
+
+			expect(1, "event 14", 0.5f, 1.0f), //
+			expect(1, "event 30", 1, 1.5f), //
+			expect(1, "complete", 1, 1.5f), //
+			expect(1, "end", 1, 1.6f), //
+			expect(1, "dispose", 1, 1.6f) //
+		);
+		state.setAnimation(0, "events0", false);
+		state.addAnimation(0, "events1", false, 0.5f).setTrackEnd(1);
+		run(0.1f, 1000, null);
+
+		setup("interrupt with delay and mix time", // 7
+			expect(0, "start", 0, 0), //
+			expect(0, "event 0", 0, 0), //
+			expect(0, "event 14", 0.5f, 0.5f), //
+			expect(0, "interrupt", 1, 1), //
+
+			expect(1, "start", 0.1f, 1), //
+
+			expect(0, "complete", 1, 1), //
+
+			expect(1, "event 0", 0.1f, 1), //
+			expect(1, "event 14", 0.5f, 1.4f), //
+
+			expect(0, "end", 1.6f, 1.7f), //
+			expect(0, "dispose", 1.6f, 1.7f), //
+
+			expect(1, "event 30", 1, 1.9f), //
+			expect(1, "complete", 1, 1.9f), //
+			expect(1, "end", 1, 2), //
+			expect(1, "dispose", 1, 2) //
+		);
+		stateData.setMix("events0", "events1", 0.7f);
+		state.setAnimation(0, "events0", true);
+		state.addAnimation(0, "events1", false, 0.9f).setTrackEnd(1);
+		run(0.1f, 1000, null);
+
+		setup("animation 0 events do not fire during mix", // 8
+			expect(0, "start", 0, 0), //
+			expect(0, "event 0", 0, 0), //
+			expect(0, "interrupt", 0.5f, 0.5f), //
+
+			expect(1, "start", 0.1f, 0.5f), //
+			expect(1, "event 0", 0.1f, 0.5f), //
+			expect(1, "event 14", 0.5f, 0.9f), //
+
+			expect(0, "complete", 1, 1), //
+			expect(0, "end", 1.1f, 1.2f), //
+			expect(0, "dispose", 1.1f, 1.2f), //
+
+			expect(1, "event 30", 1, 1.4f), //
+			expect(1, "complete", 1, 1.4f), //
+			expect(1, "end", 1, 1.5f), //
+			expect(1, "dispose", 1, 1.5f) //
+		);
+		stateData.setDefaultMix(0.7f);
+		state.setAnimation(0, "events0", false);
+		state.addAnimation(0, "events1", false, 0.4f).setTrackEnd(1);
+		run(0.1f, 1000, null);
+
+		setup("event threshold, some animation 0 events fire during mix", // 9
+			expect(0, "start", 0, 0), //
+			expect(0, "event 0", 0, 0), //
+			expect(0, "interrupt", 0.5f, 0.5f), //
+
+			expect(1, "start", 0.1f, 0.5f), //
+
+			expect(0, "event 14", 0.5f, 0.5f), //
+
+			expect(1, "event 0", 0.1f, 0.5f), //
+			expect(1, "event 14", 0.5f, 0.9f), //
+
+			expect(0, "complete", 1, 1), //
+			expect(0, "end", 1.1f, 1.2f), //
+			expect(0, "dispose", 1.1f, 1.2f), //
+
+			expect(1, "event 30", 1, 1.4f), //
+			expect(1, "complete", 1, 1.4f), //
+			expect(1, "end", 1, 1.5f), //
+			expect(1, "dispose", 1, 1.5f) //
+		);
+		stateData.setMix("events0", "events1", 0.7f);
+		state.setAnimation(0, "events0", false).setEventThreshold(0.5f);
+		state.addAnimation(0, "events1", false, 0.4f).setTrackEnd(1);
+		run(0.1f, 1000, null);
+
+		setup("event threshold, all animation 0 events fire during mix", // 10
+			expect(0, "start", 0, 0), //
+			expect(0, "event 0", 0, 0), //
+			expect(0, "event 14", 0.5f, 0.5f), //
+			expect(0, "interrupt", 0.9f, 0.9f), //
+
+			expect(1, "start", 0.1f, 0.9f), //
+			expect(1, "event 0", 0.1f, 0.9f), //
+
+			expect(0, "event 30", 1, 1), //
+			expect(0, "complete", 1, 1), //
+			expect(0, "event 0", 1, 1), //
+
+			expect(1, "event 14", 0.5f, 1.3f), //
+
+			expect(0, "end", 1.5f, 1.6f), //
+			expect(0, "dispose", 1.5f, 1.6f), //
+
+			expect(1, "event 30", 1, 1.8f), //
+			expect(1, "complete", 1, 1.8f), //
+			expect(1, "end", 1, 1.9f), //
+			expect(1, "dispose", 1, 1.9f) //
+		);
+		state.setAnimation(0, "events0", true).setEventThreshold(1);
+		entry = state.addAnimation(0, "events1", false, 0.8f);
+		entry.setMixDuration(0.7f);
+		entry.setTrackEnd(1);
+		run(0.1f, 1000, null);
+
+		setup("looping", // 11
+			expect(0, "start", 0, 0), //
+			expect(0, "event 0", 0, 0), //
+			expect(0, "event 14", 0.5f, 0.5f), //
+			expect(0, "event 30", 1, 1), //
+			expect(0, "complete", 1, 1), //
+			expect(0, "event 0", 1, 1), //
+			expect(0, "event 14", 1.5f, 1.5f), //
+			expect(0, "event 30", 2, 2), //
+			expect(0, "complete", 2, 2), //
+			expect(0, "event 0", 2, 2), //
+			expect(0, "event 14", 2.5f, 2.5f), //
+			expect(0, "event 30", 3, 3), //
+			expect(0, "complete", 3, 3), //
+			expect(0, "event 0", 3, 3), //
+			expect(0, "event 14", 3.5f, 3.5f), //
+			expect(0, "event 30", 4, 4), //
+			expect(0, "complete", 4, 4), //
+			expect(0, "event 0", 4, 4), //
+			expect(0, "end", 4.1f, 4.1f), //
+			expect(0, "dispose", 4.1f, 4.1f) //
+		);
+		state.setAnimation(0, "events0", true);
+		run(0.1f, 4, null);
+
+		setup("not looping, track end past animation 0 duration", // 12
+			expect(0, "start", 0, 0), //
+			expect(0, "event 0", 0, 0), //
+			expect(0, "event 14", 0.5f, 0.5f), //
+			expect(0, "event 30", 1, 1), //
+			expect(0, "complete", 1, 1), //
+			expect(0, "interrupt", 2.1f, 2.1f), //
+
+			expect(1, "start", 0.1f, 2.1f), //
+			expect(1, "event 0", 0.1f, 2.1f), //
+
+			expect(0, "end", 2.1f, 2.2f), //
+			expect(0, "dispose", 2.1f, 2.2f), //
+
+			expect(1, "event 14", 0.5f, 2.5f), //
+			expect(1, "event 30", 1, 3), //
+			expect(1, "complete", 1, 3), //
+			expect(1, "end", 1, 3.1f), //
+			expect(1, "dispose", 1, 3.1f) //
+		);
+		state.setAnimation(0, "events0", false);
+		state.addAnimation(0, "events1", false, 2).setTrackEnd(1);
+		run(0.1f, 4f, null);
+
+		setup("interrupt animation after first loop complete", // 13
+			expect(0, "start", 0, 0), //
+			expect(0, "event 0", 0, 0), //
+			expect(0, "event 14", 0.5f, 0.5f), //
+			expect(0, "event 30", 1, 1), //
+			expect(0, "complete", 1, 1), //
+			expect(0, "event 0", 1, 1), //
+			expect(0, "event 14", 1.5f, 1.5f), //
+			expect(0, "event 30", 2, 2), //
+			expect(0, "complete", 2, 2), //
+			expect(0, "event 0", 2, 2), //
+			expect(0, "interrupt", 2.1f, 2.1f), //
+
+			expect(1, "start", 0.1f, 2.1f), //
+			expect(1, "event 0", 0.1f, 2.1f), //
+
+			expect(0, "end", 2.1f, 2.2f), //
+			expect(0, "dispose", 2.1f, 2.2f), //
+
+			expect(1, "event 14", 0.5f, 2.5f), //
+			expect(1, "event 30", 1, 3), //
+			expect(1, "complete", 1, 3), //
+			expect(1, "end", 1, 3.1f), //
+			expect(1, "dispose", 1, 3.1f) //
+		);
+		state.setAnimation(0, "events0", true);
+		run(0.1f, 6, new TestListener() {
+			public void frame (float time) {
+				if (MathUtils.isEqual(time, 1.4f)) state.addAnimation(0, "events1", false, 0).setTrackEnd(1);
+			}
+		});
+
+		setup("add animation on empty track", // 14
+			expect(0, "start", 0, 0), //
+			expect(0, "event 0", 0, 0), //
+			expect(0, "event 14", 0.5f, 0.5f), //
+			expect(0, "event 30", 1, 1), //
+			expect(0, "complete", 1, 1), //
+			expect(0, "end", 1, 1.1f), //
+			expect(0, "dispose", 1, 1.1f) //
+		);
+		state.addAnimation(0, "events0", false, 0).setTrackEnd(1);
+		run(0.1f, 1.9f, null);
+
+		setup("end time beyond non-looping animation duration", // 15
+			expect(0, "start", 0, 0), //
+			expect(0, "event 0", 0, 0), //
+			expect(0, "event 14", 0.5f, 0.5f), //
+			expect(0, "event 30", 1, 1), //
+			expect(0, "complete", 1, 1), //
+			expect(0, "end", 9f, 9.1f), //
+			expect(0, "dispose", 9f, 9.1f) //
+		);
+		state.setAnimation(0, "events0", false).setTrackEnd(9);
+		run(0.1f, 10, null);
+
+		setup("looping with animation start", // 16
+			expect(0, "start", 0, 0), //
+			expect(0, "event 30", 0.4f, 0.4f), //
+			expect(0, "complete", 0.4f, 0.4f), //
+			expect(0, "event 30", 0.8f, 0.8f), //
+			expect(0, "complete", 0.8f, 0.8f), //
+			expect(0, "event 30", 1.2f, 1.2f), //
+			expect(0, "complete", 1.2f, 1.2f), //
+			expect(0, "end", 1.4f, 1.4f), //
+			expect(0, "dispose", 1.4f, 1.4f) //
+		);
+		entry = state.setAnimation(0, "events0", true);
+		entry.setAnimationLast(0.6f);
+		entry.setAnimationStart(0.6f);
+		run(0.1f, 1.4f, null);
+
+		setup("looping with animation start and end", // 17
+			expect(0, "start", 0, 0), //
+			expect(0, "event 14", 0.3f, 0.3f), //
+			expect(0, "complete", 0.6f, 0.6f), //
+			expect(0, "event 14", 0.9f, 0.9f), //
+			expect(0, "complete", 1.2f, 1.2f), //
+			expect(0, "event 14", 1.5f, 1.5f), //
+			expect(0, "end", 1.8f, 1.8f), //
+			expect(0, "dispose", 1.8f, 1.8f) //
+		);
+		entry = state.setAnimation(0, "events0", true);
+		entry.setAnimationStart(0.2f);
+		entry.setAnimationLast(0.2f);
+		entry.setAnimationEnd(0.8f);
+		run(0.1f, 1.8f, null);
+
+		setup("non-looping with animation start and end", // 18
+			expect(0, "start", 0, 0), //
+			expect(0, "event 14", 0.3f, 0.3f), //
+			expect(0, "complete", 0.6f, 0.6f), //
+			expect(0, "end", 1, 1.1f), //
+			expect(0, "dispose", 1, 1.1f) //
+		);
+		entry = state.setAnimation(0, "events0", false);
+		entry.setAnimationStart(0.2f);
+		entry.setAnimationLast(0.2f);
+		entry.setAnimationEnd(0.8f);
+		entry.setTrackEnd(1);
+		run(0.1f, 1.8f, null);
+
+		setup("mix out looping with animation start and end", // 19
+			expect(0, "start", 0, 0), //
+			expect(0, "event 14", 0.3f, 0.3f), //
+			expect(0, "complete", 0.6f, 0.6f), //
+			expect(0, "interrupt", 0.8f, 0.8f), //
+
+			expect(1, "start", 0.1f, 0.8f), //
+			expect(1, "event 0", 0.1f, 0.8f), //
+
+			expect(0, "event 14", 0.9f, 0.9f), //
+			expect(0, "complete", 1.2f, 1.2f), //
+
+			expect(1, "event 14", 0.5f, 1.2f), //
+
+			expect(0, "end", 1.4f, 1.5f), //
+			expect(0, "dispose", 1.4f, 1.5f), //
+
+			expect(1, "event 30", 1, 1.7f), //
+			expect(1, "complete", 1, 1.7f), //
+			expect(1, "end", 1, 1.8f), //
+			expect(1, "dispose", 1, 1.8f) //
+		);
+		entry = state.setAnimation(0, "events0", true);
+		entry.setAnimationStart(0.2f);
+		entry.setAnimationLast(0.2f);
+		entry.setAnimationEnd(0.8f);
+		entry.setEventThreshold(1);
+		entry = state.addAnimation(0, "events1", false, 0.7f);
+		entry.setMixDuration(0.7f);
+		entry.setTrackEnd(1);
+		run(0.1f, 20, null);
+
+		setup("setAnimation with track entry mix", // 20
+			expect(0, "start", 0, 0), //
+			expect(0, "event 0", 0, 0), //
+			expect(0, "event 14", 0.5f, 0.5f), //
+			expect(0, "event 30", 1, 1), //
+			expect(0, "complete", 1, 1), //
+			expect(0, "event 0", 1, 1), //
+			expect(0, "interrupt", 1, 1), //
+
+			expect(1, "start", 0, 1), //
+
+			expect(1, "event 0", 0.1f, 1.1f), //
+			expect(1, "event 14", 0.5f, 1.5f), //
+
+			expect(0, "end", 1.7f, 1.8f), //
+			expect(0, "dispose", 1.7f, 1.8f), //
+
+			expect(1, "event 30", 1, 2), //
+			expect(1, "complete", 1, 2), //
+			expect(1, "end", 1, 2.1f), //
+			expect(1, "dispose", 1, 2.1f) //
+		);
+		state.setAnimation(0, "events0", true);
+		run(0.1f, 1000, new TestListener() {
+			public void frame (float time) {
+				if (MathUtils.isEqual(time, 1f)) {
+					TrackEntry entry = state.setAnimation(0, "events1", false);
+					entry.setMixDuration(0.7f);
+					entry.setTrackEnd(1);
+				}
+			}
+		});
+
+		setup("setAnimation twice", // 21
+			expect(0, "start", 0, 0), //
+			expect(0, "interrupt", 0, 0), //
+			expect(0, "end", 0, 0), //
+			expect(0, "dispose", 0, 0), //
+
+			expect(1, "start", 0, 0), //
+			expect(1, "event 0", 0, 0), //
+			expect(1, "event 14", 0.5f, 0.5f), //
+
+			note("First 2 setAnimation calls are done."),
+
+			expect(1, "interrupt", 0.8f, 0.8f), //
+
+			expect(0, "start", 0, 0.8f), //
+			expect(0, "interrupt", 0, 0.8f), //
+			expect(0, "end", 0, 0.8f), //
+			expect(0, "dispose", 0, 0.8f), //
+
+			expect(2, "start", 0, 0.8f), //
+			expect(2, "event 0", 0.1f, 0.9f), //
+
+			expect(1, "end", 0.9f, 1), //
+			expect(1, "dispose", 0.9f, 1), //
+
+			expect(2, "event 14", 0.5f, 1.3f), //
+			expect(2, "event 30", 1, 1.8f), //
+			expect(2, "complete", 1, 1.8f), //
+			expect(2, "end", 1, 1.9f), //
+			expect(2, "dispose", 1, 1.9f) //
+		);
+		state.setAnimation(0, "events0", false); // First should be ignored.
+		state.setAnimation(0, "events1", false);
+		run(0.1f, 1000, new TestListener() {
+			public void frame (float time) {
+				if (MathUtils.isEqual(time, 0.8f)) {
+					state.setAnimation(0, "events0", false); // First should be ignored.
+					state.setAnimation(0, "events2", false).setTrackEnd(1);
+				}
+			}
+		});
+
+		setup("setAnimation twice with multiple mixing", // 22
+			expect(0, "start", 0, 0), //
+			expect(0, "interrupt", 0, 0), //
+			expect(0, "end", 0, 0), //
+			expect(0, "dispose", 0, 0), //
+
+			expect(1, "start", 0, 0), //
+			expect(1, "event 0", 0, 0), //
+
+			note("First 2 setAnimation calls are done."),
+
+			expect(1, "interrupt", 0.2f, 0.2f), //
+
+			expect(0, "start", 0, 0.2f), //
+			expect(0, "interrupt", 0, 0.2f), //
+			expect(0, "end", 0, 0.2f), //
+			expect(0, "dispose", 0, 0.2f), //
+
+			expect(2, "start", 0, 0.2f), //
+			expect(2, "event 0", 0.1f, 0.3f), //
+
+			note("Second 2 setAnimation calls are done."),
+
+			expect(2, "interrupt", 0.2f, 0.4f), //
+
+			expect(1, "start", 0, 0.4f), //
+			expect(1, "interrupt", 0, 0.4f), //
+			expect(1, "end", 0, 0.4f), //
+			expect(1, "dispose", 0, 0.4f), //
+
+			expect(0, "start", 0, 0.4f), //
+			expect(0, "event 0", 0.1f, 0.5f), //
+
+			expect(1, "end", 0.8f, 0.9f), //
+			expect(1, "dispose", 0.8f, 0.9f), //
+
+			expect(0, "event 14", 0.5f, 0.9f), //
+
+			expect(2, "end", 0.8f, 1.1f), //
+			expect(2, "dispose", 0.8f, 1.1f), //
+
+			expect(0, "event 30", 1, 1.4f), //
+			expect(0, "complete", 1, 1.4f), //
+			expect(0, "end", 1, 1.5f), //
+			expect(0, "dispose", 1, 1.5f) //
+		);
+		stateData.setDefaultMix(0.6f);
+		state.setAnimation(0, "events0", false); // First should be ignored.
+		state.setAnimation(0, "events1", false);
+		run(0.1f, 1000, new TestListener() {
+			public void frame (float time) {
+				if (MathUtils.isEqual(time, 0.2f)) {
+					state.setAnimation(0, "events0", false); // First should be ignored.
+					state.setAnimation(0, "events2", false);
+				}
+				if (MathUtils.isEqual(time, 0.4f)) {
+					state.setAnimation(0, "events1", false); // First should be ignored.
+					state.setAnimation(0, "events0", false).setTrackEnd(1);
+				}
+			}
+		});
+
+		setup("addAnimation with delay on empty track", // 23
+			expect(0, "start", 0, 0), //
+			expect(0, "event 0", 0, 5), //
+			expect(0, "event 14", 0.5f, 5.5f), //
+			expect(0, "event 30", 1, 6), //
+			expect(0, "complete", 1, 6), //
+			expect(0, "end", 1, 6.1f), //
+			expect(0, "dispose", 1, 6.1f) //
+		);
+		state.addAnimation(0, "events0", false, 5).setTrackEnd(1);
+		run(0.1f, 10, null);
+
+		setup("setAnimation during AnimationStateListener"); // 24
+		state.addListener(new AnimationStateListener() {
+			public void start (TrackEntry entry) {
+				if (entry.getAnimation().getName().equals("events0")) state.setAnimation(1, "events1", false);
+			}
+
+			public void interrupt (TrackEntry entry) {
+				state.addAnimation(3, "events1", false, 0);
+			}
+
+			public void end (TrackEntry entry) {
+				if (entry.getAnimation().getName().equals("events0")) state.setAnimation(0, "events1", false);
+			}
+
+			public void dispose (TrackEntry entry) {
+				if (entry.getAnimation().getName().equals("events0")) state.setAnimation(1, "events1", false);
+			}
+
+			public void complete (TrackEntry entry) {
+				if (entry.getAnimation().getName().equals("events0")) state.setAnimation(1, "events1", false);
+			}
+
+			public void event (TrackEntry entry, Event event) {
+				if (entry.getTrackIndex() != 2) state.setAnimation(2, "events1", false);
+			}
+		});
+		state.addAnimation(0, "events0", false, 0);
+		state.addAnimation(0, "events1", false, 0);
+		state.setAnimation(1, "events1", false).setTrackEnd(1);
+		run(0.1f, 10, null);
+
+		setup("clearTrack", // 25
+			expect(0, "start", 0, 0), //
+			expect(0, "event 0", 0, 0), //
+			expect(0, "event 14", 0.5f, 0.5f), //
+			expect(0, "end", 0.7f, 0.7f), //
+			expect(0, "dispose", 0.7f, 0.7f) //
+		);
+		state.addAnimation(0, "events0", false, 0).setTrackEnd(1);
+		run(0.1f, 10, new TestListener() {
+			public void frame (float time) {
+				if (MathUtils.isEqual(time, 0.7f)) state.clearTrack(0);
+			}
+		});
+
+		setup("setEmptyAnimation", // 26
+			expect(0, "start", 0, 0), //
+			expect(0, "event 0", 0, 0), //
+			expect(0, "event 14", 0.5f, 0.5f), //
+			expect(0, "interrupt", 0.7f, 0.7f), //
+
+			expect(-1, "start", 0, 0.7f), //
+			expect(-1, "complete", 0.1f, 0.8f), //
+
+			expect(0, "end", 0.8f, 0.9f), //
+			expect(0, "dispose", 0.8f, 0.9f), //
+
+			expect(-1, "end", 0.2f, 1), //
+			expect(-1, "dispose", 0.2f, 1) //
+		);
+		state.addAnimation(0, "events0", false, 0).setTrackEnd(1);
+		run(0.1f, 10, new TestListener() {
+			public void frame (float time) {
+				if (MathUtils.isEqual(time, 0.7f)) state.setEmptyAnimation(0, 0);
+			}
+		});
+
+		setup("TrackEntry listener"); // 27
+		final AtomicInteger counter = new AtomicInteger();
+		state.addAnimation(0, "events0", false, 0).setListener(new AnimationStateListener() {
+			public void start (TrackEntry entry) {
+				counter.addAndGet(1 << 1);
+			}
+
+			public void interrupt (TrackEntry entry) {
+				counter.addAndGet(1 << 5);
+			}
+
+			public void end (TrackEntry entry) {
+				counter.addAndGet(1 << 9);
+			}
+
+			public void dispose (TrackEntry entry) {
+				counter.addAndGet(1 << 13);
+			}
+
+			public void complete (TrackEntry entry) {
+				counter.addAndGet(1 << 17);
+			}
+
+			public void event (TrackEntry entry, Event event) {
+				counter.addAndGet(1 << 21);
+			}
+		});
+		state.addAnimation(0, "events0", false, 0);
+		state.addAnimation(0, "events1", false, 0);
+		state.setAnimation(1, "events1", false).setTrackEnd(1);
+		run(0.1f, 10, null);
+		if (counter.get() != 15082016) {
+			log("TEST 28 FAILED! " + counter);
+			System.exit(0);
+		}
+
+		setup("looping", // 28
+			expect(0, "start", 0, 0), //
+			expect(0, "event 0", 0, 0), //
+			expect(0, "event 14", 0.5f, 0.5f), //
+			expect(0, "event 30", 1, 1), //
+			expect(0, "complete", 1, 1), //
+			expect(0, "event 0", 1, 1), //
+			expect(0, "event 14", 1.5f, 1.5f), //
+			expect(0, "event 30", 2, 2), //
+			expect(0, "complete", 2, 2), //
+			expect(0, "event 0", 2, 2), //
+			expect(0, "event 14", 2.5f, 2.5f), //
+			expect(0, "end", 2.6f, 2.7f), //
+			expect(0, "dispose", 2.6f, 2.7f) //
+		);
+		state.setAnimation(0, "events0", true).setTrackEnd(2.6f);
+		run(0.1f, 1000, null);
+
+		setup("set next", // 29
+			expect(0, "start", 0, 0), //
+			expect(0, "event 0", 0, 0), //
+			expect(0, "event 14", 0.5f, 0.5f), //
+			expect(0, "event 30", 1, 1), //
+			expect(0, "complete", 1, 1), //
+			expect(0, "interrupt", 1.1f, 1.1f), //
+
+			expect(1, "start", 0.1f, 1.1f), //
+			expect(1, "event 0", 0.1f, 1.1f), //
+
+			expect(0, "end", 1.1f, 1.2f), //
+			expect(0, "dispose", 1.1f, 1.2f), //
+
+			expect(1, "event 14", 0.5f, 1.5f), //
+			expect(1, "event 30", 1, 2), //
+			expect(1, "complete", 1, 2), //
+			expect(1, "end", 1, 2.1f), //
+			expect(1, "dispose", 1, 2.1f) //
+		);
+		state.setAnimation(0, "events0", false);
+		state.addAnimation(0, "events1", false, 0).setTrackEnd(1);
+		run(0.1f, 1000, null);
+
+		System.out.println("AnimationState tests passed.");
+	}
+
+	void setup (String description, Result... expectedArray) {
+		test++;
+		expected.addAll(expectedArray);
+		stateData = new AnimationStateData(skeletonData);
+		state = new AnimationState(stateData);
+
+		Pool trackEntryPool = new Pool<TrackEntry>() {
+			public TrackEntry obtain () {
+				TrackEntry entry = super.obtain();
+				entryCount++;
+				// System.out.println("+1: " + entryCount + " " + entry.hashCode());
+				return entry;
+			}
+
+			protected TrackEntry newObject () {
+				return new TrackEntry();
+			}
+
+			public void free (TrackEntry entry) {
+				entryCount--;
+				// System.out.println("-1: " + entryCount + " " + entry.hashCode());
+				super.free(entry);
+			}
+		};
+		try {
+			Field field = state.getClass().getDeclaredField("trackEntryPool");
+			field.setAccessible(true);
+			field.set(state, trackEntryPool);
+		} catch (Exception ex) {
+			throw new RuntimeException(ex);
+		}
+
+		time = 0;
+		fail = false;
+		log(test + ": " + description);
+		if (expectedArray.length > 0) {
+			state.addListener(stateListener);
+			log(String.format("%-3s%-12s%-7s%-7s%-7s", "#", "EVENT", "TRACK", "TOTAL", "RESULT"));
+		}
+	}
+
+	void run (float incr, float endTime, TestListener listener) {
+		Skeleton skeleton = new Skeleton(skeletonData);
+		state.apply(skeleton);
+		while (time < endTime) {
+			time += incr;
+			skeleton.update(incr);
+			state.update(incr);
+
+			// Reduce float discrepancies for tests.
+			for (TrackEntry entry : state.getTracks()) {
+				if (entry == null) continue;
+				entry.trackTime = round(entry.trackTime, 6);
+				entry.delay = round(entry.delay, 3);
+				if (entry.mixingFrom != null) entry.mixingFrom.trackTime = round(entry.mixingFrom.trackTime, 6);
+			}
+
+			state.apply(skeleton);
+
+			// Apply multiple times to ensure no side effects.
+			if (expected.size > 0) state.removeListener(stateListener);
+			state.apply(skeleton);
+			state.apply(skeleton);
+			if (expected.size > 0) state.addListener(stateListener);
+
+			if (listener != null) listener.frame(time);
+		}
+		state.clearTracks();
+
+		// Expecting more than actual is a failure.
+		for (int i = actual.size, n = expected.size; i < n; i++) {
+			log(String.format("%-29s", "<none>") + "FAIL: " + expected.get(i));
+			fail = true;
+		}
+
+		// Check all allocated entries were freed.
+		if (!fail) {
+			if (entryCount != 0) {
+				log("FAIL: Pool balance: " + entryCount);
+				fail = true;
+			}
+		}
+
+		actual.clear();
+		expected.clear();
+		log("");
+		if (fail) {
+			log("TEST " + test + " FAILED!");
+			System.exit(0);
+		}
+	}
+
+	Result expect (int animationIndex, String name, float trackTime, float totalTime) {
+		Result result = new Result();
+		result.name = name;
+		result.animationIndex = animationIndex;
+		result.trackTime = trackTime;
+		result.totalTime = totalTime;
+		return result;
+	}
+
+	Result actual (String name, TrackEntry entry) {
+		Result result = new Result();
+		result.name = name;
+		result.animationIndex = skeletonData.getAnimations().indexOf(entry.animation, true);
+		result.trackTime = Math.round(entry.trackTime * 1000) / 1000f;
+		result.totalTime = Math.round(time * 1000) / 1000f;
+		return result;
+	}
+
+	Result note (String message) {
+		Result result = new Result();
+		result.name = message;
+		result.note = true;
+		return result;
+	}
+
+	void log (String message) {
+		System.out.println(message);
+	}
+
+	class Result {
+		String name;
+		int animationIndex;
+		float trackTime, totalTime;
+		boolean note;
+
+		public int hashCode () {
+			int result = 31 + animationIndex;
+			result = 31 * result + name.hashCode();
+			result = 31 * result + Float.floatToIntBits(totalTime);
+			result = 31 * result + Float.floatToIntBits(trackTime);
+			return result;
+		}
+
+		public boolean equals (Object obj) {
+			Result other = (Result)obj;
+			if (animationIndex != other.animationIndex) return false;
+			if (!name.equals(other.name)) return false;
+			if (!MathUtils.isEqual(totalTime, other.totalTime)) return false;
+			if (!MathUtils.isEqual(trackTime, other.trackTime)) return false;
+			return true;
+		}
+
+		public String toString () {
+			return String.format("%-3s%-12s%-7s%-7s", "" + animationIndex, name, roundTime(trackTime), roundTime(totalTime));
+		}
+	}
+
+	static float round (float value, int decimals) {
+		float shift = (float)Math.pow(10, decimals);
+		return Math.round(value * shift) / shift;
+	}
+
+	static String roundTime (float value) {
+		String text = Float.toString(round(value, 3));
+		return text.endsWith(".0") ? text.substring(0, text.length() - 2) : text;
+	}
+
+	static interface TestListener {
+		void frame (float time);
+	}
+
+	static public void main (String[] args) throws Exception {
+		new AnimationStateTests();
+	}
+}

+ 109 - 109
spine-libgdx/spine-libgdx-tests/src/com/esotericsoftware/spine/AttachmentTimelineTests.java

@@ -1,109 +1,109 @@
-/******************************************************************************
- * Spine Runtimes License Agreement
- * Last updated January 1, 2020. Replaces all prior versions.
- *
- * Copyright (c) 2013-2020, Esoteric Software LLC
- *
- * Integration of the Spine Runtimes into software or otherwise creating
- * derivative works of the Spine Runtimes is permitted under the terms and
- * conditions of Section 2 of the Spine Editor License Agreement:
- * http://esotericsoftware.com/spine-editor-license
- *
- * Otherwise, it is permitted to integrate the Spine Runtimes into software
- * or otherwise create derivative works of the Spine Runtimes (collectively,
- * "Products"), provided that each user of the Products must obtain their own
- * Spine Editor license and redistribution of the Products in any form must
- * include this license and copyright notice.
- *
- * THE SPINE RUNTIMES ARE PROVIDED BY ESOTERIC SOFTWARE LLC "AS IS" AND ANY
- * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
- * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
- * DISCLAIMED. IN NO EVENT SHALL ESOTERIC SOFTWARE LLC BE LIABLE FOR ANY
- * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
- * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES,
- * BUSINESS INTERRUPTION, OR LOSS OF USE, DATA, OR PROFITS) HOWEVER CAUSED AND
- * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
- * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
- * THE SPINE RUNTIMES, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
- *****************************************************************************/
-
-package com.esotericsoftware.spine;
-
-import com.badlogic.gdx.utils.Array;
-
-import com.esotericsoftware.spine.Animation.AttachmentTimeline;
-import com.esotericsoftware.spine.Animation.Timeline;
-import com.esotericsoftware.spine.attachments.Attachment;
-
-/** Unit tests for {@link AttachmentTimeline}. */
-public class AttachmentTimelineTests {
-	private final SkeletonData skeletonData;
-	private final Skeleton skeleton;
-	private Slot slot;
-	private AnimationState state;
-
-	public AttachmentTimelineTests () {
-		skeletonData = new SkeletonData();
-
-		BoneData boneData = new BoneData(0, "bone", null);
-		skeletonData.getBones().add(boneData);
-
-		skeletonData.getSlots().add(new SlotData(0, "slot", boneData));
-
-		Attachment attachment1 = new Attachment("attachment1") {
-			public Attachment copy () {
-				return null;
-			}
-		};
-		Attachment attachment2 = new Attachment("attachment2") {
-			public Attachment copy () {
-				return null;
-			}
-		};
-
-		Skin skin = new Skin("skin");
-		skin.setAttachment(0, "attachment1", attachment1);
-		skin.setAttachment(0, "attachment2", attachment2);
-		skeletonData.setDefaultSkin(skin);
-
-		skeleton = new Skeleton(skeletonData);
-		slot = skeleton.findSlot("slot");
-
-		AttachmentTimeline timeline = new AttachmentTimeline(2, 0);
-		timeline.setFrame(0, 0, "attachment1");
-		timeline.setFrame(1, 0.5f, "attachment2");
-
-		Animation animation = new Animation("animation", Array.with((Timeline)timeline), 1);
-		animation.setDuration(1);
-
-		state = new AnimationState(new AnimationStateData(skeletonData));
-		state.setAnimation(0, animation, true);
-
-		test(0, attachment1);
-		test(0, attachment1);
-		test(0.25f, attachment1);
-		test(0f, attachment1);
-		test(0.25f, attachment2);
-		test(0.25f, attachment2);
-
-		System.out.println("AttachmentTimeline tests passed.");
-	}
-
-	private void test (float delta, Attachment attachment) {
-		state.update(delta);
-		state.apply(skeleton);
-		if (slot.getAttachment() != attachment)
-			throw new FailException("Wrong attachment: " + slot.getAttachment() + " != " + attachment);
-
-	}
-
-	static class FailException extends RuntimeException {
-		public FailException (String message) {
-			super(message);
-		}
-	}
-
-	static public void main (String[] args) throws Exception {
-		new AttachmentTimelineTests();
-	}
-}
+/******************************************************************************
+ * Spine Runtimes License Agreement
+ * Last updated January 1, 2020. Replaces all prior versions.
+ *
+ * Copyright (c) 2013-2020, Esoteric Software LLC
+ *
+ * Integration of the Spine Runtimes into software or otherwise creating
+ * derivative works of the Spine Runtimes is permitted under the terms and
+ * conditions of Section 2 of the Spine Editor License Agreement:
+ * http://esotericsoftware.com/spine-editor-license
+ *
+ * Otherwise, it is permitted to integrate the Spine Runtimes into software
+ * or otherwise create derivative works of the Spine Runtimes (collectively,
+ * "Products"), provided that each user of the Products must obtain their own
+ * Spine Editor license and redistribution of the Products in any form must
+ * include this license and copyright notice.
+ *
+ * THE SPINE RUNTIMES ARE PROVIDED BY ESOTERIC SOFTWARE LLC "AS IS" AND ANY
+ * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL ESOTERIC SOFTWARE LLC BE LIABLE FOR ANY
+ * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES,
+ * BUSINESS INTERRUPTION, OR LOSS OF USE, DATA, OR PROFITS) HOWEVER CAUSED AND
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
+ * THE SPINE RUNTIMES, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ *****************************************************************************/
+
+package com.esotericsoftware.spine;
+
+import com.badlogic.gdx.utils.Array;
+
+import com.esotericsoftware.spine.Animation.AttachmentTimeline;
+import com.esotericsoftware.spine.Animation.Timeline;
+import com.esotericsoftware.spine.attachments.Attachment;
+
+/** Unit tests for {@link AttachmentTimeline}. */
+public class AttachmentTimelineTests {
+	private final SkeletonData skeletonData;
+	private final Skeleton skeleton;
+	private Slot slot;
+	private AnimationState state;
+
+	public AttachmentTimelineTests () {
+		skeletonData = new SkeletonData();
+
+		BoneData boneData = new BoneData(0, "bone", null);
+		skeletonData.getBones().add(boneData);
+
+		skeletonData.getSlots().add(new SlotData(0, "slot", boneData));
+
+		Attachment attachment1 = new Attachment("attachment1") {
+			public Attachment copy () {
+				return null;
+			}
+		};
+		Attachment attachment2 = new Attachment("attachment2") {
+			public Attachment copy () {
+				return null;
+			}
+		};
+
+		Skin skin = new Skin("skin");
+		skin.setAttachment(0, "attachment1", attachment1);
+		skin.setAttachment(0, "attachment2", attachment2);
+		skeletonData.setDefaultSkin(skin);
+
+		skeleton = new Skeleton(skeletonData);
+		slot = skeleton.findSlot("slot");
+
+		AttachmentTimeline timeline = new AttachmentTimeline(2, 0);
+		timeline.setFrame(0, 0, "attachment1");
+		timeline.setFrame(1, 0.5f, "attachment2");
+
+		Animation animation = new Animation("animation", Array.with((Timeline)timeline), 1);
+		animation.setDuration(1);
+
+		state = new AnimationState(new AnimationStateData(skeletonData));
+		state.setAnimation(0, animation, true);
+
+		test(0, attachment1);
+		test(0, attachment1);
+		test(0.25f, attachment1);
+		test(0f, attachment1);
+		test(0.25f, attachment2);
+		test(0.25f, attachment2);
+
+		System.out.println("AttachmentTimeline tests passed.");
+	}
+
+	private void test (float delta, Attachment attachment) {
+		state.update(delta);
+		state.apply(skeleton);
+		if (slot.getAttachment() != attachment)
+			throw new FailException("Wrong attachment: " + slot.getAttachment() + " != " + attachment);
+
+	}
+
+	static class FailException extends RuntimeException {
+		public FailException (String message) {
+			super(message);
+		}
+	}
+
+	static public void main (String[] args) throws Exception {
+		new AttachmentTimelineTests();
+	}
+}

+ 87 - 87
spine-libgdx/spine-libgdx-tests/src/com/esotericsoftware/spine/BonePlotting.java

@@ -1,87 +1,87 @@
-/******************************************************************************
- * Spine Runtimes License Agreement
- * Last updated January 1, 2020. Replaces all prior versions.
- *
- * Copyright (c) 2013-2020, Esoteric Software LLC
- *
- * Integration of the Spine Runtimes into software or otherwise creating
- * derivative works of the Spine Runtimes is permitted under the terms and
- * conditions of Section 2 of the Spine Editor License Agreement:
- * http://esotericsoftware.com/spine-editor-license
- *
- * Otherwise, it is permitted to integrate the Spine Runtimes into software
- * or otherwise create derivative works of the Spine Runtimes (collectively,
- * "Products"), provided that each user of the Products must obtain their own
- * Spine Editor license and redistribution of the Products in any form must
- * include this license and copyright notice.
- *
- * THE SPINE RUNTIMES ARE PROVIDED BY ESOTERIC SOFTWARE LLC "AS IS" AND ANY
- * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
- * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
- * DISCLAIMED. IN NO EVENT SHALL ESOTERIC SOFTWARE LLC BE LIABLE FOR ANY
- * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
- * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES,
- * BUSINESS INTERRUPTION, OR LOSS OF USE, DATA, OR PROFITS) HOWEVER CAUSED AND
- * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
- * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
- * THE SPINE RUNTIMES, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
- *****************************************************************************/
-
-package com.esotericsoftware.spine;
-
-import com.badlogic.gdx.files.FileHandle;
-
-import com.esotericsoftware.spine.Animation.MixBlend;
-import com.esotericsoftware.spine.Animation.MixDirection;
-import com.esotericsoftware.spine.attachments.AttachmentLoader;
-import com.esotericsoftware.spine.attachments.BoundingBoxAttachment;
-import com.esotericsoftware.spine.attachments.ClippingAttachment;
-import com.esotericsoftware.spine.attachments.MeshAttachment;
-import com.esotericsoftware.spine.attachments.PathAttachment;
-import com.esotericsoftware.spine.attachments.PointAttachment;
-import com.esotericsoftware.spine.attachments.RegionAttachment;
-
-public class BonePlotting {
-	static public void main (String[] args) throws Exception {
-		// This example shows how to load skeleton data and plot a bone transform for each animation.
-		SkeletonJson json = new SkeletonJson(new AttachmentLoader() {
-			public RegionAttachment newRegionAttachment (Skin skin, String name, String path) {
-				return null;
-			}
-
-			public MeshAttachment newMeshAttachment (Skin skin, String name, String path) {
-				return null;
-			}
-
-			public BoundingBoxAttachment newBoundingBoxAttachment (Skin skin, String name) {
-				return null;
-			}
-
-			public ClippingAttachment newClippingAttachment (Skin skin, String name) {
-				return null;
-			}
-
-			public PathAttachment newPathAttachment (Skin skin, String name) {
-				return null;
-			}
-
-			public PointAttachment newPointAttachment (Skin skin, String name) {
-				return null;
-			}
-		});
-		SkeletonData skeletonData = json.readSkeletonData(new FileHandle("assets/spineboy/spineboy-ess.json"));
-		Skeleton skeleton = new Skeleton(skeletonData);
-		Bone bone = skeleton.findBone("gun-tip");
-		float fps = 1 / 15f;
-		for (Animation animation : skeletonData.getAnimations()) {
-			float time = 0;
-			while (time < animation.getDuration()) {
-				animation.apply(skeleton, time, time, false, null, 1, MixBlend.first, MixDirection.in);
-				skeleton.updateWorldTransform();
-				System.out
-					.println(animation.getName() + "," + bone.getWorldX() + "," + bone.getWorldY() + "," + bone.getWorldRotationX());
-				time += fps;
-			}
-		}
-	}
-}
+/******************************************************************************
+ * Spine Runtimes License Agreement
+ * Last updated January 1, 2020. Replaces all prior versions.
+ *
+ * Copyright (c) 2013-2020, Esoteric Software LLC
+ *
+ * Integration of the Spine Runtimes into software or otherwise creating
+ * derivative works of the Spine Runtimes is permitted under the terms and
+ * conditions of Section 2 of the Spine Editor License Agreement:
+ * http://esotericsoftware.com/spine-editor-license
+ *
+ * Otherwise, it is permitted to integrate the Spine Runtimes into software
+ * or otherwise create derivative works of the Spine Runtimes (collectively,
+ * "Products"), provided that each user of the Products must obtain their own
+ * Spine Editor license and redistribution of the Products in any form must
+ * include this license and copyright notice.
+ *
+ * THE SPINE RUNTIMES ARE PROVIDED BY ESOTERIC SOFTWARE LLC "AS IS" AND ANY
+ * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL ESOTERIC SOFTWARE LLC BE LIABLE FOR ANY
+ * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES,
+ * BUSINESS INTERRUPTION, OR LOSS OF USE, DATA, OR PROFITS) HOWEVER CAUSED AND
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
+ * THE SPINE RUNTIMES, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ *****************************************************************************/
+
+package com.esotericsoftware.spine;
+
+import com.badlogic.gdx.files.FileHandle;
+
+import com.esotericsoftware.spine.Animation.MixBlend;
+import com.esotericsoftware.spine.Animation.MixDirection;
+import com.esotericsoftware.spine.attachments.AttachmentLoader;
+import com.esotericsoftware.spine.attachments.BoundingBoxAttachment;
+import com.esotericsoftware.spine.attachments.ClippingAttachment;
+import com.esotericsoftware.spine.attachments.MeshAttachment;
+import com.esotericsoftware.spine.attachments.PathAttachment;
+import com.esotericsoftware.spine.attachments.PointAttachment;
+import com.esotericsoftware.spine.attachments.RegionAttachment;
+
+public class BonePlotting {
+	static public void main (String[] args) throws Exception {
+		// This example shows how to load skeleton data and plot a bone transform for each animation.
+		SkeletonJson json = new SkeletonJson(new AttachmentLoader() {
+			public RegionAttachment newRegionAttachment (Skin skin, String name, String path) {
+				return null;
+			}
+
+			public MeshAttachment newMeshAttachment (Skin skin, String name, String path) {
+				return null;
+			}
+
+			public BoundingBoxAttachment newBoundingBoxAttachment (Skin skin, String name) {
+				return null;
+			}
+
+			public ClippingAttachment newClippingAttachment (Skin skin, String name) {
+				return null;
+			}
+
+			public PathAttachment newPathAttachment (Skin skin, String name) {
+				return null;
+			}
+
+			public PointAttachment newPointAttachment (Skin skin, String name) {
+				return null;
+			}
+		});
+		SkeletonData skeletonData = json.readSkeletonData(new FileHandle("assets/spineboy/spineboy-ess.json"));
+		Skeleton skeleton = new Skeleton(skeletonData);
+		Bone bone = skeleton.findBone("gun-tip");
+		float fps = 1 / 15f;
+		for (Animation animation : skeletonData.getAnimations()) {
+			float time = 0;
+			while (time < animation.getDuration()) {
+				animation.apply(skeleton, time, time, false, null, 1, MixBlend.first, MixDirection.in);
+				skeleton.updateWorldTransform();
+				System.out
+					.println(animation.getName() + "," + bone.getWorldX() + "," + bone.getWorldY() + "," + bone.getWorldRotationX());
+				time += fps;
+			}
+		}
+	}
+}

+ 249 - 249
spine-libgdx/spine-libgdx-tests/src/com/esotericsoftware/spine/Box2DExample.java

@@ -1,249 +1,249 @@
-/******************************************************************************
- * Spine Runtimes License Agreement
- * Last updated January 1, 2020. Replaces all prior versions.
- *
- * Copyright (c) 2013-2020, Esoteric Software LLC
- *
- * Integration of the Spine Runtimes into software or otherwise creating
- * derivative works of the Spine Runtimes is permitted under the terms and
- * conditions of Section 2 of the Spine Editor License Agreement:
- * http://esotericsoftware.com/spine-editor-license
- *
- * Otherwise, it is permitted to integrate the Spine Runtimes into software
- * or otherwise create derivative works of the Spine Runtimes (collectively,
- * "Products"), provided that each user of the Products must obtain their own
- * Spine Editor license and redistribution of the Products in any form must
- * include this license and copyright notice.
- *
- * THE SPINE RUNTIMES ARE PROVIDED BY ESOTERIC SOFTWARE LLC "AS IS" AND ANY
- * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
- * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
- * DISCLAIMED. IN NO EVENT SHALL ESOTERIC SOFTWARE LLC BE LIABLE FOR ANY
- * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
- * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES,
- * BUSINESS INTERRUPTION, OR LOSS OF USE, DATA, OR PROFITS) HOWEVER CAUSED AND
- * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
- * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
- * THE SPINE RUNTIMES, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
- *****************************************************************************/
-
-package com.esotericsoftware.spine;
-
-import com.badlogic.gdx.ApplicationAdapter;
-import com.badlogic.gdx.Gdx;
-import com.badlogic.gdx.backends.lwjgl.LwjglApplication;
-import com.badlogic.gdx.backends.lwjgl.LwjglApplicationConfiguration;
-import com.badlogic.gdx.graphics.OrthographicCamera;
-import com.badlogic.gdx.graphics.g2d.SpriteBatch;
-import com.badlogic.gdx.graphics.g2d.TextureAtlas;
-import com.badlogic.gdx.graphics.g2d.TextureAtlas.AtlasRegion;
-import com.badlogic.gdx.graphics.glutils.ShapeRenderer;
-import com.badlogic.gdx.math.MathUtils;
-import com.badlogic.gdx.math.Matrix4;
-import com.badlogic.gdx.math.Vector2;
-import com.badlogic.gdx.physics.box2d.Body;
-import com.badlogic.gdx.physics.box2d.BodyDef;
-import com.badlogic.gdx.physics.box2d.BodyDef.BodyType;
-import com.badlogic.gdx.physics.box2d.Box2DDebugRenderer;
-import com.badlogic.gdx.physics.box2d.FixtureDef;
-import com.badlogic.gdx.physics.box2d.PolygonShape;
-import com.badlogic.gdx.physics.box2d.World;
-import com.badlogic.gdx.utils.Array;
-import com.badlogic.gdx.utils.ScreenUtils;
-
-import com.esotericsoftware.spine.Animation.MixBlend;
-import com.esotericsoftware.spine.Animation.MixDirection;
-import com.esotericsoftware.spine.attachments.AtlasAttachmentLoader;
-import com.esotericsoftware.spine.attachments.RegionAttachment;
-
-public class Box2DExample extends ApplicationAdapter {
-	SpriteBatch batch;
-	ShapeRenderer renderer;
-	SkeletonRenderer skeletonRenderer;
-
-	TextureAtlas atlas;
-	Skeleton skeleton;
-	Animation animation;
-	float time;
-	Array<Event> events = new Array();
-
-	OrthographicCamera camera;
-	Box2DDebugRenderer box2dRenderer;
-	World world;
-	Body groundBody;
-	Matrix4 transform = new Matrix4();
-	Vector2 vector = new Vector2();
-
-	public void create () {
-		batch = new SpriteBatch();
-		renderer = new ShapeRenderer();
-		skeletonRenderer = new SkeletonRenderer();
-		skeletonRenderer.setPremultipliedAlpha(true);
-
-		atlas = new TextureAtlas(Gdx.files.internal("spineboy/spineboy-pma.atlas"));
-
-		// This loader creates Box2dAttachments instead of RegionAttachments for an easy way to keep
-		// track of the Box2D body for each attachment.
-		AtlasAttachmentLoader atlasLoader = new AtlasAttachmentLoader(atlas) {
-			public RegionAttachment newRegionAttachment (Skin skin, String name, String path) {
-				Box2dAttachment attachment = new Box2dAttachment(name);
-				AtlasRegion region = atlas.findRegion(attachment.getName());
-				if (region == null) throw new RuntimeException("Region not found in atlas: " + attachment);
-				attachment.setRegion(region);
-				return attachment;
-			}
-		};
-		SkeletonJson json = new SkeletonJson(atlasLoader);
-		json.setScale(0.6f * 0.05f);
-		SkeletonData skeletonData = json.readSkeletonData(Gdx.files.internal("spineboy/spineboy-ess.json"));
-		animation = skeletonData.findAnimation("walk");
-
-		skeleton = new Skeleton(skeletonData);
-		skeleton.x = -32;
-		skeleton.y = 1;
-		skeleton.updateWorldTransform();
-
-		// See Box2DTest in libgdx for more detailed information about Box2D setup.
-		camera = new OrthographicCamera(48, 32);
-		camera.position.set(0, 16, 0);
-		box2dRenderer = new Box2DDebugRenderer();
-		createWorld();
-
-		// Create a body for each attachment. Note it is probably better to create just a few bodies rather than one for each
-		// region attachment, but this is just an example.
-		for (Slot slot : skeleton.getSlots()) {
-			if (!(slot.getAttachment() instanceof Box2dAttachment)) continue;
-			Box2dAttachment attachment = (Box2dAttachment)slot.getAttachment();
-
-			PolygonShape boxPoly = new PolygonShape();
-			boxPoly.setAsBox(attachment.getWidth() / 2 * attachment.getScaleX(), attachment.getHeight() / 2 * attachment.getScaleY(),
-				vector.set(attachment.getX(), attachment.getY()), attachment.getRotation() * MathUtils.degRad);
-
-			BodyDef boxBodyDef = new BodyDef();
-			boxBodyDef.type = BodyType.StaticBody;
-			attachment.body = world.createBody(boxBodyDef);
-			attachment.body.createFixture(boxPoly, 1);
-
-			boxPoly.dispose();
-		}
-	}
-
-	public void render () {
-		float delta = Gdx.graphics.getDeltaTime();
-		float remaining = delta;
-		while (remaining > 0) {
-			float d = Math.min(0.016f, remaining);
-			world.step(d, 8, 3);
-			time += d;
-			remaining -= d;
-		}
-
-		camera.update();
-
-		ScreenUtils.clear(0, 0, 0, 0);
-		batch.setProjectionMatrix(camera.projection);
-		batch.setTransformMatrix(camera.view);
-		batch.begin();
-
-		animation.apply(skeleton, time, time, true, events, 1, MixBlend.first, MixDirection.in);
-		skeleton.x += 8 * delta;
-		skeleton.updateWorldTransform();
-		skeletonRenderer.draw(batch, skeleton);
-
-		batch.end();
-
-		// Position each attachment body.
-		for (Slot slot : skeleton.getSlots()) {
-			if (!(slot.getAttachment() instanceof Box2dAttachment)) continue;
-			Box2dAttachment attachment = (Box2dAttachment)slot.getAttachment();
-			if (attachment.body == null) continue;
-			float x = slot.getBone().getWorldX();
-			float y = slot.getBone().getWorldY();
-			float rotation = slot.getBone().getWorldRotationX();
-			attachment.body.setTransform(x, y, rotation * MathUtils.degRad);
-		}
-
-		box2dRenderer.render(world, camera.combined);
-	}
-
-	public void resize (int width, int height) {
-		batch.setProjectionMatrix(camera.projection);
-		renderer.setProjectionMatrix(camera.projection);
-	}
-
-	private void createWorld () {
-		world = new World(new Vector2(0, -10), true);
-
-		float[] vertices = {-0.07421887f, -0.16276085f, -0.12109375f, -0.22786504f, -0.157552f, -0.7122401f, 0.04296875f,
-			-0.7122401f, 0.110677004f, -0.6419276f, 0.13151026f, -0.49869835f, 0.08984375f, -0.3190109f};
-
-		PolygonShape shape = new PolygonShape();
-		shape.set(vertices);
-
-		// next we create a static ground platform. This platform
-		// is not moveable and will not react to any influences from
-		// outside. It will however influence other bodies. First we
-		// create a PolygonShape that holds the form of the platform.
-		// it will be 100 meters wide and 2 meters high, centered
-		// around the origin
-		PolygonShape groundPoly = new PolygonShape();
-		groundPoly.setAsBox(50, 1);
-
-		// next we create the body for the ground platform. It's
-		// simply a static body.
-		BodyDef groundBodyDef = new BodyDef();
-		groundBodyDef.type = BodyType.StaticBody;
-		groundBody = world.createBody(groundBodyDef);
-
-		// finally we add a fixture to the body using the polygon
-		// defined above. Note that we have to dispose PolygonShapes
-		// and CircleShapes once they are no longer used. This is the
-		// only time you have to care explicitely for memomry managment.
-		FixtureDef fixtureDef = new FixtureDef();
-		fixtureDef.shape = groundPoly;
-		fixtureDef.filter.groupIndex = 0;
-		groundBody.createFixture(fixtureDef);
-		groundPoly.dispose();
-
-		PolygonShape boxPoly = new PolygonShape();
-		boxPoly.setAsBox(1, 1);
-
-		// Next we create the 50 box bodies using the PolygonShape we just
-		// defined. This process is similar to the one we used for the ground
-		// body. Note that we reuse the polygon for each body fixture.
-		for (int i = 0; i < 45; i++) {
-			// Create the BodyDef, set a random position above the
-			// ground and create a new body
-			BodyDef boxBodyDef = new BodyDef();
-			boxBodyDef.type = BodyType.DynamicBody;
-			boxBodyDef.position.x = -24 + (float)(Math.random() * 48);
-			boxBodyDef.position.y = 10 + (float)(Math.random() * 100);
-			Body boxBody = world.createBody(boxBodyDef);
-
-			boxBody.createFixture(boxPoly, 1);
-		}
-
-		// we are done, all that's left is disposing the boxPoly
-		boxPoly.dispose();
-	}
-
-	public void dispose () {
-		atlas.dispose();
-	}
-
-	static class Box2dAttachment extends RegionAttachment {
-		Body body;
-
-		public Box2dAttachment (String name) {
-			super(name);
-		}
-	}
-
-	public static void main (String[] args) throws Exception {
-		LwjglApplicationConfiguration config = new LwjglApplicationConfiguration();
-		config.title = "Box2D - Spine";
-		config.width = 640;
-		config.height = 480;
-		new LwjglApplication(new Box2DExample(), config);
-	}
-}
+/******************************************************************************
+ * Spine Runtimes License Agreement
+ * Last updated January 1, 2020. Replaces all prior versions.
+ *
+ * Copyright (c) 2013-2020, Esoteric Software LLC
+ *
+ * Integration of the Spine Runtimes into software or otherwise creating
+ * derivative works of the Spine Runtimes is permitted under the terms and
+ * conditions of Section 2 of the Spine Editor License Agreement:
+ * http://esotericsoftware.com/spine-editor-license
+ *
+ * Otherwise, it is permitted to integrate the Spine Runtimes into software
+ * or otherwise create derivative works of the Spine Runtimes (collectively,
+ * "Products"), provided that each user of the Products must obtain their own
+ * Spine Editor license and redistribution of the Products in any form must
+ * include this license and copyright notice.
+ *
+ * THE SPINE RUNTIMES ARE PROVIDED BY ESOTERIC SOFTWARE LLC "AS IS" AND ANY
+ * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL ESOTERIC SOFTWARE LLC BE LIABLE FOR ANY
+ * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES,
+ * BUSINESS INTERRUPTION, OR LOSS OF USE, DATA, OR PROFITS) HOWEVER CAUSED AND
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
+ * THE SPINE RUNTIMES, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ *****************************************************************************/
+
+package com.esotericsoftware.spine;
+
+import com.badlogic.gdx.ApplicationAdapter;
+import com.badlogic.gdx.Gdx;
+import com.badlogic.gdx.backends.lwjgl.LwjglApplication;
+import com.badlogic.gdx.backends.lwjgl.LwjglApplicationConfiguration;
+import com.badlogic.gdx.graphics.OrthographicCamera;
+import com.badlogic.gdx.graphics.g2d.SpriteBatch;
+import com.badlogic.gdx.graphics.g2d.TextureAtlas;
+import com.badlogic.gdx.graphics.g2d.TextureAtlas.AtlasRegion;
+import com.badlogic.gdx.graphics.glutils.ShapeRenderer;
+import com.badlogic.gdx.math.MathUtils;
+import com.badlogic.gdx.math.Matrix4;
+import com.badlogic.gdx.math.Vector2;
+import com.badlogic.gdx.physics.box2d.Body;
+import com.badlogic.gdx.physics.box2d.BodyDef;
+import com.badlogic.gdx.physics.box2d.BodyDef.BodyType;
+import com.badlogic.gdx.physics.box2d.Box2DDebugRenderer;
+import com.badlogic.gdx.physics.box2d.FixtureDef;
+import com.badlogic.gdx.physics.box2d.PolygonShape;
+import com.badlogic.gdx.physics.box2d.World;
+import com.badlogic.gdx.utils.Array;
+import com.badlogic.gdx.utils.ScreenUtils;
+
+import com.esotericsoftware.spine.Animation.MixBlend;
+import com.esotericsoftware.spine.Animation.MixDirection;
+import com.esotericsoftware.spine.attachments.AtlasAttachmentLoader;
+import com.esotericsoftware.spine.attachments.RegionAttachment;
+
+public class Box2DExample extends ApplicationAdapter {
+	SpriteBatch batch;
+	ShapeRenderer renderer;
+	SkeletonRenderer skeletonRenderer;
+
+	TextureAtlas atlas;
+	Skeleton skeleton;
+	Animation animation;
+	float time;
+	Array<Event> events = new Array();
+
+	OrthographicCamera camera;
+	Box2DDebugRenderer box2dRenderer;
+	World world;
+	Body groundBody;
+	Matrix4 transform = new Matrix4();
+	Vector2 vector = new Vector2();
+
+	public void create () {
+		batch = new SpriteBatch();
+		renderer = new ShapeRenderer();
+		skeletonRenderer = new SkeletonRenderer();
+		skeletonRenderer.setPremultipliedAlpha(true);
+
+		atlas = new TextureAtlas(Gdx.files.internal("spineboy/spineboy-pma.atlas"));
+
+		// This loader creates Box2dAttachments instead of RegionAttachments for an easy way to keep
+		// track of the Box2D body for each attachment.
+		AtlasAttachmentLoader atlasLoader = new AtlasAttachmentLoader(atlas) {
+			public RegionAttachment newRegionAttachment (Skin skin, String name, String path) {
+				Box2dAttachment attachment = new Box2dAttachment(name);
+				AtlasRegion region = atlas.findRegion(attachment.getName());
+				if (region == null) throw new RuntimeException("Region not found in atlas: " + attachment);
+				attachment.setRegion(region);
+				return attachment;
+			}
+		};
+		SkeletonJson json = new SkeletonJson(atlasLoader);
+		json.setScale(0.6f * 0.05f);
+		SkeletonData skeletonData = json.readSkeletonData(Gdx.files.internal("spineboy/spineboy-ess.json"));
+		animation = skeletonData.findAnimation("walk");
+
+		skeleton = new Skeleton(skeletonData);
+		skeleton.x = -32;
+		skeleton.y = 1;
+		skeleton.updateWorldTransform();
+
+		// See Box2DTest in libgdx for more detailed information about Box2D setup.
+		camera = new OrthographicCamera(48, 32);
+		camera.position.set(0, 16, 0);
+		box2dRenderer = new Box2DDebugRenderer();
+		createWorld();
+
+		// Create a body for each attachment. Note it is probably better to create just a few bodies rather than one for each
+		// region attachment, but this is just an example.
+		for (Slot slot : skeleton.getSlots()) {
+			if (!(slot.getAttachment() instanceof Box2dAttachment)) continue;
+			Box2dAttachment attachment = (Box2dAttachment)slot.getAttachment();
+
+			PolygonShape boxPoly = new PolygonShape();
+			boxPoly.setAsBox(attachment.getWidth() / 2 * attachment.getScaleX(), attachment.getHeight() / 2 * attachment.getScaleY(),
+				vector.set(attachment.getX(), attachment.getY()), attachment.getRotation() * MathUtils.degRad);
+
+			BodyDef boxBodyDef = new BodyDef();
+			boxBodyDef.type = BodyType.StaticBody;
+			attachment.body = world.createBody(boxBodyDef);
+			attachment.body.createFixture(boxPoly, 1);
+
+			boxPoly.dispose();
+		}
+	}
+
+	public void render () {
+		float delta = Gdx.graphics.getDeltaTime();
+		float remaining = delta;
+		while (remaining > 0) {
+			float d = Math.min(0.016f, remaining);
+			world.step(d, 8, 3);
+			time += d;
+			remaining -= d;
+		}
+
+		camera.update();
+
+		ScreenUtils.clear(0, 0, 0, 0);
+		batch.setProjectionMatrix(camera.projection);
+		batch.setTransformMatrix(camera.view);
+		batch.begin();
+
+		animation.apply(skeleton, time, time, true, events, 1, MixBlend.first, MixDirection.in);
+		skeleton.x += 8 * delta;
+		skeleton.updateWorldTransform();
+		skeletonRenderer.draw(batch, skeleton);
+
+		batch.end();
+
+		// Position each attachment body.
+		for (Slot slot : skeleton.getSlots()) {
+			if (!(slot.getAttachment() instanceof Box2dAttachment)) continue;
+			Box2dAttachment attachment = (Box2dAttachment)slot.getAttachment();
+			if (attachment.body == null) continue;
+			float x = slot.getBone().getWorldX();
+			float y = slot.getBone().getWorldY();
+			float rotation = slot.getBone().getWorldRotationX();
+			attachment.body.setTransform(x, y, rotation * MathUtils.degRad);
+		}
+
+		box2dRenderer.render(world, camera.combined);
+	}
+
+	public void resize (int width, int height) {
+		batch.setProjectionMatrix(camera.projection);
+		renderer.setProjectionMatrix(camera.projection);
+	}
+
+	private void createWorld () {
+		world = new World(new Vector2(0, -10), true);
+
+		float[] vertices = {-0.07421887f, -0.16276085f, -0.12109375f, -0.22786504f, -0.157552f, -0.7122401f, 0.04296875f,
+			-0.7122401f, 0.110677004f, -0.6419276f, 0.13151026f, -0.49869835f, 0.08984375f, -0.3190109f};
+
+		PolygonShape shape = new PolygonShape();
+		shape.set(vertices);
+
+		// next we create a static ground platform. This platform
+		// is not moveable and will not react to any influences from
+		// outside. It will however influence other bodies. First we
+		// create a PolygonShape that holds the form of the platform.
+		// it will be 100 meters wide and 2 meters high, centered
+		// around the origin
+		PolygonShape groundPoly = new PolygonShape();
+		groundPoly.setAsBox(50, 1);
+
+		// next we create the body for the ground platform. It's
+		// simply a static body.
+		BodyDef groundBodyDef = new BodyDef();
+		groundBodyDef.type = BodyType.StaticBody;
+		groundBody = world.createBody(groundBodyDef);
+
+		// finally we add a fixture to the body using the polygon
+		// defined above. Note that we have to dispose PolygonShapes
+		// and CircleShapes once they are no longer used. This is the
+		// only time you have to care explicitely for memomry managment.
+		FixtureDef fixtureDef = new FixtureDef();
+		fixtureDef.shape = groundPoly;
+		fixtureDef.filter.groupIndex = 0;
+		groundBody.createFixture(fixtureDef);
+		groundPoly.dispose();
+
+		PolygonShape boxPoly = new PolygonShape();
+		boxPoly.setAsBox(1, 1);
+
+		// Next we create the 50 box bodies using the PolygonShape we just
+		// defined. This process is similar to the one we used for the ground
+		// body. Note that we reuse the polygon for each body fixture.
+		for (int i = 0; i < 45; i++) {
+			// Create the BodyDef, set a random position above the
+			// ground and create a new body
+			BodyDef boxBodyDef = new BodyDef();
+			boxBodyDef.type = BodyType.DynamicBody;
+			boxBodyDef.position.x = -24 + (float)(Math.random() * 48);
+			boxBodyDef.position.y = 10 + (float)(Math.random() * 100);
+			Body boxBody = world.createBody(boxBodyDef);
+
+			boxBody.createFixture(boxPoly, 1);
+		}
+
+		// we are done, all that's left is disposing the boxPoly
+		boxPoly.dispose();
+	}
+
+	public void dispose () {
+		atlas.dispose();
+	}
+
+	static class Box2dAttachment extends RegionAttachment {
+		Body body;
+
+		public Box2dAttachment (String name) {
+			super(name);
+		}
+	}
+
+	public static void main (String[] args) throws Exception {
+		LwjglApplicationConfiguration config = new LwjglApplicationConfiguration();
+		config.title = "Box2D - Spine";
+		config.width = 640;
+		config.height = 480;
+		new LwjglApplication(new Box2DExample(), config);
+	}
+}

+ 228 - 228
spine-libgdx/spine-libgdx-tests/src/com/esotericsoftware/spine/EventTimelineTests.java

@@ -1,228 +1,228 @@
-/******************************************************************************
- * Spine Runtimes License Agreement
- * Last updated January 1, 2020. Replaces all prior versions.
- *
- * Copyright (c) 2013-2020, Esoteric Software LLC
- *
- * Integration of the Spine Runtimes into software or otherwise creating
- * derivative works of the Spine Runtimes is permitted under the terms and
- * conditions of Section 2 of the Spine Editor License Agreement:
- * http://esotericsoftware.com/spine-editor-license
- *
- * Otherwise, it is permitted to integrate the Spine Runtimes into software
- * or otherwise create derivative works of the Spine Runtimes (collectively,
- * "Products"), provided that each user of the Products must obtain their own
- * Spine Editor license and redistribution of the Products in any form must
- * include this license and copyright notice.
- *
- * THE SPINE RUNTIMES ARE PROVIDED BY ESOTERIC SOFTWARE LLC "AS IS" AND ANY
- * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
- * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
- * DISCLAIMED. IN NO EVENT SHALL ESOTERIC SOFTWARE LLC BE LIABLE FOR ANY
- * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
- * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES,
- * BUSINESS INTERRUPTION, OR LOSS OF USE, DATA, OR PROFITS) HOWEVER CAUSED AND
- * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
- * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
- * THE SPINE RUNTIMES, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
- *****************************************************************************/
-
-package com.esotericsoftware.spine;
-
-import java.util.Arrays;
-
-import com.badlogic.gdx.utils.Array;
-import com.badlogic.gdx.utils.StringBuilder;
-
-import com.esotericsoftware.spine.Animation.EventTimeline;
-import com.esotericsoftware.spine.Animation.MixBlend;
-import com.esotericsoftware.spine.Animation.MixDirection;
-
-/** Unit tests for {@link EventTimeline}. */
-public class EventTimelineTests {
-	private final SkeletonData skeletonData;
-	private final Skeleton skeleton;
-	private final Array<Event> firedEvents = new Array();
-	private EventTimeline timeline = new EventTimeline(1);
-	private char[] events;
-	private float[] frames;
-
-	public EventTimelineTests () {
-		skeletonData = new SkeletonData();
-		skeleton = new Skeleton(skeletonData);
-
-		test(0);
-		test(1);
-		test(1, 1);
-		test(1, 2);
-		test(1, 2);
-		test(1, 2, 3);
-		test(1, 2, 3);
-		test(0, 0, 0);
-		test(0, 0, 1);
-		test(0, 1, 1);
-		test(1, 1, 1);
-		test(1, 2, 3, 4);
-		test(0, 2, 3, 4);
-		test(0, 2, 2, 4);
-		test(0, 0, 0, 0);
-		test(2, 2, 2, 2);
-		test(0.1f);
-		test(0.1f, 0.1f);
-		test(0.1f, 50f);
-		test(0.1f, 0.2f, 0.3f, 0.4f);
-		test(1, 2, 3, 4, 5, 6, 6, 7, 7, 8, 9, 10, 11, 11.01f, 12, 12, 12, 12);
-
-		System.out.println("EventTimeline tests passed.");
-	}
-
-	private void test (float... frames) {
-		int eventCount = frames.length;
-
-		StringBuilder buffer = new StringBuilder();
-		for (int i = 0; i < eventCount; i++)
-			buffer.append((char)('a' + i));
-
-		this.events = buffer.toString().toCharArray();
-		this.frames = frames;
-		timeline = new EventTimeline(eventCount);
-
-		float maxFrame = 0;
-		int distinctCount = 0;
-		float lastFrame = -1;
-		for (int i = 0; i < eventCount; i++) {
-			float frame = frames[i];
-			Event event = new Event(frame, new EventData("" + events[i]));
-			timeline.setFrame(i, event);
-			maxFrame = Math.max(maxFrame, frame);
-			if (lastFrame != frame) distinctCount++;
-			lastFrame = frame;
-		}
-
-		run(0, 99, 0.1f);
-		run(0, maxFrame, 0.1f);
-		run(frames[0], 999, 2f);
-		run(frames[0], maxFrame, 0.1f);
-		run(0, maxFrame, (float)Math.ceil(maxFrame / 100));
-		run(0, 99, 0.1f);
-		run(0, 999, 100f);
-		if (distinctCount > 1) {
-			float epsilon = 0.02f;
-			// Ending before last.
-			run(frames[0], maxFrame - epsilon, 0.1f);
-			run(0, maxFrame - epsilon, 0.1f);
-			// Starting after first.
-			run(frames[0] + epsilon, maxFrame, 0.1f);
-			run(frames[0] + epsilon, 99, 0.1f);
-		}
-	}
-
-	private void run (float startTime, float endTime, float timeStep) {
-		timeStep = Math.max(timeStep, 0.00001f);
-		boolean loop = false;
-		try {
-			fire(startTime, endTime, timeStep, loop, false);
-			loop = true;
-			fire(startTime, endTime, timeStep, loop, false);
-		} catch (FailException ignored) {
-			try {
-				fire(startTime, endTime, timeStep, loop, true);
-			} catch (FailException ex) {
-				System.out.println(ex.getMessage());
-				System.exit(0);
-			}
-		}
-	}
-
-	private void fire (float timeStart, float timeEnd, float timeStep, boolean loop, boolean print) {
-		if (print) {
-			System.out.println("events: " + Arrays.toString(events));
-			System.out.println("frames: " + Arrays.toString(frames));
-			System.out.println("timeStart: " + timeStart);
-			System.out.println("timeEnd: " + timeEnd);
-			System.out.println("timeStep: " + timeStep);
-			System.out.println("loop: " + loop);
-		}
-
-		// Expected starting event.
-		int eventIndex = 0;
-		while (frames[eventIndex] < timeStart)
-			eventIndex++;
-
-		// Expected number of events when not looping.
-		int eventsCount = frames.length;
-		while (frames[eventsCount - 1] > timeEnd)
-			eventsCount--;
-		eventsCount -= eventIndex;
-
-		float duration = frames[eventIndex + eventsCount - 1];
-		if (loop && duration > 0) { // When looping timeStep can't be > nyquist.
-			while (timeStep > duration / 2f)
-				timeStep /= 2f;
-		}
-		// duration *= 2; // Everything should still pass with this uncommented.
-
-		firedEvents.clear();
-		int i = 0;
-		float lastTime = timeStart - 0.00001f;
-		float timeLooped, lastTimeLooped;
-		while (true) {
-			float time = Math.min(timeStart + timeStep * i, timeEnd);
-			lastTimeLooped = lastTime;
-			timeLooped = time;
-			if (loop && duration != 0) {
-				lastTimeLooped %= duration;
-				timeLooped %= duration;
-			}
-
-			int beforeCount = firedEvents.size;
-			Array<Event> original = new Array(firedEvents);
-			timeline.apply(skeleton, lastTimeLooped, timeLooped, firedEvents, 1, MixBlend.first, MixDirection.in);
-
-			while (beforeCount < firedEvents.size) {
-				char fired = firedEvents.get(beforeCount).getData().getName().charAt(0);
-				if (loop) {
-					eventIndex %= events.length;
-				} else {
-					if (firedEvents.size > eventsCount) {
-						if (print) System.out.println(lastTimeLooped + "->" + timeLooped + ": " + fired + " == ?");
-						timeline.apply(skeleton, lastTimeLooped, timeLooped, original, 1, MixBlend.first, MixDirection.in);
-						fail("Too many events fired.");
-					}
-				}
-				if (print) {
-					System.out.println(lastTimeLooped + "->" + timeLooped + ": " + fired + " == " + events[eventIndex]);
-				}
-				if (fired != events[eventIndex]) {
-					timeline.apply(skeleton, lastTimeLooped, timeLooped, original, 1, MixBlend.first, MixDirection.in);
-					fail("Wrong event fired.");
-				}
-				eventIndex++;
-				beforeCount++;
-			}
-
-			if (time >= timeEnd) break;
-			lastTime = time;
-			i++;
-		}
-		if (firedEvents.size < eventsCount) {
-			timeline.apply(skeleton, lastTimeLooped, timeLooped, firedEvents, 1, MixBlend.first, MixDirection.in);
-			if (print) System.out.println(firedEvents);
-			fail("Event not fired: " + events[eventIndex] + ", " + frames[eventIndex]);
-		}
-	}
-
-	private void fail (String message) {
-		throw new FailException(message);
-	}
-
-	static class FailException extends RuntimeException {
-		public FailException (String message) {
-			super(message);
-		}
-	}
-
-	static public void main (String[] args) throws Exception {
-		new EventTimelineTests();
-	}
-}
+/******************************************************************************
+ * Spine Runtimes License Agreement
+ * Last updated January 1, 2020. Replaces all prior versions.
+ *
+ * Copyright (c) 2013-2020, Esoteric Software LLC
+ *
+ * Integration of the Spine Runtimes into software or otherwise creating
+ * derivative works of the Spine Runtimes is permitted under the terms and
+ * conditions of Section 2 of the Spine Editor License Agreement:
+ * http://esotericsoftware.com/spine-editor-license
+ *
+ * Otherwise, it is permitted to integrate the Spine Runtimes into software
+ * or otherwise create derivative works of the Spine Runtimes (collectively,
+ * "Products"), provided that each user of the Products must obtain their own
+ * Spine Editor license and redistribution of the Products in any form must
+ * include this license and copyright notice.
+ *
+ * THE SPINE RUNTIMES ARE PROVIDED BY ESOTERIC SOFTWARE LLC "AS IS" AND ANY
+ * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL ESOTERIC SOFTWARE LLC BE LIABLE FOR ANY
+ * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES,
+ * BUSINESS INTERRUPTION, OR LOSS OF USE, DATA, OR PROFITS) HOWEVER CAUSED AND
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
+ * THE SPINE RUNTIMES, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ *****************************************************************************/
+
+package com.esotericsoftware.spine;
+
+import java.util.Arrays;
+
+import com.badlogic.gdx.utils.Array;
+import com.badlogic.gdx.utils.StringBuilder;
+
+import com.esotericsoftware.spine.Animation.EventTimeline;
+import com.esotericsoftware.spine.Animation.MixBlend;
+import com.esotericsoftware.spine.Animation.MixDirection;
+
+/** Unit tests for {@link EventTimeline}. */
+public class EventTimelineTests {
+	private final SkeletonData skeletonData;
+	private final Skeleton skeleton;
+	private final Array<Event> firedEvents = new Array();
+	private EventTimeline timeline = new EventTimeline(1);
+	private char[] events;
+	private float[] frames;
+
+	public EventTimelineTests () {
+		skeletonData = new SkeletonData();
+		skeleton = new Skeleton(skeletonData);
+
+		test(0);
+		test(1);
+		test(1, 1);
+		test(1, 2);
+		test(1, 2);
+		test(1, 2, 3);
+		test(1, 2, 3);
+		test(0, 0, 0);
+		test(0, 0, 1);
+		test(0, 1, 1);
+		test(1, 1, 1);
+		test(1, 2, 3, 4);
+		test(0, 2, 3, 4);
+		test(0, 2, 2, 4);
+		test(0, 0, 0, 0);
+		test(2, 2, 2, 2);
+		test(0.1f);
+		test(0.1f, 0.1f);
+		test(0.1f, 50f);
+		test(0.1f, 0.2f, 0.3f, 0.4f);
+		test(1, 2, 3, 4, 5, 6, 6, 7, 7, 8, 9, 10, 11, 11.01f, 12, 12, 12, 12);
+
+		System.out.println("EventTimeline tests passed.");
+	}
+
+	private void test (float... frames) {
+		int eventCount = frames.length;
+
+		StringBuilder buffer = new StringBuilder();
+		for (int i = 0; i < eventCount; i++)
+			buffer.append((char)('a' + i));
+
+		this.events = buffer.toString().toCharArray();
+		this.frames = frames;
+		timeline = new EventTimeline(eventCount);
+
+		float maxFrame = 0;
+		int distinctCount = 0;
+		float lastFrame = -1;
+		for (int i = 0; i < eventCount; i++) {
+			float frame = frames[i];
+			Event event = new Event(frame, new EventData("" + events[i]));
+			timeline.setFrame(i, event);
+			maxFrame = Math.max(maxFrame, frame);
+			if (lastFrame != frame) distinctCount++;
+			lastFrame = frame;
+		}
+
+		run(0, 99, 0.1f);
+		run(0, maxFrame, 0.1f);
+		run(frames[0], 999, 2f);
+		run(frames[0], maxFrame, 0.1f);
+		run(0, maxFrame, (float)Math.ceil(maxFrame / 100));
+		run(0, 99, 0.1f);
+		run(0, 999, 100f);
+		if (distinctCount > 1) {
+			float epsilon = 0.02f;
+			// Ending before last.
+			run(frames[0], maxFrame - epsilon, 0.1f);
+			run(0, maxFrame - epsilon, 0.1f);
+			// Starting after first.
+			run(frames[0] + epsilon, maxFrame, 0.1f);
+			run(frames[0] + epsilon, 99, 0.1f);
+		}
+	}
+
+	private void run (float startTime, float endTime, float timeStep) {
+		timeStep = Math.max(timeStep, 0.00001f);
+		boolean loop = false;
+		try {
+			fire(startTime, endTime, timeStep, loop, false);
+			loop = true;
+			fire(startTime, endTime, timeStep, loop, false);
+		} catch (FailException ignored) {
+			try {
+				fire(startTime, endTime, timeStep, loop, true);
+			} catch (FailException ex) {
+				System.out.println(ex.getMessage());
+				System.exit(0);
+			}
+		}
+	}
+
+	private void fire (float timeStart, float timeEnd, float timeStep, boolean loop, boolean print) {
+		if (print) {
+			System.out.println("events: " + Arrays.toString(events));
+			System.out.println("frames: " + Arrays.toString(frames));
+			System.out.println("timeStart: " + timeStart);
+			System.out.println("timeEnd: " + timeEnd);
+			System.out.println("timeStep: " + timeStep);
+			System.out.println("loop: " + loop);
+		}
+
+		// Expected starting event.
+		int eventIndex = 0;
+		while (frames[eventIndex] < timeStart)
+			eventIndex++;
+
+		// Expected number of events when not looping.
+		int eventsCount = frames.length;
+		while (frames[eventsCount - 1] > timeEnd)
+			eventsCount--;
+		eventsCount -= eventIndex;
+
+		float duration = frames[eventIndex + eventsCount - 1];
+		if (loop && duration > 0) { // When looping timeStep can't be > nyquist.
+			while (timeStep > duration / 2f)
+				timeStep /= 2f;
+		}
+		// duration *= 2; // Everything should still pass with this uncommented.
+
+		firedEvents.clear();
+		int i = 0;
+		float lastTime = timeStart - 0.00001f;
+		float timeLooped, lastTimeLooped;
+		while (true) {
+			float time = Math.min(timeStart + timeStep * i, timeEnd);
+			lastTimeLooped = lastTime;
+			timeLooped = time;
+			if (loop && duration != 0) {
+				lastTimeLooped %= duration;
+				timeLooped %= duration;
+			}
+
+			int beforeCount = firedEvents.size;
+			Array<Event> original = new Array(firedEvents);
+			timeline.apply(skeleton, lastTimeLooped, timeLooped, firedEvents, 1, MixBlend.first, MixDirection.in);
+
+			while (beforeCount < firedEvents.size) {
+				char fired = firedEvents.get(beforeCount).getData().getName().charAt(0);
+				if (loop) {
+					eventIndex %= events.length;
+				} else {
+					if (firedEvents.size > eventsCount) {
+						if (print) System.out.println(lastTimeLooped + "->" + timeLooped + ": " + fired + " == ?");
+						timeline.apply(skeleton, lastTimeLooped, timeLooped, original, 1, MixBlend.first, MixDirection.in);
+						fail("Too many events fired.");
+					}
+				}
+				if (print) {
+					System.out.println(lastTimeLooped + "->" + timeLooped + ": " + fired + " == " + events[eventIndex]);
+				}
+				if (fired != events[eventIndex]) {
+					timeline.apply(skeleton, lastTimeLooped, timeLooped, original, 1, MixBlend.first, MixDirection.in);
+					fail("Wrong event fired.");
+				}
+				eventIndex++;
+				beforeCount++;
+			}
+
+			if (time >= timeEnd) break;
+			lastTime = time;
+			i++;
+		}
+		if (firedEvents.size < eventsCount) {
+			timeline.apply(skeleton, lastTimeLooped, timeLooped, firedEvents, 1, MixBlend.first, MixDirection.in);
+			if (print) System.out.println(firedEvents);
+			fail("Event not fired: " + events[eventIndex] + ", " + frames[eventIndex]);
+		}
+	}
+
+	private void fail (String message) {
+		throw new FailException(message);
+	}
+
+	static class FailException extends RuntimeException {
+		public FailException (String message) {
+			super(message);
+		}
+	}
+
+	static public void main (String[] args) throws Exception {
+		new EventTimelineTests();
+	}
+}

+ 145 - 145
spine-libgdx/spine-libgdx-tests/src/com/esotericsoftware/spine/MixTest.java

@@ -1,145 +1,145 @@
-/******************************************************************************
- * Spine Runtimes License Agreement
- * Last updated January 1, 2020. Replaces all prior versions.
- *
- * Copyright (c) 2013-2020, Esoteric Software LLC
- *
- * Integration of the Spine Runtimes into software or otherwise creating
- * derivative works of the Spine Runtimes is permitted under the terms and
- * conditions of Section 2 of the Spine Editor License Agreement:
- * http://esotericsoftware.com/spine-editor-license
- *
- * Otherwise, it is permitted to integrate the Spine Runtimes into software
- * or otherwise create derivative works of the Spine Runtimes (collectively,
- * "Products"), provided that each user of the Products must obtain their own
- * Spine Editor license and redistribution of the Products in any form must
- * include this license and copyright notice.
- *
- * THE SPINE RUNTIMES ARE PROVIDED BY ESOTERIC SOFTWARE LLC "AS IS" AND ANY
- * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
- * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
- * DISCLAIMED. IN NO EVENT SHALL ESOTERIC SOFTWARE LLC BE LIABLE FOR ANY
- * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
- * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES,
- * BUSINESS INTERRUPTION, OR LOSS OF USE, DATA, OR PROFITS) HOWEVER CAUSED AND
- * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
- * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
- * THE SPINE RUNTIMES, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
- *****************************************************************************/
-
-package com.esotericsoftware.spine;
-
-import com.badlogic.gdx.ApplicationAdapter;
-import com.badlogic.gdx.Gdx;
-import com.badlogic.gdx.backends.lwjgl.LwjglApplication;
-import com.badlogic.gdx.graphics.g2d.SpriteBatch;
-import com.badlogic.gdx.graphics.g2d.TextureAtlas;
-import com.badlogic.gdx.utils.Array;
-import com.badlogic.gdx.utils.ScreenUtils;
-
-import com.esotericsoftware.spine.Animation.MixBlend;
-import com.esotericsoftware.spine.Animation.MixDirection;
-
-public class MixTest extends ApplicationAdapter {
-	SpriteBatch batch;
-	float time;
-	Array<Event> events = new Array();
-
-	SkeletonRenderer renderer;
-	SkeletonRendererDebug debugRenderer;
-
-	SkeletonData skeletonData;
-	Skeleton skeleton;
-	Animation walkAnimation;
-	Animation jumpAnimation;
-
-	public void create () {
-		batch = new SpriteBatch();
-		renderer = new SkeletonRenderer();
-		renderer.setPremultipliedAlpha(true);
-		debugRenderer = new SkeletonRendererDebug();
-
-		final String name = "spineboy/spineboy";
-
-		TextureAtlas atlas = new TextureAtlas(Gdx.files.internal(name + "-pma.atlas"));
-
-		if (true) {
-			SkeletonJson json = new SkeletonJson(atlas);
-			json.setScale(0.6f);
-			skeletonData = json.readSkeletonData(Gdx.files.internal(name + "-ess.json"));
-		} else {
-			SkeletonBinary binary = new SkeletonBinary(atlas);
-			binary.setScale(0.6f);
-			skeletonData = binary.readSkeletonData(Gdx.files.internal(name + "-ess.skel"));
-		}
-		walkAnimation = skeletonData.findAnimation("walk");
-		jumpAnimation = skeletonData.findAnimation("jump");
-
-		skeleton = new Skeleton(skeletonData);
-		skeleton.updateWorldTransform();
-		skeleton.setPosition(-50, 20);
-	}
-
-	public void render () {
-		float delta = Gdx.graphics.getDeltaTime() * 0.25f; // Reduced to make mixing easier to see.
-
-		float jump = jumpAnimation.getDuration();
-		float beforeJump = 1f;
-		float blendIn = 0.2f;
-		float blendOut = 0.2f;
-		float blendOutStart = beforeJump + jump - blendOut;
-		float total = 3.75f;
-
-		time += delta;
-
-		float speed = 180;
-		if (time > beforeJump + blendIn && time < blendOutStart) speed = 360;
-		skeleton.setX(skeleton.getX() + speed * delta);
-
-		ScreenUtils.clear(0, 0, 0, 0);
-
-		// This shows how to manage state manually. See SimpleTest1 for a higher level API using AnimationState.
-		if (time > total) {
-			// restart
-			time = 0;
-			skeleton.setX(-50);
-		} else if (time > beforeJump + jump) {
-			// just walk after jump
-			walkAnimation.apply(skeleton, time, time, true, events, 1, MixBlend.first, MixDirection.in);
-		} else if (time > blendOutStart) {
-			// blend out jump
-			walkAnimation.apply(skeleton, time, time, true, events, 1, MixBlend.first, MixDirection.in);
-			jumpAnimation.apply(skeleton, time - beforeJump, time - beforeJump, false, events, 1 - (time - blendOutStart) / blendOut,
-				MixBlend.first, MixDirection.in);
-		} else if (time > beforeJump + blendIn) {
-			// just jump
-			jumpAnimation.apply(skeleton, time - beforeJump, time - beforeJump, false, events, 1, MixBlend.first, MixDirection.in);
-		} else if (time > beforeJump) {
-			// blend in jump
-			walkAnimation.apply(skeleton, time, time, true, events, 1, MixBlend.first, MixDirection.in);
-			jumpAnimation.apply(skeleton, time - beforeJump, time - beforeJump, false, events, (time - beforeJump) / blendIn,
-				MixBlend.first, MixDirection.in);
-		} else {
-			// just walk before jump
-			walkAnimation.apply(skeleton, time, time, true, events, 1, MixBlend.first, MixDirection.in);
-		}
-
-		skeleton.updateWorldTransform();
-		skeleton.update(Gdx.graphics.getDeltaTime());
-
-		batch.begin();
-		renderer.draw(batch, skeleton);
-		batch.end();
-
-		debugRenderer.draw(skeleton);
-	}
-
-	public void resize (int width, int height) {
-		batch.getProjectionMatrix().setToOrtho2D(0, 0, width, height);
-		debugRenderer.getShapeRenderer().setProjectionMatrix(batch.getProjectionMatrix());
-	}
-
-	public static void main (String[] args) throws Exception {
-		new LwjglApplication(new MixTest());
-	}
-}
+/******************************************************************************
+ * Spine Runtimes License Agreement
+ * Last updated January 1, 2020. Replaces all prior versions.
+ *
+ * Copyright (c) 2013-2020, Esoteric Software LLC
+ *
+ * Integration of the Spine Runtimes into software or otherwise creating
+ * derivative works of the Spine Runtimes is permitted under the terms and
+ * conditions of Section 2 of the Spine Editor License Agreement:
+ * http://esotericsoftware.com/spine-editor-license
+ *
+ * Otherwise, it is permitted to integrate the Spine Runtimes into software
+ * or otherwise create derivative works of the Spine Runtimes (collectively,
+ * "Products"), provided that each user of the Products must obtain their own
+ * Spine Editor license and redistribution of the Products in any form must
+ * include this license and copyright notice.
+ *
+ * THE SPINE RUNTIMES ARE PROVIDED BY ESOTERIC SOFTWARE LLC "AS IS" AND ANY
+ * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL ESOTERIC SOFTWARE LLC BE LIABLE FOR ANY
+ * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES,
+ * BUSINESS INTERRUPTION, OR LOSS OF USE, DATA, OR PROFITS) HOWEVER CAUSED AND
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
+ * THE SPINE RUNTIMES, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ *****************************************************************************/
+
+package com.esotericsoftware.spine;
+
+import com.badlogic.gdx.ApplicationAdapter;
+import com.badlogic.gdx.Gdx;
+import com.badlogic.gdx.backends.lwjgl.LwjglApplication;
+import com.badlogic.gdx.graphics.g2d.SpriteBatch;
+import com.badlogic.gdx.graphics.g2d.TextureAtlas;
+import com.badlogic.gdx.utils.Array;
+import com.badlogic.gdx.utils.ScreenUtils;
+
+import com.esotericsoftware.spine.Animation.MixBlend;
+import com.esotericsoftware.spine.Animation.MixDirection;
+
+public class MixTest extends ApplicationAdapter {
+	SpriteBatch batch;
+	float time;
+	Array<Event> events = new Array();
+
+	SkeletonRenderer renderer;
+	SkeletonRendererDebug debugRenderer;
+
+	SkeletonData skeletonData;
+	Skeleton skeleton;
+	Animation walkAnimation;
+	Animation jumpAnimation;
+
+	public void create () {
+		batch = new SpriteBatch();
+		renderer = new SkeletonRenderer();
+		renderer.setPremultipliedAlpha(true);
+		debugRenderer = new SkeletonRendererDebug();
+
+		final String name = "spineboy/spineboy";
+
+		TextureAtlas atlas = new TextureAtlas(Gdx.files.internal(name + "-pma.atlas"));
+
+		if (true) {
+			SkeletonJson json = new SkeletonJson(atlas);
+			json.setScale(0.6f);
+			skeletonData = json.readSkeletonData(Gdx.files.internal(name + "-ess.json"));
+		} else {
+			SkeletonBinary binary = new SkeletonBinary(atlas);
+			binary.setScale(0.6f);
+			skeletonData = binary.readSkeletonData(Gdx.files.internal(name + "-ess.skel"));
+		}
+		walkAnimation = skeletonData.findAnimation("walk");
+		jumpAnimation = skeletonData.findAnimation("jump");
+
+		skeleton = new Skeleton(skeletonData);
+		skeleton.updateWorldTransform();
+		skeleton.setPosition(-50, 20);
+	}
+
+	public void render () {
+		float delta = Gdx.graphics.getDeltaTime() * 0.25f; // Reduced to make mixing easier to see.
+
+		float jump = jumpAnimation.getDuration();
+		float beforeJump = 1f;
+		float blendIn = 0.2f;
+		float blendOut = 0.2f;
+		float blendOutStart = beforeJump + jump - blendOut;
+		float total = 3.75f;
+
+		time += delta;
+
+		float speed = 180;
+		if (time > beforeJump + blendIn && time < blendOutStart) speed = 360;
+		skeleton.setX(skeleton.getX() + speed * delta);
+
+		ScreenUtils.clear(0, 0, 0, 0);
+
+		// This shows how to manage state manually. See SimpleTest1 for a higher level API using AnimationState.
+		if (time > total) {
+			// restart
+			time = 0;
+			skeleton.setX(-50);
+		} else if (time > beforeJump + jump) {
+			// just walk after jump
+			walkAnimation.apply(skeleton, time, time, true, events, 1, MixBlend.first, MixDirection.in);
+		} else if (time > blendOutStart) {
+			// blend out jump
+			walkAnimation.apply(skeleton, time, time, true, events, 1, MixBlend.first, MixDirection.in);
+			jumpAnimation.apply(skeleton, time - beforeJump, time - beforeJump, false, events, 1 - (time - blendOutStart) / blendOut,
+				MixBlend.first, MixDirection.in);
+		} else if (time > beforeJump + blendIn) {
+			// just jump
+			jumpAnimation.apply(skeleton, time - beforeJump, time - beforeJump, false, events, 1, MixBlend.first, MixDirection.in);
+		} else if (time > beforeJump) {
+			// blend in jump
+			walkAnimation.apply(skeleton, time, time, true, events, 1, MixBlend.first, MixDirection.in);
+			jumpAnimation.apply(skeleton, time - beforeJump, time - beforeJump, false, events, (time - beforeJump) / blendIn,
+				MixBlend.first, MixDirection.in);
+		} else {
+			// just walk before jump
+			walkAnimation.apply(skeleton, time, time, true, events, 1, MixBlend.first, MixDirection.in);
+		}
+
+		skeleton.updateWorldTransform();
+		skeleton.update(Gdx.graphics.getDeltaTime());
+
+		batch.begin();
+		renderer.draw(batch, skeleton);
+		batch.end();
+
+		debugRenderer.draw(skeleton);
+	}
+
+	public void resize (int width, int height) {
+		batch.getProjectionMatrix().setToOrtho2D(0, 0, width, height);
+		debugRenderer.getShapeRenderer().setProjectionMatrix(batch.getProjectionMatrix());
+	}
+
+	public static void main (String[] args) throws Exception {
+		new LwjglApplication(new MixTest());
+	}
+}

+ 384 - 384
spine-libgdx/spine-libgdx-tests/src/com/esotericsoftware/spine/NormalMapTest.java

@@ -1,384 +1,384 @@
-/******************************************************************************
- * Spine Runtimes License Agreement
- * Last updated January 1, 2020. Replaces all prior versions.
- *
- * Copyright (c) 2013-2020, Esoteric Software LLC
- *
- * Integration of the Spine Runtimes into software or otherwise creating
- * derivative works of the Spine Runtimes is permitted under the terms and
- * conditions of Section 2 of the Spine Editor License Agreement:
- * http://esotericsoftware.com/spine-editor-license
- *
- * Otherwise, it is permitted to integrate the Spine Runtimes into software
- * or otherwise create derivative works of the Spine Runtimes (collectively,
- * "Products"), provided that each user of the Products must obtain their own
- * Spine Editor license and redistribution of the Products in any form must
- * include this license and copyright notice.
- *
- * THE SPINE RUNTIMES ARE PROVIDED BY ESOTERIC SOFTWARE LLC "AS IS" AND ANY
- * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
- * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
- * DISCLAIMED. IN NO EVENT SHALL ESOTERIC SOFTWARE LLC BE LIABLE FOR ANY
- * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
- * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES,
- * BUSINESS INTERRUPTION, OR LOSS OF USE, DATA, OR PROFITS) HOWEVER CAUSED AND
- * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
- * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
- * THE SPINE RUNTIMES, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
- *****************************************************************************/
-
-package com.esotericsoftware.spine;
-
-import com.badlogic.gdx.ApplicationAdapter;
-import com.badlogic.gdx.Gdx;
-import com.badlogic.gdx.InputAdapter;
-import com.badlogic.gdx.InputMultiplexer;
-import com.badlogic.gdx.Preferences;
-import com.badlogic.gdx.backends.lwjgl.LwjglApplication;
-import com.badlogic.gdx.files.FileHandle;
-import com.badlogic.gdx.graphics.Texture;
-import com.badlogic.gdx.graphics.g2d.SpriteBatch;
-import com.badlogic.gdx.graphics.g2d.TextureAtlas;
-import com.badlogic.gdx.graphics.glutils.ShaderProgram;
-import com.badlogic.gdx.math.Vector2;
-import com.badlogic.gdx.math.Vector3;
-import com.badlogic.gdx.scenes.scene2d.Actor;
-import com.badlogic.gdx.scenes.scene2d.InputEvent;
-import com.badlogic.gdx.scenes.scene2d.InputListener;
-import com.badlogic.gdx.scenes.scene2d.Stage;
-import com.badlogic.gdx.scenes.scene2d.ui.CheckBox;
-import com.badlogic.gdx.scenes.scene2d.ui.Label;
-import com.badlogic.gdx.scenes.scene2d.ui.Slider;
-import com.badlogic.gdx.scenes.scene2d.ui.Table;
-import com.badlogic.gdx.scenes.scene2d.ui.TextButton;
-import com.badlogic.gdx.scenes.scene2d.ui.Window;
-import com.badlogic.gdx.scenes.scene2d.utils.ChangeListener;
-import com.badlogic.gdx.utils.Align;
-import com.badlogic.gdx.utils.ScreenUtils;
-
-import com.esotericsoftware.spine.Animation.MixBlend;
-import com.esotericsoftware.spine.Animation.MixDirection;
-
-public class NormalMapTest extends ApplicationAdapter {
-	String skeletonPath, animationName;
-	SpriteBatch batch;
-	float time;
-	SkeletonRenderer renderer;
-	Texture atlasTexture, normalMapTexture;
-	ShaderProgram program;
-	UI ui;
-
-	SkeletonData skeletonData;
-	Skeleton skeleton;
-	Animation animation;
-
-	final Vector3 ambientColor = new Vector3();
-	final Vector3 lightColor = new Vector3();
-	final Vector3 lightPosition = new Vector3();
-	final Vector2 resolution = new Vector2();
-	final Vector3 attenuation = new Vector3();
-
-	public NormalMapTest (String skeletonPath, String animationName) {
-		this.skeletonPath = skeletonPath;
-		this.animationName = animationName;
-	}
-
-	public void create () {
-		ui = new UI();
-
-		program = createShader();
-		batch = new SpriteBatch();
-		batch.setShader(program);
-		renderer = new SkeletonRenderer();
-
-		FileHandle file = Gdx.files.internal(skeletonPath + "-diffuse.atlas");
-		TextureAtlas atlas = new TextureAtlas(file);
-		atlasTexture = atlas.getRegions().first().getTexture();
-
-		normalMapTexture = new Texture(Gdx.files.internal(skeletonPath + "-normal.png"));
-
-		SkeletonJson json = new SkeletonJson(atlas);
-		skeletonData = json.readSkeletonData(Gdx.files.internal(skeletonPath + ".json"));
-		if (animationName != null) animation = skeletonData.findAnimation(animationName);
-		if (animation == null) animation = skeletonData.getAnimations().first();
-
-		skeleton = new Skeleton(skeletonData);
-		skeleton.setToSetupPose();
-		skeleton = new Skeleton(skeleton);
-		skeleton.setX(ui.prefs.getFloat("x", Gdx.graphics.getWidth() / 2));
-		skeleton.setY(ui.prefs.getFloat("y", Gdx.graphics.getHeight() / 4));
-		skeleton.updateWorldTransform();
-
-		Gdx.input.setInputProcessor(new InputMultiplexer(ui.stage, new InputAdapter() {
-			public boolean touchDown (int screenX, int screenY, int pointer, int button) {
-				touchDragged(screenX, screenY, pointer);
-				return true;
-			}
-
-			public boolean touchDragged (int screenX, int screenY, int pointer) {
-				skeleton.setPosition(screenX, Gdx.graphics.getHeight() - 1 - screenY);
-				return true;
-			}
-
-			public boolean touchUp (int screenX, int screenY, int pointer, int button) {
-				ui.prefs.putFloat("x", skeleton.getX());
-				ui.prefs.putFloat("y", skeleton.getY());
-				ui.prefs.flush();
-				return true;
-			}
-		}));
-	}
-
-	public void render () {
-		float lastTime = time;
-		time += Gdx.graphics.getDeltaTime();
-		if (animation != null) animation.apply(skeleton, lastTime, time, true, null, 1, MixBlend.first, MixDirection.in);
-		skeleton.updateWorldTransform();
-		skeleton.update(Gdx.graphics.getDeltaTime());
-
-		lightPosition.x = Gdx.input.getX();
-		lightPosition.y = (Gdx.graphics.getHeight() - 1 - Gdx.input.getY());
-
-		ScreenUtils.clear(0, 0, 0, 0);
-
-		ambientColor.x = ui.ambientColorR.getValue();
-		ambientColor.y = ui.ambientColorG.getValue();
-		ambientColor.z = ui.ambientColorB.getValue();
-		lightColor.x = ui.lightColorR.getValue();
-		lightColor.y = ui.lightColorG.getValue();
-		lightColor.z = ui.lightColorB.getValue();
-		attenuation.x = ui.attenuationX.getValue();
-		attenuation.y = ui.attenuationY.getValue();
-		attenuation.z = ui.attenuationZ.getValue();
-		lightPosition.z = ui.lightZ.getValue();
-
-		batch.begin();
-		program.setUniformi("yInvert", ui.yInvert.isChecked() ? 1 : 0);
-		program.setUniformf("resolution", resolution);
-		program.setUniformf("ambientColor", ambientColor);
-		program.setUniformf("ambientIntensity", ui.ambientIntensity.getValue());
-		program.setUniformf("attenuation", attenuation);
-		program.setUniformf("light", lightPosition);
-		program.setUniformf("lightColor", lightColor);
-		program.setUniformi("useNormals", ui.useNormals.isChecked() ? 1 : 0);
-		program.setUniformi("useShadow", ui.useShadow.isChecked() ? 1 : 0);
-		program.setUniformf("strength", ui.strength.getValue());
-		normalMapTexture.bind(1);
-		atlasTexture.bind(0);
-		renderer.draw(batch, skeleton);
-		batch.end();
-
-		ui.stage.act();
-		ui.stage.draw();
-	}
-
-	public void resize (int width, int height) {
-		batch.getProjectionMatrix().setToOrtho2D(0, 0, width, height);
-		ui.stage.getViewport().update(width, height, true);
-		resolution.set(width, height);
-	}
-
-	private ShaderProgram createShader () {
-		String vert = "attribute vec4 a_position;\n" //
-			+ "attribute vec4 a_color;\n" //
-			+ "attribute vec2 a_texCoord0;\n" //
-			+ "uniform mat4 u_proj;\n" //
-			+ "uniform mat4 u_trans;\n" //
-			+ "uniform mat4 u_projTrans;\n" //
-			+ "varying vec4 v_color;\n" //
-			+ "varying vec2 v_texCoords;\n" //
-			+ "\n" //
-			+ "void main()\n" //
-			+ "{\n" //
-			+ "   v_color = a_color;\n" //
-			+ "   v_texCoords = a_texCoord0;\n" //
-			+ "   gl_Position =  u_projTrans * a_position;\n" //
-			+ "}\n" //
-			+ "";
-
-		String frag = "#ifdef GL_ES\n" //
-			+ "precision mediump float;\n" //
-			+ "#endif\n" //
-			+ "varying vec4 v_color;\n" //
-			+ "varying vec2 v_texCoords;\n" //
-			+ "uniform sampler2D u_texture;\n" //
-			+ "uniform sampler2D u_normals;\n" //
-			+ "uniform vec3 light;\n" //
-			+ "uniform vec3 ambientColor;\n" //
-			+ "uniform float ambientIntensity; \n" //
-			+ "uniform vec2 resolution;\n" //
-			+ "uniform vec3 lightColor;\n" //
-			+ "uniform bool useNormals;\n" //
-			+ "uniform bool useShadow;\n" //
-			+ "uniform vec3 attenuation;\n" //
-			+ "uniform float strength;\n" //
-			+ "uniform bool yInvert;\n" //
-			+ "\n" //
-			+ "void main() {\n" //
-			+ "  // sample color & normals from our textures\n" //
-			+ "  vec4 color = texture2D(u_texture, v_texCoords.st);\n" //
-			+ "  vec3 nColor = texture2D(u_normals, v_texCoords.st).rgb;\n" //
-			+ "\n" //
-			+ "  // some bump map programs will need the Y value flipped..\n" //
-			+ "  nColor.g = yInvert ? 1.0 - nColor.g : nColor.g;\n" //
-			+ "\n" //
-			+ "  // this is for debugging purposes, allowing us to lower the intensity of our bump map\n" //
-			+ "  vec3 nBase = vec3(0.5, 0.5, 1.0);\n" //
-			+ "  nColor = mix(nBase, nColor, strength);\n" //
-			+ "\n" //
-			+ "  // normals need to be converted to [-1.0, 1.0] range and normalized\n" //
-			+ "  vec3 normal = normalize(nColor * 2.0 - 1.0);\n" //
-			+ "\n" //
-			+ "  // here we do a simple distance calculation\n" //
-			+ "  vec3 deltaPos = vec3( (light.xy - gl_FragCoord.xy) / resolution.xy, light.z );\n" //
-			+ "\n" //
-			+ "  vec3 lightDir = normalize(deltaPos);\n" //
-			+ "  float lambert = useNormals ? clamp(dot(normal, lightDir), 0.0, 1.0) : 1.0;\n" //
-			+ "  \n" //
-			+ "  // now let's get a nice little falloff\n" //
-			+ "  float d = sqrt(dot(deltaPos, deltaPos));  \n" //
-			+ "  float att = useShadow ? 1.0 / ( attenuation.x + (attenuation.y*d) + (attenuation.z*d*d) ) : 1.0;\n" //
-			+ "  \n" //
-			+ "  vec3 result = (ambientColor * ambientIntensity) + (lightColor.rgb * lambert) * att;\n" //
-			+ "  result *= color.rgb;\n" //
-			+ "  \n" //
-			+ "  gl_FragColor = v_color * vec4(result, color.a);\n" //
-			+ "}";
-
-		// System.out.println("VERTEX PROGRAM:\n------------\n\n" + vert);
-		// System.out.println("FRAGMENT PROGRAM:\n------------\n\n" + frag);
-		ShaderProgram program = new ShaderProgram(vert, frag);
-		ShaderProgram.pedantic = false;
-		if (!program.isCompiled()) throw new IllegalArgumentException("Error compiling shader: " + program.getLog());
-
-		program.bind();
-		program.setUniformi("u_texture", 0);
-		program.setUniformi("u_normals", 1);
-		return program;
-	}
-
-	class UI {
-		Stage stage = new Stage();
-		com.badlogic.gdx.scenes.scene2d.ui.Skin skin = new com.badlogic.gdx.scenes.scene2d.ui.Skin(
-			Gdx.files.internal("skin/skin.json"));
-		Preferences prefs = Gdx.app.getPreferences(".spine/NormalMapTest");
-
-		Window window;
-		Table root;
-		Slider ambientColorR, ambientColorG, ambientColorB;
-		Slider lightColorR, lightColorG, lightColorB, lightZ;
-		Slider attenuationX, attenuationY, attenuationZ;
-		Slider ambientIntensity;
-		Slider strength;
-		CheckBox useShadow, useNormals, yInvert;
-
-		public UI () {
-			create();
-		}
-
-		public void create () {
-			window = new Window("Light", skin);
-
-			root = new Table(skin);
-			root.pad(2, 4, 4, 4).defaults().space(6);
-			root.columnDefaults(0).top().right();
-			root.columnDefaults(1).left();
-			ambientColorR = slider("Ambient R", 1);
-			ambientColorG = slider("Ambient G", 1);
-			ambientColorB = slider("Ambient B", 1);
-			ambientIntensity = slider("Ambient intensity", 0.35f);
-			lightColorR = slider("Light R", 1);
-			lightColorG = slider("Light G", 0.7f);
-			lightColorB = slider("Light B", 0.6f);
-			lightZ = slider("Light Z", 0.07f);
-			attenuationX = slider("Attenuation", 0.4f);
-			attenuationY = slider("Attenuation*d", 3);
-			attenuationZ = slider("Attenuation*d*d", 5);
-			strength = slider("Strength", 1);
-			{
-				Table table = new Table();
-				table.defaults().space(12);
-				table.add(useShadow = checkbox(" Use shadow", true));
-				table.add(useNormals = checkbox(" Use normals", true));
-				table.add(yInvert = checkbox(" Invert Y", true));
-				root.add(table).colspan(2).row();
-			}
-
-			TextButton resetButton = new TextButton("Reset", skin);
-			resetButton.getColor().a = 0.66f;
-			window.getTitleTable().add(resetButton).height(20);
-
-			window.add(root).expand().fill();
-			window.pack();
-			stage.addActor(window);
-
-			// Events.
-
-			window.addListener(new InputListener() {
-				public boolean touchDown (InputEvent event, float x, float y, int pointer, int button) {
-					event.cancel();
-					return true;
-				}
-			});
-
-			resetButton.addListener(new ChangeListener() {
-				public void changed (ChangeEvent event, Actor actor) {
-					window.remove();
-					prefs.clear();
-					prefs.flush();
-					create();
-				}
-			});
-		}
-
-		private CheckBox checkbox (final String name, boolean defaultValue) {
-			final CheckBox checkbox = new CheckBox(name, skin);
-			checkbox.setChecked(prefs.getBoolean(checkbox.getText().toString(), defaultValue));
-
-			checkbox.addListener(new ChangeListener() {
-				public void changed (ChangeEvent event, Actor actor) {
-					prefs.putBoolean(name, checkbox.isChecked());
-					prefs.flush();
-				}
-			});
-
-			return checkbox;
-		}
-
-		private Slider slider (final String name, float defaultValue) {
-			final Slider slider = new Slider(0, 1, 0.01f, false, skin);
-			slider.setValue(prefs.getFloat(name, defaultValue));
-
-			final Label label = new Label("", skin);
-			label.setAlignment(Align.right);
-			label.setText(Float.toString((int)(slider.getValue() * 100) / 100f));
-
-			slider.addListener(new ChangeListener() {
-				public void changed (ChangeEvent event, Actor actor) {
-					label.setText(Float.toString((int)(slider.getValue() * 100) / 100f));
-					if (!slider.isDragging()) {
-						prefs.putFloat(name, slider.getValue());
-						prefs.flush();
-					}
-				}
-			});
-
-			Table table = new Table();
-			table.add(label).width(35).space(12);
-			table.add(slider);
-
-			root.add(name);
-			root.add(table).fill().row();
-			return slider;
-		}
-	}
-
-	public static void main (String[] args) throws Exception {
-		if (args.length == 0)
-			args = new String[] {"spineboy-old/spineboy-old", "walk"};
-		else if (args.length == 1) //
-			args = new String[] {args[0], null};
-
-		new LwjglApplication(new NormalMapTest(args[0], args[1]));
-	}
-}
+/******************************************************************************
+ * Spine Runtimes License Agreement
+ * Last updated January 1, 2020. Replaces all prior versions.
+ *
+ * Copyright (c) 2013-2020, Esoteric Software LLC
+ *
+ * Integration of the Spine Runtimes into software or otherwise creating
+ * derivative works of the Spine Runtimes is permitted under the terms and
+ * conditions of Section 2 of the Spine Editor License Agreement:
+ * http://esotericsoftware.com/spine-editor-license
+ *
+ * Otherwise, it is permitted to integrate the Spine Runtimes into software
+ * or otherwise create derivative works of the Spine Runtimes (collectively,
+ * "Products"), provided that each user of the Products must obtain their own
+ * Spine Editor license and redistribution of the Products in any form must
+ * include this license and copyright notice.
+ *
+ * THE SPINE RUNTIMES ARE PROVIDED BY ESOTERIC SOFTWARE LLC "AS IS" AND ANY
+ * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL ESOTERIC SOFTWARE LLC BE LIABLE FOR ANY
+ * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES,
+ * BUSINESS INTERRUPTION, OR LOSS OF USE, DATA, OR PROFITS) HOWEVER CAUSED AND
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
+ * THE SPINE RUNTIMES, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ *****************************************************************************/
+
+package com.esotericsoftware.spine;
+
+import com.badlogic.gdx.ApplicationAdapter;
+import com.badlogic.gdx.Gdx;
+import com.badlogic.gdx.InputAdapter;
+import com.badlogic.gdx.InputMultiplexer;
+import com.badlogic.gdx.Preferences;
+import com.badlogic.gdx.backends.lwjgl.LwjglApplication;
+import com.badlogic.gdx.files.FileHandle;
+import com.badlogic.gdx.graphics.Texture;
+import com.badlogic.gdx.graphics.g2d.SpriteBatch;
+import com.badlogic.gdx.graphics.g2d.TextureAtlas;
+import com.badlogic.gdx.graphics.glutils.ShaderProgram;
+import com.badlogic.gdx.math.Vector2;
+import com.badlogic.gdx.math.Vector3;
+import com.badlogic.gdx.scenes.scene2d.Actor;
+import com.badlogic.gdx.scenes.scene2d.InputEvent;
+import com.badlogic.gdx.scenes.scene2d.InputListener;
+import com.badlogic.gdx.scenes.scene2d.Stage;
+import com.badlogic.gdx.scenes.scene2d.ui.CheckBox;
+import com.badlogic.gdx.scenes.scene2d.ui.Label;
+import com.badlogic.gdx.scenes.scene2d.ui.Slider;
+import com.badlogic.gdx.scenes.scene2d.ui.Table;
+import com.badlogic.gdx.scenes.scene2d.ui.TextButton;
+import com.badlogic.gdx.scenes.scene2d.ui.Window;
+import com.badlogic.gdx.scenes.scene2d.utils.ChangeListener;
+import com.badlogic.gdx.utils.Align;
+import com.badlogic.gdx.utils.ScreenUtils;
+
+import com.esotericsoftware.spine.Animation.MixBlend;
+import com.esotericsoftware.spine.Animation.MixDirection;
+
+public class NormalMapTest extends ApplicationAdapter {
+	String skeletonPath, animationName;
+	SpriteBatch batch;
+	float time;
+	SkeletonRenderer renderer;
+	Texture atlasTexture, normalMapTexture;
+	ShaderProgram program;
+	UI ui;
+
+	SkeletonData skeletonData;
+	Skeleton skeleton;
+	Animation animation;
+
+	final Vector3 ambientColor = new Vector3();
+	final Vector3 lightColor = new Vector3();
+	final Vector3 lightPosition = new Vector3();
+	final Vector2 resolution = new Vector2();
+	final Vector3 attenuation = new Vector3();
+
+	public NormalMapTest (String skeletonPath, String animationName) {
+		this.skeletonPath = skeletonPath;
+		this.animationName = animationName;
+	}
+
+	public void create () {
+		ui = new UI();
+
+		program = createShader();
+		batch = new SpriteBatch();
+		batch.setShader(program);
+		renderer = new SkeletonRenderer();
+
+		FileHandle file = Gdx.files.internal(skeletonPath + "-diffuse.atlas");
+		TextureAtlas atlas = new TextureAtlas(file);
+		atlasTexture = atlas.getRegions().first().getTexture();
+
+		normalMapTexture = new Texture(Gdx.files.internal(skeletonPath + "-normal.png"));
+
+		SkeletonJson json = new SkeletonJson(atlas);
+		skeletonData = json.readSkeletonData(Gdx.files.internal(skeletonPath + ".json"));
+		if (animationName != null) animation = skeletonData.findAnimation(animationName);
+		if (animation == null) animation = skeletonData.getAnimations().first();
+
+		skeleton = new Skeleton(skeletonData);
+		skeleton.setToSetupPose();
+		skeleton = new Skeleton(skeleton);
+		skeleton.setX(ui.prefs.getFloat("x", Gdx.graphics.getWidth() / 2));
+		skeleton.setY(ui.prefs.getFloat("y", Gdx.graphics.getHeight() / 4));
+		skeleton.updateWorldTransform();
+
+		Gdx.input.setInputProcessor(new InputMultiplexer(ui.stage, new InputAdapter() {
+			public boolean touchDown (int screenX, int screenY, int pointer, int button) {
+				touchDragged(screenX, screenY, pointer);
+				return true;
+			}
+
+			public boolean touchDragged (int screenX, int screenY, int pointer) {
+				skeleton.setPosition(screenX, Gdx.graphics.getHeight() - 1 - screenY);
+				return true;
+			}
+
+			public boolean touchUp (int screenX, int screenY, int pointer, int button) {
+				ui.prefs.putFloat("x", skeleton.getX());
+				ui.prefs.putFloat("y", skeleton.getY());
+				ui.prefs.flush();
+				return true;
+			}
+		}));
+	}
+
+	public void render () {
+		float lastTime = time;
+		time += Gdx.graphics.getDeltaTime();
+		if (animation != null) animation.apply(skeleton, lastTime, time, true, null, 1, MixBlend.first, MixDirection.in);
+		skeleton.updateWorldTransform();
+		skeleton.update(Gdx.graphics.getDeltaTime());
+
+		lightPosition.x = Gdx.input.getX();
+		lightPosition.y = (Gdx.graphics.getHeight() - 1 - Gdx.input.getY());
+
+		ScreenUtils.clear(0, 0, 0, 0);
+
+		ambientColor.x = ui.ambientColorR.getValue();
+		ambientColor.y = ui.ambientColorG.getValue();
+		ambientColor.z = ui.ambientColorB.getValue();
+		lightColor.x = ui.lightColorR.getValue();
+		lightColor.y = ui.lightColorG.getValue();
+		lightColor.z = ui.lightColorB.getValue();
+		attenuation.x = ui.attenuationX.getValue();
+		attenuation.y = ui.attenuationY.getValue();
+		attenuation.z = ui.attenuationZ.getValue();
+		lightPosition.z = ui.lightZ.getValue();
+
+		batch.begin();
+		program.setUniformi("yInvert", ui.yInvert.isChecked() ? 1 : 0);
+		program.setUniformf("resolution", resolution);
+		program.setUniformf("ambientColor", ambientColor);
+		program.setUniformf("ambientIntensity", ui.ambientIntensity.getValue());
+		program.setUniformf("attenuation", attenuation);
+		program.setUniformf("light", lightPosition);
+		program.setUniformf("lightColor", lightColor);
+		program.setUniformi("useNormals", ui.useNormals.isChecked() ? 1 : 0);
+		program.setUniformi("useShadow", ui.useShadow.isChecked() ? 1 : 0);
+		program.setUniformf("strength", ui.strength.getValue());
+		normalMapTexture.bind(1);
+		atlasTexture.bind(0);
+		renderer.draw(batch, skeleton);
+		batch.end();
+
+		ui.stage.act();
+		ui.stage.draw();
+	}
+
+	public void resize (int width, int height) {
+		batch.getProjectionMatrix().setToOrtho2D(0, 0, width, height);
+		ui.stage.getViewport().update(width, height, true);
+		resolution.set(width, height);
+	}
+
+	private ShaderProgram createShader () {
+		String vert = "attribute vec4 a_position;\n" //
+			+ "attribute vec4 a_color;\n" //
+			+ "attribute vec2 a_texCoord0;\n" //
+			+ "uniform mat4 u_proj;\n" //
+			+ "uniform mat4 u_trans;\n" //
+			+ "uniform mat4 u_projTrans;\n" //
+			+ "varying vec4 v_color;\n" //
+			+ "varying vec2 v_texCoords;\n" //
+			+ "\n" //
+			+ "void main()\n" //
+			+ "{\n" //
+			+ "   v_color = a_color;\n" //
+			+ "   v_texCoords = a_texCoord0;\n" //
+			+ "   gl_Position =  u_projTrans * a_position;\n" //
+			+ "}\n" //
+			+ "";
+
+		String frag = "#ifdef GL_ES\n" //
+			+ "precision mediump float;\n" //
+			+ "#endif\n" //
+			+ "varying vec4 v_color;\n" //
+			+ "varying vec2 v_texCoords;\n" //
+			+ "uniform sampler2D u_texture;\n" //
+			+ "uniform sampler2D u_normals;\n" //
+			+ "uniform vec3 light;\n" //
+			+ "uniform vec3 ambientColor;\n" //
+			+ "uniform float ambientIntensity; \n" //
+			+ "uniform vec2 resolution;\n" //
+			+ "uniform vec3 lightColor;\n" //
+			+ "uniform bool useNormals;\n" //
+			+ "uniform bool useShadow;\n" //
+			+ "uniform vec3 attenuation;\n" //
+			+ "uniform float strength;\n" //
+			+ "uniform bool yInvert;\n" //
+			+ "\n" //
+			+ "void main() {\n" //
+			+ "  // sample color & normals from our textures\n" //
+			+ "  vec4 color = texture2D(u_texture, v_texCoords.st);\n" //
+			+ "  vec3 nColor = texture2D(u_normals, v_texCoords.st).rgb;\n" //
+			+ "\n" //
+			+ "  // some bump map programs will need the Y value flipped..\n" //
+			+ "  nColor.g = yInvert ? 1.0 - nColor.g : nColor.g;\n" //
+			+ "\n" //
+			+ "  // this is for debugging purposes, allowing us to lower the intensity of our bump map\n" //
+			+ "  vec3 nBase = vec3(0.5, 0.5, 1.0);\n" //
+			+ "  nColor = mix(nBase, nColor, strength);\n" //
+			+ "\n" //
+			+ "  // normals need to be converted to [-1.0, 1.0] range and normalized\n" //
+			+ "  vec3 normal = normalize(nColor * 2.0 - 1.0);\n" //
+			+ "\n" //
+			+ "  // here we do a simple distance calculation\n" //
+			+ "  vec3 deltaPos = vec3( (light.xy - gl_FragCoord.xy) / resolution.xy, light.z );\n" //
+			+ "\n" //
+			+ "  vec3 lightDir = normalize(deltaPos);\n" //
+			+ "  float lambert = useNormals ? clamp(dot(normal, lightDir), 0.0, 1.0) : 1.0;\n" //
+			+ "  \n" //
+			+ "  // now let's get a nice little falloff\n" //
+			+ "  float d = sqrt(dot(deltaPos, deltaPos));  \n" //
+			+ "  float att = useShadow ? 1.0 / ( attenuation.x + (attenuation.y*d) + (attenuation.z*d*d) ) : 1.0;\n" //
+			+ "  \n" //
+			+ "  vec3 result = (ambientColor * ambientIntensity) + (lightColor.rgb * lambert) * att;\n" //
+			+ "  result *= color.rgb;\n" //
+			+ "  \n" //
+			+ "  gl_FragColor = v_color * vec4(result, color.a);\n" //
+			+ "}";
+
+		// System.out.println("VERTEX PROGRAM:\n------------\n\n" + vert);
+		// System.out.println("FRAGMENT PROGRAM:\n------------\n\n" + frag);
+		ShaderProgram program = new ShaderProgram(vert, frag);
+		ShaderProgram.pedantic = false;
+		if (!program.isCompiled()) throw new IllegalArgumentException("Error compiling shader: " + program.getLog());
+
+		program.bind();
+		program.setUniformi("u_texture", 0);
+		program.setUniformi("u_normals", 1);
+		return program;
+	}
+
+	class UI {
+		Stage stage = new Stage();
+		com.badlogic.gdx.scenes.scene2d.ui.Skin skin = new com.badlogic.gdx.scenes.scene2d.ui.Skin(
+			Gdx.files.internal("skin/skin.json"));
+		Preferences prefs = Gdx.app.getPreferences(".spine/NormalMapTest");
+
+		Window window;
+		Table root;
+		Slider ambientColorR, ambientColorG, ambientColorB;
+		Slider lightColorR, lightColorG, lightColorB, lightZ;
+		Slider attenuationX, attenuationY, attenuationZ;
+		Slider ambientIntensity;
+		Slider strength;
+		CheckBox useShadow, useNormals, yInvert;
+
+		public UI () {
+			create();
+		}
+
+		public void create () {
+			window = new Window("Light", skin);
+
+			root = new Table(skin);
+			root.pad(2, 4, 4, 4).defaults().space(6);
+			root.columnDefaults(0).top().right();
+			root.columnDefaults(1).left();
+			ambientColorR = slider("Ambient R", 1);
+			ambientColorG = slider("Ambient G", 1);
+			ambientColorB = slider("Ambient B", 1);
+			ambientIntensity = slider("Ambient intensity", 0.35f);
+			lightColorR = slider("Light R", 1);
+			lightColorG = slider("Light G", 0.7f);
+			lightColorB = slider("Light B", 0.6f);
+			lightZ = slider("Light Z", 0.07f);
+			attenuationX = slider("Attenuation", 0.4f);
+			attenuationY = slider("Attenuation*d", 3);
+			attenuationZ = slider("Attenuation*d*d", 5);
+			strength = slider("Strength", 1);
+			{
+				Table table = new Table();
+				table.defaults().space(12);
+				table.add(useShadow = checkbox(" Use shadow", true));
+				table.add(useNormals = checkbox(" Use normals", true));
+				table.add(yInvert = checkbox(" Invert Y", true));
+				root.add(table).colspan(2).row();
+			}
+
+			TextButton resetButton = new TextButton("Reset", skin);
+			resetButton.getColor().a = 0.66f;
+			window.getTitleTable().add(resetButton).height(20);
+
+			window.add(root).expand().fill();
+			window.pack();
+			stage.addActor(window);
+
+			// Events.
+
+			window.addListener(new InputListener() {
+				public boolean touchDown (InputEvent event, float x, float y, int pointer, int button) {
+					event.cancel();
+					return true;
+				}
+			});
+
+			resetButton.addListener(new ChangeListener() {
+				public void changed (ChangeEvent event, Actor actor) {
+					window.remove();
+					prefs.clear();
+					prefs.flush();
+					create();
+				}
+			});
+		}
+
+		private CheckBox checkbox (final String name, boolean defaultValue) {
+			final CheckBox checkbox = new CheckBox(name, skin);
+			checkbox.setChecked(prefs.getBoolean(checkbox.getText().toString(), defaultValue));
+
+			checkbox.addListener(new ChangeListener() {
+				public void changed (ChangeEvent event, Actor actor) {
+					prefs.putBoolean(name, checkbox.isChecked());
+					prefs.flush();
+				}
+			});
+
+			return checkbox;
+		}
+
+		private Slider slider (final String name, float defaultValue) {
+			final Slider slider = new Slider(0, 1, 0.01f, false, skin);
+			slider.setValue(prefs.getFloat(name, defaultValue));
+
+			final Label label = new Label("", skin);
+			label.setAlignment(Align.right);
+			label.setText(Float.toString((int)(slider.getValue() * 100) / 100f));
+
+			slider.addListener(new ChangeListener() {
+				public void changed (ChangeEvent event, Actor actor) {
+					label.setText(Float.toString((int)(slider.getValue() * 100) / 100f));
+					if (!slider.isDragging()) {
+						prefs.putFloat(name, slider.getValue());
+						prefs.flush();
+					}
+				}
+			});
+
+			Table table = new Table();
+			table.add(label).width(35).space(12);
+			table.add(slider);
+
+			root.add(name);
+			root.add(table).fill().row();
+			return slider;
+		}
+	}
+
+	public static void main (String[] args) throws Exception {
+		if (args.length == 0)
+			args = new String[] {"spineboy-old/spineboy-old", "walk"};
+		else if (args.length == 1) //
+			args = new String[] {args[0], null};
+
+		new LwjglApplication(new NormalMapTest(args[0], args[1]));
+	}
+}

+ 111 - 111
spine-libgdx/spine-libgdx-tests/src/com/esotericsoftware/spine/SimpleTest1.java

@@ -1,111 +1,111 @@
-/******************************************************************************
- * Spine Runtimes License Agreement
- * Last updated January 1, 2020. Replaces all prior versions.
- *
- * Copyright (c) 2013-2020, Esoteric Software LLC
- *
- * Integration of the Spine Runtimes into software or otherwise creating
- * derivative works of the Spine Runtimes is permitted under the terms and
- * conditions of Section 2 of the Spine Editor License Agreement:
- * http://esotericsoftware.com/spine-editor-license
- *
- * Otherwise, it is permitted to integrate the Spine Runtimes into software
- * or otherwise create derivative works of the Spine Runtimes (collectively,
- * "Products"), provided that each user of the Products must obtain their own
- * Spine Editor license and redistribution of the Products in any form must
- * include this license and copyright notice.
- *
- * THE SPINE RUNTIMES ARE PROVIDED BY ESOTERIC SOFTWARE LLC "AS IS" AND ANY
- * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
- * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
- * DISCLAIMED. IN NO EVENT SHALL ESOTERIC SOFTWARE LLC BE LIABLE FOR ANY
- * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
- * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES,
- * BUSINESS INTERRUPTION, OR LOSS OF USE, DATA, OR PROFITS) HOWEVER CAUSED AND
- * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
- * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
- * THE SPINE RUNTIMES, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
- *****************************************************************************/
-
-package com.esotericsoftware.spine;
-
-import com.badlogic.gdx.ApplicationAdapter;
-import com.badlogic.gdx.Gdx;
-import com.badlogic.gdx.backends.lwjgl.LwjglApplication;
-import com.badlogic.gdx.graphics.GL20;
-import com.badlogic.gdx.graphics.OrthographicCamera;
-import com.badlogic.gdx.graphics.g2d.TextureAtlas;
-import com.esotericsoftware.spine.utils.TwoColorPolygonBatch;
-
-public class SimpleTest1 extends ApplicationAdapter {
-	OrthographicCamera camera;
-	TwoColorPolygonBatch batch;
-	SkeletonRenderer renderer;
-	SkeletonRendererDebug debugRenderer;
-
-	TextureAtlas atlas;
-	Skeleton skeleton;
-	AnimationState state;
-
-	public void create () {
-		camera = new OrthographicCamera();
-		batch = new TwoColorPolygonBatch();
-		renderer = new SkeletonRenderer();
-		renderer.setPremultipliedAlpha(true); // PMA results in correct blending without outlines.
-		debugRenderer = new SkeletonRendererDebug();
-		debugRenderer.setBoundingBoxes(false);
-		debugRenderer.setRegionAttachments(false);
-
-		atlas = new TextureAtlas(Gdx.files.internal("spineboy/spineboy-pma.atlas"));
-		SkeletonBinary json = new SkeletonBinary(atlas); // This loads skeleton JSON data, which is stateless.
-		json.setScale(0.6f); // Load the skeleton at 60% the size it was in Spine.
-		SkeletonData skeletonData = json.readSkeletonData(Gdx.files.internal("spineboy/spineboy-pro.skel"));
-
-		skeleton = new Skeleton(skeletonData); // Skeleton holds skeleton state (bone positions, slot attachments, etc).
-		skeleton.setPosition(250, 20);
-
-		AnimationStateData stateData = new AnimationStateData(skeletonData); // Defines mixing (crossfading) between animations.
-		stateData.setMix("run", "jump", 0.2f);
-		stateData.setMix("jump", "run", 0.2f);
-
-		state = new AnimationState(stateData); // Holds the animation state for a skeleton (current animation, time, etc).
-		state.setTimeScale(0.5f); // Slow all animations down to 50% speed.
-
-		// Queue animations on track 0.
-		state.setAnimation(0, "run", true);
-		state.addAnimation(0, "jump", false, 2); // Jump after 2 seconds.
-		state.addAnimation(0, "run", true, 0); // Run after the jump.
-	}
-
-	public void render () {
-		state.update(Gdx.graphics.getDeltaTime()); // Update the animation time.
-
-		Gdx.gl.glClear(GL20.GL_COLOR_BUFFER_BIT);
-
-		state.apply(skeleton); // Poses skeleton using current animations. This sets the bones' local SRT.
-		skeleton.updateWorldTransform(); // Uses the bones' local SRT to compute their world SRT.
-
-		// Configure the camera, SpriteBatch, and SkeletonRendererDebug.
-		camera.update();
-		batch.getProjectionMatrix().set(camera.combined);
-		debugRenderer.getShapeRenderer().setProjectionMatrix(camera.combined);
-
-		batch.begin();
-		renderer.draw(batch, skeleton); // Draw the skeleton images.
-		batch.end();
-
-		debugRenderer.draw(skeleton); // Draw debug lines.
-	}
-
-	public void resize (int width, int height) {
-		camera.setToOrtho(false); // Update camera with new size.
-	}
-
-	public void dispose () {
-		atlas.dispose();
-	}
-
-	public static void main (String[] args) throws Exception {
-		new LwjglApplication(new SimpleTest1());
-	}
-}
+/******************************************************************************
+ * Spine Runtimes License Agreement
+ * Last updated January 1, 2020. Replaces all prior versions.
+ *
+ * Copyright (c) 2013-2020, Esoteric Software LLC
+ *
+ * Integration of the Spine Runtimes into software or otherwise creating
+ * derivative works of the Spine Runtimes is permitted under the terms and
+ * conditions of Section 2 of the Spine Editor License Agreement:
+ * http://esotericsoftware.com/spine-editor-license
+ *
+ * Otherwise, it is permitted to integrate the Spine Runtimes into software
+ * or otherwise create derivative works of the Spine Runtimes (collectively,
+ * "Products"), provided that each user of the Products must obtain their own
+ * Spine Editor license and redistribution of the Products in any form must
+ * include this license and copyright notice.
+ *
+ * THE SPINE RUNTIMES ARE PROVIDED BY ESOTERIC SOFTWARE LLC "AS IS" AND ANY
+ * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL ESOTERIC SOFTWARE LLC BE LIABLE FOR ANY
+ * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES,
+ * BUSINESS INTERRUPTION, OR LOSS OF USE, DATA, OR PROFITS) HOWEVER CAUSED AND
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
+ * THE SPINE RUNTIMES, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ *****************************************************************************/
+
+package com.esotericsoftware.spine;
+
+import com.badlogic.gdx.ApplicationAdapter;
+import com.badlogic.gdx.Gdx;
+import com.badlogic.gdx.backends.lwjgl.LwjglApplication;
+import com.badlogic.gdx.graphics.GL20;
+import com.badlogic.gdx.graphics.OrthographicCamera;
+import com.badlogic.gdx.graphics.g2d.TextureAtlas;
+import com.esotericsoftware.spine.utils.TwoColorPolygonBatch;
+
+public class SimpleTest1 extends ApplicationAdapter {
+	OrthographicCamera camera;
+	TwoColorPolygonBatch batch;
+	SkeletonRenderer renderer;
+	SkeletonRendererDebug debugRenderer;
+
+	TextureAtlas atlas;
+	Skeleton skeleton;
+	AnimationState state;
+
+	public void create () {
+		camera = new OrthographicCamera();
+		batch = new TwoColorPolygonBatch();
+		renderer = new SkeletonRenderer();
+		renderer.setPremultipliedAlpha(true); // PMA results in correct blending without outlines.
+		debugRenderer = new SkeletonRendererDebug();
+		debugRenderer.setBoundingBoxes(false);
+		debugRenderer.setRegionAttachments(false);
+
+		atlas = new TextureAtlas(Gdx.files.internal("spineboy/spineboy-pma.atlas"));
+		SkeletonBinary json = new SkeletonBinary(atlas); // This loads skeleton JSON data, which is stateless.
+		json.setScale(0.6f); // Load the skeleton at 60% the size it was in Spine.
+		SkeletonData skeletonData = json.readSkeletonData(Gdx.files.internal("spineboy/spineboy-pro.skel"));
+
+		skeleton = new Skeleton(skeletonData); // Skeleton holds skeleton state (bone positions, slot attachments, etc).
+		skeleton.setPosition(250, 20);
+
+		AnimationStateData stateData = new AnimationStateData(skeletonData); // Defines mixing (crossfading) between animations.
+		stateData.setMix("run", "jump", 0.2f);
+		stateData.setMix("jump", "run", 0.2f);
+
+		state = new AnimationState(stateData); // Holds the animation state for a skeleton (current animation, time, etc).
+		state.setTimeScale(0.5f); // Slow all animations down to 50% speed.
+
+		// Queue animations on track 0.
+		state.setAnimation(0, "run", true);
+		state.addAnimation(0, "jump", false, 2); // Jump after 2 seconds.
+		state.addAnimation(0, "run", true, 0); // Run after the jump.
+	}
+
+	public void render () {
+		state.update(Gdx.graphics.getDeltaTime()); // Update the animation time.
+
+		Gdx.gl.glClear(GL20.GL_COLOR_BUFFER_BIT);
+
+		state.apply(skeleton); // Poses skeleton using current animations. This sets the bones' local SRT.
+		skeleton.updateWorldTransform(); // Uses the bones' local SRT to compute their world SRT.
+
+		// Configure the camera, SpriteBatch, and SkeletonRendererDebug.
+		camera.update();
+		batch.getProjectionMatrix().set(camera.combined);
+		debugRenderer.getShapeRenderer().setProjectionMatrix(camera.combined);
+
+		batch.begin();
+		renderer.draw(batch, skeleton); // Draw the skeleton images.
+		batch.end();
+
+		debugRenderer.draw(skeleton); // Draw debug lines.
+	}
+
+	public void resize (int width, int height) {
+		camera.setToOrtho(false); // Update camera with new size.
+	}
+
+	public void dispose () {
+		atlas.dispose();
+	}
+
+	public static void main (String[] args) throws Exception {
+		new LwjglApplication(new SimpleTest1());
+	}
+}

+ 173 - 173
spine-libgdx/spine-libgdx-tests/src/com/esotericsoftware/spine/SimpleTest2.java

@@ -1,173 +1,173 @@
-/******************************************************************************
- * Spine Runtimes License Agreement
- * Last updated January 1, 2020. Replaces all prior versions.
- *
- * Copyright (c) 2013-2020, Esoteric Software LLC
- *
- * Integration of the Spine Runtimes into software or otherwise creating
- * derivative works of the Spine Runtimes is permitted under the terms and
- * conditions of Section 2 of the Spine Editor License Agreement:
- * http://esotericsoftware.com/spine-editor-license
- *
- * Otherwise, it is permitted to integrate the Spine Runtimes into software
- * or otherwise create derivative works of the Spine Runtimes (collectively,
- * "Products"), provided that each user of the Products must obtain their own
- * Spine Editor license and redistribution of the Products in any form must
- * include this license and copyright notice.
- *
- * THE SPINE RUNTIMES ARE PROVIDED BY ESOTERIC SOFTWARE LLC "AS IS" AND ANY
- * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
- * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
- * DISCLAIMED. IN NO EVENT SHALL ESOTERIC SOFTWARE LLC BE LIABLE FOR ANY
- * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
- * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES,
- * BUSINESS INTERRUPTION, OR LOSS OF USE, DATA, OR PROFITS) HOWEVER CAUSED AND
- * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
- * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
- * THE SPINE RUNTIMES, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
- *****************************************************************************/
-
-package com.esotericsoftware.spine;
-
-import com.badlogic.gdx.ApplicationAdapter;
-import com.badlogic.gdx.Gdx;
-import com.badlogic.gdx.InputAdapter;
-import com.badlogic.gdx.backends.lwjgl.LwjglApplication;
-import com.badlogic.gdx.graphics.Color;
-import com.badlogic.gdx.graphics.GL20;
-import com.badlogic.gdx.graphics.OrthographicCamera;
-import com.badlogic.gdx.graphics.g2d.TextureAtlas;
-import com.badlogic.gdx.math.Vector3;
-import com.esotericsoftware.spine.AnimationState.AnimationStateListener;
-import com.esotericsoftware.spine.AnimationState.TrackEntry;
-import com.esotericsoftware.spine.attachments.BoundingBoxAttachment;
-import com.esotericsoftware.spine.utils.TwoColorPolygonBatch;
-
-public class SimpleTest2 extends ApplicationAdapter {
-	OrthographicCamera camera;
-	TwoColorPolygonBatch batch;
-	SkeletonRenderer renderer;
-	SkeletonRendererDebug debugRenderer;
-
-	TextureAtlas atlas;
-	Skeleton skeleton;
-	SkeletonBounds bounds;
-	AnimationState state;
-
-	public void create () {
-		camera = new OrthographicCamera();
-		batch = new TwoColorPolygonBatch();
-		renderer = new SkeletonRenderer();
-		renderer.setPremultipliedAlpha(true);
-		debugRenderer = new SkeletonRendererDebug();
-
-		atlas = new TextureAtlas(Gdx.files.internal("spineboy/spineboy-pma.atlas"));
-		SkeletonJson json = new SkeletonJson(atlas); // This loads skeleton JSON data, which is stateless.
-		json.setScale(0.6f); // Load the skeleton at 60% the size it was in Spine.
-		SkeletonData skeletonData = json.readSkeletonData(Gdx.files.internal("spineboy/spineboy-ess.json"));
-
-		skeleton = new Skeleton(skeletonData); // Skeleton holds skeleton state (bone positions, slot attachments, etc).
-		skeleton.setPosition(250, 20);
-		skeleton.setAttachment("head-bb", "head"); // Attach "head" bounding box to "head-bb" slot.
-
-		bounds = new SkeletonBounds(); // Convenience class to do hit detection with bounding boxes.
-
-		AnimationStateData stateData = new AnimationStateData(skeletonData); // Defines mixing (crossfading) between animations.
-		stateData.setMix("run", "jump", 0.2f);
-		stateData.setMix("jump", "run", 0.2f);
-		stateData.setMix("jump", "jump", 0);
-
-		state = new AnimationState(stateData); // Holds the animation state for a skeleton (current animation, time, etc).
-		state.setTimeScale(0.3f); // Slow all animations down to 30% speed.
-		state.addListener(new AnimationStateListener() {
-
-			public void start (TrackEntry entry) {
-				System.out.println(entry.getTrackIndex() + " start: " + entry);
-			}
-
-			public void interrupt (TrackEntry entry) {
-				System.out.println(entry.getTrackIndex() + " interrupt: " + entry);
-			}
-
-			public void end (TrackEntry entry) {
-				System.out.println(entry.getTrackIndex() + " end: " + entry);
-			}
-
-			public void dispose (TrackEntry entry) {
-				System.out.println(entry.getTrackIndex() + " dispose: " + entry);
-			}
-
-			public void complete (TrackEntry entry) {
-				System.out.println(entry.getTrackIndex() + " complete: " + entry);
-			}
-
-			public void event (TrackEntry entry, Event event) {
-				System.out
-					.println(entry.getTrackIndex() + " event: " + entry + ", " + event.getData().getName() + ", " + event.getInt());
-			}
-		});
-
-		// Set animation on track 0.
-		state.setAnimation(0, "run", true);
-
-		Gdx.input.setInputProcessor(new InputAdapter() {
-			final Vector3 point = new Vector3();
-
-			public boolean touchDown (int screenX, int screenY, int pointer, int button) {
-				camera.unproject(point.set(screenX, screenY, 0)); // Convert window to world coordinates.
-				bounds.update(skeleton, true); // Update SkeletonBounds with current skeleton bounding box positions.
-				if (bounds.aabbContainsPoint(point.x, point.y)) { // Check if inside AABB first. This check is fast.
-					BoundingBoxAttachment hit = bounds.containsPoint(point.x, point.y); // Check if inside a bounding box.
-					if (hit != null) {
-						System.out.println("hit: " + hit);
-						skeleton.findSlot("head").getColor().set(Color.RED); // Turn head red until touchUp.
-					}
-				}
-				return true;
-			}
-
-			public boolean touchUp (int screenX, int screenY, int pointer, int button) {
-				skeleton.findSlot("head").getColor().set(Color.WHITE);
-				return true;
-			}
-
-			public boolean keyDown (int keycode) {
-				state.setAnimation(0, "jump", false); // Set animation on track 0 to jump.
-				state.addAnimation(0, "run", true, 0); // Queue run to play after jump.
-				return true;
-			}
-		});
-	}
-
-	public void render () {
-		state.update(Gdx.graphics.getDeltaTime()); // Update the animation time.
-
-		Gdx.gl.glClear(GL20.GL_COLOR_BUFFER_BIT);
-
-		if (state.apply(skeleton)) // Poses skeleton using current animations. This sets the bones' local SRT.
-			skeleton.updateWorldTransform(); // Uses the bones' local SRT to compute their world SRT.
-
-		// Configure the camera, SpriteBatch, and SkeletonRendererDebug.
-		camera.update();
-		batch.getProjectionMatrix().set(camera.combined);
-		debugRenderer.getShapeRenderer().setProjectionMatrix(camera.combined);
-
-		batch.begin();
-		renderer.draw(batch, skeleton); // Draw the skeleton images.
-		batch.end();
-
-		debugRenderer.draw(skeleton); // Draw debug lines.
-	}
-
-	public void resize (int width, int height) {
-		camera.setToOrtho(false); // Update camera with new size.
-	}
-
-	public void dispose () {
-		atlas.dispose();
-	}
-
-	public static void main (String[] args) throws Exception {
-		new LwjglApplication(new SimpleTest2());
-	}
-}
+/******************************************************************************
+ * Spine Runtimes License Agreement
+ * Last updated January 1, 2020. Replaces all prior versions.
+ *
+ * Copyright (c) 2013-2020, Esoteric Software LLC
+ *
+ * Integration of the Spine Runtimes into software or otherwise creating
+ * derivative works of the Spine Runtimes is permitted under the terms and
+ * conditions of Section 2 of the Spine Editor License Agreement:
+ * http://esotericsoftware.com/spine-editor-license
+ *
+ * Otherwise, it is permitted to integrate the Spine Runtimes into software
+ * or otherwise create derivative works of the Spine Runtimes (collectively,
+ * "Products"), provided that each user of the Products must obtain their own
+ * Spine Editor license and redistribution of the Products in any form must
+ * include this license and copyright notice.
+ *
+ * THE SPINE RUNTIMES ARE PROVIDED BY ESOTERIC SOFTWARE LLC "AS IS" AND ANY
+ * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL ESOTERIC SOFTWARE LLC BE LIABLE FOR ANY
+ * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES,
+ * BUSINESS INTERRUPTION, OR LOSS OF USE, DATA, OR PROFITS) HOWEVER CAUSED AND
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
+ * THE SPINE RUNTIMES, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ *****************************************************************************/
+
+package com.esotericsoftware.spine;
+
+import com.badlogic.gdx.ApplicationAdapter;
+import com.badlogic.gdx.Gdx;
+import com.badlogic.gdx.InputAdapter;
+import com.badlogic.gdx.backends.lwjgl.LwjglApplication;
+import com.badlogic.gdx.graphics.Color;
+import com.badlogic.gdx.graphics.GL20;
+import com.badlogic.gdx.graphics.OrthographicCamera;
+import com.badlogic.gdx.graphics.g2d.TextureAtlas;
+import com.badlogic.gdx.math.Vector3;
+import com.esotericsoftware.spine.AnimationState.AnimationStateListener;
+import com.esotericsoftware.spine.AnimationState.TrackEntry;
+import com.esotericsoftware.spine.attachments.BoundingBoxAttachment;
+import com.esotericsoftware.spine.utils.TwoColorPolygonBatch;
+
+public class SimpleTest2 extends ApplicationAdapter {
+	OrthographicCamera camera;
+	TwoColorPolygonBatch batch;
+	SkeletonRenderer renderer;
+	SkeletonRendererDebug debugRenderer;
+
+	TextureAtlas atlas;
+	Skeleton skeleton;
+	SkeletonBounds bounds;
+	AnimationState state;
+
+	public void create () {
+		camera = new OrthographicCamera();
+		batch = new TwoColorPolygonBatch();
+		renderer = new SkeletonRenderer();
+		renderer.setPremultipliedAlpha(true);
+		debugRenderer = new SkeletonRendererDebug();
+
+		atlas = new TextureAtlas(Gdx.files.internal("spineboy/spineboy-pma.atlas"));
+		SkeletonJson json = new SkeletonJson(atlas); // This loads skeleton JSON data, which is stateless.
+		json.setScale(0.6f); // Load the skeleton at 60% the size it was in Spine.
+		SkeletonData skeletonData = json.readSkeletonData(Gdx.files.internal("spineboy/spineboy-ess.json"));
+
+		skeleton = new Skeleton(skeletonData); // Skeleton holds skeleton state (bone positions, slot attachments, etc).
+		skeleton.setPosition(250, 20);
+		skeleton.setAttachment("head-bb", "head"); // Attach "head" bounding box to "head-bb" slot.
+
+		bounds = new SkeletonBounds(); // Convenience class to do hit detection with bounding boxes.
+
+		AnimationStateData stateData = new AnimationStateData(skeletonData); // Defines mixing (crossfading) between animations.
+		stateData.setMix("run", "jump", 0.2f);
+		stateData.setMix("jump", "run", 0.2f);
+		stateData.setMix("jump", "jump", 0);
+
+		state = new AnimationState(stateData); // Holds the animation state for a skeleton (current animation, time, etc).
+		state.setTimeScale(0.3f); // Slow all animations down to 30% speed.
+		state.addListener(new AnimationStateListener() {
+
+			public void start (TrackEntry entry) {
+				System.out.println(entry.getTrackIndex() + " start: " + entry);
+			}
+
+			public void interrupt (TrackEntry entry) {
+				System.out.println(entry.getTrackIndex() + " interrupt: " + entry);
+			}
+
+			public void end (TrackEntry entry) {
+				System.out.println(entry.getTrackIndex() + " end: " + entry);
+			}
+
+			public void dispose (TrackEntry entry) {
+				System.out.println(entry.getTrackIndex() + " dispose: " + entry);
+			}
+
+			public void complete (TrackEntry entry) {
+				System.out.println(entry.getTrackIndex() + " complete: " + entry);
+			}
+
+			public void event (TrackEntry entry, Event event) {
+				System.out
+					.println(entry.getTrackIndex() + " event: " + entry + ", " + event.getData().getName() + ", " + event.getInt());
+			}
+		});
+
+		// Set animation on track 0.
+		state.setAnimation(0, "run", true);
+
+		Gdx.input.setInputProcessor(new InputAdapter() {
+			final Vector3 point = new Vector3();
+
+			public boolean touchDown (int screenX, int screenY, int pointer, int button) {
+				camera.unproject(point.set(screenX, screenY, 0)); // Convert window to world coordinates.
+				bounds.update(skeleton, true); // Update SkeletonBounds with current skeleton bounding box positions.
+				if (bounds.aabbContainsPoint(point.x, point.y)) { // Check if inside AABB first. This check is fast.
+					BoundingBoxAttachment hit = bounds.containsPoint(point.x, point.y); // Check if inside a bounding box.
+					if (hit != null) {
+						System.out.println("hit: " + hit);
+						skeleton.findSlot("head").getColor().set(Color.RED); // Turn head red until touchUp.
+					}
+				}
+				return true;
+			}
+
+			public boolean touchUp (int screenX, int screenY, int pointer, int button) {
+				skeleton.findSlot("head").getColor().set(Color.WHITE);
+				return true;
+			}
+
+			public boolean keyDown (int keycode) {
+				state.setAnimation(0, "jump", false); // Set animation on track 0 to jump.
+				state.addAnimation(0, "run", true, 0); // Queue run to play after jump.
+				return true;
+			}
+		});
+	}
+
+	public void render () {
+		state.update(Gdx.graphics.getDeltaTime()); // Update the animation time.
+
+		Gdx.gl.glClear(GL20.GL_COLOR_BUFFER_BIT);
+
+		if (state.apply(skeleton)) // Poses skeleton using current animations. This sets the bones' local SRT.
+			skeleton.updateWorldTransform(); // Uses the bones' local SRT to compute their world SRT.
+
+		// Configure the camera, SpriteBatch, and SkeletonRendererDebug.
+		camera.update();
+		batch.getProjectionMatrix().set(camera.combined);
+		debugRenderer.getShapeRenderer().setProjectionMatrix(camera.combined);
+
+		batch.begin();
+		renderer.draw(batch, skeleton); // Draw the skeleton images.
+		batch.end();
+
+		debugRenderer.draw(skeleton); // Draw debug lines.
+	}
+
+	public void resize (int width, int height) {
+		camera.setToOrtho(false); // Update camera with new size.
+	}
+
+	public void dispose () {
+		atlas.dispose();
+	}
+
+	public static void main (String[] args) throws Exception {
+		new LwjglApplication(new SimpleTest2());
+	}
+}

+ 111 - 111
spine-libgdx/spine-libgdx-tests/src/com/esotericsoftware/spine/SimpleTest3.java

@@ -1,111 +1,111 @@
-/******************************************************************************
- * Spine Runtimes License Agreement
- * Last updated January 1, 2020. Replaces all prior versions.
- *
- * Copyright (c) 2013-2020, Esoteric Software LLC
- *
- * Integration of the Spine Runtimes into software or otherwise creating
- * derivative works of the Spine Runtimes is permitted under the terms and
- * conditions of Section 2 of the Spine Editor License Agreement:
- * http://esotericsoftware.com/spine-editor-license
- *
- * Otherwise, it is permitted to integrate the Spine Runtimes into software
- * or otherwise create derivative works of the Spine Runtimes (collectively,
- * "Products"), provided that each user of the Products must obtain their own
- * Spine Editor license and redistribution of the Products in any form must
- * include this license and copyright notice.
- *
- * THE SPINE RUNTIMES ARE PROVIDED BY ESOTERIC SOFTWARE LLC "AS IS" AND ANY
- * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
- * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
- * DISCLAIMED. IN NO EVENT SHALL ESOTERIC SOFTWARE LLC BE LIABLE FOR ANY
- * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
- * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES,
- * BUSINESS INTERRUPTION, OR LOSS OF USE, DATA, OR PROFITS) HOWEVER CAUSED AND
- * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
- * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
- * THE SPINE RUNTIMES, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
- *****************************************************************************/
-
-package com.esotericsoftware.spine;
-
-import com.badlogic.gdx.ApplicationAdapter;
-import com.badlogic.gdx.Gdx;
-import com.badlogic.gdx.backends.lwjgl.LwjglApplication;
-import com.badlogic.gdx.graphics.OrthographicCamera;
-import com.badlogic.gdx.graphics.g2d.PolygonSpriteBatch;
-import com.badlogic.gdx.graphics.g2d.TextureAtlas;
-import com.badlogic.gdx.utils.ScreenUtils;
-
-public class SimpleTest3 extends ApplicationAdapter {
-	OrthographicCamera camera;
-	PolygonSpriteBatch batch;
-	SkeletonRenderer renderer;
-	SkeletonRendererDebug debugRenderer;
-
-	TextureAtlas atlas;
-	Skeleton skeleton;
-	AnimationState state;
-
-	public void create () {
-		camera = new OrthographicCamera();
-		batch = new PolygonSpriteBatch(); // Required to render meshes. SpriteBatch can't render meshes.
-		renderer = new SkeletonRenderer();
-		renderer.setPremultipliedAlpha(true);
-		debugRenderer = new SkeletonRendererDebug();
-		debugRenderer.setMeshTriangles(false);
-		debugRenderer.setRegionAttachments(false);
-		debugRenderer.setMeshHull(false);
-
-		atlas = new TextureAtlas(Gdx.files.internal("raptor/raptor-pma.atlas"));
-
-		SkeletonJson loader = new SkeletonJson(atlas); // This loads skeleton JSON data, which is stateless.
-		// SkeletonLoader loader = new SkeletonBinary(atlas); // Or use SkeletonBinary to load binary data.
-		loader.setScale(0.1f); // Load the skeleton at 50% the size it was in Spine.
-		SkeletonData skeletonData = loader.readSkeletonData(Gdx.files.internal("raptor/raptor-pro.json"));
-
-		skeleton = new Skeleton(skeletonData); // Skeleton holds skeleton state (bone positions, slot attachments, etc).
-		skeleton.setPosition(250, 20);
-
-		AnimationStateData stateData = new AnimationStateData(skeletonData); // Defines mixing (crossfading) between animations.
-
-		state = new AnimationState(stateData); // Holds the animation state for a skeleton (current animation, time, etc).
-		state.setTimeScale(0.6f); // Slow all animations down to 60% speed.
-
-		// Queue animations on tracks 0 and 1.
-		state.setAnimation(0, "walk", true);
-		state.addAnimation(1, "gun-grab", false, 2); // Keys in higher tracks override the pose from lower tracks.
-	}
-
-	public void render () {
-		state.update(Gdx.graphics.getDeltaTime()); // Update the animation time.
-
-		ScreenUtils.clear(0, 0, 0, 0);
-
-		if (state.apply(skeleton)) // Poses skeleton using current animations. This sets the bones' local SRT.
-			skeleton.updateWorldTransform(); // Uses the bones' local SRT to compute their world SRT.
-
-		// Configure the camera, SpriteBatch, and SkeletonRendererDebug.
-		camera.update();
-		batch.getProjectionMatrix().set(camera.combined);
-		debugRenderer.getShapeRenderer().setProjectionMatrix(camera.combined);
-
-		batch.begin();
-		renderer.draw(batch, skeleton); // Draw the skeleton images.
-		batch.end();
-
-		debugRenderer.draw(skeleton); // Draw debug lines.
-	}
-
-	public void resize (int width, int height) {
-		camera.setToOrtho(false); // Update camera with new size.
-	}
-
-	public void dispose () {
-		atlas.dispose();
-	}
-
-	public static void main (String[] args) throws Exception {
-		new LwjglApplication(new SimpleTest3());
-	}
-}
+/******************************************************************************
+ * Spine Runtimes License Agreement
+ * Last updated January 1, 2020. Replaces all prior versions.
+ *
+ * Copyright (c) 2013-2020, Esoteric Software LLC
+ *
+ * Integration of the Spine Runtimes into software or otherwise creating
+ * derivative works of the Spine Runtimes is permitted under the terms and
+ * conditions of Section 2 of the Spine Editor License Agreement:
+ * http://esotericsoftware.com/spine-editor-license
+ *
+ * Otherwise, it is permitted to integrate the Spine Runtimes into software
+ * or otherwise create derivative works of the Spine Runtimes (collectively,
+ * "Products"), provided that each user of the Products must obtain their own
+ * Spine Editor license and redistribution of the Products in any form must
+ * include this license and copyright notice.
+ *
+ * THE SPINE RUNTIMES ARE PROVIDED BY ESOTERIC SOFTWARE LLC "AS IS" AND ANY
+ * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL ESOTERIC SOFTWARE LLC BE LIABLE FOR ANY
+ * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES,
+ * BUSINESS INTERRUPTION, OR LOSS OF USE, DATA, OR PROFITS) HOWEVER CAUSED AND
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
+ * THE SPINE RUNTIMES, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ *****************************************************************************/
+
+package com.esotericsoftware.spine;
+
+import com.badlogic.gdx.ApplicationAdapter;
+import com.badlogic.gdx.Gdx;
+import com.badlogic.gdx.backends.lwjgl.LwjglApplication;
+import com.badlogic.gdx.graphics.OrthographicCamera;
+import com.badlogic.gdx.graphics.g2d.PolygonSpriteBatch;
+import com.badlogic.gdx.graphics.g2d.TextureAtlas;
+import com.badlogic.gdx.utils.ScreenUtils;
+
+public class SimpleTest3 extends ApplicationAdapter {
+	OrthographicCamera camera;
+	PolygonSpriteBatch batch;
+	SkeletonRenderer renderer;
+	SkeletonRendererDebug debugRenderer;
+
+	TextureAtlas atlas;
+	Skeleton skeleton;
+	AnimationState state;
+
+	public void create () {
+		camera = new OrthographicCamera();
+		batch = new PolygonSpriteBatch(); // Required to render meshes. SpriteBatch can't render meshes.
+		renderer = new SkeletonRenderer();
+		renderer.setPremultipliedAlpha(true);
+		debugRenderer = new SkeletonRendererDebug();
+		debugRenderer.setMeshTriangles(false);
+		debugRenderer.setRegionAttachments(false);
+		debugRenderer.setMeshHull(false);
+
+		atlas = new TextureAtlas(Gdx.files.internal("raptor/raptor-pma.atlas"));
+
+		SkeletonJson loader = new SkeletonJson(atlas); // This loads skeleton JSON data, which is stateless.
+		// SkeletonLoader loader = new SkeletonBinary(atlas); // Or use SkeletonBinary to load binary data.
+		loader.setScale(0.1f); // Load the skeleton at 50% the size it was in Spine.
+		SkeletonData skeletonData = loader.readSkeletonData(Gdx.files.internal("raptor/raptor-pro.json"));
+
+		skeleton = new Skeleton(skeletonData); // Skeleton holds skeleton state (bone positions, slot attachments, etc).
+		skeleton.setPosition(250, 20);
+
+		AnimationStateData stateData = new AnimationStateData(skeletonData); // Defines mixing (crossfading) between animations.
+
+		state = new AnimationState(stateData); // Holds the animation state for a skeleton (current animation, time, etc).
+		state.setTimeScale(0.6f); // Slow all animations down to 60% speed.
+
+		// Queue animations on tracks 0 and 1.
+		state.setAnimation(0, "walk", true);
+		state.addAnimation(1, "gun-grab", false, 2); // Keys in higher tracks override the pose from lower tracks.
+	}
+
+	public void render () {
+		state.update(Gdx.graphics.getDeltaTime()); // Update the animation time.
+
+		ScreenUtils.clear(0, 0, 0, 0);
+
+		if (state.apply(skeleton)) // Poses skeleton using current animations. This sets the bones' local SRT.
+			skeleton.updateWorldTransform(); // Uses the bones' local SRT to compute their world SRT.
+
+		// Configure the camera, SpriteBatch, and SkeletonRendererDebug.
+		camera.update();
+		batch.getProjectionMatrix().set(camera.combined);
+		debugRenderer.getShapeRenderer().setProjectionMatrix(camera.combined);
+
+		batch.begin();
+		renderer.draw(batch, skeleton); // Draw the skeleton images.
+		batch.end();
+
+		debugRenderer.draw(skeleton); // Draw debug lines.
+	}
+
+	public void resize (int width, int height) {
+		camera.setToOrtho(false); // Update camera with new size.
+	}
+
+	public void dispose () {
+		atlas.dispose();
+	}
+
+	public static void main (String[] args) throws Exception {
+		new LwjglApplication(new SimpleTest3());
+	}
+}

+ 117 - 117
spine-libgdx/spine-libgdx-tests/src/com/esotericsoftware/spine/SkeletonAttachmentTest.java

@@ -1,117 +1,117 @@
-/******************************************************************************
- * Spine Runtimes License Agreement
- * Last updated January 1, 2020. Replaces all prior versions.
- *
- * Copyright (c) 2013-2020, Esoteric Software LLC
- *
- * Integration of the Spine Runtimes into software or otherwise creating
- * derivative works of the Spine Runtimes is permitted under the terms and
- * conditions of Section 2 of the Spine Editor License Agreement:
- * http://esotericsoftware.com/spine-editor-license
- *
- * Otherwise, it is permitted to integrate the Spine Runtimes into software
- * or otherwise create derivative works of the Spine Runtimes (collectively,
- * "Products"), provided that each user of the Products must obtain their own
- * Spine Editor license and redistribution of the Products in any form must
- * include this license and copyright notice.
- *
- * THE SPINE RUNTIMES ARE PROVIDED BY ESOTERIC SOFTWARE LLC "AS IS" AND ANY
- * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
- * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
- * DISCLAIMED. IN NO EVENT SHALL ESOTERIC SOFTWARE LLC BE LIABLE FOR ANY
- * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
- * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES,
- * BUSINESS INTERRUPTION, OR LOSS OF USE, DATA, OR PROFITS) HOWEVER CAUSED AND
- * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
- * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
- * THE SPINE RUNTIMES, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
- *****************************************************************************/
-
-package com.esotericsoftware.spine;
-
-import com.badlogic.gdx.ApplicationAdapter;
-import com.badlogic.gdx.Gdx;
-import com.badlogic.gdx.backends.lwjgl.LwjglApplication;
-import com.badlogic.gdx.graphics.OrthographicCamera;
-import com.badlogic.gdx.graphics.g2d.PolygonSpriteBatch;
-import com.badlogic.gdx.graphics.g2d.TextureAtlas;
-import com.badlogic.gdx.utils.ScreenUtils;
-
-import com.esotericsoftware.spine.attachments.SkeletonAttachment;
-
-public class SkeletonAttachmentTest extends ApplicationAdapter {
-	OrthographicCamera camera;
-	PolygonSpriteBatch batch;
-	SkeletonRenderer renderer;
-
-	Skeleton spineboy, goblin;
-	AnimationState spineboyState, goblinState;
-	Bone attachmentBone;
-
-	public void create () {
-		camera = new OrthographicCamera();
-		batch = new PolygonSpriteBatch();
-		renderer = new SkeletonRenderer();
-		renderer.setPremultipliedAlpha(true);
-
-		{
-			TextureAtlas atlas = new TextureAtlas(Gdx.files.internal("spineboy/spineboy-pma.atlas"));
-			SkeletonJson json = new SkeletonJson(atlas);
-			json.setScale(0.6f);
-			SkeletonData skeletonData = json.readSkeletonData(Gdx.files.internal("spineboy/spineboy-ess.json"));
-			spineboy = new Skeleton(skeletonData);
-			spineboy.setPosition(320, 20);
-
-			AnimationStateData stateData = new AnimationStateData(skeletonData);
-			stateData.setMix("walk", "jump", 0.2f);
-			stateData.setMix("jump", "walk", 0.2f);
-			spineboyState = new AnimationState(stateData);
-			spineboyState.addAnimation(0, "walk", true, 0);
-		}
-
-		{
-			TextureAtlas atlas = new TextureAtlas(Gdx.files.internal("goblins/goblins-pma.atlas"));
-			SkeletonJson json = new SkeletonJson(atlas);
-			SkeletonData skeletonData = json.readSkeletonData(Gdx.files.internal("goblins/goblins-pro.json"));
-			goblin = new Skeleton(skeletonData);
-			goblin.setSkin("goblin");
-			goblin.setSlotsToSetupPose();
-
-			goblinState = new AnimationState(new AnimationStateData(skeletonData));
-			goblinState.setAnimation(0, "walk", true);
-
-			// Instead of a right shoulder, spineboy will have a goblin!
-			SkeletonAttachment skeletonAttachment = new SkeletonAttachment("goblin");
-			skeletonAttachment.setSkeleton(goblin);
-			Slot slot = spineboy.findSlot("front-upper-arm");
-			slot.setAttachment(skeletonAttachment);
-			attachmentBone = slot.getBone();
-		}
-	}
-
-	public void render () {
-		spineboyState.update(Gdx.graphics.getDeltaTime());
-		spineboyState.apply(spineboy);
-		spineboy.updateWorldTransform();
-
-		goblinState.update(Gdx.graphics.getDeltaTime());
-		goblinState.apply(goblin);
-		goblin.updateWorldTransform(attachmentBone);
-
-		ScreenUtils.clear(0, 0, 0, 0);
-
-		camera.update();
-		batch.getProjectionMatrix().set(camera.combined);
-		batch.begin();
-		renderer.draw(batch, spineboy);
-		batch.end();
-	}
-
-	public void resize (int width, int height) {
-		camera.setToOrtho(false);
-	}
-
-	public static void main (String[] args) throws Exception {
-		new LwjglApplication(new SkeletonAttachmentTest());
-	}
-}
+/******************************************************************************
+ * Spine Runtimes License Agreement
+ * Last updated January 1, 2020. Replaces all prior versions.
+ *
+ * Copyright (c) 2013-2020, Esoteric Software LLC
+ *
+ * Integration of the Spine Runtimes into software or otherwise creating
+ * derivative works of the Spine Runtimes is permitted under the terms and
+ * conditions of Section 2 of the Spine Editor License Agreement:
+ * http://esotericsoftware.com/spine-editor-license
+ *
+ * Otherwise, it is permitted to integrate the Spine Runtimes into software
+ * or otherwise create derivative works of the Spine Runtimes (collectively,
+ * "Products"), provided that each user of the Products must obtain their own
+ * Spine Editor license and redistribution of the Products in any form must
+ * include this license and copyright notice.
+ *
+ * THE SPINE RUNTIMES ARE PROVIDED BY ESOTERIC SOFTWARE LLC "AS IS" AND ANY
+ * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL ESOTERIC SOFTWARE LLC BE LIABLE FOR ANY
+ * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES,
+ * BUSINESS INTERRUPTION, OR LOSS OF USE, DATA, OR PROFITS) HOWEVER CAUSED AND
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
+ * THE SPINE RUNTIMES, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ *****************************************************************************/
+
+package com.esotericsoftware.spine;
+
+import com.badlogic.gdx.ApplicationAdapter;
+import com.badlogic.gdx.Gdx;
+import com.badlogic.gdx.backends.lwjgl.LwjglApplication;
+import com.badlogic.gdx.graphics.OrthographicCamera;
+import com.badlogic.gdx.graphics.g2d.PolygonSpriteBatch;
+import com.badlogic.gdx.graphics.g2d.TextureAtlas;
+import com.badlogic.gdx.utils.ScreenUtils;
+
+import com.esotericsoftware.spine.attachments.SkeletonAttachment;
+
+public class SkeletonAttachmentTest extends ApplicationAdapter {
+	OrthographicCamera camera;
+	PolygonSpriteBatch batch;
+	SkeletonRenderer renderer;
+
+	Skeleton spineboy, goblin;
+	AnimationState spineboyState, goblinState;
+	Bone attachmentBone;
+
+	public void create () {
+		camera = new OrthographicCamera();
+		batch = new PolygonSpriteBatch();
+		renderer = new SkeletonRenderer();
+		renderer.setPremultipliedAlpha(true);
+
+		{
+			TextureAtlas atlas = new TextureAtlas(Gdx.files.internal("spineboy/spineboy-pma.atlas"));
+			SkeletonJson json = new SkeletonJson(atlas);
+			json.setScale(0.6f);
+			SkeletonData skeletonData = json.readSkeletonData(Gdx.files.internal("spineboy/spineboy-ess.json"));
+			spineboy = new Skeleton(skeletonData);
+			spineboy.setPosition(320, 20);
+
+			AnimationStateData stateData = new AnimationStateData(skeletonData);
+			stateData.setMix("walk", "jump", 0.2f);
+			stateData.setMix("jump", "walk", 0.2f);
+			spineboyState = new AnimationState(stateData);
+			spineboyState.addAnimation(0, "walk", true, 0);
+		}
+
+		{
+			TextureAtlas atlas = new TextureAtlas(Gdx.files.internal("goblins/goblins-pma.atlas"));
+			SkeletonJson json = new SkeletonJson(atlas);
+			SkeletonData skeletonData = json.readSkeletonData(Gdx.files.internal("goblins/goblins-pro.json"));
+			goblin = new Skeleton(skeletonData);
+			goblin.setSkin("goblin");
+			goblin.setSlotsToSetupPose();
+
+			goblinState = new AnimationState(new AnimationStateData(skeletonData));
+			goblinState.setAnimation(0, "walk", true);
+
+			// Instead of a right shoulder, spineboy will have a goblin!
+			SkeletonAttachment skeletonAttachment = new SkeletonAttachment("goblin");
+			skeletonAttachment.setSkeleton(goblin);
+			Slot slot = spineboy.findSlot("front-upper-arm");
+			slot.setAttachment(skeletonAttachment);
+			attachmentBone = slot.getBone();
+		}
+	}
+
+	public void render () {
+		spineboyState.update(Gdx.graphics.getDeltaTime());
+		spineboyState.apply(spineboy);
+		spineboy.updateWorldTransform();
+
+		goblinState.update(Gdx.graphics.getDeltaTime());
+		goblinState.apply(goblin);
+		goblin.updateWorldTransform(attachmentBone);
+
+		ScreenUtils.clear(0, 0, 0, 0);
+
+		camera.update();
+		batch.getProjectionMatrix().set(camera.combined);
+		batch.begin();
+		renderer.draw(batch, spineboy);
+		batch.end();
+	}
+
+	public void resize (int width, int height) {
+		camera.setToOrtho(false);
+	}
+
+	public static void main (String[] args) throws Exception {
+		new LwjglApplication(new SkeletonAttachmentTest());
+	}
+}

+ 2430 - 2430
spine-libgdx/spine-libgdx/src/com/esotericsoftware/spine/Animation.java

@@ -1,2430 +1,2430 @@
-/******************************************************************************
- * Spine Runtimes License Agreement
- * Last updated January 1, 2020. Replaces all prior versions.
- *
- * Copyright (c) 2013-2020, Esoteric Software LLC
- *
- * Integration of the Spine Runtimes into software or otherwise creating
- * derivative works of the Spine Runtimes is permitted under the terms and
- * conditions of Section 2 of the Spine Editor License Agreement:
- * http://esotericsoftware.com/spine-editor-license
- *
- * Otherwise, it is permitted to integrate the Spine Runtimes into software
- * or otherwise create derivative works of the Spine Runtimes (collectively,
- * "Products"), provided that each user of the Products must obtain their own
- * Spine Editor license and redistribution of the Products in any form must
- * include this license and copyright notice.
- *
- * THE SPINE RUNTIMES ARE PROVIDED BY ESOTERIC SOFTWARE LLC "AS IS" AND ANY
- * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
- * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
- * DISCLAIMED. IN NO EVENT SHALL ESOTERIC SOFTWARE LLC BE LIABLE FOR ANY
- * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
- * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES,
- * BUSINESS INTERRUPTION, OR LOSS OF USE, DATA, OR PROFITS) HOWEVER CAUSED AND
- * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
- * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
- * THE SPINE RUNTIMES, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
- *****************************************************************************/
-
-package com.esotericsoftware.spine;
-
-import static com.esotericsoftware.spine.Animation.MixBlend.*;
-import static com.esotericsoftware.spine.Animation.MixDirection.*;
-import static com.esotericsoftware.spine.utils.SpineUtils.*;
-
-import com.badlogic.gdx.graphics.Color;
-import com.badlogic.gdx.utils.Array;
-import com.badlogic.gdx.utils.FloatArray;
-import com.badlogic.gdx.utils.Null;
-import com.badlogic.gdx.utils.ObjectSet;
-
-import com.esotericsoftware.spine.attachments.Attachment;
-import com.esotericsoftware.spine.attachments.VertexAttachment;
-
-/** Stores a list of timelines to animate a skeleton's pose over time. */
-public class Animation {
-	final String name;
-	Array<Timeline> timelines;
-	final ObjectSet<String> timelineIds;
-	float duration;
-
-	public Animation (String name, Array<Timeline> timelines, float duration) {
-		if (name == null) throw new IllegalArgumentException("name cannot be null.");
-		this.name = name;
-		this.duration = duration;
-		timelineIds = new ObjectSet(timelines.size);
-		setTimelines(timelines);
-	}
-
-	/** If the returned array or the timelines it contains are modified, {@link #setTimelines(Array)} must be called. */
-	public Array<Timeline> getTimelines () {
-		return timelines;
-	}
-
-	public void setTimelines (Array<Timeline> timelines) {
-		if (timelines == null) throw new IllegalArgumentException("timelines cannot be null.");
-		this.timelines = timelines;
-
-		int n = timelines.size;
-		timelineIds.clear(n);
-		Object[] items = timelines.items;
-		for (int i = 0; i < n; i++)
-			timelineIds.addAll(((Timeline)items[i]).getPropertyIds());
-	}
-
-	/** Returns true if this animation contains a timeline with any of the specified property IDs. */
-	public boolean hasTimeline (String[] propertyIds) {
-		for (String id : propertyIds)
-			if (timelineIds.contains(id)) return true;
-		return false;
-	}
-
-	/** The duration of the animation in seconds, which is usually the highest time of all frames in the timeline. The duration is
-	 * used to know when it has completed and when it should loop back to the start. */
-	public float getDuration () {
-		return duration;
-	}
-
-	public void setDuration (float duration) {
-		this.duration = duration;
-	}
-
-	/** Applies the animation's timelines to the specified skeleton.
-	 * <p>
-	 * See Timeline {@link Timeline#apply(Skeleton, float, float, Array, float, MixBlend, MixDirection)}.
-	 * @param skeleton The skeleton the animation is being applied to. This provides access to the bones, slots, and other skeleton
-	 *           components the timelines may change.
-	 * @param lastTime The last time in seconds this animation was applied. Some timelines trigger only at specific times rather
-	 *           than every frame. Pass -1 the first time an animation is applied to ensure frame 0 is triggered.
-	 * @param time The time in seconds the skeleton is being posed for. Most timelines find the frame before and the frame after
-	 *           this time and interpolate between the frame values. If beyond the {@link #getDuration()} and <code>loop</code> is
-	 *           true then the animation will repeat, else the last frame will be applied.
-	 * @param loop If true, the animation repeats after the {@link #getDuration()}.
-	 * @param events If any events are fired, they are added to this list. Can be null to ignore fired events or if no timelines
-	 *           fire events.
-	 * @param alpha 0 applies the current or setup values (depending on <code>blend</code>). 1 applies the timeline values. Between
-	 *           0 and 1 applies values between the current or setup values and the timeline values. By adjusting
-	 *           <code>alpha</code> over time, an animation can be mixed in or out. <code>alpha</code> can also be useful to apply
-	 *           animations on top of each other (layering).
-	 * @param blend Controls how mixing is applied when <code>alpha</code> < 1.
-	 * @param direction Indicates whether the timelines are mixing in or out. Used by timelines which perform instant transitions,
-	 *           such as {@link DrawOrderTimeline} or {@link AttachmentTimeline}. */
-	public void apply (Skeleton skeleton, float lastTime, float time, boolean loop, @Null Array<Event> events, float alpha,
-		MixBlend blend, MixDirection direction) {
-		if (skeleton == null) throw new IllegalArgumentException("skeleton cannot be null.");
-
-		if (loop && duration != 0) {
-			time %= duration;
-			if (lastTime > 0) lastTime %= duration;
-		}
-
-		Object[] timelines = this.timelines.items;
-		for (int i = 0, n = this.timelines.size; i < n; i++)
-			((Timeline)timelines[i]).apply(skeleton, lastTime, time, events, alpha, blend, direction);
-	}
-
-	/** The animation's name, which is unique across all animations in the skeleton. */
-	public String getName () {
-		return name;
-	}
-
-	public String toString () {
-		return name;
-	}
-
-	/** Controls how timeline values are mixed with setup pose values or current pose values when a timeline is applied with
-	 * <code>alpha</code> < 1.
-	 * <p>
-	 * See Timeline {@link Timeline#apply(Skeleton, float, float, Array, float, MixBlend, MixDirection)}. */
-	static public enum MixBlend {
-		/** Transitions from the setup value to the timeline value (the current value is not used). Before the first frame, the
-		 * setup value is set. */
-		setup,
-		/** Transitions from the current value to the timeline value. Before the first frame, transitions from the current value to
-		 * the setup value. Timelines which perform instant transitions, such as {@link DrawOrderTimeline} or
-		 * {@link AttachmentTimeline}, use the setup value before the first frame.
-		 * <p>
-		 * <code>first</code> is intended for the first animations applied, not for animations layered on top of those. */
-		first,
-		/** Transitions from the current value to the timeline value. No change is made before the first frame (the current value is
-		 * kept until the first frame).
-		 * <p>
-		 * <code>replace</code> is intended for animations layered on top of others, not for the first animations applied. */
-		replace,
-		/** Transitions from the current value to the current value plus the timeline value. No change is made before the first
-		 * frame (the current value is kept until the first frame).
-		 * <p>
-		 * <code>add</code> is intended for animations layered on top of others, not for the first animations applied. Properties
-		 * set by additive animations must be set manually or by another animation before applying the additive animations, else the
-		 * property values will increase each time the additive animations are applied. */
-		add
-	}
-
-	/** Indicates whether a timeline's <code>alpha</code> is mixing out over time toward 0 (the setup or current pose value) or
-	 * mixing in toward 1 (the timeline's value). Some timelines use this to decide how values are applied.
-	 * <p>
-	 * See Timeline {@link Timeline#apply(Skeleton, float, float, Array, float, MixBlend, MixDirection)}. */
-	static public enum MixDirection {
-		in, out
-	}
-
-	static private enum Property {
-		rotate, x, y, scaleX, scaleY, shearX, shearY, //
-		rgb, alpha, rgb2, //
-		attachment, deform, //
-		event, drawOrder, //
-		ikConstraint, transformConstraint, //
-		pathConstraintPosition, pathConstraintSpacing, pathConstraintMix
-	}
-
-	/** The base class for all timelines. */
-	static public abstract class Timeline {
-		private final String[] propertyIds;
-		final float[] frames;
-
-		/** @param propertyIds Unique identifiers for the properties the timeline modifies. */
-		public Timeline (int frameCount, String... propertyIds) {
-			if (propertyIds == null) throw new IllegalArgumentException("propertyIds cannot be null.");
-			this.propertyIds = propertyIds;
-			frames = new float[frameCount * getFrameEntries()];
-		}
-
-		/** Uniquely encodes both the type of this timeline and the skeleton properties that it affects. */
-		public String[] getPropertyIds () {
-			return propertyIds;
-		}
-
-		/** The time in seconds and any other values for each frame. */
-		public float[] getFrames () {
-			return frames;
-		}
-
-		/** The number of entries stored per frame. */
-		public int getFrameEntries () {
-			return 1;
-		}
-
-		/** The number of frames for this timeline. */
-		public int getFrameCount () {
-			return frames.length / getFrameEntries();
-		}
-
-		public float getDuration () {
-			return frames[frames.length - getFrameEntries()];
-		}
-
-		/** Applies this timeline to the skeleton.
-		 * @param skeleton The skeleton to which the timeline is being applied. This provides access to the bones, slots, and other
-		 *           skeleton components that the timeline may change.
-		 * @param lastTime The last time in seconds this timeline was applied. Timelines such as {@link EventTimeline} trigger only
-		 *           at specific times rather than every frame. In that case, the timeline triggers everything between
-		 *           <code>lastTime</code> (exclusive) and <code>time</code> (inclusive). Pass -1 the first time an animation is
-		 *           applied to ensure frame 0 is triggered.
-		 * @param time The time in seconds that the skeleton is being posed for. Most timelines find the frame before and the frame
-		 *           after this time and interpolate between the frame values. If beyond the last frame, the last frame will be
-		 *           applied.
-		 * @param events If any events are fired, they are added to this list. Can be null to ignore fired events or if the timeline
-		 *           does not fire events.
-		 * @param alpha 0 applies the current or setup value (depending on <code>blend</code>). 1 applies the timeline value.
-		 *           Between 0 and 1 applies a value between the current or setup value and the timeline value. By adjusting
-		 *           <code>alpha</code> over time, an animation can be mixed in or out. <code>alpha</code> can also be useful to
-		 *           apply animations on top of each other (layering).
-		 * @param blend Controls how mixing is applied when <code>alpha</code> < 1.
-		 * @param direction Indicates whether the timeline is mixing in or out. Used by timelines which perform instant transitions,
-		 *           such as {@link DrawOrderTimeline} or {@link AttachmentTimeline}, and others such as {@link ScaleTimeline}. */
-		abstract public void apply (Skeleton skeleton, float lastTime, float time, @Null Array<Event> events, float alpha,
-			MixBlend blend, MixDirection direction);
-
-		/** Linear search using a stride of 1.
-		 * @param time Must be >= the first value in <code>frames</code>.
-		 * @return The index of the first value <= <code>time</code>. */
-		static int search (float[] frames, float time) {
-			int n = frames.length;
-			for (int i = 1; i < n; i++)
-				if (frames[i] > time) return i - 1;
-			return n - 1;
-		}
-
-		/** Linear search using the specified stride.
-		 * @param time Must be >= the first value in <code>frames</code>.
-		 * @return The index of the first value <= <code>time</code>. */
-		static int search (float[] frames, float time, int step) {
-			int n = frames.length;
-			for (int i = step; i < n; i += step)
-				if (frames[i] > time) return i - step;
-			return n - step;
-		}
-	}
-
-	/** An interface for timelines which change the property of a bone. */
-	static public interface BoneTimeline {
-		/** The index of the bone in {@link Skeleton#getBones()} that will be changed when this timeline is applied. */
-		public int getBoneIndex ();
-	}
-
-	/** An interface for timelines which change the property of a slot. */
-	static public interface SlotTimeline {
-		/** The index of the slot in {@link Skeleton#getSlots()} that will be changed when this timeline is applied. */
-		public int getSlotIndex ();
-	}
-
-	/** The base class for timelines that interpolate between frame values using stepped, linear, or a Bezier curve. */
-	static public abstract class CurveTimeline extends Timeline {
-		static public final int LINEAR = 0, STEPPED = 1, BEZIER = 2, BEZIER_SIZE = 18;
-
-		float[] curves;
-
-		/** @param bezierCount The maximum number of Bezier curves. See {@link #shrink(int)}.
-		 * @param propertyIds Unique identifiers for the properties the timeline modifies. */
-		public CurveTimeline (int frameCount, int bezierCount, String... propertyIds) {
-			super(frameCount, propertyIds);
-			curves = new float[frameCount + bezierCount * BEZIER_SIZE];
-			curves[frameCount - 1] = STEPPED;
-		}
-
-		/** Sets the specified frame to linear interpolation.
-		 * @param frame Between 0 and <code>frameCount - 1</code>, inclusive. */
-		public void setLinear (int frame) {
-			curves[frame] = LINEAR;
-		}
-
-		/** Sets the specified frame to stepped interpolation.
-		 * @param frame Between 0 and <code>frameCount - 1</code>, inclusive. */
-		public void setStepped (int frame) {
-			curves[frame] = STEPPED;
-		}
-
-		/** Returns the interpolation type for the specified frame.
-		 * @param frame Between 0 and <code>frameCount - 1</code>, inclusive.
-		 * @return {@link #LINEAR}, {@link #STEPPED}, or {@link #BEZIER} + the index of the Bezier segments. */
-		public int getCurveType (int frame) {
-			return (int)curves[frame];
-		}
-
-		/** Shrinks the storage for Bezier curves, for use when <code>bezierCount</code> (specified in the constructor) was larger
-		 * than the actual number of Bezier curves. */
-		public void shrink (int bezierCount) {
-			int size = getFrameCount() + bezierCount * BEZIER_SIZE;
-			if (curves.length > size) {
-				float[] newCurves = new float[size];
-				arraycopy(curves, 0, newCurves, 0, size);
-				curves = newCurves;
-			}
-		}
-
-		/** Stores the segments for the specified Bezier curve. For timelines that modify multiple values, there may be more than
-		 * one curve per frame.
-		 * @param bezier The ordinal of this Bezier curve for this timeline, between 0 and <code>bezierCount - 1</code> (specified
-		 *           in the constructor), inclusive.
-		 * @param frame Between 0 and <code>frameCount - 1</code>, inclusive.
-		 * @param value The index of the value for the frame this curve is used for.
-		 * @param time1 The time for the first key.
-		 * @param value1 The value for the first key.
-		 * @param cx1 The time for the first Bezier handle.
-		 * @param cy1 The value for the first Bezier handle.
-		 * @param cx2 The time of the second Bezier handle.
-		 * @param cy2 The value for the second Bezier handle.
-		 * @param time2 The time for the second key.
-		 * @param value2 The value for the second key. */
-		public void setBezier (int bezier, int frame, int value, float time1, float value1, float cx1, float cy1, float cx2,
-			float cy2, float time2, float value2) {
-			float[] curves = this.curves;
-			int i = getFrameCount() + bezier * BEZIER_SIZE;
-			if (value == 0) curves[frame] = BEZIER + i;
-			float tmpx = (time1 - cx1 * 2 + cx2) * 0.03f, tmpy = (value1 - cy1 * 2 + cy2) * 0.03f;
-			float dddx = ((cx1 - cx2) * 3 - time1 + time2) * 0.006f, dddy = ((cy1 - cy2) * 3 - value1 + value2) * 0.006f;
-			float ddx = tmpx * 2 + dddx, ddy = tmpy * 2 + dddy;
-			float dx = (cx1 - time1) * 0.3f + tmpx + dddx * 0.16666667f, dy = (cy1 - value1) * 0.3f + tmpy + dddy * 0.16666667f;
-			float x = time1 + dx, y = value1 + dy;
-			for (int n = i + BEZIER_SIZE; i < n; i += 2) {
-				curves[i] = x;
-				curves[i + 1] = y;
-				dx += ddx;
-				dy += ddy;
-				ddx += dddx;
-				ddy += dddy;
-				x += dx;
-				y += dy;
-			}
-		}
-
-		/** Returns the Bezier interpolated value for the specified time.
-		 * @param frameIndex The index into {@link #getFrames()} for the values of the frame before <code>time</code>.
-		 * @param valueOffset The offset from <code>frameIndex</code> to the value this curve is used for.
-		 * @param i The index of the Bezier segments. See {@link #getCurveType(int)}. */
-		public float getBezierValue (float time, int frameIndex, int valueOffset, int i) {
-			float[] curves = this.curves;
-			if (curves[i] > time) {
-				float x = frames[frameIndex], y = frames[frameIndex + valueOffset];
-				return y + (time - x) / (curves[i] - x) * (curves[i + 1] - y);
-			}
-			int n = i + BEZIER_SIZE;
-			for (i += 2; i < n; i += 2) {
-				if (curves[i] >= time) {
-					float x = curves[i - 2], y = curves[i - 1];
-					return y + (time - x) / (curves[i] - x) * (curves[i + 1] - y);
-				}
-			}
-			frameIndex += getFrameEntries();
-			float x = curves[n - 2], y = curves[n - 1];
-			return y + (time - x) / (frames[frameIndex] - x) * (frames[frameIndex + valueOffset] - y);
-		}
-	}
-
-	/** The base class for a {@link CurveTimeline} that sets one property. */
-	static public abstract class CurveTimeline1 extends CurveTimeline {
-		static public final int ENTRIES = 2;
-		static final int VALUE = 1;
-
-		/** @param bezierCount The maximum number of Bezier curves. See {@link #shrink(int)}.
-		 * @param propertyId Unique identifier for the property the timeline modifies. */
-		public CurveTimeline1 (int frameCount, int bezierCount, String propertyId) {
-			super(frameCount, bezierCount, propertyId);
-		}
-
-		public int getFrameEntries () {
-			return ENTRIES;
-		}
-
-		/** Sets the time and value for the specified frame.
-		 * @param frame Between 0 and <code>frameCount</code>, inclusive.
-		 * @param time The frame time in seconds. */
-		public void setFrame (int frame, float time, float value) {
-			frame <<= 1;
-			frames[frame] = time;
-			frames[frame + VALUE] = value;
-		}
-
-		/** Returns the interpolated value for the specified time. */
-		public float getCurveValue (float time) {
-			float[] frames = this.frames;
-			int i = frames.length - 2;
-			for (int ii = 2; ii <= i; ii += 2) {
-				if (frames[ii] > time) {
-					i = ii - 2;
-					break;
-				}
-			}
-
-			int curveType = (int)curves[i >> 1];
-			switch (curveType) {
-			case LINEAR:
-				float before = frames[i], value = frames[i + VALUE];
-				return value + (time - before) / (frames[i + ENTRIES] - before) * (frames[i + ENTRIES + VALUE] - value);
-			case STEPPED:
-				return frames[i + VALUE];
-			}
-			return getBezierValue(time, i, VALUE, curveType - BEZIER);
-		}
-	}
-
-	/** The base class for a {@link CurveTimeline} which sets two properties. */
-	static public abstract class CurveTimeline2 extends CurveTimeline {
-		static public final int ENTRIES = 3;
-		static final int VALUE1 = 1, VALUE2 = 2;
-
-		/** @param bezierCount The maximum number of Bezier curves. See {@link #shrink(int)}.
-		 * @param propertyId1 Unique identifier for the first property the timeline modifies.
-		 * @param propertyId2 Unique identifier for the second property the timeline modifies. */
-		public CurveTimeline2 (int frameCount, int bezierCount, String propertyId1, String propertyId2) {
-			super(frameCount, bezierCount, propertyId1, propertyId2);
-		}
-
-		public int getFrameEntries () {
-			return ENTRIES;
-		}
-
-		/** Sets the time and values for the specified frame.
-		 * @param frame Between 0 and <code>frameCount</code>, inclusive.
-		 * @param time The frame time in seconds. */
-		public void setFrame (int frame, float time, float value1, float value2) {
-			frame *= ENTRIES;
-			frames[frame] = time;
-			frames[frame + VALUE1] = value1;
-			frames[frame + VALUE2] = value2;
-		}
-	}
-
-	/** Changes a bone's local {@link Bone#getRotation()}. */
-	static public class RotateTimeline extends CurveTimeline1 implements BoneTimeline {
-		final int boneIndex;
-
-		public RotateTimeline (int frameCount, int bezierCount, int boneIndex) {
-			super(frameCount, bezierCount, Property.rotate.ordinal() + "|" + boneIndex);
-			this.boneIndex = boneIndex;
-		}
-
-		public int getBoneIndex () {
-			return boneIndex;
-		}
-
-		public void apply (Skeleton skeleton, float lastTime, float time, @Null Array<Event> events, float alpha, MixBlend blend,
-			MixDirection direction) {
-
-			Bone bone = skeleton.bones.get(boneIndex);
-			if (!bone.active) return;
-
-			if (time < frames[0]) { // Time is before first frame.
-				switch (blend) {
-				case setup:
-					bone.rotation = bone.data.rotation;
-					return;
-				case first:
-					bone.rotation += (bone.data.rotation - bone.rotation) * alpha;
-				}
-				return;
-			}
-
-			float r = getCurveValue(time);
-			switch (blend) {
-			case setup:
-				bone.rotation = bone.data.rotation + r * alpha;
-				break;
-			case first:
-			case replace:
-				r += bone.data.rotation - bone.rotation;
-				// Fall through.
-			case add:
-				bone.rotation += r * alpha;
-			}
-		}
-	}
-
-	/** Changes a bone's local {@link Bone#getX()} and {@link Bone#getY()}. */
-	static public class TranslateTimeline extends CurveTimeline2 implements BoneTimeline {
-		final int boneIndex;
-
-		public TranslateTimeline (int frameCount, int bezierCount, int boneIndex) {
-			super(frameCount, bezierCount, //
-				Property.x.ordinal() + "|" + boneIndex, //
-				Property.y.ordinal() + "|" + boneIndex);
-			this.boneIndex = boneIndex;
-		}
-
-		public int getBoneIndex () {
-			return boneIndex;
-		}
-
-		public void apply (Skeleton skeleton, float lastTime, float time, @Null Array<Event> events, float alpha, MixBlend blend,
-			MixDirection direction) {
-
-			Bone bone = skeleton.bones.get(boneIndex);
-			if (!bone.active) return;
-
-			float[] frames = this.frames;
-			if (time < frames[0]) { // Time is before first frame.
-				switch (blend) {
-				case setup:
-					bone.x = bone.data.x;
-					bone.y = bone.data.y;
-					return;
-				case first:
-					bone.x += (bone.data.x - bone.x) * alpha;
-					bone.y += (bone.data.y - bone.y) * alpha;
-				}
-				return;
-			}
-
-			float x, y;
-			int i = search(frames, time, ENTRIES), curveType = (int)curves[i / ENTRIES];
-			switch (curveType) {
-			case LINEAR:
-				float before = frames[i];
-				x = frames[i + VALUE1];
-				y = frames[i + VALUE2];
-				float t = (time - before) / (frames[i + ENTRIES] - before);
-				x += (frames[i + ENTRIES + VALUE1] - x) * t;
-				y += (frames[i + ENTRIES + VALUE2] - y) * t;
-				break;
-			case STEPPED:
-				x = frames[i + VALUE1];
-				y = frames[i + VALUE2];
-				break;
-			default:
-				x = getBezierValue(time, i, VALUE1, curveType - BEZIER);
-				y = getBezierValue(time, i, VALUE2, curveType + BEZIER_SIZE - BEZIER);
-			}
-
-			switch (blend) {
-			case setup:
-				bone.x = bone.data.x + x * alpha;
-				bone.y = bone.data.y + y * alpha;
-				break;
-			case first:
-			case replace:
-				bone.x += (bone.data.x + x - bone.x) * alpha;
-				bone.y += (bone.data.y + y - bone.y) * alpha;
-				break;
-			case add:
-				bone.x += x * alpha;
-				bone.y += y * alpha;
-			}
-		}
-	}
-
-	/** Changes a bone's local {@link Bone#getX()}. */
-	static public class TranslateXTimeline extends CurveTimeline1 implements BoneTimeline {
-		final int boneIndex;
-
-		public TranslateXTimeline (int frameCount, int bezierCount, int boneIndex) {
-			super(frameCount, bezierCount, Property.x.ordinal() + "|" + boneIndex);
-			this.boneIndex = boneIndex;
-		}
-
-		public int getBoneIndex () {
-			return boneIndex;
-		}
-
-		public void apply (Skeleton skeleton, float lastTime, float time, @Null Array<Event> events, float alpha, MixBlend blend,
-			MixDirection direction) {
-
-			Bone bone = skeleton.bones.get(boneIndex);
-			if (!bone.active) return;
-
-			float[] frames = this.frames;
-			if (time < frames[0]) { // Time is before first frame.
-				switch (blend) {
-				case setup:
-					bone.x = bone.data.x;
-					return;
-				case first:
-					bone.x += (bone.data.x - bone.x) * alpha;
-				}
-				return;
-			}
-
-			float x = getCurveValue(time);
-			switch (blend) {
-			case setup:
-				bone.x = bone.data.x + x * alpha;
-				break;
-			case first:
-			case replace:
-				bone.x += (bone.data.x + x - bone.x) * alpha;
-				break;
-			case add:
-				bone.x += x * alpha;
-			}
-		}
-	}
-
-	/** Changes a bone's local {@link Bone#getY()}. */
-	static public class TranslateYTimeline extends CurveTimeline1 implements BoneTimeline {
-		final int boneIndex;
-
-		public TranslateYTimeline (int frameCount, int bezierCount, int boneIndex) {
-			super(frameCount, bezierCount, Property.y.ordinal() + "|" + boneIndex);
-			this.boneIndex = boneIndex;
-		}
-
-		public int getBoneIndex () {
-			return boneIndex;
-		}
-
-		public void apply (Skeleton skeleton, float lastTime, float time, @Null Array<Event> events, float alpha, MixBlend blend,
-			MixDirection direction) {
-
-			Bone bone = skeleton.bones.get(boneIndex);
-			if (!bone.active) return;
-
-			float[] frames = this.frames;
-			if (time < frames[0]) { // Time is before first frame.
-				switch (blend) {
-				case setup:
-					bone.y = bone.data.y;
-					return;
-				case first:
-					bone.y += (bone.data.y - bone.y) * alpha;
-				}
-				return;
-			}
-
-			float y = getCurveValue(time);
-			switch (blend) {
-			case setup:
-				bone.y = bone.data.y + y * alpha;
-				break;
-			case first:
-			case replace:
-				bone.y += (bone.data.y + y - bone.y) * alpha;
-				break;
-			case add:
-				bone.y += y * alpha;
-			}
-		}
-	}
-
-	/** Changes a bone's local {@link Bone#getScaleX()} and {@link Bone#getScaleY()}. */
-	static public class ScaleTimeline extends CurveTimeline2 implements BoneTimeline {
-		final int boneIndex;
-
-		public ScaleTimeline (int frameCount, int bezierCount, int boneIndex) {
-			super(frameCount, bezierCount, //
-				Property.scaleX.ordinal() + "|" + boneIndex, //
-				Property.scaleY.ordinal() + "|" + boneIndex);
-			this.boneIndex = boneIndex;
-		}
-
-		public int getBoneIndex () {
-			return boneIndex;
-		}
-
-		public void apply (Skeleton skeleton, float lastTime, float time, @Null Array<Event> events, float alpha, MixBlend blend,
-			MixDirection direction) {
-
-			Bone bone = skeleton.bones.get(boneIndex);
-			if (!bone.active) return;
-
-			float[] frames = this.frames;
-			if (time < frames[0]) { // Time is before first frame.
-				switch (blend) {
-				case setup:
-					bone.scaleX = bone.data.scaleX;
-					bone.scaleY = bone.data.scaleY;
-					return;
-				case first:
-					bone.scaleX += (bone.data.scaleX - bone.scaleX) * alpha;
-					bone.scaleY += (bone.data.scaleY - bone.scaleY) * alpha;
-				}
-				return;
-			}
-
-			float x, y;
-			int i = search(frames, time, ENTRIES), curveType = (int)curves[i / ENTRIES];
-			switch (curveType) {
-			case LINEAR:
-				float before = frames[i];
-				x = frames[i + VALUE1];
-				y = frames[i + VALUE2];
-				float t = (time - before) / (frames[i + ENTRIES] - before);
-				x += (frames[i + ENTRIES + VALUE1] - x) * t;
-				y += (frames[i + ENTRIES + VALUE2] - y) * t;
-				break;
-			case STEPPED:
-				x = frames[i + VALUE1];
-				y = frames[i + VALUE2];
-				break;
-			default:
-				x = getBezierValue(time, i, VALUE1, curveType - BEZIER);
-				y = getBezierValue(time, i, VALUE2, curveType + BEZIER_SIZE - BEZIER);
-			}
-			x *= bone.data.scaleX;
-			y *= bone.data.scaleY;
-
-			if (alpha == 1) {
-				if (blend == add) {
-					bone.scaleX += x - bone.data.scaleX;
-					bone.scaleY += y - bone.data.scaleY;
-				} else {
-					bone.scaleX = x;
-					bone.scaleY = y;
-				}
-			} else {
-				// Mixing out uses sign of setup or current pose, else use sign of key.
-				float bx, by;
-				if (direction == out) {
-					switch (blend) {
-					case setup:
-						bx = bone.data.scaleX;
-						by = bone.data.scaleY;
-						bone.scaleX = bx + (Math.abs(x) * Math.signum(bx) - bx) * alpha;
-						bone.scaleY = by + (Math.abs(y) * Math.signum(by) - by) * alpha;
-						break;
-					case first:
-					case replace:
-						bx = bone.scaleX;
-						by = bone.scaleY;
-						bone.scaleX = bx + (Math.abs(x) * Math.signum(bx) - bx) * alpha;
-						bone.scaleY = by + (Math.abs(y) * Math.signum(by) - by) * alpha;
-						break;
-					case add:
-						bx = bone.scaleX;
-						by = bone.scaleY;
-						bone.scaleX = bx + (Math.abs(x) * Math.signum(bx) - bone.data.scaleX) * alpha;
-						bone.scaleY = by + (Math.abs(y) * Math.signum(by) - bone.data.scaleY) * alpha;
-					}
-				} else {
-					switch (blend) {
-					case setup:
-						bx = Math.abs(bone.data.scaleX) * Math.signum(x);
-						by = Math.abs(bone.data.scaleY) * Math.signum(y);
-						bone.scaleX = bx + (x - bx) * alpha;
-						bone.scaleY = by + (y - by) * alpha;
-						break;
-					case first:
-					case replace:
-						bx = Math.abs(bone.scaleX) * Math.signum(x);
-						by = Math.abs(bone.scaleY) * Math.signum(y);
-						bone.scaleX = bx + (x - bx) * alpha;
-						bone.scaleY = by + (y - by) * alpha;
-						break;
-					case add:
-						bx = Math.signum(x);
-						by = Math.signum(y);
-						bone.scaleX = Math.abs(bone.scaleX) * bx + (x - Math.abs(bone.data.scaleX) * bx) * alpha;
-						bone.scaleY = Math.abs(bone.scaleY) * by + (y - Math.abs(bone.data.scaleY) * by) * alpha;
-					}
-				}
-			}
-		}
-	}
-
-	/** Changes a bone's local {@link Bone#getScaleX()}. */
-	static public class ScaleXTimeline extends CurveTimeline1 implements BoneTimeline {
-		final int boneIndex;
-
-		public ScaleXTimeline (int frameCount, int bezierCount, int boneIndex) {
-			super(frameCount, bezierCount, Property.scaleX.ordinal() + "|" + boneIndex);
-			this.boneIndex = boneIndex;
-		}
-
-		public int getBoneIndex () {
-			return boneIndex;
-		}
-
-		public void apply (Skeleton skeleton, float lastTime, float time, @Null Array<Event> events, float alpha, MixBlend blend,
-			MixDirection direction) {
-
-			Bone bone = skeleton.bones.get(boneIndex);
-			if (!bone.active) return;
-
-			float[] frames = this.frames;
-			if (time < frames[0]) { // Time is before first frame.
-				switch (blend) {
-				case setup:
-					bone.scaleX = bone.data.scaleX;
-					return;
-				case first:
-					bone.scaleX += (bone.data.scaleX - bone.scaleX) * alpha;
-				}
-				return;
-			}
-
-			float x = getCurveValue(time) * bone.data.scaleX;
-			if (alpha == 1) {
-				if (blend == add)
-					bone.scaleX += x - bone.data.scaleX;
-				else
-					bone.scaleX = x;
-			} else {
-				// Mixing out uses sign of setup or current pose, else use sign of key.
-				float bx;
-				if (direction == out) {
-					switch (blend) {
-					case setup:
-						bx = bone.data.scaleX;
-						bone.scaleX = bx + (Math.abs(x) * Math.signum(bx) - bx) * alpha;
-						break;
-					case first:
-					case replace:
-						bx = bone.scaleX;
-						bone.scaleX = bx + (Math.abs(x) * Math.signum(bx) - bx) * alpha;
-						break;
-					case add:
-						bx = bone.scaleX;
-						bone.scaleX = bx + (Math.abs(x) * Math.signum(bx) - bone.data.scaleX) * alpha;
-					}
-				} else {
-					switch (blend) {
-					case setup:
-						bx = Math.abs(bone.data.scaleX) * Math.signum(x);
-						bone.scaleX = bx + (x - bx) * alpha;
-						break;
-					case first:
-					case replace:
-						bx = Math.abs(bone.scaleX) * Math.signum(x);
-						bone.scaleX = bx + (x - bx) * alpha;
-						break;
-					case add:
-						bx = Math.signum(x);
-						bone.scaleX = Math.abs(bone.scaleX) * bx + (x - Math.abs(bone.data.scaleX) * bx) * alpha;
-					}
-				}
-			}
-		}
-	}
-
-	/** Changes a bone's local {@link Bone#getScaleY()}. */
-	static public class ScaleYTimeline extends CurveTimeline1 implements BoneTimeline {
-		final int boneIndex;
-
-		public ScaleYTimeline (int frameCount, int bezierCount, int boneIndex) {
-			super(frameCount, bezierCount, Property.scaleY.ordinal() + "|" + boneIndex);
-			this.boneIndex = boneIndex;
-		}
-
-		public int getBoneIndex () {
-			return boneIndex;
-		}
-
-		public void apply (Skeleton skeleton, float lastTime, float time, @Null Array<Event> events, float alpha, MixBlend blend,
-			MixDirection direction) {
-
-			Bone bone = skeleton.bones.get(boneIndex);
-			if (!bone.active) return;
-
-			float[] frames = this.frames;
-			if (time < frames[0]) { // Time is before first frame.
-				switch (blend) {
-				case setup:
-					bone.scaleY = bone.data.scaleY;
-					return;
-				case first:
-					bone.scaleY += (bone.data.scaleY - bone.scaleY) * alpha;
-				}
-				return;
-			}
-
-			float y = getCurveValue(time) * bone.data.scaleY;
-			if (alpha == 1) {
-				if (blend == add)
-					bone.scaleY += y - bone.data.scaleY;
-				else
-					bone.scaleY = y;
-			} else {
-				// Mixing out uses sign of setup or current pose, else use sign of key.
-				float by;
-				if (direction == out) {
-					switch (blend) {
-					case setup:
-						by = bone.data.scaleY;
-						bone.scaleY = by + (Math.abs(y) * Math.signum(by) - by) * alpha;
-						break;
-					case first:
-					case replace:
-						by = bone.scaleY;
-						bone.scaleY = by + (Math.abs(y) * Math.signum(by) - by) * alpha;
-						break;
-					case add:
-						by = bone.scaleY;
-						bone.scaleY = by + (Math.abs(y) * Math.signum(by) - bone.data.scaleY) * alpha;
-					}
-				} else {
-					switch (blend) {
-					case setup:
-						by = Math.abs(bone.data.scaleY) * Math.signum(y);
-						bone.scaleY = by + (y - by) * alpha;
-						break;
-					case first:
-					case replace:
-						by = Math.abs(bone.scaleY) * Math.signum(y);
-						bone.scaleY = by + (y - by) * alpha;
-						break;
-					case add:
-						by = Math.signum(y);
-						bone.scaleY = Math.abs(bone.scaleY) * by + (y - Math.abs(bone.data.scaleY) * by) * alpha;
-					}
-				}
-			}
-		}
-	}
-
-	/** Changes a bone's local {@link Bone#getShearX()} and {@link Bone#getShearY()}. */
-	static public class ShearTimeline extends CurveTimeline2 implements BoneTimeline {
-		final int boneIndex;
-
-		public ShearTimeline (int frameCount, int bezierCount, int boneIndex) {
-			super(frameCount, bezierCount, //
-				Property.shearX.ordinal() + "|" + boneIndex, //
-				Property.shearY.ordinal() + "|" + boneIndex);
-			this.boneIndex = boneIndex;
-		}
-
-		public int getBoneIndex () {
-			return boneIndex;
-		}
-
-		public void apply (Skeleton skeleton, float lastTime, float time, @Null Array<Event> events, float alpha, MixBlend blend,
-			MixDirection direction) {
-
-			Bone bone = skeleton.bones.get(boneIndex);
-			if (!bone.active) return;
-
-			float[] frames = this.frames;
-			if (time < frames[0]) { // Time is before first frame.
-				switch (blend) {
-				case setup:
-					bone.shearX = bone.data.shearX;
-					bone.shearY = bone.data.shearY;
-					return;
-				case first:
-					bone.shearX += (bone.data.shearX - bone.shearX) * alpha;
-					bone.shearY += (bone.data.shearY - bone.shearY) * alpha;
-				}
-				return;
-			}
-
-			float x, y;
-			int i = search(frames, time, ENTRIES), curveType = (int)curves[i / ENTRIES];
-			switch (curveType) {
-			case LINEAR:
-				float before = frames[i];
-				x = frames[i + VALUE1];
-				y = frames[i + VALUE2];
-				float t = (time - before) / (frames[i + ENTRIES] - before);
-				x += (frames[i + ENTRIES + VALUE1] - x) * t;
-				y += (frames[i + ENTRIES + VALUE2] - y) * t;
-				break;
-			case STEPPED:
-				x = frames[i + VALUE1];
-				y = frames[i + VALUE2];
-				break;
-			default:
-				x = getBezierValue(time, i, VALUE1, curveType - BEZIER);
-				y = getBezierValue(time, i, VALUE2, curveType + BEZIER_SIZE - BEZIER);
-			}
-
-			switch (blend) {
-			case setup:
-				bone.shearX = bone.data.shearX + x * alpha;
-				bone.shearY = bone.data.shearY + y * alpha;
-				break;
-			case first:
-			case replace:
-				bone.shearX += (bone.data.shearX + x - bone.shearX) * alpha;
-				bone.shearY += (bone.data.shearY + y - bone.shearY) * alpha;
-				break;
-			case add:
-				bone.shearX += x * alpha;
-				bone.shearY += y * alpha;
-			}
-		}
-	}
-
-	/** Changes a bone's local {@link Bone#getShearX()}. */
-	static public class ShearXTimeline extends CurveTimeline1 implements BoneTimeline {
-		final int boneIndex;
-
-		public ShearXTimeline (int frameCount, int bezierCount, int boneIndex) {
-			super(frameCount, bezierCount, Property.shearX.ordinal() + "|" + boneIndex);
-			this.boneIndex = boneIndex;
-		}
-
-		public int getBoneIndex () {
-			return boneIndex;
-		}
-
-		public void apply (Skeleton skeleton, float lastTime, float time, @Null Array<Event> events, float alpha, MixBlend blend,
-			MixDirection direction) {
-
-			Bone bone = skeleton.bones.get(boneIndex);
-			if (!bone.active) return;
-
-			float[] frames = this.frames;
-			if (time < frames[0]) { // Time is before first frame.
-				switch (blend) {
-				case setup:
-					bone.shearX = bone.data.shearX;
-					return;
-				case first:
-					bone.shearX += (bone.data.shearX - bone.shearX) * alpha;
-				}
-				return;
-			}
-
-			float x = getCurveValue(time);
-			switch (blend) {
-			case setup:
-				bone.shearX = bone.data.shearX + x * alpha;
-				break;
-			case first:
-			case replace:
-				bone.shearX += (bone.data.shearX + x - bone.shearX) * alpha;
-				break;
-			case add:
-				bone.shearX += x * alpha;
-			}
-		}
-	}
-
-	/** Changes a bone's local {@link Bone#getShearY()}. */
-	static public class ShearYTimeline extends CurveTimeline1 implements BoneTimeline {
-		final int boneIndex;
-
-		public ShearYTimeline (int frameCount, int bezierCount, int boneIndex) {
-			super(frameCount, bezierCount, Property.shearY.ordinal() + "|" + boneIndex);
-			this.boneIndex = boneIndex;
-		}
-
-		public int getBoneIndex () {
-			return boneIndex;
-		}
-
-		public void apply (Skeleton skeleton, float lastTime, float time, @Null Array<Event> events, float alpha, MixBlend blend,
-			MixDirection direction) {
-
-			Bone bone = skeleton.bones.get(boneIndex);
-			if (!bone.active) return;
-
-			float[] frames = this.frames;
-			if (time < frames[0]) { // Time is before first frame.
-				switch (blend) {
-				case setup:
-					bone.shearY = bone.data.shearY;
-					return;
-				case first:
-					bone.shearY += (bone.data.shearY - bone.shearY) * alpha;
-				}
-				return;
-			}
-
-			float y = getCurveValue(time);
-			switch (blend) {
-			case setup:
-				bone.shearY = bone.data.shearY + y * alpha;
-				break;
-			case first:
-			case replace:
-				bone.shearY += (bone.data.shearY + y - bone.shearY) * alpha;
-				break;
-			case add:
-				bone.shearY += y * alpha;
-			}
-		}
-	}
-
-	/** Changes a slot's {@link Slot#getColor()}. */
-	static public class RGBATimeline extends CurveTimeline implements SlotTimeline {
-		static public final int ENTRIES = 5;
-		static private final int R = 1, G = 2, B = 3, A = 4;
-
-		final int slotIndex;
-
-		public RGBATimeline (int frameCount, int bezierCount, int slotIndex) {
-			super(frameCount, bezierCount, //
-				Property.rgb.ordinal() + "|" + slotIndex, //
-				Property.alpha.ordinal() + "|" + slotIndex);
-			this.slotIndex = slotIndex;
-		}
-
-		public int getFrameEntries () {
-			return ENTRIES;
-		}
-
-		public int getSlotIndex () {
-			return slotIndex;
-		}
-
-		/** Sets the time and color for the specified frame.
-		 * @param frame Between 0 and <code>frameCount</code>, inclusive.
-		 * @param time The frame time in seconds. */
-		public void setFrame (int frame, float time, float r, float g, float b, float a) {
-			frame *= ENTRIES;
-			frames[frame] = time;
-			frames[frame + R] = r;
-			frames[frame + G] = g;
-			frames[frame + B] = b;
-			frames[frame + A] = a;
-		}
-
-		public void apply (Skeleton skeleton, float lastTime, float time, @Null Array<Event> events, float alpha, MixBlend blend,
-			MixDirection direction) {
-
-			Slot slot = skeleton.slots.get(slotIndex);
-			if (!slot.bone.active) return;
-
-			float[] frames = this.frames;
-			Color color = slot.color;
-			if (time < frames[0]) { // Time is before first frame.
-				Color setup = slot.data.color;
-				switch (blend) {
-				case setup:
-					color.set(setup);
-					return;
-				case first:
-					color.add((setup.r - color.r) * alpha, (setup.g - color.g) * alpha, (setup.b - color.b) * alpha,
-						(setup.a - color.a) * alpha);
-				}
-				return;
-			}
-
-			float r, g, b, a;
-			int i = search(frames, time, ENTRIES), curveType = (int)curves[i / ENTRIES];
-			switch (curveType) {
-			case LINEAR:
-				float before = frames[i];
-				r = frames[i + R];
-				g = frames[i + G];
-				b = frames[i + B];
-				a = frames[i + A];
-				float t = (time - before) / (frames[i + ENTRIES] - before);
-				r += (frames[i + ENTRIES + R] - r) * t;
-				g += (frames[i + ENTRIES + G] - g) * t;
-				b += (frames[i + ENTRIES + B] - b) * t;
-				a += (frames[i + ENTRIES + A] - a) * t;
-				break;
-			case STEPPED:
-				r = frames[i + R];
-				g = frames[i + G];
-				b = frames[i + B];
-				a = frames[i + A];
-				break;
-			default:
-				r = getBezierValue(time, i, R, curveType - BEZIER);
-				g = getBezierValue(time, i, G, curveType + BEZIER_SIZE - BEZIER);
-				b = getBezierValue(time, i, B, curveType + BEZIER_SIZE * 2 - BEZIER);
-				a = getBezierValue(time, i, A, curveType + BEZIER_SIZE * 3 - BEZIER);
-			}
-
-			if (alpha == 1)
-				color.set(r, g, b, a);
-			else {
-				if (blend == setup) color.set(slot.data.color);
-				color.add((r - color.r) * alpha, (g - color.g) * alpha, (b - color.b) * alpha, (a - color.a) * alpha);
-			}
-		}
-	}
-
-	/** Changes the RGB for a slot's {@link Slot#getColor()}. */
-	static public class RGBTimeline extends CurveTimeline implements SlotTimeline {
-		static public final int ENTRIES = 4;
-		static private final int R = 1, G = 2, B = 3;
-
-		final int slotIndex;
-
-		public RGBTimeline (int frameCount, int bezierCount, int slotIndex) {
-			super(frameCount, bezierCount, Property.rgb.ordinal() + "|" + slotIndex);
-			this.slotIndex = slotIndex;
-		}
-
-		public int getFrameEntries () {
-			return ENTRIES;
-		}
-
-		public int getSlotIndex () {
-			return slotIndex;
-		}
-
-		/** Sets the time and color for the specified frame.
-		 * @param frame Between 0 and <code>frameCount</code>, inclusive.
-		 * @param time The frame time in seconds. */
-		public void setFrame (int frame, float time, float r, float g, float b) {
-			frame <<= 2;
-			frames[frame] = time;
-			frames[frame + R] = r;
-			frames[frame + G] = g;
-			frames[frame + B] = b;
-		}
-
-		public void apply (Skeleton skeleton, float lastTime, float time, @Null Array<Event> events, float alpha, MixBlend blend,
-			MixDirection direction) {
-
-			Slot slot = skeleton.slots.get(slotIndex);
-			if (!slot.bone.active) return;
-
-			float[] frames = this.frames;
-			Color color = slot.color;
-			if (time < frames[0]) { // Time is before first frame.
-				Color setup = slot.data.color;
-				switch (blend) {
-				case setup:
-					color.r = setup.r;
-					color.g = setup.g;
-					color.b = setup.b;
-					return;
-				case first:
-					color.r += (setup.r - color.r) * alpha;
-					color.g += (setup.g - color.g) * alpha;
-					color.b += (setup.b - color.b) * alpha;
-				}
-				return;
-			}
-
-			float r, g, b;
-			int i = search(frames, time, ENTRIES), curveType = (int)curves[i >> 2];
-			switch (curveType) {
-			case LINEAR:
-				float before = frames[i];
-				r = frames[i + R];
-				g = frames[i + G];
-				b = frames[i + B];
-				float t = (time - before) / (frames[i + ENTRIES] - before);
-				r += (frames[i + ENTRIES + R] - r) * t;
-				g += (frames[i + ENTRIES + G] - g) * t;
-				b += (frames[i + ENTRIES + B] - b) * t;
-				break;
-			case STEPPED:
-				r = frames[i + R];
-				g = frames[i + G];
-				b = frames[i + B];
-				break;
-			default:
-				r = getBezierValue(time, i, R, curveType - BEZIER);
-				g = getBezierValue(time, i, G, curveType + BEZIER_SIZE - BEZIER);
-				b = getBezierValue(time, i, B, curveType + BEZIER_SIZE * 2 - BEZIER);
-			}
-
-			if (alpha == 1) {
-				color.r = r;
-				color.g = g;
-				color.b = b;
-			} else {
-				if (blend == setup) {
-					Color setup = slot.data.color;
-					color.r = setup.r;
-					color.g = setup.g;
-					color.b = setup.b;
-				}
-				color.r += (r - color.r) * alpha;
-				color.g += (g - color.g) * alpha;
-				color.b += (b - color.b) * alpha;
-			}
-		}
-	}
-
-	/** Changes the alpha for a slot's {@link Slot#getColor()}. */
-	static public class AlphaTimeline extends CurveTimeline1 implements SlotTimeline {
-		final int slotIndex;
-
-		public AlphaTimeline (int frameCount, int bezierCount, int slotIndex) {
-			super(frameCount, bezierCount, Property.alpha.ordinal() + "|" + slotIndex);
-			this.slotIndex = slotIndex;
-		}
-
-		public int getSlotIndex () {
-			return slotIndex;
-		}
-
-		public void apply (Skeleton skeleton, float lastTime, float time, @Null Array<Event> events, float alpha, MixBlend blend,
-			MixDirection direction) {
-
-			Slot slot = skeleton.slots.get(slotIndex);
-			if (!slot.bone.active) return;
-
-			float[] frames = this.frames;
-			Color color = slot.color;
-			if (time < frames[0]) { // Time is before first frame.
-				Color setup = slot.data.color;
-				switch (blend) {
-				case setup:
-					color.a = setup.a;
-					return;
-				case first:
-					color.a += (setup.a - color.a) * alpha;
-				}
-				return;
-			}
-
-			float a = getCurveValue(time);
-			if (alpha == 1)
-				color.a = a;
-			else {
-				if (blend == setup) color.a = slot.data.color.a;
-				color.a += (a - color.a) * alpha;
-			}
-		}
-	}
-
-	/** Changes a slot's {@link Slot#getColor()} and {@link Slot#getDarkColor()} for two color tinting. */
-	static public class RGBA2Timeline extends CurveTimeline implements SlotTimeline {
-		static public final int ENTRIES = 8;
-		static private final int R = 1, G = 2, B = 3, A = 4, R2 = 5, G2 = 6, B2 = 7;
-
-		final int slotIndex;
-
-		public RGBA2Timeline (int frameCount, int bezierCount, int slotIndex) {
-			super(frameCount, bezierCount, //
-				Property.rgb.ordinal() + "|" + slotIndex, //
-				Property.alpha.ordinal() + "|" + slotIndex, //
-				Property.rgb2.ordinal() + "|" + slotIndex);
-			this.slotIndex = slotIndex;
-		}
-
-		public int getFrameEntries () {
-			return ENTRIES;
-		}
-
-		/** The index of the slot in {@link Skeleton#getSlots()} that will be changed when this timeline is applied. The
-		 * {@link Slot#getDarkColor()} must not be null. */
-		public int getSlotIndex () {
-			return slotIndex;
-		}
-
-		/** Sets the time, light color, and dark color for the specified frame.
-		 * @param frame Between 0 and <code>frameCount</code>, inclusive.
-		 * @param time The frame time in seconds. */
-		public void setFrame (int frame, float time, float r, float g, float b, float a, float r2, float g2, float b2) {
-			frame <<= 3;
-			frames[frame] = time;
-			frames[frame + R] = r;
-			frames[frame + G] = g;
-			frames[frame + B] = b;
-			frames[frame + A] = a;
-			frames[frame + R2] = r2;
-			frames[frame + G2] = g2;
-			frames[frame + B2] = b2;
-		}
-
-		public void apply (Skeleton skeleton, float lastTime, float time, @Null Array<Event> events, float alpha, MixBlend blend,
-			MixDirection direction) {
-
-			Slot slot = skeleton.slots.get(slotIndex);
-			if (!slot.bone.active) return;
-
-			float[] frames = this.frames;
-			Color light = slot.color, dark = slot.darkColor;
-			if (time < frames[0]) { // Time is before first frame.
-				Color setupLight = slot.data.color, setupDark = slot.data.darkColor;
-				switch (blend) {
-				case setup:
-					light.set(setupLight);
-					dark.r = setupDark.r;
-					dark.g = setupDark.g;
-					dark.b = setupDark.b;
-					return;
-				case first:
-					light.add((setupLight.r - light.r) * alpha, (setupLight.g - light.g) * alpha, (setupLight.b - light.b) * alpha,
-						(setupLight.a - light.a) * alpha);
-					dark.r += (setupDark.r - dark.r) * alpha;
-					dark.g += (setupDark.g - dark.g) * alpha;
-					dark.b += (setupDark.b - dark.b) * alpha;
-				}
-				return;
-			}
-
-			float r, g, b, a, r2, g2, b2;
-			int i = search(frames, time, ENTRIES), curveType = (int)curves[i >> 3];
-			switch (curveType) {
-			case LINEAR:
-				float before = frames[i];
-				r = frames[i + R];
-				g = frames[i + G];
-				b = frames[i + B];
-				a = frames[i + A];
-				r2 = frames[i + R2];
-				g2 = frames[i + G2];
-				b2 = frames[i + B2];
-				float t = (time - before) / (frames[i + ENTRIES] - before);
-				r += (frames[i + ENTRIES + R] - r) * t;
-				g += (frames[i + ENTRIES + G] - g) * t;
-				b += (frames[i + ENTRIES + B] - b) * t;
-				a += (frames[i + ENTRIES + A] - a) * t;
-				r2 += (frames[i + ENTRIES + R2] - r2) * t;
-				g2 += (frames[i + ENTRIES + G2] - g2) * t;
-				b2 += (frames[i + ENTRIES + B2] - b2) * t;
-				break;
-			case STEPPED:
-				r = frames[i + R];
-				g = frames[i + G];
-				b = frames[i + B];
-				a = frames[i + A];
-				r2 = frames[i + R2];
-				g2 = frames[i + G2];
-				b2 = frames[i + B2];
-				break;
-			default:
-				r = getBezierValue(time, i, R, curveType - BEZIER);
-				g = getBezierValue(time, i, G, curveType + BEZIER_SIZE - BEZIER);
-				b = getBezierValue(time, i, B, curveType + BEZIER_SIZE * 2 - BEZIER);
-				a = getBezierValue(time, i, A, curveType + BEZIER_SIZE * 3 - BEZIER);
-				r2 = getBezierValue(time, i, R2, curveType + BEZIER_SIZE * 4 - BEZIER);
-				g2 = getBezierValue(time, i, G2, curveType + BEZIER_SIZE * 5 - BEZIER);
-				b2 = getBezierValue(time, i, B2, curveType + BEZIER_SIZE * 6 - BEZIER);
-			}
-
-			if (alpha == 1) {
-				light.set(r, g, b, a);
-				dark.r = r2;
-				dark.g = g2;
-				dark.b = b2;
-			} else {
-				if (blend == setup) {
-					light.set(slot.data.color);
-					Color setupDark = slot.data.darkColor;
-					dark.r = setupDark.r;
-					dark.g = setupDark.g;
-					dark.b = setupDark.b;
-				}
-				light.add((r - light.r) * alpha, (g - light.g) * alpha, (b - light.b) * alpha, (a - light.a) * alpha);
-				dark.r += (r2 - dark.r) * alpha;
-				dark.g += (g2 - dark.g) * alpha;
-				dark.b += (b2 - dark.b) * alpha;
-			}
-		}
-	}
-
-	/** Changes the RGB for a slot's {@link Slot#getColor()} and {@link Slot#getDarkColor()} for two color tinting. */
-	static public class RGB2Timeline extends CurveTimeline implements SlotTimeline {
-		static public final int ENTRIES = 7;
-		static private final int R = 1, G = 2, B = 3, R2 = 4, G2 = 5, B2 = 6;
-
-		final int slotIndex;
-
-		public RGB2Timeline (int frameCount, int bezierCount, int slotIndex) {
-			super(frameCount, bezierCount, //
-				Property.rgb.ordinal() + "|" + slotIndex, //
-				Property.rgb2.ordinal() + "|" + slotIndex);
-			this.slotIndex = slotIndex;
-		}
-
-		public int getFrameEntries () {
-			return ENTRIES;
-		}
-
-		/** The index of the slot in {@link Skeleton#getSlots()} that will be changed when this timeline is applied. The
-		 * {@link Slot#getDarkColor()} must not be null. */
-		public int getSlotIndex () {
-			return slotIndex;
-		}
-
-		/** Sets the time, light color, and dark color for the specified frame.
-		 * @param frame Between 0 and <code>frameCount</code>, inclusive.
-		 * @param time The frame time in seconds. */
-		public void setFrame (int frame, float time, float r, float g, float b, float r2, float g2, float b2) {
-			frame *= ENTRIES;
-			frames[frame] = time;
-			frames[frame + R] = r;
-			frames[frame + G] = g;
-			frames[frame + B] = b;
-			frames[frame + R2] = r2;
-			frames[frame + G2] = g2;
-			frames[frame + B2] = b2;
-		}
-
-		public void apply (Skeleton skeleton, float lastTime, float time, @Null Array<Event> events, float alpha, MixBlend blend,
-			MixDirection direction) {
-
-			Slot slot = skeleton.slots.get(slotIndex);
-			if (!slot.bone.active) return;
-
-			float[] frames = this.frames;
-			Color light = slot.color, dark = slot.darkColor;
-			if (time < frames[0]) { // Time is before first frame.
-				Color setupLight = slot.data.color, setupDark = slot.data.darkColor;
-				switch (blend) {
-				case setup:
-					light.r = setupLight.r;
-					light.g = setupLight.g;
-					light.b = setupLight.b;
-					dark.r = setupDark.r;
-					dark.g = setupDark.g;
-					dark.b = setupDark.b;
-					return;
-				case first:
-					light.r += (setupLight.r - light.r) * alpha;
-					light.g += (setupLight.g - light.g) * alpha;
-					light.b += (setupLight.b - light.b) * alpha;
-					dark.r += (setupDark.r - dark.r) * alpha;
-					dark.g += (setupDark.g - dark.g) * alpha;
-					dark.b += (setupDark.b - dark.b) * alpha;
-				}
-				return;
-			}
-
-			float r, g, b, r2, g2, b2;
-			int i = search(frames, time, ENTRIES), curveType = (int)curves[i / ENTRIES];
-			switch (curveType) {
-			case LINEAR:
-				float before = frames[i];
-				r = frames[i + R];
-				g = frames[i + G];
-				b = frames[i + B];
-				r2 = frames[i + R2];
-				g2 = frames[i + G2];
-				b2 = frames[i + B2];
-				float t = (time - before) / (frames[i + ENTRIES] - before);
-				r += (frames[i + ENTRIES + R] - r) * t;
-				g += (frames[i + ENTRIES + G] - g) * t;
-				b += (frames[i + ENTRIES + B] - b) * t;
-				r2 += (frames[i + ENTRIES + R2] - r2) * t;
-				g2 += (frames[i + ENTRIES + G2] - g2) * t;
-				b2 += (frames[i + ENTRIES + B2] - b2) * t;
-				break;
-			case STEPPED:
-				r = frames[i + R];
-				g = frames[i + G];
-				b = frames[i + B];
-				r2 = frames[i + R2];
-				g2 = frames[i + G2];
-				b2 = frames[i + B2];
-				break;
-			default:
-				r = getBezierValue(time, i, R, curveType - BEZIER);
-				g = getBezierValue(time, i, G, curveType + BEZIER_SIZE - BEZIER);
-				b = getBezierValue(time, i, B, curveType + BEZIER_SIZE * 2 - BEZIER);
-				r2 = getBezierValue(time, i, R2, curveType + BEZIER_SIZE * 3 - BEZIER);
-				g2 = getBezierValue(time, i, G2, curveType + BEZIER_SIZE * 4 - BEZIER);
-				b2 = getBezierValue(time, i, B2, curveType + BEZIER_SIZE * 5 - BEZIER);
-			}
-
-			if (alpha == 1) {
-				light.r = r;
-				light.g = g;
-				light.b = b;
-				dark.r = r2;
-				dark.g = g2;
-				dark.b = b2;
-			} else {
-				if (blend == setup) {
-					Color setupLight = slot.data.color, setupDark = slot.data.darkColor;
-					light.r = setupLight.r;
-					light.g = setupLight.g;
-					light.b = setupLight.b;
-					dark.r = setupDark.r;
-					dark.g = setupDark.g;
-					dark.b = setupDark.b;
-				}
-				light.r += (r - light.r) * alpha;
-				light.g += (g - light.g) * alpha;
-				light.b += (b - light.b) * alpha;
-				dark.r += (r2 - dark.r) * alpha;
-				dark.g += (g2 - dark.g) * alpha;
-				dark.b += (b2 - dark.b) * alpha;
-			}
-		}
-	}
-
-	/** Changes a slot's {@link Slot#getAttachment()}. */
-	static public class AttachmentTimeline extends Timeline implements SlotTimeline {
-		final int slotIndex;
-		final String[] attachmentNames;
-
-		public AttachmentTimeline (int frameCount, int slotIndex) {
-			super(frameCount, Property.attachment.ordinal() + "|" + slotIndex);
-			this.slotIndex = slotIndex;
-			attachmentNames = new String[frameCount];
-		}
-
-		public int getFrameCount () {
-			return frames.length;
-		}
-
-		public int getSlotIndex () {
-			return slotIndex;
-		}
-
-		/** The attachment name for each frame. May contain null values to clear the attachment. */
-		public String[] getAttachmentNames () {
-			return attachmentNames;
-		}
-
-		/** Sets the time and attachment name for the specified frame.
-		 * @param frame Between 0 and <code>frameCount</code>, inclusive.
-		 * @param time The frame time in seconds. */
-		public void setFrame (int frame, float time, String attachmentName) {
-			frames[frame] = time;
-			attachmentNames[frame] = attachmentName;
-		}
-
-		public void apply (Skeleton skeleton, float lastTime, float time, @Null Array<Event> events, float alpha, MixBlend blend,
-			MixDirection direction) {
-
-			Slot slot = skeleton.slots.get(slotIndex);
-			if (!slot.bone.active) return;
-
-			if (direction == out) {
-				if (blend == setup) setAttachment(skeleton, slot, slot.data.attachmentName);
-				return;
-			}
-
-			if (time < this.frames[0]) { // Time is before first frame.
-				if (blend == setup || blend == first) setAttachment(skeleton, slot, slot.data.attachmentName);
-				return;
-			}
-
-			setAttachment(skeleton, slot, attachmentNames[search(this.frames, time)]);
-		}
-
-		private void setAttachment (Skeleton skeleton, Slot slot, String attachmentName) {
-			slot.setAttachment(attachmentName == null ? null : skeleton.getAttachment(slotIndex, attachmentName));
-		}
-	}
-
-	/** Changes a slot's {@link Slot#getDeform()} to deform a {@link VertexAttachment}. */
-	static public class DeformTimeline extends CurveTimeline implements SlotTimeline {
-		final int slotIndex;
-		final VertexAttachment attachment;
-		private final float[][] vertices;
-
-		public DeformTimeline (int frameCount, int bezierCount, int slotIndex, VertexAttachment attachment) {
-			super(frameCount, bezierCount, Property.deform.ordinal() + "|" + slotIndex + "|" + attachment.getId());
-			this.slotIndex = slotIndex;
-			this.attachment = attachment;
-			vertices = new float[frameCount][];
-		}
-
-		public int getFrameCount () {
-			return frames.length;
-		}
-
-		public int getSlotIndex () {
-			return slotIndex;
-		}
-
-		/** The attachment that will be deformed.
-		 * <p>
-		 * See {@link VertexAttachment#getDeformAttachment()}. */
-		public VertexAttachment getAttachment () {
-			return attachment;
-		}
-
-		/** The vertices for each frame. */
-		public float[][] getVertices () {
-			return vertices;
-		}
-
-		/** Sets the time and vertices for the specified frame.
-		 * @param frame Between 0 and <code>frameCount</code>, inclusive.
-		 * @param time The frame time in seconds.
-		 * @param vertices Vertex positions for an unweighted VertexAttachment, or deform offsets if it has weights. */
-		public void setFrame (int frame, float time, float[] vertices) {
-			frames[frame] = time;
-			this.vertices[frame] = vertices;
-		}
-
-		/** @param value1 Ignored (0 is used for a deform timeline).
-		 * @param value2 Ignored (1 is used for a deform timeline). */
-		public void setBezier (int bezier, int frame, int value, float time1, float value1, float cx1, float cy1, float cx2,
-			float cy2, float time2, float value2) {
-			float[] curves = this.curves;
-			int i = getFrameCount() + bezier * BEZIER_SIZE;
-			if (value == 0) curves[frame] = BEZIER + i;
-			float tmpx = (time1 - cx1 * 2 + cx2) * 0.03f, tmpy = cy2 * 0.03f - cy1 * 0.06f;
-			float dddx = ((cx1 - cx2) * 3 - time1 + time2) * 0.006f, dddy = (cy1 - cy2 + 0.33333333f) * 0.018f;
-			float ddx = tmpx * 2 + dddx, ddy = tmpy * 2 + dddy;
-			float dx = (cx1 - time1) * 0.3f + tmpx + dddx * 0.16666667f, dy = cy1 * 0.3f + tmpy + dddy * 0.16666667f;
-			float x = time1 + dx, y = dy;
-			for (int n = i + BEZIER_SIZE; i < n; i += 2) {
-				curves[i] = x;
-				curves[i + 1] = y;
-				dx += ddx;
-				dy += ddy;
-				ddx += dddx;
-				ddy += dddy;
-				x += dx;
-				y += dy;
-			}
-		}
-
-		/** Returns the interpolated percentage for the specified time.
-		 * @param frame The frame before <code>time</code>. */
-		private float getCurvePercent (float time, int frame) {
-			float[] curves = this.curves;
-			int i = (int)curves[frame];
-			switch (i) {
-			case LINEAR:
-				float x = frames[frame];
-				return (time - x) / (frames[frame + getFrameEntries()] - x);
-			case STEPPED:
-				return 0;
-			}
-			i -= BEZIER;
-			if (curves[i] > time) {
-				float x = frames[frame];
-				return curves[i + 1] * (time - x) / (curves[i] - x);
-			}
-			int n = i + BEZIER_SIZE;
-			for (i += 2; i < n; i += 2) {
-				if (curves[i] >= time) {
-					float x = curves[i - 2], y = curves[i - 1];
-					return y + (time - x) / (curves[i] - x) * (curves[i + 1] - y);
-				}
-			}
-			float x = curves[n - 2], y = curves[n - 1];
-			return y + (1 - y) * (time - x) / (frames[frame + getFrameEntries()] - x);
-		}
-
-		public void apply (Skeleton skeleton, float lastTime, float time, @Null Array<Event> events, float alpha, MixBlend blend,
-			MixDirection direction) {
-
-			Slot slot = skeleton.slots.get(slotIndex);
-			if (!slot.bone.active) return;
-			Attachment slotAttachment = slot.attachment;
-			if (!(slotAttachment instanceof VertexAttachment)
-				|| ((VertexAttachment)slotAttachment).getDeformAttachment() != attachment) return;
-
-			FloatArray deformArray = slot.getDeform();
-			if (deformArray.size == 0) blend = setup;
-
-			float[][] vertices = this.vertices;
-			int vertexCount = vertices[0].length;
-
-			float[] frames = this.frames;
-			if (time < frames[0]) { // Time is before first frame.
-				VertexAttachment vertexAttachment = (VertexAttachment)slotAttachment;
-				switch (blend) {
-				case setup:
-					deformArray.clear();
-					return;
-				case first:
-					if (alpha == 1) {
-						deformArray.clear();
-						return;
-					}
-					float[] deform = deformArray.setSize(vertexCount);
-					if (vertexAttachment.getBones() == null) {
-						// Unweighted vertex positions.
-						float[] setupVertices = vertexAttachment.getVertices();
-						for (int i = 0; i < vertexCount; i++)
-							deform[i] += (setupVertices[i] - deform[i]) * alpha;
-					} else {
-						// Weighted deform offsets.
-						alpha = 1 - alpha;
-						for (int i = 0; i < vertexCount; i++)
-							deform[i] *= alpha;
-					}
-				}
-				return;
-			}
-
-			float[] deform = deformArray.setSize(vertexCount);
-
-			if (time >= frames[frames.length - 1]) { // Time is after last frame.
-				float[] lastVertices = vertices[frames.length - 1];
-				if (alpha == 1) {
-					if (blend == add) {
-						VertexAttachment vertexAttachment = (VertexAttachment)slotAttachment;
-						if (vertexAttachment.getBones() == null) {
-							// Unweighted vertex positions, no alpha.
-							float[] setupVertices = vertexAttachment.getVertices();
-							for (int i = 0; i < vertexCount; i++)
-								deform[i] += lastVertices[i] - setupVertices[i];
-						} else {
-							// Weighted deform offsets, no alpha.
-							for (int i = 0; i < vertexCount; i++)
-								deform[i] += lastVertices[i];
-						}
-					} else {
-						// Vertex positions or deform offsets, no alpha.
-						arraycopy(lastVertices, 0, deform, 0, vertexCount);
-					}
-				} else {
-					switch (blend) {
-					case setup: {
-						VertexAttachment vertexAttachment = (VertexAttachment)slotAttachment;
-						if (vertexAttachment.getBones() == null) {
-							// Unweighted vertex positions, with alpha.
-							float[] setupVertices = vertexAttachment.getVertices();
-							for (int i = 0; i < vertexCount; i++) {
-								float setup = setupVertices[i];
-								deform[i] = setup + (lastVertices[i] - setup) * alpha;
-							}
-						} else {
-							// Weighted deform offsets, with alpha.
-							for (int i = 0; i < vertexCount; i++)
-								deform[i] = lastVertices[i] * alpha;
-						}
-						break;
-					}
-					case first:
-					case replace:
-						// Vertex positions or deform offsets, with alpha.
-						for (int i = 0; i < vertexCount; i++)
-							deform[i] += (lastVertices[i] - deform[i]) * alpha;
-						break;
-					case add:
-						VertexAttachment vertexAttachment = (VertexAttachment)slotAttachment;
-						if (vertexAttachment.getBones() == null) {
-							// Unweighted vertex positions, no alpha.
-							float[] setupVertices = vertexAttachment.getVertices();
-							for (int i = 0; i < vertexCount; i++)
-								deform[i] += (lastVertices[i] - setupVertices[i]) * alpha;
-						} else {
-							// Weighted deform offsets, alpha.
-							for (int i = 0; i < vertexCount; i++)
-								deform[i] += lastVertices[i] * alpha;
-						}
-					}
-				}
-				return;
-			}
-
-			int frame = search(frames, time);
-			float percent = getCurvePercent(time, frame);
-			float[] prevVertices = vertices[frame];
-			float[] nextVertices = vertices[frame + 1];
-
-			if (alpha == 1) {
-				if (blend == add) {
-					VertexAttachment vertexAttachment = (VertexAttachment)slotAttachment;
-					if (vertexAttachment.getBones() == null) {
-						// Unweighted vertex positions, no alpha.
-						float[] setupVertices = vertexAttachment.getVertices();
-						for (int i = 0; i < vertexCount; i++) {
-							float prev = prevVertices[i];
-							deform[i] += prev + (nextVertices[i] - prev) * percent - setupVertices[i];
-						}
-					} else {
-						// Weighted deform offsets, no alpha.
-						for (int i = 0; i < vertexCount; i++) {
-							float prev = prevVertices[i];
-							deform[i] += prev + (nextVertices[i] - prev) * percent;
-						}
-					}
-				} else {
-					// Vertex positions or deform offsets, no alpha.
-					for (int i = 0; i < vertexCount; i++) {
-						float prev = prevVertices[i];
-						deform[i] = prev + (nextVertices[i] - prev) * percent;
-					}
-				}
-			} else {
-				switch (blend) {
-				case setup: {
-					VertexAttachment vertexAttachment = (VertexAttachment)slotAttachment;
-					if (vertexAttachment.getBones() == null) {
-						// Unweighted vertex positions, with alpha.
-						float[] setupVertices = vertexAttachment.getVertices();
-						for (int i = 0; i < vertexCount; i++) {
-							float prev = prevVertices[i], setup = setupVertices[i];
-							deform[i] = setup + (prev + (nextVertices[i] - prev) * percent - setup) * alpha;
-						}
-					} else {
-						// Weighted deform offsets, with alpha.
-						for (int i = 0; i < vertexCount; i++) {
-							float prev = prevVertices[i];
-							deform[i] = (prev + (nextVertices[i] - prev) * percent) * alpha;
-						}
-					}
-					break;
-				}
-				case first:
-				case replace:
-					// Vertex positions or deform offsets, with alpha.
-					for (int i = 0; i < vertexCount; i++) {
-						float prev = prevVertices[i];
-						deform[i] += (prev + (nextVertices[i] - prev) * percent - deform[i]) * alpha;
-					}
-					break;
-				case add:
-					VertexAttachment vertexAttachment = (VertexAttachment)slotAttachment;
-					if (vertexAttachment.getBones() == null) {
-						// Unweighted vertex positions, with alpha.
-						float[] setupVertices = vertexAttachment.getVertices();
-						for (int i = 0; i < vertexCount; i++) {
-							float prev = prevVertices[i];
-							deform[i] += (prev + (nextVertices[i] - prev) * percent - setupVertices[i]) * alpha;
-						}
-					} else {
-						// Weighted deform offsets, with alpha.
-						for (int i = 0; i < vertexCount; i++) {
-							float prev = prevVertices[i];
-							deform[i] += (prev + (nextVertices[i] - prev) * percent) * alpha;
-						}
-					}
-				}
-			}
-		}
-	}
-
-	/** Fires an {@link Event} when specific animation times are reached. */
-	static public class EventTimeline extends Timeline {
-		static private final String[] propertyIds = {Integer.toString(Property.event.ordinal())};
-
-		private final Event[] events;
-
-		public EventTimeline (int frameCount) {
-			super(frameCount, propertyIds);
-			events = new Event[frameCount];
-		}
-
-		public int getFrameCount () {
-			return frames.length;
-		}
-
-		/** The event for each frame. */
-		public Event[] getEvents () {
-			return events;
-		}
-
-		/** Sets the time and event for the specified frame.
-		 * @param frame Between 0 and <code>frameCount</code>, inclusive. */
-		public void setFrame (int frame, Event event) {
-			frames[frame] = event.time;
-			events[frame] = event;
-		}
-
-		/** Fires events for frames > <code>lastTime</code> and <= <code>time</code>. */
-		public void apply (Skeleton skeleton, float lastTime, float time, @Null Array<Event> firedEvents, float alpha,
-			MixBlend blend, MixDirection direction) {
-
-			if (firedEvents == null) return;
-
-			float[] frames = this.frames;
-			int frameCount = frames.length;
-
-			if (lastTime > time) { // Fire events after last time for looped animations.
-				apply(skeleton, lastTime, Integer.MAX_VALUE, firedEvents, alpha, blend, direction);
-				lastTime = -1f;
-			} else if (lastTime >= frames[frameCount - 1]) // Last time is after last frame.
-				return;
-			if (time < frames[0]) return; // Time is before first frame.
-
-			int i;
-			if (lastTime < frames[0])
-				i = 0;
-			else {
-				i = search(frames, lastTime) + 1;
-				float frameTime = frames[i];
-				while (i > 0) { // Fire multiple events with the same frame.
-					if (frames[i - 1] != frameTime) break;
-					i--;
-				}
-			}
-			for (; i < frameCount && time >= frames[i]; i++)
-				firedEvents.add(events[i]);
-		}
-	}
-
-	/** Changes a skeleton's {@link Skeleton#getDrawOrder()}. */
-	static public class DrawOrderTimeline extends Timeline {
-		static private final String[] propertyIds = {Integer.toString(Property.drawOrder.ordinal())};
-
-		private final int[][] drawOrders;
-
-		public DrawOrderTimeline (int frameCount) {
-			super(frameCount, propertyIds);
-			drawOrders = new int[frameCount][];
-		}
-
-		public int getFrameCount () {
-			return frames.length;
-		}
-
-		/** The draw order for each frame. See {@link #setFrame(int, float, int[])}. */
-		public int[][] getDrawOrders () {
-			return drawOrders;
-		}
-
-		/** Sets the time and draw order for the specified frame.
-		 * @param frame Between 0 and <code>frameCount</code>, inclusive.
-		 * @param time The frame time in seconds.
-		 * @param drawOrder For each slot in {@link Skeleton#slots}, the index of the slot in the new draw order. May be null to use
-		 *           setup pose draw order. */
-		public void setFrame (int frame, float time, @Null int[] drawOrder) {
-			frames[frame] = time;
-			drawOrders[frame] = drawOrder;
-		}
-
-		public void apply (Skeleton skeleton, float lastTime, float time, @Null Array<Event> events, float alpha, MixBlend blend,
-			MixDirection direction) {
-
-			if (direction == out) {
-				if (blend == setup) arraycopy(skeleton.slots.items, 0, skeleton.drawOrder.items, 0, skeleton.slots.size);
-				return;
-			}
-
-			if (time < frames[0]) { // Time is before first frame.
-				if (blend == setup || blend == first)
-					arraycopy(skeleton.slots.items, 0, skeleton.drawOrder.items, 0, skeleton.slots.size);
-				return;
-			}
-
-			int[] drawOrderToSetupIndex = drawOrders[search(frames, time)];
-			if (drawOrderToSetupIndex == null)
-				arraycopy(skeleton.slots.items, 0, skeleton.drawOrder.items, 0, skeleton.slots.size);
-			else {
-				Object[] slots = skeleton.slots.items;
-				Object[] drawOrder = skeleton.drawOrder.items;
-				for (int i = 0, n = drawOrderToSetupIndex.length; i < n; i++)
-					drawOrder[i] = slots[drawOrderToSetupIndex[i]];
-			}
-		}
-	}
-
-	/** Changes an IK constraint's {@link IkConstraint#getMix()}, {@link IkConstraint#getSoftness()},
-	 * {@link IkConstraint#getBendDirection()}, {@link IkConstraint#getStretch()}, and {@link IkConstraint#getCompress()}. */
-	static public class IkConstraintTimeline extends CurveTimeline {
-		static public final int ENTRIES = 6;
-		static private final int MIX = 1, SOFTNESS = 2, BEND_DIRECTION = 3, COMPRESS = 4, STRETCH = 5;
-
-		final int ikConstraintIndex;
-
-		public IkConstraintTimeline (int frameCount, int bezierCount, int ikConstraintIndex) {
-			super(frameCount, bezierCount, Property.ikConstraint.ordinal() + "|" + ikConstraintIndex);
-			this.ikConstraintIndex = ikConstraintIndex;
-		}
-
-		public int getFrameEntries () {
-			return ENTRIES;
-		}
-
-		/** The index of the IK constraint slot in {@link Skeleton#getIkConstraints()} that will be changed when this timeline is
-		 * applied. */
-		public int getIkConstraintIndex () {
-			return ikConstraintIndex;
-		}
-
-		/** Sets the time, mix, softness, bend direction, compress, and stretch for the specified frame.
-		 * @param frame Between 0 and <code>frameCount</code>, inclusive.
-		 * @param time The frame time in seconds.
-		 * @param bendDirection 1 or -1. */
-		public void setFrame (int frame, float time, float mix, float softness, int bendDirection, boolean compress,
-			boolean stretch) {
-			frame *= ENTRIES;
-			frames[frame] = time;
-			frames[frame + MIX] = mix;
-			frames[frame + SOFTNESS] = softness;
-			frames[frame + BEND_DIRECTION] = bendDirection;
-			frames[frame + COMPRESS] = compress ? 1 : 0;
-			frames[frame + STRETCH] = stretch ? 1 : 0;
-		}
-
-		public void apply (Skeleton skeleton, float lastTime, float time, @Null Array<Event> events, float alpha, MixBlend blend,
-			MixDirection direction) {
-
-			IkConstraint constraint = skeleton.ikConstraints.get(ikConstraintIndex);
-			if (!constraint.active) return;
-
-			float[] frames = this.frames;
-			if (time < frames[0]) { // Time is before first frame.
-				switch (blend) {
-				case setup:
-					constraint.mix = constraint.data.mix;
-					constraint.softness = constraint.data.softness;
-					constraint.bendDirection = constraint.data.bendDirection;
-					constraint.compress = constraint.data.compress;
-					constraint.stretch = constraint.data.stretch;
-					return;
-				case first:
-					constraint.mix += (constraint.data.mix - constraint.mix) * alpha;
-					constraint.softness += (constraint.data.softness - constraint.softness) * alpha;
-					constraint.bendDirection = constraint.data.bendDirection;
-					constraint.compress = constraint.data.compress;
-					constraint.stretch = constraint.data.stretch;
-				}
-				return;
-			}
-
-			float mix, softness;
-			int i = search(frames, time, ENTRIES), curveType = (int)curves[i / ENTRIES];
-			switch (curveType) {
-			case LINEAR:
-				float before = frames[i];
-				mix = frames[i + MIX];
-				softness = frames[i + SOFTNESS];
-				float t = (time - before) / (frames[i + ENTRIES] - before);
-				mix += (frames[i + ENTRIES + MIX] - mix) * t;
-				softness += (frames[i + ENTRIES + SOFTNESS] - softness) * t;
-				break;
-			case STEPPED:
-				mix = frames[i + MIX];
-				softness = frames[i + SOFTNESS];
-				break;
-			default:
-				mix = getBezierValue(time, i, MIX, curveType - BEZIER);
-				softness = getBezierValue(time, i, SOFTNESS, curveType + BEZIER_SIZE - BEZIER);
-			}
-
-			if (blend == setup) {
-				constraint.mix = constraint.data.mix + (mix - constraint.data.mix) * alpha;
-				constraint.softness = constraint.data.softness + (softness - constraint.data.softness) * alpha;
-				if (direction == out) {
-					constraint.bendDirection = constraint.data.bendDirection;
-					constraint.compress = constraint.data.compress;
-					constraint.stretch = constraint.data.stretch;
-				} else {
-					constraint.bendDirection = (int)frames[i + BEND_DIRECTION];
-					constraint.compress = frames[i + COMPRESS] != 0;
-					constraint.stretch = frames[i + STRETCH] != 0;
-				}
-			} else {
-				constraint.mix += (mix - constraint.mix) * alpha;
-				constraint.softness += (softness - constraint.softness) * alpha;
-				if (direction == in) {
-					constraint.bendDirection = (int)frames[i + BEND_DIRECTION];
-					constraint.compress = frames[i + COMPRESS] != 0;
-					constraint.stretch = frames[i + STRETCH] != 0;
-				}
-			}
-		}
-	}
-
-	/** Changes a transform constraint's {@link TransformConstraint#getMixRotate()}, {@link TransformConstraint#getMixX()},
-	 * {@link TransformConstraint#getMixY()}, {@link TransformConstraint#getMixScaleX()},
-	 * {@link TransformConstraint#getMixScaleY()}, and {@link TransformConstraint#getMixShearY()}. */
-	static public class TransformConstraintTimeline extends CurveTimeline {
-		static public final int ENTRIES = 7;
-		static private final int ROTATE = 1, X = 2, Y = 3, SCALEX = 4, SCALEY = 5, SHEARY = 6;
-
-		final int transformConstraintIndex;
-
-		public TransformConstraintTimeline (int frameCount, int bezierCount, int transformConstraintIndex) {
-			super(frameCount, bezierCount, Property.transformConstraint.ordinal() + "|" + transformConstraintIndex);
-			this.transformConstraintIndex = transformConstraintIndex;
-		}
-
-		public int getFrameEntries () {
-			return ENTRIES;
-		}
-
-		/** The index of the transform constraint slot in {@link Skeleton#getTransformConstraints()} that will be changed when this
-		 * timeline is applied. */
-		public int getTransformConstraintIndex () {
-			return transformConstraintIndex;
-		}
-
-		/** Sets the time, rotate mix, translate mix, scale mix, and shear mix for the specified frame.
-		 * @param frame Between 0 and <code>frameCount</code>, inclusive.
-		 * @param time The frame time in seconds. */
-		public void setFrame (int frame, float time, float mixRotate, float mixX, float mixY, float mixScaleX, float mixScaleY,
-			float mixShearY) {
-			frame *= ENTRIES;
-			frames[frame] = time;
-			frames[frame + ROTATE] = mixRotate;
-			frames[frame + X] = mixX;
-			frames[frame + Y] = mixY;
-			frames[frame + SCALEX] = mixScaleX;
-			frames[frame + SCALEY] = mixScaleY;
-			frames[frame + SHEARY] = mixShearY;
-		}
-
-		public void apply (Skeleton skeleton, float lastTime, float time, @Null Array<Event> events, float alpha, MixBlend blend,
-			MixDirection direction) {
-
-			TransformConstraint constraint = skeleton.transformConstraints.get(transformConstraintIndex);
-			if (!constraint.active) return;
-
-			float[] frames = this.frames;
-			if (time < frames[0]) { // Time is before first frame.
-				TransformConstraintData data = constraint.data;
-				switch (blend) {
-				case setup:
-					constraint.mixRotate = data.mixRotate;
-					constraint.mixX = data.mixX;
-					constraint.mixY = data.mixY;
-					constraint.mixScaleX = data.mixScaleX;
-					constraint.mixScaleY = data.mixScaleY;
-					constraint.mixShearY = data.mixShearY;
-					return;
-				case first:
-					constraint.mixRotate += (data.mixRotate - constraint.mixRotate) * alpha;
-					constraint.mixX += (data.mixX - constraint.mixX) * alpha;
-					constraint.mixY += (data.mixY - constraint.mixY) * alpha;
-					constraint.mixScaleX += (data.mixScaleX - constraint.mixScaleX) * alpha;
-					constraint.mixScaleY += (data.mixScaleY - constraint.mixScaleY) * alpha;
-					constraint.mixShearY += (data.mixShearY - constraint.mixShearY) * alpha;
-				}
-				return;
-			}
-
-			float rotate, x, y, scaleX, scaleY, shearY;
-			int i = search(frames, time, ENTRIES), curveType = (int)curves[i / ENTRIES];
-			switch (curveType) {
-			case LINEAR:
-				float before = frames[i];
-				rotate = frames[i + ROTATE];
-				x = frames[i + X];
-				y = frames[i + Y];
-				scaleX = frames[i + SCALEX];
-				scaleY = frames[i + SCALEY];
-				shearY = frames[i + SHEARY];
-				float t = (time - before) / (frames[i + ENTRIES] - before);
-				rotate += (frames[i + ENTRIES + ROTATE] - rotate) * t;
-				x += (frames[i + ENTRIES + X] - x) * t;
-				y += (frames[i + ENTRIES + Y] - y) * t;
-				scaleX += (frames[i + ENTRIES + SCALEX] - scaleX) * t;
-				scaleY += (frames[i + ENTRIES + SCALEY] - scaleY) * t;
-				shearY += (frames[i + ENTRIES + SHEARY] - shearY) * t;
-				break;
-			case STEPPED:
-				rotate = frames[i + ROTATE];
-				x = frames[i + X];
-				y = frames[i + Y];
-				scaleX = frames[i + SCALEX];
-				scaleY = frames[i + SCALEY];
-				shearY = frames[i + SHEARY];
-				break;
-			default:
-				rotate = getBezierValue(time, i, ROTATE, curveType - BEZIER);
-				x = getBezierValue(time, i, X, curveType + BEZIER_SIZE - BEZIER);
-				y = getBezierValue(time, i, Y, curveType + BEZIER_SIZE * 2 - BEZIER);
-				scaleX = getBezierValue(time, i, SCALEX, curveType + BEZIER_SIZE * 3 - BEZIER);
-				scaleY = getBezierValue(time, i, SCALEY, curveType + BEZIER_SIZE * 4 - BEZIER);
-				shearY = getBezierValue(time, i, SHEARY, curveType + BEZIER_SIZE * 5 - BEZIER);
-			}
-
-			if (blend == setup) {
-				TransformConstraintData data = constraint.data;
-				constraint.mixRotate = data.mixRotate + (rotate - data.mixRotate) * alpha;
-				constraint.mixX = data.mixX + (x - data.mixX) * alpha;
-				constraint.mixY = data.mixY + (y - data.mixY) * alpha;
-				constraint.mixScaleX = data.mixScaleX + (scaleX - data.mixScaleX) * alpha;
-				constraint.mixScaleY = data.mixScaleY + (scaleY - data.mixScaleY) * alpha;
-				constraint.mixShearY = data.mixShearY + (shearY - data.mixShearY) * alpha;
-			} else {
-				constraint.mixRotate += (rotate - constraint.mixRotate) * alpha;
-				constraint.mixX += (x - constraint.mixX) * alpha;
-				constraint.mixY += (y - constraint.mixY) * alpha;
-				constraint.mixScaleX += (scaleX - constraint.mixScaleX) * alpha;
-				constraint.mixScaleY += (scaleY - constraint.mixScaleY) * alpha;
-				constraint.mixShearY += (shearY - constraint.mixShearY) * alpha;
-			}
-		}
-	}
-
-	/** Changes a path constraint's {@link PathConstraint#getPosition()}. */
-	static public class PathConstraintPositionTimeline extends CurveTimeline1 {
-		final int pathConstraintIndex;
-
-		public PathConstraintPositionTimeline (int frameCount, int bezierCount, int pathConstraintIndex) {
-			super(frameCount, bezierCount, Property.pathConstraintPosition.ordinal() + "|" + pathConstraintIndex);
-			this.pathConstraintIndex = pathConstraintIndex;
-		}
-
-		/** The index of the path constraint slot in {@link Skeleton#getPathConstraints()} that will be changed when this timeline
-		 * is applied. */
-		public int getPathConstraintIndex () {
-			return pathConstraintIndex;
-		}
-
-		public void apply (Skeleton skeleton, float lastTime, float time, @Null Array<Event> events, float alpha, MixBlend blend,
-			MixDirection direction) {
-
-			PathConstraint constraint = skeleton.pathConstraints.get(pathConstraintIndex);
-			if (!constraint.active) return;
-
-			if (time < frames[0]) { // Time is before first frame.
-				switch (blend) {
-				case setup:
-					constraint.position = constraint.data.position;
-					return;
-				case first:
-					constraint.position += (constraint.data.position - constraint.position) * alpha;
-				}
-				return;
-			}
-
-			float position = getCurveValue(time);
-			if (blend == setup)
-				constraint.position = constraint.data.position + (position - constraint.data.position) * alpha;
-			else
-				constraint.position += (position - constraint.position) * alpha;
-		}
-	}
-
-	/** Changes a path constraint's {@link PathConstraint#getSpacing()}. */
-	static public class PathConstraintSpacingTimeline extends CurveTimeline1 {
-		final int pathConstraintIndex;
-
-		public PathConstraintSpacingTimeline (int frameCount, int bezierCount, int pathConstraintIndex) {
-			super(frameCount, bezierCount, Property.pathConstraintSpacing.ordinal() + "|" + pathConstraintIndex);
-			this.pathConstraintIndex = pathConstraintIndex;
-		}
-
-		/** The index of the path constraint slot in {@link Skeleton#getPathConstraints()} that will be changed when this timeline
-		 * is applied. */
-		public int getPathConstraintIndex () {
-			return pathConstraintIndex;
-		}
-
-		public void apply (Skeleton skeleton, float lastTime, float time, @Null Array<Event> events, float alpha, MixBlend blend,
-			MixDirection direction) {
-
-			PathConstraint constraint = skeleton.pathConstraints.get(pathConstraintIndex);
-			if (!constraint.active) return;
-
-			if (time < frames[0]) { // Time is before first frame.
-				switch (blend) {
-				case setup:
-					constraint.spacing = constraint.data.spacing;
-					return;
-				case first:
-					constraint.spacing += (constraint.data.spacing - constraint.spacing) * alpha;
-				}
-				return;
-			}
-
-			float spacing = getCurveValue(time);
-			if (blend == setup)
-				constraint.spacing = constraint.data.spacing + (spacing - constraint.data.spacing) * alpha;
-			else
-				constraint.spacing += (spacing - constraint.spacing) * alpha;
-		}
-	}
-
-	/** Changes a transform constraint's {@link PathConstraint#getMixRotate()}, {@link PathConstraint#getMixX()}, and
-	 * {@link PathConstraint#getMixY()}. */
-	static public class PathConstraintMixTimeline extends CurveTimeline {
-		static public final int ENTRIES = 4;
-		static private final int ROTATE = 1, X = 2, Y = 3;
-
-		final int pathConstraintIndex;
-
-		public PathConstraintMixTimeline (int frameCount, int bezierCount, int pathConstraintIndex) {
-			super(frameCount, bezierCount, Property.pathConstraintMix.ordinal() + "|" + pathConstraintIndex);
-			this.pathConstraintIndex = pathConstraintIndex;
-		}
-
-		public int getFrameEntries () {
-			return ENTRIES;
-		}
-
-		/** The index of the path constraint slot in {@link Skeleton#getPathConstraints()} that will be changed when this timeline
-		 * is applied. */
-		public int getPathConstraintIndex () {
-			return pathConstraintIndex;
-		}
-
-		/** Sets the time and color for the specified frame.
-		 * @param frame Between 0 and <code>frameCount</code>, inclusive.
-		 * @param time The frame time in seconds. */
-		public void setFrame (int frame, float time, float mixRotate, float mixX, float mixY) {
-			frame <<= 2;
-			frames[frame] = time;
-			frames[frame + ROTATE] = mixRotate;
-			frames[frame + X] = mixX;
-			frames[frame + Y] = mixY;
-		}
-
-		public void apply (Skeleton skeleton, float lastTime, float time, @Null Array<Event> events, float alpha, MixBlend blend,
-			MixDirection direction) {
-
-			PathConstraint constraint = skeleton.pathConstraints.get(pathConstraintIndex);
-			if (!constraint.active) return;
-
-			float[] frames = this.frames;
-			if (time < frames[0]) { // Time is before first frame.
-				PathConstraintData data = constraint.data;
-				switch (blend) {
-				case setup:
-					constraint.mixRotate = data.mixRotate;
-					constraint.mixX = data.mixX;
-					constraint.mixY = data.mixY;
-					return;
-				case first:
-					constraint.mixRotate += (data.mixRotate - constraint.mixRotate) * alpha;
-					constraint.mixX += (data.mixX - constraint.mixX) * alpha;
-					constraint.mixY += (data.mixY - constraint.mixY) * alpha;
-				}
-				return;
-			}
-
-			float rotate, x, y;
-			int i = search(frames, time, ENTRIES), curveType = (int)curves[i >> 2];
-			switch (curveType) {
-			case LINEAR:
-				float before = frames[i];
-				rotate = frames[i + ROTATE];
-				x = frames[i + X];
-				y = frames[i + Y];
-				float t = (time - before) / (frames[i + ENTRIES] - before);
-				rotate += (frames[i + ENTRIES + ROTATE] - rotate) * t;
-				x += (frames[i + ENTRIES + X] - x) * t;
-				y += (frames[i + ENTRIES + Y] - y) * t;
-				break;
-			case STEPPED:
-				rotate = frames[i + ROTATE];
-				x = frames[i + X];
-				y = frames[i + Y];
-				break;
-			default:
-				rotate = getBezierValue(time, i, ROTATE, curveType - BEZIER);
-				x = getBezierValue(time, i, X, curveType + BEZIER_SIZE - BEZIER);
-				y = getBezierValue(time, i, Y, curveType + BEZIER_SIZE * 2 - BEZIER);
-			}
-
-			if (blend == setup) {
-				PathConstraintData data = constraint.data;
-				constraint.mixRotate = data.mixRotate + (rotate - data.mixRotate) * alpha;
-				constraint.mixX = data.mixX + (x - data.mixX) * alpha;
-				constraint.mixY = data.mixY + (y - data.mixY) * alpha;
-			} else {
-				constraint.mixRotate += (rotate - constraint.mixRotate) * alpha;
-				constraint.mixX += (x - constraint.mixX) * alpha;
-				constraint.mixY += (y - constraint.mixY) * alpha;
-			}
-		}
-	}
-}
+/******************************************************************************
+ * Spine Runtimes License Agreement
+ * Last updated January 1, 2020. Replaces all prior versions.
+ *
+ * Copyright (c) 2013-2020, Esoteric Software LLC
+ *
+ * Integration of the Spine Runtimes into software or otherwise creating
+ * derivative works of the Spine Runtimes is permitted under the terms and
+ * conditions of Section 2 of the Spine Editor License Agreement:
+ * http://esotericsoftware.com/spine-editor-license
+ *
+ * Otherwise, it is permitted to integrate the Spine Runtimes into software
+ * or otherwise create derivative works of the Spine Runtimes (collectively,
+ * "Products"), provided that each user of the Products must obtain their own
+ * Spine Editor license and redistribution of the Products in any form must
+ * include this license and copyright notice.
+ *
+ * THE SPINE RUNTIMES ARE PROVIDED BY ESOTERIC SOFTWARE LLC "AS IS" AND ANY
+ * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL ESOTERIC SOFTWARE LLC BE LIABLE FOR ANY
+ * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES,
+ * BUSINESS INTERRUPTION, OR LOSS OF USE, DATA, OR PROFITS) HOWEVER CAUSED AND
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
+ * THE SPINE RUNTIMES, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ *****************************************************************************/
+
+package com.esotericsoftware.spine;
+
+import static com.esotericsoftware.spine.Animation.MixBlend.*;
+import static com.esotericsoftware.spine.Animation.MixDirection.*;
+import static com.esotericsoftware.spine.utils.SpineUtils.*;
+
+import com.badlogic.gdx.graphics.Color;
+import com.badlogic.gdx.utils.Array;
+import com.badlogic.gdx.utils.FloatArray;
+import com.badlogic.gdx.utils.Null;
+import com.badlogic.gdx.utils.ObjectSet;
+
+import com.esotericsoftware.spine.attachments.Attachment;
+import com.esotericsoftware.spine.attachments.VertexAttachment;
+
+/** Stores a list of timelines to animate a skeleton's pose over time. */
+public class Animation {
+	final String name;
+	Array<Timeline> timelines;
+	final ObjectSet<String> timelineIds;
+	float duration;
+
+	public Animation (String name, Array<Timeline> timelines, float duration) {
+		if (name == null) throw new IllegalArgumentException("name cannot be null.");
+		this.name = name;
+		this.duration = duration;
+		timelineIds = new ObjectSet(timelines.size);
+		setTimelines(timelines);
+	}
+
+	/** If the returned array or the timelines it contains are modified, {@link #setTimelines(Array)} must be called. */
+	public Array<Timeline> getTimelines () {
+		return timelines;
+	}
+
+	public void setTimelines (Array<Timeline> timelines) {
+		if (timelines == null) throw new IllegalArgumentException("timelines cannot be null.");
+		this.timelines = timelines;
+
+		int n = timelines.size;
+		timelineIds.clear(n);
+		Object[] items = timelines.items;
+		for (int i = 0; i < n; i++)
+			timelineIds.addAll(((Timeline)items[i]).getPropertyIds());
+	}
+
+	/** Returns true if this animation contains a timeline with any of the specified property IDs. */
+	public boolean hasTimeline (String[] propertyIds) {
+		for (String id : propertyIds)
+			if (timelineIds.contains(id)) return true;
+		return false;
+	}
+
+	/** The duration of the animation in seconds, which is usually the highest time of all frames in the timeline. The duration is
+	 * used to know when it has completed and when it should loop back to the start. */
+	public float getDuration () {
+		return duration;
+	}
+
+	public void setDuration (float duration) {
+		this.duration = duration;
+	}
+
+	/** Applies the animation's timelines to the specified skeleton.
+	 * <p>
+	 * See Timeline {@link Timeline#apply(Skeleton, float, float, Array, float, MixBlend, MixDirection)}.
+	 * @param skeleton The skeleton the animation is being applied to. This provides access to the bones, slots, and other skeleton
+	 *           components the timelines may change.
+	 * @param lastTime The last time in seconds this animation was applied. Some timelines trigger only at specific times rather
+	 *           than every frame. Pass -1 the first time an animation is applied to ensure frame 0 is triggered.
+	 * @param time The time in seconds the skeleton is being posed for. Most timelines find the frame before and the frame after
+	 *           this time and interpolate between the frame values. If beyond the {@link #getDuration()} and <code>loop</code> is
+	 *           true then the animation will repeat, else the last frame will be applied.
+	 * @param loop If true, the animation repeats after the {@link #getDuration()}.
+	 * @param events If any events are fired, they are added to this list. Can be null to ignore fired events or if no timelines
+	 *           fire events.
+	 * @param alpha 0 applies the current or setup values (depending on <code>blend</code>). 1 applies the timeline values. Between
+	 *           0 and 1 applies values between the current or setup values and the timeline values. By adjusting
+	 *           <code>alpha</code> over time, an animation can be mixed in or out. <code>alpha</code> can also be useful to apply
+	 *           animations on top of each other (layering).
+	 * @param blend Controls how mixing is applied when <code>alpha</code> < 1.
+	 * @param direction Indicates whether the timelines are mixing in or out. Used by timelines which perform instant transitions,
+	 *           such as {@link DrawOrderTimeline} or {@link AttachmentTimeline}. */
+	public void apply (Skeleton skeleton, float lastTime, float time, boolean loop, @Null Array<Event> events, float alpha,
+		MixBlend blend, MixDirection direction) {
+		if (skeleton == null) throw new IllegalArgumentException("skeleton cannot be null.");
+
+		if (loop && duration != 0) {
+			time %= duration;
+			if (lastTime > 0) lastTime %= duration;
+		}
+
+		Object[] timelines = this.timelines.items;
+		for (int i = 0, n = this.timelines.size; i < n; i++)
+			((Timeline)timelines[i]).apply(skeleton, lastTime, time, events, alpha, blend, direction);
+	}
+
+	/** The animation's name, which is unique across all animations in the skeleton. */
+	public String getName () {
+		return name;
+	}
+
+	public String toString () {
+		return name;
+	}
+
+	/** Controls how timeline values are mixed with setup pose values or current pose values when a timeline is applied with
+	 * <code>alpha</code> < 1.
+	 * <p>
+	 * See Timeline {@link Timeline#apply(Skeleton, float, float, Array, float, MixBlend, MixDirection)}. */
+	static public enum MixBlend {
+		/** Transitions from the setup value to the timeline value (the current value is not used). Before the first frame, the
+		 * setup value is set. */
+		setup,
+		/** Transitions from the current value to the timeline value. Before the first frame, transitions from the current value to
+		 * the setup value. Timelines which perform instant transitions, such as {@link DrawOrderTimeline} or
+		 * {@link AttachmentTimeline}, use the setup value before the first frame.
+		 * <p>
+		 * <code>first</code> is intended for the first animations applied, not for animations layered on top of those. */
+		first,
+		/** Transitions from the current value to the timeline value. No change is made before the first frame (the current value is
+		 * kept until the first frame).
+		 * <p>
+		 * <code>replace</code> is intended for animations layered on top of others, not for the first animations applied. */
+		replace,
+		/** Transitions from the current value to the current value plus the timeline value. No change is made before the first
+		 * frame (the current value is kept until the first frame).
+		 * <p>
+		 * <code>add</code> is intended for animations layered on top of others, not for the first animations applied. Properties
+		 * set by additive animations must be set manually or by another animation before applying the additive animations, else the
+		 * property values will increase each time the additive animations are applied. */
+		add
+	}
+
+	/** Indicates whether a timeline's <code>alpha</code> is mixing out over time toward 0 (the setup or current pose value) or
+	 * mixing in toward 1 (the timeline's value). Some timelines use this to decide how values are applied.
+	 * <p>
+	 * See Timeline {@link Timeline#apply(Skeleton, float, float, Array, float, MixBlend, MixDirection)}. */
+	static public enum MixDirection {
+		in, out
+	}
+
+	static private enum Property {
+		rotate, x, y, scaleX, scaleY, shearX, shearY, //
+		rgb, alpha, rgb2, //
+		attachment, deform, //
+		event, drawOrder, //
+		ikConstraint, transformConstraint, //
+		pathConstraintPosition, pathConstraintSpacing, pathConstraintMix
+	}
+
+	/** The base class for all timelines. */
+	static public abstract class Timeline {
+		private final String[] propertyIds;
+		final float[] frames;
+
+		/** @param propertyIds Unique identifiers for the properties the timeline modifies. */
+		public Timeline (int frameCount, String... propertyIds) {
+			if (propertyIds == null) throw new IllegalArgumentException("propertyIds cannot be null.");
+			this.propertyIds = propertyIds;
+			frames = new float[frameCount * getFrameEntries()];
+		}
+
+		/** Uniquely encodes both the type of this timeline and the skeleton properties that it affects. */
+		public String[] getPropertyIds () {
+			return propertyIds;
+		}
+
+		/** The time in seconds and any other values for each frame. */
+		public float[] getFrames () {
+			return frames;
+		}
+
+		/** The number of entries stored per frame. */
+		public int getFrameEntries () {
+			return 1;
+		}
+
+		/** The number of frames for this timeline. */
+		public int getFrameCount () {
+			return frames.length / getFrameEntries();
+		}
+
+		public float getDuration () {
+			return frames[frames.length - getFrameEntries()];
+		}
+
+		/** Applies this timeline to the skeleton.
+		 * @param skeleton The skeleton to which the timeline is being applied. This provides access to the bones, slots, and other
+		 *           skeleton components that the timeline may change.
+		 * @param lastTime The last time in seconds this timeline was applied. Timelines such as {@link EventTimeline} trigger only
+		 *           at specific times rather than every frame. In that case, the timeline triggers everything between
+		 *           <code>lastTime</code> (exclusive) and <code>time</code> (inclusive). Pass -1 the first time an animation is
+		 *           applied to ensure frame 0 is triggered.
+		 * @param time The time in seconds that the skeleton is being posed for. Most timelines find the frame before and the frame
+		 *           after this time and interpolate between the frame values. If beyond the last frame, the last frame will be
+		 *           applied.
+		 * @param events If any events are fired, they are added to this list. Can be null to ignore fired events or if the timeline
+		 *           does not fire events.
+		 * @param alpha 0 applies the current or setup value (depending on <code>blend</code>). 1 applies the timeline value.
+		 *           Between 0 and 1 applies a value between the current or setup value and the timeline value. By adjusting
+		 *           <code>alpha</code> over time, an animation can be mixed in or out. <code>alpha</code> can also be useful to
+		 *           apply animations on top of each other (layering).
+		 * @param blend Controls how mixing is applied when <code>alpha</code> < 1.
+		 * @param direction Indicates whether the timeline is mixing in or out. Used by timelines which perform instant transitions,
+		 *           such as {@link DrawOrderTimeline} or {@link AttachmentTimeline}, and others such as {@link ScaleTimeline}. */
+		abstract public void apply (Skeleton skeleton, float lastTime, float time, @Null Array<Event> events, float alpha,
+			MixBlend blend, MixDirection direction);
+
+		/** Linear search using a stride of 1.
+		 * @param time Must be >= the first value in <code>frames</code>.
+		 * @return The index of the first value <= <code>time</code>. */
+		static int search (float[] frames, float time) {
+			int n = frames.length;
+			for (int i = 1; i < n; i++)
+				if (frames[i] > time) return i - 1;
+			return n - 1;
+		}
+
+		/** Linear search using the specified stride.
+		 * @param time Must be >= the first value in <code>frames</code>.
+		 * @return The index of the first value <= <code>time</code>. */
+		static int search (float[] frames, float time, int step) {
+			int n = frames.length;
+			for (int i = step; i < n; i += step)
+				if (frames[i] > time) return i - step;
+			return n - step;
+		}
+	}
+
+	/** An interface for timelines which change the property of a bone. */
+	static public interface BoneTimeline {
+		/** The index of the bone in {@link Skeleton#getBones()} that will be changed when this timeline is applied. */
+		public int getBoneIndex ();
+	}
+
+	/** An interface for timelines which change the property of a slot. */
+	static public interface SlotTimeline {
+		/** The index of the slot in {@link Skeleton#getSlots()} that will be changed when this timeline is applied. */
+		public int getSlotIndex ();
+	}
+
+	/** The base class for timelines that interpolate between frame values using stepped, linear, or a Bezier curve. */
+	static public abstract class CurveTimeline extends Timeline {
+		static public final int LINEAR = 0, STEPPED = 1, BEZIER = 2, BEZIER_SIZE = 18;
+
+		float[] curves;
+
+		/** @param bezierCount The maximum number of Bezier curves. See {@link #shrink(int)}.
+		 * @param propertyIds Unique identifiers for the properties the timeline modifies. */
+		public CurveTimeline (int frameCount, int bezierCount, String... propertyIds) {
+			super(frameCount, propertyIds);
+			curves = new float[frameCount + bezierCount * BEZIER_SIZE];
+			curves[frameCount - 1] = STEPPED;
+		}
+
+		/** Sets the specified frame to linear interpolation.
+		 * @param frame Between 0 and <code>frameCount - 1</code>, inclusive. */
+		public void setLinear (int frame) {
+			curves[frame] = LINEAR;
+		}
+
+		/** Sets the specified frame to stepped interpolation.
+		 * @param frame Between 0 and <code>frameCount - 1</code>, inclusive. */
+		public void setStepped (int frame) {
+			curves[frame] = STEPPED;
+		}
+
+		/** Returns the interpolation type for the specified frame.
+		 * @param frame Between 0 and <code>frameCount - 1</code>, inclusive.
+		 * @return {@link #LINEAR}, {@link #STEPPED}, or {@link #BEZIER} + the index of the Bezier segments. */
+		public int getCurveType (int frame) {
+			return (int)curves[frame];
+		}
+
+		/** Shrinks the storage for Bezier curves, for use when <code>bezierCount</code> (specified in the constructor) was larger
+		 * than the actual number of Bezier curves. */
+		public void shrink (int bezierCount) {
+			int size = getFrameCount() + bezierCount * BEZIER_SIZE;
+			if (curves.length > size) {
+				float[] newCurves = new float[size];
+				arraycopy(curves, 0, newCurves, 0, size);
+				curves = newCurves;
+			}
+		}
+
+		/** Stores the segments for the specified Bezier curve. For timelines that modify multiple values, there may be more than
+		 * one curve per frame.
+		 * @param bezier The ordinal of this Bezier curve for this timeline, between 0 and <code>bezierCount - 1</code> (specified
+		 *           in the constructor), inclusive.
+		 * @param frame Between 0 and <code>frameCount - 1</code>, inclusive.
+		 * @param value The index of the value for the frame this curve is used for.
+		 * @param time1 The time for the first key.
+		 * @param value1 The value for the first key.
+		 * @param cx1 The time for the first Bezier handle.
+		 * @param cy1 The value for the first Bezier handle.
+		 * @param cx2 The time of the second Bezier handle.
+		 * @param cy2 The value for the second Bezier handle.
+		 * @param time2 The time for the second key.
+		 * @param value2 The value for the second key. */
+		public void setBezier (int bezier, int frame, int value, float time1, float value1, float cx1, float cy1, float cx2,
+			float cy2, float time2, float value2) {
+			float[] curves = this.curves;
+			int i = getFrameCount() + bezier * BEZIER_SIZE;
+			if (value == 0) curves[frame] = BEZIER + i;
+			float tmpx = (time1 - cx1 * 2 + cx2) * 0.03f, tmpy = (value1 - cy1 * 2 + cy2) * 0.03f;
+			float dddx = ((cx1 - cx2) * 3 - time1 + time2) * 0.006f, dddy = ((cy1 - cy2) * 3 - value1 + value2) * 0.006f;
+			float ddx = tmpx * 2 + dddx, ddy = tmpy * 2 + dddy;
+			float dx = (cx1 - time1) * 0.3f + tmpx + dddx * 0.16666667f, dy = (cy1 - value1) * 0.3f + tmpy + dddy * 0.16666667f;
+			float x = time1 + dx, y = value1 + dy;
+			for (int n = i + BEZIER_SIZE; i < n; i += 2) {
+				curves[i] = x;
+				curves[i + 1] = y;
+				dx += ddx;
+				dy += ddy;
+				ddx += dddx;
+				ddy += dddy;
+				x += dx;
+				y += dy;
+			}
+		}
+
+		/** Returns the Bezier interpolated value for the specified time.
+		 * @param frameIndex The index into {@link #getFrames()} for the values of the frame before <code>time</code>.
+		 * @param valueOffset The offset from <code>frameIndex</code> to the value this curve is used for.
+		 * @param i The index of the Bezier segments. See {@link #getCurveType(int)}. */
+		public float getBezierValue (float time, int frameIndex, int valueOffset, int i) {
+			float[] curves = this.curves;
+			if (curves[i] > time) {
+				float x = frames[frameIndex], y = frames[frameIndex + valueOffset];
+				return y + (time - x) / (curves[i] - x) * (curves[i + 1] - y);
+			}
+			int n = i + BEZIER_SIZE;
+			for (i += 2; i < n; i += 2) {
+				if (curves[i] >= time) {
+					float x = curves[i - 2], y = curves[i - 1];
+					return y + (time - x) / (curves[i] - x) * (curves[i + 1] - y);
+				}
+			}
+			frameIndex += getFrameEntries();
+			float x = curves[n - 2], y = curves[n - 1];
+			return y + (time - x) / (frames[frameIndex] - x) * (frames[frameIndex + valueOffset] - y);
+		}
+	}
+
+	/** The base class for a {@link CurveTimeline} that sets one property. */
+	static public abstract class CurveTimeline1 extends CurveTimeline {
+		static public final int ENTRIES = 2;
+		static final int VALUE = 1;
+
+		/** @param bezierCount The maximum number of Bezier curves. See {@link #shrink(int)}.
+		 * @param propertyId Unique identifier for the property the timeline modifies. */
+		public CurveTimeline1 (int frameCount, int bezierCount, String propertyId) {
+			super(frameCount, bezierCount, propertyId);
+		}
+
+		public int getFrameEntries () {
+			return ENTRIES;
+		}
+
+		/** Sets the time and value for the specified frame.
+		 * @param frame Between 0 and <code>frameCount</code>, inclusive.
+		 * @param time The frame time in seconds. */
+		public void setFrame (int frame, float time, float value) {
+			frame <<= 1;
+			frames[frame] = time;
+			frames[frame + VALUE] = value;
+		}
+
+		/** Returns the interpolated value for the specified time. */
+		public float getCurveValue (float time) {
+			float[] frames = this.frames;
+			int i = frames.length - 2;
+			for (int ii = 2; ii <= i; ii += 2) {
+				if (frames[ii] > time) {
+					i = ii - 2;
+					break;
+				}
+			}
+
+			int curveType = (int)curves[i >> 1];
+			switch (curveType) {
+			case LINEAR:
+				float before = frames[i], value = frames[i + VALUE];
+				return value + (time - before) / (frames[i + ENTRIES] - before) * (frames[i + ENTRIES + VALUE] - value);
+			case STEPPED:
+				return frames[i + VALUE];
+			}
+			return getBezierValue(time, i, VALUE, curveType - BEZIER);
+		}
+	}
+
+	/** The base class for a {@link CurveTimeline} which sets two properties. */
+	static public abstract class CurveTimeline2 extends CurveTimeline {
+		static public final int ENTRIES = 3;
+		static final int VALUE1 = 1, VALUE2 = 2;
+
+		/** @param bezierCount The maximum number of Bezier curves. See {@link #shrink(int)}.
+		 * @param propertyId1 Unique identifier for the first property the timeline modifies.
+		 * @param propertyId2 Unique identifier for the second property the timeline modifies. */
+		public CurveTimeline2 (int frameCount, int bezierCount, String propertyId1, String propertyId2) {
+			super(frameCount, bezierCount, propertyId1, propertyId2);
+		}
+
+		public int getFrameEntries () {
+			return ENTRIES;
+		}
+
+		/** Sets the time and values for the specified frame.
+		 * @param frame Between 0 and <code>frameCount</code>, inclusive.
+		 * @param time The frame time in seconds. */
+		public void setFrame (int frame, float time, float value1, float value2) {
+			frame *= ENTRIES;
+			frames[frame] = time;
+			frames[frame + VALUE1] = value1;
+			frames[frame + VALUE2] = value2;
+		}
+	}
+
+	/** Changes a bone's local {@link Bone#getRotation()}. */
+	static public class RotateTimeline extends CurveTimeline1 implements BoneTimeline {
+		final int boneIndex;
+
+		public RotateTimeline (int frameCount, int bezierCount, int boneIndex) {
+			super(frameCount, bezierCount, Property.rotate.ordinal() + "|" + boneIndex);
+			this.boneIndex = boneIndex;
+		}
+
+		public int getBoneIndex () {
+			return boneIndex;
+		}
+
+		public void apply (Skeleton skeleton, float lastTime, float time, @Null Array<Event> events, float alpha, MixBlend blend,
+			MixDirection direction) {
+
+			Bone bone = skeleton.bones.get(boneIndex);
+			if (!bone.active) return;
+
+			if (time < frames[0]) { // Time is before first frame.
+				switch (blend) {
+				case setup:
+					bone.rotation = bone.data.rotation;
+					return;
+				case first:
+					bone.rotation += (bone.data.rotation - bone.rotation) * alpha;
+				}
+				return;
+			}
+
+			float r = getCurveValue(time);
+			switch (blend) {
+			case setup:
+				bone.rotation = bone.data.rotation + r * alpha;
+				break;
+			case first:
+			case replace:
+				r += bone.data.rotation - bone.rotation;
+				// Fall through.
+			case add:
+				bone.rotation += r * alpha;
+			}
+		}
+	}
+
+	/** Changes a bone's local {@link Bone#getX()} and {@link Bone#getY()}. */
+	static public class TranslateTimeline extends CurveTimeline2 implements BoneTimeline {
+		final int boneIndex;
+
+		public TranslateTimeline (int frameCount, int bezierCount, int boneIndex) {
+			super(frameCount, bezierCount, //
+				Property.x.ordinal() + "|" + boneIndex, //
+				Property.y.ordinal() + "|" + boneIndex);
+			this.boneIndex = boneIndex;
+		}
+
+		public int getBoneIndex () {
+			return boneIndex;
+		}
+
+		public void apply (Skeleton skeleton, float lastTime, float time, @Null Array<Event> events, float alpha, MixBlend blend,
+			MixDirection direction) {
+
+			Bone bone = skeleton.bones.get(boneIndex);
+			if (!bone.active) return;
+
+			float[] frames = this.frames;
+			if (time < frames[0]) { // Time is before first frame.
+				switch (blend) {
+				case setup:
+					bone.x = bone.data.x;
+					bone.y = bone.data.y;
+					return;
+				case first:
+					bone.x += (bone.data.x - bone.x) * alpha;
+					bone.y += (bone.data.y - bone.y) * alpha;
+				}
+				return;
+			}
+
+			float x, y;
+			int i = search(frames, time, ENTRIES), curveType = (int)curves[i / ENTRIES];
+			switch (curveType) {
+			case LINEAR:
+				float before = frames[i];
+				x = frames[i + VALUE1];
+				y = frames[i + VALUE2];
+				float t = (time - before) / (frames[i + ENTRIES] - before);
+				x += (frames[i + ENTRIES + VALUE1] - x) * t;
+				y += (frames[i + ENTRIES + VALUE2] - y) * t;
+				break;
+			case STEPPED:
+				x = frames[i + VALUE1];
+				y = frames[i + VALUE2];
+				break;
+			default:
+				x = getBezierValue(time, i, VALUE1, curveType - BEZIER);
+				y = getBezierValue(time, i, VALUE2, curveType + BEZIER_SIZE - BEZIER);
+			}
+
+			switch (blend) {
+			case setup:
+				bone.x = bone.data.x + x * alpha;
+				bone.y = bone.data.y + y * alpha;
+				break;
+			case first:
+			case replace:
+				bone.x += (bone.data.x + x - bone.x) * alpha;
+				bone.y += (bone.data.y + y - bone.y) * alpha;
+				break;
+			case add:
+				bone.x += x * alpha;
+				bone.y += y * alpha;
+			}
+		}
+	}
+
+	/** Changes a bone's local {@link Bone#getX()}. */
+	static public class TranslateXTimeline extends CurveTimeline1 implements BoneTimeline {
+		final int boneIndex;
+
+		public TranslateXTimeline (int frameCount, int bezierCount, int boneIndex) {
+			super(frameCount, bezierCount, Property.x.ordinal() + "|" + boneIndex);
+			this.boneIndex = boneIndex;
+		}
+
+		public int getBoneIndex () {
+			return boneIndex;
+		}
+
+		public void apply (Skeleton skeleton, float lastTime, float time, @Null Array<Event> events, float alpha, MixBlend blend,
+			MixDirection direction) {
+
+			Bone bone = skeleton.bones.get(boneIndex);
+			if (!bone.active) return;
+
+			float[] frames = this.frames;
+			if (time < frames[0]) { // Time is before first frame.
+				switch (blend) {
+				case setup:
+					bone.x = bone.data.x;
+					return;
+				case first:
+					bone.x += (bone.data.x - bone.x) * alpha;
+				}
+				return;
+			}
+
+			float x = getCurveValue(time);
+			switch (blend) {
+			case setup:
+				bone.x = bone.data.x + x * alpha;
+				break;
+			case first:
+			case replace:
+				bone.x += (bone.data.x + x - bone.x) * alpha;
+				break;
+			case add:
+				bone.x += x * alpha;
+			}
+		}
+	}
+
+	/** Changes a bone's local {@link Bone#getY()}. */
+	static public class TranslateYTimeline extends CurveTimeline1 implements BoneTimeline {
+		final int boneIndex;
+
+		public TranslateYTimeline (int frameCount, int bezierCount, int boneIndex) {
+			super(frameCount, bezierCount, Property.y.ordinal() + "|" + boneIndex);
+			this.boneIndex = boneIndex;
+		}
+
+		public int getBoneIndex () {
+			return boneIndex;
+		}
+
+		public void apply (Skeleton skeleton, float lastTime, float time, @Null Array<Event> events, float alpha, MixBlend blend,
+			MixDirection direction) {
+
+			Bone bone = skeleton.bones.get(boneIndex);
+			if (!bone.active) return;
+
+			float[] frames = this.frames;
+			if (time < frames[0]) { // Time is before first frame.
+				switch (blend) {
+				case setup:
+					bone.y = bone.data.y;
+					return;
+				case first:
+					bone.y += (bone.data.y - bone.y) * alpha;
+				}
+				return;
+			}
+
+			float y = getCurveValue(time);
+			switch (blend) {
+			case setup:
+				bone.y = bone.data.y + y * alpha;
+				break;
+			case first:
+			case replace:
+				bone.y += (bone.data.y + y - bone.y) * alpha;
+				break;
+			case add:
+				bone.y += y * alpha;
+			}
+		}
+	}
+
+	/** Changes a bone's local {@link Bone#getScaleX()} and {@link Bone#getScaleY()}. */
+	static public class ScaleTimeline extends CurveTimeline2 implements BoneTimeline {
+		final int boneIndex;
+
+		public ScaleTimeline (int frameCount, int bezierCount, int boneIndex) {
+			super(frameCount, bezierCount, //
+				Property.scaleX.ordinal() + "|" + boneIndex, //
+				Property.scaleY.ordinal() + "|" + boneIndex);
+			this.boneIndex = boneIndex;
+		}
+
+		public int getBoneIndex () {
+			return boneIndex;
+		}
+
+		public void apply (Skeleton skeleton, float lastTime, float time, @Null Array<Event> events, float alpha, MixBlend blend,
+			MixDirection direction) {
+
+			Bone bone = skeleton.bones.get(boneIndex);
+			if (!bone.active) return;
+
+			float[] frames = this.frames;
+			if (time < frames[0]) { // Time is before first frame.
+				switch (blend) {
+				case setup:
+					bone.scaleX = bone.data.scaleX;
+					bone.scaleY = bone.data.scaleY;
+					return;
+				case first:
+					bone.scaleX += (bone.data.scaleX - bone.scaleX) * alpha;
+					bone.scaleY += (bone.data.scaleY - bone.scaleY) * alpha;
+				}
+				return;
+			}
+
+			float x, y;
+			int i = search(frames, time, ENTRIES), curveType = (int)curves[i / ENTRIES];
+			switch (curveType) {
+			case LINEAR:
+				float before = frames[i];
+				x = frames[i + VALUE1];
+				y = frames[i + VALUE2];
+				float t = (time - before) / (frames[i + ENTRIES] - before);
+				x += (frames[i + ENTRIES + VALUE1] - x) * t;
+				y += (frames[i + ENTRIES + VALUE2] - y) * t;
+				break;
+			case STEPPED:
+				x = frames[i + VALUE1];
+				y = frames[i + VALUE2];
+				break;
+			default:
+				x = getBezierValue(time, i, VALUE1, curveType - BEZIER);
+				y = getBezierValue(time, i, VALUE2, curveType + BEZIER_SIZE - BEZIER);
+			}
+			x *= bone.data.scaleX;
+			y *= bone.data.scaleY;
+
+			if (alpha == 1) {
+				if (blend == add) {
+					bone.scaleX += x - bone.data.scaleX;
+					bone.scaleY += y - bone.data.scaleY;
+				} else {
+					bone.scaleX = x;
+					bone.scaleY = y;
+				}
+			} else {
+				// Mixing out uses sign of setup or current pose, else use sign of key.
+				float bx, by;
+				if (direction == out) {
+					switch (blend) {
+					case setup:
+						bx = bone.data.scaleX;
+						by = bone.data.scaleY;
+						bone.scaleX = bx + (Math.abs(x) * Math.signum(bx) - bx) * alpha;
+						bone.scaleY = by + (Math.abs(y) * Math.signum(by) - by) * alpha;
+						break;
+					case first:
+					case replace:
+						bx = bone.scaleX;
+						by = bone.scaleY;
+						bone.scaleX = bx + (Math.abs(x) * Math.signum(bx) - bx) * alpha;
+						bone.scaleY = by + (Math.abs(y) * Math.signum(by) - by) * alpha;
+						break;
+					case add:
+						bx = bone.scaleX;
+						by = bone.scaleY;
+						bone.scaleX = bx + (Math.abs(x) * Math.signum(bx) - bone.data.scaleX) * alpha;
+						bone.scaleY = by + (Math.abs(y) * Math.signum(by) - bone.data.scaleY) * alpha;
+					}
+				} else {
+					switch (blend) {
+					case setup:
+						bx = Math.abs(bone.data.scaleX) * Math.signum(x);
+						by = Math.abs(bone.data.scaleY) * Math.signum(y);
+						bone.scaleX = bx + (x - bx) * alpha;
+						bone.scaleY = by + (y - by) * alpha;
+						break;
+					case first:
+					case replace:
+						bx = Math.abs(bone.scaleX) * Math.signum(x);
+						by = Math.abs(bone.scaleY) * Math.signum(y);
+						bone.scaleX = bx + (x - bx) * alpha;
+						bone.scaleY = by + (y - by) * alpha;
+						break;
+					case add:
+						bx = Math.signum(x);
+						by = Math.signum(y);
+						bone.scaleX = Math.abs(bone.scaleX) * bx + (x - Math.abs(bone.data.scaleX) * bx) * alpha;
+						bone.scaleY = Math.abs(bone.scaleY) * by + (y - Math.abs(bone.data.scaleY) * by) * alpha;
+					}
+				}
+			}
+		}
+	}
+
+	/** Changes a bone's local {@link Bone#getScaleX()}. */
+	static public class ScaleXTimeline extends CurveTimeline1 implements BoneTimeline {
+		final int boneIndex;
+
+		public ScaleXTimeline (int frameCount, int bezierCount, int boneIndex) {
+			super(frameCount, bezierCount, Property.scaleX.ordinal() + "|" + boneIndex);
+			this.boneIndex = boneIndex;
+		}
+
+		public int getBoneIndex () {
+			return boneIndex;
+		}
+
+		public void apply (Skeleton skeleton, float lastTime, float time, @Null Array<Event> events, float alpha, MixBlend blend,
+			MixDirection direction) {
+
+			Bone bone = skeleton.bones.get(boneIndex);
+			if (!bone.active) return;
+
+			float[] frames = this.frames;
+			if (time < frames[0]) { // Time is before first frame.
+				switch (blend) {
+				case setup:
+					bone.scaleX = bone.data.scaleX;
+					return;
+				case first:
+					bone.scaleX += (bone.data.scaleX - bone.scaleX) * alpha;
+				}
+				return;
+			}
+
+			float x = getCurveValue(time) * bone.data.scaleX;
+			if (alpha == 1) {
+				if (blend == add)
+					bone.scaleX += x - bone.data.scaleX;
+				else
+					bone.scaleX = x;
+			} else {
+				// Mixing out uses sign of setup or current pose, else use sign of key.
+				float bx;
+				if (direction == out) {
+					switch (blend) {
+					case setup:
+						bx = bone.data.scaleX;
+						bone.scaleX = bx + (Math.abs(x) * Math.signum(bx) - bx) * alpha;
+						break;
+					case first:
+					case replace:
+						bx = bone.scaleX;
+						bone.scaleX = bx + (Math.abs(x) * Math.signum(bx) - bx) * alpha;
+						break;
+					case add:
+						bx = bone.scaleX;
+						bone.scaleX = bx + (Math.abs(x) * Math.signum(bx) - bone.data.scaleX) * alpha;
+					}
+				} else {
+					switch (blend) {
+					case setup:
+						bx = Math.abs(bone.data.scaleX) * Math.signum(x);
+						bone.scaleX = bx + (x - bx) * alpha;
+						break;
+					case first:
+					case replace:
+						bx = Math.abs(bone.scaleX) * Math.signum(x);
+						bone.scaleX = bx + (x - bx) * alpha;
+						break;
+					case add:
+						bx = Math.signum(x);
+						bone.scaleX = Math.abs(bone.scaleX) * bx + (x - Math.abs(bone.data.scaleX) * bx) * alpha;
+					}
+				}
+			}
+		}
+	}
+
+	/** Changes a bone's local {@link Bone#getScaleY()}. */
+	static public class ScaleYTimeline extends CurveTimeline1 implements BoneTimeline {
+		final int boneIndex;
+
+		public ScaleYTimeline (int frameCount, int bezierCount, int boneIndex) {
+			super(frameCount, bezierCount, Property.scaleY.ordinal() + "|" + boneIndex);
+			this.boneIndex = boneIndex;
+		}
+
+		public int getBoneIndex () {
+			return boneIndex;
+		}
+
+		public void apply (Skeleton skeleton, float lastTime, float time, @Null Array<Event> events, float alpha, MixBlend blend,
+			MixDirection direction) {
+
+			Bone bone = skeleton.bones.get(boneIndex);
+			if (!bone.active) return;
+
+			float[] frames = this.frames;
+			if (time < frames[0]) { // Time is before first frame.
+				switch (blend) {
+				case setup:
+					bone.scaleY = bone.data.scaleY;
+					return;
+				case first:
+					bone.scaleY += (bone.data.scaleY - bone.scaleY) * alpha;
+				}
+				return;
+			}
+
+			float y = getCurveValue(time) * bone.data.scaleY;
+			if (alpha == 1) {
+				if (blend == add)
+					bone.scaleY += y - bone.data.scaleY;
+				else
+					bone.scaleY = y;
+			} else {
+				// Mixing out uses sign of setup or current pose, else use sign of key.
+				float by;
+				if (direction == out) {
+					switch (blend) {
+					case setup:
+						by = bone.data.scaleY;
+						bone.scaleY = by + (Math.abs(y) * Math.signum(by) - by) * alpha;
+						break;
+					case first:
+					case replace:
+						by = bone.scaleY;
+						bone.scaleY = by + (Math.abs(y) * Math.signum(by) - by) * alpha;
+						break;
+					case add:
+						by = bone.scaleY;
+						bone.scaleY = by + (Math.abs(y) * Math.signum(by) - bone.data.scaleY) * alpha;
+					}
+				} else {
+					switch (blend) {
+					case setup:
+						by = Math.abs(bone.data.scaleY) * Math.signum(y);
+						bone.scaleY = by + (y - by) * alpha;
+						break;
+					case first:
+					case replace:
+						by = Math.abs(bone.scaleY) * Math.signum(y);
+						bone.scaleY = by + (y - by) * alpha;
+						break;
+					case add:
+						by = Math.signum(y);
+						bone.scaleY = Math.abs(bone.scaleY) * by + (y - Math.abs(bone.data.scaleY) * by) * alpha;
+					}
+				}
+			}
+		}
+	}
+
+	/** Changes a bone's local {@link Bone#getShearX()} and {@link Bone#getShearY()}. */
+	static public class ShearTimeline extends CurveTimeline2 implements BoneTimeline {
+		final int boneIndex;
+
+		public ShearTimeline (int frameCount, int bezierCount, int boneIndex) {
+			super(frameCount, bezierCount, //
+				Property.shearX.ordinal() + "|" + boneIndex, //
+				Property.shearY.ordinal() + "|" + boneIndex);
+			this.boneIndex = boneIndex;
+		}
+
+		public int getBoneIndex () {
+			return boneIndex;
+		}
+
+		public void apply (Skeleton skeleton, float lastTime, float time, @Null Array<Event> events, float alpha, MixBlend blend,
+			MixDirection direction) {
+
+			Bone bone = skeleton.bones.get(boneIndex);
+			if (!bone.active) return;
+
+			float[] frames = this.frames;
+			if (time < frames[0]) { // Time is before first frame.
+				switch (blend) {
+				case setup:
+					bone.shearX = bone.data.shearX;
+					bone.shearY = bone.data.shearY;
+					return;
+				case first:
+					bone.shearX += (bone.data.shearX - bone.shearX) * alpha;
+					bone.shearY += (bone.data.shearY - bone.shearY) * alpha;
+				}
+				return;
+			}
+
+			float x, y;
+			int i = search(frames, time, ENTRIES), curveType = (int)curves[i / ENTRIES];
+			switch (curveType) {
+			case LINEAR:
+				float before = frames[i];
+				x = frames[i + VALUE1];
+				y = frames[i + VALUE2];
+				float t = (time - before) / (frames[i + ENTRIES] - before);
+				x += (frames[i + ENTRIES + VALUE1] - x) * t;
+				y += (frames[i + ENTRIES + VALUE2] - y) * t;
+				break;
+			case STEPPED:
+				x = frames[i + VALUE1];
+				y = frames[i + VALUE2];
+				break;
+			default:
+				x = getBezierValue(time, i, VALUE1, curveType - BEZIER);
+				y = getBezierValue(time, i, VALUE2, curveType + BEZIER_SIZE - BEZIER);
+			}
+
+			switch (blend) {
+			case setup:
+				bone.shearX = bone.data.shearX + x * alpha;
+				bone.shearY = bone.data.shearY + y * alpha;
+				break;
+			case first:
+			case replace:
+				bone.shearX += (bone.data.shearX + x - bone.shearX) * alpha;
+				bone.shearY += (bone.data.shearY + y - bone.shearY) * alpha;
+				break;
+			case add:
+				bone.shearX += x * alpha;
+				bone.shearY += y * alpha;
+			}
+		}
+	}
+
+	/** Changes a bone's local {@link Bone#getShearX()}. */
+	static public class ShearXTimeline extends CurveTimeline1 implements BoneTimeline {
+		final int boneIndex;
+
+		public ShearXTimeline (int frameCount, int bezierCount, int boneIndex) {
+			super(frameCount, bezierCount, Property.shearX.ordinal() + "|" + boneIndex);
+			this.boneIndex = boneIndex;
+		}
+
+		public int getBoneIndex () {
+			return boneIndex;
+		}
+
+		public void apply (Skeleton skeleton, float lastTime, float time, @Null Array<Event> events, float alpha, MixBlend blend,
+			MixDirection direction) {
+
+			Bone bone = skeleton.bones.get(boneIndex);
+			if (!bone.active) return;
+
+			float[] frames = this.frames;
+			if (time < frames[0]) { // Time is before first frame.
+				switch (blend) {
+				case setup:
+					bone.shearX = bone.data.shearX;
+					return;
+				case first:
+					bone.shearX += (bone.data.shearX - bone.shearX) * alpha;
+				}
+				return;
+			}
+
+			float x = getCurveValue(time);
+			switch (blend) {
+			case setup:
+				bone.shearX = bone.data.shearX + x * alpha;
+				break;
+			case first:
+			case replace:
+				bone.shearX += (bone.data.shearX + x - bone.shearX) * alpha;
+				break;
+			case add:
+				bone.shearX += x * alpha;
+			}
+		}
+	}
+
+	/** Changes a bone's local {@link Bone#getShearY()}. */
+	static public class ShearYTimeline extends CurveTimeline1 implements BoneTimeline {
+		final int boneIndex;
+
+		public ShearYTimeline (int frameCount, int bezierCount, int boneIndex) {
+			super(frameCount, bezierCount, Property.shearY.ordinal() + "|" + boneIndex);
+			this.boneIndex = boneIndex;
+		}
+
+		public int getBoneIndex () {
+			return boneIndex;
+		}
+
+		public void apply (Skeleton skeleton, float lastTime, float time, @Null Array<Event> events, float alpha, MixBlend blend,
+			MixDirection direction) {
+
+			Bone bone = skeleton.bones.get(boneIndex);
+			if (!bone.active) return;
+
+			float[] frames = this.frames;
+			if (time < frames[0]) { // Time is before first frame.
+				switch (blend) {
+				case setup:
+					bone.shearY = bone.data.shearY;
+					return;
+				case first:
+					bone.shearY += (bone.data.shearY - bone.shearY) * alpha;
+				}
+				return;
+			}
+
+			float y = getCurveValue(time);
+			switch (blend) {
+			case setup:
+				bone.shearY = bone.data.shearY + y * alpha;
+				break;
+			case first:
+			case replace:
+				bone.shearY += (bone.data.shearY + y - bone.shearY) * alpha;
+				break;
+			case add:
+				bone.shearY += y * alpha;
+			}
+		}
+	}
+
+	/** Changes a slot's {@link Slot#getColor()}. */
+	static public class RGBATimeline extends CurveTimeline implements SlotTimeline {
+		static public final int ENTRIES = 5;
+		static private final int R = 1, G = 2, B = 3, A = 4;
+
+		final int slotIndex;
+
+		public RGBATimeline (int frameCount, int bezierCount, int slotIndex) {
+			super(frameCount, bezierCount, //
+				Property.rgb.ordinal() + "|" + slotIndex, //
+				Property.alpha.ordinal() + "|" + slotIndex);
+			this.slotIndex = slotIndex;
+		}
+
+		public int getFrameEntries () {
+			return ENTRIES;
+		}
+
+		public int getSlotIndex () {
+			return slotIndex;
+		}
+
+		/** Sets the time and color for the specified frame.
+		 * @param frame Between 0 and <code>frameCount</code>, inclusive.
+		 * @param time The frame time in seconds. */
+		public void setFrame (int frame, float time, float r, float g, float b, float a) {
+			frame *= ENTRIES;
+			frames[frame] = time;
+			frames[frame + R] = r;
+			frames[frame + G] = g;
+			frames[frame + B] = b;
+			frames[frame + A] = a;
+		}
+
+		public void apply (Skeleton skeleton, float lastTime, float time, @Null Array<Event> events, float alpha, MixBlend blend,
+			MixDirection direction) {
+
+			Slot slot = skeleton.slots.get(slotIndex);
+			if (!slot.bone.active) return;
+
+			float[] frames = this.frames;
+			Color color = slot.color;
+			if (time < frames[0]) { // Time is before first frame.
+				Color setup = slot.data.color;
+				switch (blend) {
+				case setup:
+					color.set(setup);
+					return;
+				case first:
+					color.add((setup.r - color.r) * alpha, (setup.g - color.g) * alpha, (setup.b - color.b) * alpha,
+						(setup.a - color.a) * alpha);
+				}
+				return;
+			}
+
+			float r, g, b, a;
+			int i = search(frames, time, ENTRIES), curveType = (int)curves[i / ENTRIES];
+			switch (curveType) {
+			case LINEAR:
+				float before = frames[i];
+				r = frames[i + R];
+				g = frames[i + G];
+				b = frames[i + B];
+				a = frames[i + A];
+				float t = (time - before) / (frames[i + ENTRIES] - before);
+				r += (frames[i + ENTRIES + R] - r) * t;
+				g += (frames[i + ENTRIES + G] - g) * t;
+				b += (frames[i + ENTRIES + B] - b) * t;
+				a += (frames[i + ENTRIES + A] - a) * t;
+				break;
+			case STEPPED:
+				r = frames[i + R];
+				g = frames[i + G];
+				b = frames[i + B];
+				a = frames[i + A];
+				break;
+			default:
+				r = getBezierValue(time, i, R, curveType - BEZIER);
+				g = getBezierValue(time, i, G, curveType + BEZIER_SIZE - BEZIER);
+				b = getBezierValue(time, i, B, curveType + BEZIER_SIZE * 2 - BEZIER);
+				a = getBezierValue(time, i, A, curveType + BEZIER_SIZE * 3 - BEZIER);
+			}
+
+			if (alpha == 1)
+				color.set(r, g, b, a);
+			else {
+				if (blend == setup) color.set(slot.data.color);
+				color.add((r - color.r) * alpha, (g - color.g) * alpha, (b - color.b) * alpha, (a - color.a) * alpha);
+			}
+		}
+	}
+
+	/** Changes the RGB for a slot's {@link Slot#getColor()}. */
+	static public class RGBTimeline extends CurveTimeline implements SlotTimeline {
+		static public final int ENTRIES = 4;
+		static private final int R = 1, G = 2, B = 3;
+
+		final int slotIndex;
+
+		public RGBTimeline (int frameCount, int bezierCount, int slotIndex) {
+			super(frameCount, bezierCount, Property.rgb.ordinal() + "|" + slotIndex);
+			this.slotIndex = slotIndex;
+		}
+
+		public int getFrameEntries () {
+			return ENTRIES;
+		}
+
+		public int getSlotIndex () {
+			return slotIndex;
+		}
+
+		/** Sets the time and color for the specified frame.
+		 * @param frame Between 0 and <code>frameCount</code>, inclusive.
+		 * @param time The frame time in seconds. */
+		public void setFrame (int frame, float time, float r, float g, float b) {
+			frame <<= 2;
+			frames[frame] = time;
+			frames[frame + R] = r;
+			frames[frame + G] = g;
+			frames[frame + B] = b;
+		}
+
+		public void apply (Skeleton skeleton, float lastTime, float time, @Null Array<Event> events, float alpha, MixBlend blend,
+			MixDirection direction) {
+
+			Slot slot = skeleton.slots.get(slotIndex);
+			if (!slot.bone.active) return;
+
+			float[] frames = this.frames;
+			Color color = slot.color;
+			if (time < frames[0]) { // Time is before first frame.
+				Color setup = slot.data.color;
+				switch (blend) {
+				case setup:
+					color.r = setup.r;
+					color.g = setup.g;
+					color.b = setup.b;
+					return;
+				case first:
+					color.r += (setup.r - color.r) * alpha;
+					color.g += (setup.g - color.g) * alpha;
+					color.b += (setup.b - color.b) * alpha;
+				}
+				return;
+			}
+
+			float r, g, b;
+			int i = search(frames, time, ENTRIES), curveType = (int)curves[i >> 2];
+			switch (curveType) {
+			case LINEAR:
+				float before = frames[i];
+				r = frames[i + R];
+				g = frames[i + G];
+				b = frames[i + B];
+				float t = (time - before) / (frames[i + ENTRIES] - before);
+				r += (frames[i + ENTRIES + R] - r) * t;
+				g += (frames[i + ENTRIES + G] - g) * t;
+				b += (frames[i + ENTRIES + B] - b) * t;
+				break;
+			case STEPPED:
+				r = frames[i + R];
+				g = frames[i + G];
+				b = frames[i + B];
+				break;
+			default:
+				r = getBezierValue(time, i, R, curveType - BEZIER);
+				g = getBezierValue(time, i, G, curveType + BEZIER_SIZE - BEZIER);
+				b = getBezierValue(time, i, B, curveType + BEZIER_SIZE * 2 - BEZIER);
+			}
+
+			if (alpha == 1) {
+				color.r = r;
+				color.g = g;
+				color.b = b;
+			} else {
+				if (blend == setup) {
+					Color setup = slot.data.color;
+					color.r = setup.r;
+					color.g = setup.g;
+					color.b = setup.b;
+				}
+				color.r += (r - color.r) * alpha;
+				color.g += (g - color.g) * alpha;
+				color.b += (b - color.b) * alpha;
+			}
+		}
+	}
+
+	/** Changes the alpha for a slot's {@link Slot#getColor()}. */
+	static public class AlphaTimeline extends CurveTimeline1 implements SlotTimeline {
+		final int slotIndex;
+
+		public AlphaTimeline (int frameCount, int bezierCount, int slotIndex) {
+			super(frameCount, bezierCount, Property.alpha.ordinal() + "|" + slotIndex);
+			this.slotIndex = slotIndex;
+		}
+
+		public int getSlotIndex () {
+			return slotIndex;
+		}
+
+		public void apply (Skeleton skeleton, float lastTime, float time, @Null Array<Event> events, float alpha, MixBlend blend,
+			MixDirection direction) {
+
+			Slot slot = skeleton.slots.get(slotIndex);
+			if (!slot.bone.active) return;
+
+			float[] frames = this.frames;
+			Color color = slot.color;
+			if (time < frames[0]) { // Time is before first frame.
+				Color setup = slot.data.color;
+				switch (blend) {
+				case setup:
+					color.a = setup.a;
+					return;
+				case first:
+					color.a += (setup.a - color.a) * alpha;
+				}
+				return;
+			}
+
+			float a = getCurveValue(time);
+			if (alpha == 1)
+				color.a = a;
+			else {
+				if (blend == setup) color.a = slot.data.color.a;
+				color.a += (a - color.a) * alpha;
+			}
+		}
+	}
+
+	/** Changes a slot's {@link Slot#getColor()} and {@link Slot#getDarkColor()} for two color tinting. */
+	static public class RGBA2Timeline extends CurveTimeline implements SlotTimeline {
+		static public final int ENTRIES = 8;
+		static private final int R = 1, G = 2, B = 3, A = 4, R2 = 5, G2 = 6, B2 = 7;
+
+		final int slotIndex;
+
+		public RGBA2Timeline (int frameCount, int bezierCount, int slotIndex) {
+			super(frameCount, bezierCount, //
+				Property.rgb.ordinal() + "|" + slotIndex, //
+				Property.alpha.ordinal() + "|" + slotIndex, //
+				Property.rgb2.ordinal() + "|" + slotIndex);
+			this.slotIndex = slotIndex;
+		}
+
+		public int getFrameEntries () {
+			return ENTRIES;
+		}
+
+		/** The index of the slot in {@link Skeleton#getSlots()} that will be changed when this timeline is applied. The
+		 * {@link Slot#getDarkColor()} must not be null. */
+		public int getSlotIndex () {
+			return slotIndex;
+		}
+
+		/** Sets the time, light color, and dark color for the specified frame.
+		 * @param frame Between 0 and <code>frameCount</code>, inclusive.
+		 * @param time The frame time in seconds. */
+		public void setFrame (int frame, float time, float r, float g, float b, float a, float r2, float g2, float b2) {
+			frame <<= 3;
+			frames[frame] = time;
+			frames[frame + R] = r;
+			frames[frame + G] = g;
+			frames[frame + B] = b;
+			frames[frame + A] = a;
+			frames[frame + R2] = r2;
+			frames[frame + G2] = g2;
+			frames[frame + B2] = b2;
+		}
+
+		public void apply (Skeleton skeleton, float lastTime, float time, @Null Array<Event> events, float alpha, MixBlend blend,
+			MixDirection direction) {
+
+			Slot slot = skeleton.slots.get(slotIndex);
+			if (!slot.bone.active) return;
+
+			float[] frames = this.frames;
+			Color light = slot.color, dark = slot.darkColor;
+			if (time < frames[0]) { // Time is before first frame.
+				Color setupLight = slot.data.color, setupDark = slot.data.darkColor;
+				switch (blend) {
+				case setup:
+					light.set(setupLight);
+					dark.r = setupDark.r;
+					dark.g = setupDark.g;
+					dark.b = setupDark.b;
+					return;
+				case first:
+					light.add((setupLight.r - light.r) * alpha, (setupLight.g - light.g) * alpha, (setupLight.b - light.b) * alpha,
+						(setupLight.a - light.a) * alpha);
+					dark.r += (setupDark.r - dark.r) * alpha;
+					dark.g += (setupDark.g - dark.g) * alpha;
+					dark.b += (setupDark.b - dark.b) * alpha;
+				}
+				return;
+			}
+
+			float r, g, b, a, r2, g2, b2;
+			int i = search(frames, time, ENTRIES), curveType = (int)curves[i >> 3];
+			switch (curveType) {
+			case LINEAR:
+				float before = frames[i];
+				r = frames[i + R];
+				g = frames[i + G];
+				b = frames[i + B];
+				a = frames[i + A];
+				r2 = frames[i + R2];
+				g2 = frames[i + G2];
+				b2 = frames[i + B2];
+				float t = (time - before) / (frames[i + ENTRIES] - before);
+				r += (frames[i + ENTRIES + R] - r) * t;
+				g += (frames[i + ENTRIES + G] - g) * t;
+				b += (frames[i + ENTRIES + B] - b) * t;
+				a += (frames[i + ENTRIES + A] - a) * t;
+				r2 += (frames[i + ENTRIES + R2] - r2) * t;
+				g2 += (frames[i + ENTRIES + G2] - g2) * t;
+				b2 += (frames[i + ENTRIES + B2] - b2) * t;
+				break;
+			case STEPPED:
+				r = frames[i + R];
+				g = frames[i + G];
+				b = frames[i + B];
+				a = frames[i + A];
+				r2 = frames[i + R2];
+				g2 = frames[i + G2];
+				b2 = frames[i + B2];
+				break;
+			default:
+				r = getBezierValue(time, i, R, curveType - BEZIER);
+				g = getBezierValue(time, i, G, curveType + BEZIER_SIZE - BEZIER);
+				b = getBezierValue(time, i, B, curveType + BEZIER_SIZE * 2 - BEZIER);
+				a = getBezierValue(time, i, A, curveType + BEZIER_SIZE * 3 - BEZIER);
+				r2 = getBezierValue(time, i, R2, curveType + BEZIER_SIZE * 4 - BEZIER);
+				g2 = getBezierValue(time, i, G2, curveType + BEZIER_SIZE * 5 - BEZIER);
+				b2 = getBezierValue(time, i, B2, curveType + BEZIER_SIZE * 6 - BEZIER);
+			}
+
+			if (alpha == 1) {
+				light.set(r, g, b, a);
+				dark.r = r2;
+				dark.g = g2;
+				dark.b = b2;
+			} else {
+				if (blend == setup) {
+					light.set(slot.data.color);
+					Color setupDark = slot.data.darkColor;
+					dark.r = setupDark.r;
+					dark.g = setupDark.g;
+					dark.b = setupDark.b;
+				}
+				light.add((r - light.r) * alpha, (g - light.g) * alpha, (b - light.b) * alpha, (a - light.a) * alpha);
+				dark.r += (r2 - dark.r) * alpha;
+				dark.g += (g2 - dark.g) * alpha;
+				dark.b += (b2 - dark.b) * alpha;
+			}
+		}
+	}
+
+	/** Changes the RGB for a slot's {@link Slot#getColor()} and {@link Slot#getDarkColor()} for two color tinting. */
+	static public class RGB2Timeline extends CurveTimeline implements SlotTimeline {
+		static public final int ENTRIES = 7;
+		static private final int R = 1, G = 2, B = 3, R2 = 4, G2 = 5, B2 = 6;
+
+		final int slotIndex;
+
+		public RGB2Timeline (int frameCount, int bezierCount, int slotIndex) {
+			super(frameCount, bezierCount, //
+				Property.rgb.ordinal() + "|" + slotIndex, //
+				Property.rgb2.ordinal() + "|" + slotIndex);
+			this.slotIndex = slotIndex;
+		}
+
+		public int getFrameEntries () {
+			return ENTRIES;
+		}
+
+		/** The index of the slot in {@link Skeleton#getSlots()} that will be changed when this timeline is applied. The
+		 * {@link Slot#getDarkColor()} must not be null. */
+		public int getSlotIndex () {
+			return slotIndex;
+		}
+
+		/** Sets the time, light color, and dark color for the specified frame.
+		 * @param frame Between 0 and <code>frameCount</code>, inclusive.
+		 * @param time The frame time in seconds. */
+		public void setFrame (int frame, float time, float r, float g, float b, float r2, float g2, float b2) {
+			frame *= ENTRIES;
+			frames[frame] = time;
+			frames[frame + R] = r;
+			frames[frame + G] = g;
+			frames[frame + B] = b;
+			frames[frame + R2] = r2;
+			frames[frame + G2] = g2;
+			frames[frame + B2] = b2;
+		}
+
+		public void apply (Skeleton skeleton, float lastTime, float time, @Null Array<Event> events, float alpha, MixBlend blend,
+			MixDirection direction) {
+
+			Slot slot = skeleton.slots.get(slotIndex);
+			if (!slot.bone.active) return;
+
+			float[] frames = this.frames;
+			Color light = slot.color, dark = slot.darkColor;
+			if (time < frames[0]) { // Time is before first frame.
+				Color setupLight = slot.data.color, setupDark = slot.data.darkColor;
+				switch (blend) {
+				case setup:
+					light.r = setupLight.r;
+					light.g = setupLight.g;
+					light.b = setupLight.b;
+					dark.r = setupDark.r;
+					dark.g = setupDark.g;
+					dark.b = setupDark.b;
+					return;
+				case first:
+					light.r += (setupLight.r - light.r) * alpha;
+					light.g += (setupLight.g - light.g) * alpha;
+					light.b += (setupLight.b - light.b) * alpha;
+					dark.r += (setupDark.r - dark.r) * alpha;
+					dark.g += (setupDark.g - dark.g) * alpha;
+					dark.b += (setupDark.b - dark.b) * alpha;
+				}
+				return;
+			}
+
+			float r, g, b, r2, g2, b2;
+			int i = search(frames, time, ENTRIES), curveType = (int)curves[i / ENTRIES];
+			switch (curveType) {
+			case LINEAR:
+				float before = frames[i];
+				r = frames[i + R];
+				g = frames[i + G];
+				b = frames[i + B];
+				r2 = frames[i + R2];
+				g2 = frames[i + G2];
+				b2 = frames[i + B2];
+				float t = (time - before) / (frames[i + ENTRIES] - before);
+				r += (frames[i + ENTRIES + R] - r) * t;
+				g += (frames[i + ENTRIES + G] - g) * t;
+				b += (frames[i + ENTRIES + B] - b) * t;
+				r2 += (frames[i + ENTRIES + R2] - r2) * t;
+				g2 += (frames[i + ENTRIES + G2] - g2) * t;
+				b2 += (frames[i + ENTRIES + B2] - b2) * t;
+				break;
+			case STEPPED:
+				r = frames[i + R];
+				g = frames[i + G];
+				b = frames[i + B];
+				r2 = frames[i + R2];
+				g2 = frames[i + G2];
+				b2 = frames[i + B2];
+				break;
+			default:
+				r = getBezierValue(time, i, R, curveType - BEZIER);
+				g = getBezierValue(time, i, G, curveType + BEZIER_SIZE - BEZIER);
+				b = getBezierValue(time, i, B, curveType + BEZIER_SIZE * 2 - BEZIER);
+				r2 = getBezierValue(time, i, R2, curveType + BEZIER_SIZE * 3 - BEZIER);
+				g2 = getBezierValue(time, i, G2, curveType + BEZIER_SIZE * 4 - BEZIER);
+				b2 = getBezierValue(time, i, B2, curveType + BEZIER_SIZE * 5 - BEZIER);
+			}
+
+			if (alpha == 1) {
+				light.r = r;
+				light.g = g;
+				light.b = b;
+				dark.r = r2;
+				dark.g = g2;
+				dark.b = b2;
+			} else {
+				if (blend == setup) {
+					Color setupLight = slot.data.color, setupDark = slot.data.darkColor;
+					light.r = setupLight.r;
+					light.g = setupLight.g;
+					light.b = setupLight.b;
+					dark.r = setupDark.r;
+					dark.g = setupDark.g;
+					dark.b = setupDark.b;
+				}
+				light.r += (r - light.r) * alpha;
+				light.g += (g - light.g) * alpha;
+				light.b += (b - light.b) * alpha;
+				dark.r += (r2 - dark.r) * alpha;
+				dark.g += (g2 - dark.g) * alpha;
+				dark.b += (b2 - dark.b) * alpha;
+			}
+		}
+	}
+
+	/** Changes a slot's {@link Slot#getAttachment()}. */
+	static public class AttachmentTimeline extends Timeline implements SlotTimeline {
+		final int slotIndex;
+		final String[] attachmentNames;
+
+		public AttachmentTimeline (int frameCount, int slotIndex) {
+			super(frameCount, Property.attachment.ordinal() + "|" + slotIndex);
+			this.slotIndex = slotIndex;
+			attachmentNames = new String[frameCount];
+		}
+
+		public int getFrameCount () {
+			return frames.length;
+		}
+
+		public int getSlotIndex () {
+			return slotIndex;
+		}
+
+		/** The attachment name for each frame. May contain null values to clear the attachment. */
+		public String[] getAttachmentNames () {
+			return attachmentNames;
+		}
+
+		/** Sets the time and attachment name for the specified frame.
+		 * @param frame Between 0 and <code>frameCount</code>, inclusive.
+		 * @param time The frame time in seconds. */
+		public void setFrame (int frame, float time, String attachmentName) {
+			frames[frame] = time;
+			attachmentNames[frame] = attachmentName;
+		}
+
+		public void apply (Skeleton skeleton, float lastTime, float time, @Null Array<Event> events, float alpha, MixBlend blend,
+			MixDirection direction) {
+
+			Slot slot = skeleton.slots.get(slotIndex);
+			if (!slot.bone.active) return;
+
+			if (direction == out) {
+				if (blend == setup) setAttachment(skeleton, slot, slot.data.attachmentName);
+				return;
+			}
+
+			if (time < this.frames[0]) { // Time is before first frame.
+				if (blend == setup || blend == first) setAttachment(skeleton, slot, slot.data.attachmentName);
+				return;
+			}
+
+			setAttachment(skeleton, slot, attachmentNames[search(this.frames, time)]);
+		}
+
+		private void setAttachment (Skeleton skeleton, Slot slot, String attachmentName) {
+			slot.setAttachment(attachmentName == null ? null : skeleton.getAttachment(slotIndex, attachmentName));
+		}
+	}
+
+	/** Changes a slot's {@link Slot#getDeform()} to deform a {@link VertexAttachment}. */
+	static public class DeformTimeline extends CurveTimeline implements SlotTimeline {
+		final int slotIndex;
+		final VertexAttachment attachment;
+		private final float[][] vertices;
+
+		public DeformTimeline (int frameCount, int bezierCount, int slotIndex, VertexAttachment attachment) {
+			super(frameCount, bezierCount, Property.deform.ordinal() + "|" + slotIndex + "|" + attachment.getId());
+			this.slotIndex = slotIndex;
+			this.attachment = attachment;
+			vertices = new float[frameCount][];
+		}
+
+		public int getFrameCount () {
+			return frames.length;
+		}
+
+		public int getSlotIndex () {
+			return slotIndex;
+		}
+
+		/** The attachment that will be deformed.
+		 * <p>
+		 * See {@link VertexAttachment#getDeformAttachment()}. */
+		public VertexAttachment getAttachment () {
+			return attachment;
+		}
+
+		/** The vertices for each frame. */
+		public float[][] getVertices () {
+			return vertices;
+		}
+
+		/** Sets the time and vertices for the specified frame.
+		 * @param frame Between 0 and <code>frameCount</code>, inclusive.
+		 * @param time The frame time in seconds.
+		 * @param vertices Vertex positions for an unweighted VertexAttachment, or deform offsets if it has weights. */
+		public void setFrame (int frame, float time, float[] vertices) {
+			frames[frame] = time;
+			this.vertices[frame] = vertices;
+		}
+
+		/** @param value1 Ignored (0 is used for a deform timeline).
+		 * @param value2 Ignored (1 is used for a deform timeline). */
+		public void setBezier (int bezier, int frame, int value, float time1, float value1, float cx1, float cy1, float cx2,
+			float cy2, float time2, float value2) {
+			float[] curves = this.curves;
+			int i = getFrameCount() + bezier * BEZIER_SIZE;
+			if (value == 0) curves[frame] = BEZIER + i;
+			float tmpx = (time1 - cx1 * 2 + cx2) * 0.03f, tmpy = cy2 * 0.03f - cy1 * 0.06f;
+			float dddx = ((cx1 - cx2) * 3 - time1 + time2) * 0.006f, dddy = (cy1 - cy2 + 0.33333333f) * 0.018f;
+			float ddx = tmpx * 2 + dddx, ddy = tmpy * 2 + dddy;
+			float dx = (cx1 - time1) * 0.3f + tmpx + dddx * 0.16666667f, dy = cy1 * 0.3f + tmpy + dddy * 0.16666667f;
+			float x = time1 + dx, y = dy;
+			for (int n = i + BEZIER_SIZE; i < n; i += 2) {
+				curves[i] = x;
+				curves[i + 1] = y;
+				dx += ddx;
+				dy += ddy;
+				ddx += dddx;
+				ddy += dddy;
+				x += dx;
+				y += dy;
+			}
+		}
+
+		/** Returns the interpolated percentage for the specified time.
+		 * @param frame The frame before <code>time</code>. */
+		private float getCurvePercent (float time, int frame) {
+			float[] curves = this.curves;
+			int i = (int)curves[frame];
+			switch (i) {
+			case LINEAR:
+				float x = frames[frame];
+				return (time - x) / (frames[frame + getFrameEntries()] - x);
+			case STEPPED:
+				return 0;
+			}
+			i -= BEZIER;
+			if (curves[i] > time) {
+				float x = frames[frame];
+				return curves[i + 1] * (time - x) / (curves[i] - x);
+			}
+			int n = i + BEZIER_SIZE;
+			for (i += 2; i < n; i += 2) {
+				if (curves[i] >= time) {
+					float x = curves[i - 2], y = curves[i - 1];
+					return y + (time - x) / (curves[i] - x) * (curves[i + 1] - y);
+				}
+			}
+			float x = curves[n - 2], y = curves[n - 1];
+			return y + (1 - y) * (time - x) / (frames[frame + getFrameEntries()] - x);
+		}
+
+		public void apply (Skeleton skeleton, float lastTime, float time, @Null Array<Event> events, float alpha, MixBlend blend,
+			MixDirection direction) {
+
+			Slot slot = skeleton.slots.get(slotIndex);
+			if (!slot.bone.active) return;
+			Attachment slotAttachment = slot.attachment;
+			if (!(slotAttachment instanceof VertexAttachment)
+				|| ((VertexAttachment)slotAttachment).getDeformAttachment() != attachment) return;
+
+			FloatArray deformArray = slot.getDeform();
+			if (deformArray.size == 0) blend = setup;
+
+			float[][] vertices = this.vertices;
+			int vertexCount = vertices[0].length;
+
+			float[] frames = this.frames;
+			if (time < frames[0]) { // Time is before first frame.
+				VertexAttachment vertexAttachment = (VertexAttachment)slotAttachment;
+				switch (blend) {
+				case setup:
+					deformArray.clear();
+					return;
+				case first:
+					if (alpha == 1) {
+						deformArray.clear();
+						return;
+					}
+					float[] deform = deformArray.setSize(vertexCount);
+					if (vertexAttachment.getBones() == null) {
+						// Unweighted vertex positions.
+						float[] setupVertices = vertexAttachment.getVertices();
+						for (int i = 0; i < vertexCount; i++)
+							deform[i] += (setupVertices[i] - deform[i]) * alpha;
+					} else {
+						// Weighted deform offsets.
+						alpha = 1 - alpha;
+						for (int i = 0; i < vertexCount; i++)
+							deform[i] *= alpha;
+					}
+				}
+				return;
+			}
+
+			float[] deform = deformArray.setSize(vertexCount);
+
+			if (time >= frames[frames.length - 1]) { // Time is after last frame.
+				float[] lastVertices = vertices[frames.length - 1];
+				if (alpha == 1) {
+					if (blend == add) {
+						VertexAttachment vertexAttachment = (VertexAttachment)slotAttachment;
+						if (vertexAttachment.getBones() == null) {
+							// Unweighted vertex positions, no alpha.
+							float[] setupVertices = vertexAttachment.getVertices();
+							for (int i = 0; i < vertexCount; i++)
+								deform[i] += lastVertices[i] - setupVertices[i];
+						} else {
+							// Weighted deform offsets, no alpha.
+							for (int i = 0; i < vertexCount; i++)
+								deform[i] += lastVertices[i];
+						}
+					} else {
+						// Vertex positions or deform offsets, no alpha.
+						arraycopy(lastVertices, 0, deform, 0, vertexCount);
+					}
+				} else {
+					switch (blend) {
+					case setup: {
+						VertexAttachment vertexAttachment = (VertexAttachment)slotAttachment;
+						if (vertexAttachment.getBones() == null) {
+							// Unweighted vertex positions, with alpha.
+							float[] setupVertices = vertexAttachment.getVertices();
+							for (int i = 0; i < vertexCount; i++) {
+								float setup = setupVertices[i];
+								deform[i] = setup + (lastVertices[i] - setup) * alpha;
+							}
+						} else {
+							// Weighted deform offsets, with alpha.
+							for (int i = 0; i < vertexCount; i++)
+								deform[i] = lastVertices[i] * alpha;
+						}
+						break;
+					}
+					case first:
+					case replace:
+						// Vertex positions or deform offsets, with alpha.
+						for (int i = 0; i < vertexCount; i++)
+							deform[i] += (lastVertices[i] - deform[i]) * alpha;
+						break;
+					case add:
+						VertexAttachment vertexAttachment = (VertexAttachment)slotAttachment;
+						if (vertexAttachment.getBones() == null) {
+							// Unweighted vertex positions, no alpha.
+							float[] setupVertices = vertexAttachment.getVertices();
+							for (int i = 0; i < vertexCount; i++)
+								deform[i] += (lastVertices[i] - setupVertices[i]) * alpha;
+						} else {
+							// Weighted deform offsets, alpha.
+							for (int i = 0; i < vertexCount; i++)
+								deform[i] += lastVertices[i] * alpha;
+						}
+					}
+				}
+				return;
+			}
+
+			int frame = search(frames, time);
+			float percent = getCurvePercent(time, frame);
+			float[] prevVertices = vertices[frame];
+			float[] nextVertices = vertices[frame + 1];
+
+			if (alpha == 1) {
+				if (blend == add) {
+					VertexAttachment vertexAttachment = (VertexAttachment)slotAttachment;
+					if (vertexAttachment.getBones() == null) {
+						// Unweighted vertex positions, no alpha.
+						float[] setupVertices = vertexAttachment.getVertices();
+						for (int i = 0; i < vertexCount; i++) {
+							float prev = prevVertices[i];
+							deform[i] += prev + (nextVertices[i] - prev) * percent - setupVertices[i];
+						}
+					} else {
+						// Weighted deform offsets, no alpha.
+						for (int i = 0; i < vertexCount; i++) {
+							float prev = prevVertices[i];
+							deform[i] += prev + (nextVertices[i] - prev) * percent;
+						}
+					}
+				} else {
+					// Vertex positions or deform offsets, no alpha.
+					for (int i = 0; i < vertexCount; i++) {
+						float prev = prevVertices[i];
+						deform[i] = prev + (nextVertices[i] - prev) * percent;
+					}
+				}
+			} else {
+				switch (blend) {
+				case setup: {
+					VertexAttachment vertexAttachment = (VertexAttachment)slotAttachment;
+					if (vertexAttachment.getBones() == null) {
+						// Unweighted vertex positions, with alpha.
+						float[] setupVertices = vertexAttachment.getVertices();
+						for (int i = 0; i < vertexCount; i++) {
+							float prev = prevVertices[i], setup = setupVertices[i];
+							deform[i] = setup + (prev + (nextVertices[i] - prev) * percent - setup) * alpha;
+						}
+					} else {
+						// Weighted deform offsets, with alpha.
+						for (int i = 0; i < vertexCount; i++) {
+							float prev = prevVertices[i];
+							deform[i] = (prev + (nextVertices[i] - prev) * percent) * alpha;
+						}
+					}
+					break;
+				}
+				case first:
+				case replace:
+					// Vertex positions or deform offsets, with alpha.
+					for (int i = 0; i < vertexCount; i++) {
+						float prev = prevVertices[i];
+						deform[i] += (prev + (nextVertices[i] - prev) * percent - deform[i]) * alpha;
+					}
+					break;
+				case add:
+					VertexAttachment vertexAttachment = (VertexAttachment)slotAttachment;
+					if (vertexAttachment.getBones() == null) {
+						// Unweighted vertex positions, with alpha.
+						float[] setupVertices = vertexAttachment.getVertices();
+						for (int i = 0; i < vertexCount; i++) {
+							float prev = prevVertices[i];
+							deform[i] += (prev + (nextVertices[i] - prev) * percent - setupVertices[i]) * alpha;
+						}
+					} else {
+						// Weighted deform offsets, with alpha.
+						for (int i = 0; i < vertexCount; i++) {
+							float prev = prevVertices[i];
+							deform[i] += (prev + (nextVertices[i] - prev) * percent) * alpha;
+						}
+					}
+				}
+			}
+		}
+	}
+
+	/** Fires an {@link Event} when specific animation times are reached. */
+	static public class EventTimeline extends Timeline {
+		static private final String[] propertyIds = {Integer.toString(Property.event.ordinal())};
+
+		private final Event[] events;
+
+		public EventTimeline (int frameCount) {
+			super(frameCount, propertyIds);
+			events = new Event[frameCount];
+		}
+
+		public int getFrameCount () {
+			return frames.length;
+		}
+
+		/** The event for each frame. */
+		public Event[] getEvents () {
+			return events;
+		}
+
+		/** Sets the time and event for the specified frame.
+		 * @param frame Between 0 and <code>frameCount</code>, inclusive. */
+		public void setFrame (int frame, Event event) {
+			frames[frame] = event.time;
+			events[frame] = event;
+		}
+
+		/** Fires events for frames > <code>lastTime</code> and <= <code>time</code>. */
+		public void apply (Skeleton skeleton, float lastTime, float time, @Null Array<Event> firedEvents, float alpha,
+			MixBlend blend, MixDirection direction) {
+
+			if (firedEvents == null) return;
+
+			float[] frames = this.frames;
+			int frameCount = frames.length;
+
+			if (lastTime > time) { // Fire events after last time for looped animations.
+				apply(skeleton, lastTime, Integer.MAX_VALUE, firedEvents, alpha, blend, direction);
+				lastTime = -1f;
+			} else if (lastTime >= frames[frameCount - 1]) // Last time is after last frame.
+				return;
+			if (time < frames[0]) return; // Time is before first frame.
+
+			int i;
+			if (lastTime < frames[0])
+				i = 0;
+			else {
+				i = search(frames, lastTime) + 1;
+				float frameTime = frames[i];
+				while (i > 0) { // Fire multiple events with the same frame.
+					if (frames[i - 1] != frameTime) break;
+					i--;
+				}
+			}
+			for (; i < frameCount && time >= frames[i]; i++)
+				firedEvents.add(events[i]);
+		}
+	}
+
+	/** Changes a skeleton's {@link Skeleton#getDrawOrder()}. */
+	static public class DrawOrderTimeline extends Timeline {
+		static private final String[] propertyIds = {Integer.toString(Property.drawOrder.ordinal())};
+
+		private final int[][] drawOrders;
+
+		public DrawOrderTimeline (int frameCount) {
+			super(frameCount, propertyIds);
+			drawOrders = new int[frameCount][];
+		}
+
+		public int getFrameCount () {
+			return frames.length;
+		}
+
+		/** The draw order for each frame. See {@link #setFrame(int, float, int[])}. */
+		public int[][] getDrawOrders () {
+			return drawOrders;
+		}
+
+		/** Sets the time and draw order for the specified frame.
+		 * @param frame Between 0 and <code>frameCount</code>, inclusive.
+		 * @param time The frame time in seconds.
+		 * @param drawOrder For each slot in {@link Skeleton#slots}, the index of the slot in the new draw order. May be null to use
+		 *           setup pose draw order. */
+		public void setFrame (int frame, float time, @Null int[] drawOrder) {
+			frames[frame] = time;
+			drawOrders[frame] = drawOrder;
+		}
+
+		public void apply (Skeleton skeleton, float lastTime, float time, @Null Array<Event> events, float alpha, MixBlend blend,
+			MixDirection direction) {
+
+			if (direction == out) {
+				if (blend == setup) arraycopy(skeleton.slots.items, 0, skeleton.drawOrder.items, 0, skeleton.slots.size);
+				return;
+			}
+
+			if (time < frames[0]) { // Time is before first frame.
+				if (blend == setup || blend == first)
+					arraycopy(skeleton.slots.items, 0, skeleton.drawOrder.items, 0, skeleton.slots.size);
+				return;
+			}
+
+			int[] drawOrderToSetupIndex = drawOrders[search(frames, time)];
+			if (drawOrderToSetupIndex == null)
+				arraycopy(skeleton.slots.items, 0, skeleton.drawOrder.items, 0, skeleton.slots.size);
+			else {
+				Object[] slots = skeleton.slots.items;
+				Object[] drawOrder = skeleton.drawOrder.items;
+				for (int i = 0, n = drawOrderToSetupIndex.length; i < n; i++)
+					drawOrder[i] = slots[drawOrderToSetupIndex[i]];
+			}
+		}
+	}
+
+	/** Changes an IK constraint's {@link IkConstraint#getMix()}, {@link IkConstraint#getSoftness()},
+	 * {@link IkConstraint#getBendDirection()}, {@link IkConstraint#getStretch()}, and {@link IkConstraint#getCompress()}. */
+	static public class IkConstraintTimeline extends CurveTimeline {
+		static public final int ENTRIES = 6;
+		static private final int MIX = 1, SOFTNESS = 2, BEND_DIRECTION = 3, COMPRESS = 4, STRETCH = 5;
+
+		final int ikConstraintIndex;
+
+		public IkConstraintTimeline (int frameCount, int bezierCount, int ikConstraintIndex) {
+			super(frameCount, bezierCount, Property.ikConstraint.ordinal() + "|" + ikConstraintIndex);
+			this.ikConstraintIndex = ikConstraintIndex;
+		}
+
+		public int getFrameEntries () {
+			return ENTRIES;
+		}
+
+		/** The index of the IK constraint slot in {@link Skeleton#getIkConstraints()} that will be changed when this timeline is
+		 * applied. */
+		public int getIkConstraintIndex () {
+			return ikConstraintIndex;
+		}
+
+		/** Sets the time, mix, softness, bend direction, compress, and stretch for the specified frame.
+		 * @param frame Between 0 and <code>frameCount</code>, inclusive.
+		 * @param time The frame time in seconds.
+		 * @param bendDirection 1 or -1. */
+		public void setFrame (int frame, float time, float mix, float softness, int bendDirection, boolean compress,
+			boolean stretch) {
+			frame *= ENTRIES;
+			frames[frame] = time;
+			frames[frame + MIX] = mix;
+			frames[frame + SOFTNESS] = softness;
+			frames[frame + BEND_DIRECTION] = bendDirection;
+			frames[frame + COMPRESS] = compress ? 1 : 0;
+			frames[frame + STRETCH] = stretch ? 1 : 0;
+		}
+
+		public void apply (Skeleton skeleton, float lastTime, float time, @Null Array<Event> events, float alpha, MixBlend blend,
+			MixDirection direction) {
+
+			IkConstraint constraint = skeleton.ikConstraints.get(ikConstraintIndex);
+			if (!constraint.active) return;
+
+			float[] frames = this.frames;
+			if (time < frames[0]) { // Time is before first frame.
+				switch (blend) {
+				case setup:
+					constraint.mix = constraint.data.mix;
+					constraint.softness = constraint.data.softness;
+					constraint.bendDirection = constraint.data.bendDirection;
+					constraint.compress = constraint.data.compress;
+					constraint.stretch = constraint.data.stretch;
+					return;
+				case first:
+					constraint.mix += (constraint.data.mix - constraint.mix) * alpha;
+					constraint.softness += (constraint.data.softness - constraint.softness) * alpha;
+					constraint.bendDirection = constraint.data.bendDirection;
+					constraint.compress = constraint.data.compress;
+					constraint.stretch = constraint.data.stretch;
+				}
+				return;
+			}
+
+			float mix, softness;
+			int i = search(frames, time, ENTRIES), curveType = (int)curves[i / ENTRIES];
+			switch (curveType) {
+			case LINEAR:
+				float before = frames[i];
+				mix = frames[i + MIX];
+				softness = frames[i + SOFTNESS];
+				float t = (time - before) / (frames[i + ENTRIES] - before);
+				mix += (frames[i + ENTRIES + MIX] - mix) * t;
+				softness += (frames[i + ENTRIES + SOFTNESS] - softness) * t;
+				break;
+			case STEPPED:
+				mix = frames[i + MIX];
+				softness = frames[i + SOFTNESS];
+				break;
+			default:
+				mix = getBezierValue(time, i, MIX, curveType - BEZIER);
+				softness = getBezierValue(time, i, SOFTNESS, curveType + BEZIER_SIZE - BEZIER);
+			}
+
+			if (blend == setup) {
+				constraint.mix = constraint.data.mix + (mix - constraint.data.mix) * alpha;
+				constraint.softness = constraint.data.softness + (softness - constraint.data.softness) * alpha;
+				if (direction == out) {
+					constraint.bendDirection = constraint.data.bendDirection;
+					constraint.compress = constraint.data.compress;
+					constraint.stretch = constraint.data.stretch;
+				} else {
+					constraint.bendDirection = (int)frames[i + BEND_DIRECTION];
+					constraint.compress = frames[i + COMPRESS] != 0;
+					constraint.stretch = frames[i + STRETCH] != 0;
+				}
+			} else {
+				constraint.mix += (mix - constraint.mix) * alpha;
+				constraint.softness += (softness - constraint.softness) * alpha;
+				if (direction == in) {
+					constraint.bendDirection = (int)frames[i + BEND_DIRECTION];
+					constraint.compress = frames[i + COMPRESS] != 0;
+					constraint.stretch = frames[i + STRETCH] != 0;
+				}
+			}
+		}
+	}
+
+	/** Changes a transform constraint's {@link TransformConstraint#getMixRotate()}, {@link TransformConstraint#getMixX()},
+	 * {@link TransformConstraint#getMixY()}, {@link TransformConstraint#getMixScaleX()},
+	 * {@link TransformConstraint#getMixScaleY()}, and {@link TransformConstraint#getMixShearY()}. */
+	static public class TransformConstraintTimeline extends CurveTimeline {
+		static public final int ENTRIES = 7;
+		static private final int ROTATE = 1, X = 2, Y = 3, SCALEX = 4, SCALEY = 5, SHEARY = 6;
+
+		final int transformConstraintIndex;
+
+		public TransformConstraintTimeline (int frameCount, int bezierCount, int transformConstraintIndex) {
+			super(frameCount, bezierCount, Property.transformConstraint.ordinal() + "|" + transformConstraintIndex);
+			this.transformConstraintIndex = transformConstraintIndex;
+		}
+
+		public int getFrameEntries () {
+			return ENTRIES;
+		}
+
+		/** The index of the transform constraint slot in {@link Skeleton#getTransformConstraints()} that will be changed when this
+		 * timeline is applied. */
+		public int getTransformConstraintIndex () {
+			return transformConstraintIndex;
+		}
+
+		/** Sets the time, rotate mix, translate mix, scale mix, and shear mix for the specified frame.
+		 * @param frame Between 0 and <code>frameCount</code>, inclusive.
+		 * @param time The frame time in seconds. */
+		public void setFrame (int frame, float time, float mixRotate, float mixX, float mixY, float mixScaleX, float mixScaleY,
+			float mixShearY) {
+			frame *= ENTRIES;
+			frames[frame] = time;
+			frames[frame + ROTATE] = mixRotate;
+			frames[frame + X] = mixX;
+			frames[frame + Y] = mixY;
+			frames[frame + SCALEX] = mixScaleX;
+			frames[frame + SCALEY] = mixScaleY;
+			frames[frame + SHEARY] = mixShearY;
+		}
+
+		public void apply (Skeleton skeleton, float lastTime, float time, @Null Array<Event> events, float alpha, MixBlend blend,
+			MixDirection direction) {
+
+			TransformConstraint constraint = skeleton.transformConstraints.get(transformConstraintIndex);
+			if (!constraint.active) return;
+
+			float[] frames = this.frames;
+			if (time < frames[0]) { // Time is before first frame.
+				TransformConstraintData data = constraint.data;
+				switch (blend) {
+				case setup:
+					constraint.mixRotate = data.mixRotate;
+					constraint.mixX = data.mixX;
+					constraint.mixY = data.mixY;
+					constraint.mixScaleX = data.mixScaleX;
+					constraint.mixScaleY = data.mixScaleY;
+					constraint.mixShearY = data.mixShearY;
+					return;
+				case first:
+					constraint.mixRotate += (data.mixRotate - constraint.mixRotate) * alpha;
+					constraint.mixX += (data.mixX - constraint.mixX) * alpha;
+					constraint.mixY += (data.mixY - constraint.mixY) * alpha;
+					constraint.mixScaleX += (data.mixScaleX - constraint.mixScaleX) * alpha;
+					constraint.mixScaleY += (data.mixScaleY - constraint.mixScaleY) * alpha;
+					constraint.mixShearY += (data.mixShearY - constraint.mixShearY) * alpha;
+				}
+				return;
+			}
+
+			float rotate, x, y, scaleX, scaleY, shearY;
+			int i = search(frames, time, ENTRIES), curveType = (int)curves[i / ENTRIES];
+			switch (curveType) {
+			case LINEAR:
+				float before = frames[i];
+				rotate = frames[i + ROTATE];
+				x = frames[i + X];
+				y = frames[i + Y];
+				scaleX = frames[i + SCALEX];
+				scaleY = frames[i + SCALEY];
+				shearY = frames[i + SHEARY];
+				float t = (time - before) / (frames[i + ENTRIES] - before);
+				rotate += (frames[i + ENTRIES + ROTATE] - rotate) * t;
+				x += (frames[i + ENTRIES + X] - x) * t;
+				y += (frames[i + ENTRIES + Y] - y) * t;
+				scaleX += (frames[i + ENTRIES + SCALEX] - scaleX) * t;
+				scaleY += (frames[i + ENTRIES + SCALEY] - scaleY) * t;
+				shearY += (frames[i + ENTRIES + SHEARY] - shearY) * t;
+				break;
+			case STEPPED:
+				rotate = frames[i + ROTATE];
+				x = frames[i + X];
+				y = frames[i + Y];
+				scaleX = frames[i + SCALEX];
+				scaleY = frames[i + SCALEY];
+				shearY = frames[i + SHEARY];
+				break;
+			default:
+				rotate = getBezierValue(time, i, ROTATE, curveType - BEZIER);
+				x = getBezierValue(time, i, X, curveType + BEZIER_SIZE - BEZIER);
+				y = getBezierValue(time, i, Y, curveType + BEZIER_SIZE * 2 - BEZIER);
+				scaleX = getBezierValue(time, i, SCALEX, curveType + BEZIER_SIZE * 3 - BEZIER);
+				scaleY = getBezierValue(time, i, SCALEY, curveType + BEZIER_SIZE * 4 - BEZIER);
+				shearY = getBezierValue(time, i, SHEARY, curveType + BEZIER_SIZE * 5 - BEZIER);
+			}
+
+			if (blend == setup) {
+				TransformConstraintData data = constraint.data;
+				constraint.mixRotate = data.mixRotate + (rotate - data.mixRotate) * alpha;
+				constraint.mixX = data.mixX + (x - data.mixX) * alpha;
+				constraint.mixY = data.mixY + (y - data.mixY) * alpha;
+				constraint.mixScaleX = data.mixScaleX + (scaleX - data.mixScaleX) * alpha;
+				constraint.mixScaleY = data.mixScaleY + (scaleY - data.mixScaleY) * alpha;
+				constraint.mixShearY = data.mixShearY + (shearY - data.mixShearY) * alpha;
+			} else {
+				constraint.mixRotate += (rotate - constraint.mixRotate) * alpha;
+				constraint.mixX += (x - constraint.mixX) * alpha;
+				constraint.mixY += (y - constraint.mixY) * alpha;
+				constraint.mixScaleX += (scaleX - constraint.mixScaleX) * alpha;
+				constraint.mixScaleY += (scaleY - constraint.mixScaleY) * alpha;
+				constraint.mixShearY += (shearY - constraint.mixShearY) * alpha;
+			}
+		}
+	}
+
+	/** Changes a path constraint's {@link PathConstraint#getPosition()}. */
+	static public class PathConstraintPositionTimeline extends CurveTimeline1 {
+		final int pathConstraintIndex;
+
+		public PathConstraintPositionTimeline (int frameCount, int bezierCount, int pathConstraintIndex) {
+			super(frameCount, bezierCount, Property.pathConstraintPosition.ordinal() + "|" + pathConstraintIndex);
+			this.pathConstraintIndex = pathConstraintIndex;
+		}
+
+		/** The index of the path constraint slot in {@link Skeleton#getPathConstraints()} that will be changed when this timeline
+		 * is applied. */
+		public int getPathConstraintIndex () {
+			return pathConstraintIndex;
+		}
+
+		public void apply (Skeleton skeleton, float lastTime, float time, @Null Array<Event> events, float alpha, MixBlend blend,
+			MixDirection direction) {
+
+			PathConstraint constraint = skeleton.pathConstraints.get(pathConstraintIndex);
+			if (!constraint.active) return;
+
+			if (time < frames[0]) { // Time is before first frame.
+				switch (blend) {
+				case setup:
+					constraint.position = constraint.data.position;
+					return;
+				case first:
+					constraint.position += (constraint.data.position - constraint.position) * alpha;
+				}
+				return;
+			}
+
+			float position = getCurveValue(time);
+			if (blend == setup)
+				constraint.position = constraint.data.position + (position - constraint.data.position) * alpha;
+			else
+				constraint.position += (position - constraint.position) * alpha;
+		}
+	}
+
+	/** Changes a path constraint's {@link PathConstraint#getSpacing()}. */
+	static public class PathConstraintSpacingTimeline extends CurveTimeline1 {
+		final int pathConstraintIndex;
+
+		public PathConstraintSpacingTimeline (int frameCount, int bezierCount, int pathConstraintIndex) {
+			super(frameCount, bezierCount, Property.pathConstraintSpacing.ordinal() + "|" + pathConstraintIndex);
+			this.pathConstraintIndex = pathConstraintIndex;
+		}
+
+		/** The index of the path constraint slot in {@link Skeleton#getPathConstraints()} that will be changed when this timeline
+		 * is applied. */
+		public int getPathConstraintIndex () {
+			return pathConstraintIndex;
+		}
+
+		public void apply (Skeleton skeleton, float lastTime, float time, @Null Array<Event> events, float alpha, MixBlend blend,
+			MixDirection direction) {
+
+			PathConstraint constraint = skeleton.pathConstraints.get(pathConstraintIndex);
+			if (!constraint.active) return;
+
+			if (time < frames[0]) { // Time is before first frame.
+				switch (blend) {
+				case setup:
+					constraint.spacing = constraint.data.spacing;
+					return;
+				case first:
+					constraint.spacing += (constraint.data.spacing - constraint.spacing) * alpha;
+				}
+				return;
+			}
+
+			float spacing = getCurveValue(time);
+			if (blend == setup)
+				constraint.spacing = constraint.data.spacing + (spacing - constraint.data.spacing) * alpha;
+			else
+				constraint.spacing += (spacing - constraint.spacing) * alpha;
+		}
+	}
+
+	/** Changes a transform constraint's {@link PathConstraint#getMixRotate()}, {@link PathConstraint#getMixX()}, and
+	 * {@link PathConstraint#getMixY()}. */
+	static public class PathConstraintMixTimeline extends CurveTimeline {
+		static public final int ENTRIES = 4;
+		static private final int ROTATE = 1, X = 2, Y = 3;
+
+		final int pathConstraintIndex;
+
+		public PathConstraintMixTimeline (int frameCount, int bezierCount, int pathConstraintIndex) {
+			super(frameCount, bezierCount, Property.pathConstraintMix.ordinal() + "|" + pathConstraintIndex);
+			this.pathConstraintIndex = pathConstraintIndex;
+		}
+
+		public int getFrameEntries () {
+			return ENTRIES;
+		}
+
+		/** The index of the path constraint slot in {@link Skeleton#getPathConstraints()} that will be changed when this timeline
+		 * is applied. */
+		public int getPathConstraintIndex () {
+			return pathConstraintIndex;
+		}
+
+		/** Sets the time and color for the specified frame.
+		 * @param frame Between 0 and <code>frameCount</code>, inclusive.
+		 * @param time The frame time in seconds. */
+		public void setFrame (int frame, float time, float mixRotate, float mixX, float mixY) {
+			frame <<= 2;
+			frames[frame] = time;
+			frames[frame + ROTATE] = mixRotate;
+			frames[frame + X] = mixX;
+			frames[frame + Y] = mixY;
+		}
+
+		public void apply (Skeleton skeleton, float lastTime, float time, @Null Array<Event> events, float alpha, MixBlend blend,
+			MixDirection direction) {
+
+			PathConstraint constraint = skeleton.pathConstraints.get(pathConstraintIndex);
+			if (!constraint.active) return;
+
+			float[] frames = this.frames;
+			if (time < frames[0]) { // Time is before first frame.
+				PathConstraintData data = constraint.data;
+				switch (blend) {
+				case setup:
+					constraint.mixRotate = data.mixRotate;
+					constraint.mixX = data.mixX;
+					constraint.mixY = data.mixY;
+					return;
+				case first:
+					constraint.mixRotate += (data.mixRotate - constraint.mixRotate) * alpha;
+					constraint.mixX += (data.mixX - constraint.mixX) * alpha;
+					constraint.mixY += (data.mixY - constraint.mixY) * alpha;
+				}
+				return;
+			}
+
+			float rotate, x, y;
+			int i = search(frames, time, ENTRIES), curveType = (int)curves[i >> 2];
+			switch (curveType) {
+			case LINEAR:
+				float before = frames[i];
+				rotate = frames[i + ROTATE];
+				x = frames[i + X];
+				y = frames[i + Y];
+				float t = (time - before) / (frames[i + ENTRIES] - before);
+				rotate += (frames[i + ENTRIES + ROTATE] - rotate) * t;
+				x += (frames[i + ENTRIES + X] - x) * t;
+				y += (frames[i + ENTRIES + Y] - y) * t;
+				break;
+			case STEPPED:
+				rotate = frames[i + ROTATE];
+				x = frames[i + X];
+				y = frames[i + Y];
+				break;
+			default:
+				rotate = getBezierValue(time, i, ROTATE, curveType - BEZIER);
+				x = getBezierValue(time, i, X, curveType + BEZIER_SIZE - BEZIER);
+				y = getBezierValue(time, i, Y, curveType + BEZIER_SIZE * 2 - BEZIER);
+			}
+
+			if (blend == setup) {
+				PathConstraintData data = constraint.data;
+				constraint.mixRotate = data.mixRotate + (rotate - data.mixRotate) * alpha;
+				constraint.mixX = data.mixX + (x - data.mixX) * alpha;
+				constraint.mixY = data.mixY + (y - data.mixY) * alpha;
+			} else {
+				constraint.mixRotate += (rotate - constraint.mixRotate) * alpha;
+				constraint.mixX += (x - constraint.mixX) * alpha;
+				constraint.mixY += (y - constraint.mixY) * alpha;
+			}
+		}
+	}
+}

+ 1376 - 1376
spine-libgdx/spine-libgdx/src/com/esotericsoftware/spine/AnimationState.java

@@ -1,1376 +1,1376 @@
-/******************************************************************************
- * Spine Runtimes License Agreement
- * Last updated January 1, 2020. Replaces all prior versions.
- *
- * Copyright (c) 2013-2020, Esoteric Software LLC
- *
- * Integration of the Spine Runtimes into software or otherwise creating
- * derivative works of the Spine Runtimes is permitted under the terms and
- * conditions of Section 2 of the Spine Editor License Agreement:
- * http://esotericsoftware.com/spine-editor-license
- *
- * Otherwise, it is permitted to integrate the Spine Runtimes into software
- * or otherwise create derivative works of the Spine Runtimes (collectively,
- * "Products"), provided that each user of the Products must obtain their own
- * Spine Editor license and redistribution of the Products in any form must
- * include this license and copyright notice.
- *
- * THE SPINE RUNTIMES ARE PROVIDED BY ESOTERIC SOFTWARE LLC "AS IS" AND ANY
- * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
- * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
- * DISCLAIMED. IN NO EVENT SHALL ESOTERIC SOFTWARE LLC BE LIABLE FOR ANY
- * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
- * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES,
- * BUSINESS INTERRUPTION, OR LOSS OF USE, DATA, OR PROFITS) HOWEVER CAUSED AND
- * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
- * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
- * THE SPINE RUNTIMES, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
- *****************************************************************************/
-
-package com.esotericsoftware.spine;
-
-import com.badlogic.gdx.utils.Array;
-import com.badlogic.gdx.utils.FloatArray;
-import com.badlogic.gdx.utils.IntArray;
-import com.badlogic.gdx.utils.Null;
-import com.badlogic.gdx.utils.ObjectSet;
-import com.badlogic.gdx.utils.Pool;
-import com.badlogic.gdx.utils.Pool.Poolable;
-import com.badlogic.gdx.utils.SnapshotArray;
-
-import com.esotericsoftware.spine.Animation.AttachmentTimeline;
-import com.esotericsoftware.spine.Animation.DrawOrderTimeline;
-import com.esotericsoftware.spine.Animation.EventTimeline;
-import com.esotericsoftware.spine.Animation.MixBlend;
-import com.esotericsoftware.spine.Animation.MixDirection;
-import com.esotericsoftware.spine.Animation.RotateTimeline;
-import com.esotericsoftware.spine.Animation.Timeline;
-
-/** Applies animations over time, queues animations for later playback, mixes (crossfading) between animations, and applies
- * multiple animations on top of each other (layering).
- * <p>
- * See <a href='http://esotericsoftware.com/spine-applying-animations/'>Applying Animations</a> in the Spine Runtimes Guide. */
-public class AnimationState {
-	static private final Animation emptyAnimation = new Animation("<empty>", new Array(0), 0);
-
-	/** 1) A previously applied timeline has set this property.<br>
-	 * Result: Mix from the current pose to the timeline pose. */
-	static private final int SUBSEQUENT = 0;
-	/** 1) This is the first timeline to set this property.<br>
-	 * 2) The next track entry applied after this one does not have a timeline to set this property.<br>
-	 * Result: Mix from the setup pose to the timeline pose. */
-	static private final int FIRST = 1;
-	/** 1) A previously applied timeline has set this property.<br>
-	 * 2) The next track entry to be applied does have a timeline to set this property.<br>
-	 * 3) The next track entry after that one does not have a timeline to set this property.<br>
-	 * Result: Mix from the current pose to the timeline pose, but do not mix out. This avoids "dipping" when crossfading
-	 * animations that key the same property. A subsequent timeline will set this property using a mix. */
-	static private final int HOLD_SUBSEQUENT = 2;
-	/** 1) This is the first timeline to set this property.<br>
-	 * 2) The next track entry to be applied does have a timeline to set this property.<br>
-	 * 3) The next track entry after that one does not have a timeline to set this property.<br>
-	 * Result: Mix from the setup pose to the timeline pose, but do not mix out. This avoids "dipping" when crossfading animations
-	 * that key the same property. A subsequent timeline will set this property using a mix. */
-	static private final int HOLD_FIRST = 3;
-	/** 1) This is the first timeline to set this property.<br>
-	 * 2) The next track entry to be applied does have a timeline to set this property.<br>
-	 * 3) The next track entry after that one does have a timeline to set this property.<br>
-	 * 4) timelineHoldMix stores the first subsequent track entry that does not have a timeline to set this property.<br>
-	 * Result: The same as HOLD except the mix percentage from the timelineHoldMix track entry is used. This handles when more than
-	 * 2 track entries in a row have a timeline that sets the same property.<br>
-	 * Eg, A -> B -> C -> D where A, B, and C have a timeline setting same property, but D does not. When A is applied, to avoid
-	 * "dipping" A is not mixed out, however D (the first entry that doesn't set the property) mixing in is used to mix out A
-	 * (which affects B and C). Without using D to mix out, A would be applied fully until mixing completes, then snap to the mixed
-	 * out position. */
-	static private final int HOLD_MIX = 4;
-
-	static private final int SETUP = 1, CURRENT = 2;
-
-	private AnimationStateData data;
-	final Array<TrackEntry> tracks = new Array();
-	private final Array<Event> events = new Array();
-	final SnapshotArray<AnimationStateListener> listeners = new SnapshotArray();
-	private final EventQueue queue = new EventQueue();
-	private final ObjectSet<String> propertyIds = new ObjectSet();
-	boolean animationsChanged;
-	private float timeScale = 1;
-	private int unkeyedState;
-
-	final Pool<TrackEntry> trackEntryPool = new Pool() {
-		protected Object newObject () {
-			return new TrackEntry();
-		}
-	};
-
-	/** Creates an uninitialized AnimationState. The animation state data must be set before use. */
-	public AnimationState () {
-	}
-
-	public AnimationState (AnimationStateData data) {
-		if (data == null) throw new IllegalArgumentException("data cannot be null.");
-		this.data = data;
-	}
-
-	/** Increments each track entry {@link TrackEntry#getTrackTime()}, setting queued animations as current if needed. */
-	public void update (float delta) {
-		delta *= timeScale;
-		Object[] tracks = this.tracks.items;
-		for (int i = 0, n = this.tracks.size; i < n; i++) {
-			TrackEntry current = (TrackEntry)tracks[i];
-			if (current == null) continue;
-
-			current.animationLast = current.nextAnimationLast;
-			current.trackLast = current.nextTrackLast;
-
-			float currentDelta = delta * current.timeScale;
-
-			if (current.delay > 0) {
-				current.delay -= currentDelta;
-				if (current.delay > 0) continue;
-				currentDelta = -current.delay;
-				current.delay = 0;
-			}
-
-			TrackEntry next = current.next;
-			if (next != null) {
-				// When the next entry's delay is passed, change to the next entry, preserving leftover time.
-				float nextTime = current.trackLast - next.delay;
-				if (nextTime >= 0) {
-					next.delay = 0;
-					next.trackTime += current.timeScale == 0 ? 0 : (nextTime / current.timeScale + delta) * next.timeScale;
-					current.trackTime += currentDelta;
-					setCurrent(i, next, true);
-					while (next.mixingFrom != null) {
-						next.mixTime += delta;
-						next = next.mixingFrom;
-					}
-					continue;
-				}
-			} else if (current.trackLast >= current.trackEnd && current.mixingFrom == null) {
-				// Clear the track when there is no next entry, the track end time is reached, and there is no mixingFrom.
-				tracks[i] = null;
-				queue.end(current);
-				clearNext(current);
-				continue;
-			}
-			if (current.mixingFrom != null && updateMixingFrom(current, delta)) {
-				// End mixing from entries once all have completed.
-				TrackEntry from = current.mixingFrom;
-				current.mixingFrom = null;
-				if (from != null) from.mixingTo = null;
-				while (from != null) {
-					queue.end(from);
-					from = from.mixingFrom;
-				}
-			}
-
-			current.trackTime += currentDelta;
-		}
-
-		queue.drain();
-	}
-
-	/** Returns true when all mixing from entries are complete. */
-	private boolean updateMixingFrom (TrackEntry to, float delta) {
-		TrackEntry from = to.mixingFrom;
-		if (from == null) return true;
-
-		boolean finished = updateMixingFrom(from, delta);
-
-		from.animationLast = from.nextAnimationLast;
-		from.trackLast = from.nextTrackLast;
-
-		// Require mixTime > 0 to ensure the mixing from entry was applied at least once.
-		if (to.mixTime > 0 && to.mixTime >= to.mixDuration) {
-			// Require totalAlpha == 0 to ensure mixing is complete, unless mixDuration == 0 (the transition is a single frame).
-			if (from.totalAlpha == 0 || to.mixDuration == 0) {
-				to.mixingFrom = from.mixingFrom;
-				if (from.mixingFrom != null) from.mixingFrom.mixingTo = to;
-				to.interruptAlpha = from.interruptAlpha;
-				queue.end(from);
-			}
-			return finished;
-		}
-
-		from.trackTime += delta * from.timeScale;
-		to.mixTime += delta;
-		return false;
-	}
-
-	/** Poses the skeleton using the track entry animations. The animation state is not changed, so can be applied to multiple
-	 * skeletons to pose them identically.
-	 * @return True if any animations were applied. */
-	public boolean apply (Skeleton skeleton) {
-		if (skeleton == null) throw new IllegalArgumentException("skeleton cannot be null.");
-		if (animationsChanged) animationsChanged();
-
-		Array<Event> events = this.events;
-		boolean applied = false;
-		Object[] tracks = this.tracks.items;
-		for (int i = 0, n = this.tracks.size; i < n; i++) {
-			TrackEntry current = (TrackEntry)tracks[i];
-			if (current == null || current.delay > 0) continue;
-			applied = true;
-
-			// Track 0 animations aren't for layering, so do not show the previously applied animations before the first key.
-			MixBlend blend = i == 0 ? MixBlend.first : current.mixBlend;
-
-			// Apply mixing from entries first.
-			float mix = current.alpha;
-			if (current.mixingFrom != null)
-				mix *= applyMixingFrom(current, skeleton, blend);
-			else if (current.trackTime >= current.trackEnd && current.next == null) //
-				mix = 0; // Set to setup pose the last time the entry will be applied.
-
-			// Apply current entry.
-			float animationLast = current.animationLast, animationTime = current.getAnimationTime(), applyTime = animationTime;
-			Array<Event> applyEvents = events;
-			if (current.reverse) {
-				applyTime = current.animation.duration - applyTime;
-				applyEvents = null;
-			}
-			int timelineCount = current.animation.timelines.size;
-			Object[] timelines = current.animation.timelines.items;
-			if ((i == 0 && mix == 1) || blend == MixBlend.add) {
-				for (int ii = 0; ii < timelineCount; ii++) {
-					Object timeline = timelines[ii];
-					if (timeline instanceof AttachmentTimeline)
-						applyAttachmentTimeline((AttachmentTimeline)timeline, skeleton, applyTime, blend, true);
-					else
-						((Timeline)timeline).apply(skeleton, animationLast, applyTime, applyEvents, mix, blend, MixDirection.in);
-				}
-			} else {
-				int[] timelineMode = current.timelineMode.items;
-
-				boolean firstFrame = current.timelinesRotation.size != timelineCount << 1;
-				if (firstFrame) current.timelinesRotation.setSize(timelineCount << 1);
-				float[] timelinesRotation = current.timelinesRotation.items;
-
-				for (int ii = 0; ii < timelineCount; ii++) {
-					Timeline timeline = (Timeline)timelines[ii];
-					MixBlend timelineBlend = timelineMode[ii] == SUBSEQUENT ? blend : MixBlend.setup;
-					if (timeline instanceof RotateTimeline) {
-						applyRotateTimeline((RotateTimeline)timeline, skeleton, applyTime, mix, timelineBlend, timelinesRotation,
-							ii << 1, firstFrame);
-					} else if (timeline instanceof AttachmentTimeline)
-						applyAttachmentTimeline((AttachmentTimeline)timeline, skeleton, applyTime, blend, true);
-					else
-						timeline.apply(skeleton, animationLast, applyTime, applyEvents, mix, timelineBlend, MixDirection.in);
-				}
-			}
-			queueEvents(current, animationTime);
-			events.clear();
-			current.nextAnimationLast = animationTime;
-			current.nextTrackLast = current.trackTime;
-		}
-
-		// Set slots attachments to the setup pose, if needed. This occurs if an animation that is mixing out sets attachments so
-		// subsequent timelines see any deform, but the subsequent timelines don't set an attachment (eg they are also mixing out or
-		// the time is before the first key).
-		int setupState = unkeyedState + SETUP;
-		Object[] slots = skeleton.slots.items;
-		for (int i = 0, n = skeleton.slots.size; i < n; i++) {
-			Slot slot = (Slot)slots[i];
-			if (slot.attachmentState == setupState) {
-				String attachmentName = slot.data.attachmentName;
-				slot.setAttachment(attachmentName == null ? null : skeleton.getAttachment(slot.data.index, attachmentName));
-			}
-		}
-		unkeyedState += 2; // Increasing after each use avoids the need to reset attachmentState for every slot.
-
-		queue.drain();
-		return applied;
-	}
-
-	private float applyMixingFrom (TrackEntry to, Skeleton skeleton, MixBlend blend) {
-		TrackEntry from = to.mixingFrom;
-		if (from.mixingFrom != null) applyMixingFrom(from, skeleton, blend);
-
-		float mix;
-		if (to.mixDuration == 0) { // Single frame mix to undo mixingFrom changes.
-			mix = 1;
-			if (blend == MixBlend.first) blend = MixBlend.setup; // Tracks >0 are transparent and can't reset to setup pose.
-		} else {
-			mix = to.mixTime / to.mixDuration;
-			if (mix > 1) mix = 1;
-			if (blend != MixBlend.first) blend = from.mixBlend; // Track 0 ignores track mix blend.
-		}
-
-		boolean attachments = mix < from.attachmentThreshold, drawOrder = mix < from.drawOrderThreshold;
-		int timelineCount = from.animation.timelines.size;
-		Object[] timelines = from.animation.timelines.items;
-		float alphaHold = from.alpha * to.interruptAlpha, alphaMix = alphaHold * (1 - mix);
-		float animationLast = from.animationLast, animationTime = from.getAnimationTime(), applyTime = animationTime;
-		Array<Event> events = null;
-		if (from.reverse)
-			applyTime = from.animation.duration - applyTime;
-		else {
-			if (mix < from.eventThreshold) events = this.events;
-		}
-
-		if (blend == MixBlend.add) {
-			for (int i = 0; i < timelineCount; i++)
-				((Timeline)timelines[i]).apply(skeleton, animationLast, applyTime, events, alphaMix, blend, MixDirection.out);
-		} else {
-			int[] timelineMode = from.timelineMode.items;
-			Object[] timelineHoldMix = from.timelineHoldMix.items;
-
-			boolean firstFrame = from.timelinesRotation.size != timelineCount << 1;
-			if (firstFrame) from.timelinesRotation.setSize(timelineCount << 1);
-			float[] timelinesRotation = from.timelinesRotation.items;
-
-			from.totalAlpha = 0;
-			for (int i = 0; i < timelineCount; i++) {
-				Timeline timeline = (Timeline)timelines[i];
-				MixDirection direction = MixDirection.out;
-				MixBlend timelineBlend;
-				float alpha;
-				switch (timelineMode[i]) {
-				case SUBSEQUENT:
-					if (!drawOrder && timeline instanceof DrawOrderTimeline) continue;
-					timelineBlend = blend;
-					alpha = alphaMix;
-					break;
-				case FIRST:
-					timelineBlend = MixBlend.setup;
-					alpha = alphaMix;
-					break;
-				case HOLD_SUBSEQUENT:
-					timelineBlend = blend;
-					alpha = alphaHold;
-					break;
-				case HOLD_FIRST:
-					timelineBlend = MixBlend.setup;
-					alpha = alphaHold;
-					break;
-				default: // HOLD_MIX
-					timelineBlend = MixBlend.setup;
-					TrackEntry holdMix = (TrackEntry)timelineHoldMix[i];
-					alpha = alphaHold * Math.max(0, 1 - holdMix.mixTime / holdMix.mixDuration);
-					break;
-				}
-				from.totalAlpha += alpha;
-				if (timeline instanceof RotateTimeline) {
-					applyRotateTimeline((RotateTimeline)timeline, skeleton, applyTime, alpha, timelineBlend, timelinesRotation, i << 1,
-						firstFrame);
-				} else if (timeline instanceof AttachmentTimeline)
-					applyAttachmentTimeline((AttachmentTimeline)timeline, skeleton, applyTime, timelineBlend, attachments);
-				else {
-					if (drawOrder && timeline instanceof DrawOrderTimeline && timelineBlend == MixBlend.setup)
-						direction = MixDirection.in;
-					timeline.apply(skeleton, animationLast, applyTime, events, alpha, timelineBlend, direction);
-				}
-			}
-		}
-
-		if (to.mixDuration > 0) queueEvents(from, animationTime);
-		this.events.clear();
-		from.nextAnimationLast = animationTime;
-		from.nextTrackLast = from.trackTime;
-
-		return mix;
-	}
-
-	/** Applies the attachment timeline and sets {@link Slot#attachmentState}.
-	 * @param attachments False when: 1) the attachment timeline is mixing out, 2) mix < attachmentThreshold, and 3) the timeline
-	 *           is not the last timeline to set the slot's attachment. In that case the timeline is applied only so subsequent
-	 *           timelines see any deform. */
-	private void applyAttachmentTimeline (AttachmentTimeline timeline, Skeleton skeleton, float time, MixBlend blend,
-		boolean attachments) {
-
-		Slot slot = skeleton.slots.get(timeline.slotIndex);
-		if (!slot.bone.active) return;
-
-		if (time < timeline.frames[0]) { // Time is before first frame.
-			if (blend == MixBlend.setup || blend == MixBlend.first)
-				setAttachment(skeleton, slot, slot.data.attachmentName, attachments);
-		} else
-			setAttachment(skeleton, slot, timeline.attachmentNames[Timeline.search(timeline.frames, time)], attachments);
-
-		// If an attachment wasn't set (ie before the first frame or attachments is false), set the setup attachment later.
-		if (slot.attachmentState <= unkeyedState) slot.attachmentState = unkeyedState + SETUP;
-	}
-
-	private void setAttachment (Skeleton skeleton, Slot slot, String attachmentName, boolean attachments) {
-		slot.setAttachment(attachmentName == null ? null : skeleton.getAttachment(slot.data.index, attachmentName));
-		if (attachments) slot.attachmentState = unkeyedState + CURRENT;
-	}
-
-	/** Applies the rotate timeline, mixing with the current pose while keeping the same rotation direction chosen as the shortest
-	 * the first time the mixing was applied. */
-	private void applyRotateTimeline (RotateTimeline timeline, Skeleton skeleton, float time, float alpha, MixBlend blend,
-		float[] timelinesRotation, int i, boolean firstFrame) {
-
-		if (firstFrame) timelinesRotation[i] = 0;
-
-		if (alpha == 1) {
-			timeline.apply(skeleton, 0, time, null, 1, blend, MixDirection.in);
-			return;
-		}
-
-		Bone bone = skeleton.bones.get(timeline.boneIndex);
-		if (!bone.active) return;
-		float[] frames = timeline.frames;
-		float r1, r2;
-		if (time < frames[0]) { // Time is before first frame.
-			switch (blend) {
-			case setup:
-				bone.rotation = bone.data.rotation;
-				// Fall through.
-			default:
-				return;
-			case first:
-				r1 = bone.rotation;
-				r2 = bone.data.rotation;
-			}
-		} else {
-			r1 = blend == MixBlend.setup ? bone.data.rotation : bone.rotation;
-			r2 = bone.data.rotation + timeline.getCurveValue(time);
-		}
-
-		// Mix between rotations using the direction of the shortest route on the first frame.
-		float total, diff = r2 - r1;
-		diff -= (16384 - (int)(16384.499999999996 - diff / 360)) * 360;
-		if (diff == 0)
-			total = timelinesRotation[i];
-		else {
-			float lastTotal, lastDiff;
-			if (firstFrame) {
-				lastTotal = 0;
-				lastDiff = diff;
-			} else {
-				lastTotal = timelinesRotation[i]; // Angle and direction of mix, including loops.
-				lastDiff = timelinesRotation[i + 1]; // Difference between bones.
-			}
-			boolean current = diff > 0, dir = lastTotal >= 0;
-			// Detect cross at 0 (not 180).
-			if (Math.signum(lastDiff) != Math.signum(diff) && Math.abs(lastDiff) <= 90) {
-				// A cross after a 360 rotation is a loop.
-				if (Math.abs(lastTotal) > 180) lastTotal += 360 * Math.signum(lastTotal);
-				dir = current;
-			}
-			total = diff + lastTotal - lastTotal % 360; // Store loops as part of lastTotal.
-			if (dir != current) total += 360 * Math.signum(lastTotal);
-			timelinesRotation[i] = total;
-		}
-		timelinesRotation[i + 1] = diff;
-		bone.rotation = r1 + total * alpha;
-	}
-
-	private void queueEvents (TrackEntry entry, float animationTime) {
-		float animationStart = entry.animationStart, animationEnd = entry.animationEnd;
-		float duration = animationEnd - animationStart;
-		float trackLastWrapped = entry.trackLast % duration;
-
-		// Queue events before complete.
-		Object[] events = this.events.items;
-		int i = 0, n = this.events.size;
-		for (; i < n; i++) {
-			Event event = (Event)events[i];
-			if (event.time < trackLastWrapped) break;
-			if (event.time > animationEnd) continue; // Discard events outside animation start/end.
-			queue.event(entry, event);
-		}
-
-		// Queue complete if completed a loop iteration or the animation.
-		boolean complete;
-		if (entry.loop)
-			complete = duration == 0 || trackLastWrapped > entry.trackTime % duration;
-		else
-			complete = animationTime >= animationEnd && entry.animationLast < animationEnd;
-		if (complete) queue.complete(entry);
-
-		// Queue events after complete.
-		for (; i < n; i++) {
-			Event event = (Event)events[i];
-			if (event.time < animationStart) continue; // Discard events outside animation start/end.
-			queue.event(entry, event);
-		}
-	}
-
-	/** Removes all animations from all tracks, leaving skeletons in their current pose.
-	 * <p>
-	 * It may be desired to use {@link AnimationState#setEmptyAnimations(float)} to mix the skeletons back to the setup pose,
-	 * rather than leaving them in their current pose. */
-	public void clearTracks () {
-		boolean oldDrainDisabled = queue.drainDisabled;
-		queue.drainDisabled = true;
-		for (int i = 0, n = tracks.size; i < n; i++)
-			clearTrack(i);
-		tracks.clear();
-		queue.drainDisabled = oldDrainDisabled;
-		queue.drain();
-	}
-
-	/** Removes all animations from the track, leaving skeletons in their current pose.
-	 * <p>
-	 * It may be desired to use {@link AnimationState#setEmptyAnimation(int, float)} to mix the skeletons back to the setup pose,
-	 * rather than leaving them in their current pose. */
-	public void clearTrack (int trackIndex) {
-		if (trackIndex < 0) throw new IllegalArgumentException("trackIndex must be >= 0.");
-		if (trackIndex >= tracks.size) return;
-		TrackEntry current = tracks.get(trackIndex);
-		if (current == null) return;
-
-		queue.end(current);
-
-		clearNext(current);
-
-		TrackEntry entry = current;
-		while (true) {
-			TrackEntry from = entry.mixingFrom;
-			if (from == null) break;
-			queue.end(from);
-			entry.mixingFrom = null;
-			entry.mixingTo = null;
-			entry = from;
-		}
-
-		tracks.set(current.trackIndex, null);
-
-		queue.drain();
-	}
-
-	private void setCurrent (int index, TrackEntry current, boolean interrupt) {
-		TrackEntry from = expandToIndex(index);
-		tracks.set(index, current);
-		current.previous = null;
-
-		if (from != null) {
-			if (interrupt) queue.interrupt(from);
-			current.mixingFrom = from;
-			from.mixingTo = current;
-			current.mixTime = 0;
-
-			// Store the interrupted mix percentage.
-			if (from.mixingFrom != null && from.mixDuration > 0)
-				current.interruptAlpha *= Math.min(1, from.mixTime / from.mixDuration);
-
-			from.timelinesRotation.clear(); // Reset rotation for mixing out, in case entry was mixed in.
-		}
-
-		queue.start(current);
-	}
-
-	/** Sets an animation by name.
-	 * <p>
-	 * See {@link #setAnimation(int, Animation, boolean)}. */
-	public TrackEntry setAnimation (int trackIndex, String animationName, boolean loop) {
-		Animation animation = data.skeletonData.findAnimation(animationName);
-		if (animation == null) throw new IllegalArgumentException("Animation not found: " + animationName);
-		return setAnimation(trackIndex, animation, loop);
-	}
-
-	/** Sets the current animation for a track, discarding any queued animations. If the formerly current track entry was never
-	 * applied to a skeleton, it is replaced (not mixed from).
-	 * @param loop If true, the animation will repeat. If false it will not, instead its last frame is applied if played beyond its
-	 *           duration. In either case {@link TrackEntry#getTrackEnd()} determines when the track is cleared.
-	 * @return A track entry to allow further customization of animation playback. References to the track entry must not be kept
-	 *         after the {@link AnimationStateListener#dispose(TrackEntry)} event occurs. */
-	public TrackEntry setAnimation (int trackIndex, Animation animation, boolean loop) {
-		if (trackIndex < 0) throw new IllegalArgumentException("trackIndex must be >= 0.");
-		if (animation == null) throw new IllegalArgumentException("animation cannot be null.");
-		boolean interrupt = true;
-		TrackEntry current = expandToIndex(trackIndex);
-		if (current != null) {
-			if (current.nextTrackLast == -1) {
-				// Don't mix from an entry that was never applied.
-				tracks.set(trackIndex, current.mixingFrom);
-				queue.interrupt(current);
-				queue.end(current);
-				clearNext(current);
-				current = current.mixingFrom;
-				interrupt = false; // mixingFrom is current again, but don't interrupt it twice.
-			} else
-				clearNext(current);
-		}
-		TrackEntry entry = trackEntry(trackIndex, animation, loop, current);
-		setCurrent(trackIndex, entry, interrupt);
-		queue.drain();
-		return entry;
-	}
-
-	/** Queues an animation by name.
-	 * <p>
-	 * See {@link #addAnimation(int, Animation, boolean, float)}. */
-	public TrackEntry addAnimation (int trackIndex, String animationName, boolean loop, float delay) {
-		Animation animation = data.skeletonData.findAnimation(animationName);
-		if (animation == null) throw new IllegalArgumentException("Animation not found: " + animationName);
-		return addAnimation(trackIndex, animation, loop, delay);
-	}
-
-	/** Adds an animation to be played after the current or last queued animation for a track. If the track is empty, it is
-	 * equivalent to calling {@link #setAnimation(int, Animation, boolean)}.
-	 * @param delay If > 0, sets {@link TrackEntry#getDelay()}. If <= 0, the delay set is the duration of the previous track entry
-	 *           minus any mix duration (from the {@link AnimationStateData}) plus the specified <code>delay</code> (ie the mix
-	 *           ends at (<code>delay</code> = 0) or before (<code>delay</code> < 0) the previous track entry duration). If the
-	 *           previous entry is looping, its next loop completion is used instead of its duration.
-	 * @return A track entry to allow further customization of animation playback. References to the track entry must not be kept
-	 *         after the {@link AnimationStateListener#dispose(TrackEntry)} event occurs. */
-	public TrackEntry addAnimation (int trackIndex, Animation animation, boolean loop, float delay) {
-		if (trackIndex < 0) throw new IllegalArgumentException("trackIndex must be >= 0.");
-		if (animation == null) throw new IllegalArgumentException("animation cannot be null.");
-
-		TrackEntry last = expandToIndex(trackIndex);
-		if (last != null) {
-			while (last.next != null)
-				last = last.next;
-		}
-
-		TrackEntry entry = trackEntry(trackIndex, animation, loop, last);
-
-		if (last == null) {
-			setCurrent(trackIndex, entry, true);
-			queue.drain();
-		} else {
-			last.next = entry;
-			entry.previous = last;
-			if (delay <= 0) delay += last.getTrackComplete() - entry.mixDuration;
-		}
-
-		entry.delay = delay;
-		return entry;
-	}
-
-	/** Sets an empty animation for a track, discarding any queued animations, and sets the track entry's
-	 * {@link TrackEntry#getMixDuration()}. An empty animation has no timelines and serves as a placeholder for mixing in or out.
-	 * <p>
-	 * Mixing out is done by setting an empty animation with a mix duration using either {@link #setEmptyAnimation(int, float)},
-	 * {@link #setEmptyAnimations(float)}, or {@link #addEmptyAnimation(int, float, float)}. Mixing to an empty animation causes
-	 * the previous animation to be applied less and less over the mix duration. Properties keyed in the previous animation
-	 * transition to the value from lower tracks or to the setup pose value if no lower tracks key the property. A mix duration of
-	 * 0 still mixes out over one frame.
-	 * <p>
-	 * Mixing in is done by first setting an empty animation, then adding an animation using
-	 * {@link #addAnimation(int, Animation, boolean, float)} with the desired delay (an empty animation has a duration of 0) and on
-	 * the returned track entry, set the {@link TrackEntry#setMixDuration(float)}. Mixing from an empty animation causes the new
-	 * animation to be applied more and more over the mix duration. Properties keyed in the new animation transition from the value
-	 * from lower tracks or from the setup pose value if no lower tracks key the property to the value keyed in the new
-	 * animation. */
-	public TrackEntry setEmptyAnimation (int trackIndex, float mixDuration) {
-		TrackEntry entry = setAnimation(trackIndex, emptyAnimation, false);
-		entry.mixDuration = mixDuration;
-		entry.trackEnd = mixDuration;
-		return entry;
-	}
-
-	/** Adds an empty animation to be played after the current or last queued animation for a track, and sets the track entry's
-	 * {@link TrackEntry#getMixDuration()}. If the track is empty, it is equivalent to calling
-	 * {@link #setEmptyAnimation(int, float)}.
-	 * <p>
-	 * See {@link #setEmptyAnimation(int, float)}.
-	 * @param delay If > 0, sets {@link TrackEntry#getDelay()}. If <= 0, the delay set is the duration of the previous track entry
-	 *           minus any mix duration plus the specified <code>delay</code> (ie the mix ends at (<code>delay</code> = 0) or
-	 *           before (<code>delay</code> < 0) the previous track entry duration). If the previous entry is looping, its next
-	 *           loop completion is used instead of its duration.
-	 * @return A track entry to allow further customization of animation playback. References to the track entry must not be kept
-	 *         after the {@link AnimationStateListener#dispose(TrackEntry)} event occurs. */
-	public TrackEntry addEmptyAnimation (int trackIndex, float mixDuration, float delay) {
-		TrackEntry entry = addAnimation(trackIndex, emptyAnimation, false, delay <= 0 ? 1 : delay);
-		entry.mixDuration = mixDuration;
-		entry.trackEnd = mixDuration;
-		if (delay <= 0 && entry.previous != null) entry.delay = entry.previous.getTrackComplete() - entry.mixDuration + delay;
-		return entry;
-	}
-
-	/** Sets an empty animation for every track, discarding any queued animations, and mixes to it over the specified mix
-	 * duration. */
-	public void setEmptyAnimations (float mixDuration) {
-		boolean oldDrainDisabled = queue.drainDisabled;
-		queue.drainDisabled = true;
-		Object[] tracks = this.tracks.items;
-		for (int i = 0, n = this.tracks.size; i < n; i++) {
-			TrackEntry current = (TrackEntry)tracks[i];
-			if (current != null) setEmptyAnimation(current.trackIndex, mixDuration);
-		}
-		queue.drainDisabled = oldDrainDisabled;
-		queue.drain();
-	}
-
-	private TrackEntry expandToIndex (int index) {
-		if (index < tracks.size) return tracks.get(index);
-		tracks.ensureCapacity(index - tracks.size + 1);
-		tracks.size = index + 1;
-		return null;
-	}
-
-	private TrackEntry trackEntry (int trackIndex, Animation animation, boolean loop, @Null TrackEntry last) {
-		TrackEntry entry = trackEntryPool.obtain();
-		entry.trackIndex = trackIndex;
-		entry.animation = animation;
-		entry.loop = loop;
-		entry.holdPrevious = false;
-
-		entry.eventThreshold = 0;
-		entry.attachmentThreshold = 0;
-		entry.drawOrderThreshold = 0;
-
-		entry.animationStart = 0;
-		entry.animationEnd = animation.getDuration();
-		entry.animationLast = -1;
-		entry.nextAnimationLast = -1;
-
-		entry.delay = 0;
-		entry.trackTime = 0;
-		entry.trackLast = -1;
-		entry.nextTrackLast = -1;
-		entry.trackEnd = Float.MAX_VALUE;
-		entry.timeScale = 1;
-
-		entry.alpha = 1;
-		entry.interruptAlpha = 1;
-		entry.mixTime = 0;
-		entry.mixDuration = last == null ? 0 : data.getMix(last.animation, animation);
-		entry.mixBlend = MixBlend.replace;
-		return entry;
-	}
-
-	/** Removes the {@link TrackEntry#getNext() next entry} and all entries after it for the specified entry. */
-	public void clearNext (TrackEntry entry) {
-		TrackEntry next = entry.next;
-		while (next != null) {
-			queue.dispose(next);
-			next = next.next;
-		}
-		entry.next = null;
-	}
-
-	void animationsChanged () {
-		animationsChanged = false;
-
-		// Process in the order that animations are applied.
-		propertyIds.clear(2048);
-		int n = tracks.size;
-		Object[] tracks = this.tracks.items;
-		for (int i = 0; i < n; i++) {
-			TrackEntry entry = (TrackEntry)tracks[i];
-			if (entry == null) continue;
-			while (entry.mixingFrom != null) // Move to last entry, then iterate in reverse.
-				entry = entry.mixingFrom;
-			do {
-				if (entry.mixingTo == null || entry.mixBlend != MixBlend.add) computeHold(entry);
-				entry = entry.mixingTo;
-			} while (entry != null);
-		}
-	}
-
-	private void computeHold (TrackEntry entry) {
-		TrackEntry to = entry.mixingTo;
-		Object[] timelines = entry.animation.timelines.items;
-		int timelinesCount = entry.animation.timelines.size;
-		int[] timelineMode = entry.timelineMode.setSize(timelinesCount);
-		entry.timelineHoldMix.clear();
-		Object[] timelineHoldMix = entry.timelineHoldMix.setSize(timelinesCount);
-		ObjectSet<String> propertyIds = this.propertyIds;
-
-		if (to != null && to.holdPrevious) {
-			for (int i = 0; i < timelinesCount; i++)
-				timelineMode[i] = propertyIds.addAll(((Timeline)timelines[i]).getPropertyIds()) ? HOLD_FIRST : HOLD_SUBSEQUENT;
-			return;
-		}
-
-		outer:
-		for (int i = 0; i < timelinesCount; i++) {
-			Timeline timeline = (Timeline)timelines[i];
-			String[] ids = timeline.getPropertyIds();
-			if (!propertyIds.addAll(ids))
-				timelineMode[i] = SUBSEQUENT;
-			else if (to == null || timeline instanceof AttachmentTimeline || timeline instanceof DrawOrderTimeline
-				|| timeline instanceof EventTimeline || !to.animation.hasTimeline(ids)) {
-				timelineMode[i] = FIRST;
-			} else {
-				for (TrackEntry next = to.mixingTo; next != null; next = next.mixingTo) {
-					if (next.animation.hasTimeline(ids)) continue;
-					if (next.mixDuration > 0) {
-						timelineMode[i] = HOLD_MIX;
-						timelineHoldMix[i] = next;
-						continue outer;
-					}
-					break;
-				}
-				timelineMode[i] = HOLD_FIRST;
-			}
-		}
-	}
-
-	/** Returns the track entry for the animation currently playing on the track, or null if no animation is currently playing. */
-	public @Null TrackEntry getCurrent (int trackIndex) {
-		if (trackIndex < 0) throw new IllegalArgumentException("trackIndex must be >= 0.");
-		if (trackIndex >= tracks.size) return null;
-		return tracks.get(trackIndex);
-	}
-
-	/** Adds a listener to receive events for all track entries. */
-	public void addListener (AnimationStateListener listener) {
-		if (listener == null) throw new IllegalArgumentException("listener cannot be null.");
-		listeners.add(listener);
-	}
-
-	/** Removes the listener added with {@link #addListener(AnimationStateListener)}. */
-	public void removeListener (AnimationStateListener listener) {
-		listeners.removeValue(listener, true);
-	}
-
-	/** Removes all listeners added with {@link #addListener(AnimationStateListener)}. */
-	public void clearListeners () {
-		listeners.clear();
-	}
-
-	/** Discards all listener notifications that have not yet been delivered. This can be useful to call from an
-	 * {@link AnimationStateListener} when it is known that further notifications that may have been already queued for delivery
-	 * are not wanted because new animations are being set. */
-	public void clearListenerNotifications () {
-		queue.clear();
-	}
-
-	/** Multiplier for the delta time when the animation state is updated, causing time for all animations and mixes to play slower
-	 * or faster. Defaults to 1.
-	 * <p>
-	 * See TrackEntry {@link TrackEntry#getTimeScale()} for affecting a single animation. */
-	public float getTimeScale () {
-		return timeScale;
-	}
-
-	public void setTimeScale (float timeScale) {
-		this.timeScale = timeScale;
-	}
-
-	/** The AnimationStateData to look up mix durations. */
-	public AnimationStateData getData () {
-		return data;
-	}
-
-	public void setData (AnimationStateData data) {
-		if (data == null) throw new IllegalArgumentException("data cannot be null.");
-		this.data = data;
-	}
-
-	/** The list of tracks that have had animations, which may contain null entries for tracks that currently have no animation. */
-	public Array<TrackEntry> getTracks () {
-		return tracks;
-	}
-
-	public String toString () {
-		StringBuilder buffer = new StringBuilder(64);
-		Object[] tracks = this.tracks.items;
-		for (int i = 0, n = this.tracks.size; i < n; i++) {
-			TrackEntry entry = (TrackEntry)tracks[i];
-			if (entry == null) continue;
-			if (buffer.length() > 0) buffer.append(", ");
-			buffer.append(entry.toString());
-		}
-		if (buffer.length() == 0) return "<none>";
-		return buffer.toString();
-	}
-
-	/** Stores settings and other state for the playback of an animation on an {@link AnimationState} track.
-	 * <p>
-	 * References to a track entry must not be kept after the {@link AnimationStateListener#dispose(TrackEntry)} event occurs. */
-	static public class TrackEntry implements Poolable {
-		Animation animation;
-		@Null TrackEntry previous, next, mixingFrom, mixingTo;
-		@Null AnimationStateListener listener;
-		int trackIndex;
-		boolean loop, holdPrevious, reverse;
-		float eventThreshold, attachmentThreshold, drawOrderThreshold;
-		float animationStart, animationEnd, animationLast, nextAnimationLast;
-		float delay, trackTime, trackLast, nextTrackLast, trackEnd, timeScale;
-		float alpha, mixTime, mixDuration, interruptAlpha, totalAlpha;
-		MixBlend mixBlend = MixBlend.replace;
-
-		final IntArray timelineMode = new IntArray();
-		final Array<TrackEntry> timelineHoldMix = new Array();
-		final FloatArray timelinesRotation = new FloatArray();
-
-		public void reset () {
-			previous = null;
-			next = null;
-			mixingFrom = null;
-			mixingTo = null;
-			animation = null;
-			listener = null;
-			timelineMode.clear();
-			timelineHoldMix.clear();
-			timelinesRotation.clear();
-		}
-
-		/** The index of the track where this track entry is either current or queued.
-		 * <p>
-		 * See {@link AnimationState#getCurrent(int)}. */
-		public int getTrackIndex () {
-			return trackIndex;
-		}
-
-		/** The animation to apply for this track entry. */
-		public Animation getAnimation () {
-			return animation;
-		}
-
-		public void setAnimation (Animation animation) {
-			if (animation == null) throw new IllegalArgumentException("animation cannot be null.");
-			this.animation = animation;
-		}
-
-		/** If true, the animation will repeat. If false it will not, instead its last frame is applied if played beyond its
-		 * duration. */
-		public boolean getLoop () {
-			return loop;
-		}
-
-		public void setLoop (boolean loop) {
-			this.loop = loop;
-		}
-
-		/** Seconds to postpone playing the animation. When this track entry is the current track entry, <code>delay</code>
-		 * postpones incrementing the {@link #getTrackTime()}. When this track entry is queued, <code>delay</code> is the time from
-		 * the start of the previous animation to when this track entry will become the current track entry (ie when the previous
-		 * track entry {@link TrackEntry#getTrackTime()} >= this track entry's <code>delay</code>).
-		 * <p>
-		 * {@link #getTimeScale()} affects the delay.
-		 * <p>
-		 * When using {@link AnimationState#addAnimation(int, Animation, boolean, float)} with a <code>delay</code> <= 0, the delay
-		 * is set using the mix duration from the {@link AnimationStateData}. If {@link #mixDuration} is set afterward, the delay
-		 * may need to be adjusted. */
-		public float getDelay () {
-			return delay;
-		}
-
-		public void setDelay (float delay) {
-			this.delay = delay;
-		}
-
-		/** Current time in seconds this track entry has been the current track entry. The track time determines
-		 * {@link #getAnimationTime()}. The track time can be set to start the animation at a time other than 0, without affecting
-		 * looping. */
-		public float getTrackTime () {
-			return trackTime;
-		}
-
-		public void setTrackTime (float trackTime) {
-			this.trackTime = trackTime;
-		}
-
-		/** The track time in seconds when this animation will be removed from the track. Defaults to the highest possible float
-		 * value, meaning the animation will be applied until a new animation is set or the track is cleared. If the track end time
-		 * is reached, no other animations are queued for playback, and mixing from any previous animations is complete, then the
-		 * properties keyed by the animation are set to the setup pose and the track is cleared.
-		 * <p>
-		 * It may be desired to use {@link AnimationState#addEmptyAnimation(int, float, float)} rather than have the animation
-		 * abruptly cease being applied. */
-		public float getTrackEnd () {
-			return trackEnd;
-		}
-
-		public void setTrackEnd (float trackEnd) {
-			this.trackEnd = trackEnd;
-		}
-
-		/** If this track entry is non-looping, the track time in seconds when {@link #getAnimationEnd()} is reached, or the current
-		 * {@link #getTrackTime()} if it has already been reached. If this track entry is looping, the track time when this
-		 * animation will reach its next {@link #getAnimationEnd()} (the next loop completion). */
-		public float getTrackComplete () {
-			float duration = animationEnd - animationStart;
-			if (duration != 0) {
-				if (loop) return duration * (1 + (int)(trackTime / duration)); // Completion of next loop.
-				if (trackTime < duration) return duration; // Before duration.
-			}
-			return trackTime; // Next update.
-		}
-
-		/** Seconds when this animation starts, both initially and after looping. Defaults to 0.
-		 * <p>
-		 * When changing the <code>animationStart</code> time, it often makes sense to set {@link #getAnimationLast()} to the same
-		 * value to prevent timeline keys before the start time from triggering. */
-		public float getAnimationStart () {
-			return animationStart;
-		}
-
-		public void setAnimationStart (float animationStart) {
-			this.animationStart = animationStart;
-		}
-
-		/** Seconds for the last frame of this animation. Non-looping animations won't play past this time. Looping animations will
-		 * loop back to {@link #getAnimationStart()} at this time. Defaults to the animation {@link Animation#duration}. */
-		public float getAnimationEnd () {
-			return animationEnd;
-		}
-
-		public void setAnimationEnd (float animationEnd) {
-			this.animationEnd = animationEnd;
-		}
-
-		/** The time in seconds this animation was last applied. Some timelines use this for one-time triggers. Eg, when this
-		 * animation is applied, event timelines will fire all events between the <code>animationLast</code> time (exclusive) and
-		 * <code>animationTime</code> (inclusive). Defaults to -1 to ensure triggers on frame 0 happen the first time this animation
-		 * is applied. */
-		public float getAnimationLast () {
-			return animationLast;
-		}
-
-		public void setAnimationLast (float animationLast) {
-			this.animationLast = animationLast;
-			nextAnimationLast = animationLast;
-		}
-
-		/** Uses {@link #getTrackTime()} to compute the <code>animationTime</code>, which is between {@link #getAnimationStart()}
-		 * and {@link #getAnimationEnd()}. When the <code>trackTime</code> is 0, the <code>animationTime</code> is equal to the
-		 * <code>animationStart</code> time. */
-		public float getAnimationTime () {
-			if (loop) {
-				float duration = animationEnd - animationStart;
-				if (duration == 0) return animationStart;
-				return (trackTime % duration) + animationStart;
-			}
-			return Math.min(trackTime + animationStart, animationEnd);
-		}
-
-		/** Multiplier for the delta time when this track entry is updated, causing time for this animation to pass slower or
-		 * faster. Defaults to 1.
-		 * <p>
-		 * Values < 0 are not supported. To play an animation in reverse, use {@link #getReverse()}.
-		 * <p>
-		 * {@link #getMixTime()} is not affected by track entry time scale, so {@link #getMixDuration()} may need to be adjusted to
-		 * match the animation speed.
-		 * <p>
-		 * When using {@link AnimationState#addAnimation(int, Animation, boolean, float)} with a <code>delay</code> <= 0, the
-		 * {@link #getDelay()} is set using the mix duration from the {@link AnimationStateData}, assuming time scale to be 1. If
-		 * the time scale is not 1, the delay may need to be adjusted.
-		 * <p>
-		 * See AnimationState {@link AnimationState#getTimeScale()} for affecting all animations. */
-		public float getTimeScale () {
-			return timeScale;
-		}
-
-		public void setTimeScale (float timeScale) {
-			this.timeScale = timeScale;
-		}
-
-		/** The listener for events generated by this track entry, or null.
-		 * <p>
-		 * A track entry returned from {@link AnimationState#setAnimation(int, Animation, boolean)} is already the current animation
-		 * for the track, so the track entry listener {@link AnimationStateListener#start(TrackEntry)} will not be called. */
-		public @Null AnimationStateListener getListener () {
-			return listener;
-		}
-
-		public void setListener (@Null AnimationStateListener listener) {
-			this.listener = listener;
-		}
-
-		/** Values < 1 mix this animation with the skeleton's current pose (usually the pose resulting from lower tracks). Defaults
-		 * to 1, which overwrites the skeleton's current pose with this animation.
-		 * <p>
-		 * Typically track 0 is used to completely pose the skeleton, then alpha is used on higher tracks. It doesn't make sense to
-		 * use alpha on track 0 if the skeleton pose is from the last frame render. */
-		public float getAlpha () {
-			return alpha;
-		}
-
-		public void setAlpha (float alpha) {
-			this.alpha = alpha;
-		}
-
-		/** When the mix percentage ({@link #getMixTime()} / {@link #getMixDuration()}) is less than the
-		 * <code>eventThreshold</code>, event timelines are applied while this animation is being mixed out. Defaults to 0, so event
-		 * timelines are not applied while this animation is being mixed out. */
-		public float getEventThreshold () {
-			return eventThreshold;
-		}
-
-		public void setEventThreshold (float eventThreshold) {
-			this.eventThreshold = eventThreshold;
-		}
-
-		/** When the mix percentage ({@link #getMixTime()} / {@link #getMixDuration()}) is less than the
-		 * <code>attachmentThreshold</code>, attachment timelines are applied while this animation is being mixed out. Defaults to
-		 * 0, so attachment timelines are not applied while this animation is being mixed out. */
-		public float getAttachmentThreshold () {
-			return attachmentThreshold;
-		}
-
-		public void setAttachmentThreshold (float attachmentThreshold) {
-			this.attachmentThreshold = attachmentThreshold;
-		}
-
-		/** When the mix percentage ({@link #getMixTime()} / {@link #getMixDuration()}) is less than the
-		 * <code>drawOrderThreshold</code>, draw order timelines are applied while this animation is being mixed out. Defaults to 0,
-		 * so draw order timelines are not applied while this animation is being mixed out. */
-		public float getDrawOrderThreshold () {
-			return drawOrderThreshold;
-		}
-
-		public void setDrawOrderThreshold (float drawOrderThreshold) {
-			this.drawOrderThreshold = drawOrderThreshold;
-		}
-
-		/** The animation queued to start after this animation, or null if there is none. <code>next</code> makes up a doubly linked
-		 * list.
-		 * <p>
-		 * See {@link AnimationState#clearNext(TrackEntry)} to truncate the list. */
-		public @Null TrackEntry getNext () {
-			return next;
-		}
-
-		/** The animation queued to play before this animation, or null. <code>previous</code> makes up a doubly linked list. */
-		public @Null TrackEntry getPrevious () {
-			return previous;
-		}
-
-		/** Returns true if at least one loop has been completed.
-		 * <p>
-		 * See {@link AnimationStateListener#complete(TrackEntry)}. */
-		public boolean isComplete () {
-			return trackTime >= animationEnd - animationStart;
-		}
-
-		/** Seconds from 0 to the {@link #getMixDuration()} when mixing from the previous animation to this animation. May be
-		 * slightly more than <code>mixDuration</code> when the mix is complete. */
-		public float getMixTime () {
-			return mixTime;
-		}
-
-		public void setMixTime (float mixTime) {
-			this.mixTime = mixTime;
-		}
-
-		/** Seconds for mixing from the previous animation to this animation. Defaults to the value provided by AnimationStateData
-		 * {@link AnimationStateData#getMix(Animation, Animation)} based on the animation before this animation (if any).
-		 * <p>
-		 * A mix duration of 0 still mixes out over one frame to provide the track entry being mixed out a chance to revert the
-		 * properties it was animating.
-		 * <p>
-		 * The <code>mixDuration</code> can be set manually rather than use the value from
-		 * {@link AnimationStateData#getMix(Animation, Animation)}. In that case, the <code>mixDuration</code> can be set for a new
-		 * track entry only before {@link AnimationState#update(float)} is first called.
-		 * <p>
-		 * When using {@link AnimationState#addAnimation(int, Animation, boolean, float)} with a <code>delay</code> <= 0, the
-		 * {@link #getDelay()} is set using the mix duration from the {@link AnimationStateData}. If <code>mixDuration</code> is set
-		 * afterward, the delay may need to be adjusted. For example:
-		 * <code>entry.delay = entry.previous.getTrackComplete() - entry.mixDuration;</code> */
-		public float getMixDuration () {
-			return mixDuration;
-		}
-
-		public void setMixDuration (float mixDuration) {
-			this.mixDuration = mixDuration;
-		}
-
-		/** Controls how properties keyed in the animation are mixed with lower tracks. Defaults to {@link MixBlend#replace}.
-		 * <p>
-		 * Track entries on track 0 ignore this setting and always use {@link MixBlend#first}.
-		 * <p>
-		 * The <code>mixBlend</code> can be set for a new track entry only before {@link AnimationState#apply(Skeleton)} is first
-		 * called. */
-		public MixBlend getMixBlend () {
-			return mixBlend;
-		}
-
-		public void setMixBlend (MixBlend mixBlend) {
-			if (mixBlend == null) throw new IllegalArgumentException("mixBlend cannot be null.");
-			this.mixBlend = mixBlend;
-		}
-
-		/** The track entry for the previous animation when mixing from the previous animation to this animation, or null if no
-		 * mixing is currently occuring. When mixing from multiple animations, <code>mixingFrom</code> makes up a linked list. */
-		public @Null TrackEntry getMixingFrom () {
-			return mixingFrom;
-		}
-
-		/** The track entry for the next animation when mixing from this animation to the next animation, or null if no mixing is
-		 * currently occuring. When mixing to multiple animations, <code>mixingTo</code> makes up a linked list. */
-		public @Null TrackEntry getMixingTo () {
-			return mixingTo;
-		}
-
-		public void setHoldPrevious (boolean holdPrevious) {
-			this.holdPrevious = holdPrevious;
-		}
-
-		/** If true, when mixing from the previous animation to this animation, the previous animation is applied as normal instead
-		 * of being mixed out.
-		 * <p>
-		 * When mixing between animations that key the same property, if a lower track also keys that property then the value will
-		 * briefly dip toward the lower track value during the mix. This happens because the first animation mixes from 100% to 0%
-		 * while the second animation mixes from 0% to 100%. Setting <code>holdPrevious</code> to true applies the first animation
-		 * at 100% during the mix so the lower track value is overwritten. Such dipping does not occur on the lowest track which
-		 * keys the property, only when a higher track also keys the property.
-		 * <p>
-		 * Snapping will occur if <code>holdPrevious</code> is true and this animation does not key all the same properties as the
-		 * previous animation. */
-		public boolean getHoldPrevious () {
-			return holdPrevious;
-		}
-
-		/** Resets the rotation directions for mixing this entry's rotate timelines. This can be useful to avoid bones rotating the
-		 * long way around when using {@link #alpha} and starting animations on other tracks.
-		 * <p>
-		 * Mixing with {@link MixBlend#replace} involves finding a rotation between two others, which has two possible solutions:
-		 * the short way or the long way around. The two rotations likely change over time, so which direction is the short or long
-		 * way also changes. If the short way was always chosen, bones would flip to the other side when that direction became the
-		 * long way. TrackEntry chooses the short way the first time it is applied and remembers that direction. */
-		public void resetRotationDirections () {
-			timelinesRotation.clear();
-		}
-
-		public void setReverse (boolean reverse) {
-			this.reverse = reverse;
-		}
-
-		/** If true, the animation will be applied in reverse. Events are not fired when an animation is applied in reverse. */
-		public boolean getReverse () {
-			return reverse;
-		}
-
-		public String toString () {
-			return animation == null ? "<none>" : animation.name;
-		}
-	}
-
-	class EventQueue {
-		private final Array objects = new Array();
-		boolean drainDisabled;
-
-		void start (TrackEntry entry) {
-			objects.add(EventType.start);
-			objects.add(entry);
-			animationsChanged = true;
-		}
-
-		void interrupt (TrackEntry entry) {
-			objects.add(EventType.interrupt);
-			objects.add(entry);
-		}
-
-		void end (TrackEntry entry) {
-			objects.add(EventType.end);
-			objects.add(entry);
-			animationsChanged = true;
-		}
-
-		void dispose (TrackEntry entry) {
-			objects.add(EventType.dispose);
-			objects.add(entry);
-		}
-
-		void complete (TrackEntry entry) {
-			objects.add(EventType.complete);
-			objects.add(entry);
-		}
-
-		void event (TrackEntry entry, Event event) {
-			objects.add(EventType.event);
-			objects.add(entry);
-			objects.add(event);
-		}
-
-		void drain () {
-			if (drainDisabled) return; // Not reentrant.
-			drainDisabled = true;
-
-			SnapshotArray<AnimationStateListener> listenersArray = AnimationState.this.listeners;
-			for (int i = 0; i < this.objects.size; i += 2) {
-				EventType type = (EventType)objects.get(i);
-				TrackEntry entry = (TrackEntry)objects.get(i + 1);
-				int listenersCount = listenersArray.size;
-				Object[] listeners = listenersArray.begin();
-				switch (type) {
-				case start:
-					if (entry.listener != null) entry.listener.start(entry);
-					for (int ii = 0; ii < listenersCount; ii++)
-						((AnimationStateListener)listeners[ii]).start(entry);
-					break;
-				case interrupt:
-					if (entry.listener != null) entry.listener.interrupt(entry);
-					for (int ii = 0; ii < listenersCount; ii++)
-						((AnimationStateListener)listeners[ii]).interrupt(entry);
-					break;
-				case end:
-					if (entry.listener != null) entry.listener.end(entry);
-					for (int ii = 0; ii < listenersCount; ii++)
-						((AnimationStateListener)listeners[ii]).end(entry);
-					// Fall through.
-				case dispose:
-					if (entry.listener != null) entry.listener.dispose(entry);
-					for (int ii = 0; ii < listenersCount; ii++)
-						((AnimationStateListener)listeners[ii]).dispose(entry);
-					trackEntryPool.free(entry);
-					break;
-				case complete:
-					if (entry.listener != null) entry.listener.complete(entry);
-					for (int ii = 0; ii < listenersCount; ii++)
-						((AnimationStateListener)listeners[ii]).complete(entry);
-					break;
-				case event:
-					Event event = (Event)objects.get(i++ + 2);
-					if (entry.listener != null) entry.listener.event(entry, event);
-					for (int ii = 0; ii < listenersCount; ii++)
-						((AnimationStateListener)listeners[ii]).event(entry, event);
-					break;
-				}
-				listenersArray.end();
-			}
-			clear();
-
-			drainDisabled = false;
-		}
-
-		void clear () {
-			objects.clear();
-		}
-	}
-
-	static private enum EventType {
-		start, interrupt, end, dispose, complete, event
-	}
-
-	/** The interface to implement for receiving TrackEntry events. It is always safe to call AnimationState methods when receiving
-	 * events.
-	 * <p>
-	 * See TrackEntry {@link TrackEntry#setListener(AnimationStateListener)} and AnimationState
-	 * {@link AnimationState#addListener(AnimationStateListener)}. */
-	static public interface AnimationStateListener {
-		/** Invoked when this entry has been set as the current entry. */
-		public void start (TrackEntry entry);
-
-		/** Invoked when another entry has replaced this entry as the current entry. This entry may continue being applied for
-		 * mixing. */
-		public void interrupt (TrackEntry entry);
-
-		/** Invoked when this entry is no longer the current entry and will never be applied again. */
-		public void end (TrackEntry entry);
-
-		/** Invoked when this entry will be disposed. This may occur without the entry ever being set as the current entry.
-		 * References to the entry should not be kept after <code>dispose</code> is called, as it may be destroyed or reused. */
-		public void dispose (TrackEntry entry);
-
-		/** Invoked every time this entry's animation completes a loop. Because this event is trigged in
-		 * {@link AnimationState#apply(Skeleton)}, any animations set in response to the event won't be applied until the next time
-		 * the AnimationState is applied. */
-		public void complete (TrackEntry entry);
-
-		/** Invoked when this entry's animation triggers an event. Because this event is trigged in
-		 * {@link AnimationState#apply(Skeleton)}, any animations set in response to the event won't be applied until the next time
-		 * the AnimationState is applied. */
-		public void event (TrackEntry entry, Event event);
-	}
-
-	static public abstract class AnimationStateAdapter implements AnimationStateListener {
-		public void start (TrackEntry entry) {
-		}
-
-		public void interrupt (TrackEntry entry) {
-		}
-
-		public void end (TrackEntry entry) {
-		}
-
-		public void dispose (TrackEntry entry) {
-		}
-
-		public void complete (TrackEntry entry) {
-		}
-
-		public void event (TrackEntry entry, Event event) {
-		}
-	}
-}
+/******************************************************************************
+ * Spine Runtimes License Agreement
+ * Last updated January 1, 2020. Replaces all prior versions.
+ *
+ * Copyright (c) 2013-2020, Esoteric Software LLC
+ *
+ * Integration of the Spine Runtimes into software or otherwise creating
+ * derivative works of the Spine Runtimes is permitted under the terms and
+ * conditions of Section 2 of the Spine Editor License Agreement:
+ * http://esotericsoftware.com/spine-editor-license
+ *
+ * Otherwise, it is permitted to integrate the Spine Runtimes into software
+ * or otherwise create derivative works of the Spine Runtimes (collectively,
+ * "Products"), provided that each user of the Products must obtain their own
+ * Spine Editor license and redistribution of the Products in any form must
+ * include this license and copyright notice.
+ *
+ * THE SPINE RUNTIMES ARE PROVIDED BY ESOTERIC SOFTWARE LLC "AS IS" AND ANY
+ * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL ESOTERIC SOFTWARE LLC BE LIABLE FOR ANY
+ * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES,
+ * BUSINESS INTERRUPTION, OR LOSS OF USE, DATA, OR PROFITS) HOWEVER CAUSED AND
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
+ * THE SPINE RUNTIMES, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ *****************************************************************************/
+
+package com.esotericsoftware.spine;
+
+import com.badlogic.gdx.utils.Array;
+import com.badlogic.gdx.utils.FloatArray;
+import com.badlogic.gdx.utils.IntArray;
+import com.badlogic.gdx.utils.Null;
+import com.badlogic.gdx.utils.ObjectSet;
+import com.badlogic.gdx.utils.Pool;
+import com.badlogic.gdx.utils.Pool.Poolable;
+import com.badlogic.gdx.utils.SnapshotArray;
+
+import com.esotericsoftware.spine.Animation.AttachmentTimeline;
+import com.esotericsoftware.spine.Animation.DrawOrderTimeline;
+import com.esotericsoftware.spine.Animation.EventTimeline;
+import com.esotericsoftware.spine.Animation.MixBlend;
+import com.esotericsoftware.spine.Animation.MixDirection;
+import com.esotericsoftware.spine.Animation.RotateTimeline;
+import com.esotericsoftware.spine.Animation.Timeline;
+
+/** Applies animations over time, queues animations for later playback, mixes (crossfading) between animations, and applies
+ * multiple animations on top of each other (layering).
+ * <p>
+ * See <a href='http://esotericsoftware.com/spine-applying-animations/'>Applying Animations</a> in the Spine Runtimes Guide. */
+public class AnimationState {
+	static private final Animation emptyAnimation = new Animation("<empty>", new Array(0), 0);
+
+	/** 1) A previously applied timeline has set this property.<br>
+	 * Result: Mix from the current pose to the timeline pose. */
+	static private final int SUBSEQUENT = 0;
+	/** 1) This is the first timeline to set this property.<br>
+	 * 2) The next track entry applied after this one does not have a timeline to set this property.<br>
+	 * Result: Mix from the setup pose to the timeline pose. */
+	static private final int FIRST = 1;
+	/** 1) A previously applied timeline has set this property.<br>
+	 * 2) The next track entry to be applied does have a timeline to set this property.<br>
+	 * 3) The next track entry after that one does not have a timeline to set this property.<br>
+	 * Result: Mix from the current pose to the timeline pose, but do not mix out. This avoids "dipping" when crossfading
+	 * animations that key the same property. A subsequent timeline will set this property using a mix. */
+	static private final int HOLD_SUBSEQUENT = 2;
+	/** 1) This is the first timeline to set this property.<br>
+	 * 2) The next track entry to be applied does have a timeline to set this property.<br>
+	 * 3) The next track entry after that one does not have a timeline to set this property.<br>
+	 * Result: Mix from the setup pose to the timeline pose, but do not mix out. This avoids "dipping" when crossfading animations
+	 * that key the same property. A subsequent timeline will set this property using a mix. */
+	static private final int HOLD_FIRST = 3;
+	/** 1) This is the first timeline to set this property.<br>
+	 * 2) The next track entry to be applied does have a timeline to set this property.<br>
+	 * 3) The next track entry after that one does have a timeline to set this property.<br>
+	 * 4) timelineHoldMix stores the first subsequent track entry that does not have a timeline to set this property.<br>
+	 * Result: The same as HOLD except the mix percentage from the timelineHoldMix track entry is used. This handles when more than
+	 * 2 track entries in a row have a timeline that sets the same property.<br>
+	 * Eg, A -> B -> C -> D where A, B, and C have a timeline setting same property, but D does not. When A is applied, to avoid
+	 * "dipping" A is not mixed out, however D (the first entry that doesn't set the property) mixing in is used to mix out A
+	 * (which affects B and C). Without using D to mix out, A would be applied fully until mixing completes, then snap to the mixed
+	 * out position. */
+	static private final int HOLD_MIX = 4;
+
+	static private final int SETUP = 1, CURRENT = 2;
+
+	private AnimationStateData data;
+	final Array<TrackEntry> tracks = new Array();
+	private final Array<Event> events = new Array();
+	final SnapshotArray<AnimationStateListener> listeners = new SnapshotArray();
+	private final EventQueue queue = new EventQueue();
+	private final ObjectSet<String> propertyIds = new ObjectSet();
+	boolean animationsChanged;
+	private float timeScale = 1;
+	private int unkeyedState;
+
+	final Pool<TrackEntry> trackEntryPool = new Pool() {
+		protected Object newObject () {
+			return new TrackEntry();
+		}
+	};
+
+	/** Creates an uninitialized AnimationState. The animation state data must be set before use. */
+	public AnimationState () {
+	}
+
+	public AnimationState (AnimationStateData data) {
+		if (data == null) throw new IllegalArgumentException("data cannot be null.");
+		this.data = data;
+	}
+
+	/** Increments each track entry {@link TrackEntry#getTrackTime()}, setting queued animations as current if needed. */
+	public void update (float delta) {
+		delta *= timeScale;
+		Object[] tracks = this.tracks.items;
+		for (int i = 0, n = this.tracks.size; i < n; i++) {
+			TrackEntry current = (TrackEntry)tracks[i];
+			if (current == null) continue;
+
+			current.animationLast = current.nextAnimationLast;
+			current.trackLast = current.nextTrackLast;
+
+			float currentDelta = delta * current.timeScale;
+
+			if (current.delay > 0) {
+				current.delay -= currentDelta;
+				if (current.delay > 0) continue;
+				currentDelta = -current.delay;
+				current.delay = 0;
+			}
+
+			TrackEntry next = current.next;
+			if (next != null) {
+				// When the next entry's delay is passed, change to the next entry, preserving leftover time.
+				float nextTime = current.trackLast - next.delay;
+				if (nextTime >= 0) {
+					next.delay = 0;
+					next.trackTime += current.timeScale == 0 ? 0 : (nextTime / current.timeScale + delta) * next.timeScale;
+					current.trackTime += currentDelta;
+					setCurrent(i, next, true);
+					while (next.mixingFrom != null) {
+						next.mixTime += delta;
+						next = next.mixingFrom;
+					}
+					continue;
+				}
+			} else if (current.trackLast >= current.trackEnd && current.mixingFrom == null) {
+				// Clear the track when there is no next entry, the track end time is reached, and there is no mixingFrom.
+				tracks[i] = null;
+				queue.end(current);
+				clearNext(current);
+				continue;
+			}
+			if (current.mixingFrom != null && updateMixingFrom(current, delta)) {
+				// End mixing from entries once all have completed.
+				TrackEntry from = current.mixingFrom;
+				current.mixingFrom = null;
+				if (from != null) from.mixingTo = null;
+				while (from != null) {
+					queue.end(from);
+					from = from.mixingFrom;
+				}
+			}
+
+			current.trackTime += currentDelta;
+		}
+
+		queue.drain();
+	}
+
+	/** Returns true when all mixing from entries are complete. */
+	private boolean updateMixingFrom (TrackEntry to, float delta) {
+		TrackEntry from = to.mixingFrom;
+		if (from == null) return true;
+
+		boolean finished = updateMixingFrom(from, delta);
+
+		from.animationLast = from.nextAnimationLast;
+		from.trackLast = from.nextTrackLast;
+
+		// Require mixTime > 0 to ensure the mixing from entry was applied at least once.
+		if (to.mixTime > 0 && to.mixTime >= to.mixDuration) {
+			// Require totalAlpha == 0 to ensure mixing is complete, unless mixDuration == 0 (the transition is a single frame).
+			if (from.totalAlpha == 0 || to.mixDuration == 0) {
+				to.mixingFrom = from.mixingFrom;
+				if (from.mixingFrom != null) from.mixingFrom.mixingTo = to;
+				to.interruptAlpha = from.interruptAlpha;
+				queue.end(from);
+			}
+			return finished;
+		}
+
+		from.trackTime += delta * from.timeScale;
+		to.mixTime += delta;
+		return false;
+	}
+
+	/** Poses the skeleton using the track entry animations. The animation state is not changed, so can be applied to multiple
+	 * skeletons to pose them identically.
+	 * @return True if any animations were applied. */
+	public boolean apply (Skeleton skeleton) {
+		if (skeleton == null) throw new IllegalArgumentException("skeleton cannot be null.");
+		if (animationsChanged) animationsChanged();
+
+		Array<Event> events = this.events;
+		boolean applied = false;
+		Object[] tracks = this.tracks.items;
+		for (int i = 0, n = this.tracks.size; i < n; i++) {
+			TrackEntry current = (TrackEntry)tracks[i];
+			if (current == null || current.delay > 0) continue;
+			applied = true;
+
+			// Track 0 animations aren't for layering, so do not show the previously applied animations before the first key.
+			MixBlend blend = i == 0 ? MixBlend.first : current.mixBlend;
+
+			// Apply mixing from entries first.
+			float mix = current.alpha;
+			if (current.mixingFrom != null)
+				mix *= applyMixingFrom(current, skeleton, blend);
+			else if (current.trackTime >= current.trackEnd && current.next == null) //
+				mix = 0; // Set to setup pose the last time the entry will be applied.
+
+			// Apply current entry.
+			float animationLast = current.animationLast, animationTime = current.getAnimationTime(), applyTime = animationTime;
+			Array<Event> applyEvents = events;
+			if (current.reverse) {
+				applyTime = current.animation.duration - applyTime;
+				applyEvents = null;
+			}
+			int timelineCount = current.animation.timelines.size;
+			Object[] timelines = current.animation.timelines.items;
+			if ((i == 0 && mix == 1) || blend == MixBlend.add) {
+				for (int ii = 0; ii < timelineCount; ii++) {
+					Object timeline = timelines[ii];
+					if (timeline instanceof AttachmentTimeline)
+						applyAttachmentTimeline((AttachmentTimeline)timeline, skeleton, applyTime, blend, true);
+					else
+						((Timeline)timeline).apply(skeleton, animationLast, applyTime, applyEvents, mix, blend, MixDirection.in);
+				}
+			} else {
+				int[] timelineMode = current.timelineMode.items;
+
+				boolean firstFrame = current.timelinesRotation.size != timelineCount << 1;
+				if (firstFrame) current.timelinesRotation.setSize(timelineCount << 1);
+				float[] timelinesRotation = current.timelinesRotation.items;
+
+				for (int ii = 0; ii < timelineCount; ii++) {
+					Timeline timeline = (Timeline)timelines[ii];
+					MixBlend timelineBlend = timelineMode[ii] == SUBSEQUENT ? blend : MixBlend.setup;
+					if (timeline instanceof RotateTimeline) {
+						applyRotateTimeline((RotateTimeline)timeline, skeleton, applyTime, mix, timelineBlend, timelinesRotation,
+							ii << 1, firstFrame);
+					} else if (timeline instanceof AttachmentTimeline)
+						applyAttachmentTimeline((AttachmentTimeline)timeline, skeleton, applyTime, blend, true);
+					else
+						timeline.apply(skeleton, animationLast, applyTime, applyEvents, mix, timelineBlend, MixDirection.in);
+				}
+			}
+			queueEvents(current, animationTime);
+			events.clear();
+			current.nextAnimationLast = animationTime;
+			current.nextTrackLast = current.trackTime;
+		}
+
+		// Set slots attachments to the setup pose, if needed. This occurs if an animation that is mixing out sets attachments so
+		// subsequent timelines see any deform, but the subsequent timelines don't set an attachment (eg they are also mixing out or
+		// the time is before the first key).
+		int setupState = unkeyedState + SETUP;
+		Object[] slots = skeleton.slots.items;
+		for (int i = 0, n = skeleton.slots.size; i < n; i++) {
+			Slot slot = (Slot)slots[i];
+			if (slot.attachmentState == setupState) {
+				String attachmentName = slot.data.attachmentName;
+				slot.setAttachment(attachmentName == null ? null : skeleton.getAttachment(slot.data.index, attachmentName));
+			}
+		}
+		unkeyedState += 2; // Increasing after each use avoids the need to reset attachmentState for every slot.
+
+		queue.drain();
+		return applied;
+	}
+
+	private float applyMixingFrom (TrackEntry to, Skeleton skeleton, MixBlend blend) {
+		TrackEntry from = to.mixingFrom;
+		if (from.mixingFrom != null) applyMixingFrom(from, skeleton, blend);
+
+		float mix;
+		if (to.mixDuration == 0) { // Single frame mix to undo mixingFrom changes.
+			mix = 1;
+			if (blend == MixBlend.first) blend = MixBlend.setup; // Tracks >0 are transparent and can't reset to setup pose.
+		} else {
+			mix = to.mixTime / to.mixDuration;
+			if (mix > 1) mix = 1;
+			if (blend != MixBlend.first) blend = from.mixBlend; // Track 0 ignores track mix blend.
+		}
+
+		boolean attachments = mix < from.attachmentThreshold, drawOrder = mix < from.drawOrderThreshold;
+		int timelineCount = from.animation.timelines.size;
+		Object[] timelines = from.animation.timelines.items;
+		float alphaHold = from.alpha * to.interruptAlpha, alphaMix = alphaHold * (1 - mix);
+		float animationLast = from.animationLast, animationTime = from.getAnimationTime(), applyTime = animationTime;
+		Array<Event> events = null;
+		if (from.reverse)
+			applyTime = from.animation.duration - applyTime;
+		else {
+			if (mix < from.eventThreshold) events = this.events;
+		}
+
+		if (blend == MixBlend.add) {
+			for (int i = 0; i < timelineCount; i++)
+				((Timeline)timelines[i]).apply(skeleton, animationLast, applyTime, events, alphaMix, blend, MixDirection.out);
+		} else {
+			int[] timelineMode = from.timelineMode.items;
+			Object[] timelineHoldMix = from.timelineHoldMix.items;
+
+			boolean firstFrame = from.timelinesRotation.size != timelineCount << 1;
+			if (firstFrame) from.timelinesRotation.setSize(timelineCount << 1);
+			float[] timelinesRotation = from.timelinesRotation.items;
+
+			from.totalAlpha = 0;
+			for (int i = 0; i < timelineCount; i++) {
+				Timeline timeline = (Timeline)timelines[i];
+				MixDirection direction = MixDirection.out;
+				MixBlend timelineBlend;
+				float alpha;
+				switch (timelineMode[i]) {
+				case SUBSEQUENT:
+					if (!drawOrder && timeline instanceof DrawOrderTimeline) continue;
+					timelineBlend = blend;
+					alpha = alphaMix;
+					break;
+				case FIRST:
+					timelineBlend = MixBlend.setup;
+					alpha = alphaMix;
+					break;
+				case HOLD_SUBSEQUENT:
+					timelineBlend = blend;
+					alpha = alphaHold;
+					break;
+				case HOLD_FIRST:
+					timelineBlend = MixBlend.setup;
+					alpha = alphaHold;
+					break;
+				default: // HOLD_MIX
+					timelineBlend = MixBlend.setup;
+					TrackEntry holdMix = (TrackEntry)timelineHoldMix[i];
+					alpha = alphaHold * Math.max(0, 1 - holdMix.mixTime / holdMix.mixDuration);
+					break;
+				}
+				from.totalAlpha += alpha;
+				if (timeline instanceof RotateTimeline) {
+					applyRotateTimeline((RotateTimeline)timeline, skeleton, applyTime, alpha, timelineBlend, timelinesRotation, i << 1,
+						firstFrame);
+				} else if (timeline instanceof AttachmentTimeline)
+					applyAttachmentTimeline((AttachmentTimeline)timeline, skeleton, applyTime, timelineBlend, attachments);
+				else {
+					if (drawOrder && timeline instanceof DrawOrderTimeline && timelineBlend == MixBlend.setup)
+						direction = MixDirection.in;
+					timeline.apply(skeleton, animationLast, applyTime, events, alpha, timelineBlend, direction);
+				}
+			}
+		}
+
+		if (to.mixDuration > 0) queueEvents(from, animationTime);
+		this.events.clear();
+		from.nextAnimationLast = animationTime;
+		from.nextTrackLast = from.trackTime;
+
+		return mix;
+	}
+
+	/** Applies the attachment timeline and sets {@link Slot#attachmentState}.
+	 * @param attachments False when: 1) the attachment timeline is mixing out, 2) mix < attachmentThreshold, and 3) the timeline
+	 *           is not the last timeline to set the slot's attachment. In that case the timeline is applied only so subsequent
+	 *           timelines see any deform. */
+	private void applyAttachmentTimeline (AttachmentTimeline timeline, Skeleton skeleton, float time, MixBlend blend,
+		boolean attachments) {
+
+		Slot slot = skeleton.slots.get(timeline.slotIndex);
+		if (!slot.bone.active) return;
+
+		if (time < timeline.frames[0]) { // Time is before first frame.
+			if (blend == MixBlend.setup || blend == MixBlend.first)
+				setAttachment(skeleton, slot, slot.data.attachmentName, attachments);
+		} else
+			setAttachment(skeleton, slot, timeline.attachmentNames[Timeline.search(timeline.frames, time)], attachments);
+
+		// If an attachment wasn't set (ie before the first frame or attachments is false), set the setup attachment later.
+		if (slot.attachmentState <= unkeyedState) slot.attachmentState = unkeyedState + SETUP;
+	}
+
+	private void setAttachment (Skeleton skeleton, Slot slot, String attachmentName, boolean attachments) {
+		slot.setAttachment(attachmentName == null ? null : skeleton.getAttachment(slot.data.index, attachmentName));
+		if (attachments) slot.attachmentState = unkeyedState + CURRENT;
+	}
+
+	/** Applies the rotate timeline, mixing with the current pose while keeping the same rotation direction chosen as the shortest
+	 * the first time the mixing was applied. */
+	private void applyRotateTimeline (RotateTimeline timeline, Skeleton skeleton, float time, float alpha, MixBlend blend,
+		float[] timelinesRotation, int i, boolean firstFrame) {
+
+		if (firstFrame) timelinesRotation[i] = 0;
+
+		if (alpha == 1) {
+			timeline.apply(skeleton, 0, time, null, 1, blend, MixDirection.in);
+			return;
+		}
+
+		Bone bone = skeleton.bones.get(timeline.boneIndex);
+		if (!bone.active) return;
+		float[] frames = timeline.frames;
+		float r1, r2;
+		if (time < frames[0]) { // Time is before first frame.
+			switch (blend) {
+			case setup:
+				bone.rotation = bone.data.rotation;
+				// Fall through.
+			default:
+				return;
+			case first:
+				r1 = bone.rotation;
+				r2 = bone.data.rotation;
+			}
+		} else {
+			r1 = blend == MixBlend.setup ? bone.data.rotation : bone.rotation;
+			r2 = bone.data.rotation + timeline.getCurveValue(time);
+		}
+
+		// Mix between rotations using the direction of the shortest route on the first frame.
+		float total, diff = r2 - r1;
+		diff -= (16384 - (int)(16384.499999999996 - diff / 360)) * 360;
+		if (diff == 0)
+			total = timelinesRotation[i];
+		else {
+			float lastTotal, lastDiff;
+			if (firstFrame) {
+				lastTotal = 0;
+				lastDiff = diff;
+			} else {
+				lastTotal = timelinesRotation[i]; // Angle and direction of mix, including loops.
+				lastDiff = timelinesRotation[i + 1]; // Difference between bones.
+			}
+			boolean current = diff > 0, dir = lastTotal >= 0;
+			// Detect cross at 0 (not 180).
+			if (Math.signum(lastDiff) != Math.signum(diff) && Math.abs(lastDiff) <= 90) {
+				// A cross after a 360 rotation is a loop.
+				if (Math.abs(lastTotal) > 180) lastTotal += 360 * Math.signum(lastTotal);
+				dir = current;
+			}
+			total = diff + lastTotal - lastTotal % 360; // Store loops as part of lastTotal.
+			if (dir != current) total += 360 * Math.signum(lastTotal);
+			timelinesRotation[i] = total;
+		}
+		timelinesRotation[i + 1] = diff;
+		bone.rotation = r1 + total * alpha;
+	}
+
+	private void queueEvents (TrackEntry entry, float animationTime) {
+		float animationStart = entry.animationStart, animationEnd = entry.animationEnd;
+		float duration = animationEnd - animationStart;
+		float trackLastWrapped = entry.trackLast % duration;
+
+		// Queue events before complete.
+		Object[] events = this.events.items;
+		int i = 0, n = this.events.size;
+		for (; i < n; i++) {
+			Event event = (Event)events[i];
+			if (event.time < trackLastWrapped) break;
+			if (event.time > animationEnd) continue; // Discard events outside animation start/end.
+			queue.event(entry, event);
+		}
+
+		// Queue complete if completed a loop iteration or the animation.
+		boolean complete;
+		if (entry.loop)
+			complete = duration == 0 || trackLastWrapped > entry.trackTime % duration;
+		else
+			complete = animationTime >= animationEnd && entry.animationLast < animationEnd;
+		if (complete) queue.complete(entry);
+
+		// Queue events after complete.
+		for (; i < n; i++) {
+			Event event = (Event)events[i];
+			if (event.time < animationStart) continue; // Discard events outside animation start/end.
+			queue.event(entry, event);
+		}
+	}
+
+	/** Removes all animations from all tracks, leaving skeletons in their current pose.
+	 * <p>
+	 * It may be desired to use {@link AnimationState#setEmptyAnimations(float)} to mix the skeletons back to the setup pose,
+	 * rather than leaving them in their current pose. */
+	public void clearTracks () {
+		boolean oldDrainDisabled = queue.drainDisabled;
+		queue.drainDisabled = true;
+		for (int i = 0, n = tracks.size; i < n; i++)
+			clearTrack(i);
+		tracks.clear();
+		queue.drainDisabled = oldDrainDisabled;
+		queue.drain();
+	}
+
+	/** Removes all animations from the track, leaving skeletons in their current pose.
+	 * <p>
+	 * It may be desired to use {@link AnimationState#setEmptyAnimation(int, float)} to mix the skeletons back to the setup pose,
+	 * rather than leaving them in their current pose. */
+	public void clearTrack (int trackIndex) {
+		if (trackIndex < 0) throw new IllegalArgumentException("trackIndex must be >= 0.");
+		if (trackIndex >= tracks.size) return;
+		TrackEntry current = tracks.get(trackIndex);
+		if (current == null) return;
+
+		queue.end(current);
+
+		clearNext(current);
+
+		TrackEntry entry = current;
+		while (true) {
+			TrackEntry from = entry.mixingFrom;
+			if (from == null) break;
+			queue.end(from);
+			entry.mixingFrom = null;
+			entry.mixingTo = null;
+			entry = from;
+		}
+
+		tracks.set(current.trackIndex, null);
+
+		queue.drain();
+	}
+
+	private void setCurrent (int index, TrackEntry current, boolean interrupt) {
+		TrackEntry from = expandToIndex(index);
+		tracks.set(index, current);
+		current.previous = null;
+
+		if (from != null) {
+			if (interrupt) queue.interrupt(from);
+			current.mixingFrom = from;
+			from.mixingTo = current;
+			current.mixTime = 0;
+
+			// Store the interrupted mix percentage.
+			if (from.mixingFrom != null && from.mixDuration > 0)
+				current.interruptAlpha *= Math.min(1, from.mixTime / from.mixDuration);
+
+			from.timelinesRotation.clear(); // Reset rotation for mixing out, in case entry was mixed in.
+		}
+
+		queue.start(current);
+	}
+
+	/** Sets an animation by name.
+	 * <p>
+	 * See {@link #setAnimation(int, Animation, boolean)}. */
+	public TrackEntry setAnimation (int trackIndex, String animationName, boolean loop) {
+		Animation animation = data.skeletonData.findAnimation(animationName);
+		if (animation == null) throw new IllegalArgumentException("Animation not found: " + animationName);
+		return setAnimation(trackIndex, animation, loop);
+	}
+
+	/** Sets the current animation for a track, discarding any queued animations. If the formerly current track entry was never
+	 * applied to a skeleton, it is replaced (not mixed from).
+	 * @param loop If true, the animation will repeat. If false it will not, instead its last frame is applied if played beyond its
+	 *           duration. In either case {@link TrackEntry#getTrackEnd()} determines when the track is cleared.
+	 * @return A track entry to allow further customization of animation playback. References to the track entry must not be kept
+	 *         after the {@link AnimationStateListener#dispose(TrackEntry)} event occurs. */
+	public TrackEntry setAnimation (int trackIndex, Animation animation, boolean loop) {
+		if (trackIndex < 0) throw new IllegalArgumentException("trackIndex must be >= 0.");
+		if (animation == null) throw new IllegalArgumentException("animation cannot be null.");
+		boolean interrupt = true;
+		TrackEntry current = expandToIndex(trackIndex);
+		if (current != null) {
+			if (current.nextTrackLast == -1) {
+				// Don't mix from an entry that was never applied.
+				tracks.set(trackIndex, current.mixingFrom);
+				queue.interrupt(current);
+				queue.end(current);
+				clearNext(current);
+				current = current.mixingFrom;
+				interrupt = false; // mixingFrom is current again, but don't interrupt it twice.
+			} else
+				clearNext(current);
+		}
+		TrackEntry entry = trackEntry(trackIndex, animation, loop, current);
+		setCurrent(trackIndex, entry, interrupt);
+		queue.drain();
+		return entry;
+	}
+
+	/** Queues an animation by name.
+	 * <p>
+	 * See {@link #addAnimation(int, Animation, boolean, float)}. */
+	public TrackEntry addAnimation (int trackIndex, String animationName, boolean loop, float delay) {
+		Animation animation = data.skeletonData.findAnimation(animationName);
+		if (animation == null) throw new IllegalArgumentException("Animation not found: " + animationName);
+		return addAnimation(trackIndex, animation, loop, delay);
+	}
+
+	/** Adds an animation to be played after the current or last queued animation for a track. If the track is empty, it is
+	 * equivalent to calling {@link #setAnimation(int, Animation, boolean)}.
+	 * @param delay If > 0, sets {@link TrackEntry#getDelay()}. If <= 0, the delay set is the duration of the previous track entry
+	 *           minus any mix duration (from the {@link AnimationStateData}) plus the specified <code>delay</code> (ie the mix
+	 *           ends at (<code>delay</code> = 0) or before (<code>delay</code> < 0) the previous track entry duration). If the
+	 *           previous entry is looping, its next loop completion is used instead of its duration.
+	 * @return A track entry to allow further customization of animation playback. References to the track entry must not be kept
+	 *         after the {@link AnimationStateListener#dispose(TrackEntry)} event occurs. */
+	public TrackEntry addAnimation (int trackIndex, Animation animation, boolean loop, float delay) {
+		if (trackIndex < 0) throw new IllegalArgumentException("trackIndex must be >= 0.");
+		if (animation == null) throw new IllegalArgumentException("animation cannot be null.");
+
+		TrackEntry last = expandToIndex(trackIndex);
+		if (last != null) {
+			while (last.next != null)
+				last = last.next;
+		}
+
+		TrackEntry entry = trackEntry(trackIndex, animation, loop, last);
+
+		if (last == null) {
+			setCurrent(trackIndex, entry, true);
+			queue.drain();
+		} else {
+			last.next = entry;
+			entry.previous = last;
+			if (delay <= 0) delay += last.getTrackComplete() - entry.mixDuration;
+		}
+
+		entry.delay = delay;
+		return entry;
+	}
+
+	/** Sets an empty animation for a track, discarding any queued animations, and sets the track entry's
+	 * {@link TrackEntry#getMixDuration()}. An empty animation has no timelines and serves as a placeholder for mixing in or out.
+	 * <p>
+	 * Mixing out is done by setting an empty animation with a mix duration using either {@link #setEmptyAnimation(int, float)},
+	 * {@link #setEmptyAnimations(float)}, or {@link #addEmptyAnimation(int, float, float)}. Mixing to an empty animation causes
+	 * the previous animation to be applied less and less over the mix duration. Properties keyed in the previous animation
+	 * transition to the value from lower tracks or to the setup pose value if no lower tracks key the property. A mix duration of
+	 * 0 still mixes out over one frame.
+	 * <p>
+	 * Mixing in is done by first setting an empty animation, then adding an animation using
+	 * {@link #addAnimation(int, Animation, boolean, float)} with the desired delay (an empty animation has a duration of 0) and on
+	 * the returned track entry, set the {@link TrackEntry#setMixDuration(float)}. Mixing from an empty animation causes the new
+	 * animation to be applied more and more over the mix duration. Properties keyed in the new animation transition from the value
+	 * from lower tracks or from the setup pose value if no lower tracks key the property to the value keyed in the new
+	 * animation. */
+	public TrackEntry setEmptyAnimation (int trackIndex, float mixDuration) {
+		TrackEntry entry = setAnimation(trackIndex, emptyAnimation, false);
+		entry.mixDuration = mixDuration;
+		entry.trackEnd = mixDuration;
+		return entry;
+	}
+
+	/** Adds an empty animation to be played after the current or last queued animation for a track, and sets the track entry's
+	 * {@link TrackEntry#getMixDuration()}. If the track is empty, it is equivalent to calling
+	 * {@link #setEmptyAnimation(int, float)}.
+	 * <p>
+	 * See {@link #setEmptyAnimation(int, float)}.
+	 * @param delay If > 0, sets {@link TrackEntry#getDelay()}. If <= 0, the delay set is the duration of the previous track entry
+	 *           minus any mix duration plus the specified <code>delay</code> (ie the mix ends at (<code>delay</code> = 0) or
+	 *           before (<code>delay</code> < 0) the previous track entry duration). If the previous entry is looping, its next
+	 *           loop completion is used instead of its duration.
+	 * @return A track entry to allow further customization of animation playback. References to the track entry must not be kept
+	 *         after the {@link AnimationStateListener#dispose(TrackEntry)} event occurs. */
+	public TrackEntry addEmptyAnimation (int trackIndex, float mixDuration, float delay) {
+		TrackEntry entry = addAnimation(trackIndex, emptyAnimation, false, delay <= 0 ? 1 : delay);
+		entry.mixDuration = mixDuration;
+		entry.trackEnd = mixDuration;
+		if (delay <= 0 && entry.previous != null) entry.delay = entry.previous.getTrackComplete() - entry.mixDuration + delay;
+		return entry;
+	}
+
+	/** Sets an empty animation for every track, discarding any queued animations, and mixes to it over the specified mix
+	 * duration. */
+	public void setEmptyAnimations (float mixDuration) {
+		boolean oldDrainDisabled = queue.drainDisabled;
+		queue.drainDisabled = true;
+		Object[] tracks = this.tracks.items;
+		for (int i = 0, n = this.tracks.size; i < n; i++) {
+			TrackEntry current = (TrackEntry)tracks[i];
+			if (current != null) setEmptyAnimation(current.trackIndex, mixDuration);
+		}
+		queue.drainDisabled = oldDrainDisabled;
+		queue.drain();
+	}
+
+	private TrackEntry expandToIndex (int index) {
+		if (index < tracks.size) return tracks.get(index);
+		tracks.ensureCapacity(index - tracks.size + 1);
+		tracks.size = index + 1;
+		return null;
+	}
+
+	private TrackEntry trackEntry (int trackIndex, Animation animation, boolean loop, @Null TrackEntry last) {
+		TrackEntry entry = trackEntryPool.obtain();
+		entry.trackIndex = trackIndex;
+		entry.animation = animation;
+		entry.loop = loop;
+		entry.holdPrevious = false;
+
+		entry.eventThreshold = 0;
+		entry.attachmentThreshold = 0;
+		entry.drawOrderThreshold = 0;
+
+		entry.animationStart = 0;
+		entry.animationEnd = animation.getDuration();
+		entry.animationLast = -1;
+		entry.nextAnimationLast = -1;
+
+		entry.delay = 0;
+		entry.trackTime = 0;
+		entry.trackLast = -1;
+		entry.nextTrackLast = -1;
+		entry.trackEnd = Float.MAX_VALUE;
+		entry.timeScale = 1;
+
+		entry.alpha = 1;
+		entry.interruptAlpha = 1;
+		entry.mixTime = 0;
+		entry.mixDuration = last == null ? 0 : data.getMix(last.animation, animation);
+		entry.mixBlend = MixBlend.replace;
+		return entry;
+	}
+
+	/** Removes the {@link TrackEntry#getNext() next entry} and all entries after it for the specified entry. */
+	public void clearNext (TrackEntry entry) {
+		TrackEntry next = entry.next;
+		while (next != null) {
+			queue.dispose(next);
+			next = next.next;
+		}
+		entry.next = null;
+	}
+
+	void animationsChanged () {
+		animationsChanged = false;
+
+		// Process in the order that animations are applied.
+		propertyIds.clear(2048);
+		int n = tracks.size;
+		Object[] tracks = this.tracks.items;
+		for (int i = 0; i < n; i++) {
+			TrackEntry entry = (TrackEntry)tracks[i];
+			if (entry == null) continue;
+			while (entry.mixingFrom != null) // Move to last entry, then iterate in reverse.
+				entry = entry.mixingFrom;
+			do {
+				if (entry.mixingTo == null || entry.mixBlend != MixBlend.add) computeHold(entry);
+				entry = entry.mixingTo;
+			} while (entry != null);
+		}
+	}
+
+	private void computeHold (TrackEntry entry) {
+		TrackEntry to = entry.mixingTo;
+		Object[] timelines = entry.animation.timelines.items;
+		int timelinesCount = entry.animation.timelines.size;
+		int[] timelineMode = entry.timelineMode.setSize(timelinesCount);
+		entry.timelineHoldMix.clear();
+		Object[] timelineHoldMix = entry.timelineHoldMix.setSize(timelinesCount);
+		ObjectSet<String> propertyIds = this.propertyIds;
+
+		if (to != null && to.holdPrevious) {
+			for (int i = 0; i < timelinesCount; i++)
+				timelineMode[i] = propertyIds.addAll(((Timeline)timelines[i]).getPropertyIds()) ? HOLD_FIRST : HOLD_SUBSEQUENT;
+			return;
+		}
+
+		outer:
+		for (int i = 0; i < timelinesCount; i++) {
+			Timeline timeline = (Timeline)timelines[i];
+			String[] ids = timeline.getPropertyIds();
+			if (!propertyIds.addAll(ids))
+				timelineMode[i] = SUBSEQUENT;
+			else if (to == null || timeline instanceof AttachmentTimeline || timeline instanceof DrawOrderTimeline
+				|| timeline instanceof EventTimeline || !to.animation.hasTimeline(ids)) {
+				timelineMode[i] = FIRST;
+			} else {
+				for (TrackEntry next = to.mixingTo; next != null; next = next.mixingTo) {
+					if (next.animation.hasTimeline(ids)) continue;
+					if (next.mixDuration > 0) {
+						timelineMode[i] = HOLD_MIX;
+						timelineHoldMix[i] = next;
+						continue outer;
+					}
+					break;
+				}
+				timelineMode[i] = HOLD_FIRST;
+			}
+		}
+	}
+
+	/** Returns the track entry for the animation currently playing on the track, or null if no animation is currently playing. */
+	public @Null TrackEntry getCurrent (int trackIndex) {
+		if (trackIndex < 0) throw new IllegalArgumentException("trackIndex must be >= 0.");
+		if (trackIndex >= tracks.size) return null;
+		return tracks.get(trackIndex);
+	}
+
+	/** Adds a listener to receive events for all track entries. */
+	public void addListener (AnimationStateListener listener) {
+		if (listener == null) throw new IllegalArgumentException("listener cannot be null.");
+		listeners.add(listener);
+	}
+
+	/** Removes the listener added with {@link #addListener(AnimationStateListener)}. */
+	public void removeListener (AnimationStateListener listener) {
+		listeners.removeValue(listener, true);
+	}
+
+	/** Removes all listeners added with {@link #addListener(AnimationStateListener)}. */
+	public void clearListeners () {
+		listeners.clear();
+	}
+
+	/** Discards all listener notifications that have not yet been delivered. This can be useful to call from an
+	 * {@link AnimationStateListener} when it is known that further notifications that may have been already queued for delivery
+	 * are not wanted because new animations are being set. */
+	public void clearListenerNotifications () {
+		queue.clear();
+	}
+
+	/** Multiplier for the delta time when the animation state is updated, causing time for all animations and mixes to play slower
+	 * or faster. Defaults to 1.
+	 * <p>
+	 * See TrackEntry {@link TrackEntry#getTimeScale()} for affecting a single animation. */
+	public float getTimeScale () {
+		return timeScale;
+	}
+
+	public void setTimeScale (float timeScale) {
+		this.timeScale = timeScale;
+	}
+
+	/** The AnimationStateData to look up mix durations. */
+	public AnimationStateData getData () {
+		return data;
+	}
+
+	public void setData (AnimationStateData data) {
+		if (data == null) throw new IllegalArgumentException("data cannot be null.");
+		this.data = data;
+	}
+
+	/** The list of tracks that have had animations, which may contain null entries for tracks that currently have no animation. */
+	public Array<TrackEntry> getTracks () {
+		return tracks;
+	}
+
+	public String toString () {
+		StringBuilder buffer = new StringBuilder(64);
+		Object[] tracks = this.tracks.items;
+		for (int i = 0, n = this.tracks.size; i < n; i++) {
+			TrackEntry entry = (TrackEntry)tracks[i];
+			if (entry == null) continue;
+			if (buffer.length() > 0) buffer.append(", ");
+			buffer.append(entry.toString());
+		}
+		if (buffer.length() == 0) return "<none>";
+		return buffer.toString();
+	}
+
+	/** Stores settings and other state for the playback of an animation on an {@link AnimationState} track.
+	 * <p>
+	 * References to a track entry must not be kept after the {@link AnimationStateListener#dispose(TrackEntry)} event occurs. */
+	static public class TrackEntry implements Poolable {
+		Animation animation;
+		@Null TrackEntry previous, next, mixingFrom, mixingTo;
+		@Null AnimationStateListener listener;
+		int trackIndex;
+		boolean loop, holdPrevious, reverse;
+		float eventThreshold, attachmentThreshold, drawOrderThreshold;
+		float animationStart, animationEnd, animationLast, nextAnimationLast;
+		float delay, trackTime, trackLast, nextTrackLast, trackEnd, timeScale;
+		float alpha, mixTime, mixDuration, interruptAlpha, totalAlpha;
+		MixBlend mixBlend = MixBlend.replace;
+
+		final IntArray timelineMode = new IntArray();
+		final Array<TrackEntry> timelineHoldMix = new Array();
+		final FloatArray timelinesRotation = new FloatArray();
+
+		public void reset () {
+			previous = null;
+			next = null;
+			mixingFrom = null;
+			mixingTo = null;
+			animation = null;
+			listener = null;
+			timelineMode.clear();
+			timelineHoldMix.clear();
+			timelinesRotation.clear();
+		}
+
+		/** The index of the track where this track entry is either current or queued.
+		 * <p>
+		 * See {@link AnimationState#getCurrent(int)}. */
+		public int getTrackIndex () {
+			return trackIndex;
+		}
+
+		/** The animation to apply for this track entry. */
+		public Animation getAnimation () {
+			return animation;
+		}
+
+		public void setAnimation (Animation animation) {
+			if (animation == null) throw new IllegalArgumentException("animation cannot be null.");
+			this.animation = animation;
+		}
+
+		/** If true, the animation will repeat. If false it will not, instead its last frame is applied if played beyond its
+		 * duration. */
+		public boolean getLoop () {
+			return loop;
+		}
+
+		public void setLoop (boolean loop) {
+			this.loop = loop;
+		}
+
+		/** Seconds to postpone playing the animation. When this track entry is the current track entry, <code>delay</code>
+		 * postpones incrementing the {@link #getTrackTime()}. When this track entry is queued, <code>delay</code> is the time from
+		 * the start of the previous animation to when this track entry will become the current track entry (ie when the previous
+		 * track entry {@link TrackEntry#getTrackTime()} >= this track entry's <code>delay</code>).
+		 * <p>
+		 * {@link #getTimeScale()} affects the delay.
+		 * <p>
+		 * When using {@link AnimationState#addAnimation(int, Animation, boolean, float)} with a <code>delay</code> <= 0, the delay
+		 * is set using the mix duration from the {@link AnimationStateData}. If {@link #mixDuration} is set afterward, the delay
+		 * may need to be adjusted. */
+		public float getDelay () {
+			return delay;
+		}
+
+		public void setDelay (float delay) {
+			this.delay = delay;
+		}
+
+		/** Current time in seconds this track entry has been the current track entry. The track time determines
+		 * {@link #getAnimationTime()}. The track time can be set to start the animation at a time other than 0, without affecting
+		 * looping. */
+		public float getTrackTime () {
+			return trackTime;
+		}
+
+		public void setTrackTime (float trackTime) {
+			this.trackTime = trackTime;
+		}
+
+		/** The track time in seconds when this animation will be removed from the track. Defaults to the highest possible float
+		 * value, meaning the animation will be applied until a new animation is set or the track is cleared. If the track end time
+		 * is reached, no other animations are queued for playback, and mixing from any previous animations is complete, then the
+		 * properties keyed by the animation are set to the setup pose and the track is cleared.
+		 * <p>
+		 * It may be desired to use {@link AnimationState#addEmptyAnimation(int, float, float)} rather than have the animation
+		 * abruptly cease being applied. */
+		public float getTrackEnd () {
+			return trackEnd;
+		}
+
+		public void setTrackEnd (float trackEnd) {
+			this.trackEnd = trackEnd;
+		}
+
+		/** If this track entry is non-looping, the track time in seconds when {@link #getAnimationEnd()} is reached, or the current
+		 * {@link #getTrackTime()} if it has already been reached. If this track entry is looping, the track time when this
+		 * animation will reach its next {@link #getAnimationEnd()} (the next loop completion). */
+		public float getTrackComplete () {
+			float duration = animationEnd - animationStart;
+			if (duration != 0) {
+				if (loop) return duration * (1 + (int)(trackTime / duration)); // Completion of next loop.
+				if (trackTime < duration) return duration; // Before duration.
+			}
+			return trackTime; // Next update.
+		}
+
+		/** Seconds when this animation starts, both initially and after looping. Defaults to 0.
+		 * <p>
+		 * When changing the <code>animationStart</code> time, it often makes sense to set {@link #getAnimationLast()} to the same
+		 * value to prevent timeline keys before the start time from triggering. */
+		public float getAnimationStart () {
+			return animationStart;
+		}
+
+		public void setAnimationStart (float animationStart) {
+			this.animationStart = animationStart;
+		}
+
+		/** Seconds for the last frame of this animation. Non-looping animations won't play past this time. Looping animations will
+		 * loop back to {@link #getAnimationStart()} at this time. Defaults to the animation {@link Animation#duration}. */
+		public float getAnimationEnd () {
+			return animationEnd;
+		}
+
+		public void setAnimationEnd (float animationEnd) {
+			this.animationEnd = animationEnd;
+		}
+
+		/** The time in seconds this animation was last applied. Some timelines use this for one-time triggers. Eg, when this
+		 * animation is applied, event timelines will fire all events between the <code>animationLast</code> time (exclusive) and
+		 * <code>animationTime</code> (inclusive). Defaults to -1 to ensure triggers on frame 0 happen the first time this animation
+		 * is applied. */
+		public float getAnimationLast () {
+			return animationLast;
+		}
+
+		public void setAnimationLast (float animationLast) {
+			this.animationLast = animationLast;
+			nextAnimationLast = animationLast;
+		}
+
+		/** Uses {@link #getTrackTime()} to compute the <code>animationTime</code>, which is between {@link #getAnimationStart()}
+		 * and {@link #getAnimationEnd()}. When the <code>trackTime</code> is 0, the <code>animationTime</code> is equal to the
+		 * <code>animationStart</code> time. */
+		public float getAnimationTime () {
+			if (loop) {
+				float duration = animationEnd - animationStart;
+				if (duration == 0) return animationStart;
+				return (trackTime % duration) + animationStart;
+			}
+			return Math.min(trackTime + animationStart, animationEnd);
+		}
+
+		/** Multiplier for the delta time when this track entry is updated, causing time for this animation to pass slower or
+		 * faster. Defaults to 1.
+		 * <p>
+		 * Values < 0 are not supported. To play an animation in reverse, use {@link #getReverse()}.
+		 * <p>
+		 * {@link #getMixTime()} is not affected by track entry time scale, so {@link #getMixDuration()} may need to be adjusted to
+		 * match the animation speed.
+		 * <p>
+		 * When using {@link AnimationState#addAnimation(int, Animation, boolean, float)} with a <code>delay</code> <= 0, the
+		 * {@link #getDelay()} is set using the mix duration from the {@link AnimationStateData}, assuming time scale to be 1. If
+		 * the time scale is not 1, the delay may need to be adjusted.
+		 * <p>
+		 * See AnimationState {@link AnimationState#getTimeScale()} for affecting all animations. */
+		public float getTimeScale () {
+			return timeScale;
+		}
+
+		public void setTimeScale (float timeScale) {
+			this.timeScale = timeScale;
+		}
+
+		/** The listener for events generated by this track entry, or null.
+		 * <p>
+		 * A track entry returned from {@link AnimationState#setAnimation(int, Animation, boolean)} is already the current animation
+		 * for the track, so the track entry listener {@link AnimationStateListener#start(TrackEntry)} will not be called. */
+		public @Null AnimationStateListener getListener () {
+			return listener;
+		}
+
+		public void setListener (@Null AnimationStateListener listener) {
+			this.listener = listener;
+		}
+
+		/** Values < 1 mix this animation with the skeleton's current pose (usually the pose resulting from lower tracks). Defaults
+		 * to 1, which overwrites the skeleton's current pose with this animation.
+		 * <p>
+		 * Typically track 0 is used to completely pose the skeleton, then alpha is used on higher tracks. It doesn't make sense to
+		 * use alpha on track 0 if the skeleton pose is from the last frame render. */
+		public float getAlpha () {
+			return alpha;
+		}
+
+		public void setAlpha (float alpha) {
+			this.alpha = alpha;
+		}
+
+		/** When the mix percentage ({@link #getMixTime()} / {@link #getMixDuration()}) is less than the
+		 * <code>eventThreshold</code>, event timelines are applied while this animation is being mixed out. Defaults to 0, so event
+		 * timelines are not applied while this animation is being mixed out. */
+		public float getEventThreshold () {
+			return eventThreshold;
+		}
+
+		public void setEventThreshold (float eventThreshold) {
+			this.eventThreshold = eventThreshold;
+		}
+
+		/** When the mix percentage ({@link #getMixTime()} / {@link #getMixDuration()}) is less than the
+		 * <code>attachmentThreshold</code>, attachment timelines are applied while this animation is being mixed out. Defaults to
+		 * 0, so attachment timelines are not applied while this animation is being mixed out. */
+		public float getAttachmentThreshold () {
+			return attachmentThreshold;
+		}
+
+		public void setAttachmentThreshold (float attachmentThreshold) {
+			this.attachmentThreshold = attachmentThreshold;
+		}
+
+		/** When the mix percentage ({@link #getMixTime()} / {@link #getMixDuration()}) is less than the
+		 * <code>drawOrderThreshold</code>, draw order timelines are applied while this animation is being mixed out. Defaults to 0,
+		 * so draw order timelines are not applied while this animation is being mixed out. */
+		public float getDrawOrderThreshold () {
+			return drawOrderThreshold;
+		}
+
+		public void setDrawOrderThreshold (float drawOrderThreshold) {
+			this.drawOrderThreshold = drawOrderThreshold;
+		}
+
+		/** The animation queued to start after this animation, or null if there is none. <code>next</code> makes up a doubly linked
+		 * list.
+		 * <p>
+		 * See {@link AnimationState#clearNext(TrackEntry)} to truncate the list. */
+		public @Null TrackEntry getNext () {
+			return next;
+		}
+
+		/** The animation queued to play before this animation, or null. <code>previous</code> makes up a doubly linked list. */
+		public @Null TrackEntry getPrevious () {
+			return previous;
+		}
+
+		/** Returns true if at least one loop has been completed.
+		 * <p>
+		 * See {@link AnimationStateListener#complete(TrackEntry)}. */
+		public boolean isComplete () {
+			return trackTime >= animationEnd - animationStart;
+		}
+
+		/** Seconds from 0 to the {@link #getMixDuration()} when mixing from the previous animation to this animation. May be
+		 * slightly more than <code>mixDuration</code> when the mix is complete. */
+		public float getMixTime () {
+			return mixTime;
+		}
+
+		public void setMixTime (float mixTime) {
+			this.mixTime = mixTime;
+		}
+
+		/** Seconds for mixing from the previous animation to this animation. Defaults to the value provided by AnimationStateData
+		 * {@link AnimationStateData#getMix(Animation, Animation)} based on the animation before this animation (if any).
+		 * <p>
+		 * A mix duration of 0 still mixes out over one frame to provide the track entry being mixed out a chance to revert the
+		 * properties it was animating.
+		 * <p>
+		 * The <code>mixDuration</code> can be set manually rather than use the value from
+		 * {@link AnimationStateData#getMix(Animation, Animation)}. In that case, the <code>mixDuration</code> can be set for a new
+		 * track entry only before {@link AnimationState#update(float)} is first called.
+		 * <p>
+		 * When using {@link AnimationState#addAnimation(int, Animation, boolean, float)} with a <code>delay</code> <= 0, the
+		 * {@link #getDelay()} is set using the mix duration from the {@link AnimationStateData}. If <code>mixDuration</code> is set
+		 * afterward, the delay may need to be adjusted. For example:
+		 * <code>entry.delay = entry.previous.getTrackComplete() - entry.mixDuration;</code> */
+		public float getMixDuration () {
+			return mixDuration;
+		}
+
+		public void setMixDuration (float mixDuration) {
+			this.mixDuration = mixDuration;
+		}
+
+		/** Controls how properties keyed in the animation are mixed with lower tracks. Defaults to {@link MixBlend#replace}.
+		 * <p>
+		 * Track entries on track 0 ignore this setting and always use {@link MixBlend#first}.
+		 * <p>
+		 * The <code>mixBlend</code> can be set for a new track entry only before {@link AnimationState#apply(Skeleton)} is first
+		 * called. */
+		public MixBlend getMixBlend () {
+			return mixBlend;
+		}
+
+		public void setMixBlend (MixBlend mixBlend) {
+			if (mixBlend == null) throw new IllegalArgumentException("mixBlend cannot be null.");
+			this.mixBlend = mixBlend;
+		}
+
+		/** The track entry for the previous animation when mixing from the previous animation to this animation, or null if no
+		 * mixing is currently occuring. When mixing from multiple animations, <code>mixingFrom</code> makes up a linked list. */
+		public @Null TrackEntry getMixingFrom () {
+			return mixingFrom;
+		}
+
+		/** The track entry for the next animation when mixing from this animation to the next animation, or null if no mixing is
+		 * currently occuring. When mixing to multiple animations, <code>mixingTo</code> makes up a linked list. */
+		public @Null TrackEntry getMixingTo () {
+			return mixingTo;
+		}
+
+		public void setHoldPrevious (boolean holdPrevious) {
+			this.holdPrevious = holdPrevious;
+		}
+
+		/** If true, when mixing from the previous animation to this animation, the previous animation is applied as normal instead
+		 * of being mixed out.
+		 * <p>
+		 * When mixing between animations that key the same property, if a lower track also keys that property then the value will
+		 * briefly dip toward the lower track value during the mix. This happens because the first animation mixes from 100% to 0%
+		 * while the second animation mixes from 0% to 100%. Setting <code>holdPrevious</code> to true applies the first animation
+		 * at 100% during the mix so the lower track value is overwritten. Such dipping does not occur on the lowest track which
+		 * keys the property, only when a higher track also keys the property.
+		 * <p>
+		 * Snapping will occur if <code>holdPrevious</code> is true and this animation does not key all the same properties as the
+		 * previous animation. */
+		public boolean getHoldPrevious () {
+			return holdPrevious;
+		}
+
+		/** Resets the rotation directions for mixing this entry's rotate timelines. This can be useful to avoid bones rotating the
+		 * long way around when using {@link #alpha} and starting animations on other tracks.
+		 * <p>
+		 * Mixing with {@link MixBlend#replace} involves finding a rotation between two others, which has two possible solutions:
+		 * the short way or the long way around. The two rotations likely change over time, so which direction is the short or long
+		 * way also changes. If the short way was always chosen, bones would flip to the other side when that direction became the
+		 * long way. TrackEntry chooses the short way the first time it is applied and remembers that direction. */
+		public void resetRotationDirections () {
+			timelinesRotation.clear();
+		}
+
+		public void setReverse (boolean reverse) {
+			this.reverse = reverse;
+		}
+
+		/** If true, the animation will be applied in reverse. Events are not fired when an animation is applied in reverse. */
+		public boolean getReverse () {
+			return reverse;
+		}
+
+		public String toString () {
+			return animation == null ? "<none>" : animation.name;
+		}
+	}
+
+	class EventQueue {
+		private final Array objects = new Array();
+		boolean drainDisabled;
+
+		void start (TrackEntry entry) {
+			objects.add(EventType.start);
+			objects.add(entry);
+			animationsChanged = true;
+		}
+
+		void interrupt (TrackEntry entry) {
+			objects.add(EventType.interrupt);
+			objects.add(entry);
+		}
+
+		void end (TrackEntry entry) {
+			objects.add(EventType.end);
+			objects.add(entry);
+			animationsChanged = true;
+		}
+
+		void dispose (TrackEntry entry) {
+			objects.add(EventType.dispose);
+			objects.add(entry);
+		}
+
+		void complete (TrackEntry entry) {
+			objects.add(EventType.complete);
+			objects.add(entry);
+		}
+
+		void event (TrackEntry entry, Event event) {
+			objects.add(EventType.event);
+			objects.add(entry);
+			objects.add(event);
+		}
+
+		void drain () {
+			if (drainDisabled) return; // Not reentrant.
+			drainDisabled = true;
+
+			SnapshotArray<AnimationStateListener> listenersArray = AnimationState.this.listeners;
+			for (int i = 0; i < this.objects.size; i += 2) {
+				EventType type = (EventType)objects.get(i);
+				TrackEntry entry = (TrackEntry)objects.get(i + 1);
+				int listenersCount = listenersArray.size;
+				Object[] listeners = listenersArray.begin();
+				switch (type) {
+				case start:
+					if (entry.listener != null) entry.listener.start(entry);
+					for (int ii = 0; ii < listenersCount; ii++)
+						((AnimationStateListener)listeners[ii]).start(entry);
+					break;
+				case interrupt:
+					if (entry.listener != null) entry.listener.interrupt(entry);
+					for (int ii = 0; ii < listenersCount; ii++)
+						((AnimationStateListener)listeners[ii]).interrupt(entry);
+					break;
+				case end:
+					if (entry.listener != null) entry.listener.end(entry);
+					for (int ii = 0; ii < listenersCount; ii++)
+						((AnimationStateListener)listeners[ii]).end(entry);
+					// Fall through.
+				case dispose:
+					if (entry.listener != null) entry.listener.dispose(entry);
+					for (int ii = 0; ii < listenersCount; ii++)
+						((AnimationStateListener)listeners[ii]).dispose(entry);
+					trackEntryPool.free(entry);
+					break;
+				case complete:
+					if (entry.listener != null) entry.listener.complete(entry);
+					for (int ii = 0; ii < listenersCount; ii++)
+						((AnimationStateListener)listeners[ii]).complete(entry);
+					break;
+				case event:
+					Event event = (Event)objects.get(i++ + 2);
+					if (entry.listener != null) entry.listener.event(entry, event);
+					for (int ii = 0; ii < listenersCount; ii++)
+						((AnimationStateListener)listeners[ii]).event(entry, event);
+					break;
+				}
+				listenersArray.end();
+			}
+			clear();
+
+			drainDisabled = false;
+		}
+
+		void clear () {
+			objects.clear();
+		}
+	}
+
+	static private enum EventType {
+		start, interrupt, end, dispose, complete, event
+	}
+
+	/** The interface to implement for receiving TrackEntry events. It is always safe to call AnimationState methods when receiving
+	 * events.
+	 * <p>
+	 * See TrackEntry {@link TrackEntry#setListener(AnimationStateListener)} and AnimationState
+	 * {@link AnimationState#addListener(AnimationStateListener)}. */
+	static public interface AnimationStateListener {
+		/** Invoked when this entry has been set as the current entry. */
+		public void start (TrackEntry entry);
+
+		/** Invoked when another entry has replaced this entry as the current entry. This entry may continue being applied for
+		 * mixing. */
+		public void interrupt (TrackEntry entry);
+
+		/** Invoked when this entry is no longer the current entry and will never be applied again. */
+		public void end (TrackEntry entry);
+
+		/** Invoked when this entry will be disposed. This may occur without the entry ever being set as the current entry.
+		 * References to the entry should not be kept after <code>dispose</code> is called, as it may be destroyed or reused. */
+		public void dispose (TrackEntry entry);
+
+		/** Invoked every time this entry's animation completes a loop. Because this event is trigged in
+		 * {@link AnimationState#apply(Skeleton)}, any animations set in response to the event won't be applied until the next time
+		 * the AnimationState is applied. */
+		public void complete (TrackEntry entry);
+
+		/** Invoked when this entry's animation triggers an event. Because this event is trigged in
+		 * {@link AnimationState#apply(Skeleton)}, any animations set in response to the event won't be applied until the next time
+		 * the AnimationState is applied. */
+		public void event (TrackEntry entry, Event event);
+	}
+
+	static public abstract class AnimationStateAdapter implements AnimationStateListener {
+		public void start (TrackEntry entry) {
+		}
+
+		public void interrupt (TrackEntry entry) {
+		}
+
+		public void end (TrackEntry entry) {
+		}
+
+		public void dispose (TrackEntry entry) {
+		}
+
+		public void complete (TrackEntry entry) {
+		}
+
+		public void event (TrackEntry entry, Event event) {
+		}
+	}
+}

+ 119 - 119
spine-libgdx/spine-libgdx/src/com/esotericsoftware/spine/AnimationStateData.java

@@ -1,119 +1,119 @@
-/******************************************************************************
- * Spine Runtimes License Agreement
- * Last updated January 1, 2020. Replaces all prior versions.
- *
- * Copyright (c) 2013-2020, Esoteric Software LLC
- *
- * Integration of the Spine Runtimes into software or otherwise creating
- * derivative works of the Spine Runtimes is permitted under the terms and
- * conditions of Section 2 of the Spine Editor License Agreement:
- * http://esotericsoftware.com/spine-editor-license
- *
- * Otherwise, it is permitted to integrate the Spine Runtimes into software
- * or otherwise create derivative works of the Spine Runtimes (collectively,
- * "Products"), provided that each user of the Products must obtain their own
- * Spine Editor license and redistribution of the Products in any form must
- * include this license and copyright notice.
- *
- * THE SPINE RUNTIMES ARE PROVIDED BY ESOTERIC SOFTWARE LLC "AS IS" AND ANY
- * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
- * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
- * DISCLAIMED. IN NO EVENT SHALL ESOTERIC SOFTWARE LLC BE LIABLE FOR ANY
- * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
- * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES,
- * BUSINESS INTERRUPTION, OR LOSS OF USE, DATA, OR PROFITS) HOWEVER CAUSED AND
- * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
- * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
- * THE SPINE RUNTIMES, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
- *****************************************************************************/
-
-package com.esotericsoftware.spine;
-
-import com.badlogic.gdx.utils.ObjectFloatMap;
-
-import com.esotericsoftware.spine.AnimationState.TrackEntry;
-
-/** Stores mix (crossfade) durations to be applied when {@link AnimationState} animations are changed. */
-public class AnimationStateData {
-	final SkeletonData skeletonData;
-	final ObjectFloatMap<Key> animationToMixTime = new ObjectFloatMap(51, 0.8f);
-	final Key tempKey = new Key();
-	float defaultMix;
-
-	public AnimationStateData (SkeletonData skeletonData) {
-		if (skeletonData == null) throw new IllegalArgumentException("skeletonData cannot be null.");
-		this.skeletonData = skeletonData;
-	}
-
-	/** The SkeletonData to look up animations when they are specified by name. */
-	public SkeletonData getSkeletonData () {
-		return skeletonData;
-	}
-
-	/** Sets a mix duration by animation name.
-	 * <p>
-	 * See {@link #setMix(Animation, Animation, float)}. */
-	public void setMix (String fromName, String toName, float duration) {
-		Animation from = skeletonData.findAnimation(fromName);
-		if (from == null) throw new IllegalArgumentException("Animation not found: " + fromName);
-		Animation to = skeletonData.findAnimation(toName);
-		if (to == null) throw new IllegalArgumentException("Animation not found: " + toName);
-		setMix(from, to, duration);
-	}
-
-	/** Sets the mix duration when changing from the specified animation to the other.
-	 * <p>
-	 * See {@link TrackEntry#mixDuration}. */
-	public void setMix (Animation from, Animation to, float duration) {
-		if (from == null) throw new IllegalArgumentException("from cannot be null.");
-		if (to == null) throw new IllegalArgumentException("to cannot be null.");
-		Key key = new Key();
-		key.a1 = from;
-		key.a2 = to;
-		animationToMixTime.put(key, duration);
-	}
-
-	/** Returns the mix duration to use when changing from the specified animation to the other, or the {@link #getDefaultMix()} if
-	 * no mix duration has been set. */
-	public float getMix (Animation from, Animation to) {
-		if (from == null) throw new IllegalArgumentException("from cannot be null.");
-		if (to == null) throw new IllegalArgumentException("to cannot be null.");
-		tempKey.a1 = from;
-		tempKey.a2 = to;
-		return animationToMixTime.get(tempKey, defaultMix);
-	}
-
-	/** The mix duration to use when no mix duration has been defined between two animations. */
-	public float getDefaultMix () {
-		return defaultMix;
-	}
-
-	public void setDefaultMix (float defaultMix) {
-		this.defaultMix = defaultMix;
-	}
-
-	static class Key {
-		Animation a1, a2;
-
-		public int hashCode () {
-			return 31 * (31 + a1.hashCode()) + a2.hashCode();
-		}
-
-		public boolean equals (Object obj) {
-			if (this == obj) return true;
-			if (obj == null) return false;
-			Key other = (Key)obj;
-			if (a1 == null) {
-				if (other.a1 != null) return false;
-			} else if (!a1.equals(other.a1)) return false;
-			if (a2 == null) {
-				if (other.a2 != null) return false;
-			} else if (!a2.equals(other.a2)) return false;
-			return true;
-		}
-
-		public String toString () {
-			return a1.name + "->" + a2.name;
-		}
-	}
-}
+/******************************************************************************
+ * Spine Runtimes License Agreement
+ * Last updated January 1, 2020. Replaces all prior versions.
+ *
+ * Copyright (c) 2013-2020, Esoteric Software LLC
+ *
+ * Integration of the Spine Runtimes into software or otherwise creating
+ * derivative works of the Spine Runtimes is permitted under the terms and
+ * conditions of Section 2 of the Spine Editor License Agreement:
+ * http://esotericsoftware.com/spine-editor-license
+ *
+ * Otherwise, it is permitted to integrate the Spine Runtimes into software
+ * or otherwise create derivative works of the Spine Runtimes (collectively,
+ * "Products"), provided that each user of the Products must obtain their own
+ * Spine Editor license and redistribution of the Products in any form must
+ * include this license and copyright notice.
+ *
+ * THE SPINE RUNTIMES ARE PROVIDED BY ESOTERIC SOFTWARE LLC "AS IS" AND ANY
+ * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL ESOTERIC SOFTWARE LLC BE LIABLE FOR ANY
+ * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES,
+ * BUSINESS INTERRUPTION, OR LOSS OF USE, DATA, OR PROFITS) HOWEVER CAUSED AND
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
+ * THE SPINE RUNTIMES, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ *****************************************************************************/
+
+package com.esotericsoftware.spine;
+
+import com.badlogic.gdx.utils.ObjectFloatMap;
+
+import com.esotericsoftware.spine.AnimationState.TrackEntry;
+
+/** Stores mix (crossfade) durations to be applied when {@link AnimationState} animations are changed. */
+public class AnimationStateData {
+	final SkeletonData skeletonData;
+	final ObjectFloatMap<Key> animationToMixTime = new ObjectFloatMap(51, 0.8f);
+	final Key tempKey = new Key();
+	float defaultMix;
+
+	public AnimationStateData (SkeletonData skeletonData) {
+		if (skeletonData == null) throw new IllegalArgumentException("skeletonData cannot be null.");
+		this.skeletonData = skeletonData;
+	}
+
+	/** The SkeletonData to look up animations when they are specified by name. */
+	public SkeletonData getSkeletonData () {
+		return skeletonData;
+	}
+
+	/** Sets a mix duration by animation name.
+	 * <p>
+	 * See {@link #setMix(Animation, Animation, float)}. */
+	public void setMix (String fromName, String toName, float duration) {
+		Animation from = skeletonData.findAnimation(fromName);
+		if (from == null) throw new IllegalArgumentException("Animation not found: " + fromName);
+		Animation to = skeletonData.findAnimation(toName);
+		if (to == null) throw new IllegalArgumentException("Animation not found: " + toName);
+		setMix(from, to, duration);
+	}
+
+	/** Sets the mix duration when changing from the specified animation to the other.
+	 * <p>
+	 * See {@link TrackEntry#mixDuration}. */
+	public void setMix (Animation from, Animation to, float duration) {
+		if (from == null) throw new IllegalArgumentException("from cannot be null.");
+		if (to == null) throw new IllegalArgumentException("to cannot be null.");
+		Key key = new Key();
+		key.a1 = from;
+		key.a2 = to;
+		animationToMixTime.put(key, duration);
+	}
+
+	/** Returns the mix duration to use when changing from the specified animation to the other, or the {@link #getDefaultMix()} if
+	 * no mix duration has been set. */
+	public float getMix (Animation from, Animation to) {
+		if (from == null) throw new IllegalArgumentException("from cannot be null.");
+		if (to == null) throw new IllegalArgumentException("to cannot be null.");
+		tempKey.a1 = from;
+		tempKey.a2 = to;
+		return animationToMixTime.get(tempKey, defaultMix);
+	}
+
+	/** The mix duration to use when no mix duration has been defined between two animations. */
+	public float getDefaultMix () {
+		return defaultMix;
+	}
+
+	public void setDefaultMix (float defaultMix) {
+		this.defaultMix = defaultMix;
+	}
+
+	static class Key {
+		Animation a1, a2;
+
+		public int hashCode () {
+			return 31 * (31 + a1.hashCode()) + a2.hashCode();
+		}
+
+		public boolean equals (Object obj) {
+			if (this == obj) return true;
+			if (obj == null) return false;
+			Key other = (Key)obj;
+			if (a1 == null) {
+				if (other.a1 != null) return false;
+			} else if (!a1.equals(other.a1)) return false;
+			if (a2 == null) {
+				if (other.a2 != null) return false;
+			} else if (!a2.equals(other.a2)) return false;
+			return true;
+		}
+
+		public String toString () {
+			return a1.name + "->" + a2.name;
+		}
+	}
+}

+ 57 - 57
spine-libgdx/spine-libgdx/src/com/esotericsoftware/spine/BlendMode.java

@@ -1,57 +1,57 @@
-/******************************************************************************
- * Spine Runtimes License Agreement
- * Last updated January 1, 2020. Replaces all prior versions.
- *
- * Copyright (c) 2013-2020, Esoteric Software LLC
- *
- * Integration of the Spine Runtimes into software or otherwise creating
- * derivative works of the Spine Runtimes is permitted under the terms and
- * conditions of Section 2 of the Spine Editor License Agreement:
- * http://esotericsoftware.com/spine-editor-license
- *
- * Otherwise, it is permitted to integrate the Spine Runtimes into software
- * or otherwise create derivative works of the Spine Runtimes (collectively,
- * "Products"), provided that each user of the Products must obtain their own
- * Spine Editor license and redistribution of the Products in any form must
- * include this license and copyright notice.
- *
- * THE SPINE RUNTIMES ARE PROVIDED BY ESOTERIC SOFTWARE LLC "AS IS" AND ANY
- * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
- * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
- * DISCLAIMED. IN NO EVENT SHALL ESOTERIC SOFTWARE LLC BE LIABLE FOR ANY
- * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
- * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES,
- * BUSINESS INTERRUPTION, OR LOSS OF USE, DATA, OR PROFITS) HOWEVER CAUSED AND
- * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
- * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
- * THE SPINE RUNTIMES, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
- *****************************************************************************/
-
-package com.esotericsoftware.spine;
-
-import static com.badlogic.gdx.graphics.GL20.*;
-
-import com.badlogic.gdx.graphics.g2d.Batch;
-
-/** Determines how images are blended with existing pixels when drawn. */
-public enum BlendMode {
-	normal(GL_SRC_ALPHA, GL_ONE, GL_ONE_MINUS_SRC_ALPHA, GL_ONE), //
-	additive(GL_SRC_ALPHA, GL_ONE, GL_ONE, GL_ONE), //
-	multiply(GL_DST_COLOR, GL_DST_COLOR, GL_ONE_MINUS_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA), //
-	screen(GL_ONE, GL_ONE, GL_ONE_MINUS_SRC_COLOR, GL_ONE_MINUS_SRC_COLOR);
-
-	public final int source, sourcePMA, destColor, sourceAlpha;
-
-	BlendMode (int source, int sourcePMA, int destColor, int sourceAlpha) {
-		this.source = source;
-		this.sourcePMA = sourcePMA;
-		this.destColor = destColor;
-		this.sourceAlpha = sourceAlpha;
-	}
-
-	public void apply (Batch batch, boolean premultipliedAlpha) {
-		batch.setBlendFunctionSeparate(premultipliedAlpha ? sourcePMA : source, destColor, sourceAlpha, destColor);
-	}
-
-	static public final BlendMode[] values = values();
-}
+/******************************************************************************
+ * Spine Runtimes License Agreement
+ * Last updated January 1, 2020. Replaces all prior versions.
+ *
+ * Copyright (c) 2013-2020, Esoteric Software LLC
+ *
+ * Integration of the Spine Runtimes into software or otherwise creating
+ * derivative works of the Spine Runtimes is permitted under the terms and
+ * conditions of Section 2 of the Spine Editor License Agreement:
+ * http://esotericsoftware.com/spine-editor-license
+ *
+ * Otherwise, it is permitted to integrate the Spine Runtimes into software
+ * or otherwise create derivative works of the Spine Runtimes (collectively,
+ * "Products"), provided that each user of the Products must obtain their own
+ * Spine Editor license and redistribution of the Products in any form must
+ * include this license and copyright notice.
+ *
+ * THE SPINE RUNTIMES ARE PROVIDED BY ESOTERIC SOFTWARE LLC "AS IS" AND ANY
+ * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL ESOTERIC SOFTWARE LLC BE LIABLE FOR ANY
+ * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES,
+ * BUSINESS INTERRUPTION, OR LOSS OF USE, DATA, OR PROFITS) HOWEVER CAUSED AND
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
+ * THE SPINE RUNTIMES, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ *****************************************************************************/
+
+package com.esotericsoftware.spine;
+
+import static com.badlogic.gdx.graphics.GL20.*;
+
+import com.badlogic.gdx.graphics.g2d.Batch;
+
+/** Determines how images are blended with existing pixels when drawn. */
+public enum BlendMode {
+	normal(GL_SRC_ALPHA, GL_ONE, GL_ONE_MINUS_SRC_ALPHA, GL_ONE), //
+	additive(GL_SRC_ALPHA, GL_ONE, GL_ONE, GL_ONE), //
+	multiply(GL_DST_COLOR, GL_DST_COLOR, GL_ONE_MINUS_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA), //
+	screen(GL_ONE, GL_ONE, GL_ONE_MINUS_SRC_COLOR, GL_ONE_MINUS_SRC_COLOR);
+
+	public final int source, sourcePMA, destColor, sourceAlpha;
+
+	BlendMode (int source, int sourcePMA, int destColor, int sourceAlpha) {
+		this.source = source;
+		this.sourcePMA = sourcePMA;
+		this.destColor = destColor;
+		this.sourceAlpha = sourceAlpha;
+	}
+
+	public void apply (Batch batch, boolean premultipliedAlpha) {
+		batch.setBlendFunctionSeparate(premultipliedAlpha ? sourcePMA : source, destColor, sourceAlpha, destColor);
+	}
+
+	static public final BlendMode[] values = values();
+}

+ 578 - 578
spine-libgdx/spine-libgdx/src/com/esotericsoftware/spine/Bone.java

@@ -1,578 +1,578 @@
-/******************************************************************************
- * Spine Runtimes License Agreement
- * Last updated January 1, 2020. Replaces all prior versions.
- *
- * Copyright (c) 2013-2020, Esoteric Software LLC
- *
- * Integration of the Spine Runtimes into software or otherwise creating
- * derivative works of the Spine Runtimes is permitted under the terms and
- * conditions of Section 2 of the Spine Editor License Agreement:
- * http://esotericsoftware.com/spine-editor-license
- *
- * Otherwise, it is permitted to integrate the Spine Runtimes into software
- * or otherwise create derivative works of the Spine Runtimes (collectively,
- * "Products"), provided that each user of the Products must obtain their own
- * Spine Editor license and redistribution of the Products in any form must
- * include this license and copyright notice.
- *
- * THE SPINE RUNTIMES ARE PROVIDED BY ESOTERIC SOFTWARE LLC "AS IS" AND ANY
- * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
- * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
- * DISCLAIMED. IN NO EVENT SHALL ESOTERIC SOFTWARE LLC BE LIABLE FOR ANY
- * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
- * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES,
- * BUSINESS INTERRUPTION, OR LOSS OF USE, DATA, OR PROFITS) HOWEVER CAUSED AND
- * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
- * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
- * THE SPINE RUNTIMES, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
- *****************************************************************************/
-
-package com.esotericsoftware.spine;
-
-import static com.badlogic.gdx.math.Matrix3.*;
-import static com.esotericsoftware.spine.utils.SpineUtils.*;
-
-import com.badlogic.gdx.math.Matrix3;
-import com.badlogic.gdx.math.Vector2;
-import com.badlogic.gdx.utils.Array;
-import com.badlogic.gdx.utils.Null;
-
-import com.esotericsoftware.spine.BoneData.TransformMode;
-
-/** Stores a bone's current pose.
- * <p>
- * A bone has a local transform which is used to compute its world transform. A bone also has an applied transform, which is a
- * local transform that can be applied to compute the world transform. The local transform and applied transform may differ if a
- * constraint or application code modifies the world transform after it was computed from the local transform. */
-public class Bone implements Updatable {
-	final BoneData data;
-	final Skeleton skeleton;
-	@Null final Bone parent;
-	final Array<Bone> children = new Array();
-	float x, y, rotation, scaleX, scaleY, shearX, shearY;
-	float ax, ay, arotation, ascaleX, ascaleY, ashearX, ashearY;
-	float a, b, worldX;
-	float c, d, worldY;
-
-	boolean sorted, active;
-
-	public Bone (BoneData data, Skeleton skeleton, @Null Bone parent) {
-		if (data == null) throw new IllegalArgumentException("data cannot be null.");
-		if (skeleton == null) throw new IllegalArgumentException("skeleton cannot be null.");
-		this.data = data;
-		this.skeleton = skeleton;
-		this.parent = parent;
-		setToSetupPose();
-	}
-
-	/** Copy constructor. Does not copy the {@link #getChildren()} bones. */
-	public Bone (Bone bone, Skeleton skeleton, @Null Bone parent) {
-		if (bone == null) throw new IllegalArgumentException("bone cannot be null.");
-		if (skeleton == null) throw new IllegalArgumentException("skeleton cannot be null.");
-		this.skeleton = skeleton;
-		this.parent = parent;
-		data = bone.data;
-		x = bone.x;
-		y = bone.y;
-		rotation = bone.rotation;
-		scaleX = bone.scaleX;
-		scaleY = bone.scaleY;
-		shearX = bone.shearX;
-		shearY = bone.shearY;
-	}
-
-	/** Computes the world transform using the parent bone and this bone's local applied transform. */
-	public void update () {
-		updateWorldTransform(ax, ay, arotation, ascaleX, ascaleY, ashearX, ashearY);
-	}
-
-	/** Computes the world transform using the parent bone and this bone's local transform.
-	 * <p>
-	 * See {@link #updateWorldTransform(float, float, float, float, float, float, float)}. */
-	public void updateWorldTransform () {
-		updateWorldTransform(x, y, rotation, scaleX, scaleY, shearX, shearY);
-	}
-
-	/** Computes the world transform using the parent bone and the specified local transform. The applied transform is set to the
-	 * specified local transform. Child bones are not updated.
-	 * <p>
-	 * See <a href="http://esotericsoftware.com/spine-runtime-skeletons#World-transforms">World transforms</a> in the Spine
-	 * Runtimes Guide. */
-	public void updateWorldTransform (float x, float y, float rotation, float scaleX, float scaleY, float shearX, float shearY) {
-		ax = x;
-		ay = y;
-		arotation = rotation;
-		ascaleX = scaleX;
-		ascaleY = scaleY;
-		ashearX = shearX;
-		ashearY = shearY;
-
-		Bone parent = this.parent;
-		if (parent == null) { // Root bone.
-			Skeleton skeleton = this.skeleton;
-			float rotationY = rotation + 90 + shearY, sx = skeleton.scaleX, sy = skeleton.scaleY;
-			a = cosDeg(rotation + shearX) * scaleX * sx;
-			b = cosDeg(rotationY) * scaleY * sx;
-			c = sinDeg(rotation + shearX) * scaleX * sy;
-			d = sinDeg(rotationY) * scaleY * sy;
-			worldX = x * sx + skeleton.x;
-			worldY = y * sy + skeleton.y;
-			return;
-		}
-
-		float pa = parent.a, pb = parent.b, pc = parent.c, pd = parent.d;
-		worldX = pa * x + pb * y + parent.worldX;
-		worldY = pc * x + pd * y + parent.worldY;
-
-		switch (data.transformMode) {
-		case normal: {
-			float rotationY = rotation + 90 + shearY;
-			float la = cosDeg(rotation + shearX) * scaleX;
-			float lb = cosDeg(rotationY) * scaleY;
-			float lc = sinDeg(rotation + shearX) * scaleX;
-			float ld = sinDeg(rotationY) * scaleY;
-			a = pa * la + pb * lc;
-			b = pa * lb + pb * ld;
-			c = pc * la + pd * lc;
-			d = pc * lb + pd * ld;
-			return;
-		}
-		case onlyTranslation: {
-			float rotationY = rotation + 90 + shearY;
-			a = cosDeg(rotation + shearX) * scaleX;
-			b = cosDeg(rotationY) * scaleY;
-			c = sinDeg(rotation + shearX) * scaleX;
-			d = sinDeg(rotationY) * scaleY;
-			break;
-		}
-		case noRotationOrReflection: {
-			float s = pa * pa + pc * pc, prx;
-			if (s > 0.0001f) {
-				s = Math.abs(pa * pd - pb * pc) / s;
-				pa /= skeleton.scaleX;
-				pc /= skeleton.scaleY;
-				pb = pc * s;
-				pd = pa * s;
-				prx = atan2(pc, pa) * radDeg;
-			} else {
-				pa = 0;
-				pc = 0;
-				prx = 90 - atan2(pd, pb) * radDeg;
-			}
-			float rx = rotation + shearX - prx;
-			float ry = rotation + shearY - prx + 90;
-			float la = cosDeg(rx) * scaleX;
-			float lb = cosDeg(ry) * scaleY;
-			float lc = sinDeg(rx) * scaleX;
-			float ld = sinDeg(ry) * scaleY;
-			a = pa * la - pb * lc;
-			b = pa * lb - pb * ld;
-			c = pc * la + pd * lc;
-			d = pc * lb + pd * ld;
-			break;
-		}
-		case noScale:
-		case noScaleOrReflection: {
-			float cos = cosDeg(rotation), sin = sinDeg(rotation);
-			float za = (pa * cos + pb * sin) / skeleton.scaleX;
-			float zc = (pc * cos + pd * sin) / skeleton.scaleY;
-			float s = (float)Math.sqrt(za * za + zc * zc);
-			if (s > 0.00001f) s = 1 / s;
-			za *= s;
-			zc *= s;
-			s = (float)Math.sqrt(za * za + zc * zc);
-			if (data.transformMode == TransformMode.noScale
-				&& (pa * pd - pb * pc < 0) != (skeleton.scaleX < 0 != skeleton.scaleY < 0)) s = -s;
-			float r = PI / 2 + atan2(zc, za);
-			float zb = cos(r) * s;
-			float zd = sin(r) * s;
-			float la = cosDeg(shearX) * scaleX;
-			float lb = cosDeg(90 + shearY) * scaleY;
-			float lc = sinDeg(shearX) * scaleX;
-			float ld = sinDeg(90 + shearY) * scaleY;
-			a = za * la + zb * lc;
-			b = za * lb + zb * ld;
-			c = zc * la + zd * lc;
-			d = zc * lb + zd * ld;
-			break;
-		}
-		}
-		a *= skeleton.scaleX;
-		b *= skeleton.scaleX;
-		c *= skeleton.scaleY;
-		d *= skeleton.scaleY;
-	}
-
-	/** Sets this bone's local transform to the setup pose. */
-	public void setToSetupPose () {
-		BoneData data = this.data;
-		x = data.x;
-		y = data.y;
-		rotation = data.rotation;
-		scaleX = data.scaleX;
-		scaleY = data.scaleY;
-		shearX = data.shearX;
-		shearY = data.shearY;
-	}
-
-	/** The bone's setup pose data. */
-	public BoneData getData () {
-		return data;
-	}
-
-	/** The skeleton this bone belongs to. */
-	public Skeleton getSkeleton () {
-		return skeleton;
-	}
-
-	/** The parent bone, or null if this is the root bone. */
-	public @Null Bone getParent () {
-		return parent;
-	}
-
-	/** The immediate children of this bone. */
-	public Array<Bone> getChildren () {
-		return children;
-	}
-
-	/** Returns false when the bone has not been computed because {@link BoneData#getSkinRequired()} is true and the
-	 * {@link Skeleton#getSkin() active skin} does not {@link Skin#getBones() contain} this bone. */
-	public boolean isActive () {
-		return active;
-	}
-
-	// -- Local transform
-
-	/** The local x translation. */
-	public float getX () {
-		return x;
-	}
-
-	public void setX (float x) {
-		this.x = x;
-	}
-
-	/** The local y translation. */
-	public float getY () {
-		return y;
-	}
-
-	public void setY (float y) {
-		this.y = y;
-	}
-
-	public void setPosition (float x, float y) {
-		this.x = x;
-		this.y = y;
-	}
-
-	/** The local rotation in degrees, counter clockwise. */
-	public float getRotation () {
-		return rotation;
-	}
-
-	public void setRotation (float rotation) {
-		this.rotation = rotation;
-	}
-
-	/** The local scaleX. */
-	public float getScaleX () {
-		return scaleX;
-	}
-
-	public void setScaleX (float scaleX) {
-		this.scaleX = scaleX;
-	}
-
-	/** The local scaleY. */
-	public float getScaleY () {
-		return scaleY;
-	}
-
-	public void setScaleY (float scaleY) {
-		this.scaleY = scaleY;
-	}
-
-	public void setScale (float scaleX, float scaleY) {
-		this.scaleX = scaleX;
-		this.scaleY = scaleY;
-	}
-
-	public void setScale (float scale) {
-		scaleX = scale;
-		scaleY = scale;
-	}
-
-	/** The local shearX. */
-	public float getShearX () {
-		return shearX;
-	}
-
-	public void setShearX (float shearX) {
-		this.shearX = shearX;
-	}
-
-	/** The local shearY. */
-	public float getShearY () {
-		return shearY;
-	}
-
-	public void setShearY (float shearY) {
-		this.shearY = shearY;
-	}
-
-	// -- Applied transform
-
-	/** The applied local x translation. */
-	public float getAX () {
-		return ax;
-	}
-
-	public void setAX (float ax) {
-		this.ax = ax;
-	}
-
-	/** The applied local y translation. */
-	public float getAY () {
-		return ay;
-	}
-
-	public void setAY (float ay) {
-		this.ay = ay;
-	}
-
-	/** The applied local rotation in degrees, counter clockwise. */
-	public float getARotation () {
-		return arotation;
-	}
-
-	public void setARotation (float arotation) {
-		this.arotation = arotation;
-	}
-
-	/** The applied local scaleX. */
-	public float getAScaleX () {
-		return ascaleX;
-	}
-
-	public void setAScaleX (float ascaleX) {
-		this.ascaleX = ascaleX;
-	}
-
-	/** The applied local scaleY. */
-	public float getAScaleY () {
-		return ascaleY;
-	}
-
-	public void setAScaleY (float ascaleY) {
-		this.ascaleY = ascaleY;
-	}
-
-	/** The applied local shearX. */
-	public float getAShearX () {
-		return ashearX;
-	}
-
-	public void setAShearX (float ashearX) {
-		this.ashearX = ashearX;
-	}
-
-	/** The applied local shearY. */
-	public float getAShearY () {
-		return ashearY;
-	}
-
-	public void setAShearY (float ashearY) {
-		this.ashearY = ashearY;
-	}
-
-	/** Computes the applied transform values from the world transform.
-	 * <p>
-	 * If the world transform is modified (by a constraint, {@link #rotateWorld(float)}, etc) then this method should be called so
-	 * the applied transform matches the world transform. The applied transform may be needed by other code (eg to apply another
-	 * constraint).
-	 * <p>
-	 * Some information is ambiguous in the world transform, such as -1,-1 scale versus 180 rotation. The applied transform after
-	 * calling this method is equivalent to the local transform used to compute the world transform, but may not be identical. */
-	public void updateAppliedTransform () {
-		Bone parent = this.parent;
-		if (parent == null) {
-			ax = worldX;
-			ay = worldY;
-			float a = this.a, b = this.b, c = this.c, d = this.d;
-			arotation = atan2(c, a) * radDeg;
-			ascaleX = (float)Math.sqrt(a * a + c * c);
-			ascaleY = (float)Math.sqrt(b * b + d * d);
-			ashearX = 0;
-			ashearY = atan2(a * b + c * d, a * d - b * c) * radDeg;
-			return;
-		}
-		float pa = parent.a, pb = parent.b, pc = parent.c, pd = parent.d;
-		float pid = 1 / (pa * pd - pb * pc);
-		float dx = worldX - parent.worldX, dy = worldY - parent.worldY;
-		ax = (dx * pd * pid - dy * pb * pid);
-		ay = (dy * pa * pid - dx * pc * pid);
-		float ia = pid * pd;
-		float id = pid * pa;
-		float ib = pid * pb;
-		float ic = pid * pc;
-		float ra = ia * a - ib * c;
-		float rb = ia * b - ib * d;
-		float rc = id * c - ic * a;
-		float rd = id * d - ic * b;
-		ashearX = 0;
-		ascaleX = (float)Math.sqrt(ra * ra + rc * rc);
-		if (ascaleX > 0.0001f) {
-			float det = ra * rd - rb * rc;
-			ascaleY = det / ascaleX;
-			ashearY = atan2(ra * rb + rc * rd, det) * radDeg;
-			arotation = atan2(rc, ra) * radDeg;
-		} else {
-			ascaleX = 0;
-			ascaleY = (float)Math.sqrt(rb * rb + rd * rd);
-			ashearY = 0;
-			arotation = 90 - atan2(rd, rb) * radDeg;
-		}
-	}
-
-	// -- World transform
-
-	/** Part of the world transform matrix for the X axis. If changed, {@link #updateAppliedTransform()} should be called. */
-	public float getA () {
-		return a;
-	}
-
-	public void setA (float a) {
-		this.a = a;
-	}
-
-	/** Part of the world transform matrix for the Y axis. If changed, {@link #updateAppliedTransform()} should be called. */
-	public float getB () {
-		return b;
-	}
-
-	public void setB (float b) {
-		this.b = b;
-	}
-
-	/** Part of the world transform matrix for the X axis. If changed, {@link #updateAppliedTransform()} should be called. */
-	public float getC () {
-		return c;
-	}
-
-	public void setC (float c) {
-		this.c = c;
-	}
-
-	/** Part of the world transform matrix for the Y axis. If changed, {@link #updateAppliedTransform()} should be called. */
-	public float getD () {
-		return d;
-	}
-
-	public void setD (float d) {
-		this.d = d;
-	}
-
-	/** The world X position. If changed, {@link #updateAppliedTransform()} should be called. */
-	public float getWorldX () {
-		return worldX;
-	}
-
-	public void setWorldX (float worldX) {
-		this.worldX = worldX;
-	}
-
-	/** The world Y position. If changed, {@link #updateAppliedTransform()} should be called. */
-	public float getWorldY () {
-		return worldY;
-	}
-
-	public void setWorldY (float worldY) {
-		this.worldY = worldY;
-	}
-
-	/** The world rotation for the X axis, calculated using {@link #a} and {@link #c}. */
-	public float getWorldRotationX () {
-		return atan2(c, a) * radDeg;
-	}
-
-	/** The world rotation for the Y axis, calculated using {@link #b} and {@link #d}. */
-	public float getWorldRotationY () {
-		return atan2(d, b) * radDeg;
-	}
-
-	/** The magnitude (always positive) of the world scale X, calculated using {@link #a} and {@link #c}. */
-	public float getWorldScaleX () {
-		return (float)Math.sqrt(a * a + c * c);
-	}
-
-	/** The magnitude (always positive) of the world scale Y, calculated using {@link #b} and {@link #d}. */
-	public float getWorldScaleY () {
-		return (float)Math.sqrt(b * b + d * d);
-	}
-
-	public Matrix3 getWorldTransform (Matrix3 worldTransform) {
-		if (worldTransform == null) throw new IllegalArgumentException("worldTransform cannot be null.");
-		float[] val = worldTransform.val;
-		val[M00] = a;
-		val[M01] = b;
-		val[M10] = c;
-		val[M11] = d;
-		val[M02] = worldX;
-		val[M12] = worldY;
-		val[M20] = 0;
-		val[M21] = 0;
-		val[M22] = 1;
-		return worldTransform;
-	}
-
-	/** Transforms a point from world coordinates to the bone's local coordinates. */
-	public Vector2 worldToLocal (Vector2 world) {
-		if (world == null) throw new IllegalArgumentException("world cannot be null.");
-		float det = a * d - b * c;
-		float x = world.x - worldX, y = world.y - worldY;
-		world.x = (x * d - y * b) / det;
-		world.y = (y * a - x * c) / det;
-		return world;
-	}
-
-	/** Transforms a point from the bone's local coordinates to world coordinates. */
-	public Vector2 localToWorld (Vector2 local) {
-		if (local == null) throw new IllegalArgumentException("local cannot be null.");
-		float x = local.x, y = local.y;
-		local.x = x * a + y * b + worldX;
-		local.y = x * c + y * d + worldY;
-		return local;
-	}
-
-	/** Transforms a world rotation to a local rotation. */
-	public float worldToLocalRotation (float worldRotation) {
-		float sin = sinDeg(worldRotation), cos = cosDeg(worldRotation);
-		return atan2(a * sin - c * cos, d * cos - b * sin) * radDeg + rotation - shearX;
-	}
-
-	/** Transforms a local rotation to a world rotation. */
-	public float localToWorldRotation (float localRotation) {
-		localRotation -= rotation - shearX;
-		float sin = sinDeg(localRotation), cos = cosDeg(localRotation);
-		return atan2(cos * c + sin * d, cos * a + sin * b) * radDeg;
-	}
-
-	/** Rotates the world transform the specified amount.
-	 * <p>
-	 * After changes are made to the world transform, {@link #updateAppliedTransform()} should be called and {@link #update()} will
-	 * need to be called on any child bones, recursively. */
-	public void rotateWorld (float degrees) {
-		float cos = cosDeg(degrees), sin = sinDeg(degrees);
-		a = cos * a - sin * c;
-		b = cos * b - sin * d;
-		c = sin * a + cos * c;
-		d = sin * b + cos * d;
-	}
-
-	// ---
-
-	public String toString () {
-		return data.name;
-	}
-}
+/******************************************************************************
+ * Spine Runtimes License Agreement
+ * Last updated January 1, 2020. Replaces all prior versions.
+ *
+ * Copyright (c) 2013-2020, Esoteric Software LLC
+ *
+ * Integration of the Spine Runtimes into software or otherwise creating
+ * derivative works of the Spine Runtimes is permitted under the terms and
+ * conditions of Section 2 of the Spine Editor License Agreement:
+ * http://esotericsoftware.com/spine-editor-license
+ *
+ * Otherwise, it is permitted to integrate the Spine Runtimes into software
+ * or otherwise create derivative works of the Spine Runtimes (collectively,
+ * "Products"), provided that each user of the Products must obtain their own
+ * Spine Editor license and redistribution of the Products in any form must
+ * include this license and copyright notice.
+ *
+ * THE SPINE RUNTIMES ARE PROVIDED BY ESOTERIC SOFTWARE LLC "AS IS" AND ANY
+ * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL ESOTERIC SOFTWARE LLC BE LIABLE FOR ANY
+ * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES,
+ * BUSINESS INTERRUPTION, OR LOSS OF USE, DATA, OR PROFITS) HOWEVER CAUSED AND
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
+ * THE SPINE RUNTIMES, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ *****************************************************************************/
+
+package com.esotericsoftware.spine;
+
+import static com.badlogic.gdx.math.Matrix3.*;
+import static com.esotericsoftware.spine.utils.SpineUtils.*;
+
+import com.badlogic.gdx.math.Matrix3;
+import com.badlogic.gdx.math.Vector2;
+import com.badlogic.gdx.utils.Array;
+import com.badlogic.gdx.utils.Null;
+
+import com.esotericsoftware.spine.BoneData.TransformMode;
+
+/** Stores a bone's current pose.
+ * <p>
+ * A bone has a local transform which is used to compute its world transform. A bone also has an applied transform, which is a
+ * local transform that can be applied to compute the world transform. The local transform and applied transform may differ if a
+ * constraint or application code modifies the world transform after it was computed from the local transform. */
+public class Bone implements Updatable {
+	final BoneData data;
+	final Skeleton skeleton;
+	@Null final Bone parent;
+	final Array<Bone> children = new Array();
+	float x, y, rotation, scaleX, scaleY, shearX, shearY;
+	float ax, ay, arotation, ascaleX, ascaleY, ashearX, ashearY;
+	float a, b, worldX;
+	float c, d, worldY;
+
+	boolean sorted, active;
+
+	public Bone (BoneData data, Skeleton skeleton, @Null Bone parent) {
+		if (data == null) throw new IllegalArgumentException("data cannot be null.");
+		if (skeleton == null) throw new IllegalArgumentException("skeleton cannot be null.");
+		this.data = data;
+		this.skeleton = skeleton;
+		this.parent = parent;
+		setToSetupPose();
+	}
+
+	/** Copy constructor. Does not copy the {@link #getChildren()} bones. */
+	public Bone (Bone bone, Skeleton skeleton, @Null Bone parent) {
+		if (bone == null) throw new IllegalArgumentException("bone cannot be null.");
+		if (skeleton == null) throw new IllegalArgumentException("skeleton cannot be null.");
+		this.skeleton = skeleton;
+		this.parent = parent;
+		data = bone.data;
+		x = bone.x;
+		y = bone.y;
+		rotation = bone.rotation;
+		scaleX = bone.scaleX;
+		scaleY = bone.scaleY;
+		shearX = bone.shearX;
+		shearY = bone.shearY;
+	}
+
+	/** Computes the world transform using the parent bone and this bone's local applied transform. */
+	public void update () {
+		updateWorldTransform(ax, ay, arotation, ascaleX, ascaleY, ashearX, ashearY);
+	}
+
+	/** Computes the world transform using the parent bone and this bone's local transform.
+	 * <p>
+	 * See {@link #updateWorldTransform(float, float, float, float, float, float, float)}. */
+	public void updateWorldTransform () {
+		updateWorldTransform(x, y, rotation, scaleX, scaleY, shearX, shearY);
+	}
+
+	/** Computes the world transform using the parent bone and the specified local transform. The applied transform is set to the
+	 * specified local transform. Child bones are not updated.
+	 * <p>
+	 * See <a href="http://esotericsoftware.com/spine-runtime-skeletons#World-transforms">World transforms</a> in the Spine
+	 * Runtimes Guide. */
+	public void updateWorldTransform (float x, float y, float rotation, float scaleX, float scaleY, float shearX, float shearY) {
+		ax = x;
+		ay = y;
+		arotation = rotation;
+		ascaleX = scaleX;
+		ascaleY = scaleY;
+		ashearX = shearX;
+		ashearY = shearY;
+
+		Bone parent = this.parent;
+		if (parent == null) { // Root bone.
+			Skeleton skeleton = this.skeleton;
+			float rotationY = rotation + 90 + shearY, sx = skeleton.scaleX, sy = skeleton.scaleY;
+			a = cosDeg(rotation + shearX) * scaleX * sx;
+			b = cosDeg(rotationY) * scaleY * sx;
+			c = sinDeg(rotation + shearX) * scaleX * sy;
+			d = sinDeg(rotationY) * scaleY * sy;
+			worldX = x * sx + skeleton.x;
+			worldY = y * sy + skeleton.y;
+			return;
+		}
+
+		float pa = parent.a, pb = parent.b, pc = parent.c, pd = parent.d;
+		worldX = pa * x + pb * y + parent.worldX;
+		worldY = pc * x + pd * y + parent.worldY;
+
+		switch (data.transformMode) {
+		case normal: {
+			float rotationY = rotation + 90 + shearY;
+			float la = cosDeg(rotation + shearX) * scaleX;
+			float lb = cosDeg(rotationY) * scaleY;
+			float lc = sinDeg(rotation + shearX) * scaleX;
+			float ld = sinDeg(rotationY) * scaleY;
+			a = pa * la + pb * lc;
+			b = pa * lb + pb * ld;
+			c = pc * la + pd * lc;
+			d = pc * lb + pd * ld;
+			return;
+		}
+		case onlyTranslation: {
+			float rotationY = rotation + 90 + shearY;
+			a = cosDeg(rotation + shearX) * scaleX;
+			b = cosDeg(rotationY) * scaleY;
+			c = sinDeg(rotation + shearX) * scaleX;
+			d = sinDeg(rotationY) * scaleY;
+			break;
+		}
+		case noRotationOrReflection: {
+			float s = pa * pa + pc * pc, prx;
+			if (s > 0.0001f) {
+				s = Math.abs(pa * pd - pb * pc) / s;
+				pa /= skeleton.scaleX;
+				pc /= skeleton.scaleY;
+				pb = pc * s;
+				pd = pa * s;
+				prx = atan2(pc, pa) * radDeg;
+			} else {
+				pa = 0;
+				pc = 0;
+				prx = 90 - atan2(pd, pb) * radDeg;
+			}
+			float rx = rotation + shearX - prx;
+			float ry = rotation + shearY - prx + 90;
+			float la = cosDeg(rx) * scaleX;
+			float lb = cosDeg(ry) * scaleY;
+			float lc = sinDeg(rx) * scaleX;
+			float ld = sinDeg(ry) * scaleY;
+			a = pa * la - pb * lc;
+			b = pa * lb - pb * ld;
+			c = pc * la + pd * lc;
+			d = pc * lb + pd * ld;
+			break;
+		}
+		case noScale:
+		case noScaleOrReflection: {
+			float cos = cosDeg(rotation), sin = sinDeg(rotation);
+			float za = (pa * cos + pb * sin) / skeleton.scaleX;
+			float zc = (pc * cos + pd * sin) / skeleton.scaleY;
+			float s = (float)Math.sqrt(za * za + zc * zc);
+			if (s > 0.00001f) s = 1 / s;
+			za *= s;
+			zc *= s;
+			s = (float)Math.sqrt(za * za + zc * zc);
+			if (data.transformMode == TransformMode.noScale
+				&& (pa * pd - pb * pc < 0) != (skeleton.scaleX < 0 != skeleton.scaleY < 0)) s = -s;
+			float r = PI / 2 + atan2(zc, za);
+			float zb = cos(r) * s;
+			float zd = sin(r) * s;
+			float la = cosDeg(shearX) * scaleX;
+			float lb = cosDeg(90 + shearY) * scaleY;
+			float lc = sinDeg(shearX) * scaleX;
+			float ld = sinDeg(90 + shearY) * scaleY;
+			a = za * la + zb * lc;
+			b = za * lb + zb * ld;
+			c = zc * la + zd * lc;
+			d = zc * lb + zd * ld;
+			break;
+		}
+		}
+		a *= skeleton.scaleX;
+		b *= skeleton.scaleX;
+		c *= skeleton.scaleY;
+		d *= skeleton.scaleY;
+	}
+
+	/** Sets this bone's local transform to the setup pose. */
+	public void setToSetupPose () {
+		BoneData data = this.data;
+		x = data.x;
+		y = data.y;
+		rotation = data.rotation;
+		scaleX = data.scaleX;
+		scaleY = data.scaleY;
+		shearX = data.shearX;
+		shearY = data.shearY;
+	}
+
+	/** The bone's setup pose data. */
+	public BoneData getData () {
+		return data;
+	}
+
+	/** The skeleton this bone belongs to. */
+	public Skeleton getSkeleton () {
+		return skeleton;
+	}
+
+	/** The parent bone, or null if this is the root bone. */
+	public @Null Bone getParent () {
+		return parent;
+	}
+
+	/** The immediate children of this bone. */
+	public Array<Bone> getChildren () {
+		return children;
+	}
+
+	/** Returns false when the bone has not been computed because {@link BoneData#getSkinRequired()} is true and the
+	 * {@link Skeleton#getSkin() active skin} does not {@link Skin#getBones() contain} this bone. */
+	public boolean isActive () {
+		return active;
+	}
+
+	// -- Local transform
+
+	/** The local x translation. */
+	public float getX () {
+		return x;
+	}
+
+	public void setX (float x) {
+		this.x = x;
+	}
+
+	/** The local y translation. */
+	public float getY () {
+		return y;
+	}
+
+	public void setY (float y) {
+		this.y = y;
+	}
+
+	public void setPosition (float x, float y) {
+		this.x = x;
+		this.y = y;
+	}
+
+	/** The local rotation in degrees, counter clockwise. */
+	public float getRotation () {
+		return rotation;
+	}
+
+	public void setRotation (float rotation) {
+		this.rotation = rotation;
+	}
+
+	/** The local scaleX. */
+	public float getScaleX () {
+		return scaleX;
+	}
+
+	public void setScaleX (float scaleX) {
+		this.scaleX = scaleX;
+	}
+
+	/** The local scaleY. */
+	public float getScaleY () {
+		return scaleY;
+	}
+
+	public void setScaleY (float scaleY) {
+		this.scaleY = scaleY;
+	}
+
+	public void setScale (float scaleX, float scaleY) {
+		this.scaleX = scaleX;
+		this.scaleY = scaleY;
+	}
+
+	public void setScale (float scale) {
+		scaleX = scale;
+		scaleY = scale;
+	}
+
+	/** The local shearX. */
+	public float getShearX () {
+		return shearX;
+	}
+
+	public void setShearX (float shearX) {
+		this.shearX = shearX;
+	}
+
+	/** The local shearY. */
+	public float getShearY () {
+		return shearY;
+	}
+
+	public void setShearY (float shearY) {
+		this.shearY = shearY;
+	}
+
+	// -- Applied transform
+
+	/** The applied local x translation. */
+	public float getAX () {
+		return ax;
+	}
+
+	public void setAX (float ax) {
+		this.ax = ax;
+	}
+
+	/** The applied local y translation. */
+	public float getAY () {
+		return ay;
+	}
+
+	public void setAY (float ay) {
+		this.ay = ay;
+	}
+
+	/** The applied local rotation in degrees, counter clockwise. */
+	public float getARotation () {
+		return arotation;
+	}
+
+	public void setARotation (float arotation) {
+		this.arotation = arotation;
+	}
+
+	/** The applied local scaleX. */
+	public float getAScaleX () {
+		return ascaleX;
+	}
+
+	public void setAScaleX (float ascaleX) {
+		this.ascaleX = ascaleX;
+	}
+
+	/** The applied local scaleY. */
+	public float getAScaleY () {
+		return ascaleY;
+	}
+
+	public void setAScaleY (float ascaleY) {
+		this.ascaleY = ascaleY;
+	}
+
+	/** The applied local shearX. */
+	public float getAShearX () {
+		return ashearX;
+	}
+
+	public void setAShearX (float ashearX) {
+		this.ashearX = ashearX;
+	}
+
+	/** The applied local shearY. */
+	public float getAShearY () {
+		return ashearY;
+	}
+
+	public void setAShearY (float ashearY) {
+		this.ashearY = ashearY;
+	}
+
+	/** Computes the applied transform values from the world transform.
+	 * <p>
+	 * If the world transform is modified (by a constraint, {@link #rotateWorld(float)}, etc) then this method should be called so
+	 * the applied transform matches the world transform. The applied transform may be needed by other code (eg to apply another
+	 * constraint).
+	 * <p>
+	 * Some information is ambiguous in the world transform, such as -1,-1 scale versus 180 rotation. The applied transform after
+	 * calling this method is equivalent to the local transform used to compute the world transform, but may not be identical. */
+	public void updateAppliedTransform () {
+		Bone parent = this.parent;
+		if (parent == null) {
+			ax = worldX;
+			ay = worldY;
+			float a = this.a, b = this.b, c = this.c, d = this.d;
+			arotation = atan2(c, a) * radDeg;
+			ascaleX = (float)Math.sqrt(a * a + c * c);
+			ascaleY = (float)Math.sqrt(b * b + d * d);
+			ashearX = 0;
+			ashearY = atan2(a * b + c * d, a * d - b * c) * radDeg;
+			return;
+		}
+		float pa = parent.a, pb = parent.b, pc = parent.c, pd = parent.d;
+		float pid = 1 / (pa * pd - pb * pc);
+		float dx = worldX - parent.worldX, dy = worldY - parent.worldY;
+		ax = (dx * pd * pid - dy * pb * pid);
+		ay = (dy * pa * pid - dx * pc * pid);
+		float ia = pid * pd;
+		float id = pid * pa;
+		float ib = pid * pb;
+		float ic = pid * pc;
+		float ra = ia * a - ib * c;
+		float rb = ia * b - ib * d;
+		float rc = id * c - ic * a;
+		float rd = id * d - ic * b;
+		ashearX = 0;
+		ascaleX = (float)Math.sqrt(ra * ra + rc * rc);
+		if (ascaleX > 0.0001f) {
+			float det = ra * rd - rb * rc;
+			ascaleY = det / ascaleX;
+			ashearY = atan2(ra * rb + rc * rd, det) * radDeg;
+			arotation = atan2(rc, ra) * radDeg;
+		} else {
+			ascaleX = 0;
+			ascaleY = (float)Math.sqrt(rb * rb + rd * rd);
+			ashearY = 0;
+			arotation = 90 - atan2(rd, rb) * radDeg;
+		}
+	}
+
+	// -- World transform
+
+	/** Part of the world transform matrix for the X axis. If changed, {@link #updateAppliedTransform()} should be called. */
+	public float getA () {
+		return a;
+	}
+
+	public void setA (float a) {
+		this.a = a;
+	}
+
+	/** Part of the world transform matrix for the Y axis. If changed, {@link #updateAppliedTransform()} should be called. */
+	public float getB () {
+		return b;
+	}
+
+	public void setB (float b) {
+		this.b = b;
+	}
+
+	/** Part of the world transform matrix for the X axis. If changed, {@link #updateAppliedTransform()} should be called. */
+	public float getC () {
+		return c;
+	}
+
+	public void setC (float c) {
+		this.c = c;
+	}
+
+	/** Part of the world transform matrix for the Y axis. If changed, {@link #updateAppliedTransform()} should be called. */
+	public float getD () {
+		return d;
+	}
+
+	public void setD (float d) {
+		this.d = d;
+	}
+
+	/** The world X position. If changed, {@link #updateAppliedTransform()} should be called. */
+	public float getWorldX () {
+		return worldX;
+	}
+
+	public void setWorldX (float worldX) {
+		this.worldX = worldX;
+	}
+
+	/** The world Y position. If changed, {@link #updateAppliedTransform()} should be called. */
+	public float getWorldY () {
+		return worldY;
+	}
+
+	public void setWorldY (float worldY) {
+		this.worldY = worldY;
+	}
+
+	/** The world rotation for the X axis, calculated using {@link #a} and {@link #c}. */
+	public float getWorldRotationX () {
+		return atan2(c, a) * radDeg;
+	}
+
+	/** The world rotation for the Y axis, calculated using {@link #b} and {@link #d}. */
+	public float getWorldRotationY () {
+		return atan2(d, b) * radDeg;
+	}
+
+	/** The magnitude (always positive) of the world scale X, calculated using {@link #a} and {@link #c}. */
+	public float getWorldScaleX () {
+		return (float)Math.sqrt(a * a + c * c);
+	}
+
+	/** The magnitude (always positive) of the world scale Y, calculated using {@link #b} and {@link #d}. */
+	public float getWorldScaleY () {
+		return (float)Math.sqrt(b * b + d * d);
+	}
+
+	public Matrix3 getWorldTransform (Matrix3 worldTransform) {
+		if (worldTransform == null) throw new IllegalArgumentException("worldTransform cannot be null.");
+		float[] val = worldTransform.val;
+		val[M00] = a;
+		val[M01] = b;
+		val[M10] = c;
+		val[M11] = d;
+		val[M02] = worldX;
+		val[M12] = worldY;
+		val[M20] = 0;
+		val[M21] = 0;
+		val[M22] = 1;
+		return worldTransform;
+	}
+
+	/** Transforms a point from world coordinates to the bone's local coordinates. */
+	public Vector2 worldToLocal (Vector2 world) {
+		if (world == null) throw new IllegalArgumentException("world cannot be null.");
+		float det = a * d - b * c;
+		float x = world.x - worldX, y = world.y - worldY;
+		world.x = (x * d - y * b) / det;
+		world.y = (y * a - x * c) / det;
+		return world;
+	}
+
+	/** Transforms a point from the bone's local coordinates to world coordinates. */
+	public Vector2 localToWorld (Vector2 local) {
+		if (local == null) throw new IllegalArgumentException("local cannot be null.");
+		float x = local.x, y = local.y;
+		local.x = x * a + y * b + worldX;
+		local.y = x * c + y * d + worldY;
+		return local;
+	}
+
+	/** Transforms a world rotation to a local rotation. */
+	public float worldToLocalRotation (float worldRotation) {
+		float sin = sinDeg(worldRotation), cos = cosDeg(worldRotation);
+		return atan2(a * sin - c * cos, d * cos - b * sin) * radDeg + rotation - shearX;
+	}
+
+	/** Transforms a local rotation to a world rotation. */
+	public float localToWorldRotation (float localRotation) {
+		localRotation -= rotation - shearX;
+		float sin = sinDeg(localRotation), cos = cosDeg(localRotation);
+		return atan2(cos * c + sin * d, cos * a + sin * b) * radDeg;
+	}
+
+	/** Rotates the world transform the specified amount.
+	 * <p>
+	 * After changes are made to the world transform, {@link #updateAppliedTransform()} should be called and {@link #update()} will
+	 * need to be called on any child bones, recursively. */
+	public void rotateWorld (float degrees) {
+		float cos = cosDeg(degrees), sin = sinDeg(degrees);
+		a = cos * a - sin * c;
+		b = cos * b - sin * d;
+		c = sin * a + cos * c;
+		d = sin * b + cos * d;
+	}
+
+	// ---
+
+	public String toString () {
+		return data.name;
+	}
+}

+ 206 - 206
spine-libgdx/spine-libgdx/src/com/esotericsoftware/spine/BoneData.java

@@ -1,206 +1,206 @@
-/******************************************************************************
- * Spine Runtimes License Agreement
- * Last updated January 1, 2020. Replaces all prior versions.
- *
- * Copyright (c) 2013-2020, Esoteric Software LLC
- *
- * Integration of the Spine Runtimes into software or otherwise creating
- * derivative works of the Spine Runtimes is permitted under the terms and
- * conditions of Section 2 of the Spine Editor License Agreement:
- * http://esotericsoftware.com/spine-editor-license
- *
- * Otherwise, it is permitted to integrate the Spine Runtimes into software
- * or otherwise create derivative works of the Spine Runtimes (collectively,
- * "Products"), provided that each user of the Products must obtain their own
- * Spine Editor license and redistribution of the Products in any form must
- * include this license and copyright notice.
- *
- * THE SPINE RUNTIMES ARE PROVIDED BY ESOTERIC SOFTWARE LLC "AS IS" AND ANY
- * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
- * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
- * DISCLAIMED. IN NO EVENT SHALL ESOTERIC SOFTWARE LLC BE LIABLE FOR ANY
- * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
- * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES,
- * BUSINESS INTERRUPTION, OR LOSS OF USE, DATA, OR PROFITS) HOWEVER CAUSED AND
- * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
- * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
- * THE SPINE RUNTIMES, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
- *****************************************************************************/
-
-package com.esotericsoftware.spine;
-
-import com.badlogic.gdx.graphics.Color;
-import com.badlogic.gdx.utils.Null;
-
-/** Stores the setup pose for a {@link Bone}. */
-public class BoneData {
-	final int index;
-	final String name;
-	@Null final BoneData parent;
-	float length;
-	float x, y, rotation, scaleX = 1, scaleY = 1, shearX, shearY;
-	TransformMode transformMode = TransformMode.normal;
-	boolean skinRequired;
-
-	// Nonessential.
-	final Color color = new Color(0.61f, 0.61f, 0.61f, 1); // 9b9b9bff
-
-	public BoneData (int index, String name, @Null BoneData parent) {
-		if (index < 0) throw new IllegalArgumentException("index must be >= 0.");
-		if (name == null) throw new IllegalArgumentException("name cannot be null.");
-		this.index = index;
-		this.name = name;
-		this.parent = parent;
-	}
-
-	/** Copy constructor. */
-	public BoneData (BoneData bone, @Null BoneData parent) {
-		if (bone == null) throw new IllegalArgumentException("bone cannot be null.");
-		index = bone.index;
-		name = bone.name;
-		this.parent = parent;
-		length = bone.length;
-		x = bone.x;
-		y = bone.y;
-		rotation = bone.rotation;
-		scaleX = bone.scaleX;
-		scaleY = bone.scaleY;
-		shearX = bone.shearX;
-		shearY = bone.shearY;
-	}
-
-	/** The index of the bone in {@link Skeleton#getBones()}. */
-	public int getIndex () {
-		return index;
-	}
-
-	/** The name of the bone, which is unique across all bones in the skeleton. */
-	public String getName () {
-		return name;
-	}
-
-	public @Null BoneData getParent () {
-		return parent;
-	}
-
-	/** The bone's length. */
-	public float getLength () {
-		return length;
-	}
-
-	public void setLength (float length) {
-		this.length = length;
-	}
-
-	/** The local x translation. */
-	public float getX () {
-		return x;
-	}
-
-	public void setX (float x) {
-		this.x = x;
-	}
-
-	/** The local y translation. */
-	public float getY () {
-		return y;
-	}
-
-	public void setY (float y) {
-		this.y = y;
-	}
-
-	public void setPosition (float x, float y) {
-		this.x = x;
-		this.y = y;
-	}
-
-	/** The local rotation. */
-	public float getRotation () {
-		return rotation;
-	}
-
-	public void setRotation (float rotation) {
-		this.rotation = rotation;
-	}
-
-	/** The local scaleX. */
-	public float getScaleX () {
-		return scaleX;
-	}
-
-	public void setScaleX (float scaleX) {
-		this.scaleX = scaleX;
-	}
-
-	/** The local scaleY. */
-	public float getScaleY () {
-		return scaleY;
-	}
-
-	public void setScaleY (float scaleY) {
-		this.scaleY = scaleY;
-	}
-
-	public void setScale (float scaleX, float scaleY) {
-		this.scaleX = scaleX;
-		this.scaleY = scaleY;
-	}
-
-	/** The local shearX. */
-	public float getShearX () {
-		return shearX;
-	}
-
-	public void setShearX (float shearX) {
-		this.shearX = shearX;
-	}
-
-	/** The local shearX. */
-	public float getShearY () {
-		return shearY;
-	}
-
-	public void setShearY (float shearY) {
-		this.shearY = shearY;
-	}
-
-	/** The transform mode for how parent world transforms affect this bone. */
-	public TransformMode getTransformMode () {
-		return transformMode;
-	}
-
-	public void setTransformMode (TransformMode transformMode) {
-		if (transformMode == null) throw new IllegalArgumentException("transformMode cannot be null.");
-		this.transformMode = transformMode;
-	}
-
-	/** When true, {@link Skeleton#updateWorldTransform()} only updates this bone if the {@link Skeleton#getSkin()} contains this
-	 * bone.
-	 * <p>
-	 * See {@link Skin#getBones()}. */
-	public boolean getSkinRequired () {
-		return skinRequired;
-	}
-
-	public void setSkinRequired (boolean skinRequired) {
-		this.skinRequired = skinRequired;
-	}
-
-	/** The color of the bone as it was in Spine, or a default color if nonessential data was not exported. Bones are not usually
-	 * rendered at runtime. */
-	public Color getColor () {
-		return color;
-	}
-
-	public String toString () {
-		return name;
-	}
-
-	/** Determines how a bone inherits world transforms from parent bones. */
-	static public enum TransformMode {
-		normal, onlyTranslation, noRotationOrReflection, noScale, noScaleOrReflection;
-
-		static public final TransformMode[] values = TransformMode.values();
-	}
-}
+/******************************************************************************
+ * Spine Runtimes License Agreement
+ * Last updated January 1, 2020. Replaces all prior versions.
+ *
+ * Copyright (c) 2013-2020, Esoteric Software LLC
+ *
+ * Integration of the Spine Runtimes into software or otherwise creating
+ * derivative works of the Spine Runtimes is permitted under the terms and
+ * conditions of Section 2 of the Spine Editor License Agreement:
+ * http://esotericsoftware.com/spine-editor-license
+ *
+ * Otherwise, it is permitted to integrate the Spine Runtimes into software
+ * or otherwise create derivative works of the Spine Runtimes (collectively,
+ * "Products"), provided that each user of the Products must obtain their own
+ * Spine Editor license and redistribution of the Products in any form must
+ * include this license and copyright notice.
+ *
+ * THE SPINE RUNTIMES ARE PROVIDED BY ESOTERIC SOFTWARE LLC "AS IS" AND ANY
+ * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL ESOTERIC SOFTWARE LLC BE LIABLE FOR ANY
+ * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES,
+ * BUSINESS INTERRUPTION, OR LOSS OF USE, DATA, OR PROFITS) HOWEVER CAUSED AND
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
+ * THE SPINE RUNTIMES, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ *****************************************************************************/
+
+package com.esotericsoftware.spine;
+
+import com.badlogic.gdx.graphics.Color;
+import com.badlogic.gdx.utils.Null;
+
+/** Stores the setup pose for a {@link Bone}. */
+public class BoneData {
+	final int index;
+	final String name;
+	@Null final BoneData parent;
+	float length;
+	float x, y, rotation, scaleX = 1, scaleY = 1, shearX, shearY;
+	TransformMode transformMode = TransformMode.normal;
+	boolean skinRequired;
+
+	// Nonessential.
+	final Color color = new Color(0.61f, 0.61f, 0.61f, 1); // 9b9b9bff
+
+	public BoneData (int index, String name, @Null BoneData parent) {
+		if (index < 0) throw new IllegalArgumentException("index must be >= 0.");
+		if (name == null) throw new IllegalArgumentException("name cannot be null.");
+		this.index = index;
+		this.name = name;
+		this.parent = parent;
+	}
+
+	/** Copy constructor. */
+	public BoneData (BoneData bone, @Null BoneData parent) {
+		if (bone == null) throw new IllegalArgumentException("bone cannot be null.");
+		index = bone.index;
+		name = bone.name;
+		this.parent = parent;
+		length = bone.length;
+		x = bone.x;
+		y = bone.y;
+		rotation = bone.rotation;
+		scaleX = bone.scaleX;
+		scaleY = bone.scaleY;
+		shearX = bone.shearX;
+		shearY = bone.shearY;
+	}
+
+	/** The index of the bone in {@link Skeleton#getBones()}. */
+	public int getIndex () {
+		return index;
+	}
+
+	/** The name of the bone, which is unique across all bones in the skeleton. */
+	public String getName () {
+		return name;
+	}
+
+	public @Null BoneData getParent () {
+		return parent;
+	}
+
+	/** The bone's length. */
+	public float getLength () {
+		return length;
+	}
+
+	public void setLength (float length) {
+		this.length = length;
+	}
+
+	/** The local x translation. */
+	public float getX () {
+		return x;
+	}
+
+	public void setX (float x) {
+		this.x = x;
+	}
+
+	/** The local y translation. */
+	public float getY () {
+		return y;
+	}
+
+	public void setY (float y) {
+		this.y = y;
+	}
+
+	public void setPosition (float x, float y) {
+		this.x = x;
+		this.y = y;
+	}
+
+	/** The local rotation. */
+	public float getRotation () {
+		return rotation;
+	}
+
+	public void setRotation (float rotation) {
+		this.rotation = rotation;
+	}
+
+	/** The local scaleX. */
+	public float getScaleX () {
+		return scaleX;
+	}
+
+	public void setScaleX (float scaleX) {
+		this.scaleX = scaleX;
+	}
+
+	/** The local scaleY. */
+	public float getScaleY () {
+		return scaleY;
+	}
+
+	public void setScaleY (float scaleY) {
+		this.scaleY = scaleY;
+	}
+
+	public void setScale (float scaleX, float scaleY) {
+		this.scaleX = scaleX;
+		this.scaleY = scaleY;
+	}
+
+	/** The local shearX. */
+	public float getShearX () {
+		return shearX;
+	}
+
+	public void setShearX (float shearX) {
+		this.shearX = shearX;
+	}
+
+	/** The local shearX. */
+	public float getShearY () {
+		return shearY;
+	}
+
+	public void setShearY (float shearY) {
+		this.shearY = shearY;
+	}
+
+	/** The transform mode for how parent world transforms affect this bone. */
+	public TransformMode getTransformMode () {
+		return transformMode;
+	}
+
+	public void setTransformMode (TransformMode transformMode) {
+		if (transformMode == null) throw new IllegalArgumentException("transformMode cannot be null.");
+		this.transformMode = transformMode;
+	}
+
+	/** When true, {@link Skeleton#updateWorldTransform()} only updates this bone if the {@link Skeleton#getSkin()} contains this
+	 * bone.
+	 * <p>
+	 * See {@link Skin#getBones()}. */
+	public boolean getSkinRequired () {
+		return skinRequired;
+	}
+
+	public void setSkinRequired (boolean skinRequired) {
+		this.skinRequired = skinRequired;
+	}
+
+	/** The color of the bone as it was in Spine, or a default color if nonessential data was not exported. Bones are not usually
+	 * rendered at runtime. */
+	public Color getColor () {
+		return color;
+	}
+
+	public String toString () {
+		return name;
+	}
+
+	/** Determines how a bone inherits world transforms from parent bones. */
+	static public enum TransformMode {
+		normal, onlyTranslation, noRotationOrReflection, noScale, noScaleOrReflection;
+
+		static public final TransformMode[] values = TransformMode.values();
+	}
+}

+ 109 - 109
spine-libgdx/spine-libgdx/src/com/esotericsoftware/spine/Event.java

@@ -1,109 +1,109 @@
-/******************************************************************************
- * Spine Runtimes License Agreement
- * Last updated January 1, 2020. Replaces all prior versions.
- *
- * Copyright (c) 2013-2020, Esoteric Software LLC
- *
- * Integration of the Spine Runtimes into software or otherwise creating
- * derivative works of the Spine Runtimes is permitted under the terms and
- * conditions of Section 2 of the Spine Editor License Agreement:
- * http://esotericsoftware.com/spine-editor-license
- *
- * Otherwise, it is permitted to integrate the Spine Runtimes into software
- * or otherwise create derivative works of the Spine Runtimes (collectively,
- * "Products"), provided that each user of the Products must obtain their own
- * Spine Editor license and redistribution of the Products in any form must
- * include this license and copyright notice.
- *
- * THE SPINE RUNTIMES ARE PROVIDED BY ESOTERIC SOFTWARE LLC "AS IS" AND ANY
- * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
- * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
- * DISCLAIMED. IN NO EVENT SHALL ESOTERIC SOFTWARE LLC BE LIABLE FOR ANY
- * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
- * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES,
- * BUSINESS INTERRUPTION, OR LOSS OF USE, DATA, OR PROFITS) HOWEVER CAUSED AND
- * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
- * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
- * THE SPINE RUNTIMES, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
- *****************************************************************************/
-
-package com.esotericsoftware.spine;
-
-import com.esotericsoftware.spine.Animation.Timeline;
-import com.esotericsoftware.spine.AnimationState.AnimationStateListener;
-
-/** Stores the current pose values for an {@link Event}.
- * <p>
- * See Timeline
- * {@link Timeline#apply(Skeleton, float, float, com.badlogic.gdx.utils.Array, float, com.esotericsoftware.spine.Animation.MixBlend, com.esotericsoftware.spine.Animation.MixDirection)},
- * AnimationStateListener {@link AnimationStateListener#event(com.esotericsoftware.spine.AnimationState.TrackEntry, Event)}, and
- * <a href="http://esotericsoftware.com/spine-events">Events</a> in the Spine User Guide. */
-public class Event {
-	private final EventData data;
-	int intValue;
-	float floatValue;
-	String stringValue;
-	float volume, balance;
-	final float time;
-
-	public Event (float time, EventData data) {
-		if (data == null) throw new IllegalArgumentException("data cannot be null.");
-		this.time = time;
-		this.data = data;
-	}
-
-	public int getInt () {
-		return intValue;
-	}
-
-	public void setInt (int intValue) {
-		this.intValue = intValue;
-	}
-
-	public float getFloat () {
-		return floatValue;
-	}
-
-	public void setFloat (float floatValue) {
-		this.floatValue = floatValue;
-	}
-
-	public String getString () {
-		return stringValue;
-	}
-
-	public void setString (String stringValue) {
-		if (stringValue == null) throw new IllegalArgumentException("stringValue cannot be null.");
-		this.stringValue = stringValue;
-	}
-
-	public float getVolume () {
-		return volume;
-	}
-
-	public void setVolume (float volume) {
-		this.volume = volume;
-	}
-
-	public float getBalance () {
-		return balance;
-	}
-
-	public void setBalance (float balance) {
-		this.balance = balance;
-	}
-
-	/** The animation time this event was keyed. */
-	public float getTime () {
-		return time;
-	}
-
-	/** The events's setup pose data. */
-	public EventData getData () {
-		return data;
-	}
-
-	public String toString () {
-		return data.name;
-	}
-}
+/******************************************************************************
+ * Spine Runtimes License Agreement
+ * Last updated January 1, 2020. Replaces all prior versions.
+ *
+ * Copyright (c) 2013-2020, Esoteric Software LLC
+ *
+ * Integration of the Spine Runtimes into software or otherwise creating
+ * derivative works of the Spine Runtimes is permitted under the terms and
+ * conditions of Section 2 of the Spine Editor License Agreement:
+ * http://esotericsoftware.com/spine-editor-license
+ *
+ * Otherwise, it is permitted to integrate the Spine Runtimes into software
+ * or otherwise create derivative works of the Spine Runtimes (collectively,
+ * "Products"), provided that each user of the Products must obtain their own
+ * Spine Editor license and redistribution of the Products in any form must
+ * include this license and copyright notice.
+ *
+ * THE SPINE RUNTIMES ARE PROVIDED BY ESOTERIC SOFTWARE LLC "AS IS" AND ANY
+ * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL ESOTERIC SOFTWARE LLC BE LIABLE FOR ANY
+ * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES,
+ * BUSINESS INTERRUPTION, OR LOSS OF USE, DATA, OR PROFITS) HOWEVER CAUSED AND
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
+ * THE SPINE RUNTIMES, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ *****************************************************************************/
+
+package com.esotericsoftware.spine;
+
+import com.esotericsoftware.spine.Animation.Timeline;
+import com.esotericsoftware.spine.AnimationState.AnimationStateListener;
+
+/** Stores the current pose values for an {@link Event}.
+ * <p>
+ * See Timeline
+ * {@link Timeline#apply(Skeleton, float, float, com.badlogic.gdx.utils.Array, float, com.esotericsoftware.spine.Animation.MixBlend, com.esotericsoftware.spine.Animation.MixDirection)},
+ * AnimationStateListener {@link AnimationStateListener#event(com.esotericsoftware.spine.AnimationState.TrackEntry, Event)}, and
+ * <a href="http://esotericsoftware.com/spine-events">Events</a> in the Spine User Guide. */
+public class Event {
+	private final EventData data;
+	int intValue;
+	float floatValue;
+	String stringValue;
+	float volume, balance;
+	final float time;
+
+	public Event (float time, EventData data) {
+		if (data == null) throw new IllegalArgumentException("data cannot be null.");
+		this.time = time;
+		this.data = data;
+	}
+
+	public int getInt () {
+		return intValue;
+	}
+
+	public void setInt (int intValue) {
+		this.intValue = intValue;
+	}
+
+	public float getFloat () {
+		return floatValue;
+	}
+
+	public void setFloat (float floatValue) {
+		this.floatValue = floatValue;
+	}
+
+	public String getString () {
+		return stringValue;
+	}
+
+	public void setString (String stringValue) {
+		if (stringValue == null) throw new IllegalArgumentException("stringValue cannot be null.");
+		this.stringValue = stringValue;
+	}
+
+	public float getVolume () {
+		return volume;
+	}
+
+	public void setVolume (float volume) {
+		this.volume = volume;
+	}
+
+	public float getBalance () {
+		return balance;
+	}
+
+	public void setBalance (float balance) {
+		this.balance = balance;
+	}
+
+	/** The animation time this event was keyed. */
+	public float getTime () {
+		return time;
+	}
+
+	/** The events's setup pose data. */
+	public EventData getData () {
+		return data;
+	}
+
+	public String toString () {
+		return data.name;
+	}
+}

+ 105 - 105
spine-libgdx/spine-libgdx/src/com/esotericsoftware/spine/EventData.java

@@ -1,105 +1,105 @@
-/******************************************************************************
- * Spine Runtimes License Agreement
- * Last updated January 1, 2020. Replaces all prior versions.
- *
- * Copyright (c) 2013-2020, Esoteric Software LLC
- *
- * Integration of the Spine Runtimes into software or otherwise creating
- * derivative works of the Spine Runtimes is permitted under the terms and
- * conditions of Section 2 of the Spine Editor License Agreement:
- * http://esotericsoftware.com/spine-editor-license
- *
- * Otherwise, it is permitted to integrate the Spine Runtimes into software
- * or otherwise create derivative works of the Spine Runtimes (collectively,
- * "Products"), provided that each user of the Products must obtain their own
- * Spine Editor license and redistribution of the Products in any form must
- * include this license and copyright notice.
- *
- * THE SPINE RUNTIMES ARE PROVIDED BY ESOTERIC SOFTWARE LLC "AS IS" AND ANY
- * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
- * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
- * DISCLAIMED. IN NO EVENT SHALL ESOTERIC SOFTWARE LLC BE LIABLE FOR ANY
- * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
- * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES,
- * BUSINESS INTERRUPTION, OR LOSS OF USE, DATA, OR PROFITS) HOWEVER CAUSED AND
- * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
- * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
- * THE SPINE RUNTIMES, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
- *****************************************************************************/
-
-package com.esotericsoftware.spine;
-
-/** Stores the setup pose values for an {@link Event}.
- * <p>
- * See <a href="http://esotericsoftware.com/spine-events">Events</a> in the Spine User Guide. */
-public class EventData {
-	final String name;
-	int intValue;
-	float floatValue;
-	String stringValue, audioPath;
-	float volume, balance;
-
-	public EventData (String name) {
-		if (name == null) throw new IllegalArgumentException("name cannot be null.");
-		this.name = name;
-	}
-
-	public int getInt () {
-		return intValue;
-	}
-
-	public void setInt (int intValue) {
-		this.intValue = intValue;
-	}
-
-	public float getFloat () {
-		return floatValue;
-	}
-
-	public void setFloat (float floatValue) {
-		this.floatValue = floatValue;
-	}
-
-	public String getString () {
-		return stringValue;
-	}
-
-	public void setString (String stringValue) {
-		if (stringValue == null) throw new IllegalArgumentException("stringValue cannot be null.");
-		this.stringValue = stringValue;
-	}
-
-	public String getAudioPath () {
-		return audioPath;
-	}
-
-	public void setAudioPath (String audioPath) {
-		if (audioPath == null) throw new IllegalArgumentException("audioPath cannot be null.");
-		this.audioPath = audioPath;
-	}
-
-	public float getVolume () {
-		return volume;
-	}
-
-	public void setVolume (float volume) {
-		this.volume = volume;
-	}
-
-	public float getBalance () {
-		return balance;
-	}
-
-	public void setBalance (float balance) {
-		this.balance = balance;
-	}
-
-	/** The name of the event, which is unique across all events in the skeleton. */
-	public String getName () {
-		return name;
-	}
-
-	public String toString () {
-		return name;
-	}
-}
+/******************************************************************************
+ * Spine Runtimes License Agreement
+ * Last updated January 1, 2020. Replaces all prior versions.
+ *
+ * Copyright (c) 2013-2020, Esoteric Software LLC
+ *
+ * Integration of the Spine Runtimes into software or otherwise creating
+ * derivative works of the Spine Runtimes is permitted under the terms and
+ * conditions of Section 2 of the Spine Editor License Agreement:
+ * http://esotericsoftware.com/spine-editor-license
+ *
+ * Otherwise, it is permitted to integrate the Spine Runtimes into software
+ * or otherwise create derivative works of the Spine Runtimes (collectively,
+ * "Products"), provided that each user of the Products must obtain their own
+ * Spine Editor license and redistribution of the Products in any form must
+ * include this license and copyright notice.
+ *
+ * THE SPINE RUNTIMES ARE PROVIDED BY ESOTERIC SOFTWARE LLC "AS IS" AND ANY
+ * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL ESOTERIC SOFTWARE LLC BE LIABLE FOR ANY
+ * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES,
+ * BUSINESS INTERRUPTION, OR LOSS OF USE, DATA, OR PROFITS) HOWEVER CAUSED AND
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
+ * THE SPINE RUNTIMES, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ *****************************************************************************/
+
+package com.esotericsoftware.spine;
+
+/** Stores the setup pose values for an {@link Event}.
+ * <p>
+ * See <a href="http://esotericsoftware.com/spine-events">Events</a> in the Spine User Guide. */
+public class EventData {
+	final String name;
+	int intValue;
+	float floatValue;
+	String stringValue, audioPath;
+	float volume, balance;
+
+	public EventData (String name) {
+		if (name == null) throw new IllegalArgumentException("name cannot be null.");
+		this.name = name;
+	}
+
+	public int getInt () {
+		return intValue;
+	}
+
+	public void setInt (int intValue) {
+		this.intValue = intValue;
+	}
+
+	public float getFloat () {
+		return floatValue;
+	}
+
+	public void setFloat (float floatValue) {
+		this.floatValue = floatValue;
+	}
+
+	public String getString () {
+		return stringValue;
+	}
+
+	public void setString (String stringValue) {
+		if (stringValue == null) throw new IllegalArgumentException("stringValue cannot be null.");
+		this.stringValue = stringValue;
+	}
+
+	public String getAudioPath () {
+		return audioPath;
+	}
+
+	public void setAudioPath (String audioPath) {
+		if (audioPath == null) throw new IllegalArgumentException("audioPath cannot be null.");
+		this.audioPath = audioPath;
+	}
+
+	public float getVolume () {
+		return volume;
+	}
+
+	public void setVolume (float volume) {
+		this.volume = volume;
+	}
+
+	public float getBalance () {
+		return balance;
+	}
+
+	public void setBalance (float balance) {
+		this.balance = balance;
+	}
+
+	/** The name of the event, which is unique across all events in the skeleton. */
+	public String getName () {
+		return name;
+	}
+
+	public String toString () {
+		return name;
+	}
+}

+ 375 - 375
spine-libgdx/spine-libgdx/src/com/esotericsoftware/spine/IkConstraint.java

@@ -1,375 +1,375 @@
-/******************************************************************************
- * Spine Runtimes License Agreement
- * Last updated January 1, 2020. Replaces all prior versions.
- *
- * Copyright (c) 2013-2020, Esoteric Software LLC
- *
- * Integration of the Spine Runtimes into software or otherwise creating
- * derivative works of the Spine Runtimes is permitted under the terms and
- * conditions of Section 2 of the Spine Editor License Agreement:
- * http://esotericsoftware.com/spine-editor-license
- *
- * Otherwise, it is permitted to integrate the Spine Runtimes into software
- * or otherwise create derivative works of the Spine Runtimes (collectively,
- * "Products"), provided that each user of the Products must obtain their own
- * Spine Editor license and redistribution of the Products in any form must
- * include this license and copyright notice.
- *
- * THE SPINE RUNTIMES ARE PROVIDED BY ESOTERIC SOFTWARE LLC "AS IS" AND ANY
- * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
- * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
- * DISCLAIMED. IN NO EVENT SHALL ESOTERIC SOFTWARE LLC BE LIABLE FOR ANY
- * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
- * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES,
- * BUSINESS INTERRUPTION, OR LOSS OF USE, DATA, OR PROFITS) HOWEVER CAUSED AND
- * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
- * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
- * THE SPINE RUNTIMES, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
- *****************************************************************************/
-
-package com.esotericsoftware.spine;
-
-import static com.esotericsoftware.spine.utils.SpineUtils.*;
-
-import com.badlogic.gdx.utils.Array;
-
-/** Stores the current pose for an IK constraint. An IK constraint adjusts the rotation of 1 or 2 constrained bones so the tip of
- * the last bone is as close to the target bone as possible.
- * <p>
- * See <a href="http://esotericsoftware.com/spine-ik-constraints">IK constraints</a> in the Spine User Guide. */
-public class IkConstraint implements Updatable {
-	final IkConstraintData data;
-	final Array<Bone> bones;
-	Bone target;
-	int bendDirection;
-	boolean compress, stretch;
-	float mix = 1, softness;
-
-	boolean active;
-
-	public IkConstraint (IkConstraintData data, Skeleton skeleton) {
-		if (data == null) throw new IllegalArgumentException("data cannot be null.");
-		if (skeleton == null) throw new IllegalArgumentException("skeleton cannot be null.");
-		this.data = data;
-		mix = data.mix;
-		softness = data.softness;
-		bendDirection = data.bendDirection;
-		compress = data.compress;
-		stretch = data.stretch;
-
-		bones = new Array(data.bones.size);
-		for (BoneData boneData : data.bones)
-			bones.add(skeleton.findBone(boneData.name));
-		target = skeleton.findBone(data.target.name);
-	}
-
-	/** Copy constructor. */
-	public IkConstraint (IkConstraint constraint, Skeleton skeleton) {
-		if (constraint == null) throw new IllegalArgumentException("constraint cannot be null.");
-		if (skeleton == null) throw new IllegalArgumentException("skeleton cannot be null.");
-		data = constraint.data;
-		bones = new Array(constraint.bones.size);
-		for (Bone bone : constraint.bones)
-			bones.add(skeleton.bones.get(bone.data.index));
-		target = skeleton.bones.get(constraint.target.data.index);
-		mix = constraint.mix;
-		softness = constraint.softness;
-		bendDirection = constraint.bendDirection;
-		compress = constraint.compress;
-		stretch = constraint.stretch;
-	}
-
-	/** Applies the constraint to the constrained bones. */
-	public void update () {
-		if (mix == 0) return;
-		Bone target = this.target;
-		Object[] bones = this.bones.items;
-		switch (this.bones.size) {
-		case 1:
-			apply((Bone)bones[0], target.worldX, target.worldY, compress, stretch, data.uniform, mix);
-			break;
-		case 2:
-			apply((Bone)bones[0], (Bone)bones[1], target.worldX, target.worldY, bendDirection, stretch, data.uniform, softness, mix);
-			break;
-		}
-	}
-
-	/** The bones that will be modified by this IK constraint. */
-	public Array<Bone> getBones () {
-		return bones;
-	}
-
-	/** The bone that is the IK target. */
-	public Bone getTarget () {
-		return target;
-	}
-
-	public void setTarget (Bone target) {
-		if (target == null) throw new IllegalArgumentException("target cannot be null.");
-		this.target = target;
-	}
-
-	/** A percentage (0-1) that controls the mix between the constrained and unconstrained rotation.
-	 * <p>
-	 * For two bone IK: if the parent bone has local nonuniform scale, the child bone's local Y translation is set to 0. */
-	public float getMix () {
-		return mix;
-	}
-
-	public void setMix (float mix) {
-		this.mix = mix;
-	}
-
-	/** For two bone IK, the target bone's distance from the maximum reach of the bones where rotation begins to slow. The bones
-	 * will not straighten completely until the target is this far out of range. */
-	public float getSoftness () {
-		return softness;
-	}
-
-	public void setSoftness (float softness) {
-		this.softness = softness;
-	}
-
-	/** For two bone IK, controls the bend direction of the IK bones, either 1 or -1. */
-	public int getBendDirection () {
-		return bendDirection;
-	}
-
-	public void setBendDirection (int bendDirection) {
-		this.bendDirection = bendDirection;
-	}
-
-	/** For one bone IK, when true and the target is too close, the bone is scaled to reach it. */
-	public boolean getCompress () {
-		return compress;
-	}
-
-	public void setCompress (boolean compress) {
-		this.compress = compress;
-	}
-
-	/** When true and the target is out of range, the parent bone is scaled to reach it.
-	 * <p>
-	 * For two bone IK: 1) the child bone's local Y translation is set to 0, 2) stretch is not applied if {@link #getSoftness()} is
-	 * > 0, and 3) if the parent bone has local nonuniform scale, stretch is not applied. */
-	public boolean getStretch () {
-		return stretch;
-	}
-
-	public void setStretch (boolean stretch) {
-		this.stretch = stretch;
-	}
-
-	public boolean isActive () {
-		return active;
-	}
-
-	/** The IK constraint's setup pose data. */
-	public IkConstraintData getData () {
-		return data;
-	}
-
-	public String toString () {
-		return data.name;
-	}
-
-	/** Applies 1 bone IK. The target is specified in the world coordinate system. */
-	static public void apply (Bone bone, float targetX, float targetY, boolean compress, boolean stretch, boolean uniform,
-		float alpha) {
-		if (bone == null) throw new IllegalArgumentException("bone cannot be null.");
-		Bone p = bone.parent;
-		float pa = p.a, pb = p.b, pc = p.c, pd = p.d;
-		float rotationIK = -bone.ashearX - bone.arotation, tx, ty;
-		switch (bone.data.transformMode) {
-		case onlyTranslation:
-			tx = targetX - bone.worldX;
-			ty = targetY - bone.worldY;
-			break;
-		case noRotationOrReflection:
-			float s = Math.abs(pa * pd - pb * pc) / (pa * pa + pc * pc);
-			float sa = pa / bone.skeleton.scaleX;
-			float sc = pc / bone.skeleton.scaleY;
-			pb = -sc * s * bone.skeleton.scaleX;
-			pd = sa * s * bone.skeleton.scaleY;
-			rotationIK += atan2(sc, sa) * radDeg;
-			// Fall through.
-		default:
-			float x = targetX - p.worldX, y = targetY - p.worldY;
-			float d = pa * pd - pb * pc;
-			tx = (x * pd - y * pb) / d - bone.ax;
-			ty = (y * pa - x * pc) / d - bone.ay;
-		}
-		rotationIK += atan2(ty, tx) * radDeg;
-		if (bone.ascaleX < 0) rotationIK += 180;
-		if (rotationIK > 180)
-			rotationIK -= 360;
-		else if (rotationIK < -180) //
-			rotationIK += 360;
-		float sx = bone.ascaleX, sy = bone.ascaleY;
-		if (compress || stretch) {
-			switch (bone.data.transformMode) {
-			case noScale:
-			case noScaleOrReflection:
-				tx = targetX - bone.worldX;
-				ty = targetY - bone.worldY;
-			}
-			float b = bone.data.length * sx, dd = (float)Math.sqrt(tx * tx + ty * ty);
-			if ((compress && dd < b) || (stretch && dd > b) && b > 0.0001f) {
-				float s = (dd / b - 1) * alpha + 1;
-				sx *= s;
-				if (uniform) sy *= s;
-			}
-		}
-		bone.updateWorldTransform(bone.ax, bone.ay, bone.arotation + rotationIK * alpha, sx, sy, bone.ashearX, bone.ashearY);
-	}
-
-	/** Applies 2 bone IK. The target is specified in the world coordinate system.
-	 * @param child A direct descendant of the parent bone. */
-	static public void apply (Bone parent, Bone child, float targetX, float targetY, int bendDir, boolean stretch, boolean uniform,
-		float softness, float alpha) {
-		if (parent == null) throw new IllegalArgumentException("parent cannot be null.");
-		if (child == null) throw new IllegalArgumentException("child cannot be null.");
-		float px = parent.ax, py = parent.ay, psx = parent.ascaleX, psy = parent.ascaleY, sx = psx, sy = psy, csx = child.ascaleX;
-		int os1, os2, s2;
-		if (psx < 0) {
-			psx = -psx;
-			os1 = 180;
-			s2 = -1;
-		} else {
-			os1 = 0;
-			s2 = 1;
-		}
-		if (psy < 0) {
-			psy = -psy;
-			s2 = -s2;
-		}
-		if (csx < 0) {
-			csx = -csx;
-			os2 = 180;
-		} else
-			os2 = 0;
-		float cx = child.ax, cy, cwx, cwy, a = parent.a, b = parent.b, c = parent.c, d = parent.d;
-		boolean u = Math.abs(psx - psy) <= 0.0001f;
-		if (!u || stretch) {
-			cy = 0;
-			cwx = a * cx + parent.worldX;
-			cwy = c * cx + parent.worldY;
-		} else {
-			cy = child.ay;
-			cwx = a * cx + b * cy + parent.worldX;
-			cwy = c * cx + d * cy + parent.worldY;
-		}
-		Bone pp = parent.parent;
-		a = pp.a;
-		b = pp.b;
-		c = pp.c;
-		d = pp.d;
-		float id = 1 / (a * d - b * c), x = cwx - pp.worldX, y = cwy - pp.worldY;
-		float dx = (x * d - y * b) * id - px, dy = (y * a - x * c) * id - py;
-		float l1 = (float)Math.sqrt(dx * dx + dy * dy), l2 = child.data.length * csx, a1, a2;
-		if (l1 < 0.0001f) {
-			apply(parent, targetX, targetY, false, stretch, false, alpha);
-			child.updateWorldTransform(cx, cy, 0, child.ascaleX, child.ascaleY, child.ashearX, child.ashearY);
-			return;
-		}
-		x = targetX - pp.worldX;
-		y = targetY - pp.worldY;
-		float tx = (x * d - y * b) * id - px, ty = (y * a - x * c) * id - py;
-		float dd = tx * tx + ty * ty;
-		if (softness != 0) {
-			softness *= psx * (csx + 1) * 0.5f;
-			float td = (float)Math.sqrt(dd), sd = td - l1 - l2 * psx + softness;
-			if (sd > 0) {
-				float p = Math.min(1, sd / (softness * 2)) - 1;
-				p = (sd - softness * (1 - p * p)) / td;
-				tx -= p * tx;
-				ty -= p * ty;
-				dd = tx * tx + ty * ty;
-			}
-		}
-		outer:
-		if (u) {
-			l2 *= psx;
-			float cos = (dd - l1 * l1 - l2 * l2) / (2 * l1 * l2);
-			if (cos < -1) {
-				cos = -1;
-				a2 = PI * bendDir;
-			} else if (cos > 1) {
-				cos = 1;
-				a2 = 0;
-				if (stretch) {
-					a = ((float)Math.sqrt(dd) / (l1 + l2) - 1) * alpha + 1;
-					sx *= a;
-					if (uniform) sy *= a;
-				}
-			} else
-				a2 = (float)Math.acos(cos) * bendDir;
-			a = l1 + l2 * cos;
-			b = l2 * sin(a2);
-			a1 = atan2(ty * a - tx * b, tx * a + ty * b);
-		} else {
-			a = psx * l2;
-			b = psy * l2;
-			float aa = a * a, bb = b * b, ta = atan2(ty, tx);
-			c = bb * l1 * l1 + aa * dd - aa * bb;
-			float c1 = -2 * bb * l1, c2 = bb - aa;
-			d = c1 * c1 - 4 * c2 * c;
-			if (d >= 0) {
-				float q = (float)Math.sqrt(d);
-				if (c1 < 0) q = -q;
-				q = -(c1 + q) * 0.5f;
-				float r0 = q / c2, r1 = c / q;
-				float r = Math.abs(r0) < Math.abs(r1) ? r0 : r1;
-				if (r * r <= dd) {
-					y = (float)Math.sqrt(dd - r * r) * bendDir;
-					a1 = ta - atan2(y, r);
-					a2 = atan2(y / psy, (r - l1) / psx);
-					break outer;
-				}
-			}
-			float minAngle = PI, minX = l1 - a, minDist = minX * minX, minY = 0;
-			float maxAngle = 0, maxX = l1 + a, maxDist = maxX * maxX, maxY = 0;
-			c = -a * l1 / (aa - bb);
-			if (c >= -1 && c <= 1) {
-				c = (float)Math.acos(c);
-				x = a * cos(c) + l1;
-				y = b * sin(c);
-				d = x * x + y * y;
-				if (d < minDist) {
-					minAngle = c;
-					minDist = d;
-					minX = x;
-					minY = y;
-				}
-				if (d > maxDist) {
-					maxAngle = c;
-					maxDist = d;
-					maxX = x;
-					maxY = y;
-				}
-			}
-			if (dd <= (minDist + maxDist) * 0.5f) {
-				a1 = ta - atan2(minY * bendDir, minX);
-				a2 = minAngle * bendDir;
-			} else {
-				a1 = ta - atan2(maxY * bendDir, maxX);
-				a2 = maxAngle * bendDir;
-			}
-		}
-		float os = atan2(cy, cx) * s2;
-		float rotation = parent.arotation;
-		a1 = (a1 - os) * radDeg + os1 - rotation;
-		if (a1 > 180)
-			a1 -= 360;
-		else if (a1 < -180) //
-			a1 += 360;
-		parent.updateWorldTransform(px, py, rotation + a1 * alpha, sx, sy, 0, 0);
-		rotation = child.arotation;
-		a2 = ((a2 + os) * radDeg - child.ashearX) * s2 + os2 - rotation;
-		if (a2 > 180)
-			a2 -= 360;
-		else if (a2 < -180) //
-			a2 += 360;
-		child.updateWorldTransform(cx, cy, rotation + a2 * alpha, child.ascaleX, child.ascaleY, child.ashearX, child.ashearY);
-	}
-}
+/******************************************************************************
+ * Spine Runtimes License Agreement
+ * Last updated January 1, 2020. Replaces all prior versions.
+ *
+ * Copyright (c) 2013-2020, Esoteric Software LLC
+ *
+ * Integration of the Spine Runtimes into software or otherwise creating
+ * derivative works of the Spine Runtimes is permitted under the terms and
+ * conditions of Section 2 of the Spine Editor License Agreement:
+ * http://esotericsoftware.com/spine-editor-license
+ *
+ * Otherwise, it is permitted to integrate the Spine Runtimes into software
+ * or otherwise create derivative works of the Spine Runtimes (collectively,
+ * "Products"), provided that each user of the Products must obtain their own
+ * Spine Editor license and redistribution of the Products in any form must
+ * include this license and copyright notice.
+ *
+ * THE SPINE RUNTIMES ARE PROVIDED BY ESOTERIC SOFTWARE LLC "AS IS" AND ANY
+ * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL ESOTERIC SOFTWARE LLC BE LIABLE FOR ANY
+ * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES,
+ * BUSINESS INTERRUPTION, OR LOSS OF USE, DATA, OR PROFITS) HOWEVER CAUSED AND
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
+ * THE SPINE RUNTIMES, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ *****************************************************************************/
+
+package com.esotericsoftware.spine;
+
+import static com.esotericsoftware.spine.utils.SpineUtils.*;
+
+import com.badlogic.gdx.utils.Array;
+
+/** Stores the current pose for an IK constraint. An IK constraint adjusts the rotation of 1 or 2 constrained bones so the tip of
+ * the last bone is as close to the target bone as possible.
+ * <p>
+ * See <a href="http://esotericsoftware.com/spine-ik-constraints">IK constraints</a> in the Spine User Guide. */
+public class IkConstraint implements Updatable {
+	final IkConstraintData data;
+	final Array<Bone> bones;
+	Bone target;
+	int bendDirection;
+	boolean compress, stretch;
+	float mix = 1, softness;
+
+	boolean active;
+
+	public IkConstraint (IkConstraintData data, Skeleton skeleton) {
+		if (data == null) throw new IllegalArgumentException("data cannot be null.");
+		if (skeleton == null) throw new IllegalArgumentException("skeleton cannot be null.");
+		this.data = data;
+		mix = data.mix;
+		softness = data.softness;
+		bendDirection = data.bendDirection;
+		compress = data.compress;
+		stretch = data.stretch;
+
+		bones = new Array(data.bones.size);
+		for (BoneData boneData : data.bones)
+			bones.add(skeleton.findBone(boneData.name));
+		target = skeleton.findBone(data.target.name);
+	}
+
+	/** Copy constructor. */
+	public IkConstraint (IkConstraint constraint, Skeleton skeleton) {
+		if (constraint == null) throw new IllegalArgumentException("constraint cannot be null.");
+		if (skeleton == null) throw new IllegalArgumentException("skeleton cannot be null.");
+		data = constraint.data;
+		bones = new Array(constraint.bones.size);
+		for (Bone bone : constraint.bones)
+			bones.add(skeleton.bones.get(bone.data.index));
+		target = skeleton.bones.get(constraint.target.data.index);
+		mix = constraint.mix;
+		softness = constraint.softness;
+		bendDirection = constraint.bendDirection;
+		compress = constraint.compress;
+		stretch = constraint.stretch;
+	}
+
+	/** Applies the constraint to the constrained bones. */
+	public void update () {
+		if (mix == 0) return;
+		Bone target = this.target;
+		Object[] bones = this.bones.items;
+		switch (this.bones.size) {
+		case 1:
+			apply((Bone)bones[0], target.worldX, target.worldY, compress, stretch, data.uniform, mix);
+			break;
+		case 2:
+			apply((Bone)bones[0], (Bone)bones[1], target.worldX, target.worldY, bendDirection, stretch, data.uniform, softness, mix);
+			break;
+		}
+	}
+
+	/** The bones that will be modified by this IK constraint. */
+	public Array<Bone> getBones () {
+		return bones;
+	}
+
+	/** The bone that is the IK target. */
+	public Bone getTarget () {
+		return target;
+	}
+
+	public void setTarget (Bone target) {
+		if (target == null) throw new IllegalArgumentException("target cannot be null.");
+		this.target = target;
+	}
+
+	/** A percentage (0-1) that controls the mix between the constrained and unconstrained rotation.
+	 * <p>
+	 * For two bone IK: if the parent bone has local nonuniform scale, the child bone's local Y translation is set to 0. */
+	public float getMix () {
+		return mix;
+	}
+
+	public void setMix (float mix) {
+		this.mix = mix;
+	}
+
+	/** For two bone IK, the target bone's distance from the maximum reach of the bones where rotation begins to slow. The bones
+	 * will not straighten completely until the target is this far out of range. */
+	public float getSoftness () {
+		return softness;
+	}
+
+	public void setSoftness (float softness) {
+		this.softness = softness;
+	}
+
+	/** For two bone IK, controls the bend direction of the IK bones, either 1 or -1. */
+	public int getBendDirection () {
+		return bendDirection;
+	}
+
+	public void setBendDirection (int bendDirection) {
+		this.bendDirection = bendDirection;
+	}
+
+	/** For one bone IK, when true and the target is too close, the bone is scaled to reach it. */
+	public boolean getCompress () {
+		return compress;
+	}
+
+	public void setCompress (boolean compress) {
+		this.compress = compress;
+	}
+
+	/** When true and the target is out of range, the parent bone is scaled to reach it.
+	 * <p>
+	 * For two bone IK: 1) the child bone's local Y translation is set to 0, 2) stretch is not applied if {@link #getSoftness()} is
+	 * > 0, and 3) if the parent bone has local nonuniform scale, stretch is not applied. */
+	public boolean getStretch () {
+		return stretch;
+	}
+
+	public void setStretch (boolean stretch) {
+		this.stretch = stretch;
+	}
+
+	public boolean isActive () {
+		return active;
+	}
+
+	/** The IK constraint's setup pose data. */
+	public IkConstraintData getData () {
+		return data;
+	}
+
+	public String toString () {
+		return data.name;
+	}
+
+	/** Applies 1 bone IK. The target is specified in the world coordinate system. */
+	static public void apply (Bone bone, float targetX, float targetY, boolean compress, boolean stretch, boolean uniform,
+		float alpha) {
+		if (bone == null) throw new IllegalArgumentException("bone cannot be null.");
+		Bone p = bone.parent;
+		float pa = p.a, pb = p.b, pc = p.c, pd = p.d;
+		float rotationIK = -bone.ashearX - bone.arotation, tx, ty;
+		switch (bone.data.transformMode) {
+		case onlyTranslation:
+			tx = targetX - bone.worldX;
+			ty = targetY - bone.worldY;
+			break;
+		case noRotationOrReflection:
+			float s = Math.abs(pa * pd - pb * pc) / (pa * pa + pc * pc);
+			float sa = pa / bone.skeleton.scaleX;
+			float sc = pc / bone.skeleton.scaleY;
+			pb = -sc * s * bone.skeleton.scaleX;
+			pd = sa * s * bone.skeleton.scaleY;
+			rotationIK += atan2(sc, sa) * radDeg;
+			// Fall through.
+		default:
+			float x = targetX - p.worldX, y = targetY - p.worldY;
+			float d = pa * pd - pb * pc;
+			tx = (x * pd - y * pb) / d - bone.ax;
+			ty = (y * pa - x * pc) / d - bone.ay;
+		}
+		rotationIK += atan2(ty, tx) * radDeg;
+		if (bone.ascaleX < 0) rotationIK += 180;
+		if (rotationIK > 180)
+			rotationIK -= 360;
+		else if (rotationIK < -180) //
+			rotationIK += 360;
+		float sx = bone.ascaleX, sy = bone.ascaleY;
+		if (compress || stretch) {
+			switch (bone.data.transformMode) {
+			case noScale:
+			case noScaleOrReflection:
+				tx = targetX - bone.worldX;
+				ty = targetY - bone.worldY;
+			}
+			float b = bone.data.length * sx, dd = (float)Math.sqrt(tx * tx + ty * ty);
+			if ((compress && dd < b) || (stretch && dd > b) && b > 0.0001f) {
+				float s = (dd / b - 1) * alpha + 1;
+				sx *= s;
+				if (uniform) sy *= s;
+			}
+		}
+		bone.updateWorldTransform(bone.ax, bone.ay, bone.arotation + rotationIK * alpha, sx, sy, bone.ashearX, bone.ashearY);
+	}
+
+	/** Applies 2 bone IK. The target is specified in the world coordinate system.
+	 * @param child A direct descendant of the parent bone. */
+	static public void apply (Bone parent, Bone child, float targetX, float targetY, int bendDir, boolean stretch, boolean uniform,
+		float softness, float alpha) {
+		if (parent == null) throw new IllegalArgumentException("parent cannot be null.");
+		if (child == null) throw new IllegalArgumentException("child cannot be null.");
+		float px = parent.ax, py = parent.ay, psx = parent.ascaleX, psy = parent.ascaleY, sx = psx, sy = psy, csx = child.ascaleX;
+		int os1, os2, s2;
+		if (psx < 0) {
+			psx = -psx;
+			os1 = 180;
+			s2 = -1;
+		} else {
+			os1 = 0;
+			s2 = 1;
+		}
+		if (psy < 0) {
+			psy = -psy;
+			s2 = -s2;
+		}
+		if (csx < 0) {
+			csx = -csx;
+			os2 = 180;
+		} else
+			os2 = 0;
+		float cx = child.ax, cy, cwx, cwy, a = parent.a, b = parent.b, c = parent.c, d = parent.d;
+		boolean u = Math.abs(psx - psy) <= 0.0001f;
+		if (!u || stretch) {
+			cy = 0;
+			cwx = a * cx + parent.worldX;
+			cwy = c * cx + parent.worldY;
+		} else {
+			cy = child.ay;
+			cwx = a * cx + b * cy + parent.worldX;
+			cwy = c * cx + d * cy + parent.worldY;
+		}
+		Bone pp = parent.parent;
+		a = pp.a;
+		b = pp.b;
+		c = pp.c;
+		d = pp.d;
+		float id = 1 / (a * d - b * c), x = cwx - pp.worldX, y = cwy - pp.worldY;
+		float dx = (x * d - y * b) * id - px, dy = (y * a - x * c) * id - py;
+		float l1 = (float)Math.sqrt(dx * dx + dy * dy), l2 = child.data.length * csx, a1, a2;
+		if (l1 < 0.0001f) {
+			apply(parent, targetX, targetY, false, stretch, false, alpha);
+			child.updateWorldTransform(cx, cy, 0, child.ascaleX, child.ascaleY, child.ashearX, child.ashearY);
+			return;
+		}
+		x = targetX - pp.worldX;
+		y = targetY - pp.worldY;
+		float tx = (x * d - y * b) * id - px, ty = (y * a - x * c) * id - py;
+		float dd = tx * tx + ty * ty;
+		if (softness != 0) {
+			softness *= psx * (csx + 1) * 0.5f;
+			float td = (float)Math.sqrt(dd), sd = td - l1 - l2 * psx + softness;
+			if (sd > 0) {
+				float p = Math.min(1, sd / (softness * 2)) - 1;
+				p = (sd - softness * (1 - p * p)) / td;
+				tx -= p * tx;
+				ty -= p * ty;
+				dd = tx * tx + ty * ty;
+			}
+		}
+		outer:
+		if (u) {
+			l2 *= psx;
+			float cos = (dd - l1 * l1 - l2 * l2) / (2 * l1 * l2);
+			if (cos < -1) {
+				cos = -1;
+				a2 = PI * bendDir;
+			} else if (cos > 1) {
+				cos = 1;
+				a2 = 0;
+				if (stretch) {
+					a = ((float)Math.sqrt(dd) / (l1 + l2) - 1) * alpha + 1;
+					sx *= a;
+					if (uniform) sy *= a;
+				}
+			} else
+				a2 = (float)Math.acos(cos) * bendDir;
+			a = l1 + l2 * cos;
+			b = l2 * sin(a2);
+			a1 = atan2(ty * a - tx * b, tx * a + ty * b);
+		} else {
+			a = psx * l2;
+			b = psy * l2;
+			float aa = a * a, bb = b * b, ta = atan2(ty, tx);
+			c = bb * l1 * l1 + aa * dd - aa * bb;
+			float c1 = -2 * bb * l1, c2 = bb - aa;
+			d = c1 * c1 - 4 * c2 * c;
+			if (d >= 0) {
+				float q = (float)Math.sqrt(d);
+				if (c1 < 0) q = -q;
+				q = -(c1 + q) * 0.5f;
+				float r0 = q / c2, r1 = c / q;
+				float r = Math.abs(r0) < Math.abs(r1) ? r0 : r1;
+				if (r * r <= dd) {
+					y = (float)Math.sqrt(dd - r * r) * bendDir;
+					a1 = ta - atan2(y, r);
+					a2 = atan2(y / psy, (r - l1) / psx);
+					break outer;
+				}
+			}
+			float minAngle = PI, minX = l1 - a, minDist = minX * minX, minY = 0;
+			float maxAngle = 0, maxX = l1 + a, maxDist = maxX * maxX, maxY = 0;
+			c = -a * l1 / (aa - bb);
+			if (c >= -1 && c <= 1) {
+				c = (float)Math.acos(c);
+				x = a * cos(c) + l1;
+				y = b * sin(c);
+				d = x * x + y * y;
+				if (d < minDist) {
+					minAngle = c;
+					minDist = d;
+					minX = x;
+					minY = y;
+				}
+				if (d > maxDist) {
+					maxAngle = c;
+					maxDist = d;
+					maxX = x;
+					maxY = y;
+				}
+			}
+			if (dd <= (minDist + maxDist) * 0.5f) {
+				a1 = ta - atan2(minY * bendDir, minX);
+				a2 = minAngle * bendDir;
+			} else {
+				a1 = ta - atan2(maxY * bendDir, maxX);
+				a2 = maxAngle * bendDir;
+			}
+		}
+		float os = atan2(cy, cx) * s2;
+		float rotation = parent.arotation;
+		a1 = (a1 - os) * radDeg + os1 - rotation;
+		if (a1 > 180)
+			a1 -= 360;
+		else if (a1 < -180) //
+			a1 += 360;
+		parent.updateWorldTransform(px, py, rotation + a1 * alpha, sx, sy, 0, 0);
+		rotation = child.arotation;
+		a2 = ((a2 + os) * radDeg - child.ashearX) * s2 + os2 - rotation;
+		if (a2 > 180)
+			a2 -= 360;
+		else if (a2 < -180) //
+			a2 += 360;
+		child.updateWorldTransform(cx, cy, rotation + a2 * alpha, child.ascaleX, child.ascaleY, child.ashearX, child.ashearY);
+	}
+}

+ 122 - 122
spine-libgdx/spine-libgdx/src/com/esotericsoftware/spine/IkConstraintData.java

@@ -1,122 +1,122 @@
-/******************************************************************************
- * Spine Runtimes License Agreement
- * Last updated January 1, 2020. Replaces all prior versions.
- *
- * Copyright (c) 2013-2020, Esoteric Software LLC
- *
- * Integration of the Spine Runtimes into software or otherwise creating
- * derivative works of the Spine Runtimes is permitted under the terms and
- * conditions of Section 2 of the Spine Editor License Agreement:
- * http://esotericsoftware.com/spine-editor-license
- *
- * Otherwise, it is permitted to integrate the Spine Runtimes into software
- * or otherwise create derivative works of the Spine Runtimes (collectively,
- * "Products"), provided that each user of the Products must obtain their own
- * Spine Editor license and redistribution of the Products in any form must
- * include this license and copyright notice.
- *
- * THE SPINE RUNTIMES ARE PROVIDED BY ESOTERIC SOFTWARE LLC "AS IS" AND ANY
- * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
- * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
- * DISCLAIMED. IN NO EVENT SHALL ESOTERIC SOFTWARE LLC BE LIABLE FOR ANY
- * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
- * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES,
- * BUSINESS INTERRUPTION, OR LOSS OF USE, DATA, OR PROFITS) HOWEVER CAUSED AND
- * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
- * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
- * THE SPINE RUNTIMES, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
- *****************************************************************************/
-
-package com.esotericsoftware.spine;
-
-import com.badlogic.gdx.utils.Array;
-
-/** Stores the setup pose for an {@link IkConstraint}.
- * <p>
- * See <a href="http://esotericsoftware.com/spine-ik-constraints">IK constraints</a> in the Spine User Guide. */
-public class IkConstraintData extends ConstraintData {
-	final Array<BoneData> bones = new Array();
-	BoneData target;
-	int bendDirection = 1;
-	boolean compress, stretch, uniform;
-	float mix = 1, softness;
-
-	public IkConstraintData (String name) {
-		super(name);
-	}
-
-	/** The bones that are constrained by this IK constraint. */
-	public Array<BoneData> getBones () {
-		return bones;
-	}
-
-	/** The bone that is the IK target. */
-	public BoneData getTarget () {
-		return target;
-	}
-
-	public void setTarget (BoneData target) {
-		if (target == null) throw new IllegalArgumentException("target cannot be null.");
-		this.target = target;
-	}
-
-	/** A percentage (0-1) that controls the mix between the constrained and unconstrained rotation.
-	 * <p>
-	 * For two bone IK: if the parent bone has local nonuniform scale, the child bone's local Y translation is set to 0. */
-	public float getMix () {
-		return mix;
-	}
-
-	public void setMix (float mix) {
-		this.mix = mix;
-	}
-
-	/** For two bone IK, the target bone's distance from the maximum reach of the bones where rotation begins to slow. The bones
-	 * will not straighten completely until the target is this far out of range. */
-	public float getSoftness () {
-		return softness;
-	}
-
-	public void setSoftness (float softness) {
-		this.softness = softness;
-	}
-
-	/** For two bone IK, controls the bend direction of the IK bones, either 1 or -1. */
-	public int getBendDirection () {
-		return bendDirection;
-	}
-
-	public void setBendDirection (int bendDirection) {
-		this.bendDirection = bendDirection;
-	}
-
-	/** For one bone IK, when true and the target is too close, the bone is scaled to reach it. */
-	public boolean getCompress () {
-		return compress;
-	}
-
-	public void setCompress (boolean compress) {
-		this.compress = compress;
-	}
-
-	/** When true and the target is out of range, the parent bone is scaled to reach it.
-	 * <p>
-	 * For two bone IK: 1) the child bone's local Y translation is set to 0, 2) stretch is not applied if {@link #getSoftness()} is
-	 * > 0, and 3) if the parent bone has local nonuniform scale, stretch is not applied. */
-	public boolean getStretch () {
-		return stretch;
-	}
-
-	public void setStretch (boolean stretch) {
-		this.stretch = stretch;
-	}
-
-	/** When true and {@link #getCompress()} or {@link #getStretch()} is used, the bone is scaled on both the X and Y axes. */
-	public boolean getUniform () {
-		return uniform;
-	}
-
-	public void setUniform (boolean uniform) {
-		this.uniform = uniform;
-	}
-}
+/******************************************************************************
+ * Spine Runtimes License Agreement
+ * Last updated January 1, 2020. Replaces all prior versions.
+ *
+ * Copyright (c) 2013-2020, Esoteric Software LLC
+ *
+ * Integration of the Spine Runtimes into software or otherwise creating
+ * derivative works of the Spine Runtimes is permitted under the terms and
+ * conditions of Section 2 of the Spine Editor License Agreement:
+ * http://esotericsoftware.com/spine-editor-license
+ *
+ * Otherwise, it is permitted to integrate the Spine Runtimes into software
+ * or otherwise create derivative works of the Spine Runtimes (collectively,
+ * "Products"), provided that each user of the Products must obtain their own
+ * Spine Editor license and redistribution of the Products in any form must
+ * include this license and copyright notice.
+ *
+ * THE SPINE RUNTIMES ARE PROVIDED BY ESOTERIC SOFTWARE LLC "AS IS" AND ANY
+ * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL ESOTERIC SOFTWARE LLC BE LIABLE FOR ANY
+ * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES,
+ * BUSINESS INTERRUPTION, OR LOSS OF USE, DATA, OR PROFITS) HOWEVER CAUSED AND
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
+ * THE SPINE RUNTIMES, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ *****************************************************************************/
+
+package com.esotericsoftware.spine;
+
+import com.badlogic.gdx.utils.Array;
+
+/** Stores the setup pose for an {@link IkConstraint}.
+ * <p>
+ * See <a href="http://esotericsoftware.com/spine-ik-constraints">IK constraints</a> in the Spine User Guide. */
+public class IkConstraintData extends ConstraintData {
+	final Array<BoneData> bones = new Array();
+	BoneData target;
+	int bendDirection = 1;
+	boolean compress, stretch, uniform;
+	float mix = 1, softness;
+
+	public IkConstraintData (String name) {
+		super(name);
+	}
+
+	/** The bones that are constrained by this IK constraint. */
+	public Array<BoneData> getBones () {
+		return bones;
+	}
+
+	/** The bone that is the IK target. */
+	public BoneData getTarget () {
+		return target;
+	}
+
+	public void setTarget (BoneData target) {
+		if (target == null) throw new IllegalArgumentException("target cannot be null.");
+		this.target = target;
+	}
+
+	/** A percentage (0-1) that controls the mix between the constrained and unconstrained rotation.
+	 * <p>
+	 * For two bone IK: if the parent bone has local nonuniform scale, the child bone's local Y translation is set to 0. */
+	public float getMix () {
+		return mix;
+	}
+
+	public void setMix (float mix) {
+		this.mix = mix;
+	}
+
+	/** For two bone IK, the target bone's distance from the maximum reach of the bones where rotation begins to slow. The bones
+	 * will not straighten completely until the target is this far out of range. */
+	public float getSoftness () {
+		return softness;
+	}
+
+	public void setSoftness (float softness) {
+		this.softness = softness;
+	}
+
+	/** For two bone IK, controls the bend direction of the IK bones, either 1 or -1. */
+	public int getBendDirection () {
+		return bendDirection;
+	}
+
+	public void setBendDirection (int bendDirection) {
+		this.bendDirection = bendDirection;
+	}
+
+	/** For one bone IK, when true and the target is too close, the bone is scaled to reach it. */
+	public boolean getCompress () {
+		return compress;
+	}
+
+	public void setCompress (boolean compress) {
+		this.compress = compress;
+	}
+
+	/** When true and the target is out of range, the parent bone is scaled to reach it.
+	 * <p>
+	 * For two bone IK: 1) the child bone's local Y translation is set to 0, 2) stretch is not applied if {@link #getSoftness()} is
+	 * > 0, and 3) if the parent bone has local nonuniform scale, stretch is not applied. */
+	public boolean getStretch () {
+		return stretch;
+	}
+
+	public void setStretch (boolean stretch) {
+		this.stretch = stretch;
+	}
+
+	/** When true and {@link #getCompress()} or {@link #getStretch()} is used, the bone is scaled on both the X and Y axes. */
+	public boolean getUniform () {
+		return uniform;
+	}
+
+	public void setUniform (boolean uniform) {
+		this.uniform = uniform;
+	}
+}

+ 564 - 564
spine-libgdx/spine-libgdx/src/com/esotericsoftware/spine/PathConstraint.java

@@ -1,564 +1,564 @@
-/******************************************************************************
- * Spine Runtimes License Agreement
- * Last updated January 1, 2020. Replaces all prior versions.
- *
- * Copyright (c) 2013-2020, Esoteric Software LLC
- *
- * Integration of the Spine Runtimes into software or otherwise creating
- * derivative works of the Spine Runtimes is permitted under the terms and
- * conditions of Section 2 of the Spine Editor License Agreement:
- * http://esotericsoftware.com/spine-editor-license
- *
- * Otherwise, it is permitted to integrate the Spine Runtimes into software
- * or otherwise create derivative works of the Spine Runtimes (collectively,
- * "Products"), provided that each user of the Products must obtain their own
- * Spine Editor license and redistribution of the Products in any form must
- * include this license and copyright notice.
- *
- * THE SPINE RUNTIMES ARE PROVIDED BY ESOTERIC SOFTWARE LLC "AS IS" AND ANY
- * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
- * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
- * DISCLAIMED. IN NO EVENT SHALL ESOTERIC SOFTWARE LLC BE LIABLE FOR ANY
- * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
- * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES,
- * BUSINESS INTERRUPTION, OR LOSS OF USE, DATA, OR PROFITS) HOWEVER CAUSED AND
- * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
- * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
- * THE SPINE RUNTIMES, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
- *****************************************************************************/
-
-package com.esotericsoftware.spine;
-
-import java.util.Arrays;
-
-import com.badlogic.gdx.utils.Array;
-import com.badlogic.gdx.utils.FloatArray;
-
-import com.esotericsoftware.spine.PathConstraintData.PositionMode;
-import com.esotericsoftware.spine.PathConstraintData.RotateMode;
-import com.esotericsoftware.spine.PathConstraintData.SpacingMode;
-import com.esotericsoftware.spine.attachments.Attachment;
-import com.esotericsoftware.spine.attachments.PathAttachment;
-import com.esotericsoftware.spine.utils.SpineUtils;
-
-/** Stores the current pose for a path constraint. A path constraint adjusts the rotation, translation, and scale of the
- * constrained bones so they follow a {@link PathAttachment}.
- * <p>
- * See <a href="http://esotericsoftware.com/spine-path-constraints">Path constraints</a> in the Spine User Guide. */
-public class PathConstraint implements Updatable {
-	static private final int NONE = -1, BEFORE = -2, AFTER = -3;
-	static private final float epsilon = 0.00001f;
-
-	final PathConstraintData data;
-	final Array<Bone> bones;
-	Slot target;
-	float position, spacing, mixRotate, mixX, mixY;
-
-	boolean active;
-
-	private final FloatArray spaces = new FloatArray(), positions = new FloatArray();
-	private final FloatArray world = new FloatArray(), curves = new FloatArray(), lengths = new FloatArray();
-	private final float[] segments = new float[10];
-
-	public PathConstraint (PathConstraintData data, Skeleton skeleton) {
-		if (data == null) throw new IllegalArgumentException("data cannot be null.");
-		if (skeleton == null) throw new IllegalArgumentException("skeleton cannot be null.");
-		this.data = data;
-		bones = new Array(data.bones.size);
-		for (BoneData boneData : data.bones)
-			bones.add(skeleton.findBone(boneData.name));
-		target = skeleton.findSlot(data.target.name);
-		position = data.position;
-		spacing = data.spacing;
-		mixRotate = data.mixRotate;
-		mixX = data.mixX;
-		mixY = data.mixY;
-	}
-
-	/** Copy constructor. */
-	public PathConstraint (PathConstraint constraint, Skeleton skeleton) {
-		if (constraint == null) throw new IllegalArgumentException("constraint cannot be null.");
-		if (skeleton == null) throw new IllegalArgumentException("skeleton cannot be null.");
-		data = constraint.data;
-		bones = new Array(constraint.bones.size);
-		for (Bone bone : constraint.bones)
-			bones.add(skeleton.bones.get(bone.data.index));
-		target = skeleton.slots.get(constraint.target.data.index);
-		position = constraint.position;
-		spacing = constraint.spacing;
-		mixRotate = constraint.mixRotate;
-		mixX = constraint.mixX;
-		mixY = constraint.mixY;
-	}
-
-	/** Applies the constraint to the constrained bones. */
-	public void update () {
-		Attachment attachment = target.attachment;
-		if (!(attachment instanceof PathAttachment)) return;
-
-		float mixRotate = this.mixRotate, mixX = this.mixX, mixY = this.mixY;
-		if (mixRotate == 0 && mixX == 0 && mixY == 0) return;
-
-		PathConstraintData data = this.data;
-		boolean tangents = data.rotateMode == RotateMode.tangent, scale = data.rotateMode == RotateMode.chainScale;
-		int boneCount = this.bones.size, spacesCount = tangents ? boneCount : boneCount + 1;
-		Object[] bones = this.bones.items;
-		float[] spaces = this.spaces.setSize(spacesCount), lengths = scale ? this.lengths.setSize(boneCount) : null;
-		float spacing = this.spacing;
-
-		switch (data.spacingMode) {
-		case percent:
-			if (scale) {
-				for (int i = 0, n = spacesCount - 1; i < n; i++) {
-					Bone bone = (Bone)bones[i];
-					float setupLength = bone.data.length;
-					if (setupLength < epsilon)
-						lengths[i] = 0;
-					else {
-						float x = setupLength * bone.a, y = setupLength * bone.c;
-						lengths[i] = (float)Math.sqrt(x * x + y * y);
-					}
-				}
-			}
-			Arrays.fill(spaces, 1, spacesCount, spacing);
-			break;
-		case proportional:
-			float sum = 0;
-			for (int i = 0; i < boneCount;) {
-				Bone bone = (Bone)bones[i];
-				float setupLength = bone.data.length;
-				if (setupLength < epsilon) {
-					if (scale) lengths[i] = 0;
-					spaces[++i] = spacing;
-				} else {
-					float x = setupLength * bone.a, y = setupLength * bone.c;
-					float length = (float)Math.sqrt(x * x + y * y);
-					if (scale) lengths[i] = length;
-					spaces[++i] = length;
-					sum += length;
-				}
-			}
-			if (sum > 0) {
-				sum = spacesCount / sum * spacing;
-				for (int i = 1; i < spacesCount; i++)
-					spaces[i] *= sum;
-			}
-			break;
-		default:
-			boolean lengthSpacing = data.spacingMode == SpacingMode.length;
-			for (int i = 0, n = spacesCount - 1; i < n;) {
-				Bone bone = (Bone)bones[i];
-				float setupLength = bone.data.length;
-				if (setupLength < epsilon) {
-					if (scale) lengths[i] = 0;
-					spaces[++i] = spacing;
-				} else {
-					float x = setupLength * bone.a, y = setupLength * bone.c;
-					float length = (float)Math.sqrt(x * x + y * y);
-					if (scale) lengths[i] = length;
-					spaces[++i] = (lengthSpacing ? setupLength + spacing : spacing) * length / setupLength;
-				}
-			}
-		}
-
-		float[] positions = computeWorldPositions((PathAttachment)attachment, spacesCount, tangents);
-		float boneX = positions[0], boneY = positions[1], offsetRotation = data.offsetRotation;
-		boolean tip;
-		if (offsetRotation == 0)
-			tip = data.rotateMode == RotateMode.chain;
-		else {
-			tip = false;
-			Bone p = target.bone;
-			offsetRotation *= p.a * p.d - p.b * p.c > 0 ? SpineUtils.degRad : -SpineUtils.degRad;
-		}
-		for (int i = 0, p = 3; i < boneCount; i++, p += 3) {
-			Bone bone = (Bone)bones[i];
-			bone.worldX += (boneX - bone.worldX) * mixX;
-			bone.worldY += (boneY - bone.worldY) * mixY;
-			float x = positions[p], y = positions[p + 1], dx = x - boneX, dy = y - boneY;
-			if (scale) {
-				float length = lengths[i];
-				if (length >= epsilon) {
-					float s = ((float)Math.sqrt(dx * dx + dy * dy) / length - 1) * mixRotate + 1;
-					bone.a *= s;
-					bone.c *= s;
-				}
-			}
-			boneX = x;
-			boneY = y;
-			if (mixRotate > 0) {
-				float a = bone.a, b = bone.b, c = bone.c, d = bone.d, r, cos, sin;
-				if (tangents)
-					r = positions[p - 1];
-				else if (spaces[i + 1] < epsilon)
-					r = positions[p + 2];
-				else
-					r = (float)Math.atan2(dy, dx);
-				r -= (float)Math.atan2(c, a);
-				if (tip) {
-					cos = (float)Math.cos(r);
-					sin = (float)Math.sin(r);
-					float length = bone.data.length;
-					boneX += (length * (cos * a - sin * c) - dx) * mixRotate;
-					boneY += (length * (sin * a + cos * c) - dy) * mixRotate;
-				} else
-					r += offsetRotation;
-				if (r > SpineUtils.PI)
-					r -= SpineUtils.PI2;
-				else if (r < -SpineUtils.PI) //
-					r += SpineUtils.PI2;
-				r *= mixRotate;
-				cos = (float)Math.cos(r);
-				sin = (float)Math.sin(r);
-				bone.a = cos * a - sin * c;
-				bone.b = cos * b - sin * d;
-				bone.c = sin * a + cos * c;
-				bone.d = sin * b + cos * d;
-			}
-			bone.updateAppliedTransform();
-		}
-	}
-
-	float[] computeWorldPositions (PathAttachment path, int spacesCount, boolean tangents) {
-		Slot target = this.target;
-		float position = this.position;
-		float[] spaces = this.spaces.items, out = this.positions.setSize(spacesCount * 3 + 2), world;
-		boolean closed = path.getClosed();
-		int verticesLength = path.getWorldVerticesLength(), curveCount = verticesLength / 6, prevCurve = NONE;
-
-		if (!path.getConstantSpeed()) {
-			float[] lengths = path.getLengths();
-			curveCount -= closed ? 1 : 2;
-			float pathLength = lengths[curveCount];
-
-			if (data.positionMode == PositionMode.percent) position *= pathLength;
-
-			float multiplier;
-			switch (data.spacingMode) {
-			case percent:
-				multiplier = pathLength;
-				break;
-			case proportional:
-				multiplier = pathLength / spacesCount;
-				break;
-			default:
-				multiplier = 1;
-			}
-
-			world = this.world.setSize(8);
-			for (int i = 0, o = 0, curve = 0; i < spacesCount; i++, o += 3) {
-				float space = spaces[i] * multiplier;
-				position += space;
-				float p = position;
-
-				if (closed) {
-					p %= pathLength;
-					if (p < 0) p += pathLength;
-					curve = 0;
-				} else if (p < 0) {
-					if (prevCurve != BEFORE) {
-						prevCurve = BEFORE;
-						path.computeWorldVertices(target, 2, 4, world, 0, 2);
-					}
-					addBeforePosition(p, world, 0, out, o);
-					continue;
-				} else if (p > pathLength) {
-					if (prevCurve != AFTER) {
-						prevCurve = AFTER;
-						path.computeWorldVertices(target, verticesLength - 6, 4, world, 0, 2);
-					}
-					addAfterPosition(p - pathLength, world, 0, out, o);
-					continue;
-				}
-
-				// Determine curve containing position.
-				for (;; curve++) {
-					float length = lengths[curve];
-					if (p > length) continue;
-					if (curve == 0)
-						p /= length;
-					else {
-						float prev = lengths[curve - 1];
-						p = (p - prev) / (length - prev);
-					}
-					break;
-				}
-				if (curve != prevCurve) {
-					prevCurve = curve;
-					if (closed && curve == curveCount) {
-						path.computeWorldVertices(target, verticesLength - 4, 4, world, 0, 2);
-						path.computeWorldVertices(target, 0, 4, world, 4, 2);
-					} else
-						path.computeWorldVertices(target, curve * 6 + 2, 8, world, 0, 2);
-				}
-				addCurvePosition(p, world[0], world[1], world[2], world[3], world[4], world[5], world[6], world[7], out, o,
-					tangents || (i > 0 && space < epsilon));
-			}
-			return out;
-		}
-
-		// World vertices.
-		if (closed) {
-			verticesLength += 2;
-			world = this.world.setSize(verticesLength);
-			path.computeWorldVertices(target, 2, verticesLength - 4, world, 0, 2);
-			path.computeWorldVertices(target, 0, 2, world, verticesLength - 4, 2);
-			world[verticesLength - 2] = world[0];
-			world[verticesLength - 1] = world[1];
-		} else {
-			curveCount--;
-			verticesLength -= 4;
-			world = this.world.setSize(verticesLength);
-			path.computeWorldVertices(target, 2, verticesLength, world, 0, 2);
-		}
-
-		// Curve lengths.
-		float[] curves = this.curves.setSize(curveCount);
-		float pathLength = 0;
-		float x1 = world[0], y1 = world[1], cx1 = 0, cy1 = 0, cx2 = 0, cy2 = 0, x2 = 0, y2 = 0;
-		float tmpx, tmpy, dddfx, dddfy, ddfx, ddfy, dfx, dfy;
-		for (int i = 0, w = 2; i < curveCount; i++, w += 6) {
-			cx1 = world[w];
-			cy1 = world[w + 1];
-			cx2 = world[w + 2];
-			cy2 = world[w + 3];
-			x2 = world[w + 4];
-			y2 = world[w + 5];
-			tmpx = (x1 - cx1 * 2 + cx2) * 0.1875f;
-			tmpy = (y1 - cy1 * 2 + cy2) * 0.1875f;
-			dddfx = ((cx1 - cx2) * 3 - x1 + x2) * 0.09375f;
-			dddfy = ((cy1 - cy2) * 3 - y1 + y2) * 0.09375f;
-			ddfx = tmpx * 2 + dddfx;
-			ddfy = tmpy * 2 + dddfy;
-			dfx = (cx1 - x1) * 0.75f + tmpx + dddfx * 0.16666667f;
-			dfy = (cy1 - y1) * 0.75f + tmpy + dddfy * 0.16666667f;
-			pathLength += (float)Math.sqrt(dfx * dfx + dfy * dfy);
-			dfx += ddfx;
-			dfy += ddfy;
-			ddfx += dddfx;
-			ddfy += dddfy;
-			pathLength += (float)Math.sqrt(dfx * dfx + dfy * dfy);
-			dfx += ddfx;
-			dfy += ddfy;
-			pathLength += (float)Math.sqrt(dfx * dfx + dfy * dfy);
-			dfx += ddfx + dddfx;
-			dfy += ddfy + dddfy;
-			pathLength += (float)Math.sqrt(dfx * dfx + dfy * dfy);
-			curves[i] = pathLength;
-			x1 = x2;
-			y1 = y2;
-		}
-
-		if (data.positionMode == PositionMode.percent) position *= pathLength;
-
-		float multiplier;
-		switch (data.spacingMode) {
-		case percent:
-			multiplier = pathLength;
-			break;
-		case proportional:
-			multiplier = pathLength / spacesCount;
-			break;
-		default:
-			multiplier = 1;
-		}
-
-		float[] segments = this.segments;
-		float curveLength = 0;
-		for (int i = 0, o = 0, curve = 0, segment = 0; i < spacesCount; i++, o += 3) {
-			float space = spaces[i] * multiplier;
-			position += space;
-			float p = position;
-
-			if (closed) {
-				p %= pathLength;
-				if (p < 0) p += pathLength;
-				curve = 0;
-			} else if (p < 0) {
-				addBeforePosition(p, world, 0, out, o);
-				continue;
-			} else if (p > pathLength) {
-				addAfterPosition(p - pathLength, world, verticesLength - 4, out, o);
-				continue;
-			}
-
-			// Determine curve containing position.
-			for (;; curve++) {
-				float length = curves[curve];
-				if (p > length) continue;
-				if (curve == 0)
-					p /= length;
-				else {
-					float prev = curves[curve - 1];
-					p = (p - prev) / (length - prev);
-				}
-				break;
-			}
-
-			// Curve segment lengths.
-			if (curve != prevCurve) {
-				prevCurve = curve;
-				int ii = curve * 6;
-				x1 = world[ii];
-				y1 = world[ii + 1];
-				cx1 = world[ii + 2];
-				cy1 = world[ii + 3];
-				cx2 = world[ii + 4];
-				cy2 = world[ii + 5];
-				x2 = world[ii + 6];
-				y2 = world[ii + 7];
-				tmpx = (x1 - cx1 * 2 + cx2) * 0.03f;
-				tmpy = (y1 - cy1 * 2 + cy2) * 0.03f;
-				dddfx = ((cx1 - cx2) * 3 - x1 + x2) * 0.006f;
-				dddfy = ((cy1 - cy2) * 3 - y1 + y2) * 0.006f;
-				ddfx = tmpx * 2 + dddfx;
-				ddfy = tmpy * 2 + dddfy;
-				dfx = (cx1 - x1) * 0.3f + tmpx + dddfx * 0.16666667f;
-				dfy = (cy1 - y1) * 0.3f + tmpy + dddfy * 0.16666667f;
-				curveLength = (float)Math.sqrt(dfx * dfx + dfy * dfy);
-				segments[0] = curveLength;
-				for (ii = 1; ii < 8; ii++) {
-					dfx += ddfx;
-					dfy += ddfy;
-					ddfx += dddfx;
-					ddfy += dddfy;
-					curveLength += (float)Math.sqrt(dfx * dfx + dfy * dfy);
-					segments[ii] = curveLength;
-				}
-				dfx += ddfx;
-				dfy += ddfy;
-				curveLength += (float)Math.sqrt(dfx * dfx + dfy * dfy);
-				segments[8] = curveLength;
-				dfx += ddfx + dddfx;
-				dfy += ddfy + dddfy;
-				curveLength += (float)Math.sqrt(dfx * dfx + dfy * dfy);
-				segments[9] = curveLength;
-				segment = 0;
-			}
-
-			// Weight by segment length.
-			p *= curveLength;
-			for (;; segment++) {
-				float length = segments[segment];
-				if (p > length) continue;
-				if (segment == 0)
-					p /= length;
-				else {
-					float prev = segments[segment - 1];
-					p = segment + (p - prev) / (length - prev);
-				}
-				break;
-			}
-			addCurvePosition(p * 0.1f, x1, y1, cx1, cy1, cx2, cy2, x2, y2, out, o, tangents || (i > 0 && space < epsilon));
-		}
-		return out;
-	}
-
-	private void addBeforePosition (float p, float[] temp, int i, float[] out, int o) {
-		float x1 = temp[i], y1 = temp[i + 1], dx = temp[i + 2] - x1, dy = temp[i + 3] - y1, r = (float)Math.atan2(dy, dx);
-		out[o] = x1 + p * (float)Math.cos(r);
-		out[o + 1] = y1 + p * (float)Math.sin(r);
-		out[o + 2] = r;
-	}
-
-	private void addAfterPosition (float p, float[] temp, int i, float[] out, int o) {
-		float x1 = temp[i + 2], y1 = temp[i + 3], dx = x1 - temp[i], dy = y1 - temp[i + 1], r = (float)Math.atan2(dy, dx);
-		out[o] = x1 + p * (float)Math.cos(r);
-		out[o + 1] = y1 + p * (float)Math.sin(r);
-		out[o + 2] = r;
-	}
-
-	private void addCurvePosition (float p, float x1, float y1, float cx1, float cy1, float cx2, float cy2, float x2, float y2,
-		float[] out, int o, boolean tangents) {
-		if (p < epsilon || Float.isNaN(p)) {
-			out[o] = x1;
-			out[o + 1] = y1;
-			out[o + 2] = (float)Math.atan2(cy1 - y1, cx1 - x1);
-			return;
-		}
-		float tt = p * p, ttt = tt * p, u = 1 - p, uu = u * u, uuu = uu * u;
-		float ut = u * p, ut3 = ut * 3, uut3 = u * ut3, utt3 = ut3 * p;
-		float x = x1 * uuu + cx1 * uut3 + cx2 * utt3 + x2 * ttt, y = y1 * uuu + cy1 * uut3 + cy2 * utt3 + y2 * ttt;
-		out[o] = x;
-		out[o + 1] = y;
-		if (tangents) {
-			if (p < 0.001f)
-				out[o + 2] = (float)Math.atan2(cy1 - y1, cx1 - x1);
-			else
-				out[o + 2] = (float)Math.atan2(y - (y1 * uu + cy1 * ut * 2 + cy2 * tt), x - (x1 * uu + cx1 * ut * 2 + cx2 * tt));
-		}
-	}
-
-	/** The position along the path. */
-	public float getPosition () {
-		return position;
-	}
-
-	public void setPosition (float position) {
-		this.position = position;
-	}
-
-	/** The spacing between bones. */
-	public float getSpacing () {
-		return spacing;
-	}
-
-	public void setSpacing (float spacing) {
-		this.spacing = spacing;
-	}
-
-	/** A percentage (0-1) that controls the mix between the constrained and unconstrained rotation. */
-	public float getMixRotate () {
-		return mixRotate;
-	}
-
-	public void setMixRotate (float mixRotate) {
-		this.mixRotate = mixRotate;
-	}
-
-	/** A percentage (0-1) that controls the mix between the constrained and unconstrained translation X. */
-	public float getMixX () {
-		return mixX;
-	}
-
-	public void setMixX (float mixX) {
-		this.mixX = mixX;
-	}
-
-	/** A percentage (0-1) that controls the mix between the constrained and unconstrained translation Y. */
-	public float getMixY () {
-		return mixY;
-	}
-
-	public void setMixY (float mixY) {
-		this.mixY = mixY;
-	}
-
-	/** The bones that will be modified by this path constraint. */
-	public Array<Bone> getBones () {
-		return bones;
-	}
-
-	/** The slot whose path attachment will be used to constrained the bones. */
-	public Slot getTarget () {
-		return target;
-	}
-
-	public void setTarget (Slot target) {
-		if (target == null) throw new IllegalArgumentException("target cannot be null.");
-		this.target = target;
-	}
-
-	public boolean isActive () {
-		return active;
-	}
-
-	/** The path constraint's setup pose data. */
-	public PathConstraintData getData () {
-		return data;
-	}
-
-	public String toString () {
-		return data.name;
-	}
-}
+/******************************************************************************
+ * Spine Runtimes License Agreement
+ * Last updated January 1, 2020. Replaces all prior versions.
+ *
+ * Copyright (c) 2013-2020, Esoteric Software LLC
+ *
+ * Integration of the Spine Runtimes into software or otherwise creating
+ * derivative works of the Spine Runtimes is permitted under the terms and
+ * conditions of Section 2 of the Spine Editor License Agreement:
+ * http://esotericsoftware.com/spine-editor-license
+ *
+ * Otherwise, it is permitted to integrate the Spine Runtimes into software
+ * or otherwise create derivative works of the Spine Runtimes (collectively,
+ * "Products"), provided that each user of the Products must obtain their own
+ * Spine Editor license and redistribution of the Products in any form must
+ * include this license and copyright notice.
+ *
+ * THE SPINE RUNTIMES ARE PROVIDED BY ESOTERIC SOFTWARE LLC "AS IS" AND ANY
+ * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL ESOTERIC SOFTWARE LLC BE LIABLE FOR ANY
+ * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES,
+ * BUSINESS INTERRUPTION, OR LOSS OF USE, DATA, OR PROFITS) HOWEVER CAUSED AND
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
+ * THE SPINE RUNTIMES, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ *****************************************************************************/
+
+package com.esotericsoftware.spine;
+
+import java.util.Arrays;
+
+import com.badlogic.gdx.utils.Array;
+import com.badlogic.gdx.utils.FloatArray;
+
+import com.esotericsoftware.spine.PathConstraintData.PositionMode;
+import com.esotericsoftware.spine.PathConstraintData.RotateMode;
+import com.esotericsoftware.spine.PathConstraintData.SpacingMode;
+import com.esotericsoftware.spine.attachments.Attachment;
+import com.esotericsoftware.spine.attachments.PathAttachment;
+import com.esotericsoftware.spine.utils.SpineUtils;
+
+/** Stores the current pose for a path constraint. A path constraint adjusts the rotation, translation, and scale of the
+ * constrained bones so they follow a {@link PathAttachment}.
+ * <p>
+ * See <a href="http://esotericsoftware.com/spine-path-constraints">Path constraints</a> in the Spine User Guide. */
+public class PathConstraint implements Updatable {
+	static private final int NONE = -1, BEFORE = -2, AFTER = -3;
+	static private final float epsilon = 0.00001f;
+
+	final PathConstraintData data;
+	final Array<Bone> bones;
+	Slot target;
+	float position, spacing, mixRotate, mixX, mixY;
+
+	boolean active;
+
+	private final FloatArray spaces = new FloatArray(), positions = new FloatArray();
+	private final FloatArray world = new FloatArray(), curves = new FloatArray(), lengths = new FloatArray();
+	private final float[] segments = new float[10];
+
+	public PathConstraint (PathConstraintData data, Skeleton skeleton) {
+		if (data == null) throw new IllegalArgumentException("data cannot be null.");
+		if (skeleton == null) throw new IllegalArgumentException("skeleton cannot be null.");
+		this.data = data;
+		bones = new Array(data.bones.size);
+		for (BoneData boneData : data.bones)
+			bones.add(skeleton.findBone(boneData.name));
+		target = skeleton.findSlot(data.target.name);
+		position = data.position;
+		spacing = data.spacing;
+		mixRotate = data.mixRotate;
+		mixX = data.mixX;
+		mixY = data.mixY;
+	}
+
+	/** Copy constructor. */
+	public PathConstraint (PathConstraint constraint, Skeleton skeleton) {
+		if (constraint == null) throw new IllegalArgumentException("constraint cannot be null.");
+		if (skeleton == null) throw new IllegalArgumentException("skeleton cannot be null.");
+		data = constraint.data;
+		bones = new Array(constraint.bones.size);
+		for (Bone bone : constraint.bones)
+			bones.add(skeleton.bones.get(bone.data.index));
+		target = skeleton.slots.get(constraint.target.data.index);
+		position = constraint.position;
+		spacing = constraint.spacing;
+		mixRotate = constraint.mixRotate;
+		mixX = constraint.mixX;
+		mixY = constraint.mixY;
+	}
+
+	/** Applies the constraint to the constrained bones. */
+	public void update () {
+		Attachment attachment = target.attachment;
+		if (!(attachment instanceof PathAttachment)) return;
+
+		float mixRotate = this.mixRotate, mixX = this.mixX, mixY = this.mixY;
+		if (mixRotate == 0 && mixX == 0 && mixY == 0) return;
+
+		PathConstraintData data = this.data;
+		boolean tangents = data.rotateMode == RotateMode.tangent, scale = data.rotateMode == RotateMode.chainScale;
+		int boneCount = this.bones.size, spacesCount = tangents ? boneCount : boneCount + 1;
+		Object[] bones = this.bones.items;
+		float[] spaces = this.spaces.setSize(spacesCount), lengths = scale ? this.lengths.setSize(boneCount) : null;
+		float spacing = this.spacing;
+
+		switch (data.spacingMode) {
+		case percent:
+			if (scale) {
+				for (int i = 0, n = spacesCount - 1; i < n; i++) {
+					Bone bone = (Bone)bones[i];
+					float setupLength = bone.data.length;
+					if (setupLength < epsilon)
+						lengths[i] = 0;
+					else {
+						float x = setupLength * bone.a, y = setupLength * bone.c;
+						lengths[i] = (float)Math.sqrt(x * x + y * y);
+					}
+				}
+			}
+			Arrays.fill(spaces, 1, spacesCount, spacing);
+			break;
+		case proportional:
+			float sum = 0;
+			for (int i = 0; i < boneCount;) {
+				Bone bone = (Bone)bones[i];
+				float setupLength = bone.data.length;
+				if (setupLength < epsilon) {
+					if (scale) lengths[i] = 0;
+					spaces[++i] = spacing;
+				} else {
+					float x = setupLength * bone.a, y = setupLength * bone.c;
+					float length = (float)Math.sqrt(x * x + y * y);
+					if (scale) lengths[i] = length;
+					spaces[++i] = length;
+					sum += length;
+				}
+			}
+			if (sum > 0) {
+				sum = spacesCount / sum * spacing;
+				for (int i = 1; i < spacesCount; i++)
+					spaces[i] *= sum;
+			}
+			break;
+		default:
+			boolean lengthSpacing = data.spacingMode == SpacingMode.length;
+			for (int i = 0, n = spacesCount - 1; i < n;) {
+				Bone bone = (Bone)bones[i];
+				float setupLength = bone.data.length;
+				if (setupLength < epsilon) {
+					if (scale) lengths[i] = 0;
+					spaces[++i] = spacing;
+				} else {
+					float x = setupLength * bone.a, y = setupLength * bone.c;
+					float length = (float)Math.sqrt(x * x + y * y);
+					if (scale) lengths[i] = length;
+					spaces[++i] = (lengthSpacing ? setupLength + spacing : spacing) * length / setupLength;
+				}
+			}
+		}
+
+		float[] positions = computeWorldPositions((PathAttachment)attachment, spacesCount, tangents);
+		float boneX = positions[0], boneY = positions[1], offsetRotation = data.offsetRotation;
+		boolean tip;
+		if (offsetRotation == 0)
+			tip = data.rotateMode == RotateMode.chain;
+		else {
+			tip = false;
+			Bone p = target.bone;
+			offsetRotation *= p.a * p.d - p.b * p.c > 0 ? SpineUtils.degRad : -SpineUtils.degRad;
+		}
+		for (int i = 0, p = 3; i < boneCount; i++, p += 3) {
+			Bone bone = (Bone)bones[i];
+			bone.worldX += (boneX - bone.worldX) * mixX;
+			bone.worldY += (boneY - bone.worldY) * mixY;
+			float x = positions[p], y = positions[p + 1], dx = x - boneX, dy = y - boneY;
+			if (scale) {
+				float length = lengths[i];
+				if (length >= epsilon) {
+					float s = ((float)Math.sqrt(dx * dx + dy * dy) / length - 1) * mixRotate + 1;
+					bone.a *= s;
+					bone.c *= s;
+				}
+			}
+			boneX = x;
+			boneY = y;
+			if (mixRotate > 0) {
+				float a = bone.a, b = bone.b, c = bone.c, d = bone.d, r, cos, sin;
+				if (tangents)
+					r = positions[p - 1];
+				else if (spaces[i + 1] < epsilon)
+					r = positions[p + 2];
+				else
+					r = (float)Math.atan2(dy, dx);
+				r -= (float)Math.atan2(c, a);
+				if (tip) {
+					cos = (float)Math.cos(r);
+					sin = (float)Math.sin(r);
+					float length = bone.data.length;
+					boneX += (length * (cos * a - sin * c) - dx) * mixRotate;
+					boneY += (length * (sin * a + cos * c) - dy) * mixRotate;
+				} else
+					r += offsetRotation;
+				if (r > SpineUtils.PI)
+					r -= SpineUtils.PI2;
+				else if (r < -SpineUtils.PI) //
+					r += SpineUtils.PI2;
+				r *= mixRotate;
+				cos = (float)Math.cos(r);
+				sin = (float)Math.sin(r);
+				bone.a = cos * a - sin * c;
+				bone.b = cos * b - sin * d;
+				bone.c = sin * a + cos * c;
+				bone.d = sin * b + cos * d;
+			}
+			bone.updateAppliedTransform();
+		}
+	}
+
+	float[] computeWorldPositions (PathAttachment path, int spacesCount, boolean tangents) {
+		Slot target = this.target;
+		float position = this.position;
+		float[] spaces = this.spaces.items, out = this.positions.setSize(spacesCount * 3 + 2), world;
+		boolean closed = path.getClosed();
+		int verticesLength = path.getWorldVerticesLength(), curveCount = verticesLength / 6, prevCurve = NONE;
+
+		if (!path.getConstantSpeed()) {
+			float[] lengths = path.getLengths();
+			curveCount -= closed ? 1 : 2;
+			float pathLength = lengths[curveCount];
+
+			if (data.positionMode == PositionMode.percent) position *= pathLength;
+
+			float multiplier;
+			switch (data.spacingMode) {
+			case percent:
+				multiplier = pathLength;
+				break;
+			case proportional:
+				multiplier = pathLength / spacesCount;
+				break;
+			default:
+				multiplier = 1;
+			}
+
+			world = this.world.setSize(8);
+			for (int i = 0, o = 0, curve = 0; i < spacesCount; i++, o += 3) {
+				float space = spaces[i] * multiplier;
+				position += space;
+				float p = position;
+
+				if (closed) {
+					p %= pathLength;
+					if (p < 0) p += pathLength;
+					curve = 0;
+				} else if (p < 0) {
+					if (prevCurve != BEFORE) {
+						prevCurve = BEFORE;
+						path.computeWorldVertices(target, 2, 4, world, 0, 2);
+					}
+					addBeforePosition(p, world, 0, out, o);
+					continue;
+				} else if (p > pathLength) {
+					if (prevCurve != AFTER) {
+						prevCurve = AFTER;
+						path.computeWorldVertices(target, verticesLength - 6, 4, world, 0, 2);
+					}
+					addAfterPosition(p - pathLength, world, 0, out, o);
+					continue;
+				}
+
+				// Determine curve containing position.
+				for (;; curve++) {
+					float length = lengths[curve];
+					if (p > length) continue;
+					if (curve == 0)
+						p /= length;
+					else {
+						float prev = lengths[curve - 1];
+						p = (p - prev) / (length - prev);
+					}
+					break;
+				}
+				if (curve != prevCurve) {
+					prevCurve = curve;
+					if (closed && curve == curveCount) {
+						path.computeWorldVertices(target, verticesLength - 4, 4, world, 0, 2);
+						path.computeWorldVertices(target, 0, 4, world, 4, 2);
+					} else
+						path.computeWorldVertices(target, curve * 6 + 2, 8, world, 0, 2);
+				}
+				addCurvePosition(p, world[0], world[1], world[2], world[3], world[4], world[5], world[6], world[7], out, o,
+					tangents || (i > 0 && space < epsilon));
+			}
+			return out;
+		}
+
+		// World vertices.
+		if (closed) {
+			verticesLength += 2;
+			world = this.world.setSize(verticesLength);
+			path.computeWorldVertices(target, 2, verticesLength - 4, world, 0, 2);
+			path.computeWorldVertices(target, 0, 2, world, verticesLength - 4, 2);
+			world[verticesLength - 2] = world[0];
+			world[verticesLength - 1] = world[1];
+		} else {
+			curveCount--;
+			verticesLength -= 4;
+			world = this.world.setSize(verticesLength);
+			path.computeWorldVertices(target, 2, verticesLength, world, 0, 2);
+		}
+
+		// Curve lengths.
+		float[] curves = this.curves.setSize(curveCount);
+		float pathLength = 0;
+		float x1 = world[0], y1 = world[1], cx1 = 0, cy1 = 0, cx2 = 0, cy2 = 0, x2 = 0, y2 = 0;
+		float tmpx, tmpy, dddfx, dddfy, ddfx, ddfy, dfx, dfy;
+		for (int i = 0, w = 2; i < curveCount; i++, w += 6) {
+			cx1 = world[w];
+			cy1 = world[w + 1];
+			cx2 = world[w + 2];
+			cy2 = world[w + 3];
+			x2 = world[w + 4];
+			y2 = world[w + 5];
+			tmpx = (x1 - cx1 * 2 + cx2) * 0.1875f;
+			tmpy = (y1 - cy1 * 2 + cy2) * 0.1875f;
+			dddfx = ((cx1 - cx2) * 3 - x1 + x2) * 0.09375f;
+			dddfy = ((cy1 - cy2) * 3 - y1 + y2) * 0.09375f;
+			ddfx = tmpx * 2 + dddfx;
+			ddfy = tmpy * 2 + dddfy;
+			dfx = (cx1 - x1) * 0.75f + tmpx + dddfx * 0.16666667f;
+			dfy = (cy1 - y1) * 0.75f + tmpy + dddfy * 0.16666667f;
+			pathLength += (float)Math.sqrt(dfx * dfx + dfy * dfy);
+			dfx += ddfx;
+			dfy += ddfy;
+			ddfx += dddfx;
+			ddfy += dddfy;
+			pathLength += (float)Math.sqrt(dfx * dfx + dfy * dfy);
+			dfx += ddfx;
+			dfy += ddfy;
+			pathLength += (float)Math.sqrt(dfx * dfx + dfy * dfy);
+			dfx += ddfx + dddfx;
+			dfy += ddfy + dddfy;
+			pathLength += (float)Math.sqrt(dfx * dfx + dfy * dfy);
+			curves[i] = pathLength;
+			x1 = x2;
+			y1 = y2;
+		}
+
+		if (data.positionMode == PositionMode.percent) position *= pathLength;
+
+		float multiplier;
+		switch (data.spacingMode) {
+		case percent:
+			multiplier = pathLength;
+			break;
+		case proportional:
+			multiplier = pathLength / spacesCount;
+			break;
+		default:
+			multiplier = 1;
+		}
+
+		float[] segments = this.segments;
+		float curveLength = 0;
+		for (int i = 0, o = 0, curve = 0, segment = 0; i < spacesCount; i++, o += 3) {
+			float space = spaces[i] * multiplier;
+			position += space;
+			float p = position;
+
+			if (closed) {
+				p %= pathLength;
+				if (p < 0) p += pathLength;
+				curve = 0;
+			} else if (p < 0) {
+				addBeforePosition(p, world, 0, out, o);
+				continue;
+			} else if (p > pathLength) {
+				addAfterPosition(p - pathLength, world, verticesLength - 4, out, o);
+				continue;
+			}
+
+			// Determine curve containing position.
+			for (;; curve++) {
+				float length = curves[curve];
+				if (p > length) continue;
+				if (curve == 0)
+					p /= length;
+				else {
+					float prev = curves[curve - 1];
+					p = (p - prev) / (length - prev);
+				}
+				break;
+			}
+
+			// Curve segment lengths.
+			if (curve != prevCurve) {
+				prevCurve = curve;
+				int ii = curve * 6;
+				x1 = world[ii];
+				y1 = world[ii + 1];
+				cx1 = world[ii + 2];
+				cy1 = world[ii + 3];
+				cx2 = world[ii + 4];
+				cy2 = world[ii + 5];
+				x2 = world[ii + 6];
+				y2 = world[ii + 7];
+				tmpx = (x1 - cx1 * 2 + cx2) * 0.03f;
+				tmpy = (y1 - cy1 * 2 + cy2) * 0.03f;
+				dddfx = ((cx1 - cx2) * 3 - x1 + x2) * 0.006f;
+				dddfy = ((cy1 - cy2) * 3 - y1 + y2) * 0.006f;
+				ddfx = tmpx * 2 + dddfx;
+				ddfy = tmpy * 2 + dddfy;
+				dfx = (cx1 - x1) * 0.3f + tmpx + dddfx * 0.16666667f;
+				dfy = (cy1 - y1) * 0.3f + tmpy + dddfy * 0.16666667f;
+				curveLength = (float)Math.sqrt(dfx * dfx + dfy * dfy);
+				segments[0] = curveLength;
+				for (ii = 1; ii < 8; ii++) {
+					dfx += ddfx;
+					dfy += ddfy;
+					ddfx += dddfx;
+					ddfy += dddfy;
+					curveLength += (float)Math.sqrt(dfx * dfx + dfy * dfy);
+					segments[ii] = curveLength;
+				}
+				dfx += ddfx;
+				dfy += ddfy;
+				curveLength += (float)Math.sqrt(dfx * dfx + dfy * dfy);
+				segments[8] = curveLength;
+				dfx += ddfx + dddfx;
+				dfy += ddfy + dddfy;
+				curveLength += (float)Math.sqrt(dfx * dfx + dfy * dfy);
+				segments[9] = curveLength;
+				segment = 0;
+			}
+
+			// Weight by segment length.
+			p *= curveLength;
+			for (;; segment++) {
+				float length = segments[segment];
+				if (p > length) continue;
+				if (segment == 0)
+					p /= length;
+				else {
+					float prev = segments[segment - 1];
+					p = segment + (p - prev) / (length - prev);
+				}
+				break;
+			}
+			addCurvePosition(p * 0.1f, x1, y1, cx1, cy1, cx2, cy2, x2, y2, out, o, tangents || (i > 0 && space < epsilon));
+		}
+		return out;
+	}
+
+	private void addBeforePosition (float p, float[] temp, int i, float[] out, int o) {
+		float x1 = temp[i], y1 = temp[i + 1], dx = temp[i + 2] - x1, dy = temp[i + 3] - y1, r = (float)Math.atan2(dy, dx);
+		out[o] = x1 + p * (float)Math.cos(r);
+		out[o + 1] = y1 + p * (float)Math.sin(r);
+		out[o + 2] = r;
+	}
+
+	private void addAfterPosition (float p, float[] temp, int i, float[] out, int o) {
+		float x1 = temp[i + 2], y1 = temp[i + 3], dx = x1 - temp[i], dy = y1 - temp[i + 1], r = (float)Math.atan2(dy, dx);
+		out[o] = x1 + p * (float)Math.cos(r);
+		out[o + 1] = y1 + p * (float)Math.sin(r);
+		out[o + 2] = r;
+	}
+
+	private void addCurvePosition (float p, float x1, float y1, float cx1, float cy1, float cx2, float cy2, float x2, float y2,
+		float[] out, int o, boolean tangents) {
+		if (p < epsilon || Float.isNaN(p)) {
+			out[o] = x1;
+			out[o + 1] = y1;
+			out[o + 2] = (float)Math.atan2(cy1 - y1, cx1 - x1);
+			return;
+		}
+		float tt = p * p, ttt = tt * p, u = 1 - p, uu = u * u, uuu = uu * u;
+		float ut = u * p, ut3 = ut * 3, uut3 = u * ut3, utt3 = ut3 * p;
+		float x = x1 * uuu + cx1 * uut3 + cx2 * utt3 + x2 * ttt, y = y1 * uuu + cy1 * uut3 + cy2 * utt3 + y2 * ttt;
+		out[o] = x;
+		out[o + 1] = y;
+		if (tangents) {
+			if (p < 0.001f)
+				out[o + 2] = (float)Math.atan2(cy1 - y1, cx1 - x1);
+			else
+				out[o + 2] = (float)Math.atan2(y - (y1 * uu + cy1 * ut * 2 + cy2 * tt), x - (x1 * uu + cx1 * ut * 2 + cx2 * tt));
+		}
+	}
+
+	/** The position along the path. */
+	public float getPosition () {
+		return position;
+	}
+
+	public void setPosition (float position) {
+		this.position = position;
+	}
+
+	/** The spacing between bones. */
+	public float getSpacing () {
+		return spacing;
+	}
+
+	public void setSpacing (float spacing) {
+		this.spacing = spacing;
+	}
+
+	/** A percentage (0-1) that controls the mix between the constrained and unconstrained rotation. */
+	public float getMixRotate () {
+		return mixRotate;
+	}
+
+	public void setMixRotate (float mixRotate) {
+		this.mixRotate = mixRotate;
+	}
+
+	/** A percentage (0-1) that controls the mix between the constrained and unconstrained translation X. */
+	public float getMixX () {
+		return mixX;
+	}
+
+	public void setMixX (float mixX) {
+		this.mixX = mixX;
+	}
+
+	/** A percentage (0-1) that controls the mix between the constrained and unconstrained translation Y. */
+	public float getMixY () {
+		return mixY;
+	}
+
+	public void setMixY (float mixY) {
+		this.mixY = mixY;
+	}
+
+	/** The bones that will be modified by this path constraint. */
+	public Array<Bone> getBones () {
+		return bones;
+	}
+
+	/** The slot whose path attachment will be used to constrained the bones. */
+	public Slot getTarget () {
+		return target;
+	}
+
+	public void setTarget (Slot target) {
+		if (target == null) throw new IllegalArgumentException("target cannot be null.");
+		this.target = target;
+	}
+
+	public boolean isActive () {
+		return active;
+	}
+
+	/** The path constraint's setup pose data. */
+	public PathConstraintData getData () {
+		return data;
+	}
+
+	public String toString () {
+		return data.name;
+	}
+}

+ 175 - 175
spine-libgdx/spine-libgdx/src/com/esotericsoftware/spine/PathConstraintData.java

@@ -1,175 +1,175 @@
-/******************************************************************************
- * Spine Runtimes License Agreement
- * Last updated January 1, 2020. Replaces all prior versions.
- *
- * Copyright (c) 2013-2020, Esoteric Software LLC
- *
- * Integration of the Spine Runtimes into software or otherwise creating
- * derivative works of the Spine Runtimes is permitted under the terms and
- * conditions of Section 2 of the Spine Editor License Agreement:
- * http://esotericsoftware.com/spine-editor-license
- *
- * Otherwise, it is permitted to integrate the Spine Runtimes into software
- * or otherwise create derivative works of the Spine Runtimes (collectively,
- * "Products"), provided that each user of the Products must obtain their own
- * Spine Editor license and redistribution of the Products in any form must
- * include this license and copyright notice.
- *
- * THE SPINE RUNTIMES ARE PROVIDED BY ESOTERIC SOFTWARE LLC "AS IS" AND ANY
- * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
- * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
- * DISCLAIMED. IN NO EVENT SHALL ESOTERIC SOFTWARE LLC BE LIABLE FOR ANY
- * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
- * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES,
- * BUSINESS INTERRUPTION, OR LOSS OF USE, DATA, OR PROFITS) HOWEVER CAUSED AND
- * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
- * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
- * THE SPINE RUNTIMES, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
- *****************************************************************************/
-
-package com.esotericsoftware.spine;
-
-import com.badlogic.gdx.utils.Array;
-
-/** Stores the setup pose for a {@link PathConstraint}.
- * <p>
- * See <a href="http://esotericsoftware.com/spine-path-constraints">Path constraints</a> in the Spine User Guide. */
-public class PathConstraintData extends ConstraintData {
-	final Array<BoneData> bones = new Array();
-	SlotData target;
-	PositionMode positionMode;
-	SpacingMode spacingMode;
-	RotateMode rotateMode;
-	float offsetRotation;
-	float position, spacing, mixRotate, mixX, mixY;
-
-	public PathConstraintData (String name) {
-		super(name);
-	}
-
-	/** The bones that will be modified by this path constraint. */
-	public Array<BoneData> getBones () {
-		return bones;
-	}
-
-	/** The slot whose path attachment will be used to constrained the bones. */
-	public SlotData getTarget () {
-		return target;
-	}
-
-	public void setTarget (SlotData target) {
-		if (target == null) throw new IllegalArgumentException("target cannot be null.");
-		this.target = target;
-	}
-
-	/** The mode for positioning the first bone on the path. */
-	public PositionMode getPositionMode () {
-		return positionMode;
-	}
-
-	public void setPositionMode (PositionMode positionMode) {
-		if (positionMode == null) throw new IllegalArgumentException("positionMode cannot be null.");
-		this.positionMode = positionMode;
-	}
-
-	/** The mode for positioning the bones after the first bone on the path. */
-	public SpacingMode getSpacingMode () {
-		return spacingMode;
-	}
-
-	public void setSpacingMode (SpacingMode spacingMode) {
-		if (spacingMode == null) throw new IllegalArgumentException("spacingMode cannot be null.");
-		this.spacingMode = spacingMode;
-	}
-
-	/** The mode for adjusting the rotation of the bones. */
-	public RotateMode getRotateMode () {
-		return rotateMode;
-	}
-
-	public void setRotateMode (RotateMode rotateMode) {
-		if (rotateMode == null) throw new IllegalArgumentException("rotateMode cannot be null.");
-		this.rotateMode = rotateMode;
-	}
-
-	/** An offset added to the constrained bone rotation. */
-	public float getOffsetRotation () {
-		return offsetRotation;
-	}
-
-	public void setOffsetRotation (float offsetRotation) {
-		this.offsetRotation = offsetRotation;
-	}
-
-	/** The position along the path. */
-	public float getPosition () {
-		return position;
-	}
-
-	public void setPosition (float position) {
-		this.position = position;
-	}
-
-	/** The spacing between bones. */
-	public float getSpacing () {
-		return spacing;
-	}
-
-	public void setSpacing (float spacing) {
-		this.spacing = spacing;
-	}
-
-	/** A percentage (0-1) that controls the mix between the constrained and unconstrained rotation. */
-	public float getMixRotate () {
-		return mixRotate;
-	}
-
-	public void setMixRotate (float mixRotate) {
-		this.mixRotate = mixRotate;
-	}
-
-	/** A percentage (0-1) that controls the mix between the constrained and unconstrained translation X. */
-	public float getMixX () {
-		return mixX;
-	}
-
-	public void setMixX (float mixX) {
-		this.mixX = mixX;
-	}
-
-	/** A percentage (0-1) that controls the mix between the constrained and unconstrained translation Y. */
-	public float getMixY () {
-		return mixY;
-	}
-
-	public void setMixY (float mixY) {
-		this.mixY = mixY;
-	}
-
-	/** Controls how the first bone is positioned along the path.
-	 * <p>
-	 * See <a href="http://esotericsoftware.com/spine-path-constraints#Position-mode">Position mode</a> in the Spine User Guide. */
-	static public enum PositionMode {
-		fixed, percent;
-
-		static public final PositionMode[] values = PositionMode.values();
-	}
-
-	/** Controls how bones after the first bone are positioned along the path.
-	 * <p>
-	 * See <a href="http://esotericsoftware.com/spine-path-constraints#Spacing-mode">Spacing mode</a> in the Spine User Guide. */
-	static public enum SpacingMode {
-		length, fixed, percent, proportional;
-
-		static public final SpacingMode[] values = SpacingMode.values();
-	}
-
-	/** Controls how bones are rotated, translated, and scaled to match the path.
-	 * <p>
-	 * See <a href="http://esotericsoftware.com/spine-path-constraints#Rotate-mode">Rotate mode</a> in the Spine User Guide. */
-	static public enum RotateMode {
-		tangent, chain, chainScale;
-
-		static public final RotateMode[] values = RotateMode.values();
-	}
-}
+/******************************************************************************
+ * Spine Runtimes License Agreement
+ * Last updated January 1, 2020. Replaces all prior versions.
+ *
+ * Copyright (c) 2013-2020, Esoteric Software LLC
+ *
+ * Integration of the Spine Runtimes into software or otherwise creating
+ * derivative works of the Spine Runtimes is permitted under the terms and
+ * conditions of Section 2 of the Spine Editor License Agreement:
+ * http://esotericsoftware.com/spine-editor-license
+ *
+ * Otherwise, it is permitted to integrate the Spine Runtimes into software
+ * or otherwise create derivative works of the Spine Runtimes (collectively,
+ * "Products"), provided that each user of the Products must obtain their own
+ * Spine Editor license and redistribution of the Products in any form must
+ * include this license and copyright notice.
+ *
+ * THE SPINE RUNTIMES ARE PROVIDED BY ESOTERIC SOFTWARE LLC "AS IS" AND ANY
+ * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL ESOTERIC SOFTWARE LLC BE LIABLE FOR ANY
+ * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES,
+ * BUSINESS INTERRUPTION, OR LOSS OF USE, DATA, OR PROFITS) HOWEVER CAUSED AND
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
+ * THE SPINE RUNTIMES, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ *****************************************************************************/
+
+package com.esotericsoftware.spine;
+
+import com.badlogic.gdx.utils.Array;
+
+/** Stores the setup pose for a {@link PathConstraint}.
+ * <p>
+ * See <a href="http://esotericsoftware.com/spine-path-constraints">Path constraints</a> in the Spine User Guide. */
+public class PathConstraintData extends ConstraintData {
+	final Array<BoneData> bones = new Array();
+	SlotData target;
+	PositionMode positionMode;
+	SpacingMode spacingMode;
+	RotateMode rotateMode;
+	float offsetRotation;
+	float position, spacing, mixRotate, mixX, mixY;
+
+	public PathConstraintData (String name) {
+		super(name);
+	}
+
+	/** The bones that will be modified by this path constraint. */
+	public Array<BoneData> getBones () {
+		return bones;
+	}
+
+	/** The slot whose path attachment will be used to constrained the bones. */
+	public SlotData getTarget () {
+		return target;
+	}
+
+	public void setTarget (SlotData target) {
+		if (target == null) throw new IllegalArgumentException("target cannot be null.");
+		this.target = target;
+	}
+
+	/** The mode for positioning the first bone on the path. */
+	public PositionMode getPositionMode () {
+		return positionMode;
+	}
+
+	public void setPositionMode (PositionMode positionMode) {
+		if (positionMode == null) throw new IllegalArgumentException("positionMode cannot be null.");
+		this.positionMode = positionMode;
+	}
+
+	/** The mode for positioning the bones after the first bone on the path. */
+	public SpacingMode getSpacingMode () {
+		return spacingMode;
+	}
+
+	public void setSpacingMode (SpacingMode spacingMode) {
+		if (spacingMode == null) throw new IllegalArgumentException("spacingMode cannot be null.");
+		this.spacingMode = spacingMode;
+	}
+
+	/** The mode for adjusting the rotation of the bones. */
+	public RotateMode getRotateMode () {
+		return rotateMode;
+	}
+
+	public void setRotateMode (RotateMode rotateMode) {
+		if (rotateMode == null) throw new IllegalArgumentException("rotateMode cannot be null.");
+		this.rotateMode = rotateMode;
+	}
+
+	/** An offset added to the constrained bone rotation. */
+	public float getOffsetRotation () {
+		return offsetRotation;
+	}
+
+	public void setOffsetRotation (float offsetRotation) {
+		this.offsetRotation = offsetRotation;
+	}
+
+	/** The position along the path. */
+	public float getPosition () {
+		return position;
+	}
+
+	public void setPosition (float position) {
+		this.position = position;
+	}
+
+	/** The spacing between bones. */
+	public float getSpacing () {
+		return spacing;
+	}
+
+	public void setSpacing (float spacing) {
+		this.spacing = spacing;
+	}
+
+	/** A percentage (0-1) that controls the mix between the constrained and unconstrained rotation. */
+	public float getMixRotate () {
+		return mixRotate;
+	}
+
+	public void setMixRotate (float mixRotate) {
+		this.mixRotate = mixRotate;
+	}
+
+	/** A percentage (0-1) that controls the mix between the constrained and unconstrained translation X. */
+	public float getMixX () {
+		return mixX;
+	}
+
+	public void setMixX (float mixX) {
+		this.mixX = mixX;
+	}
+
+	/** A percentage (0-1) that controls the mix between the constrained and unconstrained translation Y. */
+	public float getMixY () {
+		return mixY;
+	}
+
+	public void setMixY (float mixY) {
+		this.mixY = mixY;
+	}
+
+	/** Controls how the first bone is positioned along the path.
+	 * <p>
+	 * See <a href="http://esotericsoftware.com/spine-path-constraints#Position-mode">Position mode</a> in the Spine User Guide. */
+	static public enum PositionMode {
+		fixed, percent;
+
+		static public final PositionMode[] values = PositionMode.values();
+	}
+
+	/** Controls how bones after the first bone are positioned along the path.
+	 * <p>
+	 * See <a href="http://esotericsoftware.com/spine-path-constraints#Spacing-mode">Spacing mode</a> in the Spine User Guide. */
+	static public enum SpacingMode {
+		length, fixed, percent, proportional;
+
+		static public final SpacingMode[] values = SpacingMode.values();
+	}
+
+	/** Controls how bones are rotated, translated, and scaled to match the path.
+	 * <p>
+	 * See <a href="http://esotericsoftware.com/spine-path-constraints#Rotate-mode">Rotate mode</a> in the Spine User Guide. */
+	static public enum RotateMode {
+		tangent, chain, chainScale;
+
+		static public final RotateMode[] values = RotateMode.values();
+	}
+}

+ 768 - 768
spine-libgdx/spine-libgdx/src/com/esotericsoftware/spine/Skeleton.java

@@ -1,768 +1,768 @@
-/******************************************************************************
- * Spine Runtimes License Agreement
- * Last updated January 1, 2020. Replaces all prior versions.
- *
- * Copyright (c) 2013-2020, Esoteric Software LLC
- *
- * Integration of the Spine Runtimes into software or otherwise creating
- * derivative works of the Spine Runtimes is permitted under the terms and
- * conditions of Section 2 of the Spine Editor License Agreement:
- * http://esotericsoftware.com/spine-editor-license
- *
- * Otherwise, it is permitted to integrate the Spine Runtimes into software
- * or otherwise create derivative works of the Spine Runtimes (collectively,
- * "Products"), provided that each user of the Products must obtain their own
- * Spine Editor license and redistribution of the Products in any form must
- * include this license and copyright notice.
- *
- * THE SPINE RUNTIMES ARE PROVIDED BY ESOTERIC SOFTWARE LLC "AS IS" AND ANY
- * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
- * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
- * DISCLAIMED. IN NO EVENT SHALL ESOTERIC SOFTWARE LLC BE LIABLE FOR ANY
- * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
- * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES,
- * BUSINESS INTERRUPTION, OR LOSS OF USE, DATA, OR PROFITS) HOWEVER CAUSED AND
- * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
- * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
- * THE SPINE RUNTIMES, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
- *****************************************************************************/
-
-package com.esotericsoftware.spine;
-
-import static com.esotericsoftware.spine.utils.SpineUtils.*;
-
-import com.badlogic.gdx.graphics.Color;
-import com.badlogic.gdx.math.Vector2;
-import com.badlogic.gdx.utils.Array;
-import com.badlogic.gdx.utils.FloatArray;
-import com.badlogic.gdx.utils.Null;
-
-import com.esotericsoftware.spine.Skin.SkinEntry;
-import com.esotericsoftware.spine.attachments.Attachment;
-import com.esotericsoftware.spine.attachments.MeshAttachment;
-import com.esotericsoftware.spine.attachments.PathAttachment;
-import com.esotericsoftware.spine.attachments.RegionAttachment;
-
-/** Stores the current pose for a skeleton.
- * <p>
- * See <a href="http://esotericsoftware.com/spine-runtime-architecture#Instance-objects">Instance objects</a> in the Spine
- * Runtimes Guide. */
-public class Skeleton {
-	final SkeletonData data;
-	final Array<Bone> bones;
-	final Array<Slot> slots;
-	Array<Slot> drawOrder;
-	final Array<IkConstraint> ikConstraints;
-	final Array<TransformConstraint> transformConstraints;
-	final Array<PathConstraint> pathConstraints;
-	final Array<Updatable> updateCache = new Array();
-	@Null Skin skin;
-	final Color color;
-	float time;
-	float scaleX = 1, scaleY = 1;
-	float x, y;
-
-	public Skeleton (SkeletonData data) {
-		if (data == null) throw new IllegalArgumentException("data cannot be null.");
-		this.data = data;
-
-		bones = new Array(data.bones.size);
-		Object[] bones = this.bones.items;
-		for (BoneData boneData : data.bones) {
-			Bone bone;
-			if (boneData.parent == null)
-				bone = new Bone(boneData, this, null);
-			else {
-				Bone parent = (Bone)bones[boneData.parent.index];
-				bone = new Bone(boneData, this, parent);
-				parent.children.add(bone);
-			}
-			this.bones.add(bone);
-		}
-
-		slots = new Array(data.slots.size);
-		drawOrder = new Array(data.slots.size);
-		for (SlotData slotData : data.slots) {
-			Bone bone = (Bone)bones[slotData.boneData.index];
-			Slot slot = new Slot(slotData, bone);
-			slots.add(slot);
-			drawOrder.add(slot);
-		}
-
-		ikConstraints = new Array(data.ikConstraints.size);
-		for (IkConstraintData ikConstraintData : data.ikConstraints)
-			ikConstraints.add(new IkConstraint(ikConstraintData, this));
-
-		transformConstraints = new Array(data.transformConstraints.size);
-		for (TransformConstraintData transformConstraintData : data.transformConstraints)
-			transformConstraints.add(new TransformConstraint(transformConstraintData, this));
-
-		pathConstraints = new Array(data.pathConstraints.size);
-		for (PathConstraintData pathConstraintData : data.pathConstraints)
-			pathConstraints.add(new PathConstraint(pathConstraintData, this));
-
-		color = new Color(1, 1, 1, 1);
-
-		updateCache();
-	}
-
-	/** Copy constructor. */
-	public Skeleton (Skeleton skeleton) {
-		if (skeleton == null) throw new IllegalArgumentException("skeleton cannot be null.");
-		data = skeleton.data;
-
-		bones = new Array(skeleton.bones.size);
-		for (Bone bone : skeleton.bones) {
-			Bone newBone;
-			if (bone.parent == null)
-				newBone = new Bone(bone, this, null);
-			else {
-				Bone parent = bones.get(bone.parent.data.index);
-				newBone = new Bone(bone, this, parent);
-				parent.children.add(newBone);
-			}
-			bones.add(newBone);
-		}
-
-		slots = new Array(skeleton.slots.size);
-		for (Slot slot : skeleton.slots) {
-			Bone bone = bones.get(slot.bone.data.index);
-			slots.add(new Slot(slot, bone));
-		}
-
-		drawOrder = new Array(slots.size);
-		for (Slot slot : skeleton.drawOrder)
-			drawOrder.add(slots.get(slot.data.index));
-
-		ikConstraints = new Array(skeleton.ikConstraints.size);
-		for (IkConstraint ikConstraint : skeleton.ikConstraints)
-			ikConstraints.add(new IkConstraint(ikConstraint, this));
-
-		transformConstraints = new Array(skeleton.transformConstraints.size);
-		for (TransformConstraint transformConstraint : skeleton.transformConstraints)
-			transformConstraints.add(new TransformConstraint(transformConstraint, this));
-
-		pathConstraints = new Array(skeleton.pathConstraints.size);
-		for (PathConstraint pathConstraint : skeleton.pathConstraints)
-			pathConstraints.add(new PathConstraint(pathConstraint, this));
-
-		skin = skeleton.skin;
-		color = new Color(skeleton.color);
-		time = skeleton.time;
-		scaleX = skeleton.scaleX;
-		scaleY = skeleton.scaleY;
-
-		updateCache();
-	}
-
-	/** Caches information about bones and constraints. Must be called if the {@link #getSkin()} is modified or if bones,
-	 * constraints, or weighted path attachments are added or removed. */
-	public void updateCache () {
-		Array<Updatable> updateCache = this.updateCache;
-		updateCache.clear();
-
-		int boneCount = bones.size;
-		Object[] bones = this.bones.items;
-		for (int i = 0; i < boneCount; i++) {
-			Bone bone = (Bone)bones[i];
-			bone.sorted = bone.data.skinRequired;
-			bone.active = !bone.sorted;
-		}
-		if (skin != null) {
-			Object[] skinBones = skin.bones.items;
-			for (int i = 0, n = skin.bones.size; i < n; i++) {
-				Bone bone = (Bone)bones[((BoneData)skinBones[i]).index];
-				do {
-					bone.sorted = false;
-					bone.active = true;
-					bone = bone.parent;
-				} while (bone != null);
-			}
-		}
-
-		int ikCount = ikConstraints.size, transformCount = transformConstraints.size, pathCount = pathConstraints.size;
-		Object[] ikConstraints = this.ikConstraints.items;
-		Object[] transformConstraints = this.transformConstraints.items;
-		Object[] pathConstraints = this.pathConstraints.items;
-		int constraintCount = ikCount + transformCount + pathCount;
-		outer:
-		for (int i = 0; i < constraintCount; i++) {
-			for (int ii = 0; ii < ikCount; ii++) {
-				IkConstraint constraint = (IkConstraint)ikConstraints[ii];
-				if (constraint.data.order == i) {
-					sortIkConstraint(constraint);
-					continue outer;
-				}
-			}
-			for (int ii = 0; ii < transformCount; ii++) {
-				TransformConstraint constraint = (TransformConstraint)transformConstraints[ii];
-				if (constraint.data.order == i) {
-					sortTransformConstraint(constraint);
-					continue outer;
-				}
-			}
-			for (int ii = 0; ii < pathCount; ii++) {
-				PathConstraint constraint = (PathConstraint)pathConstraints[ii];
-				if (constraint.data.order == i) {
-					sortPathConstraint(constraint);
-					continue outer;
-				}
-			}
-		}
-
-		for (int i = 0; i < boneCount; i++)
-			sortBone((Bone)bones[i]);
-	}
-
-	private void sortIkConstraint (IkConstraint constraint) {
-		constraint.active = constraint.target.active
-			&& (!constraint.data.skinRequired || (skin != null && skin.constraints.contains(constraint.data, true)));
-		if (!constraint.active) return;
-
-		Bone target = constraint.target;
-		sortBone(target);
-
-		Array<Bone> constrained = constraint.bones;
-		Bone parent = constrained.first();
-		sortBone(parent);
-		if (constrained.size == 1) {
-			updateCache.add(constraint);
-			sortReset(parent.children);
-		} else {
-			Bone child = constrained.peek();
-			sortBone(child);
-
-			updateCache.add(constraint);
-
-			sortReset(parent.children);
-			child.sorted = true;
-		}
-	}
-
-	private void sortPathConstraint (PathConstraint constraint) {
-		constraint.active = constraint.target.bone.active
-			&& (!constraint.data.skinRequired || (skin != null && skin.constraints.contains(constraint.data, true)));
-		if (!constraint.active) return;
-
-		Slot slot = constraint.target;
-		int slotIndex = slot.getData().index;
-		Bone slotBone = slot.bone;
-		if (skin != null) sortPathConstraintAttachment(skin, slotIndex, slotBone);
-		if (data.defaultSkin != null && data.defaultSkin != skin)
-			sortPathConstraintAttachment(data.defaultSkin, slotIndex, slotBone);
-
-		Attachment attachment = slot.attachment;
-		if (attachment instanceof PathAttachment) sortPathConstraintAttachment(attachment, slotBone);
-
-		Object[] constrained = constraint.bones.items;
-		int boneCount = constraint.bones.size;
-		for (int i = 0; i < boneCount; i++)
-			sortBone((Bone)constrained[i]);
-
-		updateCache.add(constraint);
-
-		for (int i = 0; i < boneCount; i++)
-			sortReset(((Bone)constrained[i]).children);
-		for (int i = 0; i < boneCount; i++)
-			((Bone)constrained[i]).sorted = true;
-	}
-
-	private void sortTransformConstraint (TransformConstraint constraint) {
-		constraint.active = constraint.target.active
-			&& (!constraint.data.skinRequired || (skin != null && skin.constraints.contains(constraint.data, true)));
-		if (!constraint.active) return;
-
-		sortBone(constraint.target);
-
-		Object[] constrained = constraint.bones.items;
-		int boneCount = constraint.bones.size;
-		if (constraint.data.local) {
-			for (int i = 0; i < boneCount; i++) {
-				Bone child = (Bone)constrained[i];
-				sortBone(child.parent);
-				sortBone(child);
-			}
-		} else {
-			for (int i = 0; i < boneCount; i++)
-				sortBone((Bone)constrained[i]);
-		}
-
-		updateCache.add(constraint);
-
-		for (int i = 0; i < boneCount; i++)
-			sortReset(((Bone)constrained[i]).children);
-		for (int i = 0; i < boneCount; i++)
-			((Bone)constrained[i]).sorted = true;
-	}
-
-	private void sortPathConstraintAttachment (Skin skin, int slotIndex, Bone slotBone) {
-		Object[] entries = skin.attachments.orderedItems().items;
-		for (int i = 0, n = skin.attachments.size; i < n; i++) {
-			SkinEntry entry = (SkinEntry)entries[i];
-			if (entry.slotIndex == slotIndex) sortPathConstraintAttachment(entry.attachment, slotBone);
-		}
-	}
-
-	private void sortPathConstraintAttachment (Attachment attachment, Bone slotBone) {
-		if (!(attachment instanceof PathAttachment)) return;
-		int[] pathBones = ((PathAttachment)attachment).getBones();
-		if (pathBones == null)
-			sortBone(slotBone);
-		else {
-			Object[] bones = this.bones.items;
-			for (int i = 0, n = pathBones.length; i < n;) {
-				int nn = pathBones[i++];
-				nn += i;
-				while (i < nn)
-					sortBone((Bone)bones[pathBones[i++]]);
-			}
-		}
-	}
-
-	private void sortBone (Bone bone) {
-		if (bone.sorted) return;
-		Bone parent = bone.parent;
-		if (parent != null) sortBone(parent);
-		bone.sorted = true;
-		updateCache.add(bone);
-	}
-
-	private void sortReset (Array<Bone> bones) {
-		Object[] items = bones.items;
-		for (int i = 0, n = bones.size; i < n; i++) {
-			Bone bone = (Bone)items[i];
-			if (!bone.active) continue;
-			if (bone.sorted) sortReset(bone.children);
-			bone.sorted = false;
-		}
-	}
-
-	/** Updates the world transform for each bone and applies all constraints.
-	 * <p>
-	 * See <a href="http://esotericsoftware.com/spine-runtime-skeletons#World-transforms">World transforms</a> in the Spine
-	 * Runtimes Guide. */
-	public void updateWorldTransform () {
-		Object[] bones = this.bones.items;
-		for (int i = 0, n = this.bones.size; i < n; i++) {
-			Bone bone = (Bone)bones[i];
-			bone.ax = bone.x;
-			bone.ay = bone.y;
-			bone.arotation = bone.rotation;
-			bone.ascaleX = bone.scaleX;
-			bone.ascaleY = bone.scaleY;
-			bone.ashearX = bone.shearX;
-			bone.ashearY = bone.shearY;
-		}
-
-		Object[] updateCache = this.updateCache.items;
-		for (int i = 0, n = this.updateCache.size; i < n; i++)
-			((Updatable)updateCache[i]).update();
-	}
-
-	/** Temporarily sets the root bone as a child of the specified bone, then updates the world transform for each bone and applies
-	 * all constraints.
-	 * <p>
-	 * See <a href="http://esotericsoftware.com/spine-runtime-skeletons#World-transforms">World transforms</a> in the Spine
-	 * Runtimes Guide. */
-	public void updateWorldTransform (Bone parent) {
-		if (parent == null) throw new IllegalArgumentException("parent cannot be null.");
-
-		// Apply the parent bone transform to the root bone. The root bone always inherits scale, rotation and reflection.
-		Bone rootBone = getRootBone();
-		float pa = parent.a, pb = parent.b, pc = parent.c, pd = parent.d;
-		rootBone.worldX = pa * x + pb * y + parent.worldX;
-		rootBone.worldY = pc * x + pd * y + parent.worldY;
-
-		float rotationY = rootBone.rotation + 90 + rootBone.shearY;
-		float la = cosDeg(rootBone.rotation + rootBone.shearX) * rootBone.scaleX;
-		float lb = cosDeg(rotationY) * rootBone.scaleY;
-		float lc = sinDeg(rootBone.rotation + rootBone.shearX) * rootBone.scaleX;
-		float ld = sinDeg(rotationY) * rootBone.scaleY;
-		rootBone.a = (pa * la + pb * lc) * scaleX;
-		rootBone.b = (pa * lb + pb * ld) * scaleX;
-		rootBone.c = (pc * la + pd * lc) * scaleY;
-		rootBone.d = (pc * lb + pd * ld) * scaleY;
-
-		// Update everything except root bone.
-		Object[] updateCache = this.updateCache.items;
-		for (int i = 0, n = this.updateCache.size; i < n; i++) {
-			Updatable updatable = (Updatable)updateCache[i];
-			if (updatable != rootBone) updatable.update();
-		}
-	}
-
-	/** Sets the bones, constraints, slots, and draw order to their setup pose values. */
-	public void setToSetupPose () {
-		setBonesToSetupPose();
-		setSlotsToSetupPose();
-	}
-
-	/** Sets the bones and constraints to their setup pose values. */
-	public void setBonesToSetupPose () {
-		Object[] bones = this.bones.items;
-		for (int i = 0, n = this.bones.size; i < n; i++)
-			((Bone)bones[i]).setToSetupPose();
-
-		Object[] ikConstraints = this.ikConstraints.items;
-		for (int i = 0, n = this.ikConstraints.size; i < n; i++) {
-			IkConstraint constraint = (IkConstraint)ikConstraints[i];
-			constraint.mix = constraint.data.mix;
-			constraint.softness = constraint.data.softness;
-			constraint.bendDirection = constraint.data.bendDirection;
-			constraint.compress = constraint.data.compress;
-			constraint.stretch = constraint.data.stretch;
-		}
-
-		Object[] transformConstraints = this.transformConstraints.items;
-		for (int i = 0, n = this.transformConstraints.size; i < n; i++) {
-			TransformConstraint constraint = (TransformConstraint)transformConstraints[i];
-			TransformConstraintData data = constraint.data;
-			constraint.mixRotate = data.mixRotate;
-			constraint.mixX = data.mixX;
-			constraint.mixY = data.mixY;
-			constraint.mixScaleX = data.mixScaleX;
-			constraint.mixScaleY = data.mixScaleY;
-			constraint.mixShearY = data.mixShearY;
-		}
-
-		Object[] pathConstraints = this.pathConstraints.items;
-		for (int i = 0, n = this.pathConstraints.size; i < n; i++) {
-			PathConstraint constraint = (PathConstraint)pathConstraints[i];
-			PathConstraintData data = constraint.data;
-			constraint.position = data.position;
-			constraint.spacing = data.spacing;
-			constraint.mixRotate = data.mixRotate;
-			constraint.mixX = data.mixX;
-			constraint.mixY = data.mixY;
-		}
-	}
-
-	/** Sets the slots and draw order to their setup pose values. */
-	public void setSlotsToSetupPose () {
-		Object[] slots = this.slots.items;
-		int n = this.slots.size;
-		arraycopy(slots, 0, drawOrder.items, 0, n);
-		for (int i = 0; i < n; i++)
-			((Slot)slots[i]).setToSetupPose();
-	}
-
-	/** The skeleton's setup pose data. */
-	public SkeletonData getData () {
-		return data;
-	}
-
-	/** The skeleton's bones, sorted parent first. The root bone is always the first bone. */
-	public Array<Bone> getBones () {
-		return bones;
-	}
-
-	/** The list of bones and constraints, sorted in the order they should be updated, as computed by {@link #updateCache()}. */
-	public Array<Updatable> getUpdateCache () {
-		return updateCache;
-	}
-
-	/** Returns the root bone, or null if the skeleton has no bones. */
-	public Bone getRootBone () {
-		return bones.size == 0 ? null : bones.first();
-	}
-
-	/** Finds a bone by comparing each bone's name. It is more efficient to cache the results of this method than to call it
-	 * repeatedly. */
-	public @Null Bone findBone (String boneName) {
-		if (boneName == null) throw new IllegalArgumentException("boneName cannot be null.");
-		Object[] bones = this.bones.items;
-		for (int i = 0, n = this.bones.size; i < n; i++) {
-			Bone bone = (Bone)bones[i];
-			if (bone.data.name.equals(boneName)) return bone;
-		}
-		return null;
-	}
-
-	/** The skeleton's slots. */
-	public Array<Slot> getSlots () {
-		return slots;
-	}
-
-	/** Finds a slot by comparing each slot's name. It is more efficient to cache the results of this method than to call it
-	 * repeatedly. */
-	public @Null Slot findSlot (String slotName) {
-		if (slotName == null) throw new IllegalArgumentException("slotName cannot be null.");
-		Object[] slots = this.slots.items;
-		for (int i = 0, n = this.slots.size; i < n; i++) {
-			Slot slot = (Slot)slots[i];
-			if (slot.data.name.equals(slotName)) return slot;
-		}
-		return null;
-	}
-
-	/** The skeleton's slots in the order they should be drawn. The returned array may be modified to change the draw order. */
-	public Array<Slot> getDrawOrder () {
-		return drawOrder;
-	}
-
-	public void setDrawOrder (Array<Slot> drawOrder) {
-		if (drawOrder == null) throw new IllegalArgumentException("drawOrder cannot be null.");
-		this.drawOrder = drawOrder;
-	}
-
-	/** The skeleton's current skin. */
-	public @Null Skin getSkin () {
-		return skin;
-	}
-
-	/** Sets a skin by name.
-	 * <p>
-	 * See {@link #setSkin(Skin)}. */
-	public void setSkin (String skinName) {
-		Skin skin = data.findSkin(skinName);
-		if (skin == null) throw new IllegalArgumentException("Skin not found: " + skinName);
-		setSkin(skin);
-	}
-
-	/** Sets the skin used to look up attachments before looking in the {@link SkeletonData#getDefaultSkin() default skin}. If the
-	 * skin is changed, {@link #updateCache()} is called.
-	 * <p>
-	 * Attachments from the new skin are attached if the corresponding attachment from the old skin was attached. If there was no
-	 * old skin, each slot's setup mode attachment is attached from the new skin.
-	 * <p>
-	 * After changing the skin, the visible attachments can be reset to those attached in the setup pose by calling
-	 * {@link #setSlotsToSetupPose()}. Also, often {@link AnimationState#apply(Skeleton)} is called before the next time the
-	 * skeleton is rendered to allow any attachment keys in the current animation(s) to hide or show attachments from the new
-	 * skin. */
-	public void setSkin (@Null Skin newSkin) {
-		if (newSkin == skin) return;
-		if (newSkin != null) {
-			if (skin != null)
-				newSkin.attachAll(this, skin);
-			else {
-				Object[] slots = this.slots.items;
-				for (int i = 0, n = this.slots.size; i < n; i++) {
-					Slot slot = (Slot)slots[i];
-					String name = slot.data.attachmentName;
-					if (name != null) {
-						Attachment attachment = newSkin.getAttachment(i, name);
-						if (attachment != null) slot.setAttachment(attachment);
-					}
-				}
-			}
-		}
-		skin = newSkin;
-		updateCache();
-	}
-
-	/** Finds an attachment by looking in the {@link #skin} and {@link SkeletonData#defaultSkin} using the slot name and attachment
-	 * name.
-	 * <p>
-	 * See {@link #getAttachment(int, String)}. */
-	public @Null Attachment getAttachment (String slotName, String attachmentName) {
-		SlotData slot = data.findSlot(slotName);
-		if (slot == null) throw new IllegalArgumentException("Slot not found: " + slotName);
-		return getAttachment(slot.getIndex(), attachmentName);
-	}
-
-	/** Finds an attachment by looking in the {@link #skin} and {@link SkeletonData#defaultSkin} using the slot index and
-	 * attachment name. First the skin is checked and if the attachment was not found, the default skin is checked.
-	 * <p>
-	 * See <a href="http://esotericsoftware.com/spine-runtime-skins">Runtime skins</a> in the Spine Runtimes Guide. */
-	public @Null Attachment getAttachment (int slotIndex, String attachmentName) {
-		if (attachmentName == null) throw new IllegalArgumentException("attachmentName cannot be null.");
-		if (skin != null) {
-			Attachment attachment = skin.getAttachment(slotIndex, attachmentName);
-			if (attachment != null) return attachment;
-		}
-		if (data.defaultSkin != null) return data.defaultSkin.getAttachment(slotIndex, attachmentName);
-		return null;
-	}
-
-	/** A convenience method to set an attachment by finding the slot with {@link #findSlot(String)}, finding the attachment with
-	 * {@link #getAttachment(int, String)}, then setting the slot's {@link Slot#attachment}.
-	 * @param attachmentName May be null to clear the slot's attachment. */
-	public void setAttachment (String slotName, @Null String attachmentName) {
-		if (slotName == null) throw new IllegalArgumentException("slotName cannot be null.");
-		Slot slot = findSlot(slotName);
-		if (slot == null) throw new IllegalArgumentException("Slot not found: " + slotName);
-		Attachment attachment = null;
-		if (attachmentName != null) {
-			attachment = getAttachment(slot.data.index, attachmentName);
-			if (attachment == null)
-				throw new IllegalArgumentException("Attachment not found: " + attachmentName + ", for slot: " + slotName);
-		}
-		slot.setAttachment(attachment);
-	}
-
-	/** The skeleton's IK constraints. */
-	public Array<IkConstraint> getIkConstraints () {
-		return ikConstraints;
-	}
-
-	/** Finds an IK constraint by comparing each IK constraint's name. It is more efficient to cache the results of this method
-	 * than to call it repeatedly. */
-	public @Null IkConstraint findIkConstraint (String constraintName) {
-		if (constraintName == null) throw new IllegalArgumentException("constraintName cannot be null.");
-		Object[] ikConstraints = this.ikConstraints.items;
-		for (int i = 0, n = this.ikConstraints.size; i < n; i++) {
-			IkConstraint ikConstraint = (IkConstraint)ikConstraints[i];
-			if (ikConstraint.data.name.equals(constraintName)) return ikConstraint;
-		}
-		return null;
-	}
-
-	/** The skeleton's transform constraints. */
-	public Array<TransformConstraint> getTransformConstraints () {
-		return transformConstraints;
-	}
-
-	/** Finds a transform constraint by comparing each transform constraint's name. It is more efficient to cache the results of
-	 * this method than to call it repeatedly. */
-	public @Null TransformConstraint findTransformConstraint (String constraintName) {
-		if (constraintName == null) throw new IllegalArgumentException("constraintName cannot be null.");
-		Object[] transformConstraints = this.transformConstraints.items;
-		for (int i = 0, n = this.transformConstraints.size; i < n; i++) {
-			TransformConstraint constraint = (TransformConstraint)transformConstraints[i];
-			if (constraint.data.name.equals(constraintName)) return constraint;
-		}
-		return null;
-	}
-
-	/** The skeleton's path constraints. */
-	public Array<PathConstraint> getPathConstraints () {
-		return pathConstraints;
-	}
-
-	/** Finds a path constraint by comparing each path constraint's name. It is more efficient to cache the results of this method
-	 * than to call it repeatedly. */
-	public @Null PathConstraint findPathConstraint (String constraintName) {
-		if (constraintName == null) throw new IllegalArgumentException("constraintName cannot be null.");
-		Object[] pathConstraints = this.pathConstraints.items;
-		for (int i = 0, n = this.pathConstraints.size; i < n; i++) {
-			PathConstraint constraint = (PathConstraint)pathConstraints[i];
-			if (constraint.data.name.equals(constraintName)) return constraint;
-		}
-		return null;
-	}
-
-	/** Returns the axis aligned bounding box (AABB) of the region and mesh attachments for the current pose.
-	 * @param offset An output value, the distance from the skeleton origin to the bottom left corner of the AABB.
-	 * @param size An output value, the width and height of the AABB.
-	 * @param temp Working memory to temporarily store attachments' computed world vertices. */
-	public void getBounds (Vector2 offset, Vector2 size, FloatArray temp) {
-		if (offset == null) throw new IllegalArgumentException("offset cannot be null.");
-		if (size == null) throw new IllegalArgumentException("size cannot be null.");
-		if (temp == null) throw new IllegalArgumentException("temp cannot be null.");
-		Object[] drawOrder = this.drawOrder.items;
-		float minX = Integer.MAX_VALUE, minY = Integer.MAX_VALUE, maxX = Integer.MIN_VALUE, maxY = Integer.MIN_VALUE;
-		for (int i = 0, n = this.drawOrder.size; i < n; i++) {
-			Slot slot = (Slot)drawOrder[i];
-			if (!slot.bone.active) continue;
-			int verticesLength = 0;
-			float[] vertices = null;
-			Attachment attachment = slot.attachment;
-			if (attachment instanceof RegionAttachment) {
-				verticesLength = 8;
-				vertices = temp.setSize(8);
-				((RegionAttachment)attachment).computeWorldVertices(slot.getBone(), vertices, 0, 2);
-			} else if (attachment instanceof MeshAttachment) {
-				MeshAttachment mesh = (MeshAttachment)attachment;
-				verticesLength = mesh.getWorldVerticesLength();
-				vertices = temp.setSize(verticesLength);
-				mesh.computeWorldVertices(slot, 0, verticesLength, vertices, 0, 2);
-			}
-			if (vertices != null) {
-				for (int ii = 0; ii < verticesLength; ii += 2) {
-					float x = vertices[ii], y = vertices[ii + 1];
-					minX = Math.min(minX, x);
-					minY = Math.min(minY, y);
-					maxX = Math.max(maxX, x);
-					maxY = Math.max(maxY, y);
-				}
-			}
-		}
-		offset.set(minX, minY);
-		size.set(maxX - minX, maxY - minY);
-	}
-
-	/** The color to tint all the skeleton's attachments. */
-	public Color getColor () {
-		return color;
-	}
-
-	/** A convenience method for setting the skeleton color. The color can also be set by modifying {@link #getColor()}. */
-	public void setColor (Color color) {
-		if (color == null) throw new IllegalArgumentException("color cannot be null.");
-		this.color.set(color);
-	}
-
-	/** A convenience method for setting the skeleton color. The color can also be set by modifying {@link #getColor()}. */
-	public void setColor (float r, float g, float b, float a) {
-		color.set(r, g, b, a);
-	}
-
-	/** Scales the entire skeleton on the X axis. This affects all bones, even if the bone's transform mode disallows scale
-	 * inheritance. */
-	public float getScaleX () {
-		return scaleX;
-	}
-
-	public void setScaleX (float scaleX) {
-		this.scaleX = scaleX;
-	}
-
-	/** Scales the entire skeleton on the Y axis. This affects all bones, even if the bone's transform mode disallows scale
-	 * inheritance. */
-	public float getScaleY () {
-		return scaleY;
-	}
-
-	public void setScaleY (float scaleY) {
-		this.scaleY = scaleY;
-	}
-
-	public void setScale (float scaleX, float scaleY) {
-		this.scaleX = scaleX;
-		this.scaleY = scaleY;
-	}
-
-	/** Sets the skeleton X position, which is added to the root bone worldX position. */
-	public float getX () {
-		return x;
-	}
-
-	public void setX (float x) {
-		this.x = x;
-	}
-
-	/** Sets the skeleton Y position, which is added to the root bone worldY position. */
-	public float getY () {
-		return y;
-	}
-
-	public void setY (float y) {
-		this.y = y;
-	}
-
-	/** Sets the skeleton X and Y position, which is added to the root bone worldX and worldY position. */
-	public void setPosition (float x, float y) {
-		this.x = x;
-		this.y = y;
-	}
-
-	/** Returns the skeleton's time. This can be used for tracking, such as with Slot {@link Slot#getAttachmentTime()}.
-	 * <p>
-	 * See {@link #update(float)}. */
-	public float getTime () {
-		return time;
-	}
-
-	public void setTime (float time) {
-		this.time = time;
-	}
-
-	/** Increments the skeleton's {@link #time}. */
-	public void update (float delta) {
-		time += delta;
-	}
-
-	public String toString () {
-		return data.name != null ? data.name : super.toString();
-	}
-}
+/******************************************************************************
+ * Spine Runtimes License Agreement
+ * Last updated January 1, 2020. Replaces all prior versions.
+ *
+ * Copyright (c) 2013-2020, Esoteric Software LLC
+ *
+ * Integration of the Spine Runtimes into software or otherwise creating
+ * derivative works of the Spine Runtimes is permitted under the terms and
+ * conditions of Section 2 of the Spine Editor License Agreement:
+ * http://esotericsoftware.com/spine-editor-license
+ *
+ * Otherwise, it is permitted to integrate the Spine Runtimes into software
+ * or otherwise create derivative works of the Spine Runtimes (collectively,
+ * "Products"), provided that each user of the Products must obtain their own
+ * Spine Editor license and redistribution of the Products in any form must
+ * include this license and copyright notice.
+ *
+ * THE SPINE RUNTIMES ARE PROVIDED BY ESOTERIC SOFTWARE LLC "AS IS" AND ANY
+ * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL ESOTERIC SOFTWARE LLC BE LIABLE FOR ANY
+ * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES,
+ * BUSINESS INTERRUPTION, OR LOSS OF USE, DATA, OR PROFITS) HOWEVER CAUSED AND
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
+ * THE SPINE RUNTIMES, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ *****************************************************************************/
+
+package com.esotericsoftware.spine;
+
+import static com.esotericsoftware.spine.utils.SpineUtils.*;
+
+import com.badlogic.gdx.graphics.Color;
+import com.badlogic.gdx.math.Vector2;
+import com.badlogic.gdx.utils.Array;
+import com.badlogic.gdx.utils.FloatArray;
+import com.badlogic.gdx.utils.Null;
+
+import com.esotericsoftware.spine.Skin.SkinEntry;
+import com.esotericsoftware.spine.attachments.Attachment;
+import com.esotericsoftware.spine.attachments.MeshAttachment;
+import com.esotericsoftware.spine.attachments.PathAttachment;
+import com.esotericsoftware.spine.attachments.RegionAttachment;
+
+/** Stores the current pose for a skeleton.
+ * <p>
+ * See <a href="http://esotericsoftware.com/spine-runtime-architecture#Instance-objects">Instance objects</a> in the Spine
+ * Runtimes Guide. */
+public class Skeleton {
+	final SkeletonData data;
+	final Array<Bone> bones;
+	final Array<Slot> slots;
+	Array<Slot> drawOrder;
+	final Array<IkConstraint> ikConstraints;
+	final Array<TransformConstraint> transformConstraints;
+	final Array<PathConstraint> pathConstraints;
+	final Array<Updatable> updateCache = new Array();
+	@Null Skin skin;
+	final Color color;
+	float time;
+	float scaleX = 1, scaleY = 1;
+	float x, y;
+
+	public Skeleton (SkeletonData data) {
+		if (data == null) throw new IllegalArgumentException("data cannot be null.");
+		this.data = data;
+
+		bones = new Array(data.bones.size);
+		Object[] bones = this.bones.items;
+		for (BoneData boneData : data.bones) {
+			Bone bone;
+			if (boneData.parent == null)
+				bone = new Bone(boneData, this, null);
+			else {
+				Bone parent = (Bone)bones[boneData.parent.index];
+				bone = new Bone(boneData, this, parent);
+				parent.children.add(bone);
+			}
+			this.bones.add(bone);
+		}
+
+		slots = new Array(data.slots.size);
+		drawOrder = new Array(data.slots.size);
+		for (SlotData slotData : data.slots) {
+			Bone bone = (Bone)bones[slotData.boneData.index];
+			Slot slot = new Slot(slotData, bone);
+			slots.add(slot);
+			drawOrder.add(slot);
+		}
+
+		ikConstraints = new Array(data.ikConstraints.size);
+		for (IkConstraintData ikConstraintData : data.ikConstraints)
+			ikConstraints.add(new IkConstraint(ikConstraintData, this));
+
+		transformConstraints = new Array(data.transformConstraints.size);
+		for (TransformConstraintData transformConstraintData : data.transformConstraints)
+			transformConstraints.add(new TransformConstraint(transformConstraintData, this));
+
+		pathConstraints = new Array(data.pathConstraints.size);
+		for (PathConstraintData pathConstraintData : data.pathConstraints)
+			pathConstraints.add(new PathConstraint(pathConstraintData, this));
+
+		color = new Color(1, 1, 1, 1);
+
+		updateCache();
+	}
+
+	/** Copy constructor. */
+	public Skeleton (Skeleton skeleton) {
+		if (skeleton == null) throw new IllegalArgumentException("skeleton cannot be null.");
+		data = skeleton.data;
+
+		bones = new Array(skeleton.bones.size);
+		for (Bone bone : skeleton.bones) {
+			Bone newBone;
+			if (bone.parent == null)
+				newBone = new Bone(bone, this, null);
+			else {
+				Bone parent = bones.get(bone.parent.data.index);
+				newBone = new Bone(bone, this, parent);
+				parent.children.add(newBone);
+			}
+			bones.add(newBone);
+		}
+
+		slots = new Array(skeleton.slots.size);
+		for (Slot slot : skeleton.slots) {
+			Bone bone = bones.get(slot.bone.data.index);
+			slots.add(new Slot(slot, bone));
+		}
+
+		drawOrder = new Array(slots.size);
+		for (Slot slot : skeleton.drawOrder)
+			drawOrder.add(slots.get(slot.data.index));
+
+		ikConstraints = new Array(skeleton.ikConstraints.size);
+		for (IkConstraint ikConstraint : skeleton.ikConstraints)
+			ikConstraints.add(new IkConstraint(ikConstraint, this));
+
+		transformConstraints = new Array(skeleton.transformConstraints.size);
+		for (TransformConstraint transformConstraint : skeleton.transformConstraints)
+			transformConstraints.add(new TransformConstraint(transformConstraint, this));
+
+		pathConstraints = new Array(skeleton.pathConstraints.size);
+		for (PathConstraint pathConstraint : skeleton.pathConstraints)
+			pathConstraints.add(new PathConstraint(pathConstraint, this));
+
+		skin = skeleton.skin;
+		color = new Color(skeleton.color);
+		time = skeleton.time;
+		scaleX = skeleton.scaleX;
+		scaleY = skeleton.scaleY;
+
+		updateCache();
+	}
+
+	/** Caches information about bones and constraints. Must be called if the {@link #getSkin()} is modified or if bones,
+	 * constraints, or weighted path attachments are added or removed. */
+	public void updateCache () {
+		Array<Updatable> updateCache = this.updateCache;
+		updateCache.clear();
+
+		int boneCount = bones.size;
+		Object[] bones = this.bones.items;
+		for (int i = 0; i < boneCount; i++) {
+			Bone bone = (Bone)bones[i];
+			bone.sorted = bone.data.skinRequired;
+			bone.active = !bone.sorted;
+		}
+		if (skin != null) {
+			Object[] skinBones = skin.bones.items;
+			for (int i = 0, n = skin.bones.size; i < n; i++) {
+				Bone bone = (Bone)bones[((BoneData)skinBones[i]).index];
+				do {
+					bone.sorted = false;
+					bone.active = true;
+					bone = bone.parent;
+				} while (bone != null);
+			}
+		}
+
+		int ikCount = ikConstraints.size, transformCount = transformConstraints.size, pathCount = pathConstraints.size;
+		Object[] ikConstraints = this.ikConstraints.items;
+		Object[] transformConstraints = this.transformConstraints.items;
+		Object[] pathConstraints = this.pathConstraints.items;
+		int constraintCount = ikCount + transformCount + pathCount;
+		outer:
+		for (int i = 0; i < constraintCount; i++) {
+			for (int ii = 0; ii < ikCount; ii++) {
+				IkConstraint constraint = (IkConstraint)ikConstraints[ii];
+				if (constraint.data.order == i) {
+					sortIkConstraint(constraint);
+					continue outer;
+				}
+			}
+			for (int ii = 0; ii < transformCount; ii++) {
+				TransformConstraint constraint = (TransformConstraint)transformConstraints[ii];
+				if (constraint.data.order == i) {
+					sortTransformConstraint(constraint);
+					continue outer;
+				}
+			}
+			for (int ii = 0; ii < pathCount; ii++) {
+				PathConstraint constraint = (PathConstraint)pathConstraints[ii];
+				if (constraint.data.order == i) {
+					sortPathConstraint(constraint);
+					continue outer;
+				}
+			}
+		}
+
+		for (int i = 0; i < boneCount; i++)
+			sortBone((Bone)bones[i]);
+	}
+
+	private void sortIkConstraint (IkConstraint constraint) {
+		constraint.active = constraint.target.active
+			&& (!constraint.data.skinRequired || (skin != null && skin.constraints.contains(constraint.data, true)));
+		if (!constraint.active) return;
+
+		Bone target = constraint.target;
+		sortBone(target);
+
+		Array<Bone> constrained = constraint.bones;
+		Bone parent = constrained.first();
+		sortBone(parent);
+		if (constrained.size == 1) {
+			updateCache.add(constraint);
+			sortReset(parent.children);
+		} else {
+			Bone child = constrained.peek();
+			sortBone(child);
+
+			updateCache.add(constraint);
+
+			sortReset(parent.children);
+			child.sorted = true;
+		}
+	}
+
+	private void sortPathConstraint (PathConstraint constraint) {
+		constraint.active = constraint.target.bone.active
+			&& (!constraint.data.skinRequired || (skin != null && skin.constraints.contains(constraint.data, true)));
+		if (!constraint.active) return;
+
+		Slot slot = constraint.target;
+		int slotIndex = slot.getData().index;
+		Bone slotBone = slot.bone;
+		if (skin != null) sortPathConstraintAttachment(skin, slotIndex, slotBone);
+		if (data.defaultSkin != null && data.defaultSkin != skin)
+			sortPathConstraintAttachment(data.defaultSkin, slotIndex, slotBone);
+
+		Attachment attachment = slot.attachment;
+		if (attachment instanceof PathAttachment) sortPathConstraintAttachment(attachment, slotBone);
+
+		Object[] constrained = constraint.bones.items;
+		int boneCount = constraint.bones.size;
+		for (int i = 0; i < boneCount; i++)
+			sortBone((Bone)constrained[i]);
+
+		updateCache.add(constraint);
+
+		for (int i = 0; i < boneCount; i++)
+			sortReset(((Bone)constrained[i]).children);
+		for (int i = 0; i < boneCount; i++)
+			((Bone)constrained[i]).sorted = true;
+	}
+
+	private void sortTransformConstraint (TransformConstraint constraint) {
+		constraint.active = constraint.target.active
+			&& (!constraint.data.skinRequired || (skin != null && skin.constraints.contains(constraint.data, true)));
+		if (!constraint.active) return;
+
+		sortBone(constraint.target);
+
+		Object[] constrained = constraint.bones.items;
+		int boneCount = constraint.bones.size;
+		if (constraint.data.local) {
+			for (int i = 0; i < boneCount; i++) {
+				Bone child = (Bone)constrained[i];
+				sortBone(child.parent);
+				sortBone(child);
+			}
+		} else {
+			for (int i = 0; i < boneCount; i++)
+				sortBone((Bone)constrained[i]);
+		}
+
+		updateCache.add(constraint);
+
+		for (int i = 0; i < boneCount; i++)
+			sortReset(((Bone)constrained[i]).children);
+		for (int i = 0; i < boneCount; i++)
+			((Bone)constrained[i]).sorted = true;
+	}
+
+	private void sortPathConstraintAttachment (Skin skin, int slotIndex, Bone slotBone) {
+		Object[] entries = skin.attachments.orderedItems().items;
+		for (int i = 0, n = skin.attachments.size; i < n; i++) {
+			SkinEntry entry = (SkinEntry)entries[i];
+			if (entry.slotIndex == slotIndex) sortPathConstraintAttachment(entry.attachment, slotBone);
+		}
+	}
+
+	private void sortPathConstraintAttachment (Attachment attachment, Bone slotBone) {
+		if (!(attachment instanceof PathAttachment)) return;
+		int[] pathBones = ((PathAttachment)attachment).getBones();
+		if (pathBones == null)
+			sortBone(slotBone);
+		else {
+			Object[] bones = this.bones.items;
+			for (int i = 0, n = pathBones.length; i < n;) {
+				int nn = pathBones[i++];
+				nn += i;
+				while (i < nn)
+					sortBone((Bone)bones[pathBones[i++]]);
+			}
+		}
+	}
+
+	private void sortBone (Bone bone) {
+		if (bone.sorted) return;
+		Bone parent = bone.parent;
+		if (parent != null) sortBone(parent);
+		bone.sorted = true;
+		updateCache.add(bone);
+	}
+
+	private void sortReset (Array<Bone> bones) {
+		Object[] items = bones.items;
+		for (int i = 0, n = bones.size; i < n; i++) {
+			Bone bone = (Bone)items[i];
+			if (!bone.active) continue;
+			if (bone.sorted) sortReset(bone.children);
+			bone.sorted = false;
+		}
+	}
+
+	/** Updates the world transform for each bone and applies all constraints.
+	 * <p>
+	 * See <a href="http://esotericsoftware.com/spine-runtime-skeletons#World-transforms">World transforms</a> in the Spine
+	 * Runtimes Guide. */
+	public void updateWorldTransform () {
+		Object[] bones = this.bones.items;
+		for (int i = 0, n = this.bones.size; i < n; i++) {
+			Bone bone = (Bone)bones[i];
+			bone.ax = bone.x;
+			bone.ay = bone.y;
+			bone.arotation = bone.rotation;
+			bone.ascaleX = bone.scaleX;
+			bone.ascaleY = bone.scaleY;
+			bone.ashearX = bone.shearX;
+			bone.ashearY = bone.shearY;
+		}
+
+		Object[] updateCache = this.updateCache.items;
+		for (int i = 0, n = this.updateCache.size; i < n; i++)
+			((Updatable)updateCache[i]).update();
+	}
+
+	/** Temporarily sets the root bone as a child of the specified bone, then updates the world transform for each bone and applies
+	 * all constraints.
+	 * <p>
+	 * See <a href="http://esotericsoftware.com/spine-runtime-skeletons#World-transforms">World transforms</a> in the Spine
+	 * Runtimes Guide. */
+	public void updateWorldTransform (Bone parent) {
+		if (parent == null) throw new IllegalArgumentException("parent cannot be null.");
+
+		// Apply the parent bone transform to the root bone. The root bone always inherits scale, rotation and reflection.
+		Bone rootBone = getRootBone();
+		float pa = parent.a, pb = parent.b, pc = parent.c, pd = parent.d;
+		rootBone.worldX = pa * x + pb * y + parent.worldX;
+		rootBone.worldY = pc * x + pd * y + parent.worldY;
+
+		float rotationY = rootBone.rotation + 90 + rootBone.shearY;
+		float la = cosDeg(rootBone.rotation + rootBone.shearX) * rootBone.scaleX;
+		float lb = cosDeg(rotationY) * rootBone.scaleY;
+		float lc = sinDeg(rootBone.rotation + rootBone.shearX) * rootBone.scaleX;
+		float ld = sinDeg(rotationY) * rootBone.scaleY;
+		rootBone.a = (pa * la + pb * lc) * scaleX;
+		rootBone.b = (pa * lb + pb * ld) * scaleX;
+		rootBone.c = (pc * la + pd * lc) * scaleY;
+		rootBone.d = (pc * lb + pd * ld) * scaleY;
+
+		// Update everything except root bone.
+		Object[] updateCache = this.updateCache.items;
+		for (int i = 0, n = this.updateCache.size; i < n; i++) {
+			Updatable updatable = (Updatable)updateCache[i];
+			if (updatable != rootBone) updatable.update();
+		}
+	}
+
+	/** Sets the bones, constraints, slots, and draw order to their setup pose values. */
+	public void setToSetupPose () {
+		setBonesToSetupPose();
+		setSlotsToSetupPose();
+	}
+
+	/** Sets the bones and constraints to their setup pose values. */
+	public void setBonesToSetupPose () {
+		Object[] bones = this.bones.items;
+		for (int i = 0, n = this.bones.size; i < n; i++)
+			((Bone)bones[i]).setToSetupPose();
+
+		Object[] ikConstraints = this.ikConstraints.items;
+		for (int i = 0, n = this.ikConstraints.size; i < n; i++) {
+			IkConstraint constraint = (IkConstraint)ikConstraints[i];
+			constraint.mix = constraint.data.mix;
+			constraint.softness = constraint.data.softness;
+			constraint.bendDirection = constraint.data.bendDirection;
+			constraint.compress = constraint.data.compress;
+			constraint.stretch = constraint.data.stretch;
+		}
+
+		Object[] transformConstraints = this.transformConstraints.items;
+		for (int i = 0, n = this.transformConstraints.size; i < n; i++) {
+			TransformConstraint constraint = (TransformConstraint)transformConstraints[i];
+			TransformConstraintData data = constraint.data;
+			constraint.mixRotate = data.mixRotate;
+			constraint.mixX = data.mixX;
+			constraint.mixY = data.mixY;
+			constraint.mixScaleX = data.mixScaleX;
+			constraint.mixScaleY = data.mixScaleY;
+			constraint.mixShearY = data.mixShearY;
+		}
+
+		Object[] pathConstraints = this.pathConstraints.items;
+		for (int i = 0, n = this.pathConstraints.size; i < n; i++) {
+			PathConstraint constraint = (PathConstraint)pathConstraints[i];
+			PathConstraintData data = constraint.data;
+			constraint.position = data.position;
+			constraint.spacing = data.spacing;
+			constraint.mixRotate = data.mixRotate;
+			constraint.mixX = data.mixX;
+			constraint.mixY = data.mixY;
+		}
+	}
+
+	/** Sets the slots and draw order to their setup pose values. */
+	public void setSlotsToSetupPose () {
+		Object[] slots = this.slots.items;
+		int n = this.slots.size;
+		arraycopy(slots, 0, drawOrder.items, 0, n);
+		for (int i = 0; i < n; i++)
+			((Slot)slots[i]).setToSetupPose();
+	}
+
+	/** The skeleton's setup pose data. */
+	public SkeletonData getData () {
+		return data;
+	}
+
+	/** The skeleton's bones, sorted parent first. The root bone is always the first bone. */
+	public Array<Bone> getBones () {
+		return bones;
+	}
+
+	/** The list of bones and constraints, sorted in the order they should be updated, as computed by {@link #updateCache()}. */
+	public Array<Updatable> getUpdateCache () {
+		return updateCache;
+	}
+
+	/** Returns the root bone, or null if the skeleton has no bones. */
+	public Bone getRootBone () {
+		return bones.size == 0 ? null : bones.first();
+	}
+
+	/** Finds a bone by comparing each bone's name. It is more efficient to cache the results of this method than to call it
+	 * repeatedly. */
+	public @Null Bone findBone (String boneName) {
+		if (boneName == null) throw new IllegalArgumentException("boneName cannot be null.");
+		Object[] bones = this.bones.items;
+		for (int i = 0, n = this.bones.size; i < n; i++) {
+			Bone bone = (Bone)bones[i];
+			if (bone.data.name.equals(boneName)) return bone;
+		}
+		return null;
+	}
+
+	/** The skeleton's slots. */
+	public Array<Slot> getSlots () {
+		return slots;
+	}
+
+	/** Finds a slot by comparing each slot's name. It is more efficient to cache the results of this method than to call it
+	 * repeatedly. */
+	public @Null Slot findSlot (String slotName) {
+		if (slotName == null) throw new IllegalArgumentException("slotName cannot be null.");
+		Object[] slots = this.slots.items;
+		for (int i = 0, n = this.slots.size; i < n; i++) {
+			Slot slot = (Slot)slots[i];
+			if (slot.data.name.equals(slotName)) return slot;
+		}
+		return null;
+	}
+
+	/** The skeleton's slots in the order they should be drawn. The returned array may be modified to change the draw order. */
+	public Array<Slot> getDrawOrder () {
+		return drawOrder;
+	}
+
+	public void setDrawOrder (Array<Slot> drawOrder) {
+		if (drawOrder == null) throw new IllegalArgumentException("drawOrder cannot be null.");
+		this.drawOrder = drawOrder;
+	}
+
+	/** The skeleton's current skin. */
+	public @Null Skin getSkin () {
+		return skin;
+	}
+
+	/** Sets a skin by name.
+	 * <p>
+	 * See {@link #setSkin(Skin)}. */
+	public void setSkin (String skinName) {
+		Skin skin = data.findSkin(skinName);
+		if (skin == null) throw new IllegalArgumentException("Skin not found: " + skinName);
+		setSkin(skin);
+	}
+
+	/** Sets the skin used to look up attachments before looking in the {@link SkeletonData#getDefaultSkin() default skin}. If the
+	 * skin is changed, {@link #updateCache()} is called.
+	 * <p>
+	 * Attachments from the new skin are attached if the corresponding attachment from the old skin was attached. If there was no
+	 * old skin, each slot's setup mode attachment is attached from the new skin.
+	 * <p>
+	 * After changing the skin, the visible attachments can be reset to those attached in the setup pose by calling
+	 * {@link #setSlotsToSetupPose()}. Also, often {@link AnimationState#apply(Skeleton)} is called before the next time the
+	 * skeleton is rendered to allow any attachment keys in the current animation(s) to hide or show attachments from the new
+	 * skin. */
+	public void setSkin (@Null Skin newSkin) {
+		if (newSkin == skin) return;
+		if (newSkin != null) {
+			if (skin != null)
+				newSkin.attachAll(this, skin);
+			else {
+				Object[] slots = this.slots.items;
+				for (int i = 0, n = this.slots.size; i < n; i++) {
+					Slot slot = (Slot)slots[i];
+					String name = slot.data.attachmentName;
+					if (name != null) {
+						Attachment attachment = newSkin.getAttachment(i, name);
+						if (attachment != null) slot.setAttachment(attachment);
+					}
+				}
+			}
+		}
+		skin = newSkin;
+		updateCache();
+	}
+
+	/** Finds an attachment by looking in the {@link #skin} and {@link SkeletonData#defaultSkin} using the slot name and attachment
+	 * name.
+	 * <p>
+	 * See {@link #getAttachment(int, String)}. */
+	public @Null Attachment getAttachment (String slotName, String attachmentName) {
+		SlotData slot = data.findSlot(slotName);
+		if (slot == null) throw new IllegalArgumentException("Slot not found: " + slotName);
+		return getAttachment(slot.getIndex(), attachmentName);
+	}
+
+	/** Finds an attachment by looking in the {@link #skin} and {@link SkeletonData#defaultSkin} using the slot index and
+	 * attachment name. First the skin is checked and if the attachment was not found, the default skin is checked.
+	 * <p>
+	 * See <a href="http://esotericsoftware.com/spine-runtime-skins">Runtime skins</a> in the Spine Runtimes Guide. */
+	public @Null Attachment getAttachment (int slotIndex, String attachmentName) {
+		if (attachmentName == null) throw new IllegalArgumentException("attachmentName cannot be null.");
+		if (skin != null) {
+			Attachment attachment = skin.getAttachment(slotIndex, attachmentName);
+			if (attachment != null) return attachment;
+		}
+		if (data.defaultSkin != null) return data.defaultSkin.getAttachment(slotIndex, attachmentName);
+		return null;
+	}
+
+	/** A convenience method to set an attachment by finding the slot with {@link #findSlot(String)}, finding the attachment with
+	 * {@link #getAttachment(int, String)}, then setting the slot's {@link Slot#attachment}.
+	 * @param attachmentName May be null to clear the slot's attachment. */
+	public void setAttachment (String slotName, @Null String attachmentName) {
+		if (slotName == null) throw new IllegalArgumentException("slotName cannot be null.");
+		Slot slot = findSlot(slotName);
+		if (slot == null) throw new IllegalArgumentException("Slot not found: " + slotName);
+		Attachment attachment = null;
+		if (attachmentName != null) {
+			attachment = getAttachment(slot.data.index, attachmentName);
+			if (attachment == null)
+				throw new IllegalArgumentException("Attachment not found: " + attachmentName + ", for slot: " + slotName);
+		}
+		slot.setAttachment(attachment);
+	}
+
+	/** The skeleton's IK constraints. */
+	public Array<IkConstraint> getIkConstraints () {
+		return ikConstraints;
+	}
+
+	/** Finds an IK constraint by comparing each IK constraint's name. It is more efficient to cache the results of this method
+	 * than to call it repeatedly. */
+	public @Null IkConstraint findIkConstraint (String constraintName) {
+		if (constraintName == null) throw new IllegalArgumentException("constraintName cannot be null.");
+		Object[] ikConstraints = this.ikConstraints.items;
+		for (int i = 0, n = this.ikConstraints.size; i < n; i++) {
+			IkConstraint ikConstraint = (IkConstraint)ikConstraints[i];
+			if (ikConstraint.data.name.equals(constraintName)) return ikConstraint;
+		}
+		return null;
+	}
+
+	/** The skeleton's transform constraints. */
+	public Array<TransformConstraint> getTransformConstraints () {
+		return transformConstraints;
+	}
+
+	/** Finds a transform constraint by comparing each transform constraint's name. It is more efficient to cache the results of
+	 * this method than to call it repeatedly. */
+	public @Null TransformConstraint findTransformConstraint (String constraintName) {
+		if (constraintName == null) throw new IllegalArgumentException("constraintName cannot be null.");
+		Object[] transformConstraints = this.transformConstraints.items;
+		for (int i = 0, n = this.transformConstraints.size; i < n; i++) {
+			TransformConstraint constraint = (TransformConstraint)transformConstraints[i];
+			if (constraint.data.name.equals(constraintName)) return constraint;
+		}
+		return null;
+	}
+
+	/** The skeleton's path constraints. */
+	public Array<PathConstraint> getPathConstraints () {
+		return pathConstraints;
+	}
+
+	/** Finds a path constraint by comparing each path constraint's name. It is more efficient to cache the results of this method
+	 * than to call it repeatedly. */
+	public @Null PathConstraint findPathConstraint (String constraintName) {
+		if (constraintName == null) throw new IllegalArgumentException("constraintName cannot be null.");
+		Object[] pathConstraints = this.pathConstraints.items;
+		for (int i = 0, n = this.pathConstraints.size; i < n; i++) {
+			PathConstraint constraint = (PathConstraint)pathConstraints[i];
+			if (constraint.data.name.equals(constraintName)) return constraint;
+		}
+		return null;
+	}
+
+	/** Returns the axis aligned bounding box (AABB) of the region and mesh attachments for the current pose.
+	 * @param offset An output value, the distance from the skeleton origin to the bottom left corner of the AABB.
+	 * @param size An output value, the width and height of the AABB.
+	 * @param temp Working memory to temporarily store attachments' computed world vertices. */
+	public void getBounds (Vector2 offset, Vector2 size, FloatArray temp) {
+		if (offset == null) throw new IllegalArgumentException("offset cannot be null.");
+		if (size == null) throw new IllegalArgumentException("size cannot be null.");
+		if (temp == null) throw new IllegalArgumentException("temp cannot be null.");
+		Object[] drawOrder = this.drawOrder.items;
+		float minX = Integer.MAX_VALUE, minY = Integer.MAX_VALUE, maxX = Integer.MIN_VALUE, maxY = Integer.MIN_VALUE;
+		for (int i = 0, n = this.drawOrder.size; i < n; i++) {
+			Slot slot = (Slot)drawOrder[i];
+			if (!slot.bone.active) continue;
+			int verticesLength = 0;
+			float[] vertices = null;
+			Attachment attachment = slot.attachment;
+			if (attachment instanceof RegionAttachment) {
+				verticesLength = 8;
+				vertices = temp.setSize(8);
+				((RegionAttachment)attachment).computeWorldVertices(slot.getBone(), vertices, 0, 2);
+			} else if (attachment instanceof MeshAttachment) {
+				MeshAttachment mesh = (MeshAttachment)attachment;
+				verticesLength = mesh.getWorldVerticesLength();
+				vertices = temp.setSize(verticesLength);
+				mesh.computeWorldVertices(slot, 0, verticesLength, vertices, 0, 2);
+			}
+			if (vertices != null) {
+				for (int ii = 0; ii < verticesLength; ii += 2) {
+					float x = vertices[ii], y = vertices[ii + 1];
+					minX = Math.min(minX, x);
+					minY = Math.min(minY, y);
+					maxX = Math.max(maxX, x);
+					maxY = Math.max(maxY, y);
+				}
+			}
+		}
+		offset.set(minX, minY);
+		size.set(maxX - minX, maxY - minY);
+	}
+
+	/** The color to tint all the skeleton's attachments. */
+	public Color getColor () {
+		return color;
+	}
+
+	/** A convenience method for setting the skeleton color. The color can also be set by modifying {@link #getColor()}. */
+	public void setColor (Color color) {
+		if (color == null) throw new IllegalArgumentException("color cannot be null.");
+		this.color.set(color);
+	}
+
+	/** A convenience method for setting the skeleton color. The color can also be set by modifying {@link #getColor()}. */
+	public void setColor (float r, float g, float b, float a) {
+		color.set(r, g, b, a);
+	}
+
+	/** Scales the entire skeleton on the X axis. This affects all bones, even if the bone's transform mode disallows scale
+	 * inheritance. */
+	public float getScaleX () {
+		return scaleX;
+	}
+
+	public void setScaleX (float scaleX) {
+		this.scaleX = scaleX;
+	}
+
+	/** Scales the entire skeleton on the Y axis. This affects all bones, even if the bone's transform mode disallows scale
+	 * inheritance. */
+	public float getScaleY () {
+		return scaleY;
+	}
+
+	public void setScaleY (float scaleY) {
+		this.scaleY = scaleY;
+	}
+
+	public void setScale (float scaleX, float scaleY) {
+		this.scaleX = scaleX;
+		this.scaleY = scaleY;
+	}
+
+	/** Sets the skeleton X position, which is added to the root bone worldX position. */
+	public float getX () {
+		return x;
+	}
+
+	public void setX (float x) {
+		this.x = x;
+	}
+
+	/** Sets the skeleton Y position, which is added to the root bone worldY position. */
+	public float getY () {
+		return y;
+	}
+
+	public void setY (float y) {
+		this.y = y;
+	}
+
+	/** Sets the skeleton X and Y position, which is added to the root bone worldX and worldY position. */
+	public void setPosition (float x, float y) {
+		this.x = x;
+		this.y = y;
+	}
+
+	/** Returns the skeleton's time. This can be used for tracking, such as with Slot {@link Slot#getAttachmentTime()}.
+	 * <p>
+	 * See {@link #update(float)}. */
+	public float getTime () {
+		return time;
+	}
+
+	public void setTime (float time) {
+		this.time = time;
+	}
+
+	/** Increments the skeleton's {@link #time}. */
+	public void update (float delta) {
+		time += delta;
+	}
+
+	public String toString () {
+		return data.name != null ? data.name : super.toString();
+	}
+}

+ 1110 - 1110
spine-libgdx/spine-libgdx/src/com/esotericsoftware/spine/SkeletonBinary.java

@@ -1,1110 +1,1110 @@
-/******************************************************************************
- * Spine Runtimes License Agreement
- * Last updated January 1, 2020. Replaces all prior versions.
- *
- * Copyright (c) 2013-2020, Esoteric Software LLC
- *
- * Integration of the Spine Runtimes into software or otherwise creating
- * derivative works of the Spine Runtimes is permitted under the terms and
- * conditions of Section 2 of the Spine Editor License Agreement:
- * http://esotericsoftware.com/spine-editor-license
- *
- * Otherwise, it is permitted to integrate the Spine Runtimes into software
- * or otherwise create derivative works of the Spine Runtimes (collectively,
- * "Products"), provided that each user of the Products must obtain their own
- * Spine Editor license and redistribution of the Products in any form must
- * include this license and copyright notice.
- *
- * THE SPINE RUNTIMES ARE PROVIDED BY ESOTERIC SOFTWARE LLC "AS IS" AND ANY
- * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
- * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
- * DISCLAIMED. IN NO EVENT SHALL ESOTERIC SOFTWARE LLC BE LIABLE FOR ANY
- * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
- * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES,
- * BUSINESS INTERRUPTION, OR LOSS OF USE, DATA, OR PROFITS) HOWEVER CAUSED AND
- * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
- * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
- * THE SPINE RUNTIMES, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
- *****************************************************************************/
-
-package com.esotericsoftware.spine;
-
-import java.io.EOFException;
-import java.io.IOException;
-import java.io.InputStream;
-
-import com.badlogic.gdx.files.FileHandle;
-import com.badlogic.gdx.graphics.Color;
-import com.badlogic.gdx.graphics.g2d.TextureAtlas;
-import com.badlogic.gdx.utils.Array;
-import com.badlogic.gdx.utils.DataInput;
-import com.badlogic.gdx.utils.FloatArray;
-import com.badlogic.gdx.utils.IntArray;
-import com.badlogic.gdx.utils.Null;
-import com.badlogic.gdx.utils.SerializationException;
-
-import com.esotericsoftware.spine.Animation.AlphaTimeline;
-import com.esotericsoftware.spine.Animation.AttachmentTimeline;
-import com.esotericsoftware.spine.Animation.CurveTimeline;
-import com.esotericsoftware.spine.Animation.CurveTimeline1;
-import com.esotericsoftware.spine.Animation.CurveTimeline2;
-import com.esotericsoftware.spine.Animation.DeformTimeline;
-import com.esotericsoftware.spine.Animation.DrawOrderTimeline;
-import com.esotericsoftware.spine.Animation.EventTimeline;
-import com.esotericsoftware.spine.Animation.IkConstraintTimeline;
-import com.esotericsoftware.spine.Animation.PathConstraintMixTimeline;
-import com.esotericsoftware.spine.Animation.PathConstraintPositionTimeline;
-import com.esotericsoftware.spine.Animation.PathConstraintSpacingTimeline;
-import com.esotericsoftware.spine.Animation.RGB2Timeline;
-import com.esotericsoftware.spine.Animation.RGBA2Timeline;
-import com.esotericsoftware.spine.Animation.RGBATimeline;
-import com.esotericsoftware.spine.Animation.RGBTimeline;
-import com.esotericsoftware.spine.Animation.RotateTimeline;
-import com.esotericsoftware.spine.Animation.ScaleTimeline;
-import com.esotericsoftware.spine.Animation.ScaleXTimeline;
-import com.esotericsoftware.spine.Animation.ScaleYTimeline;
-import com.esotericsoftware.spine.Animation.ShearTimeline;
-import com.esotericsoftware.spine.Animation.ShearXTimeline;
-import com.esotericsoftware.spine.Animation.ShearYTimeline;
-import com.esotericsoftware.spine.Animation.Timeline;
-import com.esotericsoftware.spine.Animation.TransformConstraintTimeline;
-import com.esotericsoftware.spine.Animation.TranslateTimeline;
-import com.esotericsoftware.spine.Animation.TranslateXTimeline;
-import com.esotericsoftware.spine.Animation.TranslateYTimeline;
-import com.esotericsoftware.spine.BoneData.TransformMode;
-import com.esotericsoftware.spine.PathConstraintData.PositionMode;
-import com.esotericsoftware.spine.PathConstraintData.RotateMode;
-import com.esotericsoftware.spine.PathConstraintData.SpacingMode;
-import com.esotericsoftware.spine.SkeletonJson.LinkedMesh;
-import com.esotericsoftware.spine.attachments.Attachment;
-import com.esotericsoftware.spine.attachments.AttachmentLoader;
-import com.esotericsoftware.spine.attachments.AttachmentType;
-import com.esotericsoftware.spine.attachments.BoundingBoxAttachment;
-import com.esotericsoftware.spine.attachments.ClippingAttachment;
-import com.esotericsoftware.spine.attachments.MeshAttachment;
-import com.esotericsoftware.spine.attachments.PathAttachment;
-import com.esotericsoftware.spine.attachments.PointAttachment;
-import com.esotericsoftware.spine.attachments.RegionAttachment;
-import com.esotericsoftware.spine.attachments.VertexAttachment;
-
-/** Loads skeleton data in the Spine binary format.
- * <p>
- * See <a href="http://esotericsoftware.com/spine-binary-format">Spine binary format</a> and
- * <a href="http://esotericsoftware.com/spine-loading-skeleton-data#JSON-and-binary-data">JSON and binary data</a> in the Spine
- * Runtimes Guide. */
-public class SkeletonBinary extends SkeletonLoader {
-	static public final int BONE_ROTATE = 0;
-	static public final int BONE_TRANSLATE = 1;
-	static public final int BONE_TRANSLATEX = 2;
-	static public final int BONE_TRANSLATEY = 3;
-	static public final int BONE_SCALE = 4;
-	static public final int BONE_SCALEX = 5;
-	static public final int BONE_SCALEY = 6;
-	static public final int BONE_SHEAR = 7;
-	static public final int BONE_SHEARX = 8;
-	static public final int BONE_SHEARY = 9;
-
-	static public final int SLOT_ATTACHMENT = 0;
-	static public final int SLOT_RGBA = 1;
-	static public final int SLOT_RGB = 2;
-	static public final int SLOT_RGBA2 = 3;
-	static public final int SLOT_RGB2 = 4;
-	static public final int SLOT_ALPHA = 5;
-
-	static public final int PATH_POSITION = 0;
-	static public final int PATH_SPACING = 1;
-	static public final int PATH_MIX = 2;
-
-	static public final int CURVE_LINEAR = 0;
-	static public final int CURVE_STEPPED = 1;
-	static public final int CURVE_BEZIER = 2;
-
-	public SkeletonBinary (AttachmentLoader attachmentLoader) {
-		super(attachmentLoader);
-	}
-
-	public SkeletonBinary (TextureAtlas atlas) {
-		super(atlas);
-	}
-
-	public SkeletonData readSkeletonData (FileHandle file) {
-		if (file == null) throw new IllegalArgumentException("file cannot be null.");
-		SkeletonData skeletonData = readSkeletonData(file.read());
-		skeletonData.name = file.nameWithoutExtension();
-		return skeletonData;
-	}
-
-	public SkeletonData readSkeletonData (InputStream dataInput) {
-		if (dataInput == null) throw new IllegalArgumentException("dataInput cannot be null.");
-
-		float scale = this.scale;
-
-		SkeletonInput input = new SkeletonInput(dataInput);
-		SkeletonData skeletonData = new SkeletonData();
-		try {
-			long hash = input.readLong();
-			skeletonData.hash = hash == 0 ? null : Long.toString(hash);
-			skeletonData.version = input.readString();
-			if (skeletonData.version.isEmpty()) skeletonData.version = null;
-			skeletonData.x = input.readFloat();
-			skeletonData.y = input.readFloat();
-			skeletonData.width = input.readFloat();
-			skeletonData.height = input.readFloat();
-
-			boolean nonessential = input.readBoolean();
-			if (nonessential) {
-				skeletonData.fps = input.readFloat();
-
-				skeletonData.imagesPath = input.readString();
-				if (skeletonData.imagesPath.isEmpty()) skeletonData.imagesPath = null;
-
-				skeletonData.audioPath = input.readString();
-				if (skeletonData.audioPath.isEmpty()) skeletonData.audioPath = null;
-			}
-
-			int n;
-			Object[] o;
-
-			// Strings.
-			o = input.strings = new String[n = input.readInt(true)];
-			for (int i = 0; i < n; i++)
-				o[i] = input.readString();
-
-			// Bones.
-			Object[] bones = skeletonData.bones.setSize(n = input.readInt(true));
-			for (int i = 0; i < n; i++) {
-				String name = input.readString();
-				BoneData parent = i == 0 ? null : (BoneData)bones[input.readInt(true)];
-				BoneData data = new BoneData(i, name, parent);
-				data.rotation = input.readFloat();
-				data.x = input.readFloat() * scale;
-				data.y = input.readFloat() * scale;
-				data.scaleX = input.readFloat();
-				data.scaleY = input.readFloat();
-				data.shearX = input.readFloat();
-				data.shearY = input.readFloat();
-				data.length = input.readFloat() * scale;
-				data.transformMode = TransformMode.values[input.readInt(true)];
-				data.skinRequired = input.readBoolean();
-				if (nonessential) Color.rgba8888ToColor(data.color, input.readInt());
-				bones[i] = data;
-			}
-
-			// Slots.
-			Object[] slots = skeletonData.slots.setSize(n = input.readInt(true));
-			for (int i = 0; i < n; i++) {
-				String slotName = input.readString();
-				BoneData boneData = (BoneData)bones[input.readInt(true)];
-				SlotData data = new SlotData(i, slotName, boneData);
-				Color.rgba8888ToColor(data.color, input.readInt());
-
-				int darkColor = input.readInt();
-				if (darkColor != -1) Color.rgb888ToColor(data.darkColor = new Color(), darkColor);
-
-				data.attachmentName = input.readStringRef();
-				data.blendMode = BlendMode.values[input.readInt(true)];
-				slots[i] = data;
-			}
-
-			// IK constraints.
-			o = skeletonData.ikConstraints.setSize(n = input.readInt(true));
-			for (int i = 0, nn; i < n; i++) {
-				IkConstraintData data = new IkConstraintData(input.readString());
-				data.order = input.readInt(true);
-				data.skinRequired = input.readBoolean();
-				Object[] constraintBones = data.bones.setSize(nn = input.readInt(true));
-				for (int ii = 0; ii < nn; ii++)
-					constraintBones[ii] = bones[input.readInt(true)];
-				data.target = (BoneData)bones[input.readInt(true)];
-				data.mix = input.readFloat();
-				data.softness = input.readFloat() * scale;
-				data.bendDirection = input.readByte();
-				data.compress = input.readBoolean();
-				data.stretch = input.readBoolean();
-				data.uniform = input.readBoolean();
-				o[i] = data;
-			}
-
-			// Transform constraints.
-			o = skeletonData.transformConstraints.setSize(n = input.readInt(true));
-			for (int i = 0, nn; i < n; i++) {
-				TransformConstraintData data = new TransformConstraintData(input.readString());
-				data.order = input.readInt(true);
-				data.skinRequired = input.readBoolean();
-				Object[] constraintBones = data.bones.setSize(nn = input.readInt(true));
-				for (int ii = 0; ii < nn; ii++)
-					constraintBones[ii] = bones[input.readInt(true)];
-				data.target = (BoneData)bones[input.readInt(true)];
-				data.local = input.readBoolean();
-				data.relative = input.readBoolean();
-				data.offsetRotation = input.readFloat();
-				data.offsetX = input.readFloat() * scale;
-				data.offsetY = input.readFloat() * scale;
-				data.offsetScaleX = input.readFloat();
-				data.offsetScaleY = input.readFloat();
-				data.offsetShearY = input.readFloat();
-				data.mixRotate = input.readFloat();
-				data.mixX = input.readFloat();
-				data.mixY = input.readFloat();
-				data.mixScaleX = input.readFloat();
-				data.mixScaleY = input.readFloat();
-				data.mixShearY = input.readFloat();
-				o[i] = data;
-			}
-
-			// Path constraints.
-			o = skeletonData.pathConstraints.setSize(n = input.readInt(true));
-			for (int i = 0, nn; i < n; i++) {
-				PathConstraintData data = new PathConstraintData(input.readString());
-				data.order = input.readInt(true);
-				data.skinRequired = input.readBoolean();
-				Object[] constraintBones = data.bones.setSize(nn = input.readInt(true));
-				for (int ii = 0; ii < nn; ii++)
-					constraintBones[ii] = bones[input.readInt(true)];
-				data.target = (SlotData)slots[input.readInt(true)];
-				data.positionMode = PositionMode.values[input.readInt(true)];
-				data.spacingMode = SpacingMode.values[input.readInt(true)];
-				data.rotateMode = RotateMode.values[input.readInt(true)];
-				data.offsetRotation = input.readFloat();
-				data.position = input.readFloat();
-				if (data.positionMode == PositionMode.fixed) data.position *= scale;
-				data.spacing = input.readFloat();
-				if (data.spacingMode == SpacingMode.length || data.spacingMode == SpacingMode.fixed) data.spacing *= scale;
-				data.mixRotate = input.readFloat();
-				data.mixX = input.readFloat();
-				data.mixY = input.readFloat();
-				o[i] = data;
-			}
-
-			// Default skin.
-			Skin defaultSkin = readSkin(input, skeletonData, true, nonessential);
-			if (defaultSkin != null) {
-				skeletonData.defaultSkin = defaultSkin;
-				skeletonData.skins.add(defaultSkin);
-			}
-
-			// Skins.
-			{
-				int i = skeletonData.skins.size;
-				o = skeletonData.skins.setSize(n = i + input.readInt(true));
-				for (; i < n; i++)
-					o[i] = readSkin(input, skeletonData, false, nonessential);
-			}
-
-			// Linked meshes.
-			n = linkedMeshes.size;
-			Object[] items = linkedMeshes.items;
-			for (int i = 0; i < n; i++) {
-				LinkedMesh linkedMesh = (LinkedMesh)items[i];
-				Skin skin = linkedMesh.skin == null ? skeletonData.getDefaultSkin() : skeletonData.findSkin(linkedMesh.skin);
-				if (skin == null) throw new SerializationException("Skin not found: " + linkedMesh.skin);
-				Attachment parent = skin.getAttachment(linkedMesh.slotIndex, linkedMesh.parent);
-				if (parent == null) throw new SerializationException("Parent mesh not found: " + linkedMesh.parent);
-				linkedMesh.mesh.setDeformAttachment(linkedMesh.inheritDeform ? (VertexAttachment)parent : linkedMesh.mesh);
-				linkedMesh.mesh.setParentMesh((MeshAttachment)parent);
-				linkedMesh.mesh.updateUVs();
-			}
-			linkedMeshes.clear();
-
-			// Events.
-			o = skeletonData.events.setSize(n = input.readInt(true));
-			for (int i = 0; i < n; i++) {
-				EventData data = new EventData(input.readStringRef());
-				data.intValue = input.readInt(false);
-				data.floatValue = input.readFloat();
-				data.stringValue = input.readString();
-				data.audioPath = input.readString();
-				if (data.audioPath != null) {
-					data.volume = input.readFloat();
-					data.balance = input.readFloat();
-				}
-				o[i] = data;
-			}
-
-			// Animations.
-			o = skeletonData.animations.setSize(n = input.readInt(true));
-			for (int i = 0; i < n; i++)
-				o[i] = readAnimation(input, input.readString(), skeletonData);
-
-		} catch (IOException ex) {
-			throw new SerializationException("Error reading skeleton file.", ex);
-		} finally {
-			try {
-				input.close();
-			} catch (IOException ignored) {
-			}
-		}
-		return skeletonData;
-	}
-
-	private @Null Skin readSkin (SkeletonInput input, SkeletonData skeletonData, boolean defaultSkin, boolean nonessential)
-		throws IOException {
-
-		Skin skin;
-		int slotCount;
-		if (defaultSkin) {
-			slotCount = input.readInt(true);
-			if (slotCount == 0) return null;
-			skin = new Skin("default");
-		} else {
-			skin = new Skin(input.readStringRef());
-			Object[] bones = skin.bones.setSize(input.readInt(true)), items = skeletonData.bones.items;
-			for (int i = 0, n = skin.bones.size; i < n; i++)
-				bones[i] = items[input.readInt(true)];
-
-			items = skeletonData.ikConstraints.items;
-			for (int i = 0, n = input.readInt(true); i < n; i++)
-				skin.constraints.add((ConstraintData)items[input.readInt(true)]);
-			items = skeletonData.transformConstraints.items;
-			for (int i = 0, n = input.readInt(true); i < n; i++)
-				skin.constraints.add((ConstraintData)items[input.readInt(true)]);
-			items = skeletonData.pathConstraints.items;
-			for (int i = 0, n = input.readInt(true); i < n; i++)
-				skin.constraints.add((ConstraintData)items[input.readInt(true)]);
-			skin.constraints.shrink();
-
-			slotCount = input.readInt(true);
-		}
-
-		for (int i = 0; i < slotCount; i++) {
-			int slotIndex = input.readInt(true);
-			for (int ii = 0, nn = input.readInt(true); ii < nn; ii++) {
-				String name = input.readStringRef();
-				Attachment attachment = readAttachment(input, skeletonData, skin, slotIndex, name, nonessential);
-				if (attachment != null) skin.setAttachment(slotIndex, name, attachment);
-			}
-		}
-		return skin;
-	}
-
-	private Attachment readAttachment (SkeletonInput input, SkeletonData skeletonData, Skin skin, int slotIndex,
-		String attachmentName, boolean nonessential) throws IOException {
-		float scale = this.scale;
-
-		String name = input.readStringRef();
-		if (name == null) name = attachmentName;
-
-		switch (AttachmentType.values[input.readByte()]) {
-		case region: {
-			String path = input.readStringRef();
-			float rotation = input.readFloat();
-			float x = input.readFloat();
-			float y = input.readFloat();
-			float scaleX = input.readFloat();
-			float scaleY = input.readFloat();
-			float width = input.readFloat();
-			float height = input.readFloat();
-			int color = input.readInt();
-
-			if (path == null) path = name;
-			RegionAttachment region = attachmentLoader.newRegionAttachment(skin, name, path);
-			if (region == null) return null;
-			region.setPath(path);
-			region.setX(x * scale);
-			region.setY(y * scale);
-			region.setScaleX(scaleX);
-			region.setScaleY(scaleY);
-			region.setRotation(rotation);
-			region.setWidth(width * scale);
-			region.setHeight(height * scale);
-			Color.rgba8888ToColor(region.getColor(), color);
-			region.updateOffset();
-			return region;
-		}
-		case boundingbox: {
-			int vertexCount = input.readInt(true);
-			Vertices vertices = readVertices(input, vertexCount);
-			int color = nonessential ? input.readInt() : 0;
-
-			BoundingBoxAttachment box = attachmentLoader.newBoundingBoxAttachment(skin, name);
-			if (box == null) return null;
-			box.setWorldVerticesLength(vertexCount << 1);
-			box.setVertices(vertices.vertices);
-			box.setBones(vertices.bones);
-			if (nonessential) Color.rgba8888ToColor(box.getColor(), color);
-			return box;
-		}
-		case mesh: {
-			String path = input.readStringRef();
-			int color = input.readInt();
-			int vertexCount = input.readInt(true);
-			float[] uvs = readFloatArray(input, vertexCount << 1, 1);
-			short[] triangles = readShortArray(input);
-			Vertices vertices = readVertices(input, vertexCount);
-			int hullLength = input.readInt(true);
-			short[] edges = null;
-			float width = 0, height = 0;
-			if (nonessential) {
-				edges = readShortArray(input);
-				width = input.readFloat();
-				height = input.readFloat();
-			}
-
-			if (path == null) path = name;
-			MeshAttachment mesh = attachmentLoader.newMeshAttachment(skin, name, path);
-			if (mesh == null) return null;
-			mesh.setPath(path);
-			Color.rgba8888ToColor(mesh.getColor(), color);
-			mesh.setBones(vertices.bones);
-			mesh.setVertices(vertices.vertices);
-			mesh.setWorldVerticesLength(vertexCount << 1);
-			mesh.setTriangles(triangles);
-			mesh.setRegionUVs(uvs);
-			mesh.updateUVs();
-			mesh.setHullLength(hullLength << 1);
-			if (nonessential) {
-				mesh.setEdges(edges);
-				mesh.setWidth(width * scale);
-				mesh.setHeight(height * scale);
-			}
-			return mesh;
-		}
-		case linkedmesh: {
-			String path = input.readStringRef();
-			int color = input.readInt();
-			String skinName = input.readStringRef();
-			String parent = input.readStringRef();
-			boolean inheritDeform = input.readBoolean();
-			float width = 0, height = 0;
-			if (nonessential) {
-				width = input.readFloat();
-				height = input.readFloat();
-			}
-
-			if (path == null) path = name;
-			MeshAttachment mesh = attachmentLoader.newMeshAttachment(skin, name, path);
-			if (mesh == null) return null;
-			mesh.setPath(path);
-			Color.rgba8888ToColor(mesh.getColor(), color);
-			if (nonessential) {
-				mesh.setWidth(width * scale);
-				mesh.setHeight(height * scale);
-			}
-			linkedMeshes.add(new LinkedMesh(mesh, skinName, slotIndex, parent, inheritDeform));
-			return mesh;
-		}
-		case path: {
-			boolean closed = input.readBoolean();
-			boolean constantSpeed = input.readBoolean();
-			int vertexCount = input.readInt(true);
-			Vertices vertices = readVertices(input, vertexCount);
-			float[] lengths = new float[vertexCount / 3];
-			for (int i = 0, n = lengths.length; i < n; i++)
-				lengths[i] = input.readFloat() * scale;
-			int color = nonessential ? input.readInt() : 0;
-
-			PathAttachment path = attachmentLoader.newPathAttachment(skin, name);
-			if (path == null) return null;
-			path.setClosed(closed);
-			path.setConstantSpeed(constantSpeed);
-			path.setWorldVerticesLength(vertexCount << 1);
-			path.setVertices(vertices.vertices);
-			path.setBones(vertices.bones);
-			path.setLengths(lengths);
-			if (nonessential) Color.rgba8888ToColor(path.getColor(), color);
-			return path;
-		}
-		case point: {
-			float rotation = input.readFloat();
-			float x = input.readFloat();
-			float y = input.readFloat();
-			int color = nonessential ? input.readInt() : 0;
-
-			PointAttachment point = attachmentLoader.newPointAttachment(skin, name);
-			if (point == null) return null;
-			point.setX(x * scale);
-			point.setY(y * scale);
-			point.setRotation(rotation);
-			if (nonessential) Color.rgba8888ToColor(point.getColor(), color);
-			return point;
-		}
-		case clipping:
-			int endSlotIndex = input.readInt(true);
-			int vertexCount = input.readInt(true);
-			Vertices vertices = readVertices(input, vertexCount);
-			int color = nonessential ? input.readInt() : 0;
-
-			ClippingAttachment clip = attachmentLoader.newClippingAttachment(skin, name);
-			if (clip == null) return null;
-			clip.setEndSlot(skeletonData.slots.get(endSlotIndex));
-			clip.setWorldVerticesLength(vertexCount << 1);
-			clip.setVertices(vertices.vertices);
-			clip.setBones(vertices.bones);
-			if (nonessential) Color.rgba8888ToColor(clip.getColor(), color);
-			return clip;
-		}
-		return null;
-	}
-
-	private Vertices readVertices (SkeletonInput input, int vertexCount) throws IOException {
-		float scale = this.scale;
-		int verticesLength = vertexCount << 1;
-		Vertices vertices = new Vertices();
-		if (!input.readBoolean()) {
-			vertices.vertices = readFloatArray(input, verticesLength, scale);
-			return vertices;
-		}
-		FloatArray weights = new FloatArray(verticesLength * 3 * 3);
-		IntArray bonesArray = new IntArray(verticesLength * 3);
-		for (int i = 0; i < vertexCount; i++) {
-			int boneCount = input.readInt(true);
-			bonesArray.add(boneCount);
-			for (int ii = 0; ii < boneCount; ii++) {
-				bonesArray.add(input.readInt(true));
-				weights.add(input.readFloat() * scale);
-				weights.add(input.readFloat() * scale);
-				weights.add(input.readFloat());
-			}
-		}
-		vertices.vertices = weights.toArray();
-		vertices.bones = bonesArray.toArray();
-		return vertices;
-	}
-
-	private float[] readFloatArray (SkeletonInput input, int n, float scale) throws IOException {
-		float[] array = new float[n];
-		if (scale == 1) {
-			for (int i = 0; i < n; i++)
-				array[i] = input.readFloat();
-		} else {
-			for (int i = 0; i < n; i++)
-				array[i] = input.readFloat() * scale;
-		}
-		return array;
-	}
-
-	private short[] readShortArray (SkeletonInput input) throws IOException {
-		int n = input.readInt(true);
-		short[] array = new short[n];
-		for (int i = 0; i < n; i++)
-			array[i] = input.readShort();
-		return array;
-	}
-
-	private Animation readAnimation (SkeletonInput input, String name, SkeletonData skeletonData) throws IOException {
-		Array<Timeline> timelines = new Array(input.readInt(true));
-		float scale = this.scale;
-
-		// Slot timelines.
-		for (int i = 0, n = input.readInt(true); i < n; i++) {
-			int slotIndex = input.readInt(true);
-			for (int ii = 0, nn = input.readInt(true); ii < nn; ii++) {
-				int timelineType = input.readByte(), frameCount = input.readInt(true), frameLast = frameCount - 1;
-				switch (timelineType) {
-				case SLOT_ATTACHMENT: {
-					AttachmentTimeline timeline = new AttachmentTimeline(frameCount, slotIndex);
-					for (int frame = 0; frame < frameCount; frame++)
-						timeline.setFrame(frame, input.readFloat(), input.readStringRef());
-					timelines.add(timeline);
-					break;
-				}
-				case SLOT_RGBA: {
-					RGBATimeline timeline = new RGBATimeline(frameCount, input.readInt(true), slotIndex);
-					float time = input.readFloat();
-					float r = input.read() / 255f, g = input.read() / 255f;
-					float b = input.read() / 255f, a = input.read() / 255f;
-					for (int frame = 0, bezier = 0;; frame++) {
-						timeline.setFrame(frame, time, r, g, b, a);
-						if (frame == frameLast) break;
-						float time2 = input.readFloat();
-						float r2 = input.read() / 255f, g2 = input.read() / 255f;
-						float b2 = input.read() / 255f, a2 = input.read() / 255f;
-						switch (input.readByte()) {
-						case CURVE_STEPPED:
-							timeline.setStepped(frame);
-							break;
-						case CURVE_BEZIER:
-							setBezier(input, timeline, bezier++, frame, 0, time, time2, r, r2, 1);
-							setBezier(input, timeline, bezier++, frame, 1, time, time2, g, g2, 1);
-							setBezier(input, timeline, bezier++, frame, 2, time, time2, b, b2, 1);
-							setBezier(input, timeline, bezier++, frame, 3, time, time2, a, a2, 1);
-						}
-						time = time2;
-						r = r2;
-						g = g2;
-						b = b2;
-						a = a2;
-					}
-					timelines.add(timeline);
-					break;
-				}
-				case SLOT_RGB: {
-					RGBTimeline timeline = new RGBTimeline(frameCount, input.readInt(true), slotIndex);
-					float time = input.readFloat();
-					float r = input.read() / 255f, g = input.read() / 255f, b = input.read() / 255f;
-					for (int frame = 0, bezier = 0;; frame++) {
-						timeline.setFrame(frame, time, r, g, b);
-						if (frame == frameLast) break;
-						float time2 = input.readFloat();
-						float r2 = input.read() / 255f, g2 = input.read() / 255f, b2 = input.read() / 255f;
-						switch (input.readByte()) {
-						case CURVE_STEPPED:
-							timeline.setStepped(frame);
-							break;
-						case CURVE_BEZIER:
-							setBezier(input, timeline, bezier++, frame, 0, time, time2, r, r2, 1);
-							setBezier(input, timeline, bezier++, frame, 1, time, time2, g, g2, 1);
-							setBezier(input, timeline, bezier++, frame, 2, time, time2, b, b2, 1);
-						}
-						time = time2;
-						r = r2;
-						g = g2;
-						b = b2;
-					}
-					timelines.add(timeline);
-					break;
-				}
-				case SLOT_RGBA2: {
-					RGBA2Timeline timeline = new RGBA2Timeline(frameCount, input.readInt(true), slotIndex);
-					float time = input.readFloat();
-					float r = input.read() / 255f, g = input.read() / 255f;
-					float b = input.read() / 255f, a = input.read() / 255f;
-					float r2 = input.read() / 255f, g2 = input.read() / 255f, b2 = input.read() / 255f;
-					for (int frame = 0, bezier = 0;; frame++) {
-						timeline.setFrame(frame, time, r, g, b, a, r2, g2, b2);
-						if (frame == frameLast) break;
-						float time2 = input.readFloat();
-						float nr = input.read() / 255f, ng = input.read() / 255f;
-						float nb = input.read() / 255f, na = input.read() / 255f;
-						float nr2 = input.read() / 255f, ng2 = input.read() / 255f, nb2 = input.read() / 255f;
-						switch (input.readByte()) {
-						case CURVE_STEPPED:
-							timeline.setStepped(frame);
-							break;
-						case CURVE_BEZIER:
-							setBezier(input, timeline, bezier++, frame, 0, time, time2, r, nr, 1);
-							setBezier(input, timeline, bezier++, frame, 1, time, time2, g, ng, 1);
-							setBezier(input, timeline, bezier++, frame, 2, time, time2, b, nb, 1);
-							setBezier(input, timeline, bezier++, frame, 3, time, time2, a, na, 1);
-							setBezier(input, timeline, bezier++, frame, 4, time, time2, r2, nr2, 1);
-							setBezier(input, timeline, bezier++, frame, 5, time, time2, g2, ng2, 1);
-							setBezier(input, timeline, bezier++, frame, 6, time, time2, b2, nb2, 1);
-						}
-						time = time2;
-						r = nr;
-						g = ng;
-						b = nb;
-						a = na;
-						r2 = nr2;
-						g2 = ng2;
-						b2 = nb2;
-					}
-					timelines.add(timeline);
-					break;
-				}
-				case SLOT_RGB2: {
-					RGB2Timeline timeline = new RGB2Timeline(frameCount, input.readInt(true), slotIndex);
-					float time = input.readFloat();
-					float r = input.read() / 255f, g = input.read() / 255f, b = input.read() / 255f;
-					float r2 = input.read() / 255f, g2 = input.read() / 255f, b2 = input.read() / 255f;
-					for (int frame = 0, bezier = 0;; frame++) {
-						timeline.setFrame(frame, time, r, g, b, r2, g2, b2);
-						if (frame == frameLast) break;
-						float time2 = input.readFloat();
-						float nr = input.read() / 255f, ng = input.read() / 255f, nb = input.read() / 255f;
-						float nr2 = input.read() / 255f, ng2 = input.read() / 255f, nb2 = input.read() / 255f;
-						switch (input.readByte()) {
-						case CURVE_STEPPED:
-							timeline.setStepped(frame);
-							break;
-						case CURVE_BEZIER:
-							setBezier(input, timeline, bezier++, frame, 0, time, time2, r, nr, 1);
-							setBezier(input, timeline, bezier++, frame, 1, time, time2, g, ng, 1);
-							setBezier(input, timeline, bezier++, frame, 2, time, time2, b, nb, 1);
-							setBezier(input, timeline, bezier++, frame, 3, time, time2, r2, nr2, 1);
-							setBezier(input, timeline, bezier++, frame, 4, time, time2, g2, ng2, 1);
-							setBezier(input, timeline, bezier++, frame, 5, time, time2, b2, nb2, 1);
-						}
-						time = time2;
-						r = nr;
-						g = ng;
-						b = nb;
-						r2 = nr2;
-						g2 = ng2;
-						b2 = nb2;
-					}
-					timelines.add(timeline);
-					break;
-				}
-				case SLOT_ALPHA:
-					AlphaTimeline timeline = new AlphaTimeline(frameCount, input.readInt(true), slotIndex);
-					float time = input.readFloat(), a = input.read() / 255f;
-					for (int frame = 0, bezier = 0;; frame++) {
-						timeline.setFrame(frame, time, a);
-						if (frame == frameLast) break;
-						float time2 = input.readFloat();
-						float a2 = input.read() / 255f;
-						switch (input.readByte()) {
-						case CURVE_STEPPED:
-							timeline.setStepped(frame);
-							break;
-						case CURVE_BEZIER:
-							setBezier(input, timeline, bezier++, frame, 0, time, time2, a, a2, 1);
-						}
-						time = time2;
-						a = a2;
-					}
-					timelines.add(timeline);
-					break;
-				}
-			}
-		}
-
-		// Bone timelines.
-		for (int i = 0, n = input.readInt(true); i < n; i++) {
-			int boneIndex = input.readInt(true);
-			for (int ii = 0, nn = input.readInt(true); ii < nn; ii++) {
-				int type = input.readByte(), frameCount = input.readInt(true), bezierCount = input.readInt(true);
-				switch (type) {
-				case BONE_ROTATE:
-					timelines.add(readTimeline(input, new RotateTimeline(frameCount, bezierCount, boneIndex), 1));
-					break;
-				case BONE_TRANSLATE:
-					timelines.add(readTimeline(input, new TranslateTimeline(frameCount, bezierCount, boneIndex), scale));
-					break;
-				case BONE_TRANSLATEX:
-					timelines.add(readTimeline(input, new TranslateXTimeline(frameCount, bezierCount, boneIndex), scale));
-					break;
-				case BONE_TRANSLATEY:
-					timelines.add(readTimeline(input, new TranslateYTimeline(frameCount, bezierCount, boneIndex), scale));
-					break;
-				case BONE_SCALE:
-					timelines.add(readTimeline(input, new ScaleTimeline(frameCount, bezierCount, boneIndex), 1));
-					break;
-				case BONE_SCALEX:
-					timelines.add(readTimeline(input, new ScaleXTimeline(frameCount, bezierCount, boneIndex), 1));
-					break;
-				case BONE_SCALEY:
-					timelines.add(readTimeline(input, new ScaleYTimeline(frameCount, bezierCount, boneIndex), 1));
-					break;
-				case BONE_SHEAR:
-					timelines.add(readTimeline(input, new ShearTimeline(frameCount, bezierCount, boneIndex), 1));
-					break;
-				case BONE_SHEARX:
-					timelines.add(readTimeline(input, new ShearXTimeline(frameCount, bezierCount, boneIndex), 1));
-					break;
-				case BONE_SHEARY:
-					timelines.add(readTimeline(input, new ShearYTimeline(frameCount, bezierCount, boneIndex), 1));
-				}
-			}
-		}
-
-		// IK constraint timelines.
-		for (int i = 0, n = input.readInt(true); i < n; i++) {
-			int index = input.readInt(true), frameCount = input.readInt(true), frameLast = frameCount - 1;
-			IkConstraintTimeline timeline = new IkConstraintTimeline(frameCount, input.readInt(true), index);
-			float time = input.readFloat(), mix = input.readFloat(), softness = input.readFloat() * scale;
-			for (int frame = 0, bezier = 0;; frame++) {
-				timeline.setFrame(frame, time, mix, softness, input.readByte(), input.readBoolean(), input.readBoolean());
-				if (frame == frameLast) break;
-				float time2 = input.readFloat(), mix2 = input.readFloat(), softness2 = input.readFloat() * scale;
-				switch (input.readByte()) {
-				case CURVE_STEPPED:
-					timeline.setStepped(frame);
-					break;
-				case CURVE_BEZIER:
-					setBezier(input, timeline, bezier++, frame, 0, time, time2, mix, mix2, 1);
-					setBezier(input, timeline, bezier++, frame, 1, time, time2, softness, softness2, scale);
-				}
-				time = time2;
-				mix = mix2;
-				softness = softness2;
-			}
-			timelines.add(timeline);
-		}
-
-		// Transform constraint timelines.
-		for (int i = 0, n = input.readInt(true); i < n; i++) {
-			int index = input.readInt(true), frameCount = input.readInt(true), frameLast = frameCount - 1;
-			TransformConstraintTimeline timeline = new TransformConstraintTimeline(frameCount, input.readInt(true), index);
-			float time = input.readFloat(), mixRotate = input.readFloat(), mixX = input.readFloat(), mixY = input.readFloat(),
-				mixScaleX = input.readFloat(), mixScaleY = input.readFloat(), mixShearY = input.readFloat();
-			for (int frame = 0, bezier = 0;; frame++) {
-				timeline.setFrame(frame, time, mixRotate, mixX, mixY, mixScaleX, mixScaleY, mixShearY);
-				if (frame == frameLast) break;
-				float time2 = input.readFloat(), mixRotate2 = input.readFloat(), mixX2 = input.readFloat(), mixY2 = input.readFloat(),
-					mixScaleX2 = input.readFloat(), mixScaleY2 = input.readFloat(), mixShearY2 = input.readFloat();
-				switch (input.readByte()) {
-				case CURVE_STEPPED:
-					timeline.setStepped(frame);
-					break;
-				case CURVE_BEZIER:
-					setBezier(input, timeline, bezier++, frame, 0, time, time2, mixRotate, mixRotate2, 1);
-					setBezier(input, timeline, bezier++, frame, 1, time, time2, mixX, mixX2, 1);
-					setBezier(input, timeline, bezier++, frame, 2, time, time2, mixY, mixY2, 1);
-					setBezier(input, timeline, bezier++, frame, 3, time, time2, mixScaleX, mixScaleX2, 1);
-					setBezier(input, timeline, bezier++, frame, 4, time, time2, mixScaleY, mixScaleY2, 1);
-					setBezier(input, timeline, bezier++, frame, 5, time, time2, mixShearY, mixShearY2, 1);
-				}
-				time = time2;
-				mixRotate = mixRotate2;
-				mixX = mixX2;
-				mixY = mixY2;
-				mixScaleX = mixScaleX2;
-				mixScaleY = mixScaleY2;
-				mixShearY = mixShearY2;
-			}
-			timelines.add(timeline);
-		}
-
-		// Path constraint timelines.
-		for (int i = 0, n = input.readInt(true); i < n; i++) {
-			int index = input.readInt(true);
-			PathConstraintData data = skeletonData.pathConstraints.get(index);
-			for (int ii = 0, nn = input.readInt(true); ii < nn; ii++) {
-				switch (input.readByte()) {
-				case PATH_POSITION:
-					timelines
-						.add(readTimeline(input, new PathConstraintPositionTimeline(input.readInt(true), input.readInt(true), index),
-							data.positionMode == PositionMode.fixed ? scale : 1));
-					break;
-				case PATH_SPACING:
-					timelines
-						.add(readTimeline(input, new PathConstraintSpacingTimeline(input.readInt(true), input.readInt(true), index),
-							data.spacingMode == SpacingMode.length || data.spacingMode == SpacingMode.fixed ? scale : 1));
-					break;
-				case PATH_MIX:
-					PathConstraintMixTimeline timeline = new PathConstraintMixTimeline(input.readInt(true), input.readInt(true),
-						index);
-					float time = input.readFloat(), mixRotate = input.readFloat(), mixX = input.readFloat(), mixY = input.readFloat();
-					for (int frame = 0, bezier = 0, frameLast = timeline.getFrameCount() - 1;; frame++) {
-						timeline.setFrame(frame, time, mixRotate, mixX, mixY);
-						if (frame == frameLast) break;
-						float time2 = input.readFloat(), mixRotate2 = input.readFloat(), mixX2 = input.readFloat(),
-							mixY2 = input.readFloat();
-						switch (input.readByte()) {
-						case CURVE_STEPPED:
-							timeline.setStepped(frame);
-							break;
-						case CURVE_BEZIER:
-							setBezier(input, timeline, bezier++, frame, 0, time, time2, mixRotate, mixRotate2, 1);
-							setBezier(input, timeline, bezier++, frame, 1, time, time2, mixX, mixX2, 1);
-							setBezier(input, timeline, bezier++, frame, 2, time, time2, mixY, mixY2, 1);
-						}
-						time = time2;
-						mixRotate = mixRotate2;
-						mixX = mixX2;
-						mixY = mixY2;
-					}
-					timelines.add(timeline);
-				}
-			}
-		}
-
-		// Deform timelines.
-		for (int i = 0, n = input.readInt(true); i < n; i++) {
-			Skin skin = skeletonData.skins.get(input.readInt(true));
-			for (int ii = 0, nn = input.readInt(true); ii < nn; ii++) {
-				int slotIndex = input.readInt(true);
-				for (int iii = 0, nnn = input.readInt(true); iii < nnn; iii++) {
-					String attachmentName = input.readStringRef();
-					VertexAttachment attachment = (VertexAttachment)skin.getAttachment(slotIndex, attachmentName);
-					if (attachment == null) throw new SerializationException("Vertex attachment not found: " + attachmentName);
-					boolean weighted = attachment.getBones() != null;
-					float[] vertices = attachment.getVertices();
-					int deformLength = weighted ? (vertices.length / 3) << 1 : vertices.length;
-
-					int frameCount = input.readInt(true), frameLast = frameCount - 1;
-					DeformTimeline timeline = new DeformTimeline(frameCount, input.readInt(true), slotIndex, attachment);
-
-					float time = input.readFloat();
-					for (int frame = 0, bezier = 0;; frame++) {
-						float[] deform;
-						int end = input.readInt(true);
-						if (end == 0)
-							deform = weighted ? new float[deformLength] : vertices;
-						else {
-							deform = new float[deformLength];
-							int start = input.readInt(true);
-							end += start;
-							if (scale == 1) {
-								for (int v = start; v < end; v++)
-									deform[v] = input.readFloat();
-							} else {
-								for (int v = start; v < end; v++)
-									deform[v] = input.readFloat() * scale;
-							}
-							if (!weighted) {
-								for (int v = 0, vn = deform.length; v < vn; v++)
-									deform[v] += vertices[v];
-							}
-						}
-						timeline.setFrame(frame, time, deform);
-						if (frame == frameLast) break;
-						float time2 = input.readFloat();
-						switch (input.readByte()) {
-						case CURVE_STEPPED:
-							timeline.setStepped(frame);
-							break;
-						case CURVE_BEZIER:
-							setBezier(input, timeline, bezier++, frame, 0, time, time2, 0, 1, 1);
-						}
-						time = time2;
-					}
-					timelines.add(timeline);
-				}
-			}
-		}
-
-		// Draw order timeline.
-		int drawOrderCount = input.readInt(true);
-		if (drawOrderCount > 0) {
-			DrawOrderTimeline timeline = new DrawOrderTimeline(drawOrderCount);
-			int slotCount = skeletonData.slots.size;
-			for (int i = 0; i < drawOrderCount; i++) {
-				float time = input.readFloat();
-				int offsetCount = input.readInt(true);
-				int[] drawOrder = new int[slotCount];
-				for (int ii = slotCount - 1; ii >= 0; ii--)
-					drawOrder[ii] = -1;
-				int[] unchanged = new int[slotCount - offsetCount];
-				int originalIndex = 0, unchangedIndex = 0;
-				for (int ii = 0; ii < offsetCount; ii++) {
-					int slotIndex = input.readInt(true);
-					// Collect unchanged items.
-					while (originalIndex != slotIndex)
-						unchanged[unchangedIndex++] = originalIndex++;
-					// Set changed items.
-					drawOrder[originalIndex + input.readInt(true)] = originalIndex++;
-				}
-				// Collect remaining unchanged items.
-				while (originalIndex < slotCount)
-					unchanged[unchangedIndex++] = originalIndex++;
-				// Fill in unchanged items.
-				for (int ii = slotCount - 1; ii >= 0; ii--)
-					if (drawOrder[ii] == -1) drawOrder[ii] = unchanged[--unchangedIndex];
-				timeline.setFrame(i, time, drawOrder);
-			}
-			timelines.add(timeline);
-		}
-
-		// Event timeline.
-		int eventCount = input.readInt(true);
-		if (eventCount > 0) {
-			EventTimeline timeline = new EventTimeline(eventCount);
-			for (int i = 0; i < eventCount; i++) {
-				float time = input.readFloat();
-				EventData eventData = skeletonData.events.get(input.readInt(true));
-				Event event = new Event(time, eventData);
-				event.intValue = input.readInt(false);
-				event.floatValue = input.readFloat();
-				event.stringValue = input.readBoolean() ? input.readString() : eventData.stringValue;
-				if (event.getData().audioPath != null) {
-					event.volume = input.readFloat();
-					event.balance = input.readFloat();
-				}
-				timeline.setFrame(i, event);
-			}
-			timelines.add(timeline);
-		}
-
-		float duration = 0;
-		Object[] items = timelines.items;
-		for (int i = 0, n = timelines.size; i < n; i++)
-			duration = Math.max(duration, ((Timeline)items[i]).getDuration());
-		return new Animation(name, timelines, duration);
-	}
-
-	private Timeline readTimeline (SkeletonInput input, CurveTimeline1 timeline, float scale) throws IOException {
-		float time = input.readFloat(), value = input.readFloat() * scale;
-		for (int frame = 0, bezier = 0, frameLast = timeline.getFrameCount() - 1;; frame++) {
-			timeline.setFrame(frame, time, value);
-			if (frame == frameLast) break;
-			float time2 = input.readFloat(), value2 = input.readFloat() * scale;
-			switch (input.readByte()) {
-			case CURVE_STEPPED:
-				timeline.setStepped(frame);
-				break;
-			case CURVE_BEZIER:
-				setBezier(input, timeline, bezier++, frame, 0, time, time2, value, value2, 1);
-			}
-			time = time2;
-			value = value2;
-		}
-		return timeline;
-	}
-
-	private Timeline readTimeline (SkeletonInput input, CurveTimeline2 timeline, float scale) throws IOException {
-		float time = input.readFloat(), value1 = input.readFloat() * scale, value2 = input.readFloat() * scale;
-		for (int frame = 0, bezier = 0, frameLast = timeline.getFrameCount() - 1;; frame++) {
-			timeline.setFrame(frame, time, value1, value2);
-			if (frame == frameLast) break;
-			float time2 = input.readFloat(), nvalue1 = input.readFloat() * scale, nvalue2 = input.readFloat() * scale;
-			switch (input.readByte()) {
-			case CURVE_STEPPED:
-				timeline.setStepped(frame);
-				break;
-			case CURVE_BEZIER:
-				setBezier(input, timeline, bezier++, frame, 0, time, time2, value1, nvalue1, scale);
-				setBezier(input, timeline, bezier++, frame, 1, time, time2, value2, nvalue2, scale);
-			}
-			time = time2;
-			value1 = nvalue1;
-			value2 = nvalue2;
-		}
-		return timeline;
-	}
-
-	void setBezier (SkeletonInput input, CurveTimeline timeline, int bezier, int frame, int value, float time1, float time2,
-		float value1, float value2, float scale) throws IOException {
-		timeline.setBezier(bezier, frame, value, time1, value1, input.readFloat(), input.readFloat() * scale, input.readFloat(),
-			input.readFloat() * scale, time2, value2);
-	}
-
-	static class Vertices {
-		int[] bones;
-		float[] vertices;
-	}
-
-	static class SkeletonInput extends DataInput {
-		private char[] chars = new char[32];
-		String[] strings;
-
-		public SkeletonInput (InputStream input) {
-			super(input);
-		}
-
-		public SkeletonInput (FileHandle file) {
-			super(file.read(512));
-		}
-
-		public @Null String readStringRef () throws IOException {
-			int index = readInt(true);
-			return index == 0 ? null : strings[index - 1];
-		}
-
-		public String readString () throws IOException {
-			int byteCount = readInt(true);
-			switch (byteCount) {
-			case 0:
-				return null;
-			case 1:
-				return "";
-			}
-			byteCount--;
-			if (chars.length < byteCount) chars = new char[byteCount];
-			char[] chars = this.chars;
-			int charCount = 0;
-			for (int i = 0; i < byteCount;) {
-				int b = read();
-				switch (b >> 4) {
-				case -1:
-					throw new EOFException();
-				case 12:
-				case 13:
-					chars[charCount++] = (char)((b & 0x1F) << 6 | read() & 0x3F);
-					i += 2;
-					break;
-				case 14:
-					chars[charCount++] = (char)((b & 0x0F) << 12 | (read() & 0x3F) << 6 | read() & 0x3F);
-					i += 3;
-					break;
-				default:
-					chars[charCount++] = (char)b;
-					i++;
-				}
-			}
-			return new String(chars, 0, charCount);
-		}
-	}
-}
+/******************************************************************************
+ * Spine Runtimes License Agreement
+ * Last updated January 1, 2020. Replaces all prior versions.
+ *
+ * Copyright (c) 2013-2020, Esoteric Software LLC
+ *
+ * Integration of the Spine Runtimes into software or otherwise creating
+ * derivative works of the Spine Runtimes is permitted under the terms and
+ * conditions of Section 2 of the Spine Editor License Agreement:
+ * http://esotericsoftware.com/spine-editor-license
+ *
+ * Otherwise, it is permitted to integrate the Spine Runtimes into software
+ * or otherwise create derivative works of the Spine Runtimes (collectively,
+ * "Products"), provided that each user of the Products must obtain their own
+ * Spine Editor license and redistribution of the Products in any form must
+ * include this license and copyright notice.
+ *
+ * THE SPINE RUNTIMES ARE PROVIDED BY ESOTERIC SOFTWARE LLC "AS IS" AND ANY
+ * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL ESOTERIC SOFTWARE LLC BE LIABLE FOR ANY
+ * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES,
+ * BUSINESS INTERRUPTION, OR LOSS OF USE, DATA, OR PROFITS) HOWEVER CAUSED AND
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
+ * THE SPINE RUNTIMES, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ *****************************************************************************/
+
+package com.esotericsoftware.spine;
+
+import java.io.EOFException;
+import java.io.IOException;
+import java.io.InputStream;
+
+import com.badlogic.gdx.files.FileHandle;
+import com.badlogic.gdx.graphics.Color;
+import com.badlogic.gdx.graphics.g2d.TextureAtlas;
+import com.badlogic.gdx.utils.Array;
+import com.badlogic.gdx.utils.DataInput;
+import com.badlogic.gdx.utils.FloatArray;
+import com.badlogic.gdx.utils.IntArray;
+import com.badlogic.gdx.utils.Null;
+import com.badlogic.gdx.utils.SerializationException;
+
+import com.esotericsoftware.spine.Animation.AlphaTimeline;
+import com.esotericsoftware.spine.Animation.AttachmentTimeline;
+import com.esotericsoftware.spine.Animation.CurveTimeline;
+import com.esotericsoftware.spine.Animation.CurveTimeline1;
+import com.esotericsoftware.spine.Animation.CurveTimeline2;
+import com.esotericsoftware.spine.Animation.DeformTimeline;
+import com.esotericsoftware.spine.Animation.DrawOrderTimeline;
+import com.esotericsoftware.spine.Animation.EventTimeline;
+import com.esotericsoftware.spine.Animation.IkConstraintTimeline;
+import com.esotericsoftware.spine.Animation.PathConstraintMixTimeline;
+import com.esotericsoftware.spine.Animation.PathConstraintPositionTimeline;
+import com.esotericsoftware.spine.Animation.PathConstraintSpacingTimeline;
+import com.esotericsoftware.spine.Animation.RGB2Timeline;
+import com.esotericsoftware.spine.Animation.RGBA2Timeline;
+import com.esotericsoftware.spine.Animation.RGBATimeline;
+import com.esotericsoftware.spine.Animation.RGBTimeline;
+import com.esotericsoftware.spine.Animation.RotateTimeline;
+import com.esotericsoftware.spine.Animation.ScaleTimeline;
+import com.esotericsoftware.spine.Animation.ScaleXTimeline;
+import com.esotericsoftware.spine.Animation.ScaleYTimeline;
+import com.esotericsoftware.spine.Animation.ShearTimeline;
+import com.esotericsoftware.spine.Animation.ShearXTimeline;
+import com.esotericsoftware.spine.Animation.ShearYTimeline;
+import com.esotericsoftware.spine.Animation.Timeline;
+import com.esotericsoftware.spine.Animation.TransformConstraintTimeline;
+import com.esotericsoftware.spine.Animation.TranslateTimeline;
+import com.esotericsoftware.spine.Animation.TranslateXTimeline;
+import com.esotericsoftware.spine.Animation.TranslateYTimeline;
+import com.esotericsoftware.spine.BoneData.TransformMode;
+import com.esotericsoftware.spine.PathConstraintData.PositionMode;
+import com.esotericsoftware.spine.PathConstraintData.RotateMode;
+import com.esotericsoftware.spine.PathConstraintData.SpacingMode;
+import com.esotericsoftware.spine.SkeletonJson.LinkedMesh;
+import com.esotericsoftware.spine.attachments.Attachment;
+import com.esotericsoftware.spine.attachments.AttachmentLoader;
+import com.esotericsoftware.spine.attachments.AttachmentType;
+import com.esotericsoftware.spine.attachments.BoundingBoxAttachment;
+import com.esotericsoftware.spine.attachments.ClippingAttachment;
+import com.esotericsoftware.spine.attachments.MeshAttachment;
+import com.esotericsoftware.spine.attachments.PathAttachment;
+import com.esotericsoftware.spine.attachments.PointAttachment;
+import com.esotericsoftware.spine.attachments.RegionAttachment;
+import com.esotericsoftware.spine.attachments.VertexAttachment;
+
+/** Loads skeleton data in the Spine binary format.
+ * <p>
+ * See <a href="http://esotericsoftware.com/spine-binary-format">Spine binary format</a> and
+ * <a href="http://esotericsoftware.com/spine-loading-skeleton-data#JSON-and-binary-data">JSON and binary data</a> in the Spine
+ * Runtimes Guide. */
+public class SkeletonBinary extends SkeletonLoader {
+	static public final int BONE_ROTATE = 0;
+	static public final int BONE_TRANSLATE = 1;
+	static public final int BONE_TRANSLATEX = 2;
+	static public final int BONE_TRANSLATEY = 3;
+	static public final int BONE_SCALE = 4;
+	static public final int BONE_SCALEX = 5;
+	static public final int BONE_SCALEY = 6;
+	static public final int BONE_SHEAR = 7;
+	static public final int BONE_SHEARX = 8;
+	static public final int BONE_SHEARY = 9;
+
+	static public final int SLOT_ATTACHMENT = 0;
+	static public final int SLOT_RGBA = 1;
+	static public final int SLOT_RGB = 2;
+	static public final int SLOT_RGBA2 = 3;
+	static public final int SLOT_RGB2 = 4;
+	static public final int SLOT_ALPHA = 5;
+
+	static public final int PATH_POSITION = 0;
+	static public final int PATH_SPACING = 1;
+	static public final int PATH_MIX = 2;
+
+	static public final int CURVE_LINEAR = 0;
+	static public final int CURVE_STEPPED = 1;
+	static public final int CURVE_BEZIER = 2;
+
+	public SkeletonBinary (AttachmentLoader attachmentLoader) {
+		super(attachmentLoader);
+	}
+
+	public SkeletonBinary (TextureAtlas atlas) {
+		super(atlas);
+	}
+
+	public SkeletonData readSkeletonData (FileHandle file) {
+		if (file == null) throw new IllegalArgumentException("file cannot be null.");
+		SkeletonData skeletonData = readSkeletonData(file.read());
+		skeletonData.name = file.nameWithoutExtension();
+		return skeletonData;
+	}
+
+	public SkeletonData readSkeletonData (InputStream dataInput) {
+		if (dataInput == null) throw new IllegalArgumentException("dataInput cannot be null.");
+
+		float scale = this.scale;
+
+		SkeletonInput input = new SkeletonInput(dataInput);
+		SkeletonData skeletonData = new SkeletonData();
+		try {
+			long hash = input.readLong();
+			skeletonData.hash = hash == 0 ? null : Long.toString(hash);
+			skeletonData.version = input.readString();
+			if (skeletonData.version.isEmpty()) skeletonData.version = null;
+			skeletonData.x = input.readFloat();
+			skeletonData.y = input.readFloat();
+			skeletonData.width = input.readFloat();
+			skeletonData.height = input.readFloat();
+
+			boolean nonessential = input.readBoolean();
+			if (nonessential) {
+				skeletonData.fps = input.readFloat();
+
+				skeletonData.imagesPath = input.readString();
+				if (skeletonData.imagesPath.isEmpty()) skeletonData.imagesPath = null;
+
+				skeletonData.audioPath = input.readString();
+				if (skeletonData.audioPath.isEmpty()) skeletonData.audioPath = null;
+			}
+
+			int n;
+			Object[] o;
+
+			// Strings.
+			o = input.strings = new String[n = input.readInt(true)];
+			for (int i = 0; i < n; i++)
+				o[i] = input.readString();
+
+			// Bones.
+			Object[] bones = skeletonData.bones.setSize(n = input.readInt(true));
+			for (int i = 0; i < n; i++) {
+				String name = input.readString();
+				BoneData parent = i == 0 ? null : (BoneData)bones[input.readInt(true)];
+				BoneData data = new BoneData(i, name, parent);
+				data.rotation = input.readFloat();
+				data.x = input.readFloat() * scale;
+				data.y = input.readFloat() * scale;
+				data.scaleX = input.readFloat();
+				data.scaleY = input.readFloat();
+				data.shearX = input.readFloat();
+				data.shearY = input.readFloat();
+				data.length = input.readFloat() * scale;
+				data.transformMode = TransformMode.values[input.readInt(true)];
+				data.skinRequired = input.readBoolean();
+				if (nonessential) Color.rgba8888ToColor(data.color, input.readInt());
+				bones[i] = data;
+			}
+
+			// Slots.
+			Object[] slots = skeletonData.slots.setSize(n = input.readInt(true));
+			for (int i = 0; i < n; i++) {
+				String slotName = input.readString();
+				BoneData boneData = (BoneData)bones[input.readInt(true)];
+				SlotData data = new SlotData(i, slotName, boneData);
+				Color.rgba8888ToColor(data.color, input.readInt());
+
+				int darkColor = input.readInt();
+				if (darkColor != -1) Color.rgb888ToColor(data.darkColor = new Color(), darkColor);
+
+				data.attachmentName = input.readStringRef();
+				data.blendMode = BlendMode.values[input.readInt(true)];
+				slots[i] = data;
+			}
+
+			// IK constraints.
+			o = skeletonData.ikConstraints.setSize(n = input.readInt(true));
+			for (int i = 0, nn; i < n; i++) {
+				IkConstraintData data = new IkConstraintData(input.readString());
+				data.order = input.readInt(true);
+				data.skinRequired = input.readBoolean();
+				Object[] constraintBones = data.bones.setSize(nn = input.readInt(true));
+				for (int ii = 0; ii < nn; ii++)
+					constraintBones[ii] = bones[input.readInt(true)];
+				data.target = (BoneData)bones[input.readInt(true)];
+				data.mix = input.readFloat();
+				data.softness = input.readFloat() * scale;
+				data.bendDirection = input.readByte();
+				data.compress = input.readBoolean();
+				data.stretch = input.readBoolean();
+				data.uniform = input.readBoolean();
+				o[i] = data;
+			}
+
+			// Transform constraints.
+			o = skeletonData.transformConstraints.setSize(n = input.readInt(true));
+			for (int i = 0, nn; i < n; i++) {
+				TransformConstraintData data = new TransformConstraintData(input.readString());
+				data.order = input.readInt(true);
+				data.skinRequired = input.readBoolean();
+				Object[] constraintBones = data.bones.setSize(nn = input.readInt(true));
+				for (int ii = 0; ii < nn; ii++)
+					constraintBones[ii] = bones[input.readInt(true)];
+				data.target = (BoneData)bones[input.readInt(true)];
+				data.local = input.readBoolean();
+				data.relative = input.readBoolean();
+				data.offsetRotation = input.readFloat();
+				data.offsetX = input.readFloat() * scale;
+				data.offsetY = input.readFloat() * scale;
+				data.offsetScaleX = input.readFloat();
+				data.offsetScaleY = input.readFloat();
+				data.offsetShearY = input.readFloat();
+				data.mixRotate = input.readFloat();
+				data.mixX = input.readFloat();
+				data.mixY = input.readFloat();
+				data.mixScaleX = input.readFloat();
+				data.mixScaleY = input.readFloat();
+				data.mixShearY = input.readFloat();
+				o[i] = data;
+			}
+
+			// Path constraints.
+			o = skeletonData.pathConstraints.setSize(n = input.readInt(true));
+			for (int i = 0, nn; i < n; i++) {
+				PathConstraintData data = new PathConstraintData(input.readString());
+				data.order = input.readInt(true);
+				data.skinRequired = input.readBoolean();
+				Object[] constraintBones = data.bones.setSize(nn = input.readInt(true));
+				for (int ii = 0; ii < nn; ii++)
+					constraintBones[ii] = bones[input.readInt(true)];
+				data.target = (SlotData)slots[input.readInt(true)];
+				data.positionMode = PositionMode.values[input.readInt(true)];
+				data.spacingMode = SpacingMode.values[input.readInt(true)];
+				data.rotateMode = RotateMode.values[input.readInt(true)];
+				data.offsetRotation = input.readFloat();
+				data.position = input.readFloat();
+				if (data.positionMode == PositionMode.fixed) data.position *= scale;
+				data.spacing = input.readFloat();
+				if (data.spacingMode == SpacingMode.length || data.spacingMode == SpacingMode.fixed) data.spacing *= scale;
+				data.mixRotate = input.readFloat();
+				data.mixX = input.readFloat();
+				data.mixY = input.readFloat();
+				o[i] = data;
+			}
+
+			// Default skin.
+			Skin defaultSkin = readSkin(input, skeletonData, true, nonessential);
+			if (defaultSkin != null) {
+				skeletonData.defaultSkin = defaultSkin;
+				skeletonData.skins.add(defaultSkin);
+			}
+
+			// Skins.
+			{
+				int i = skeletonData.skins.size;
+				o = skeletonData.skins.setSize(n = i + input.readInt(true));
+				for (; i < n; i++)
+					o[i] = readSkin(input, skeletonData, false, nonessential);
+			}
+
+			// Linked meshes.
+			n = linkedMeshes.size;
+			Object[] items = linkedMeshes.items;
+			for (int i = 0; i < n; i++) {
+				LinkedMesh linkedMesh = (LinkedMesh)items[i];
+				Skin skin = linkedMesh.skin == null ? skeletonData.getDefaultSkin() : skeletonData.findSkin(linkedMesh.skin);
+				if (skin == null) throw new SerializationException("Skin not found: " + linkedMesh.skin);
+				Attachment parent = skin.getAttachment(linkedMesh.slotIndex, linkedMesh.parent);
+				if (parent == null) throw new SerializationException("Parent mesh not found: " + linkedMesh.parent);
+				linkedMesh.mesh.setDeformAttachment(linkedMesh.inheritDeform ? (VertexAttachment)parent : linkedMesh.mesh);
+				linkedMesh.mesh.setParentMesh((MeshAttachment)parent);
+				linkedMesh.mesh.updateUVs();
+			}
+			linkedMeshes.clear();
+
+			// Events.
+			o = skeletonData.events.setSize(n = input.readInt(true));
+			for (int i = 0; i < n; i++) {
+				EventData data = new EventData(input.readStringRef());
+				data.intValue = input.readInt(false);
+				data.floatValue = input.readFloat();
+				data.stringValue = input.readString();
+				data.audioPath = input.readString();
+				if (data.audioPath != null) {
+					data.volume = input.readFloat();
+					data.balance = input.readFloat();
+				}
+				o[i] = data;
+			}
+
+			// Animations.
+			o = skeletonData.animations.setSize(n = input.readInt(true));
+			for (int i = 0; i < n; i++)
+				o[i] = readAnimation(input, input.readString(), skeletonData);
+
+		} catch (IOException ex) {
+			throw new SerializationException("Error reading skeleton file.", ex);
+		} finally {
+			try {
+				input.close();
+			} catch (IOException ignored) {
+			}
+		}
+		return skeletonData;
+	}
+
+	private @Null Skin readSkin (SkeletonInput input, SkeletonData skeletonData, boolean defaultSkin, boolean nonessential)
+		throws IOException {
+
+		Skin skin;
+		int slotCount;
+		if (defaultSkin) {
+			slotCount = input.readInt(true);
+			if (slotCount == 0) return null;
+			skin = new Skin("default");
+		} else {
+			skin = new Skin(input.readStringRef());
+			Object[] bones = skin.bones.setSize(input.readInt(true)), items = skeletonData.bones.items;
+			for (int i = 0, n = skin.bones.size; i < n; i++)
+				bones[i] = items[input.readInt(true)];
+
+			items = skeletonData.ikConstraints.items;
+			for (int i = 0, n = input.readInt(true); i < n; i++)
+				skin.constraints.add((ConstraintData)items[input.readInt(true)]);
+			items = skeletonData.transformConstraints.items;
+			for (int i = 0, n = input.readInt(true); i < n; i++)
+				skin.constraints.add((ConstraintData)items[input.readInt(true)]);
+			items = skeletonData.pathConstraints.items;
+			for (int i = 0, n = input.readInt(true); i < n; i++)
+				skin.constraints.add((ConstraintData)items[input.readInt(true)]);
+			skin.constraints.shrink();
+
+			slotCount = input.readInt(true);
+		}
+
+		for (int i = 0; i < slotCount; i++) {
+			int slotIndex = input.readInt(true);
+			for (int ii = 0, nn = input.readInt(true); ii < nn; ii++) {
+				String name = input.readStringRef();
+				Attachment attachment = readAttachment(input, skeletonData, skin, slotIndex, name, nonessential);
+				if (attachment != null) skin.setAttachment(slotIndex, name, attachment);
+			}
+		}
+		return skin;
+	}
+
+	private Attachment readAttachment (SkeletonInput input, SkeletonData skeletonData, Skin skin, int slotIndex,
+		String attachmentName, boolean nonessential) throws IOException {
+		float scale = this.scale;
+
+		String name = input.readStringRef();
+		if (name == null) name = attachmentName;
+
+		switch (AttachmentType.values[input.readByte()]) {
+		case region: {
+			String path = input.readStringRef();
+			float rotation = input.readFloat();
+			float x = input.readFloat();
+			float y = input.readFloat();
+			float scaleX = input.readFloat();
+			float scaleY = input.readFloat();
+			float width = input.readFloat();
+			float height = input.readFloat();
+			int color = input.readInt();
+
+			if (path == null) path = name;
+			RegionAttachment region = attachmentLoader.newRegionAttachment(skin, name, path);
+			if (region == null) return null;
+			region.setPath(path);
+			region.setX(x * scale);
+			region.setY(y * scale);
+			region.setScaleX(scaleX);
+			region.setScaleY(scaleY);
+			region.setRotation(rotation);
+			region.setWidth(width * scale);
+			region.setHeight(height * scale);
+			Color.rgba8888ToColor(region.getColor(), color);
+			region.updateOffset();
+			return region;
+		}
+		case boundingbox: {
+			int vertexCount = input.readInt(true);
+			Vertices vertices = readVertices(input, vertexCount);
+			int color = nonessential ? input.readInt() : 0;
+
+			BoundingBoxAttachment box = attachmentLoader.newBoundingBoxAttachment(skin, name);
+			if (box == null) return null;
+			box.setWorldVerticesLength(vertexCount << 1);
+			box.setVertices(vertices.vertices);
+			box.setBones(vertices.bones);
+			if (nonessential) Color.rgba8888ToColor(box.getColor(), color);
+			return box;
+		}
+		case mesh: {
+			String path = input.readStringRef();
+			int color = input.readInt();
+			int vertexCount = input.readInt(true);
+			float[] uvs = readFloatArray(input, vertexCount << 1, 1);
+			short[] triangles = readShortArray(input);
+			Vertices vertices = readVertices(input, vertexCount);
+			int hullLength = input.readInt(true);
+			short[] edges = null;
+			float width = 0, height = 0;
+			if (nonessential) {
+				edges = readShortArray(input);
+				width = input.readFloat();
+				height = input.readFloat();
+			}
+
+			if (path == null) path = name;
+			MeshAttachment mesh = attachmentLoader.newMeshAttachment(skin, name, path);
+			if (mesh == null) return null;
+			mesh.setPath(path);
+			Color.rgba8888ToColor(mesh.getColor(), color);
+			mesh.setBones(vertices.bones);
+			mesh.setVertices(vertices.vertices);
+			mesh.setWorldVerticesLength(vertexCount << 1);
+			mesh.setTriangles(triangles);
+			mesh.setRegionUVs(uvs);
+			mesh.updateUVs();
+			mesh.setHullLength(hullLength << 1);
+			if (nonessential) {
+				mesh.setEdges(edges);
+				mesh.setWidth(width * scale);
+				mesh.setHeight(height * scale);
+			}
+			return mesh;
+		}
+		case linkedmesh: {
+			String path = input.readStringRef();
+			int color = input.readInt();
+			String skinName = input.readStringRef();
+			String parent = input.readStringRef();
+			boolean inheritDeform = input.readBoolean();
+			float width = 0, height = 0;
+			if (nonessential) {
+				width = input.readFloat();
+				height = input.readFloat();
+			}
+
+			if (path == null) path = name;
+			MeshAttachment mesh = attachmentLoader.newMeshAttachment(skin, name, path);
+			if (mesh == null) return null;
+			mesh.setPath(path);
+			Color.rgba8888ToColor(mesh.getColor(), color);
+			if (nonessential) {
+				mesh.setWidth(width * scale);
+				mesh.setHeight(height * scale);
+			}
+			linkedMeshes.add(new LinkedMesh(mesh, skinName, slotIndex, parent, inheritDeform));
+			return mesh;
+		}
+		case path: {
+			boolean closed = input.readBoolean();
+			boolean constantSpeed = input.readBoolean();
+			int vertexCount = input.readInt(true);
+			Vertices vertices = readVertices(input, vertexCount);
+			float[] lengths = new float[vertexCount / 3];
+			for (int i = 0, n = lengths.length; i < n; i++)
+				lengths[i] = input.readFloat() * scale;
+			int color = nonessential ? input.readInt() : 0;
+
+			PathAttachment path = attachmentLoader.newPathAttachment(skin, name);
+			if (path == null) return null;
+			path.setClosed(closed);
+			path.setConstantSpeed(constantSpeed);
+			path.setWorldVerticesLength(vertexCount << 1);
+			path.setVertices(vertices.vertices);
+			path.setBones(vertices.bones);
+			path.setLengths(lengths);
+			if (nonessential) Color.rgba8888ToColor(path.getColor(), color);
+			return path;
+		}
+		case point: {
+			float rotation = input.readFloat();
+			float x = input.readFloat();
+			float y = input.readFloat();
+			int color = nonessential ? input.readInt() : 0;
+
+			PointAttachment point = attachmentLoader.newPointAttachment(skin, name);
+			if (point == null) return null;
+			point.setX(x * scale);
+			point.setY(y * scale);
+			point.setRotation(rotation);
+			if (nonessential) Color.rgba8888ToColor(point.getColor(), color);
+			return point;
+		}
+		case clipping:
+			int endSlotIndex = input.readInt(true);
+			int vertexCount = input.readInt(true);
+			Vertices vertices = readVertices(input, vertexCount);
+			int color = nonessential ? input.readInt() : 0;
+
+			ClippingAttachment clip = attachmentLoader.newClippingAttachment(skin, name);
+			if (clip == null) return null;
+			clip.setEndSlot(skeletonData.slots.get(endSlotIndex));
+			clip.setWorldVerticesLength(vertexCount << 1);
+			clip.setVertices(vertices.vertices);
+			clip.setBones(vertices.bones);
+			if (nonessential) Color.rgba8888ToColor(clip.getColor(), color);
+			return clip;
+		}
+		return null;
+	}
+
+	private Vertices readVertices (SkeletonInput input, int vertexCount) throws IOException {
+		float scale = this.scale;
+		int verticesLength = vertexCount << 1;
+		Vertices vertices = new Vertices();
+		if (!input.readBoolean()) {
+			vertices.vertices = readFloatArray(input, verticesLength, scale);
+			return vertices;
+		}
+		FloatArray weights = new FloatArray(verticesLength * 3 * 3);
+		IntArray bonesArray = new IntArray(verticesLength * 3);
+		for (int i = 0; i < vertexCount; i++) {
+			int boneCount = input.readInt(true);
+			bonesArray.add(boneCount);
+			for (int ii = 0; ii < boneCount; ii++) {
+				bonesArray.add(input.readInt(true));
+				weights.add(input.readFloat() * scale);
+				weights.add(input.readFloat() * scale);
+				weights.add(input.readFloat());
+			}
+		}
+		vertices.vertices = weights.toArray();
+		vertices.bones = bonesArray.toArray();
+		return vertices;
+	}
+
+	private float[] readFloatArray (SkeletonInput input, int n, float scale) throws IOException {
+		float[] array = new float[n];
+		if (scale == 1) {
+			for (int i = 0; i < n; i++)
+				array[i] = input.readFloat();
+		} else {
+			for (int i = 0; i < n; i++)
+				array[i] = input.readFloat() * scale;
+		}
+		return array;
+	}
+
+	private short[] readShortArray (SkeletonInput input) throws IOException {
+		int n = input.readInt(true);
+		short[] array = new short[n];
+		for (int i = 0; i < n; i++)
+			array[i] = input.readShort();
+		return array;
+	}
+
+	private Animation readAnimation (SkeletonInput input, String name, SkeletonData skeletonData) throws IOException {
+		Array<Timeline> timelines = new Array(input.readInt(true));
+		float scale = this.scale;
+
+		// Slot timelines.
+		for (int i = 0, n = input.readInt(true); i < n; i++) {
+			int slotIndex = input.readInt(true);
+			for (int ii = 0, nn = input.readInt(true); ii < nn; ii++) {
+				int timelineType = input.readByte(), frameCount = input.readInt(true), frameLast = frameCount - 1;
+				switch (timelineType) {
+				case SLOT_ATTACHMENT: {
+					AttachmentTimeline timeline = new AttachmentTimeline(frameCount, slotIndex);
+					for (int frame = 0; frame < frameCount; frame++)
+						timeline.setFrame(frame, input.readFloat(), input.readStringRef());
+					timelines.add(timeline);
+					break;
+				}
+				case SLOT_RGBA: {
+					RGBATimeline timeline = new RGBATimeline(frameCount, input.readInt(true), slotIndex);
+					float time = input.readFloat();
+					float r = input.read() / 255f, g = input.read() / 255f;
+					float b = input.read() / 255f, a = input.read() / 255f;
+					for (int frame = 0, bezier = 0;; frame++) {
+						timeline.setFrame(frame, time, r, g, b, a);
+						if (frame == frameLast) break;
+						float time2 = input.readFloat();
+						float r2 = input.read() / 255f, g2 = input.read() / 255f;
+						float b2 = input.read() / 255f, a2 = input.read() / 255f;
+						switch (input.readByte()) {
+						case CURVE_STEPPED:
+							timeline.setStepped(frame);
+							break;
+						case CURVE_BEZIER:
+							setBezier(input, timeline, bezier++, frame, 0, time, time2, r, r2, 1);
+							setBezier(input, timeline, bezier++, frame, 1, time, time2, g, g2, 1);
+							setBezier(input, timeline, bezier++, frame, 2, time, time2, b, b2, 1);
+							setBezier(input, timeline, bezier++, frame, 3, time, time2, a, a2, 1);
+						}
+						time = time2;
+						r = r2;
+						g = g2;
+						b = b2;
+						a = a2;
+					}
+					timelines.add(timeline);
+					break;
+				}
+				case SLOT_RGB: {
+					RGBTimeline timeline = new RGBTimeline(frameCount, input.readInt(true), slotIndex);
+					float time = input.readFloat();
+					float r = input.read() / 255f, g = input.read() / 255f, b = input.read() / 255f;
+					for (int frame = 0, bezier = 0;; frame++) {
+						timeline.setFrame(frame, time, r, g, b);
+						if (frame == frameLast) break;
+						float time2 = input.readFloat();
+						float r2 = input.read() / 255f, g2 = input.read() / 255f, b2 = input.read() / 255f;
+						switch (input.readByte()) {
+						case CURVE_STEPPED:
+							timeline.setStepped(frame);
+							break;
+						case CURVE_BEZIER:
+							setBezier(input, timeline, bezier++, frame, 0, time, time2, r, r2, 1);
+							setBezier(input, timeline, bezier++, frame, 1, time, time2, g, g2, 1);
+							setBezier(input, timeline, bezier++, frame, 2, time, time2, b, b2, 1);
+						}
+						time = time2;
+						r = r2;
+						g = g2;
+						b = b2;
+					}
+					timelines.add(timeline);
+					break;
+				}
+				case SLOT_RGBA2: {
+					RGBA2Timeline timeline = new RGBA2Timeline(frameCount, input.readInt(true), slotIndex);
+					float time = input.readFloat();
+					float r = input.read() / 255f, g = input.read() / 255f;
+					float b = input.read() / 255f, a = input.read() / 255f;
+					float r2 = input.read() / 255f, g2 = input.read() / 255f, b2 = input.read() / 255f;
+					for (int frame = 0, bezier = 0;; frame++) {
+						timeline.setFrame(frame, time, r, g, b, a, r2, g2, b2);
+						if (frame == frameLast) break;
+						float time2 = input.readFloat();
+						float nr = input.read() / 255f, ng = input.read() / 255f;
+						float nb = input.read() / 255f, na = input.read() / 255f;
+						float nr2 = input.read() / 255f, ng2 = input.read() / 255f, nb2 = input.read() / 255f;
+						switch (input.readByte()) {
+						case CURVE_STEPPED:
+							timeline.setStepped(frame);
+							break;
+						case CURVE_BEZIER:
+							setBezier(input, timeline, bezier++, frame, 0, time, time2, r, nr, 1);
+							setBezier(input, timeline, bezier++, frame, 1, time, time2, g, ng, 1);
+							setBezier(input, timeline, bezier++, frame, 2, time, time2, b, nb, 1);
+							setBezier(input, timeline, bezier++, frame, 3, time, time2, a, na, 1);
+							setBezier(input, timeline, bezier++, frame, 4, time, time2, r2, nr2, 1);
+							setBezier(input, timeline, bezier++, frame, 5, time, time2, g2, ng2, 1);
+							setBezier(input, timeline, bezier++, frame, 6, time, time2, b2, nb2, 1);
+						}
+						time = time2;
+						r = nr;
+						g = ng;
+						b = nb;
+						a = na;
+						r2 = nr2;
+						g2 = ng2;
+						b2 = nb2;
+					}
+					timelines.add(timeline);
+					break;
+				}
+				case SLOT_RGB2: {
+					RGB2Timeline timeline = new RGB2Timeline(frameCount, input.readInt(true), slotIndex);
+					float time = input.readFloat();
+					float r = input.read() / 255f, g = input.read() / 255f, b = input.read() / 255f;
+					float r2 = input.read() / 255f, g2 = input.read() / 255f, b2 = input.read() / 255f;
+					for (int frame = 0, bezier = 0;; frame++) {
+						timeline.setFrame(frame, time, r, g, b, r2, g2, b2);
+						if (frame == frameLast) break;
+						float time2 = input.readFloat();
+						float nr = input.read() / 255f, ng = input.read() / 255f, nb = input.read() / 255f;
+						float nr2 = input.read() / 255f, ng2 = input.read() / 255f, nb2 = input.read() / 255f;
+						switch (input.readByte()) {
+						case CURVE_STEPPED:
+							timeline.setStepped(frame);
+							break;
+						case CURVE_BEZIER:
+							setBezier(input, timeline, bezier++, frame, 0, time, time2, r, nr, 1);
+							setBezier(input, timeline, bezier++, frame, 1, time, time2, g, ng, 1);
+							setBezier(input, timeline, bezier++, frame, 2, time, time2, b, nb, 1);
+							setBezier(input, timeline, bezier++, frame, 3, time, time2, r2, nr2, 1);
+							setBezier(input, timeline, bezier++, frame, 4, time, time2, g2, ng2, 1);
+							setBezier(input, timeline, bezier++, frame, 5, time, time2, b2, nb2, 1);
+						}
+						time = time2;
+						r = nr;
+						g = ng;
+						b = nb;
+						r2 = nr2;
+						g2 = ng2;
+						b2 = nb2;
+					}
+					timelines.add(timeline);
+					break;
+				}
+				case SLOT_ALPHA:
+					AlphaTimeline timeline = new AlphaTimeline(frameCount, input.readInt(true), slotIndex);
+					float time = input.readFloat(), a = input.read() / 255f;
+					for (int frame = 0, bezier = 0;; frame++) {
+						timeline.setFrame(frame, time, a);
+						if (frame == frameLast) break;
+						float time2 = input.readFloat();
+						float a2 = input.read() / 255f;
+						switch (input.readByte()) {
+						case CURVE_STEPPED:
+							timeline.setStepped(frame);
+							break;
+						case CURVE_BEZIER:
+							setBezier(input, timeline, bezier++, frame, 0, time, time2, a, a2, 1);
+						}
+						time = time2;
+						a = a2;
+					}
+					timelines.add(timeline);
+					break;
+				}
+			}
+		}
+
+		// Bone timelines.
+		for (int i = 0, n = input.readInt(true); i < n; i++) {
+			int boneIndex = input.readInt(true);
+			for (int ii = 0, nn = input.readInt(true); ii < nn; ii++) {
+				int type = input.readByte(), frameCount = input.readInt(true), bezierCount = input.readInt(true);
+				switch (type) {
+				case BONE_ROTATE:
+					timelines.add(readTimeline(input, new RotateTimeline(frameCount, bezierCount, boneIndex), 1));
+					break;
+				case BONE_TRANSLATE:
+					timelines.add(readTimeline(input, new TranslateTimeline(frameCount, bezierCount, boneIndex), scale));
+					break;
+				case BONE_TRANSLATEX:
+					timelines.add(readTimeline(input, new TranslateXTimeline(frameCount, bezierCount, boneIndex), scale));
+					break;
+				case BONE_TRANSLATEY:
+					timelines.add(readTimeline(input, new TranslateYTimeline(frameCount, bezierCount, boneIndex), scale));
+					break;
+				case BONE_SCALE:
+					timelines.add(readTimeline(input, new ScaleTimeline(frameCount, bezierCount, boneIndex), 1));
+					break;
+				case BONE_SCALEX:
+					timelines.add(readTimeline(input, new ScaleXTimeline(frameCount, bezierCount, boneIndex), 1));
+					break;
+				case BONE_SCALEY:
+					timelines.add(readTimeline(input, new ScaleYTimeline(frameCount, bezierCount, boneIndex), 1));
+					break;
+				case BONE_SHEAR:
+					timelines.add(readTimeline(input, new ShearTimeline(frameCount, bezierCount, boneIndex), 1));
+					break;
+				case BONE_SHEARX:
+					timelines.add(readTimeline(input, new ShearXTimeline(frameCount, bezierCount, boneIndex), 1));
+					break;
+				case BONE_SHEARY:
+					timelines.add(readTimeline(input, new ShearYTimeline(frameCount, bezierCount, boneIndex), 1));
+				}
+			}
+		}
+
+		// IK constraint timelines.
+		for (int i = 0, n = input.readInt(true); i < n; i++) {
+			int index = input.readInt(true), frameCount = input.readInt(true), frameLast = frameCount - 1;
+			IkConstraintTimeline timeline = new IkConstraintTimeline(frameCount, input.readInt(true), index);
+			float time = input.readFloat(), mix = input.readFloat(), softness = input.readFloat() * scale;
+			for (int frame = 0, bezier = 0;; frame++) {
+				timeline.setFrame(frame, time, mix, softness, input.readByte(), input.readBoolean(), input.readBoolean());
+				if (frame == frameLast) break;
+				float time2 = input.readFloat(), mix2 = input.readFloat(), softness2 = input.readFloat() * scale;
+				switch (input.readByte()) {
+				case CURVE_STEPPED:
+					timeline.setStepped(frame);
+					break;
+				case CURVE_BEZIER:
+					setBezier(input, timeline, bezier++, frame, 0, time, time2, mix, mix2, 1);
+					setBezier(input, timeline, bezier++, frame, 1, time, time2, softness, softness2, scale);
+				}
+				time = time2;
+				mix = mix2;
+				softness = softness2;
+			}
+			timelines.add(timeline);
+		}
+
+		// Transform constraint timelines.
+		for (int i = 0, n = input.readInt(true); i < n; i++) {
+			int index = input.readInt(true), frameCount = input.readInt(true), frameLast = frameCount - 1;
+			TransformConstraintTimeline timeline = new TransformConstraintTimeline(frameCount, input.readInt(true), index);
+			float time = input.readFloat(), mixRotate = input.readFloat(), mixX = input.readFloat(), mixY = input.readFloat(),
+				mixScaleX = input.readFloat(), mixScaleY = input.readFloat(), mixShearY = input.readFloat();
+			for (int frame = 0, bezier = 0;; frame++) {
+				timeline.setFrame(frame, time, mixRotate, mixX, mixY, mixScaleX, mixScaleY, mixShearY);
+				if (frame == frameLast) break;
+				float time2 = input.readFloat(), mixRotate2 = input.readFloat(), mixX2 = input.readFloat(), mixY2 = input.readFloat(),
+					mixScaleX2 = input.readFloat(), mixScaleY2 = input.readFloat(), mixShearY2 = input.readFloat();
+				switch (input.readByte()) {
+				case CURVE_STEPPED:
+					timeline.setStepped(frame);
+					break;
+				case CURVE_BEZIER:
+					setBezier(input, timeline, bezier++, frame, 0, time, time2, mixRotate, mixRotate2, 1);
+					setBezier(input, timeline, bezier++, frame, 1, time, time2, mixX, mixX2, 1);
+					setBezier(input, timeline, bezier++, frame, 2, time, time2, mixY, mixY2, 1);
+					setBezier(input, timeline, bezier++, frame, 3, time, time2, mixScaleX, mixScaleX2, 1);
+					setBezier(input, timeline, bezier++, frame, 4, time, time2, mixScaleY, mixScaleY2, 1);
+					setBezier(input, timeline, bezier++, frame, 5, time, time2, mixShearY, mixShearY2, 1);
+				}
+				time = time2;
+				mixRotate = mixRotate2;
+				mixX = mixX2;
+				mixY = mixY2;
+				mixScaleX = mixScaleX2;
+				mixScaleY = mixScaleY2;
+				mixShearY = mixShearY2;
+			}
+			timelines.add(timeline);
+		}
+
+		// Path constraint timelines.
+		for (int i = 0, n = input.readInt(true); i < n; i++) {
+			int index = input.readInt(true);
+			PathConstraintData data = skeletonData.pathConstraints.get(index);
+			for (int ii = 0, nn = input.readInt(true); ii < nn; ii++) {
+				switch (input.readByte()) {
+				case PATH_POSITION:
+					timelines
+						.add(readTimeline(input, new PathConstraintPositionTimeline(input.readInt(true), input.readInt(true), index),
+							data.positionMode == PositionMode.fixed ? scale : 1));
+					break;
+				case PATH_SPACING:
+					timelines
+						.add(readTimeline(input, new PathConstraintSpacingTimeline(input.readInt(true), input.readInt(true), index),
+							data.spacingMode == SpacingMode.length || data.spacingMode == SpacingMode.fixed ? scale : 1));
+					break;
+				case PATH_MIX:
+					PathConstraintMixTimeline timeline = new PathConstraintMixTimeline(input.readInt(true), input.readInt(true),
+						index);
+					float time = input.readFloat(), mixRotate = input.readFloat(), mixX = input.readFloat(), mixY = input.readFloat();
+					for (int frame = 0, bezier = 0, frameLast = timeline.getFrameCount() - 1;; frame++) {
+						timeline.setFrame(frame, time, mixRotate, mixX, mixY);
+						if (frame == frameLast) break;
+						float time2 = input.readFloat(), mixRotate2 = input.readFloat(), mixX2 = input.readFloat(),
+							mixY2 = input.readFloat();
+						switch (input.readByte()) {
+						case CURVE_STEPPED:
+							timeline.setStepped(frame);
+							break;
+						case CURVE_BEZIER:
+							setBezier(input, timeline, bezier++, frame, 0, time, time2, mixRotate, mixRotate2, 1);
+							setBezier(input, timeline, bezier++, frame, 1, time, time2, mixX, mixX2, 1);
+							setBezier(input, timeline, bezier++, frame, 2, time, time2, mixY, mixY2, 1);
+						}
+						time = time2;
+						mixRotate = mixRotate2;
+						mixX = mixX2;
+						mixY = mixY2;
+					}
+					timelines.add(timeline);
+				}
+			}
+		}
+
+		// Deform timelines.
+		for (int i = 0, n = input.readInt(true); i < n; i++) {
+			Skin skin = skeletonData.skins.get(input.readInt(true));
+			for (int ii = 0, nn = input.readInt(true); ii < nn; ii++) {
+				int slotIndex = input.readInt(true);
+				for (int iii = 0, nnn = input.readInt(true); iii < nnn; iii++) {
+					String attachmentName = input.readStringRef();
+					VertexAttachment attachment = (VertexAttachment)skin.getAttachment(slotIndex, attachmentName);
+					if (attachment == null) throw new SerializationException("Vertex attachment not found: " + attachmentName);
+					boolean weighted = attachment.getBones() != null;
+					float[] vertices = attachment.getVertices();
+					int deformLength = weighted ? (vertices.length / 3) << 1 : vertices.length;
+
+					int frameCount = input.readInt(true), frameLast = frameCount - 1;
+					DeformTimeline timeline = new DeformTimeline(frameCount, input.readInt(true), slotIndex, attachment);
+
+					float time = input.readFloat();
+					for (int frame = 0, bezier = 0;; frame++) {
+						float[] deform;
+						int end = input.readInt(true);
+						if (end == 0)
+							deform = weighted ? new float[deformLength] : vertices;
+						else {
+							deform = new float[deformLength];
+							int start = input.readInt(true);
+							end += start;
+							if (scale == 1) {
+								for (int v = start; v < end; v++)
+									deform[v] = input.readFloat();
+							} else {
+								for (int v = start; v < end; v++)
+									deform[v] = input.readFloat() * scale;
+							}
+							if (!weighted) {
+								for (int v = 0, vn = deform.length; v < vn; v++)
+									deform[v] += vertices[v];
+							}
+						}
+						timeline.setFrame(frame, time, deform);
+						if (frame == frameLast) break;
+						float time2 = input.readFloat();
+						switch (input.readByte()) {
+						case CURVE_STEPPED:
+							timeline.setStepped(frame);
+							break;
+						case CURVE_BEZIER:
+							setBezier(input, timeline, bezier++, frame, 0, time, time2, 0, 1, 1);
+						}
+						time = time2;
+					}
+					timelines.add(timeline);
+				}
+			}
+		}
+
+		// Draw order timeline.
+		int drawOrderCount = input.readInt(true);
+		if (drawOrderCount > 0) {
+			DrawOrderTimeline timeline = new DrawOrderTimeline(drawOrderCount);
+			int slotCount = skeletonData.slots.size;
+			for (int i = 0; i < drawOrderCount; i++) {
+				float time = input.readFloat();
+				int offsetCount = input.readInt(true);
+				int[] drawOrder = new int[slotCount];
+				for (int ii = slotCount - 1; ii >= 0; ii--)
+					drawOrder[ii] = -1;
+				int[] unchanged = new int[slotCount - offsetCount];
+				int originalIndex = 0, unchangedIndex = 0;
+				for (int ii = 0; ii < offsetCount; ii++) {
+					int slotIndex = input.readInt(true);
+					// Collect unchanged items.
+					while (originalIndex != slotIndex)
+						unchanged[unchangedIndex++] = originalIndex++;
+					// Set changed items.
+					drawOrder[originalIndex + input.readInt(true)] = originalIndex++;
+				}
+				// Collect remaining unchanged items.
+				while (originalIndex < slotCount)
+					unchanged[unchangedIndex++] = originalIndex++;
+				// Fill in unchanged items.
+				for (int ii = slotCount - 1; ii >= 0; ii--)
+					if (drawOrder[ii] == -1) drawOrder[ii] = unchanged[--unchangedIndex];
+				timeline.setFrame(i, time, drawOrder);
+			}
+			timelines.add(timeline);
+		}
+
+		// Event timeline.
+		int eventCount = input.readInt(true);
+		if (eventCount > 0) {
+			EventTimeline timeline = new EventTimeline(eventCount);
+			for (int i = 0; i < eventCount; i++) {
+				float time = input.readFloat();
+				EventData eventData = skeletonData.events.get(input.readInt(true));
+				Event event = new Event(time, eventData);
+				event.intValue = input.readInt(false);
+				event.floatValue = input.readFloat();
+				event.stringValue = input.readBoolean() ? input.readString() : eventData.stringValue;
+				if (event.getData().audioPath != null) {
+					event.volume = input.readFloat();
+					event.balance = input.readFloat();
+				}
+				timeline.setFrame(i, event);
+			}
+			timelines.add(timeline);
+		}
+
+		float duration = 0;
+		Object[] items = timelines.items;
+		for (int i = 0, n = timelines.size; i < n; i++)
+			duration = Math.max(duration, ((Timeline)items[i]).getDuration());
+		return new Animation(name, timelines, duration);
+	}
+
+	private Timeline readTimeline (SkeletonInput input, CurveTimeline1 timeline, float scale) throws IOException {
+		float time = input.readFloat(), value = input.readFloat() * scale;
+		for (int frame = 0, bezier = 0, frameLast = timeline.getFrameCount() - 1;; frame++) {
+			timeline.setFrame(frame, time, value);
+			if (frame == frameLast) break;
+			float time2 = input.readFloat(), value2 = input.readFloat() * scale;
+			switch (input.readByte()) {
+			case CURVE_STEPPED:
+				timeline.setStepped(frame);
+				break;
+			case CURVE_BEZIER:
+				setBezier(input, timeline, bezier++, frame, 0, time, time2, value, value2, 1);
+			}
+			time = time2;
+			value = value2;
+		}
+		return timeline;
+	}
+
+	private Timeline readTimeline (SkeletonInput input, CurveTimeline2 timeline, float scale) throws IOException {
+		float time = input.readFloat(), value1 = input.readFloat() * scale, value2 = input.readFloat() * scale;
+		for (int frame = 0, bezier = 0, frameLast = timeline.getFrameCount() - 1;; frame++) {
+			timeline.setFrame(frame, time, value1, value2);
+			if (frame == frameLast) break;
+			float time2 = input.readFloat(), nvalue1 = input.readFloat() * scale, nvalue2 = input.readFloat() * scale;
+			switch (input.readByte()) {
+			case CURVE_STEPPED:
+				timeline.setStepped(frame);
+				break;
+			case CURVE_BEZIER:
+				setBezier(input, timeline, bezier++, frame, 0, time, time2, value1, nvalue1, scale);
+				setBezier(input, timeline, bezier++, frame, 1, time, time2, value2, nvalue2, scale);
+			}
+			time = time2;
+			value1 = nvalue1;
+			value2 = nvalue2;
+		}
+		return timeline;
+	}
+
+	void setBezier (SkeletonInput input, CurveTimeline timeline, int bezier, int frame, int value, float time1, float time2,
+		float value1, float value2, float scale) throws IOException {
+		timeline.setBezier(bezier, frame, value, time1, value1, input.readFloat(), input.readFloat() * scale, input.readFloat(),
+			input.readFloat() * scale, time2, value2);
+	}
+
+	static class Vertices {
+		int[] bones;
+		float[] vertices;
+	}
+
+	static class SkeletonInput extends DataInput {
+		private char[] chars = new char[32];
+		String[] strings;
+
+		public SkeletonInput (InputStream input) {
+			super(input);
+		}
+
+		public SkeletonInput (FileHandle file) {
+			super(file.read(512));
+		}
+
+		public @Null String readStringRef () throws IOException {
+			int index = readInt(true);
+			return index == 0 ? null : strings[index - 1];
+		}
+
+		public String readString () throws IOException {
+			int byteCount = readInt(true);
+			switch (byteCount) {
+			case 0:
+				return null;
+			case 1:
+				return "";
+			}
+			byteCount--;
+			if (chars.length < byteCount) chars = new char[byteCount];
+			char[] chars = this.chars;
+			int charCount = 0;
+			for (int i = 0; i < byteCount;) {
+				int b = read();
+				switch (b >> 4) {
+				case -1:
+					throw new EOFException();
+				case 12:
+				case 13:
+					chars[charCount++] = (char)((b & 0x1F) << 6 | read() & 0x3F);
+					i += 2;
+					break;
+				case 14:
+					chars[charCount++] = (char)((b & 0x0F) << 12 | (read() & 0x3F) << 6 | read() & 0x3F);
+					i += 3;
+					break;
+				default:
+					chars[charCount++] = (char)b;
+					i++;
+				}
+			}
+			return new String(chars, 0, charCount);
+		}
+	}
+}

+ 254 - 254
spine-libgdx/spine-libgdx/src/com/esotericsoftware/spine/SkeletonBounds.java

@@ -1,254 +1,254 @@
-/******************************************************************************
- * Spine Runtimes License Agreement
- * Last updated January 1, 2020. Replaces all prior versions.
- *
- * Copyright (c) 2013-2020, Esoteric Software LLC
- *
- * Integration of the Spine Runtimes into software or otherwise creating
- * derivative works of the Spine Runtimes is permitted under the terms and
- * conditions of Section 2 of the Spine Editor License Agreement:
- * http://esotericsoftware.com/spine-editor-license
- *
- * Otherwise, it is permitted to integrate the Spine Runtimes into software
- * or otherwise create derivative works of the Spine Runtimes (collectively,
- * "Products"), provided that each user of the Products must obtain their own
- * Spine Editor license and redistribution of the Products in any form must
- * include this license and copyright notice.
- *
- * THE SPINE RUNTIMES ARE PROVIDED BY ESOTERIC SOFTWARE LLC "AS IS" AND ANY
- * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
- * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
- * DISCLAIMED. IN NO EVENT SHALL ESOTERIC SOFTWARE LLC BE LIABLE FOR ANY
- * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
- * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES,
- * BUSINESS INTERRUPTION, OR LOSS OF USE, DATA, OR PROFITS) HOWEVER CAUSED AND
- * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
- * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
- * THE SPINE RUNTIMES, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
- *****************************************************************************/
-
-package com.esotericsoftware.spine;
-
-import com.badlogic.gdx.utils.Array;
-import com.badlogic.gdx.utils.FloatArray;
-import com.badlogic.gdx.utils.Null;
-import com.badlogic.gdx.utils.Pool;
-
-import com.esotericsoftware.spine.attachments.Attachment;
-import com.esotericsoftware.spine.attachments.BoundingBoxAttachment;
-
-/** Collects each visible {@link BoundingBoxAttachment} and computes the world vertices for its polygon. The polygon vertices are
- * provided along with convenience methods for doing hit detection. */
-public class SkeletonBounds {
-	private float minX, minY, maxX, maxY;
-	private Array<BoundingBoxAttachment> boundingBoxes = new Array();
-	private Array<FloatArray> polygons = new Array();
-	private Pool<FloatArray> polygonPool = new Pool() {
-		protected Object newObject () {
-			return new FloatArray();
-		}
-	};
-
-	/** Clears any previous polygons, finds all visible bounding box attachments, and computes the world vertices for each bounding
-	 * box's polygon.
-	 * @param updateAabb If true, the axis aligned bounding box containing all the polygons is computed. If false, the
-	 *           SkeletonBounds AABB methods will always return true. */
-	public void update (Skeleton skeleton, boolean updateAabb) {
-		if (skeleton == null) throw new IllegalArgumentException("skeleton cannot be null.");
-		Array<BoundingBoxAttachment> boundingBoxes = this.boundingBoxes;
-		Array<FloatArray> polygons = this.polygons;
-		Object[] slots = skeleton.slots.items;
-		int slotCount = skeleton.slots.size;
-
-		boundingBoxes.clear();
-		polygonPool.freeAll(polygons);
-		polygons.clear();
-
-		for (int i = 0; i < slotCount; i++) {
-			Slot slot = (Slot)slots[i];
-			if (!slot.bone.active) continue;
-			Attachment attachment = slot.attachment;
-			if (attachment instanceof BoundingBoxAttachment) {
-				BoundingBoxAttachment boundingBox = (BoundingBoxAttachment)attachment;
-				boundingBoxes.add(boundingBox);
-
-				FloatArray polygon = polygonPool.obtain();
-				polygons.add(polygon);
-				boundingBox.computeWorldVertices(slot, 0, boundingBox.getWorldVerticesLength(),
-					polygon.setSize(boundingBox.getWorldVerticesLength()), 0, 2);
-			}
-		}
-
-		if (updateAabb)
-			aabbCompute();
-		else {
-			minX = Integer.MIN_VALUE;
-			minY = Integer.MIN_VALUE;
-			maxX = Integer.MAX_VALUE;
-			maxY = Integer.MAX_VALUE;
-		}
-	}
-
-	private void aabbCompute () {
-		float minX = Integer.MAX_VALUE, minY = Integer.MAX_VALUE, maxX = Integer.MIN_VALUE, maxY = Integer.MIN_VALUE;
-		Object[] polygons = this.polygons.items;
-		for (int i = 0, n = this.polygons.size; i < n; i++) {
-			FloatArray polygon = (FloatArray)polygons[i];
-			float[] vertices = polygon.items;
-			for (int ii = 0, nn = polygon.size; ii < nn; ii += 2) {
-				float x = vertices[ii];
-				float y = vertices[ii + 1];
-				minX = Math.min(minX, x);
-				minY = Math.min(minY, y);
-				maxX = Math.max(maxX, x);
-				maxY = Math.max(maxY, y);
-			}
-		}
-		this.minX = minX;
-		this.minY = minY;
-		this.maxX = maxX;
-		this.maxY = maxY;
-	}
-
-	/** Returns true if the axis aligned bounding box contains the point. */
-	public boolean aabbContainsPoint (float x, float y) {
-		return x >= minX && x <= maxX && y >= minY && y <= maxY;
-	}
-
-	/** Returns true if the axis aligned bounding box intersects the line segment. */
-	public boolean aabbIntersectsSegment (float x1, float y1, float x2, float y2) {
-		float minX = this.minX;
-		float minY = this.minY;
-		float maxX = this.maxX;
-		float maxY = this.maxY;
-		if ((x1 <= minX && x2 <= minX) || (y1 <= minY && y2 <= minY) || (x1 >= maxX && x2 >= maxX) || (y1 >= maxY && y2 >= maxY))
-			return false;
-		float m = (y2 - y1) / (x2 - x1);
-		float y = m * (minX - x1) + y1;
-		if (y > minY && y < maxY) return true;
-		y = m * (maxX - x1) + y1;
-		if (y > minY && y < maxY) return true;
-		float x = (minY - y1) / m + x1;
-		if (x > minX && x < maxX) return true;
-		x = (maxY - y1) / m + x1;
-		if (x > minX && x < maxX) return true;
-		return false;
-	}
-
-	/** Returns true if the axis aligned bounding box intersects the axis aligned bounding box of the specified bounds. */
-	public boolean aabbIntersectsSkeleton (SkeletonBounds bounds) {
-		if (bounds == null) throw new IllegalArgumentException("bounds cannot be null.");
-		return minX < bounds.maxX && maxX > bounds.minX && minY < bounds.maxY && maxY > bounds.minY;
-	}
-
-	/** Returns the first bounding box attachment that contains the point, or null. When doing many checks, it is usually more
-	 * efficient to only call this method if {@link #aabbContainsPoint(float, float)} returns true. */
-	public @Null BoundingBoxAttachment containsPoint (float x, float y) {
-		Object[] polygons = this.polygons.items;
-		for (int i = 0, n = this.polygons.size; i < n; i++)
-			if (containsPoint((FloatArray)polygons[i], x, y)) return boundingBoxes.get(i);
-		return null;
-	}
-
-	/** Returns true if the polygon contains the point. */
-	public boolean containsPoint (FloatArray polygon, float x, float y) {
-		if (polygon == null) throw new IllegalArgumentException("polygon cannot be null.");
-		float[] vertices = polygon.items;
-		int nn = polygon.size;
-
-		int prevIndex = nn - 2;
-		boolean inside = false;
-		for (int ii = 0; ii < nn; ii += 2) {
-			float vertexY = vertices[ii + 1];
-			float prevY = vertices[prevIndex + 1];
-			if ((vertexY < y && prevY >= y) || (prevY < y && vertexY >= y)) {
-				float vertexX = vertices[ii];
-				if (vertexX + (y - vertexY) / (prevY - vertexY) * (vertices[prevIndex] - vertexX) < x) inside = !inside;
-			}
-			prevIndex = ii;
-		}
-		return inside;
-	}
-
-	/** Returns the first bounding box attachment that contains any part of the line segment, or null. When doing many checks, it
-	 * is usually more efficient to only call this method if {@link #aabbIntersectsSegment(float, float, float, float)} returns
-	 * true. */
-	public @Null BoundingBoxAttachment intersectsSegment (float x1, float y1, float x2, float y2) {
-		Object[] polygons = this.polygons.items;
-		for (int i = 0, n = this.polygons.size; i < n; i++)
-			if (intersectsSegment((FloatArray)polygons[i], x1, y1, x2, y2)) return boundingBoxes.get(i);
-		return null;
-	}
-
-	/** Returns true if the polygon contains any part of the line segment. */
-	public boolean intersectsSegment (FloatArray polygon, float x1, float y1, float x2, float y2) {
-		if (polygon == null) throw new IllegalArgumentException("polygon cannot be null.");
-		float[] vertices = polygon.items;
-		int nn = polygon.size;
-
-		float width12 = x1 - x2, height12 = y1 - y2;
-		float det1 = x1 * y2 - y1 * x2;
-		float x3 = vertices[nn - 2], y3 = vertices[nn - 1];
-		for (int ii = 0; ii < nn; ii += 2) {
-			float x4 = vertices[ii], y4 = vertices[ii + 1];
-			float det2 = x3 * y4 - y3 * x4;
-			float width34 = x3 - x4, height34 = y3 - y4;
-			float det3 = width12 * height34 - height12 * width34;
-			float x = (det1 * width34 - width12 * det2) / det3;
-			if (((x >= x3 && x <= x4) || (x >= x4 && x <= x3)) && ((x >= x1 && x <= x2) || (x >= x2 && x <= x1))) {
-				float y = (det1 * height34 - height12 * det2) / det3;
-				if (((y >= y3 && y <= y4) || (y >= y4 && y <= y3)) && ((y >= y1 && y <= y2) || (y >= y2 && y <= y1))) return true;
-			}
-			x3 = x4;
-			y3 = y4;
-		}
-		return false;
-	}
-
-	/** The left edge of the axis aligned bounding box. */
-	public float getMinX () {
-		return minX;
-	}
-
-	/** The bottom edge of the axis aligned bounding box. */
-	public float getMinY () {
-		return minY;
-	}
-
-	/** The right edge of the axis aligned bounding box. */
-	public float getMaxX () {
-		return maxX;
-	}
-
-	/** The top edge of the axis aligned bounding box. */
-	public float getMaxY () {
-		return maxY;
-	}
-
-	/** The width of the axis aligned bounding box. */
-	public float getWidth () {
-		return maxX - minX;
-	}
-
-	/** The height of the axis aligned bounding box. */
-	public float getHeight () {
-		return maxY - minY;
-	}
-
-	/** The visible bounding boxes. */
-	public Array<BoundingBoxAttachment> getBoundingBoxes () {
-		return boundingBoxes;
-	}
-
-	/** The world vertices for the bounding box polygons. */
-	public Array<FloatArray> getPolygons () {
-		return polygons;
-	}
-
-	/** Returns the polygon for the specified bounding box, or null. */
-	public @Null FloatArray getPolygon (BoundingBoxAttachment boundingBox) {
-		if (boundingBox == null) throw new IllegalArgumentException("boundingBox cannot be null.");
-		int index = boundingBoxes.indexOf(boundingBox, true);
-		return index == -1 ? null : polygons.get(index);
-	}
-}
+/******************************************************************************
+ * Spine Runtimes License Agreement
+ * Last updated January 1, 2020. Replaces all prior versions.
+ *
+ * Copyright (c) 2013-2020, Esoteric Software LLC
+ *
+ * Integration of the Spine Runtimes into software or otherwise creating
+ * derivative works of the Spine Runtimes is permitted under the terms and
+ * conditions of Section 2 of the Spine Editor License Agreement:
+ * http://esotericsoftware.com/spine-editor-license
+ *
+ * Otherwise, it is permitted to integrate the Spine Runtimes into software
+ * or otherwise create derivative works of the Spine Runtimes (collectively,
+ * "Products"), provided that each user of the Products must obtain their own
+ * Spine Editor license and redistribution of the Products in any form must
+ * include this license and copyright notice.
+ *
+ * THE SPINE RUNTIMES ARE PROVIDED BY ESOTERIC SOFTWARE LLC "AS IS" AND ANY
+ * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL ESOTERIC SOFTWARE LLC BE LIABLE FOR ANY
+ * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES,
+ * BUSINESS INTERRUPTION, OR LOSS OF USE, DATA, OR PROFITS) HOWEVER CAUSED AND
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
+ * THE SPINE RUNTIMES, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ *****************************************************************************/
+
+package com.esotericsoftware.spine;
+
+import com.badlogic.gdx.utils.Array;
+import com.badlogic.gdx.utils.FloatArray;
+import com.badlogic.gdx.utils.Null;
+import com.badlogic.gdx.utils.Pool;
+
+import com.esotericsoftware.spine.attachments.Attachment;
+import com.esotericsoftware.spine.attachments.BoundingBoxAttachment;
+
+/** Collects each visible {@link BoundingBoxAttachment} and computes the world vertices for its polygon. The polygon vertices are
+ * provided along with convenience methods for doing hit detection. */
+public class SkeletonBounds {
+	private float minX, minY, maxX, maxY;
+	private Array<BoundingBoxAttachment> boundingBoxes = new Array();
+	private Array<FloatArray> polygons = new Array();
+	private Pool<FloatArray> polygonPool = new Pool() {
+		protected Object newObject () {
+			return new FloatArray();
+		}
+	};
+
+	/** Clears any previous polygons, finds all visible bounding box attachments, and computes the world vertices for each bounding
+	 * box's polygon.
+	 * @param updateAabb If true, the axis aligned bounding box containing all the polygons is computed. If false, the
+	 *           SkeletonBounds AABB methods will always return true. */
+	public void update (Skeleton skeleton, boolean updateAabb) {
+		if (skeleton == null) throw new IllegalArgumentException("skeleton cannot be null.");
+		Array<BoundingBoxAttachment> boundingBoxes = this.boundingBoxes;
+		Array<FloatArray> polygons = this.polygons;
+		Object[] slots = skeleton.slots.items;
+		int slotCount = skeleton.slots.size;
+
+		boundingBoxes.clear();
+		polygonPool.freeAll(polygons);
+		polygons.clear();
+
+		for (int i = 0; i < slotCount; i++) {
+			Slot slot = (Slot)slots[i];
+			if (!slot.bone.active) continue;
+			Attachment attachment = slot.attachment;
+			if (attachment instanceof BoundingBoxAttachment) {
+				BoundingBoxAttachment boundingBox = (BoundingBoxAttachment)attachment;
+				boundingBoxes.add(boundingBox);
+
+				FloatArray polygon = polygonPool.obtain();
+				polygons.add(polygon);
+				boundingBox.computeWorldVertices(slot, 0, boundingBox.getWorldVerticesLength(),
+					polygon.setSize(boundingBox.getWorldVerticesLength()), 0, 2);
+			}
+		}
+
+		if (updateAabb)
+			aabbCompute();
+		else {
+			minX = Integer.MIN_VALUE;
+			minY = Integer.MIN_VALUE;
+			maxX = Integer.MAX_VALUE;
+			maxY = Integer.MAX_VALUE;
+		}
+	}
+
+	private void aabbCompute () {
+		float minX = Integer.MAX_VALUE, minY = Integer.MAX_VALUE, maxX = Integer.MIN_VALUE, maxY = Integer.MIN_VALUE;
+		Object[] polygons = this.polygons.items;
+		for (int i = 0, n = this.polygons.size; i < n; i++) {
+			FloatArray polygon = (FloatArray)polygons[i];
+			float[] vertices = polygon.items;
+			for (int ii = 0, nn = polygon.size; ii < nn; ii += 2) {
+				float x = vertices[ii];
+				float y = vertices[ii + 1];
+				minX = Math.min(minX, x);
+				minY = Math.min(minY, y);
+				maxX = Math.max(maxX, x);
+				maxY = Math.max(maxY, y);
+			}
+		}
+		this.minX = minX;
+		this.minY = minY;
+		this.maxX = maxX;
+		this.maxY = maxY;
+	}
+
+	/** Returns true if the axis aligned bounding box contains the point. */
+	public boolean aabbContainsPoint (float x, float y) {
+		return x >= minX && x <= maxX && y >= minY && y <= maxY;
+	}
+
+	/** Returns true if the axis aligned bounding box intersects the line segment. */
+	public boolean aabbIntersectsSegment (float x1, float y1, float x2, float y2) {
+		float minX = this.minX;
+		float minY = this.minY;
+		float maxX = this.maxX;
+		float maxY = this.maxY;
+		if ((x1 <= minX && x2 <= minX) || (y1 <= minY && y2 <= minY) || (x1 >= maxX && x2 >= maxX) || (y1 >= maxY && y2 >= maxY))
+			return false;
+		float m = (y2 - y1) / (x2 - x1);
+		float y = m * (minX - x1) + y1;
+		if (y > minY && y < maxY) return true;
+		y = m * (maxX - x1) + y1;
+		if (y > minY && y < maxY) return true;
+		float x = (minY - y1) / m + x1;
+		if (x > minX && x < maxX) return true;
+		x = (maxY - y1) / m + x1;
+		if (x > minX && x < maxX) return true;
+		return false;
+	}
+
+	/** Returns true if the axis aligned bounding box intersects the axis aligned bounding box of the specified bounds. */
+	public boolean aabbIntersectsSkeleton (SkeletonBounds bounds) {
+		if (bounds == null) throw new IllegalArgumentException("bounds cannot be null.");
+		return minX < bounds.maxX && maxX > bounds.minX && minY < bounds.maxY && maxY > bounds.minY;
+	}
+
+	/** Returns the first bounding box attachment that contains the point, or null. When doing many checks, it is usually more
+	 * efficient to only call this method if {@link #aabbContainsPoint(float, float)} returns true. */
+	public @Null BoundingBoxAttachment containsPoint (float x, float y) {
+		Object[] polygons = this.polygons.items;
+		for (int i = 0, n = this.polygons.size; i < n; i++)
+			if (containsPoint((FloatArray)polygons[i], x, y)) return boundingBoxes.get(i);
+		return null;
+	}
+
+	/** Returns true if the polygon contains the point. */
+	public boolean containsPoint (FloatArray polygon, float x, float y) {
+		if (polygon == null) throw new IllegalArgumentException("polygon cannot be null.");
+		float[] vertices = polygon.items;
+		int nn = polygon.size;
+
+		int prevIndex = nn - 2;
+		boolean inside = false;
+		for (int ii = 0; ii < nn; ii += 2) {
+			float vertexY = vertices[ii + 1];
+			float prevY = vertices[prevIndex + 1];
+			if ((vertexY < y && prevY >= y) || (prevY < y && vertexY >= y)) {
+				float vertexX = vertices[ii];
+				if (vertexX + (y - vertexY) / (prevY - vertexY) * (vertices[prevIndex] - vertexX) < x) inside = !inside;
+			}
+			prevIndex = ii;
+		}
+		return inside;
+	}
+
+	/** Returns the first bounding box attachment that contains any part of the line segment, or null. When doing many checks, it
+	 * is usually more efficient to only call this method if {@link #aabbIntersectsSegment(float, float, float, float)} returns
+	 * true. */
+	public @Null BoundingBoxAttachment intersectsSegment (float x1, float y1, float x2, float y2) {
+		Object[] polygons = this.polygons.items;
+		for (int i = 0, n = this.polygons.size; i < n; i++)
+			if (intersectsSegment((FloatArray)polygons[i], x1, y1, x2, y2)) return boundingBoxes.get(i);
+		return null;
+	}
+
+	/** Returns true if the polygon contains any part of the line segment. */
+	public boolean intersectsSegment (FloatArray polygon, float x1, float y1, float x2, float y2) {
+		if (polygon == null) throw new IllegalArgumentException("polygon cannot be null.");
+		float[] vertices = polygon.items;
+		int nn = polygon.size;
+
+		float width12 = x1 - x2, height12 = y1 - y2;
+		float det1 = x1 * y2 - y1 * x2;
+		float x3 = vertices[nn - 2], y3 = vertices[nn - 1];
+		for (int ii = 0; ii < nn; ii += 2) {
+			float x4 = vertices[ii], y4 = vertices[ii + 1];
+			float det2 = x3 * y4 - y3 * x4;
+			float width34 = x3 - x4, height34 = y3 - y4;
+			float det3 = width12 * height34 - height12 * width34;
+			float x = (det1 * width34 - width12 * det2) / det3;
+			if (((x >= x3 && x <= x4) || (x >= x4 && x <= x3)) && ((x >= x1 && x <= x2) || (x >= x2 && x <= x1))) {
+				float y = (det1 * height34 - height12 * det2) / det3;
+				if (((y >= y3 && y <= y4) || (y >= y4 && y <= y3)) && ((y >= y1 && y <= y2) || (y >= y2 && y <= y1))) return true;
+			}
+			x3 = x4;
+			y3 = y4;
+		}
+		return false;
+	}
+
+	/** The left edge of the axis aligned bounding box. */
+	public float getMinX () {
+		return minX;
+	}
+
+	/** The bottom edge of the axis aligned bounding box. */
+	public float getMinY () {
+		return minY;
+	}
+
+	/** The right edge of the axis aligned bounding box. */
+	public float getMaxX () {
+		return maxX;
+	}
+
+	/** The top edge of the axis aligned bounding box. */
+	public float getMaxY () {
+		return maxY;
+	}
+
+	/** The width of the axis aligned bounding box. */
+	public float getWidth () {
+		return maxX - minX;
+	}
+
+	/** The height of the axis aligned bounding box. */
+	public float getHeight () {
+		return maxY - minY;
+	}
+
+	/** The visible bounding boxes. */
+	public Array<BoundingBoxAttachment> getBoundingBoxes () {
+		return boundingBoxes;
+	}
+
+	/** The world vertices for the bounding box polygons. */
+	public Array<FloatArray> getPolygons () {
+		return polygons;
+	}
+
+	/** Returns the polygon for the specified bounding box, or null. */
+	public @Null FloatArray getPolygon (BoundingBoxAttachment boundingBox) {
+		if (boundingBox == null) throw new IllegalArgumentException("boundingBox cannot be null.");
+		int index = boundingBoxes.indexOf(boundingBox, true);
+		return index == -1 ? null : polygons.get(index);
+	}
+}

+ 314 - 314
spine-libgdx/spine-libgdx/src/com/esotericsoftware/spine/SkeletonData.java

@@ -1,314 +1,314 @@
-/******************************************************************************
- * Spine Runtimes License Agreement
- * Last updated January 1, 2020. Replaces all prior versions.
- *
- * Copyright (c) 2013-2020, Esoteric Software LLC
- *
- * Integration of the Spine Runtimes into software or otherwise creating
- * derivative works of the Spine Runtimes is permitted under the terms and
- * conditions of Section 2 of the Spine Editor License Agreement:
- * http://esotericsoftware.com/spine-editor-license
- *
- * Otherwise, it is permitted to integrate the Spine Runtimes into software
- * or otherwise create derivative works of the Spine Runtimes (collectively,
- * "Products"), provided that each user of the Products must obtain their own
- * Spine Editor license and redistribution of the Products in any form must
- * include this license and copyright notice.
- *
- * THE SPINE RUNTIMES ARE PROVIDED BY ESOTERIC SOFTWARE LLC "AS IS" AND ANY
- * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
- * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
- * DISCLAIMED. IN NO EVENT SHALL ESOTERIC SOFTWARE LLC BE LIABLE FOR ANY
- * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
- * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES,
- * BUSINESS INTERRUPTION, OR LOSS OF USE, DATA, OR PROFITS) HOWEVER CAUSED AND
- * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
- * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
- * THE SPINE RUNTIMES, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
- *****************************************************************************/
-
-package com.esotericsoftware.spine;
-
-import com.badlogic.gdx.utils.Array;
-import com.badlogic.gdx.utils.Null;
-
-/** Stores the setup pose and all of the stateless data for a skeleton.
- * <p>
- * See <a href="http://esotericsoftware.com/spine-runtime-architecture#Data-objects">Data objects</a> in the Spine Runtimes
- * Guide. */
-public class SkeletonData {
-	@Null String name;
-	final Array<BoneData> bones = new Array(); // Ordered parents first.
-	final Array<SlotData> slots = new Array(); // Setup pose draw order.
-	final Array<Skin> skins = new Array();
-	@Null Skin defaultSkin;
-	final Array<EventData> events = new Array();
-	final Array<Animation> animations = new Array();
-	final Array<IkConstraintData> ikConstraints = new Array();
-	final Array<TransformConstraintData> transformConstraints = new Array();
-	final Array<PathConstraintData> pathConstraints = new Array();
-	float x, y, width, height;
-	@Null String version, hash;
-
-	// Nonessential.
-	float fps = 30;
-	@Null String imagesPath, audioPath;
-
-	// --- Bones.
-
-	public SkeletonData () {
-		super();
-	}
-
-	/** The skeleton's bones, sorted parent first. The root bone is always the first bone. */
-	public Array<BoneData> getBones () {
-		return bones;
-	}
-
-	/** Finds a bone by comparing each bone's name. It is more efficient to cache the results of this method than to call it
-	 * multiple times. */
-	public @Null BoneData findBone (String boneName) {
-		if (boneName == null) throw new IllegalArgumentException("boneName cannot be null.");
-		Object[] bones = this.bones.items;
-		for (int i = 0, n = this.bones.size; i < n; i++) {
-			BoneData bone = (BoneData)bones[i];
-			if (bone.name.equals(boneName)) return bone;
-		}
-		return null;
-	}
-
-	// --- Slots.
-
-	/** The skeleton's slots. */
-	public Array<SlotData> getSlots () {
-		return slots;
-	}
-
-	/** Finds a slot by comparing each slot's name. It is more efficient to cache the results of this method than to call it
-	 * multiple times. */
-	public @Null SlotData findSlot (String slotName) {
-		if (slotName == null) throw new IllegalArgumentException("slotName cannot be null.");
-		Object[] slots = this.slots.items;
-		for (int i = 0, n = this.slots.size; i < n; i++) {
-			SlotData slot = (SlotData)slots[i];
-			if (slot.name.equals(slotName)) return slot;
-		}
-		return null;
-	}
-
-	// --- Skins.
-
-	/** The skeleton's default skin. By default this skin contains all attachments that were not in a skin in Spine.
-	 * <p>
-	 * See {@link Skeleton#getAttachment(int, String)}. */
-	public @Null Skin getDefaultSkin () {
-		return defaultSkin;
-	}
-
-	public void setDefaultSkin (@Null Skin defaultSkin) {
-		this.defaultSkin = defaultSkin;
-	}
-
-	/** Finds a skin by comparing each skin's name. It is more efficient to cache the results of this method than to call it
-	 * multiple times. */
-	public @Null Skin findSkin (String skinName) {
-		if (skinName == null) throw new IllegalArgumentException("skinName cannot be null.");
-		for (Skin skin : skins)
-			if (skin.name.equals(skinName)) return skin;
-		return null;
-	}
-
-	/** All skins, including the default skin. */
-	public Array<Skin> getSkins () {
-		return skins;
-	}
-
-	// --- Events.
-
-	/** Finds an event by comparing each events's name. It is more efficient to cache the results of this method than to call it
-	 * multiple times. */
-	public @Null EventData findEvent (String eventDataName) {
-		if (eventDataName == null) throw new IllegalArgumentException("eventDataName cannot be null.");
-		for (EventData eventData : events)
-			if (eventData.name.equals(eventDataName)) return eventData;
-		return null;
-	}
-
-	/** The skeleton's events. */
-	public Array<EventData> getEvents () {
-		return events;
-	}
-
-	// --- Animations.
-
-	/** The skeleton's animations. */
-	public Array<Animation> getAnimations () {
-		return animations;
-	}
-
-	/** Finds an animation by comparing each animation's name. It is more efficient to cache the results of this method than to
-	 * call it multiple times. */
-	public @Null Animation findAnimation (String animationName) {
-		if (animationName == null) throw new IllegalArgumentException("animationName cannot be null.");
-		Object[] animations = this.animations.items;
-		for (int i = 0, n = this.animations.size; i < n; i++) {
-			Animation animation = (Animation)animations[i];
-			if (animation.name.equals(animationName)) return animation;
-		}
-		return null;
-	}
-
-	// --- IK constraints
-
-	/** The skeleton's IK constraints. */
-	public Array<IkConstraintData> getIkConstraints () {
-		return ikConstraints;
-	}
-
-	/** Finds an IK constraint by comparing each IK constraint's name. It is more efficient to cache the results of this method
-	 * than to call it multiple times. */
-	public @Null IkConstraintData findIkConstraint (String constraintName) {
-		if (constraintName == null) throw new IllegalArgumentException("constraintName cannot be null.");
-		Object[] ikConstraints = this.ikConstraints.items;
-		for (int i = 0, n = this.ikConstraints.size; i < n; i++) {
-			IkConstraintData constraint = (IkConstraintData)ikConstraints[i];
-			if (constraint.name.equals(constraintName)) return constraint;
-		}
-		return null;
-	}
-
-	// --- Transform constraints
-
-	/** The skeleton's transform constraints. */
-	public Array<TransformConstraintData> getTransformConstraints () {
-		return transformConstraints;
-	}
-
-	/** Finds a transform constraint by comparing each transform constraint's name. It is more efficient to cache the results of
-	 * this method than to call it multiple times. */
-	public @Null TransformConstraintData findTransformConstraint (String constraintName) {
-		if (constraintName == null) throw new IllegalArgumentException("constraintName cannot be null.");
-		Object[] transformConstraints = this.transformConstraints.items;
-		for (int i = 0, n = this.transformConstraints.size; i < n; i++) {
-			TransformConstraintData constraint = (TransformConstraintData)transformConstraints[i];
-			if (constraint.name.equals(constraintName)) return constraint;
-		}
-		return null;
-	}
-
-	// --- Path constraints
-
-	/** The skeleton's path constraints. */
-	public Array<PathConstraintData> getPathConstraints () {
-		return pathConstraints;
-	}
-
-	/** Finds a path constraint by comparing each path constraint's name. It is more efficient to cache the results of this method
-	 * than to call it multiple times. */
-	public @Null PathConstraintData findPathConstraint (String constraintName) {
-		if (constraintName == null) throw new IllegalArgumentException("constraintName cannot be null.");
-		Object[] pathConstraints = this.pathConstraints.items;
-		for (int i = 0, n = this.pathConstraints.size; i < n; i++) {
-			PathConstraintData constraint = (PathConstraintData)pathConstraints[i];
-			if (constraint.name.equals(constraintName)) return constraint;
-		}
-		return null;
-	}
-
-	// ---
-
-	/** The skeleton's name, which by default is the name of the skeleton data file when possible, or null when a name hasn't been
-	 * set. */
-	public @Null String getName () {
-		return name;
-	}
-
-	public void setName (@Null String name) {
-		this.name = name;
-	}
-
-	/** The X coordinate of the skeleton's axis aligned bounding box in the setup pose. */
-	public float getX () {
-		return x;
-	}
-
-	public void setX (float x) {
-		this.x = x;
-	}
-
-	/** The Y coordinate of the skeleton's axis aligned bounding box in the setup pose. */
-	public float getY () {
-		return y;
-	}
-
-	public void setY (float y) {
-		this.y = y;
-	}
-
-	/** The width of the skeleton's axis aligned bounding box in the setup pose. */
-	public float getWidth () {
-		return width;
-	}
-
-	public void setWidth (float width) {
-		this.width = width;
-	}
-
-	/** The height of the skeleton's axis aligned bounding box in the setup pose. */
-	public float getHeight () {
-		return height;
-	}
-
-	public void setHeight (float height) {
-		this.height = height;
-	}
-
-	/** The Spine version used to export the skeleton data, or null. */
-	public @Null String getVersion () {
-		return version;
-	}
-
-	public void setVersion (@Null String version) {
-		this.version = version;
-	}
-
-	/** The skeleton data hash. This value will change if any of the skeleton data has changed. */
-	public @Null String getHash () {
-		return hash;
-	}
-
-	public void setHash (@Null String hash) {
-		this.hash = hash;
-	}
-
-	/** The path to the images directory as defined in Spine, or null if nonessential data was not exported. */
-	public @Null String getImagesPath () {
-		return imagesPath;
-	}
-
-	public void setImagesPath (@Null String imagesPath) {
-		this.imagesPath = imagesPath;
-	}
-
-	/** The path to the audio directory as defined in Spine, or null if nonessential data was not exported. */
-	public @Null String getAudioPath () {
-		return audioPath;
-	}
-
-	public void setAudioPath (@Null String audioPath) {
-		this.audioPath = audioPath;
-	}
-
-	/** The dopesheet FPS in Spine, or zero if nonessential data was not exported. */
-	public float getFps () {
-		return fps;
-	}
-
-	public void setFps (float fps) {
-		this.fps = fps;
-	}
-
-	public String toString () {
-		return name != null ? name : super.toString();
-	}
-}
+/******************************************************************************
+ * Spine Runtimes License Agreement
+ * Last updated January 1, 2020. Replaces all prior versions.
+ *
+ * Copyright (c) 2013-2020, Esoteric Software LLC
+ *
+ * Integration of the Spine Runtimes into software or otherwise creating
+ * derivative works of the Spine Runtimes is permitted under the terms and
+ * conditions of Section 2 of the Spine Editor License Agreement:
+ * http://esotericsoftware.com/spine-editor-license
+ *
+ * Otherwise, it is permitted to integrate the Spine Runtimes into software
+ * or otherwise create derivative works of the Spine Runtimes (collectively,
+ * "Products"), provided that each user of the Products must obtain their own
+ * Spine Editor license and redistribution of the Products in any form must
+ * include this license and copyright notice.
+ *
+ * THE SPINE RUNTIMES ARE PROVIDED BY ESOTERIC SOFTWARE LLC "AS IS" AND ANY
+ * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL ESOTERIC SOFTWARE LLC BE LIABLE FOR ANY
+ * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES,
+ * BUSINESS INTERRUPTION, OR LOSS OF USE, DATA, OR PROFITS) HOWEVER CAUSED AND
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
+ * THE SPINE RUNTIMES, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ *****************************************************************************/
+
+package com.esotericsoftware.spine;
+
+import com.badlogic.gdx.utils.Array;
+import com.badlogic.gdx.utils.Null;
+
+/** Stores the setup pose and all of the stateless data for a skeleton.
+ * <p>
+ * See <a href="http://esotericsoftware.com/spine-runtime-architecture#Data-objects">Data objects</a> in the Spine Runtimes
+ * Guide. */
+public class SkeletonData {
+	@Null String name;
+	final Array<BoneData> bones = new Array(); // Ordered parents first.
+	final Array<SlotData> slots = new Array(); // Setup pose draw order.
+	final Array<Skin> skins = new Array();
+	@Null Skin defaultSkin;
+	final Array<EventData> events = new Array();
+	final Array<Animation> animations = new Array();
+	final Array<IkConstraintData> ikConstraints = new Array();
+	final Array<TransformConstraintData> transformConstraints = new Array();
+	final Array<PathConstraintData> pathConstraints = new Array();
+	float x, y, width, height;
+	@Null String version, hash;
+
+	// Nonessential.
+	float fps = 30;
+	@Null String imagesPath, audioPath;
+
+	// --- Bones.
+
+	public SkeletonData () {
+		super();
+	}
+
+	/** The skeleton's bones, sorted parent first. The root bone is always the first bone. */
+	public Array<BoneData> getBones () {
+		return bones;
+	}
+
+	/** Finds a bone by comparing each bone's name. It is more efficient to cache the results of this method than to call it
+	 * multiple times. */
+	public @Null BoneData findBone (String boneName) {
+		if (boneName == null) throw new IllegalArgumentException("boneName cannot be null.");
+		Object[] bones = this.bones.items;
+		for (int i = 0, n = this.bones.size; i < n; i++) {
+			BoneData bone = (BoneData)bones[i];
+			if (bone.name.equals(boneName)) return bone;
+		}
+		return null;
+	}
+
+	// --- Slots.
+
+	/** The skeleton's slots. */
+	public Array<SlotData> getSlots () {
+		return slots;
+	}
+
+	/** Finds a slot by comparing each slot's name. It is more efficient to cache the results of this method than to call it
+	 * multiple times. */
+	public @Null SlotData findSlot (String slotName) {
+		if (slotName == null) throw new IllegalArgumentException("slotName cannot be null.");
+		Object[] slots = this.slots.items;
+		for (int i = 0, n = this.slots.size; i < n; i++) {
+			SlotData slot = (SlotData)slots[i];
+			if (slot.name.equals(slotName)) return slot;
+		}
+		return null;
+	}
+
+	// --- Skins.
+
+	/** The skeleton's default skin. By default this skin contains all attachments that were not in a skin in Spine.
+	 * <p>
+	 * See {@link Skeleton#getAttachment(int, String)}. */
+	public @Null Skin getDefaultSkin () {
+		return defaultSkin;
+	}
+
+	public void setDefaultSkin (@Null Skin defaultSkin) {
+		this.defaultSkin = defaultSkin;
+	}
+
+	/** Finds a skin by comparing each skin's name. It is more efficient to cache the results of this method than to call it
+	 * multiple times. */
+	public @Null Skin findSkin (String skinName) {
+		if (skinName == null) throw new IllegalArgumentException("skinName cannot be null.");
+		for (Skin skin : skins)
+			if (skin.name.equals(skinName)) return skin;
+		return null;
+	}
+
+	/** All skins, including the default skin. */
+	public Array<Skin> getSkins () {
+		return skins;
+	}
+
+	// --- Events.
+
+	/** Finds an event by comparing each events's name. It is more efficient to cache the results of this method than to call it
+	 * multiple times. */
+	public @Null EventData findEvent (String eventDataName) {
+		if (eventDataName == null) throw new IllegalArgumentException("eventDataName cannot be null.");
+		for (EventData eventData : events)
+			if (eventData.name.equals(eventDataName)) return eventData;
+		return null;
+	}
+
+	/** The skeleton's events. */
+	public Array<EventData> getEvents () {
+		return events;
+	}
+
+	// --- Animations.
+
+	/** The skeleton's animations. */
+	public Array<Animation> getAnimations () {
+		return animations;
+	}
+
+	/** Finds an animation by comparing each animation's name. It is more efficient to cache the results of this method than to
+	 * call it multiple times. */
+	public @Null Animation findAnimation (String animationName) {
+		if (animationName == null) throw new IllegalArgumentException("animationName cannot be null.");
+		Object[] animations = this.animations.items;
+		for (int i = 0, n = this.animations.size; i < n; i++) {
+			Animation animation = (Animation)animations[i];
+			if (animation.name.equals(animationName)) return animation;
+		}
+		return null;
+	}
+
+	// --- IK constraints
+
+	/** The skeleton's IK constraints. */
+	public Array<IkConstraintData> getIkConstraints () {
+		return ikConstraints;
+	}
+
+	/** Finds an IK constraint by comparing each IK constraint's name. It is more efficient to cache the results of this method
+	 * than to call it multiple times. */
+	public @Null IkConstraintData findIkConstraint (String constraintName) {
+		if (constraintName == null) throw new IllegalArgumentException("constraintName cannot be null.");
+		Object[] ikConstraints = this.ikConstraints.items;
+		for (int i = 0, n = this.ikConstraints.size; i < n; i++) {
+			IkConstraintData constraint = (IkConstraintData)ikConstraints[i];
+			if (constraint.name.equals(constraintName)) return constraint;
+		}
+		return null;
+	}
+
+	// --- Transform constraints
+
+	/** The skeleton's transform constraints. */
+	public Array<TransformConstraintData> getTransformConstraints () {
+		return transformConstraints;
+	}
+
+	/** Finds a transform constraint by comparing each transform constraint's name. It is more efficient to cache the results of
+	 * this method than to call it multiple times. */
+	public @Null TransformConstraintData findTransformConstraint (String constraintName) {
+		if (constraintName == null) throw new IllegalArgumentException("constraintName cannot be null.");
+		Object[] transformConstraints = this.transformConstraints.items;
+		for (int i = 0, n = this.transformConstraints.size; i < n; i++) {
+			TransformConstraintData constraint = (TransformConstraintData)transformConstraints[i];
+			if (constraint.name.equals(constraintName)) return constraint;
+		}
+		return null;
+	}
+
+	// --- Path constraints
+
+	/** The skeleton's path constraints. */
+	public Array<PathConstraintData> getPathConstraints () {
+		return pathConstraints;
+	}
+
+	/** Finds a path constraint by comparing each path constraint's name. It is more efficient to cache the results of this method
+	 * than to call it multiple times. */
+	public @Null PathConstraintData findPathConstraint (String constraintName) {
+		if (constraintName == null) throw new IllegalArgumentException("constraintName cannot be null.");
+		Object[] pathConstraints = this.pathConstraints.items;
+		for (int i = 0, n = this.pathConstraints.size; i < n; i++) {
+			PathConstraintData constraint = (PathConstraintData)pathConstraints[i];
+			if (constraint.name.equals(constraintName)) return constraint;
+		}
+		return null;
+	}
+
+	// ---
+
+	/** The skeleton's name, which by default is the name of the skeleton data file when possible, or null when a name hasn't been
+	 * set. */
+	public @Null String getName () {
+		return name;
+	}
+
+	public void setName (@Null String name) {
+		this.name = name;
+	}
+
+	/** The X coordinate of the skeleton's axis aligned bounding box in the setup pose. */
+	public float getX () {
+		return x;
+	}
+
+	public void setX (float x) {
+		this.x = x;
+	}
+
+	/** The Y coordinate of the skeleton's axis aligned bounding box in the setup pose. */
+	public float getY () {
+		return y;
+	}
+
+	public void setY (float y) {
+		this.y = y;
+	}
+
+	/** The width of the skeleton's axis aligned bounding box in the setup pose. */
+	public float getWidth () {
+		return width;
+	}
+
+	public void setWidth (float width) {
+		this.width = width;
+	}
+
+	/** The height of the skeleton's axis aligned bounding box in the setup pose. */
+	public float getHeight () {
+		return height;
+	}
+
+	public void setHeight (float height) {
+		this.height = height;
+	}
+
+	/** The Spine version used to export the skeleton data, or null. */
+	public @Null String getVersion () {
+		return version;
+	}
+
+	public void setVersion (@Null String version) {
+		this.version = version;
+	}
+
+	/** The skeleton data hash. This value will change if any of the skeleton data has changed. */
+	public @Null String getHash () {
+		return hash;
+	}
+
+	public void setHash (@Null String hash) {
+		this.hash = hash;
+	}
+
+	/** The path to the images directory as defined in Spine, or null if nonessential data was not exported. */
+	public @Null String getImagesPath () {
+		return imagesPath;
+	}
+
+	public void setImagesPath (@Null String imagesPath) {
+		this.imagesPath = imagesPath;
+	}
+
+	/** The path to the audio directory as defined in Spine, or null if nonessential data was not exported. */
+	public @Null String getAudioPath () {
+		return audioPath;
+	}
+
+	public void setAudioPath (@Null String audioPath) {
+		this.audioPath = audioPath;
+	}
+
+	/** The dopesheet FPS in Spine, or zero if nonessential data was not exported. */
+	public float getFps () {
+		return fps;
+	}
+
+	public void setFps (float fps) {
+		this.fps = fps;
+	}
+
+	public String toString () {
+		return name != null ? name : super.toString();
+	}
+}

+ 1062 - 1062
spine-libgdx/spine-libgdx/src/com/esotericsoftware/spine/SkeletonJson.java

@@ -1,1062 +1,1062 @@
-/******************************************************************************
- * Spine Runtimes License Agreement
- * Last updated January 1, 2020. Replaces all prior versions.
- *
- * Copyright (c) 2013-2020, Esoteric Software LLC
- *
- * Integration of the Spine Runtimes into software or otherwise creating
- * derivative works of the Spine Runtimes is permitted under the terms and
- * conditions of Section 2 of the Spine Editor License Agreement:
- * http://esotericsoftware.com/spine-editor-license
- *
- * Otherwise, it is permitted to integrate the Spine Runtimes into software
- * or otherwise create derivative works of the Spine Runtimes (collectively,
- * "Products"), provided that each user of the Products must obtain their own
- * Spine Editor license and redistribution of the Products in any form must
- * include this license and copyright notice.
- *
- * THE SPINE RUNTIMES ARE PROVIDED BY ESOTERIC SOFTWARE LLC "AS IS" AND ANY
- * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
- * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
- * DISCLAIMED. IN NO EVENT SHALL ESOTERIC SOFTWARE LLC BE LIABLE FOR ANY
- * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
- * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES,
- * BUSINESS INTERRUPTION, OR LOSS OF USE, DATA, OR PROFITS) HOWEVER CAUSED AND
- * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
- * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
- * THE SPINE RUNTIMES, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
- *****************************************************************************/
-
-package com.esotericsoftware.spine;
-
-import static com.esotericsoftware.spine.utils.SpineUtils.*;
-
-import java.io.InputStream;
-
-import com.badlogic.gdx.files.FileHandle;
-import com.badlogic.gdx.graphics.Color;
-import com.badlogic.gdx.graphics.g2d.TextureAtlas;
-import com.badlogic.gdx.utils.Array;
-import com.badlogic.gdx.utils.FloatArray;
-import com.badlogic.gdx.utils.IntArray;
-import com.badlogic.gdx.utils.JsonReader;
-import com.badlogic.gdx.utils.JsonValue;
-import com.badlogic.gdx.utils.SerializationException;
-
-import com.esotericsoftware.spine.Animation.AlphaTimeline;
-import com.esotericsoftware.spine.Animation.AttachmentTimeline;
-import com.esotericsoftware.spine.Animation.CurveTimeline;
-import com.esotericsoftware.spine.Animation.CurveTimeline1;
-import com.esotericsoftware.spine.Animation.CurveTimeline2;
-import com.esotericsoftware.spine.Animation.DeformTimeline;
-import com.esotericsoftware.spine.Animation.DrawOrderTimeline;
-import com.esotericsoftware.spine.Animation.EventTimeline;
-import com.esotericsoftware.spine.Animation.IkConstraintTimeline;
-import com.esotericsoftware.spine.Animation.PathConstraintMixTimeline;
-import com.esotericsoftware.spine.Animation.PathConstraintPositionTimeline;
-import com.esotericsoftware.spine.Animation.PathConstraintSpacingTimeline;
-import com.esotericsoftware.spine.Animation.RGB2Timeline;
-import com.esotericsoftware.spine.Animation.RGBA2Timeline;
-import com.esotericsoftware.spine.Animation.RGBATimeline;
-import com.esotericsoftware.spine.Animation.RGBTimeline;
-import com.esotericsoftware.spine.Animation.RotateTimeline;
-import com.esotericsoftware.spine.Animation.ScaleTimeline;
-import com.esotericsoftware.spine.Animation.ScaleXTimeline;
-import com.esotericsoftware.spine.Animation.ScaleYTimeline;
-import com.esotericsoftware.spine.Animation.ShearTimeline;
-import com.esotericsoftware.spine.Animation.ShearXTimeline;
-import com.esotericsoftware.spine.Animation.ShearYTimeline;
-import com.esotericsoftware.spine.Animation.Timeline;
-import com.esotericsoftware.spine.Animation.TransformConstraintTimeline;
-import com.esotericsoftware.spine.Animation.TranslateTimeline;
-import com.esotericsoftware.spine.Animation.TranslateXTimeline;
-import com.esotericsoftware.spine.Animation.TranslateYTimeline;
-import com.esotericsoftware.spine.BoneData.TransformMode;
-import com.esotericsoftware.spine.PathConstraintData.PositionMode;
-import com.esotericsoftware.spine.PathConstraintData.RotateMode;
-import com.esotericsoftware.spine.PathConstraintData.SpacingMode;
-import com.esotericsoftware.spine.attachments.Attachment;
-import com.esotericsoftware.spine.attachments.AttachmentLoader;
-import com.esotericsoftware.spine.attachments.AttachmentType;
-import com.esotericsoftware.spine.attachments.BoundingBoxAttachment;
-import com.esotericsoftware.spine.attachments.ClippingAttachment;
-import com.esotericsoftware.spine.attachments.MeshAttachment;
-import com.esotericsoftware.spine.attachments.PathAttachment;
-import com.esotericsoftware.spine.attachments.PointAttachment;
-import com.esotericsoftware.spine.attachments.RegionAttachment;
-import com.esotericsoftware.spine.attachments.VertexAttachment;
-
-/** Loads skeleton data in the Spine JSON format.
- * <p>
- * JSON is human readable but the binary format is much smaller on disk and faster to load. See {@link SkeletonBinary}.
- * <p>
- * See <a href="http://esotericsoftware.com/spine-json-format">Spine JSON format</a> and
- * <a href="http://esotericsoftware.com/spine-loading-skeleton-data#JSON-and-binary-data">JSON and binary data</a> in the Spine
- * Runtimes Guide. */
-public class SkeletonJson extends SkeletonLoader {
-	public SkeletonJson (AttachmentLoader attachmentLoader) {
-		super(attachmentLoader);
-	}
-
-	public SkeletonJson (TextureAtlas atlas) {
-		super(atlas);
-	}
-
-	public SkeletonData readSkeletonData (FileHandle file) {
-		if (file == null) throw new IllegalArgumentException("file cannot be null.");
-		SkeletonData skeletonData = readSkeletonData(new JsonReader().parse(file));
-		skeletonData.name = file.nameWithoutExtension();
-		return skeletonData;
-	}
-
-	public SkeletonData readSkeletonData (InputStream input) {
-		if (input == null) throw new IllegalArgumentException("dataInput cannot be null.");
-		return readSkeletonData(new JsonReader().parse(input));
-	}
-
-	public SkeletonData readSkeletonData (JsonValue root) {
-		if (root == null) throw new IllegalArgumentException("root cannot be null.");
-
-		float scale = this.scale;
-
-		// Skeleton.
-		SkeletonData skeletonData = new SkeletonData();
-		JsonValue skeletonMap = root.get("skeleton");
-		if (skeletonMap != null) {
-			skeletonData.hash = skeletonMap.getString("hash", null);
-			skeletonData.version = skeletonMap.getString("spine", null);
-			skeletonData.x = skeletonMap.getFloat("x", 0);
-			skeletonData.y = skeletonMap.getFloat("y", 0);
-			skeletonData.width = skeletonMap.getFloat("width", 0);
-			skeletonData.height = skeletonMap.getFloat("height", 0);
-			skeletonData.fps = skeletonMap.getFloat("fps", 30);
-			skeletonData.imagesPath = skeletonMap.getString("images", null);
-			skeletonData.audioPath = skeletonMap.getString("audio", null);
-		}
-
-		// Bones.
-		for (JsonValue boneMap = root.getChild("bones"); boneMap != null; boneMap = boneMap.next) {
-			BoneData parent = null;
-			String parentName = boneMap.getString("parent", null);
-			if (parentName != null) {
-				parent = skeletonData.findBone(parentName);
-				if (parent == null) throw new SerializationException("Parent bone not found: " + parentName);
-			}
-			BoneData data = new BoneData(skeletonData.bones.size, boneMap.getString("name"), parent);
-			data.length = boneMap.getFloat("length", 0) * scale;
-			data.x = boneMap.getFloat("x", 0) * scale;
-			data.y = boneMap.getFloat("y", 0) * scale;
-			data.rotation = boneMap.getFloat("rotation", 0);
-			data.scaleX = boneMap.getFloat("scaleX", 1);
-			data.scaleY = boneMap.getFloat("scaleY", 1);
-			data.shearX = boneMap.getFloat("shearX", 0);
-			data.shearY = boneMap.getFloat("shearY", 0);
-			data.transformMode = TransformMode.valueOf(boneMap.getString("transform", TransformMode.normal.name()));
-			data.skinRequired = boneMap.getBoolean("skin", false);
-
-			String color = boneMap.getString("color", null);
-			if (color != null) Color.valueOf(color, data.getColor());
-
-			skeletonData.bones.add(data);
-		}
-
-		// Slots.
-		for (JsonValue slotMap = root.getChild("slots"); slotMap != null; slotMap = slotMap.next) {
-			String slotName = slotMap.getString("name");
-			String boneName = slotMap.getString("bone");
-			BoneData boneData = skeletonData.findBone(boneName);
-			if (boneData == null) throw new SerializationException("Slot bone not found: " + boneName);
-			SlotData data = new SlotData(skeletonData.slots.size, slotName, boneData);
-
-			String color = slotMap.getString("color", null);
-			if (color != null) Color.valueOf(color, data.getColor());
-
-			String dark = slotMap.getString("dark", null);
-			if (dark != null) data.setDarkColor(Color.valueOf(dark));
-
-			data.attachmentName = slotMap.getString("attachment", null);
-			data.blendMode = BlendMode.valueOf(slotMap.getString("blend", BlendMode.normal.name()));
-			skeletonData.slots.add(data);
-		}
-
-		// IK constraints.
-		for (JsonValue constraintMap = root.getChild("ik"); constraintMap != null; constraintMap = constraintMap.next) {
-			IkConstraintData data = new IkConstraintData(constraintMap.getString("name"));
-			data.order = constraintMap.getInt("order", 0);
-			data.skinRequired = constraintMap.getBoolean("skin", false);
-
-			for (JsonValue entry = constraintMap.getChild("bones"); entry != null; entry = entry.next) {
-				BoneData bone = skeletonData.findBone(entry.asString());
-				if (bone == null) throw new SerializationException("IK bone not found: " + entry);
-				data.bones.add(bone);
-			}
-
-			String targetName = constraintMap.getString("target");
-			data.target = skeletonData.findBone(targetName);
-			if (data.target == null) throw new SerializationException("IK target bone not found: " + targetName);
-
-			data.mix = constraintMap.getFloat("mix", 1);
-			data.softness = constraintMap.getFloat("softness", 0) * scale;
-			data.bendDirection = constraintMap.getBoolean("bendPositive", true) ? 1 : -1;
-			data.compress = constraintMap.getBoolean("compress", false);
-			data.stretch = constraintMap.getBoolean("stretch", false);
-			data.uniform = constraintMap.getBoolean("uniform", false);
-
-			skeletonData.ikConstraints.add(data);
-		}
-
-		// Transform constraints.
-		for (JsonValue constraintMap = root.getChild("transform"); constraintMap != null; constraintMap = constraintMap.next) {
-			TransformConstraintData data = new TransformConstraintData(constraintMap.getString("name"));
-			data.order = constraintMap.getInt("order", 0);
-			data.skinRequired = constraintMap.getBoolean("skin", false);
-
-			for (JsonValue entry = constraintMap.getChild("bones"); entry != null; entry = entry.next) {
-				BoneData bone = skeletonData.findBone(entry.asString());
-				if (bone == null) throw new SerializationException("Transform constraint bone not found: " + entry);
-				data.bones.add(bone);
-			}
-
-			String targetName = constraintMap.getString("target");
-			data.target = skeletonData.findBone(targetName);
-			if (data.target == null) throw new SerializationException("Transform constraint target bone not found: " + targetName);
-
-			data.local = constraintMap.getBoolean("local", false);
-			data.relative = constraintMap.getBoolean("relative", false);
-
-			data.offsetRotation = constraintMap.getFloat("rotation", 0);
-			data.offsetX = constraintMap.getFloat("x", 0) * scale;
-			data.offsetY = constraintMap.getFloat("y", 0) * scale;
-			data.offsetScaleX = constraintMap.getFloat("scaleX", 0);
-			data.offsetScaleY = constraintMap.getFloat("scaleY", 0);
-			data.offsetShearY = constraintMap.getFloat("shearY", 0);
-
-			data.mixRotate = constraintMap.getFloat("mixRotate", 1);
-			data.mixX = constraintMap.getFloat("mixX", 1);
-			data.mixY = constraintMap.getFloat("mixY", data.mixX);
-			data.mixScaleX = constraintMap.getFloat("mixScaleX", 1);
-			data.mixScaleY = constraintMap.getFloat("mixScaleY", data.mixScaleX);
-			data.mixShearY = constraintMap.getFloat("mixShearY", 1);
-
-			skeletonData.transformConstraints.add(data);
-		}
-
-		// Path constraints.
-		for (JsonValue constraintMap = root.getChild("path"); constraintMap != null; constraintMap = constraintMap.next) {
-			PathConstraintData data = new PathConstraintData(constraintMap.getString("name"));
-			data.order = constraintMap.getInt("order", 0);
-			data.skinRequired = constraintMap.getBoolean("skin", false);
-
-			for (JsonValue entry = constraintMap.getChild("bones"); entry != null; entry = entry.next) {
-				BoneData bone = skeletonData.findBone(entry.asString());
-				if (bone == null) throw new SerializationException("Path bone not found: " + entry);
-				data.bones.add(bone);
-			}
-
-			String targetName = constraintMap.getString("target");
-			data.target = skeletonData.findSlot(targetName);
-			if (data.target == null) throw new SerializationException("Path target slot not found: " + targetName);
-
-			data.positionMode = PositionMode.valueOf(constraintMap.getString("positionMode", "percent"));
-			data.spacingMode = SpacingMode.valueOf(constraintMap.getString("spacingMode", "length"));
-			data.rotateMode = RotateMode.valueOf(constraintMap.getString("rotateMode", "tangent"));
-			data.offsetRotation = constraintMap.getFloat("rotation", 0);
-			data.position = constraintMap.getFloat("position", 0);
-			if (data.positionMode == PositionMode.fixed) data.position *= scale;
-			data.spacing = constraintMap.getFloat("spacing", 0);
-			if (data.spacingMode == SpacingMode.length || data.spacingMode == SpacingMode.fixed) data.spacing *= scale;
-			data.mixRotate = constraintMap.getFloat("mixRotate", 1);
-			data.mixX = constraintMap.getFloat("mixX", 1);
-			data.mixY = constraintMap.getFloat("mixY", 1);
-
-			skeletonData.pathConstraints.add(data);
-		}
-
-		// Skins.
-		for (JsonValue skinMap = root.getChild("skins"); skinMap != null; skinMap = skinMap.next) {
-			Skin skin = new Skin(skinMap.getString("name"));
-			for (JsonValue entry = skinMap.getChild("bones"); entry != null; entry = entry.next) {
-				BoneData bone = skeletonData.findBone(entry.asString());
-				if (bone == null) throw new SerializationException("Skin bone not found: " + entry);
-				skin.bones.add(bone);
-			}
-			skin.bones.shrink();
-			for (JsonValue entry = skinMap.getChild("ik"); entry != null; entry = entry.next) {
-				IkConstraintData constraint = skeletonData.findIkConstraint(entry.asString());
-				if (constraint == null) throw new SerializationException("Skin IK constraint not found: " + entry);
-				skin.constraints.add(constraint);
-			}
-			for (JsonValue entry = skinMap.getChild("transform"); entry != null; entry = entry.next) {
-				TransformConstraintData constraint = skeletonData.findTransformConstraint(entry.asString());
-				if (constraint == null) throw new SerializationException("Skin transform constraint not found: " + entry);
-				skin.constraints.add(constraint);
-			}
-			for (JsonValue entry = skinMap.getChild("path"); entry != null; entry = entry.next) {
-				PathConstraintData constraint = skeletonData.findPathConstraint(entry.asString());
-				if (constraint == null) throw new SerializationException("Skin path constraint not found: " + entry);
-				skin.constraints.add(constraint);
-			}
-			skin.constraints.shrink();
-			for (JsonValue slotEntry = skinMap.getChild("attachments"); slotEntry != null; slotEntry = slotEntry.next) {
-				SlotData slot = skeletonData.findSlot(slotEntry.name);
-				if (slot == null) throw new SerializationException("Slot not found: " + slotEntry.name);
-				for (JsonValue entry = slotEntry.child; entry != null; entry = entry.next) {
-					try {
-						Attachment attachment = readAttachment(entry, skin, slot.index, entry.name, skeletonData);
-						if (attachment != null) skin.setAttachment(slot.index, entry.name, attachment);
-					} catch (Throwable ex) {
-						throw new SerializationException("Error reading attachment: " + entry.name + ", skin: " + skin, ex);
-					}
-				}
-			}
-			skeletonData.skins.add(skin);
-			if (skin.name.equals("default")) skeletonData.defaultSkin = skin;
-		}
-
-		// Linked meshes.
-		Object[] items = linkedMeshes.items;
-		for (int i = 0, n = linkedMeshes.size; i < n; i++) {
-			LinkedMesh linkedMesh = (LinkedMesh)items[i];
-			Skin skin = linkedMesh.skin == null ? skeletonData.getDefaultSkin() : skeletonData.findSkin(linkedMesh.skin);
-			if (skin == null) throw new SerializationException("Skin not found: " + linkedMesh.skin);
-			Attachment parent = skin.getAttachment(linkedMesh.slotIndex, linkedMesh.parent);
-			if (parent == null) throw new SerializationException("Parent mesh not found: " + linkedMesh.parent);
-			linkedMesh.mesh.setDeformAttachment(linkedMesh.inheritDeform ? (VertexAttachment)parent : linkedMesh.mesh);
-			linkedMesh.mesh.setParentMesh((MeshAttachment)parent);
-			linkedMesh.mesh.updateUVs();
-		}
-		linkedMeshes.clear();
-
-		// Events.
-		for (JsonValue eventMap = root.getChild("events"); eventMap != null; eventMap = eventMap.next) {
-			EventData data = new EventData(eventMap.name);
-			data.intValue = eventMap.getInt("int", 0);
-			data.floatValue = eventMap.getFloat("float", 0f);
-			data.stringValue = eventMap.getString("string", "");
-			data.audioPath = eventMap.getString("audio", null);
-			if (data.audioPath != null) {
-				data.volume = eventMap.getFloat("volume", 1);
-				data.balance = eventMap.getFloat("balance", 0);
-			}
-			skeletonData.events.add(data);
-		}
-
-		// Animations.
-		for (JsonValue animationMap = root.getChild("animations"); animationMap != null; animationMap = animationMap.next) {
-			try {
-				readAnimation(animationMap, animationMap.name, skeletonData);
-			} catch (Throwable ex) {
-				throw new SerializationException("Error reading animation: " + animationMap.name, ex);
-			}
-		}
-
-		skeletonData.bones.shrink();
-		skeletonData.slots.shrink();
-		skeletonData.skins.shrink();
-		skeletonData.events.shrink();
-		skeletonData.animations.shrink();
-		skeletonData.ikConstraints.shrink();
-		return skeletonData;
-	}
-
-	private Attachment readAttachment (JsonValue map, Skin skin, int slotIndex, String name, SkeletonData skeletonData) {
-		float scale = this.scale;
-		name = map.getString("name", name);
-
-		switch (AttachmentType.valueOf(map.getString("type", AttachmentType.region.name()))) {
-		case region: {
-			String path = map.getString("path", name);
-			RegionAttachment region = attachmentLoader.newRegionAttachment(skin, name, path);
-			if (region == null) return null;
-			region.setPath(path);
-			region.setX(map.getFloat("x", 0) * scale);
-			region.setY(map.getFloat("y", 0) * scale);
-			region.setScaleX(map.getFloat("scaleX", 1));
-			region.setScaleY(map.getFloat("scaleY", 1));
-			region.setRotation(map.getFloat("rotation", 0));
-			region.setWidth(map.getFloat("width") * scale);
-			region.setHeight(map.getFloat("height") * scale);
-
-			String color = map.getString("color", null);
-			if (color != null) Color.valueOf(color, region.getColor());
-
-			region.updateOffset();
-			return region;
-		}
-		case boundingbox: {
-			BoundingBoxAttachment box = attachmentLoader.newBoundingBoxAttachment(skin, name);
-			if (box == null) return null;
-			readVertices(map, box, map.getInt("vertexCount") << 1);
-
-			String color = map.getString("color", null);
-			if (color != null) Color.valueOf(color, box.getColor());
-			return box;
-		}
-		case mesh:
-		case linkedmesh: {
-			String path = map.getString("path", name);
-			MeshAttachment mesh = attachmentLoader.newMeshAttachment(skin, name, path);
-			if (mesh == null) return null;
-			mesh.setPath(path);
-
-			String color = map.getString("color", null);
-			if (color != null) Color.valueOf(color, mesh.getColor());
-
-			mesh.setWidth(map.getFloat("width", 0) * scale);
-			mesh.setHeight(map.getFloat("height", 0) * scale);
-
-			String parent = map.getString("parent", null);
-			if (parent != null) {
-				linkedMeshes
-					.add(new LinkedMesh(mesh, map.getString("skin", null), slotIndex, parent, map.getBoolean("deform", true)));
-				return mesh;
-			}
-
-			float[] uvs = map.require("uvs").asFloatArray();
-			readVertices(map, mesh, uvs.length);
-			mesh.setTriangles(map.require("triangles").asShortArray());
-			mesh.setRegionUVs(uvs);
-			mesh.updateUVs();
-
-			if (map.has("hull")) mesh.setHullLength(map.require("hull").asInt() << 1);
-			if (map.has("edges")) mesh.setEdges(map.require("edges").asShortArray());
-			return mesh;
-		}
-		case path: {
-			PathAttachment path = attachmentLoader.newPathAttachment(skin, name);
-			if (path == null) return null;
-			path.setClosed(map.getBoolean("closed", false));
-			path.setConstantSpeed(map.getBoolean("constantSpeed", true));
-
-			int vertexCount = map.getInt("vertexCount");
-			readVertices(map, path, vertexCount << 1);
-
-			float[] lengths = new float[vertexCount / 3];
-			int i = 0;
-			for (JsonValue curves = map.require("lengths").child; curves != null; curves = curves.next)
-				lengths[i++] = curves.asFloat() * scale;
-			path.setLengths(lengths);
-
-			String color = map.getString("color", null);
-			if (color != null) Color.valueOf(color, path.getColor());
-			return path;
-		}
-		case point: {
-			PointAttachment point = attachmentLoader.newPointAttachment(skin, name);
-			if (point == null) return null;
-			point.setX(map.getFloat("x", 0) * scale);
-			point.setY(map.getFloat("y", 0) * scale);
-			point.setRotation(map.getFloat("rotation", 0));
-
-			String color = map.getString("color", null);
-			if (color != null) Color.valueOf(color, point.getColor());
-			return point;
-		}
-		case clipping:
-			ClippingAttachment clip = attachmentLoader.newClippingAttachment(skin, name);
-			if (clip == null) return null;
-
-			String end = map.getString("end", null);
-			if (end != null) {
-				SlotData slot = skeletonData.findSlot(end);
-				if (slot == null) throw new SerializationException("Clipping end slot not found: " + end);
-				clip.setEndSlot(slot);
-			}
-
-			readVertices(map, clip, map.getInt("vertexCount") << 1);
-
-			String color = map.getString("color", null);
-			if (color != null) Color.valueOf(color, clip.getColor());
-			return clip;
-		}
-		return null;
-	}
-
-	private void readVertices (JsonValue map, VertexAttachment attachment, int verticesLength) {
-		attachment.setWorldVerticesLength(verticesLength);
-		float[] vertices = map.require("vertices").asFloatArray();
-		if (verticesLength == vertices.length) {
-			if (scale != 1) {
-				for (int i = 0, n = vertices.length; i < n; i++)
-					vertices[i] *= scale;
-			}
-			attachment.setVertices(vertices);
-			return;
-		}
-		FloatArray weights = new FloatArray(verticesLength * 3 * 3);
-		IntArray bones = new IntArray(verticesLength * 3);
-		for (int i = 0, n = vertices.length; i < n;) {
-			int boneCount = (int)vertices[i++];
-			bones.add(boneCount);
-			for (int nn = i + (boneCount << 2); i < nn; i += 4) {
-				bones.add((int)vertices[i]);
-				weights.add(vertices[i + 1] * scale);
-				weights.add(vertices[i + 2] * scale);
-				weights.add(vertices[i + 3]);
-			}
-		}
-		attachment.setBones(bones.toArray());
-		attachment.setVertices(weights.toArray());
-	}
-
-	private void readAnimation (JsonValue map, String name, SkeletonData skeletonData) {
-		float scale = this.scale;
-		Array<Timeline> timelines = new Array();
-
-		// Slot timelines.
-		for (JsonValue slotMap = map.getChild("slots"); slotMap != null; slotMap = slotMap.next) {
-			SlotData slot = skeletonData.findSlot(slotMap.name);
-			if (slot == null) throw new SerializationException("Slot not found: " + slotMap.name);
-			for (JsonValue timelineMap = slotMap.child; timelineMap != null; timelineMap = timelineMap.next) {
-				JsonValue keyMap = timelineMap.child;
-				if (keyMap == null) continue;
-				String timelineName = timelineMap.name;
-
-				if (timelineName.equals("attachment")) {
-					AttachmentTimeline timeline = new AttachmentTimeline(timelineMap.size, slot.index);
-					for (int frame = 0; keyMap != null; keyMap = keyMap.next, frame++)
-						timeline.setFrame(frame, keyMap.getFloat("time", 0), keyMap.getString("name"));
-					timelines.add(timeline);
-
-				} else if (timelineName.equals("rgba")) {
-					RGBATimeline timeline = new RGBATimeline(timelineMap.size, timelineMap.size << 2, slot.index);
-					float time = keyMap.getFloat("time", 0);
-					String color = keyMap.getString("color");
-					float r = Integer.parseInt(color.substring(0, 2), 16) / 255f;
-					float g = Integer.parseInt(color.substring(2, 4), 16) / 255f;
-					float b = Integer.parseInt(color.substring(4, 6), 16) / 255f;
-					float a = Integer.parseInt(color.substring(6, 8), 16) / 255f;
-					for (int frame = 0, bezier = 0;; frame++) {
-						timeline.setFrame(frame, time, r, g, b, a);
-						JsonValue nextMap = keyMap.next;
-						if (nextMap == null) {
-							timeline.shrink(bezier);
-							break;
-						}
-						float time2 = nextMap.getFloat("time", 0);
-						color = nextMap.getString("color");
-						float nr = Integer.parseInt(color.substring(0, 2), 16) / 255f;
-						float ng = Integer.parseInt(color.substring(2, 4), 16) / 255f;
-						float nb = Integer.parseInt(color.substring(4, 6), 16) / 255f;
-						float na = Integer.parseInt(color.substring(6, 8), 16) / 255f;
-						JsonValue curve = keyMap.get("curve");
-						if (curve != null) {
-							bezier = readCurve(curve, timeline, bezier, frame, 0, time, time2, r, nr, 1);
-							bezier = readCurve(curve, timeline, bezier, frame, 1, time, time2, g, ng, 1);
-							bezier = readCurve(curve, timeline, bezier, frame, 2, time, time2, b, nb, 1);
-							bezier = readCurve(curve, timeline, bezier, frame, 3, time, time2, a, na, 1);
-						}
-						time = time2;
-						r = nr;
-						g = ng;
-						b = nb;
-						a = na;
-						keyMap = nextMap;
-					}
-					timelines.add(timeline);
-
-				} else if (timelineName.equals("rgb")) {
-					RGBTimeline timeline = new RGBTimeline(timelineMap.size, timelineMap.size * 3, slot.index);
-					float time = keyMap.getFloat("time", 0);
-					String color = keyMap.getString("color");
-					float r = Integer.parseInt(color.substring(0, 2), 16) / 255f;
-					float g = Integer.parseInt(color.substring(2, 4), 16) / 255f;
-					float b = Integer.parseInt(color.substring(4, 6), 16) / 255f;
-					for (int frame = 0, bezier = 0;; frame++) {
-						timeline.setFrame(frame, time, r, g, b);
-						JsonValue nextMap = keyMap.next;
-						if (nextMap == null) {
-							timeline.shrink(bezier);
-							break;
-						}
-						float time2 = nextMap.getFloat("time", 0);
-						color = nextMap.getString("color");
-						float nr = Integer.parseInt(color.substring(0, 2), 16) / 255f;
-						float ng = Integer.parseInt(color.substring(2, 4), 16) / 255f;
-						float nb = Integer.parseInt(color.substring(4, 6), 16) / 255f;
-						JsonValue curve = keyMap.get("curve");
-						if (curve != null) {
-							bezier = readCurve(curve, timeline, bezier, frame, 0, time, time2, r, nr, 1);
-							bezier = readCurve(curve, timeline, bezier, frame, 1, time, time2, g, ng, 1);
-							bezier = readCurve(curve, timeline, bezier, frame, 2, time, time2, b, nb, 1);
-						}
-						time = time2;
-						r = nr;
-						g = ng;
-						b = nb;
-						keyMap = nextMap;
-					}
-					timelines.add(timeline);
-
-				} else if (timelineName.equals("alpha")) {
-					timelines.add(readTimeline(keyMap, new AlphaTimeline(timelineMap.size, timelineMap.size, slot.index), 0, 1));
-
-				} else if (timelineName.equals("rgba2")) {
-					RGBA2Timeline timeline = new RGBA2Timeline(timelineMap.size, timelineMap.size * 7, slot.index);
-					float time = keyMap.getFloat("time", 0);
-					String color = keyMap.getString("light");
-					float r = Integer.parseInt(color.substring(0, 2), 16) / 255f;
-					float g = Integer.parseInt(color.substring(2, 4), 16) / 255f;
-					float b = Integer.parseInt(color.substring(4, 6), 16) / 255f;
-					float a = Integer.parseInt(color.substring(6, 8), 16) / 255f;
-					color = keyMap.getString("dark");
-					float r2 = Integer.parseInt(color.substring(0, 2), 16) / 255f;
-					float g2 = Integer.parseInt(color.substring(2, 4), 16) / 255f;
-					float b2 = Integer.parseInt(color.substring(4, 6), 16) / 255f;
-					for (int frame = 0, bezier = 0;; frame++) {
-						timeline.setFrame(frame, time, r, g, b, a, r2, g2, b2);
-						JsonValue nextMap = keyMap.next;
-						if (nextMap == null) {
-							timeline.shrink(bezier);
-							break;
-						}
-						float time2 = nextMap.getFloat("time", 0);
-						color = nextMap.getString("light");
-						float nr = Integer.parseInt(color.substring(0, 2), 16) / 255f;
-						float ng = Integer.parseInt(color.substring(2, 4), 16) / 255f;
-						float nb = Integer.parseInt(color.substring(4, 6), 16) / 255f;
-						float na = Integer.parseInt(color.substring(6, 8), 16) / 255f;
-						color = nextMap.getString("dark");
-						float nr2 = Integer.parseInt(color.substring(0, 2), 16) / 255f;
-						float ng2 = Integer.parseInt(color.substring(2, 4), 16) / 255f;
-						float nb2 = Integer.parseInt(color.substring(4, 6), 16) / 255f;
-						JsonValue curve = keyMap.get("curve");
-						if (curve != null) {
-							bezier = readCurve(curve, timeline, bezier, frame, 0, time, time2, r, nr, 1);
-							bezier = readCurve(curve, timeline, bezier, frame, 1, time, time2, g, ng, 1);
-							bezier = readCurve(curve, timeline, bezier, frame, 2, time, time2, b, nb, 1);
-							bezier = readCurve(curve, timeline, bezier, frame, 3, time, time2, a, na, 1);
-							bezier = readCurve(curve, timeline, bezier, frame, 4, time, time2, r2, nr2, 1);
-							bezier = readCurve(curve, timeline, bezier, frame, 5, time, time2, g2, ng2, 1);
-							bezier = readCurve(curve, timeline, bezier, frame, 6, time, time2, b2, nb2, 1);
-						}
-						time = time2;
-						r = nr;
-						g = ng;
-						b = nb;
-						a = na;
-						r2 = nr2;
-						g2 = ng2;
-						b2 = nb2;
-						keyMap = nextMap;
-					}
-					timelines.add(timeline);
-
-				} else if (timelineName.equals("rgb2")) {
-					RGB2Timeline timeline = new RGB2Timeline(timelineMap.size, timelineMap.size * 6, slot.index);
-					float time = keyMap.getFloat("time", 0);
-					String color = keyMap.getString("light");
-					float r = Integer.parseInt(color.substring(0, 2), 16) / 255f;
-					float g = Integer.parseInt(color.substring(2, 4), 16) / 255f;
-					float b = Integer.parseInt(color.substring(4, 6), 16) / 255f;
-					color = keyMap.getString("dark");
-					float r2 = Integer.parseInt(color.substring(0, 2), 16) / 255f;
-					float g2 = Integer.parseInt(color.substring(2, 4), 16) / 255f;
-					float b2 = Integer.parseInt(color.substring(4, 6), 16) / 255f;
-					for (int frame = 0, bezier = 0;; frame++) {
-						timeline.setFrame(frame, time, r, g, b, r2, g2, b2);
-						JsonValue nextMap = keyMap.next;
-						if (nextMap == null) {
-							timeline.shrink(bezier);
-							break;
-						}
-						float time2 = nextMap.getFloat("time", 0);
-						color = nextMap.getString("light");
-						float nr = Integer.parseInt(color.substring(0, 2), 16) / 255f;
-						float ng = Integer.parseInt(color.substring(2, 4), 16) / 255f;
-						float nb = Integer.parseInt(color.substring(4, 6), 16) / 255f;
-						color = nextMap.getString("dark");
-						float nr2 = Integer.parseInt(color.substring(0, 2), 16) / 255f;
-						float ng2 = Integer.parseInt(color.substring(2, 4), 16) / 255f;
-						float nb2 = Integer.parseInt(color.substring(4, 6), 16) / 255f;
-						JsonValue curve = keyMap.get("curve");
-						if (curve != null) {
-							bezier = readCurve(curve, timeline, bezier, frame, 0, time, time2, r, nr, 1);
-							bezier = readCurve(curve, timeline, bezier, frame, 1, time, time2, g, ng, 1);
-							bezier = readCurve(curve, timeline, bezier, frame, 2, time, time2, b, nb, 1);
-							bezier = readCurve(curve, timeline, bezier, frame, 3, time, time2, r2, nr2, 1);
-							bezier = readCurve(curve, timeline, bezier, frame, 4, time, time2, g2, ng2, 1);
-							bezier = readCurve(curve, timeline, bezier, frame, 5, time, time2, b2, nb2, 1);
-						}
-						time = time2;
-						r = nr;
-						g = ng;
-						b = nb;
-						r2 = nr2;
-						g2 = ng2;
-						b2 = nb2;
-						keyMap = nextMap;
-					}
-					timelines.add(timeline);
-
-				} else
-					throw new RuntimeException("Invalid timeline type for a slot: " + timelineName + " (" + slotMap.name + ")");
-			}
-		}
-
-		// Bone timelines.
-		for (JsonValue boneMap = map.getChild("bones"); boneMap != null; boneMap = boneMap.next) {
-			BoneData bone = skeletonData.findBone(boneMap.name);
-			if (bone == null) throw new SerializationException("Bone not found: " + boneMap.name);
-			for (JsonValue timelineMap = boneMap.child; timelineMap != null; timelineMap = timelineMap.next) {
-				JsonValue keyMap = timelineMap.child;
-				if (keyMap == null) continue;
-
-				String timelineName = timelineMap.name;
-				if (timelineName.equals("rotate"))
-					timelines.add(readTimeline(keyMap, new RotateTimeline(timelineMap.size, timelineMap.size, bone.index), 0, 1));
-				else if (timelineName.equals("translate")) {
-					TranslateTimeline timeline = new TranslateTimeline(timelineMap.size, timelineMap.size << 1, bone.index);
-					timelines.add(readTimeline(keyMap, timeline, "x", "y", 0, scale));
-				} else if (timelineName.equals("translatex")) {
-					timelines
-						.add(readTimeline(keyMap, new TranslateXTimeline(timelineMap.size, timelineMap.size, bone.index), 0, scale));
-				} else if (timelineName.equals("translatey")) {
-					timelines
-						.add(readTimeline(keyMap, new TranslateYTimeline(timelineMap.size, timelineMap.size, bone.index), 0, scale));
-				} else if (timelineName.equals("scale")) {
-					ScaleTimeline timeline = new ScaleTimeline(timelineMap.size, timelineMap.size << 1, bone.index);
-					timelines.add(readTimeline(keyMap, timeline, "x", "y", 1, 1));
-				} else if (timelineName.equals("scalex"))
-					timelines.add(readTimeline(keyMap, new ScaleXTimeline(timelineMap.size, timelineMap.size, bone.index), 1, 1));
-				else if (timelineName.equals("scaley"))
-					timelines.add(readTimeline(keyMap, new ScaleYTimeline(timelineMap.size, timelineMap.size, bone.index), 1, 1));
-				else if (timelineName.equals("shear")) {
-					ShearTimeline timeline = new ShearTimeline(timelineMap.size, timelineMap.size << 1, bone.index);
-					timelines.add(readTimeline(keyMap, timeline, "x", "y", 0, 1));
-				} else if (timelineName.equals("shearx"))
-					timelines.add(readTimeline(keyMap, new ShearXTimeline(timelineMap.size, timelineMap.size, bone.index), 0, 1));
-				else if (timelineName.equals("sheary"))
-					timelines.add(readTimeline(keyMap, new ShearYTimeline(timelineMap.size, timelineMap.size, bone.index), 0, 1));
-				else
-					throw new RuntimeException("Invalid timeline type for a bone: " + timelineName + " (" + boneMap.name + ")");
-			}
-		}
-
-		// IK constraint timelines.
-		for (JsonValue timelineMap = map.getChild("ik"); timelineMap != null; timelineMap = timelineMap.next) {
-			JsonValue keyMap = timelineMap.child;
-			if (keyMap == null) continue;
-			IkConstraintData constraint = skeletonData.findIkConstraint(timelineMap.name);
-			IkConstraintTimeline timeline = new IkConstraintTimeline(timelineMap.size, timelineMap.size << 1,
-				skeletonData.getIkConstraints().indexOf(constraint, true));
-			float time = keyMap.getFloat("time", 0);
-			float mix = keyMap.getFloat("mix", 1), softness = keyMap.getFloat("softness", 0) * scale;
-			for (int frame = 0, bezier = 0;; frame++) {
-				timeline.setFrame(frame, time, mix, softness, keyMap.getBoolean("bendPositive", true) ? 1 : -1,
-					keyMap.getBoolean("compress", false), keyMap.getBoolean("stretch", false));
-				JsonValue nextMap = keyMap.next;
-				if (nextMap == null) {
-					timeline.shrink(bezier);
-					break;
-				}
-				float time2 = nextMap.getFloat("time", 0);
-				float mix2 = nextMap.getFloat("mix", 1), softness2 = nextMap.getFloat("softness", 0) * scale;
-				JsonValue curve = keyMap.get("curve");
-				if (curve != null) {
-					bezier = readCurve(curve, timeline, bezier, frame, 0, time, time2, mix, mix2, 1);
-					bezier = readCurve(curve, timeline, bezier, frame, 1, time, time2, softness, softness2, scale);
-				}
-				time = time2;
-				mix = mix2;
-				softness = softness2;
-				keyMap = nextMap;
-			}
-			timelines.add(timeline);
-		}
-
-		// Transform constraint timelines.
-		for (JsonValue timelineMap = map.getChild("transform"); timelineMap != null; timelineMap = timelineMap.next) {
-			JsonValue keyMap = timelineMap.child;
-			if (keyMap == null) continue;
-			TransformConstraintData constraint = skeletonData.findTransformConstraint(timelineMap.name);
-			TransformConstraintTimeline timeline = new TransformConstraintTimeline(timelineMap.size, timelineMap.size << 2,
-				skeletonData.getTransformConstraints().indexOf(constraint, true));
-			float time = keyMap.getFloat("time", 0);
-			float mixRotate = keyMap.getFloat("mixRotate", 1);
-			float mixX = keyMap.getFloat("mixX", 1), mixY = keyMap.getFloat("mixY", mixX);
-			float mixScaleX = keyMap.getFloat("mixScaleX", 1), mixScaleY = keyMap.getFloat("mixScaleY", mixScaleX);
-			float mixShearY = keyMap.getFloat("mixShearY", 1);
-			for (int frame = 0, bezier = 0;; frame++) {
-				timeline.setFrame(frame, time, mixRotate, mixX, mixY, mixScaleX, mixScaleY, mixShearY);
-				JsonValue nextMap = keyMap.next;
-				if (nextMap == null) {
-					timeline.shrink(bezier);
-					break;
-				}
-				float time2 = nextMap.getFloat("time", 0);
-				float mixRotate2 = nextMap.getFloat("mixRotate", 1);
-				float mixX2 = nextMap.getFloat("mixX", 1), mixY2 = nextMap.getFloat("mixY", mixX2);
-				float mixScaleX2 = nextMap.getFloat("mixScaleX", 1), mixScaleY2 = nextMap.getFloat("mixScaleY", mixScaleX2);
-				float mixShearY2 = nextMap.getFloat("mixShearY", 1);
-				JsonValue curve = keyMap.get("curve");
-				if (curve != null) {
-					bezier = readCurve(curve, timeline, bezier, frame, 0, time, time2, mixRotate, mixRotate2, 1);
-					bezier = readCurve(curve, timeline, bezier, frame, 1, time, time2, mixX, mixX2, 1);
-					bezier = readCurve(curve, timeline, bezier, frame, 2, time, time2, mixY, mixY2, 1);
-					bezier = readCurve(curve, timeline, bezier, frame, 3, time, time2, mixScaleX, mixScaleX2, 1);
-					bezier = readCurve(curve, timeline, bezier, frame, 4, time, time2, mixScaleY, mixScaleY2, 1);
-					bezier = readCurve(curve, timeline, bezier, frame, 5, time, time2, mixShearY, mixShearY2, 1);
-				}
-				time = time2;
-				mixRotate = mixRotate2;
-				mixX = mixX2;
-				mixY = mixY2;
-				mixScaleX = mixScaleX2;
-				mixScaleY = mixScaleY2;
-				mixScaleX = mixScaleX2;
-				keyMap = nextMap;
-			}
-			timelines.add(timeline);
-		}
-
-		// Path constraint timelines.
-		for (JsonValue constraintMap = map.getChild("path"); constraintMap != null; constraintMap = constraintMap.next) {
-			PathConstraintData constraint = skeletonData.findPathConstraint(constraintMap.name);
-			if (constraint == null) throw new SerializationException("Path constraint not found: " + constraintMap.name);
-			int index = skeletonData.pathConstraints.indexOf(constraint, true);
-			for (JsonValue timelineMap = constraintMap.child; timelineMap != null; timelineMap = timelineMap.next) {
-				JsonValue keyMap = timelineMap.child;
-				if (keyMap == null) continue;
-				String timelineName = timelineMap.name;
-				if (timelineName.equals("position")) {
-					CurveTimeline1 timeline = new PathConstraintPositionTimeline(timelineMap.size, timelineMap.size, index);
-					timelines.add(readTimeline(keyMap, timeline, 0, constraint.positionMode == PositionMode.fixed ? scale : 1));
-				} else if (timelineName.equals("spacing")) {
-					CurveTimeline1 timeline = new PathConstraintSpacingTimeline(timelineMap.size, timelineMap.size, index);
-					timelines.add(readTimeline(keyMap, timeline, 0,
-						constraint.spacingMode == SpacingMode.length || constraint.spacingMode == SpacingMode.fixed ? scale : 1));
-				} else if (timelineName.equals("mix")) {
-					PathConstraintMixTimeline timeline = new PathConstraintMixTimeline(timelineMap.size, timelineMap.size * 3, index);
-					float time = keyMap.getFloat("time", 0);
-					float mixRotate = keyMap.getFloat("mixRotate", 1);
-					float mixX = keyMap.getFloat("mixX", 1), mixY = keyMap.getFloat("mixY", mixX);
-					for (int frame = 0, bezier = 0;; frame++) {
-						timeline.setFrame(frame, time, mixRotate, mixX, mixY);
-						JsonValue nextMap = keyMap.next;
-						if (nextMap == null) {
-							timeline.shrink(bezier);
-							break;
-						}
-						float time2 = nextMap.getFloat("time", 0);
-						float mixRotate2 = nextMap.getFloat("mixRotate", 1);
-						float mixX2 = nextMap.getFloat("mixX", 1), mixY2 = nextMap.getFloat("mixY", mixX2);
-						JsonValue curve = keyMap.get("curve");
-						if (curve != null) {
-							bezier = readCurve(curve, timeline, bezier, frame, 0, time, time2, mixRotate, mixRotate2, 1);
-							bezier = readCurve(curve, timeline, bezier, frame, 1, time, time2, mixX, mixX2, 1);
-							bezier = readCurve(curve, timeline, bezier, frame, 2, time, time2, mixY, mixY2, 1);
-						}
-						time = time2;
-						mixRotate = mixRotate2;
-						mixX = mixX2;
-						mixY = mixY2;
-						keyMap = nextMap;
-					}
-					timelines.add(timeline);
-				}
-			}
-		}
-
-		// Deform timelines.
-		for (JsonValue deformMap = map.getChild("deform"); deformMap != null; deformMap = deformMap.next) {
-			Skin skin = skeletonData.findSkin(deformMap.name);
-			if (skin == null) throw new SerializationException("Skin not found: " + deformMap.name);
-			for (JsonValue slotMap = deformMap.child; slotMap != null; slotMap = slotMap.next) {
-				SlotData slot = skeletonData.findSlot(slotMap.name);
-				if (slot == null) throw new SerializationException("Slot not found: " + slotMap.name);
-				for (JsonValue timelineMap = slotMap.child; timelineMap != null; timelineMap = timelineMap.next) {
-					JsonValue keyMap = timelineMap.child;
-					if (keyMap == null) continue;
-
-					VertexAttachment attachment = (VertexAttachment)skin.getAttachment(slot.index, timelineMap.name);
-					if (attachment == null) throw new SerializationException("Deform attachment not found: " + timelineMap.name);
-					boolean weighted = attachment.getBones() != null;
-					float[] vertices = attachment.getVertices();
-					int deformLength = weighted ? (vertices.length / 3) << 1 : vertices.length;
-
-					DeformTimeline timeline = new DeformTimeline(timelineMap.size, timelineMap.size, slot.index, attachment);
-					float time = keyMap.getFloat("time", 0);
-					for (int frame = 0, bezier = 0;; frame++) {
-						float[] deform;
-						JsonValue verticesValue = keyMap.get("vertices");
-						if (verticesValue == null)
-							deform = weighted ? new float[deformLength] : vertices;
-						else {
-							deform = new float[deformLength];
-							int start = keyMap.getInt("offset", 0);
-							arraycopy(verticesValue.asFloatArray(), 0, deform, start, verticesValue.size);
-							if (scale != 1) {
-								for (int i = start, n = i + verticesValue.size; i < n; i++)
-									deform[i] *= scale;
-							}
-							if (!weighted) {
-								for (int i = 0; i < deformLength; i++)
-									deform[i] += vertices[i];
-							}
-						}
-
-						timeline.setFrame(frame, time, deform);
-						JsonValue nextMap = keyMap.next;
-						if (nextMap == null) {
-							timeline.shrink(bezier);
-							break;
-						}
-						float time2 = nextMap.getFloat("time", 0);
-						JsonValue curve = keyMap.get("curve");
-						if (curve != null) bezier = readCurve(curve, timeline, bezier, frame, 0, time, time2, 0, 1, 1);
-						time = time2;
-						keyMap = nextMap;
-					}
-					timelines.add(timeline);
-				}
-			}
-		}
-
-		// Draw order timeline.
-		JsonValue drawOrdersMap = map.get("drawOrder");
-		if (drawOrdersMap != null) {
-			DrawOrderTimeline timeline = new DrawOrderTimeline(drawOrdersMap.size);
-			int slotCount = skeletonData.slots.size;
-			int frame = 0;
-			for (JsonValue drawOrderMap = drawOrdersMap.child; drawOrderMap != null; drawOrderMap = drawOrderMap.next, frame++) {
-				int[] drawOrder = null;
-				JsonValue offsets = drawOrderMap.get("offsets");
-				if (offsets != null) {
-					drawOrder = new int[slotCount];
-					for (int i = slotCount - 1; i >= 0; i--)
-						drawOrder[i] = -1;
-					int[] unchanged = new int[slotCount - offsets.size];
-					int originalIndex = 0, unchangedIndex = 0;
-					for (JsonValue offsetMap = offsets.child; offsetMap != null; offsetMap = offsetMap.next) {
-						SlotData slot = skeletonData.findSlot(offsetMap.getString("slot"));
-						if (slot == null) throw new SerializationException("Slot not found: " + offsetMap.getString("slot"));
-						// Collect unchanged items.
-						while (originalIndex != slot.index)
-							unchanged[unchangedIndex++] = originalIndex++;
-						// Set changed items.
-						drawOrder[originalIndex + offsetMap.getInt("offset")] = originalIndex++;
-					}
-					// Collect remaining unchanged items.
-					while (originalIndex < slotCount)
-						unchanged[unchangedIndex++] = originalIndex++;
-					// Fill in unchanged items.
-					for (int i = slotCount - 1; i >= 0; i--)
-						if (drawOrder[i] == -1) drawOrder[i] = unchanged[--unchangedIndex];
-				}
-				timeline.setFrame(frame, drawOrderMap.getFloat("time", 0), drawOrder);
-			}
-			timelines.add(timeline);
-		}
-
-		// Event timeline.
-		JsonValue eventsMap = map.get("events");
-		if (eventsMap != null) {
-			EventTimeline timeline = new EventTimeline(eventsMap.size);
-			int frame = 0;
-			for (JsonValue eventMap = eventsMap.child; eventMap != null; eventMap = eventMap.next, frame++) {
-				EventData eventData = skeletonData.findEvent(eventMap.getString("name"));
-				if (eventData == null) throw new SerializationException("Event not found: " + eventMap.getString("name"));
-				Event event = new Event(eventMap.getFloat("time", 0), eventData);
-				event.intValue = eventMap.getInt("int", eventData.intValue);
-				event.floatValue = eventMap.getFloat("float", eventData.floatValue);
-				event.stringValue = eventMap.getString("string", eventData.stringValue);
-				if (event.getData().audioPath != null) {
-					event.volume = eventMap.getFloat("volume", eventData.volume);
-					event.balance = eventMap.getFloat("balance", eventData.balance);
-				}
-				timeline.setFrame(frame, event);
-			}
-			timelines.add(timeline);
-		}
-
-		timelines.shrink();
-		float duration = 0;
-		Object[] items = timelines.items;
-		for (int i = 0, n = timelines.size; i < n; i++)
-			duration = Math.max(duration, ((Timeline)items[i]).getDuration());
-		skeletonData.animations.add(new Animation(name, timelines, duration));
-	}
-
-	private Timeline readTimeline (JsonValue keyMap, CurveTimeline1 timeline, float defaultValue, float scale) {
-		float time = keyMap.getFloat("time", 0), value = keyMap.getFloat("value", defaultValue) * scale;
-		for (int frame = 0, bezier = 0;; frame++) {
-			timeline.setFrame(frame, time, value);
-			JsonValue nextMap = keyMap.next;
-			if (nextMap == null) {
-				timeline.shrink(bezier);
-				return timeline;
-			}
-			float time2 = nextMap.getFloat("time", 0);
-			float value2 = nextMap.getFloat("value", defaultValue) * scale;
-			JsonValue curve = keyMap.get("curve");
-			if (curve != null) bezier = readCurve(curve, timeline, bezier, frame, 0, time, time2, value, value2, scale);
-			time = time2;
-			value = value2;
-			keyMap = nextMap;
-		}
-	}
-
-	private Timeline readTimeline (JsonValue keyMap, CurveTimeline2 timeline, String name1, String name2, float defaultValue,
-		float scale) {
-		float time = keyMap.getFloat("time", 0);
-		float value1 = keyMap.getFloat(name1, defaultValue) * scale, value2 = keyMap.getFloat(name2, defaultValue) * scale;
-		for (int frame = 0, bezier = 0;; frame++) {
-			timeline.setFrame(frame, time, value1, value2);
-			JsonValue nextMap = keyMap.next;
-			if (nextMap == null) {
-				timeline.shrink(bezier);
-				return timeline;
-			}
-			float time2 = nextMap.getFloat("time", 0);
-			float nvalue1 = nextMap.getFloat(name1, defaultValue) * scale, nvalue2 = nextMap.getFloat(name2, defaultValue) * scale;
-			JsonValue curve = keyMap.get("curve");
-			if (curve != null) {
-				bezier = readCurve(curve, timeline, bezier, frame, 0, time, time2, value1, nvalue1, scale);
-				bezier = readCurve(curve, timeline, bezier, frame, 1, time, time2, value2, nvalue2, scale);
-			}
-			time = time2;
-			value1 = nvalue1;
-			value2 = nvalue2;
-			keyMap = nextMap;
-		}
-	}
-
-	int readCurve (JsonValue curve, CurveTimeline timeline, int bezier, int frame, int value, float time1, float time2,
-		float value1, float value2, float scale) {
-		if (curve.isString()) {
-			if (curve.asString().equals("stepped")) timeline.setStepped(frame);
-			return bezier;
-		}
-		curve = curve.get(value << 2);
-		float cx1 = curve.asFloat();
-		curve = curve.next;
-		float cy1 = curve.asFloat() * scale;
-		curve = curve.next;
-		float cx2 = curve.asFloat();
-		curve = curve.next;
-		float cy2 = curve.asFloat() * scale;
-		setBezier(timeline, frame, value, bezier, time1, value1, cx1, cy1, cx2, cy2, time2, value2);
-		return bezier + 1;
-	}
-
-	static void setBezier (CurveTimeline timeline, int frame, int value, int bezier, float time1, float value1, float cx1,
-		float cy1, float cx2, float cy2, float time2, float value2) {
-		timeline.setBezier(bezier, frame, value, time1, value1, cx1, cy1, cx2, cy2, time2, value2);
-	}
-
-	static class LinkedMesh {
-		String parent, skin;
-		int slotIndex;
-		MeshAttachment mesh;
-		boolean inheritDeform;
-
-		public LinkedMesh (MeshAttachment mesh, String skin, int slotIndex, String parent, boolean inheritDeform) {
-			this.mesh = mesh;
-			this.skin = skin;
-			this.slotIndex = slotIndex;
-			this.parent = parent;
-			this.inheritDeform = inheritDeform;
-		}
-	}
-}
+/******************************************************************************
+ * Spine Runtimes License Agreement
+ * Last updated January 1, 2020. Replaces all prior versions.
+ *
+ * Copyright (c) 2013-2020, Esoteric Software LLC
+ *
+ * Integration of the Spine Runtimes into software or otherwise creating
+ * derivative works of the Spine Runtimes is permitted under the terms and
+ * conditions of Section 2 of the Spine Editor License Agreement:
+ * http://esotericsoftware.com/spine-editor-license
+ *
+ * Otherwise, it is permitted to integrate the Spine Runtimes into software
+ * or otherwise create derivative works of the Spine Runtimes (collectively,
+ * "Products"), provided that each user of the Products must obtain their own
+ * Spine Editor license and redistribution of the Products in any form must
+ * include this license and copyright notice.
+ *
+ * THE SPINE RUNTIMES ARE PROVIDED BY ESOTERIC SOFTWARE LLC "AS IS" AND ANY
+ * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL ESOTERIC SOFTWARE LLC BE LIABLE FOR ANY
+ * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES,
+ * BUSINESS INTERRUPTION, OR LOSS OF USE, DATA, OR PROFITS) HOWEVER CAUSED AND
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
+ * THE SPINE RUNTIMES, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ *****************************************************************************/
+
+package com.esotericsoftware.spine;
+
+import static com.esotericsoftware.spine.utils.SpineUtils.*;
+
+import java.io.InputStream;
+
+import com.badlogic.gdx.files.FileHandle;
+import com.badlogic.gdx.graphics.Color;
+import com.badlogic.gdx.graphics.g2d.TextureAtlas;
+import com.badlogic.gdx.utils.Array;
+import com.badlogic.gdx.utils.FloatArray;
+import com.badlogic.gdx.utils.IntArray;
+import com.badlogic.gdx.utils.JsonReader;
+import com.badlogic.gdx.utils.JsonValue;
+import com.badlogic.gdx.utils.SerializationException;
+
+import com.esotericsoftware.spine.Animation.AlphaTimeline;
+import com.esotericsoftware.spine.Animation.AttachmentTimeline;
+import com.esotericsoftware.spine.Animation.CurveTimeline;
+import com.esotericsoftware.spine.Animation.CurveTimeline1;
+import com.esotericsoftware.spine.Animation.CurveTimeline2;
+import com.esotericsoftware.spine.Animation.DeformTimeline;
+import com.esotericsoftware.spine.Animation.DrawOrderTimeline;
+import com.esotericsoftware.spine.Animation.EventTimeline;
+import com.esotericsoftware.spine.Animation.IkConstraintTimeline;
+import com.esotericsoftware.spine.Animation.PathConstraintMixTimeline;
+import com.esotericsoftware.spine.Animation.PathConstraintPositionTimeline;
+import com.esotericsoftware.spine.Animation.PathConstraintSpacingTimeline;
+import com.esotericsoftware.spine.Animation.RGB2Timeline;
+import com.esotericsoftware.spine.Animation.RGBA2Timeline;
+import com.esotericsoftware.spine.Animation.RGBATimeline;
+import com.esotericsoftware.spine.Animation.RGBTimeline;
+import com.esotericsoftware.spine.Animation.RotateTimeline;
+import com.esotericsoftware.spine.Animation.ScaleTimeline;
+import com.esotericsoftware.spine.Animation.ScaleXTimeline;
+import com.esotericsoftware.spine.Animation.ScaleYTimeline;
+import com.esotericsoftware.spine.Animation.ShearTimeline;
+import com.esotericsoftware.spine.Animation.ShearXTimeline;
+import com.esotericsoftware.spine.Animation.ShearYTimeline;
+import com.esotericsoftware.spine.Animation.Timeline;
+import com.esotericsoftware.spine.Animation.TransformConstraintTimeline;
+import com.esotericsoftware.spine.Animation.TranslateTimeline;
+import com.esotericsoftware.spine.Animation.TranslateXTimeline;
+import com.esotericsoftware.spine.Animation.TranslateYTimeline;
+import com.esotericsoftware.spine.BoneData.TransformMode;
+import com.esotericsoftware.spine.PathConstraintData.PositionMode;
+import com.esotericsoftware.spine.PathConstraintData.RotateMode;
+import com.esotericsoftware.spine.PathConstraintData.SpacingMode;
+import com.esotericsoftware.spine.attachments.Attachment;
+import com.esotericsoftware.spine.attachments.AttachmentLoader;
+import com.esotericsoftware.spine.attachments.AttachmentType;
+import com.esotericsoftware.spine.attachments.BoundingBoxAttachment;
+import com.esotericsoftware.spine.attachments.ClippingAttachment;
+import com.esotericsoftware.spine.attachments.MeshAttachment;
+import com.esotericsoftware.spine.attachments.PathAttachment;
+import com.esotericsoftware.spine.attachments.PointAttachment;
+import com.esotericsoftware.spine.attachments.RegionAttachment;
+import com.esotericsoftware.spine.attachments.VertexAttachment;
+
+/** Loads skeleton data in the Spine JSON format.
+ * <p>
+ * JSON is human readable but the binary format is much smaller on disk and faster to load. See {@link SkeletonBinary}.
+ * <p>
+ * See <a href="http://esotericsoftware.com/spine-json-format">Spine JSON format</a> and
+ * <a href="http://esotericsoftware.com/spine-loading-skeleton-data#JSON-and-binary-data">JSON and binary data</a> in the Spine
+ * Runtimes Guide. */
+public class SkeletonJson extends SkeletonLoader {
+	public SkeletonJson (AttachmentLoader attachmentLoader) {
+		super(attachmentLoader);
+	}
+
+	public SkeletonJson (TextureAtlas atlas) {
+		super(atlas);
+	}
+
+	public SkeletonData readSkeletonData (FileHandle file) {
+		if (file == null) throw new IllegalArgumentException("file cannot be null.");
+		SkeletonData skeletonData = readSkeletonData(new JsonReader().parse(file));
+		skeletonData.name = file.nameWithoutExtension();
+		return skeletonData;
+	}
+
+	public SkeletonData readSkeletonData (InputStream input) {
+		if (input == null) throw new IllegalArgumentException("dataInput cannot be null.");
+		return readSkeletonData(new JsonReader().parse(input));
+	}
+
+	public SkeletonData readSkeletonData (JsonValue root) {
+		if (root == null) throw new IllegalArgumentException("root cannot be null.");
+
+		float scale = this.scale;
+
+		// Skeleton.
+		SkeletonData skeletonData = new SkeletonData();
+		JsonValue skeletonMap = root.get("skeleton");
+		if (skeletonMap != null) {
+			skeletonData.hash = skeletonMap.getString("hash", null);
+			skeletonData.version = skeletonMap.getString("spine", null);
+			skeletonData.x = skeletonMap.getFloat("x", 0);
+			skeletonData.y = skeletonMap.getFloat("y", 0);
+			skeletonData.width = skeletonMap.getFloat("width", 0);
+			skeletonData.height = skeletonMap.getFloat("height", 0);
+			skeletonData.fps = skeletonMap.getFloat("fps", 30);
+			skeletonData.imagesPath = skeletonMap.getString("images", null);
+			skeletonData.audioPath = skeletonMap.getString("audio", null);
+		}
+
+		// Bones.
+		for (JsonValue boneMap = root.getChild("bones"); boneMap != null; boneMap = boneMap.next) {
+			BoneData parent = null;
+			String parentName = boneMap.getString("parent", null);
+			if (parentName != null) {
+				parent = skeletonData.findBone(parentName);
+				if (parent == null) throw new SerializationException("Parent bone not found: " + parentName);
+			}
+			BoneData data = new BoneData(skeletonData.bones.size, boneMap.getString("name"), parent);
+			data.length = boneMap.getFloat("length", 0) * scale;
+			data.x = boneMap.getFloat("x", 0) * scale;
+			data.y = boneMap.getFloat("y", 0) * scale;
+			data.rotation = boneMap.getFloat("rotation", 0);
+			data.scaleX = boneMap.getFloat("scaleX", 1);
+			data.scaleY = boneMap.getFloat("scaleY", 1);
+			data.shearX = boneMap.getFloat("shearX", 0);
+			data.shearY = boneMap.getFloat("shearY", 0);
+			data.transformMode = TransformMode.valueOf(boneMap.getString("transform", TransformMode.normal.name()));
+			data.skinRequired = boneMap.getBoolean("skin", false);
+
+			String color = boneMap.getString("color", null);
+			if (color != null) Color.valueOf(color, data.getColor());
+
+			skeletonData.bones.add(data);
+		}
+
+		// Slots.
+		for (JsonValue slotMap = root.getChild("slots"); slotMap != null; slotMap = slotMap.next) {
+			String slotName = slotMap.getString("name");
+			String boneName = slotMap.getString("bone");
+			BoneData boneData = skeletonData.findBone(boneName);
+			if (boneData == null) throw new SerializationException("Slot bone not found: " + boneName);
+			SlotData data = new SlotData(skeletonData.slots.size, slotName, boneData);
+
+			String color = slotMap.getString("color", null);
+			if (color != null) Color.valueOf(color, data.getColor());
+
+			String dark = slotMap.getString("dark", null);
+			if (dark != null) data.setDarkColor(Color.valueOf(dark));
+
+			data.attachmentName = slotMap.getString("attachment", null);
+			data.blendMode = BlendMode.valueOf(slotMap.getString("blend", BlendMode.normal.name()));
+			skeletonData.slots.add(data);
+		}
+
+		// IK constraints.
+		for (JsonValue constraintMap = root.getChild("ik"); constraintMap != null; constraintMap = constraintMap.next) {
+			IkConstraintData data = new IkConstraintData(constraintMap.getString("name"));
+			data.order = constraintMap.getInt("order", 0);
+			data.skinRequired = constraintMap.getBoolean("skin", false);
+
+			for (JsonValue entry = constraintMap.getChild("bones"); entry != null; entry = entry.next) {
+				BoneData bone = skeletonData.findBone(entry.asString());
+				if (bone == null) throw new SerializationException("IK bone not found: " + entry);
+				data.bones.add(bone);
+			}
+
+			String targetName = constraintMap.getString("target");
+			data.target = skeletonData.findBone(targetName);
+			if (data.target == null) throw new SerializationException("IK target bone not found: " + targetName);
+
+			data.mix = constraintMap.getFloat("mix", 1);
+			data.softness = constraintMap.getFloat("softness", 0) * scale;
+			data.bendDirection = constraintMap.getBoolean("bendPositive", true) ? 1 : -1;
+			data.compress = constraintMap.getBoolean("compress", false);
+			data.stretch = constraintMap.getBoolean("stretch", false);
+			data.uniform = constraintMap.getBoolean("uniform", false);
+
+			skeletonData.ikConstraints.add(data);
+		}
+
+		// Transform constraints.
+		for (JsonValue constraintMap = root.getChild("transform"); constraintMap != null; constraintMap = constraintMap.next) {
+			TransformConstraintData data = new TransformConstraintData(constraintMap.getString("name"));
+			data.order = constraintMap.getInt("order", 0);
+			data.skinRequired = constraintMap.getBoolean("skin", false);
+
+			for (JsonValue entry = constraintMap.getChild("bones"); entry != null; entry = entry.next) {
+				BoneData bone = skeletonData.findBone(entry.asString());
+				if (bone == null) throw new SerializationException("Transform constraint bone not found: " + entry);
+				data.bones.add(bone);
+			}
+
+			String targetName = constraintMap.getString("target");
+			data.target = skeletonData.findBone(targetName);
+			if (data.target == null) throw new SerializationException("Transform constraint target bone not found: " + targetName);
+
+			data.local = constraintMap.getBoolean("local", false);
+			data.relative = constraintMap.getBoolean("relative", false);
+
+			data.offsetRotation = constraintMap.getFloat("rotation", 0);
+			data.offsetX = constraintMap.getFloat("x", 0) * scale;
+			data.offsetY = constraintMap.getFloat("y", 0) * scale;
+			data.offsetScaleX = constraintMap.getFloat("scaleX", 0);
+			data.offsetScaleY = constraintMap.getFloat("scaleY", 0);
+			data.offsetShearY = constraintMap.getFloat("shearY", 0);
+
+			data.mixRotate = constraintMap.getFloat("mixRotate", 1);
+			data.mixX = constraintMap.getFloat("mixX", 1);
+			data.mixY = constraintMap.getFloat("mixY", data.mixX);
+			data.mixScaleX = constraintMap.getFloat("mixScaleX", 1);
+			data.mixScaleY = constraintMap.getFloat("mixScaleY", data.mixScaleX);
+			data.mixShearY = constraintMap.getFloat("mixShearY", 1);
+
+			skeletonData.transformConstraints.add(data);
+		}
+
+		// Path constraints.
+		for (JsonValue constraintMap = root.getChild("path"); constraintMap != null; constraintMap = constraintMap.next) {
+			PathConstraintData data = new PathConstraintData(constraintMap.getString("name"));
+			data.order = constraintMap.getInt("order", 0);
+			data.skinRequired = constraintMap.getBoolean("skin", false);
+
+			for (JsonValue entry = constraintMap.getChild("bones"); entry != null; entry = entry.next) {
+				BoneData bone = skeletonData.findBone(entry.asString());
+				if (bone == null) throw new SerializationException("Path bone not found: " + entry);
+				data.bones.add(bone);
+			}
+
+			String targetName = constraintMap.getString("target");
+			data.target = skeletonData.findSlot(targetName);
+			if (data.target == null) throw new SerializationException("Path target slot not found: " + targetName);
+
+			data.positionMode = PositionMode.valueOf(constraintMap.getString("positionMode", "percent"));
+			data.spacingMode = SpacingMode.valueOf(constraintMap.getString("spacingMode", "length"));
+			data.rotateMode = RotateMode.valueOf(constraintMap.getString("rotateMode", "tangent"));
+			data.offsetRotation = constraintMap.getFloat("rotation", 0);
+			data.position = constraintMap.getFloat("position", 0);
+			if (data.positionMode == PositionMode.fixed) data.position *= scale;
+			data.spacing = constraintMap.getFloat("spacing", 0);
+			if (data.spacingMode == SpacingMode.length || data.spacingMode == SpacingMode.fixed) data.spacing *= scale;
+			data.mixRotate = constraintMap.getFloat("mixRotate", 1);
+			data.mixX = constraintMap.getFloat("mixX", 1);
+			data.mixY = constraintMap.getFloat("mixY", 1);
+
+			skeletonData.pathConstraints.add(data);
+		}
+
+		// Skins.
+		for (JsonValue skinMap = root.getChild("skins"); skinMap != null; skinMap = skinMap.next) {
+			Skin skin = new Skin(skinMap.getString("name"));
+			for (JsonValue entry = skinMap.getChild("bones"); entry != null; entry = entry.next) {
+				BoneData bone = skeletonData.findBone(entry.asString());
+				if (bone == null) throw new SerializationException("Skin bone not found: " + entry);
+				skin.bones.add(bone);
+			}
+			skin.bones.shrink();
+			for (JsonValue entry = skinMap.getChild("ik"); entry != null; entry = entry.next) {
+				IkConstraintData constraint = skeletonData.findIkConstraint(entry.asString());
+				if (constraint == null) throw new SerializationException("Skin IK constraint not found: " + entry);
+				skin.constraints.add(constraint);
+			}
+			for (JsonValue entry = skinMap.getChild("transform"); entry != null; entry = entry.next) {
+				TransformConstraintData constraint = skeletonData.findTransformConstraint(entry.asString());
+				if (constraint == null) throw new SerializationException("Skin transform constraint not found: " + entry);
+				skin.constraints.add(constraint);
+			}
+			for (JsonValue entry = skinMap.getChild("path"); entry != null; entry = entry.next) {
+				PathConstraintData constraint = skeletonData.findPathConstraint(entry.asString());
+				if (constraint == null) throw new SerializationException("Skin path constraint not found: " + entry);
+				skin.constraints.add(constraint);
+			}
+			skin.constraints.shrink();
+			for (JsonValue slotEntry = skinMap.getChild("attachments"); slotEntry != null; slotEntry = slotEntry.next) {
+				SlotData slot = skeletonData.findSlot(slotEntry.name);
+				if (slot == null) throw new SerializationException("Slot not found: " + slotEntry.name);
+				for (JsonValue entry = slotEntry.child; entry != null; entry = entry.next) {
+					try {
+						Attachment attachment = readAttachment(entry, skin, slot.index, entry.name, skeletonData);
+						if (attachment != null) skin.setAttachment(slot.index, entry.name, attachment);
+					} catch (Throwable ex) {
+						throw new SerializationException("Error reading attachment: " + entry.name + ", skin: " + skin, ex);
+					}
+				}
+			}
+			skeletonData.skins.add(skin);
+			if (skin.name.equals("default")) skeletonData.defaultSkin = skin;
+		}
+
+		// Linked meshes.
+		Object[] items = linkedMeshes.items;
+		for (int i = 0, n = linkedMeshes.size; i < n; i++) {
+			LinkedMesh linkedMesh = (LinkedMesh)items[i];
+			Skin skin = linkedMesh.skin == null ? skeletonData.getDefaultSkin() : skeletonData.findSkin(linkedMesh.skin);
+			if (skin == null) throw new SerializationException("Skin not found: " + linkedMesh.skin);
+			Attachment parent = skin.getAttachment(linkedMesh.slotIndex, linkedMesh.parent);
+			if (parent == null) throw new SerializationException("Parent mesh not found: " + linkedMesh.parent);
+			linkedMesh.mesh.setDeformAttachment(linkedMesh.inheritDeform ? (VertexAttachment)parent : linkedMesh.mesh);
+			linkedMesh.mesh.setParentMesh((MeshAttachment)parent);
+			linkedMesh.mesh.updateUVs();
+		}
+		linkedMeshes.clear();
+
+		// Events.
+		for (JsonValue eventMap = root.getChild("events"); eventMap != null; eventMap = eventMap.next) {
+			EventData data = new EventData(eventMap.name);
+			data.intValue = eventMap.getInt("int", 0);
+			data.floatValue = eventMap.getFloat("float", 0f);
+			data.stringValue = eventMap.getString("string", "");
+			data.audioPath = eventMap.getString("audio", null);
+			if (data.audioPath != null) {
+				data.volume = eventMap.getFloat("volume", 1);
+				data.balance = eventMap.getFloat("balance", 0);
+			}
+			skeletonData.events.add(data);
+		}
+
+		// Animations.
+		for (JsonValue animationMap = root.getChild("animations"); animationMap != null; animationMap = animationMap.next) {
+			try {
+				readAnimation(animationMap, animationMap.name, skeletonData);
+			} catch (Throwable ex) {
+				throw new SerializationException("Error reading animation: " + animationMap.name, ex);
+			}
+		}
+
+		skeletonData.bones.shrink();
+		skeletonData.slots.shrink();
+		skeletonData.skins.shrink();
+		skeletonData.events.shrink();
+		skeletonData.animations.shrink();
+		skeletonData.ikConstraints.shrink();
+		return skeletonData;
+	}
+
+	private Attachment readAttachment (JsonValue map, Skin skin, int slotIndex, String name, SkeletonData skeletonData) {
+		float scale = this.scale;
+		name = map.getString("name", name);
+
+		switch (AttachmentType.valueOf(map.getString("type", AttachmentType.region.name()))) {
+		case region: {
+			String path = map.getString("path", name);
+			RegionAttachment region = attachmentLoader.newRegionAttachment(skin, name, path);
+			if (region == null) return null;
+			region.setPath(path);
+			region.setX(map.getFloat("x", 0) * scale);
+			region.setY(map.getFloat("y", 0) * scale);
+			region.setScaleX(map.getFloat("scaleX", 1));
+			region.setScaleY(map.getFloat("scaleY", 1));
+			region.setRotation(map.getFloat("rotation", 0));
+			region.setWidth(map.getFloat("width") * scale);
+			region.setHeight(map.getFloat("height") * scale);
+
+			String color = map.getString("color", null);
+			if (color != null) Color.valueOf(color, region.getColor());
+
+			region.updateOffset();
+			return region;
+		}
+		case boundingbox: {
+			BoundingBoxAttachment box = attachmentLoader.newBoundingBoxAttachment(skin, name);
+			if (box == null) return null;
+			readVertices(map, box, map.getInt("vertexCount") << 1);
+
+			String color = map.getString("color", null);
+			if (color != null) Color.valueOf(color, box.getColor());
+			return box;
+		}
+		case mesh:
+		case linkedmesh: {
+			String path = map.getString("path", name);
+			MeshAttachment mesh = attachmentLoader.newMeshAttachment(skin, name, path);
+			if (mesh == null) return null;
+			mesh.setPath(path);
+
+			String color = map.getString("color", null);
+			if (color != null) Color.valueOf(color, mesh.getColor());
+
+			mesh.setWidth(map.getFloat("width", 0) * scale);
+			mesh.setHeight(map.getFloat("height", 0) * scale);
+
+			String parent = map.getString("parent", null);
+			if (parent != null) {
+				linkedMeshes
+					.add(new LinkedMesh(mesh, map.getString("skin", null), slotIndex, parent, map.getBoolean("deform", true)));
+				return mesh;
+			}
+
+			float[] uvs = map.require("uvs").asFloatArray();
+			readVertices(map, mesh, uvs.length);
+			mesh.setTriangles(map.require("triangles").asShortArray());
+			mesh.setRegionUVs(uvs);
+			mesh.updateUVs();
+
+			if (map.has("hull")) mesh.setHullLength(map.require("hull").asInt() << 1);
+			if (map.has("edges")) mesh.setEdges(map.require("edges").asShortArray());
+			return mesh;
+		}
+		case path: {
+			PathAttachment path = attachmentLoader.newPathAttachment(skin, name);
+			if (path == null) return null;
+			path.setClosed(map.getBoolean("closed", false));
+			path.setConstantSpeed(map.getBoolean("constantSpeed", true));
+
+			int vertexCount = map.getInt("vertexCount");
+			readVertices(map, path, vertexCount << 1);
+
+			float[] lengths = new float[vertexCount / 3];
+			int i = 0;
+			for (JsonValue curves = map.require("lengths").child; curves != null; curves = curves.next)
+				lengths[i++] = curves.asFloat() * scale;
+			path.setLengths(lengths);
+
+			String color = map.getString("color", null);
+			if (color != null) Color.valueOf(color, path.getColor());
+			return path;
+		}
+		case point: {
+			PointAttachment point = attachmentLoader.newPointAttachment(skin, name);
+			if (point == null) return null;
+			point.setX(map.getFloat("x", 0) * scale);
+			point.setY(map.getFloat("y", 0) * scale);
+			point.setRotation(map.getFloat("rotation", 0));
+
+			String color = map.getString("color", null);
+			if (color != null) Color.valueOf(color, point.getColor());
+			return point;
+		}
+		case clipping:
+			ClippingAttachment clip = attachmentLoader.newClippingAttachment(skin, name);
+			if (clip == null) return null;
+
+			String end = map.getString("end", null);
+			if (end != null) {
+				SlotData slot = skeletonData.findSlot(end);
+				if (slot == null) throw new SerializationException("Clipping end slot not found: " + end);
+				clip.setEndSlot(slot);
+			}
+
+			readVertices(map, clip, map.getInt("vertexCount") << 1);
+
+			String color = map.getString("color", null);
+			if (color != null) Color.valueOf(color, clip.getColor());
+			return clip;
+		}
+		return null;
+	}
+
+	private void readVertices (JsonValue map, VertexAttachment attachment, int verticesLength) {
+		attachment.setWorldVerticesLength(verticesLength);
+		float[] vertices = map.require("vertices").asFloatArray();
+		if (verticesLength == vertices.length) {
+			if (scale != 1) {
+				for (int i = 0, n = vertices.length; i < n; i++)
+					vertices[i] *= scale;
+			}
+			attachment.setVertices(vertices);
+			return;
+		}
+		FloatArray weights = new FloatArray(verticesLength * 3 * 3);
+		IntArray bones = new IntArray(verticesLength * 3);
+		for (int i = 0, n = vertices.length; i < n;) {
+			int boneCount = (int)vertices[i++];
+			bones.add(boneCount);
+			for (int nn = i + (boneCount << 2); i < nn; i += 4) {
+				bones.add((int)vertices[i]);
+				weights.add(vertices[i + 1] * scale);
+				weights.add(vertices[i + 2] * scale);
+				weights.add(vertices[i + 3]);
+			}
+		}
+		attachment.setBones(bones.toArray());
+		attachment.setVertices(weights.toArray());
+	}
+
+	private void readAnimation (JsonValue map, String name, SkeletonData skeletonData) {
+		float scale = this.scale;
+		Array<Timeline> timelines = new Array();
+
+		// Slot timelines.
+		for (JsonValue slotMap = map.getChild("slots"); slotMap != null; slotMap = slotMap.next) {
+			SlotData slot = skeletonData.findSlot(slotMap.name);
+			if (slot == null) throw new SerializationException("Slot not found: " + slotMap.name);
+			for (JsonValue timelineMap = slotMap.child; timelineMap != null; timelineMap = timelineMap.next) {
+				JsonValue keyMap = timelineMap.child;
+				if (keyMap == null) continue;
+				String timelineName = timelineMap.name;
+
+				if (timelineName.equals("attachment")) {
+					AttachmentTimeline timeline = new AttachmentTimeline(timelineMap.size, slot.index);
+					for (int frame = 0; keyMap != null; keyMap = keyMap.next, frame++)
+						timeline.setFrame(frame, keyMap.getFloat("time", 0), keyMap.getString("name"));
+					timelines.add(timeline);
+
+				} else if (timelineName.equals("rgba")) {
+					RGBATimeline timeline = new RGBATimeline(timelineMap.size, timelineMap.size << 2, slot.index);
+					float time = keyMap.getFloat("time", 0);
+					String color = keyMap.getString("color");
+					float r = Integer.parseInt(color.substring(0, 2), 16) / 255f;
+					float g = Integer.parseInt(color.substring(2, 4), 16) / 255f;
+					float b = Integer.parseInt(color.substring(4, 6), 16) / 255f;
+					float a = Integer.parseInt(color.substring(6, 8), 16) / 255f;
+					for (int frame = 0, bezier = 0;; frame++) {
+						timeline.setFrame(frame, time, r, g, b, a);
+						JsonValue nextMap = keyMap.next;
+						if (nextMap == null) {
+							timeline.shrink(bezier);
+							break;
+						}
+						float time2 = nextMap.getFloat("time", 0);
+						color = nextMap.getString("color");
+						float nr = Integer.parseInt(color.substring(0, 2), 16) / 255f;
+						float ng = Integer.parseInt(color.substring(2, 4), 16) / 255f;
+						float nb = Integer.parseInt(color.substring(4, 6), 16) / 255f;
+						float na = Integer.parseInt(color.substring(6, 8), 16) / 255f;
+						JsonValue curve = keyMap.get("curve");
+						if (curve != null) {
+							bezier = readCurve(curve, timeline, bezier, frame, 0, time, time2, r, nr, 1);
+							bezier = readCurve(curve, timeline, bezier, frame, 1, time, time2, g, ng, 1);
+							bezier = readCurve(curve, timeline, bezier, frame, 2, time, time2, b, nb, 1);
+							bezier = readCurve(curve, timeline, bezier, frame, 3, time, time2, a, na, 1);
+						}
+						time = time2;
+						r = nr;
+						g = ng;
+						b = nb;
+						a = na;
+						keyMap = nextMap;
+					}
+					timelines.add(timeline);
+
+				} else if (timelineName.equals("rgb")) {
+					RGBTimeline timeline = new RGBTimeline(timelineMap.size, timelineMap.size * 3, slot.index);
+					float time = keyMap.getFloat("time", 0);
+					String color = keyMap.getString("color");
+					float r = Integer.parseInt(color.substring(0, 2), 16) / 255f;
+					float g = Integer.parseInt(color.substring(2, 4), 16) / 255f;
+					float b = Integer.parseInt(color.substring(4, 6), 16) / 255f;
+					for (int frame = 0, bezier = 0;; frame++) {
+						timeline.setFrame(frame, time, r, g, b);
+						JsonValue nextMap = keyMap.next;
+						if (nextMap == null) {
+							timeline.shrink(bezier);
+							break;
+						}
+						float time2 = nextMap.getFloat("time", 0);
+						color = nextMap.getString("color");
+						float nr = Integer.parseInt(color.substring(0, 2), 16) / 255f;
+						float ng = Integer.parseInt(color.substring(2, 4), 16) / 255f;
+						float nb = Integer.parseInt(color.substring(4, 6), 16) / 255f;
+						JsonValue curve = keyMap.get("curve");
+						if (curve != null) {
+							bezier = readCurve(curve, timeline, bezier, frame, 0, time, time2, r, nr, 1);
+							bezier = readCurve(curve, timeline, bezier, frame, 1, time, time2, g, ng, 1);
+							bezier = readCurve(curve, timeline, bezier, frame, 2, time, time2, b, nb, 1);
+						}
+						time = time2;
+						r = nr;
+						g = ng;
+						b = nb;
+						keyMap = nextMap;
+					}
+					timelines.add(timeline);
+
+				} else if (timelineName.equals("alpha")) {
+					timelines.add(readTimeline(keyMap, new AlphaTimeline(timelineMap.size, timelineMap.size, slot.index), 0, 1));
+
+				} else if (timelineName.equals("rgba2")) {
+					RGBA2Timeline timeline = new RGBA2Timeline(timelineMap.size, timelineMap.size * 7, slot.index);
+					float time = keyMap.getFloat("time", 0);
+					String color = keyMap.getString("light");
+					float r = Integer.parseInt(color.substring(0, 2), 16) / 255f;
+					float g = Integer.parseInt(color.substring(2, 4), 16) / 255f;
+					float b = Integer.parseInt(color.substring(4, 6), 16) / 255f;
+					float a = Integer.parseInt(color.substring(6, 8), 16) / 255f;
+					color = keyMap.getString("dark");
+					float r2 = Integer.parseInt(color.substring(0, 2), 16) / 255f;
+					float g2 = Integer.parseInt(color.substring(2, 4), 16) / 255f;
+					float b2 = Integer.parseInt(color.substring(4, 6), 16) / 255f;
+					for (int frame = 0, bezier = 0;; frame++) {
+						timeline.setFrame(frame, time, r, g, b, a, r2, g2, b2);
+						JsonValue nextMap = keyMap.next;
+						if (nextMap == null) {
+							timeline.shrink(bezier);
+							break;
+						}
+						float time2 = nextMap.getFloat("time", 0);
+						color = nextMap.getString("light");
+						float nr = Integer.parseInt(color.substring(0, 2), 16) / 255f;
+						float ng = Integer.parseInt(color.substring(2, 4), 16) / 255f;
+						float nb = Integer.parseInt(color.substring(4, 6), 16) / 255f;
+						float na = Integer.parseInt(color.substring(6, 8), 16) / 255f;
+						color = nextMap.getString("dark");
+						float nr2 = Integer.parseInt(color.substring(0, 2), 16) / 255f;
+						float ng2 = Integer.parseInt(color.substring(2, 4), 16) / 255f;
+						float nb2 = Integer.parseInt(color.substring(4, 6), 16) / 255f;
+						JsonValue curve = keyMap.get("curve");
+						if (curve != null) {
+							bezier = readCurve(curve, timeline, bezier, frame, 0, time, time2, r, nr, 1);
+							bezier = readCurve(curve, timeline, bezier, frame, 1, time, time2, g, ng, 1);
+							bezier = readCurve(curve, timeline, bezier, frame, 2, time, time2, b, nb, 1);
+							bezier = readCurve(curve, timeline, bezier, frame, 3, time, time2, a, na, 1);
+							bezier = readCurve(curve, timeline, bezier, frame, 4, time, time2, r2, nr2, 1);
+							bezier = readCurve(curve, timeline, bezier, frame, 5, time, time2, g2, ng2, 1);
+							bezier = readCurve(curve, timeline, bezier, frame, 6, time, time2, b2, nb2, 1);
+						}
+						time = time2;
+						r = nr;
+						g = ng;
+						b = nb;
+						a = na;
+						r2 = nr2;
+						g2 = ng2;
+						b2 = nb2;
+						keyMap = nextMap;
+					}
+					timelines.add(timeline);
+
+				} else if (timelineName.equals("rgb2")) {
+					RGB2Timeline timeline = new RGB2Timeline(timelineMap.size, timelineMap.size * 6, slot.index);
+					float time = keyMap.getFloat("time", 0);
+					String color = keyMap.getString("light");
+					float r = Integer.parseInt(color.substring(0, 2), 16) / 255f;
+					float g = Integer.parseInt(color.substring(2, 4), 16) / 255f;
+					float b = Integer.parseInt(color.substring(4, 6), 16) / 255f;
+					color = keyMap.getString("dark");
+					float r2 = Integer.parseInt(color.substring(0, 2), 16) / 255f;
+					float g2 = Integer.parseInt(color.substring(2, 4), 16) / 255f;
+					float b2 = Integer.parseInt(color.substring(4, 6), 16) / 255f;
+					for (int frame = 0, bezier = 0;; frame++) {
+						timeline.setFrame(frame, time, r, g, b, r2, g2, b2);
+						JsonValue nextMap = keyMap.next;
+						if (nextMap == null) {
+							timeline.shrink(bezier);
+							break;
+						}
+						float time2 = nextMap.getFloat("time", 0);
+						color = nextMap.getString("light");
+						float nr = Integer.parseInt(color.substring(0, 2), 16) / 255f;
+						float ng = Integer.parseInt(color.substring(2, 4), 16) / 255f;
+						float nb = Integer.parseInt(color.substring(4, 6), 16) / 255f;
+						color = nextMap.getString("dark");
+						float nr2 = Integer.parseInt(color.substring(0, 2), 16) / 255f;
+						float ng2 = Integer.parseInt(color.substring(2, 4), 16) / 255f;
+						float nb2 = Integer.parseInt(color.substring(4, 6), 16) / 255f;
+						JsonValue curve = keyMap.get("curve");
+						if (curve != null) {
+							bezier = readCurve(curve, timeline, bezier, frame, 0, time, time2, r, nr, 1);
+							bezier = readCurve(curve, timeline, bezier, frame, 1, time, time2, g, ng, 1);
+							bezier = readCurve(curve, timeline, bezier, frame, 2, time, time2, b, nb, 1);
+							bezier = readCurve(curve, timeline, bezier, frame, 3, time, time2, r2, nr2, 1);
+							bezier = readCurve(curve, timeline, bezier, frame, 4, time, time2, g2, ng2, 1);
+							bezier = readCurve(curve, timeline, bezier, frame, 5, time, time2, b2, nb2, 1);
+						}
+						time = time2;
+						r = nr;
+						g = ng;
+						b = nb;
+						r2 = nr2;
+						g2 = ng2;
+						b2 = nb2;
+						keyMap = nextMap;
+					}
+					timelines.add(timeline);
+
+				} else
+					throw new RuntimeException("Invalid timeline type for a slot: " + timelineName + " (" + slotMap.name + ")");
+			}
+		}
+
+		// Bone timelines.
+		for (JsonValue boneMap = map.getChild("bones"); boneMap != null; boneMap = boneMap.next) {
+			BoneData bone = skeletonData.findBone(boneMap.name);
+			if (bone == null) throw new SerializationException("Bone not found: " + boneMap.name);
+			for (JsonValue timelineMap = boneMap.child; timelineMap != null; timelineMap = timelineMap.next) {
+				JsonValue keyMap = timelineMap.child;
+				if (keyMap == null) continue;
+
+				String timelineName = timelineMap.name;
+				if (timelineName.equals("rotate"))
+					timelines.add(readTimeline(keyMap, new RotateTimeline(timelineMap.size, timelineMap.size, bone.index), 0, 1));
+				else if (timelineName.equals("translate")) {
+					TranslateTimeline timeline = new TranslateTimeline(timelineMap.size, timelineMap.size << 1, bone.index);
+					timelines.add(readTimeline(keyMap, timeline, "x", "y", 0, scale));
+				} else if (timelineName.equals("translatex")) {
+					timelines
+						.add(readTimeline(keyMap, new TranslateXTimeline(timelineMap.size, timelineMap.size, bone.index), 0, scale));
+				} else if (timelineName.equals("translatey")) {
+					timelines
+						.add(readTimeline(keyMap, new TranslateYTimeline(timelineMap.size, timelineMap.size, bone.index), 0, scale));
+				} else if (timelineName.equals("scale")) {
+					ScaleTimeline timeline = new ScaleTimeline(timelineMap.size, timelineMap.size << 1, bone.index);
+					timelines.add(readTimeline(keyMap, timeline, "x", "y", 1, 1));
+				} else if (timelineName.equals("scalex"))
+					timelines.add(readTimeline(keyMap, new ScaleXTimeline(timelineMap.size, timelineMap.size, bone.index), 1, 1));
+				else if (timelineName.equals("scaley"))
+					timelines.add(readTimeline(keyMap, new ScaleYTimeline(timelineMap.size, timelineMap.size, bone.index), 1, 1));
+				else if (timelineName.equals("shear")) {
+					ShearTimeline timeline = new ShearTimeline(timelineMap.size, timelineMap.size << 1, bone.index);
+					timelines.add(readTimeline(keyMap, timeline, "x", "y", 0, 1));
+				} else if (timelineName.equals("shearx"))
+					timelines.add(readTimeline(keyMap, new ShearXTimeline(timelineMap.size, timelineMap.size, bone.index), 0, 1));
+				else if (timelineName.equals("sheary"))
+					timelines.add(readTimeline(keyMap, new ShearYTimeline(timelineMap.size, timelineMap.size, bone.index), 0, 1));
+				else
+					throw new RuntimeException("Invalid timeline type for a bone: " + timelineName + " (" + boneMap.name + ")");
+			}
+		}
+
+		// IK constraint timelines.
+		for (JsonValue timelineMap = map.getChild("ik"); timelineMap != null; timelineMap = timelineMap.next) {
+			JsonValue keyMap = timelineMap.child;
+			if (keyMap == null) continue;
+			IkConstraintData constraint = skeletonData.findIkConstraint(timelineMap.name);
+			IkConstraintTimeline timeline = new IkConstraintTimeline(timelineMap.size, timelineMap.size << 1,
+				skeletonData.getIkConstraints().indexOf(constraint, true));
+			float time = keyMap.getFloat("time", 0);
+			float mix = keyMap.getFloat("mix", 1), softness = keyMap.getFloat("softness", 0) * scale;
+			for (int frame = 0, bezier = 0;; frame++) {
+				timeline.setFrame(frame, time, mix, softness, keyMap.getBoolean("bendPositive", true) ? 1 : -1,
+					keyMap.getBoolean("compress", false), keyMap.getBoolean("stretch", false));
+				JsonValue nextMap = keyMap.next;
+				if (nextMap == null) {
+					timeline.shrink(bezier);
+					break;
+				}
+				float time2 = nextMap.getFloat("time", 0);
+				float mix2 = nextMap.getFloat("mix", 1), softness2 = nextMap.getFloat("softness", 0) * scale;
+				JsonValue curve = keyMap.get("curve");
+				if (curve != null) {
+					bezier = readCurve(curve, timeline, bezier, frame, 0, time, time2, mix, mix2, 1);
+					bezier = readCurve(curve, timeline, bezier, frame, 1, time, time2, softness, softness2, scale);
+				}
+				time = time2;
+				mix = mix2;
+				softness = softness2;
+				keyMap = nextMap;
+			}
+			timelines.add(timeline);
+		}
+
+		// Transform constraint timelines.
+		for (JsonValue timelineMap = map.getChild("transform"); timelineMap != null; timelineMap = timelineMap.next) {
+			JsonValue keyMap = timelineMap.child;
+			if (keyMap == null) continue;
+			TransformConstraintData constraint = skeletonData.findTransformConstraint(timelineMap.name);
+			TransformConstraintTimeline timeline = new TransformConstraintTimeline(timelineMap.size, timelineMap.size << 2,
+				skeletonData.getTransformConstraints().indexOf(constraint, true));
+			float time = keyMap.getFloat("time", 0);
+			float mixRotate = keyMap.getFloat("mixRotate", 1);
+			float mixX = keyMap.getFloat("mixX", 1), mixY = keyMap.getFloat("mixY", mixX);
+			float mixScaleX = keyMap.getFloat("mixScaleX", 1), mixScaleY = keyMap.getFloat("mixScaleY", mixScaleX);
+			float mixShearY = keyMap.getFloat("mixShearY", 1);
+			for (int frame = 0, bezier = 0;; frame++) {
+				timeline.setFrame(frame, time, mixRotate, mixX, mixY, mixScaleX, mixScaleY, mixShearY);
+				JsonValue nextMap = keyMap.next;
+				if (nextMap == null) {
+					timeline.shrink(bezier);
+					break;
+				}
+				float time2 = nextMap.getFloat("time", 0);
+				float mixRotate2 = nextMap.getFloat("mixRotate", 1);
+				float mixX2 = nextMap.getFloat("mixX", 1), mixY2 = nextMap.getFloat("mixY", mixX2);
+				float mixScaleX2 = nextMap.getFloat("mixScaleX", 1), mixScaleY2 = nextMap.getFloat("mixScaleY", mixScaleX2);
+				float mixShearY2 = nextMap.getFloat("mixShearY", 1);
+				JsonValue curve = keyMap.get("curve");
+				if (curve != null) {
+					bezier = readCurve(curve, timeline, bezier, frame, 0, time, time2, mixRotate, mixRotate2, 1);
+					bezier = readCurve(curve, timeline, bezier, frame, 1, time, time2, mixX, mixX2, 1);
+					bezier = readCurve(curve, timeline, bezier, frame, 2, time, time2, mixY, mixY2, 1);
+					bezier = readCurve(curve, timeline, bezier, frame, 3, time, time2, mixScaleX, mixScaleX2, 1);
+					bezier = readCurve(curve, timeline, bezier, frame, 4, time, time2, mixScaleY, mixScaleY2, 1);
+					bezier = readCurve(curve, timeline, bezier, frame, 5, time, time2, mixShearY, mixShearY2, 1);
+				}
+				time = time2;
+				mixRotate = mixRotate2;
+				mixX = mixX2;
+				mixY = mixY2;
+				mixScaleX = mixScaleX2;
+				mixScaleY = mixScaleY2;
+				mixScaleX = mixScaleX2;
+				keyMap = nextMap;
+			}
+			timelines.add(timeline);
+		}
+
+		// Path constraint timelines.
+		for (JsonValue constraintMap = map.getChild("path"); constraintMap != null; constraintMap = constraintMap.next) {
+			PathConstraintData constraint = skeletonData.findPathConstraint(constraintMap.name);
+			if (constraint == null) throw new SerializationException("Path constraint not found: " + constraintMap.name);
+			int index = skeletonData.pathConstraints.indexOf(constraint, true);
+			for (JsonValue timelineMap = constraintMap.child; timelineMap != null; timelineMap = timelineMap.next) {
+				JsonValue keyMap = timelineMap.child;
+				if (keyMap == null) continue;
+				String timelineName = timelineMap.name;
+				if (timelineName.equals("position")) {
+					CurveTimeline1 timeline = new PathConstraintPositionTimeline(timelineMap.size, timelineMap.size, index);
+					timelines.add(readTimeline(keyMap, timeline, 0, constraint.positionMode == PositionMode.fixed ? scale : 1));
+				} else if (timelineName.equals("spacing")) {
+					CurveTimeline1 timeline = new PathConstraintSpacingTimeline(timelineMap.size, timelineMap.size, index);
+					timelines.add(readTimeline(keyMap, timeline, 0,
+						constraint.spacingMode == SpacingMode.length || constraint.spacingMode == SpacingMode.fixed ? scale : 1));
+				} else if (timelineName.equals("mix")) {
+					PathConstraintMixTimeline timeline = new PathConstraintMixTimeline(timelineMap.size, timelineMap.size * 3, index);
+					float time = keyMap.getFloat("time", 0);
+					float mixRotate = keyMap.getFloat("mixRotate", 1);
+					float mixX = keyMap.getFloat("mixX", 1), mixY = keyMap.getFloat("mixY", mixX);
+					for (int frame = 0, bezier = 0;; frame++) {
+						timeline.setFrame(frame, time, mixRotate, mixX, mixY);
+						JsonValue nextMap = keyMap.next;
+						if (nextMap == null) {
+							timeline.shrink(bezier);
+							break;
+						}
+						float time2 = nextMap.getFloat("time", 0);
+						float mixRotate2 = nextMap.getFloat("mixRotate", 1);
+						float mixX2 = nextMap.getFloat("mixX", 1), mixY2 = nextMap.getFloat("mixY", mixX2);
+						JsonValue curve = keyMap.get("curve");
+						if (curve != null) {
+							bezier = readCurve(curve, timeline, bezier, frame, 0, time, time2, mixRotate, mixRotate2, 1);
+							bezier = readCurve(curve, timeline, bezier, frame, 1, time, time2, mixX, mixX2, 1);
+							bezier = readCurve(curve, timeline, bezier, frame, 2, time, time2, mixY, mixY2, 1);
+						}
+						time = time2;
+						mixRotate = mixRotate2;
+						mixX = mixX2;
+						mixY = mixY2;
+						keyMap = nextMap;
+					}
+					timelines.add(timeline);
+				}
+			}
+		}
+
+		// Deform timelines.
+		for (JsonValue deformMap = map.getChild("deform"); deformMap != null; deformMap = deformMap.next) {
+			Skin skin = skeletonData.findSkin(deformMap.name);
+			if (skin == null) throw new SerializationException("Skin not found: " + deformMap.name);
+			for (JsonValue slotMap = deformMap.child; slotMap != null; slotMap = slotMap.next) {
+				SlotData slot = skeletonData.findSlot(slotMap.name);
+				if (slot == null) throw new SerializationException("Slot not found: " + slotMap.name);
+				for (JsonValue timelineMap = slotMap.child; timelineMap != null; timelineMap = timelineMap.next) {
+					JsonValue keyMap = timelineMap.child;
+					if (keyMap == null) continue;
+
+					VertexAttachment attachment = (VertexAttachment)skin.getAttachment(slot.index, timelineMap.name);
+					if (attachment == null) throw new SerializationException("Deform attachment not found: " + timelineMap.name);
+					boolean weighted = attachment.getBones() != null;
+					float[] vertices = attachment.getVertices();
+					int deformLength = weighted ? (vertices.length / 3) << 1 : vertices.length;
+
+					DeformTimeline timeline = new DeformTimeline(timelineMap.size, timelineMap.size, slot.index, attachment);
+					float time = keyMap.getFloat("time", 0);
+					for (int frame = 0, bezier = 0;; frame++) {
+						float[] deform;
+						JsonValue verticesValue = keyMap.get("vertices");
+						if (verticesValue == null)
+							deform = weighted ? new float[deformLength] : vertices;
+						else {
+							deform = new float[deformLength];
+							int start = keyMap.getInt("offset", 0);
+							arraycopy(verticesValue.asFloatArray(), 0, deform, start, verticesValue.size);
+							if (scale != 1) {
+								for (int i = start, n = i + verticesValue.size; i < n; i++)
+									deform[i] *= scale;
+							}
+							if (!weighted) {
+								for (int i = 0; i < deformLength; i++)
+									deform[i] += vertices[i];
+							}
+						}
+
+						timeline.setFrame(frame, time, deform);
+						JsonValue nextMap = keyMap.next;
+						if (nextMap == null) {
+							timeline.shrink(bezier);
+							break;
+						}
+						float time2 = nextMap.getFloat("time", 0);
+						JsonValue curve = keyMap.get("curve");
+						if (curve != null) bezier = readCurve(curve, timeline, bezier, frame, 0, time, time2, 0, 1, 1);
+						time = time2;
+						keyMap = nextMap;
+					}
+					timelines.add(timeline);
+				}
+			}
+		}
+
+		// Draw order timeline.
+		JsonValue drawOrdersMap = map.get("drawOrder");
+		if (drawOrdersMap != null) {
+			DrawOrderTimeline timeline = new DrawOrderTimeline(drawOrdersMap.size);
+			int slotCount = skeletonData.slots.size;
+			int frame = 0;
+			for (JsonValue drawOrderMap = drawOrdersMap.child; drawOrderMap != null; drawOrderMap = drawOrderMap.next, frame++) {
+				int[] drawOrder = null;
+				JsonValue offsets = drawOrderMap.get("offsets");
+				if (offsets != null) {
+					drawOrder = new int[slotCount];
+					for (int i = slotCount - 1; i >= 0; i--)
+						drawOrder[i] = -1;
+					int[] unchanged = new int[slotCount - offsets.size];
+					int originalIndex = 0, unchangedIndex = 0;
+					for (JsonValue offsetMap = offsets.child; offsetMap != null; offsetMap = offsetMap.next) {
+						SlotData slot = skeletonData.findSlot(offsetMap.getString("slot"));
+						if (slot == null) throw new SerializationException("Slot not found: " + offsetMap.getString("slot"));
+						// Collect unchanged items.
+						while (originalIndex != slot.index)
+							unchanged[unchangedIndex++] = originalIndex++;
+						// Set changed items.
+						drawOrder[originalIndex + offsetMap.getInt("offset")] = originalIndex++;
+					}
+					// Collect remaining unchanged items.
+					while (originalIndex < slotCount)
+						unchanged[unchangedIndex++] = originalIndex++;
+					// Fill in unchanged items.
+					for (int i = slotCount - 1; i >= 0; i--)
+						if (drawOrder[i] == -1) drawOrder[i] = unchanged[--unchangedIndex];
+				}
+				timeline.setFrame(frame, drawOrderMap.getFloat("time", 0), drawOrder);
+			}
+			timelines.add(timeline);
+		}
+
+		// Event timeline.
+		JsonValue eventsMap = map.get("events");
+		if (eventsMap != null) {
+			EventTimeline timeline = new EventTimeline(eventsMap.size);
+			int frame = 0;
+			for (JsonValue eventMap = eventsMap.child; eventMap != null; eventMap = eventMap.next, frame++) {
+				EventData eventData = skeletonData.findEvent(eventMap.getString("name"));
+				if (eventData == null) throw new SerializationException("Event not found: " + eventMap.getString("name"));
+				Event event = new Event(eventMap.getFloat("time", 0), eventData);
+				event.intValue = eventMap.getInt("int", eventData.intValue);
+				event.floatValue = eventMap.getFloat("float", eventData.floatValue);
+				event.stringValue = eventMap.getString("string", eventData.stringValue);
+				if (event.getData().audioPath != null) {
+					event.volume = eventMap.getFloat("volume", eventData.volume);
+					event.balance = eventMap.getFloat("balance", eventData.balance);
+				}
+				timeline.setFrame(frame, event);
+			}
+			timelines.add(timeline);
+		}
+
+		timelines.shrink();
+		float duration = 0;
+		Object[] items = timelines.items;
+		for (int i = 0, n = timelines.size; i < n; i++)
+			duration = Math.max(duration, ((Timeline)items[i]).getDuration());
+		skeletonData.animations.add(new Animation(name, timelines, duration));
+	}
+
+	private Timeline readTimeline (JsonValue keyMap, CurveTimeline1 timeline, float defaultValue, float scale) {
+		float time = keyMap.getFloat("time", 0), value = keyMap.getFloat("value", defaultValue) * scale;
+		for (int frame = 0, bezier = 0;; frame++) {
+			timeline.setFrame(frame, time, value);
+			JsonValue nextMap = keyMap.next;
+			if (nextMap == null) {
+				timeline.shrink(bezier);
+				return timeline;
+			}
+			float time2 = nextMap.getFloat("time", 0);
+			float value2 = nextMap.getFloat("value", defaultValue) * scale;
+			JsonValue curve = keyMap.get("curve");
+			if (curve != null) bezier = readCurve(curve, timeline, bezier, frame, 0, time, time2, value, value2, scale);
+			time = time2;
+			value = value2;
+			keyMap = nextMap;
+		}
+	}
+
+	private Timeline readTimeline (JsonValue keyMap, CurveTimeline2 timeline, String name1, String name2, float defaultValue,
+		float scale) {
+		float time = keyMap.getFloat("time", 0);
+		float value1 = keyMap.getFloat(name1, defaultValue) * scale, value2 = keyMap.getFloat(name2, defaultValue) * scale;
+		for (int frame = 0, bezier = 0;; frame++) {
+			timeline.setFrame(frame, time, value1, value2);
+			JsonValue nextMap = keyMap.next;
+			if (nextMap == null) {
+				timeline.shrink(bezier);
+				return timeline;
+			}
+			float time2 = nextMap.getFloat("time", 0);
+			float nvalue1 = nextMap.getFloat(name1, defaultValue) * scale, nvalue2 = nextMap.getFloat(name2, defaultValue) * scale;
+			JsonValue curve = keyMap.get("curve");
+			if (curve != null) {
+				bezier = readCurve(curve, timeline, bezier, frame, 0, time, time2, value1, nvalue1, scale);
+				bezier = readCurve(curve, timeline, bezier, frame, 1, time, time2, value2, nvalue2, scale);
+			}
+			time = time2;
+			value1 = nvalue1;
+			value2 = nvalue2;
+			keyMap = nextMap;
+		}
+	}
+
+	int readCurve (JsonValue curve, CurveTimeline timeline, int bezier, int frame, int value, float time1, float time2,
+		float value1, float value2, float scale) {
+		if (curve.isString()) {
+			if (curve.asString().equals("stepped")) timeline.setStepped(frame);
+			return bezier;
+		}
+		curve = curve.get(value << 2);
+		float cx1 = curve.asFloat();
+		curve = curve.next;
+		float cy1 = curve.asFloat() * scale;
+		curve = curve.next;
+		float cx2 = curve.asFloat();
+		curve = curve.next;
+		float cy2 = curve.asFloat() * scale;
+		setBezier(timeline, frame, value, bezier, time1, value1, cx1, cy1, cx2, cy2, time2, value2);
+		return bezier + 1;
+	}
+
+	static void setBezier (CurveTimeline timeline, int frame, int value, int bezier, float time1, float value1, float cx1,
+		float cy1, float cx2, float cy2, float time2, float value2) {
+		timeline.setBezier(bezier, frame, value, time1, value1, cx1, cy1, cx2, cy2, time2, value2);
+	}
+
+	static class LinkedMesh {
+		String parent, skin;
+		int slotIndex;
+		MeshAttachment mesh;
+		boolean inheritDeform;
+
+		public LinkedMesh (MeshAttachment mesh, String skin, int slotIndex, String parent, boolean inheritDeform) {
+			this.mesh = mesh;
+			this.skin = skin;
+			this.slotIndex = slotIndex;
+			this.parent = parent;
+			this.inheritDeform = inheritDeform;
+		}
+	}
+}

+ 494 - 494
spine-libgdx/spine-libgdx/src/com/esotericsoftware/spine/SkeletonRenderer.java

@@ -1,494 +1,494 @@
-/******************************************************************************
- * Spine Runtimes License Agreement
- * Last updated January 1, 2020. Replaces all prior versions.
- *
- * Copyright (c) 2013-2020, Esoteric Software LLC
- *
- * Integration of the Spine Runtimes into software or otherwise creating
- * derivative works of the Spine Runtimes is permitted under the terms and
- * conditions of Section 2 of the Spine Editor License Agreement:
- * http://esotericsoftware.com/spine-editor-license
- *
- * Otherwise, it is permitted to integrate the Spine Runtimes into software
- * or otherwise create derivative works of the Spine Runtimes (collectively,
- * "Products"), provided that each user of the Products must obtain their own
- * Spine Editor license and redistribution of the Products in any form must
- * include this license and copyright notice.
- *
- * THE SPINE RUNTIMES ARE PROVIDED BY ESOTERIC SOFTWARE LLC "AS IS" AND ANY
- * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
- * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
- * DISCLAIMED. IN NO EVENT SHALL ESOTERIC SOFTWARE LLC BE LIABLE FOR ANY
- * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
- * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES,
- * BUSINESS INTERRUPTION, OR LOSS OF USE, DATA, OR PROFITS) HOWEVER CAUSED AND
- * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
- * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
- * THE SPINE RUNTIMES, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
- *****************************************************************************/
-
-package com.esotericsoftware.spine;
-
-import com.badlogic.gdx.graphics.Color;
-import com.badlogic.gdx.graphics.Texture;
-import com.badlogic.gdx.graphics.g2d.Batch;
-import com.badlogic.gdx.graphics.g2d.PolygonSpriteBatch;
-import com.badlogic.gdx.math.Vector2;
-import com.badlogic.gdx.utils.FloatArray;
-import com.badlogic.gdx.utils.Null;
-import com.badlogic.gdx.utils.NumberUtils;
-import com.badlogic.gdx.utils.ShortArray;
-
-import com.esotericsoftware.spine.attachments.Attachment;
-import com.esotericsoftware.spine.attachments.ClippingAttachment;
-import com.esotericsoftware.spine.attachments.MeshAttachment;
-import com.esotericsoftware.spine.attachments.RegionAttachment;
-import com.esotericsoftware.spine.attachments.SkeletonAttachment;
-import com.esotericsoftware.spine.utils.SkeletonClipping;
-import com.esotericsoftware.spine.utils.TwoColorPolygonBatch;
-
-public class SkeletonRenderer {
-	static private final short[] quadTriangles = {0, 1, 2, 2, 3, 0};
-
-	private boolean pmaColors, pmaBlendModes;
-	private final FloatArray vertices = new FloatArray(32);
-	private final SkeletonClipping clipper = new SkeletonClipping();
-	private @Null VertexEffect vertexEffect;
-	private final Vector2 temp = new Vector2();
-	private final Vector2 temp2 = new Vector2();
-	private final Color temp3 = new Color();
-	private final Color temp4 = new Color();
-	private final Color temp5 = new Color();
-	private final Color temp6 = new Color();
-
-	/** Renders the specified skeleton. If the batch is a PolygonSpriteBatch, {@link #draw(PolygonSpriteBatch, Skeleton)} is
-	 * called. If the batch is a TwoColorPolygonBatch, {@link #draw(TwoColorPolygonBatch, Skeleton)} is called. Otherwise the
-	 * skeleton is rendered without two color tinting and any mesh attachments will throw an exception.
-	 * <p>
-	 * This method may change the batch's {@link Batch#setBlendFunctionSeparate(int, int, int, int) blending function}. The
-	 * previous blend function is not restored, since that could result in unnecessary flushes, depending on what is rendered
-	 * next. */
-	public void draw (Batch batch, Skeleton skeleton) {
-		if (batch instanceof TwoColorPolygonBatch) {
-			draw((TwoColorPolygonBatch)batch, skeleton);
-			return;
-		}
-		if (batch instanceof PolygonSpriteBatch) {
-			draw((PolygonSpriteBatch)batch, skeleton);
-			return;
-		}
-		if (batch == null) throw new IllegalArgumentException("batch cannot be null.");
-		if (skeleton == null) throw new IllegalArgumentException("skeleton cannot be null.");
-
-		VertexEffect vertexEffect = this.vertexEffect;
-		if (vertexEffect != null) vertexEffect.begin(skeleton);
-
-		boolean pmaColors = this.pmaColors, pmaBlendModes = this.pmaBlendModes;
-		BlendMode blendMode = null;
-		float[] vertices = this.vertices.items;
-		Color skeletonColor = skeleton.color;
-		float r = skeletonColor.r, g = skeletonColor.g, b = skeletonColor.b, a = skeletonColor.a;
-		Object[] drawOrder = skeleton.drawOrder.items;
-		for (int i = 0, n = skeleton.drawOrder.size; i < n; i++) {
-			Slot slot = (Slot)drawOrder[i];
-			if (!slot.bone.active) {
-				clipper.clipEnd(slot);
-				continue;
-			}
-			Attachment attachment = slot.attachment;
-			if (attachment instanceof RegionAttachment) {
-				RegionAttachment region = (RegionAttachment)attachment;
-				region.computeWorldVertices(slot.getBone(), vertices, 0, 5);
-				Color color = region.getColor(), slotColor = slot.getColor();
-				float alpha = a * slotColor.a * color.a * 255;
-				float multiplier = pmaColors ? alpha : 255;
-
-				BlendMode slotBlendMode = slot.data.getBlendMode();
-				if (slotBlendMode != blendMode) {
-					if (slotBlendMode == BlendMode.additive && pmaColors) {
-						slotBlendMode = BlendMode.normal;
-						alpha = 0;
-					}
-					blendMode = slotBlendMode;
-					blendMode.apply(batch, pmaBlendModes);
-				}
-
-				float c = NumberUtils.intToFloatColor((int)alpha << 24 //
-					| (int)(b * slotColor.b * color.b * multiplier) << 16 //
-					| (int)(g * slotColor.g * color.g * multiplier) << 8 //
-					| (int)(r * slotColor.r * color.r * multiplier));
-				float[] uvs = region.getUVs();
-				for (int u = 0, v = 2; u < 8; u += 2, v += 5) {
-					vertices[v] = c;
-					vertices[v + 1] = uvs[u];
-					vertices[v + 2] = uvs[u + 1];
-				}
-
-				if (vertexEffect != null) applyVertexEffect(vertices, 20, 5, c, 0);
-
-				batch.draw(region.getRegion().getTexture(), vertices, 0, 20);
-
-			} else if (attachment instanceof ClippingAttachment) {
-				clipper.clipStart(slot, (ClippingAttachment)attachment);
-				continue;
-
-			} else if (attachment instanceof MeshAttachment) {
-				throw new RuntimeException(batch.getClass().getSimpleName()
-					+ " cannot render meshes, PolygonSpriteBatch or TwoColorPolygonBatch is required.");
-
-			} else if (attachment instanceof SkeletonAttachment) {
-				Skeleton attachmentSkeleton = ((SkeletonAttachment)attachment).getSkeleton();
-				if (attachmentSkeleton != null) draw(batch, attachmentSkeleton);
-			}
-
-			clipper.clipEnd(slot);
-		}
-		clipper.clipEnd();
-		if (vertexEffect != null) vertexEffect.end();
-	}
-
-	/** Renders the specified skeleton, including meshes, but without two color tinting.
-	 * <p>
-	 * This method may change the batch's {@link Batch#setBlendFunctionSeparate(int, int, int, int) blending function}. The
-	 * previous blend function is not restored, since that could result in unnecessary flushes, depending on what is rendered
-	 * next. */
-	public void draw (PolygonSpriteBatch batch, Skeleton skeleton) {
-		if (batch == null) throw new IllegalArgumentException("batch cannot be null.");
-		if (skeleton == null) throw new IllegalArgumentException("skeleton cannot be null.");
-
-		Vector2 tempPosition = this.temp, tempUV = this.temp2;
-		Color tempLight1 = this.temp3, tempDark1 = this.temp4;
-		Color tempLight2 = this.temp5, tempDark2 = this.temp6;
-		VertexEffect vertexEffect = this.vertexEffect;
-		if (vertexEffect != null) vertexEffect.begin(skeleton);
-
-		boolean pmaColors = this.pmaColors, pmaBlendModes = this.pmaBlendModes;
-		BlendMode blendMode = null;
-		int verticesLength = 0;
-		float[] vertices = null, uvs = null;
-		short[] triangles = null;
-		Color color = null, skeletonColor = skeleton.color;
-		float r = skeletonColor.r, g = skeletonColor.g, b = skeletonColor.b, a = skeletonColor.a;
-		Object[] drawOrder = skeleton.drawOrder.items;
-		for (int i = 0, n = skeleton.drawOrder.size; i < n; i++) {
-			Slot slot = (Slot)drawOrder[i];
-			if (!slot.bone.active) {
-				clipper.clipEnd(slot);
-				continue;
-			}
-			Texture texture = null;
-			int vertexSize = clipper.isClipping() ? 2 : 5;
-			Attachment attachment = slot.attachment;
-			if (attachment instanceof RegionAttachment) {
-				RegionAttachment region = (RegionAttachment)attachment;
-				verticesLength = vertexSize << 2;
-				vertices = this.vertices.items;
-				region.computeWorldVertices(slot.getBone(), vertices, 0, vertexSize);
-				triangles = quadTriangles;
-				texture = region.getRegion().getTexture();
-				uvs = region.getUVs();
-				color = region.getColor();
-
-			} else if (attachment instanceof MeshAttachment) {
-				MeshAttachment mesh = (MeshAttachment)attachment;
-				int count = mesh.getWorldVerticesLength();
-				verticesLength = (count >> 1) * vertexSize;
-				vertices = this.vertices.setSize(verticesLength);
-				mesh.computeWorldVertices(slot, 0, count, vertices, 0, vertexSize);
-				triangles = mesh.getTriangles();
-				texture = mesh.getRegion().getTexture();
-				uvs = mesh.getUVs();
-				color = mesh.getColor();
-
-			} else if (attachment instanceof ClippingAttachment) {
-				ClippingAttachment clip = (ClippingAttachment)attachment;
-				clipper.clipStart(slot, clip);
-				continue;
-
-			} else if (attachment instanceof SkeletonAttachment) {
-				Skeleton attachmentSkeleton = ((SkeletonAttachment)attachment).getSkeleton();
-				if (attachmentSkeleton != null) draw(batch, attachmentSkeleton);
-			}
-
-			if (texture != null) {
-				Color slotColor = slot.getColor();
-				float alpha = a * slotColor.a * color.a * 255;
-				float multiplier = pmaColors ? alpha : 255;
-
-				BlendMode slotBlendMode = slot.data.getBlendMode();
-				if (slotBlendMode != blendMode) {
-					if (slotBlendMode == BlendMode.additive && pmaColors) {
-						slotBlendMode = BlendMode.normal;
-						alpha = 0;
-					}
-					blendMode = slotBlendMode;
-					blendMode.apply(batch, pmaBlendModes);
-				}
-
-				float c = NumberUtils.intToFloatColor((int)alpha << 24 //
-					| (int)(b * slotColor.b * color.b * multiplier) << 16 //
-					| (int)(g * slotColor.g * color.g * multiplier) << 8 //
-					| (int)(r * slotColor.r * color.r * multiplier));
-
-				if (clipper.isClipping()) {
-					clipper.clipTriangles(vertices, verticesLength, triangles, triangles.length, uvs, c, 0, false);
-					FloatArray clippedVertices = clipper.getClippedVertices();
-					ShortArray clippedTriangles = clipper.getClippedTriangles();
-					if (vertexEffect != null) applyVertexEffect(clippedVertices.items, clippedVertices.size, 5, c, 0);
-					batch.draw(texture, clippedVertices.items, 0, clippedVertices.size, clippedTriangles.items, 0,
-						clippedTriangles.size);
-				} else {
-					if (vertexEffect != null) {
-						tempLight1.set(NumberUtils.floatToIntColor(c));
-						tempDark1.set(0);
-						for (int v = 0, u = 0; v < verticesLength; v += 5, u += 2) {
-							tempPosition.x = vertices[v];
-							tempPosition.y = vertices[v + 1];
-							tempLight2.set(tempLight1);
-							tempDark2.set(tempDark1);
-							tempUV.x = uvs[u];
-							tempUV.y = uvs[u + 1];
-							vertexEffect.transform(tempPosition, tempUV, tempLight2, tempDark2);
-							vertices[v] = tempPosition.x;
-							vertices[v + 1] = tempPosition.y;
-							vertices[v + 2] = tempLight2.toFloatBits();
-							vertices[v + 3] = tempUV.x;
-							vertices[v + 4] = tempUV.y;
-						}
-					} else {
-						for (int v = 2, u = 0; v < verticesLength; v += 5, u += 2) {
-							vertices[v] = c;
-							vertices[v + 1] = uvs[u];
-							vertices[v + 2] = uvs[u + 1];
-						}
-					}
-					batch.draw(texture, vertices, 0, verticesLength, triangles, 0, triangles.length);
-				}
-			}
-
-			clipper.clipEnd(slot);
-		}
-		clipper.clipEnd();
-		if (vertexEffect != null) vertexEffect.end();
-	}
-
-	/** Renders the specified skeleton, including meshes and two color tinting.
-	 * <p>
-	 * This method may change the batch's {@link Batch#setBlendFunctionSeparate(int, int, int, int) blending function}. The
-	 * previous blend function is not restored, since that could result in unnecessary flushes, depending on what is rendered
-	 * next. */
-	public void draw (TwoColorPolygonBatch batch, Skeleton skeleton) {
-		if (batch == null) throw new IllegalArgumentException("batch cannot be null.");
-		if (skeleton == null) throw new IllegalArgumentException("skeleton cannot be null.");
-
-		Vector2 tempPosition = this.temp, tempUV = this.temp2;
-		Color tempLight1 = this.temp3, tempDark1 = this.temp4;
-		Color tempLight2 = this.temp5, tempDark2 = this.temp6;
-		VertexEffect vertexEffect = this.vertexEffect;
-		if (vertexEffect != null) vertexEffect.begin(skeleton);
-
-		boolean pmaColors = this.pmaColors, pmaBlendModes = this.pmaBlendModes;
-		batch.setPremultipliedAlpha(pmaColors);
-		BlendMode blendMode = null;
-		int verticesLength = 0;
-		float[] vertices = null, uvs = null;
-		short[] triangles = null;
-		Color color = null, skeletonColor = skeleton.color;
-		float r = skeletonColor.r, g = skeletonColor.g, b = skeletonColor.b, a = skeletonColor.a;
-		Object[] drawOrder = skeleton.drawOrder.items;
-		for (int i = 0, n = skeleton.drawOrder.size; i < n; i++) {
-			Slot slot = (Slot)drawOrder[i];
-			if (!slot.bone.active) {
-				clipper.clipEnd(slot);
-				continue;
-			}
-			Texture texture = null;
-			int vertexSize = clipper.isClipping() ? 2 : 6;
-			Attachment attachment = slot.attachment;
-			if (attachment instanceof RegionAttachment) {
-				RegionAttachment region = (RegionAttachment)attachment;
-				verticesLength = vertexSize << 2;
-				vertices = this.vertices.items;
-				region.computeWorldVertices(slot.getBone(), vertices, 0, vertexSize);
-				triangles = quadTriangles;
-				texture = region.getRegion().getTexture();
-				uvs = region.getUVs();
-				color = region.getColor();
-
-			} else if (attachment instanceof MeshAttachment) {
-				MeshAttachment mesh = (MeshAttachment)attachment;
-				int count = mesh.getWorldVerticesLength();
-				verticesLength = (count >> 1) * vertexSize;
-				vertices = this.vertices.setSize(verticesLength);
-				mesh.computeWorldVertices(slot, 0, count, vertices, 0, vertexSize);
-				triangles = mesh.getTriangles();
-				texture = mesh.getRegion().getTexture();
-				uvs = mesh.getUVs();
-				color = mesh.getColor();
-
-			} else if (attachment instanceof ClippingAttachment) {
-				ClippingAttachment clip = (ClippingAttachment)attachment;
-				clipper.clipStart(slot, clip);
-				continue;
-
-			} else if (attachment instanceof SkeletonAttachment) {
-				Skeleton attachmentSkeleton = ((SkeletonAttachment)attachment).getSkeleton();
-				if (attachmentSkeleton != null) draw(batch, attachmentSkeleton);
-			}
-
-			if (texture != null) {
-				Color lightColor = slot.getColor();
-				float alpha = a * lightColor.a * color.a * 255;
-				float multiplier = pmaColors ? alpha : 255;
-
-				BlendMode slotBlendMode = slot.data.getBlendMode();
-				if (slotBlendMode != blendMode) {
-					if (slotBlendMode == BlendMode.additive && pmaColors) {
-						slotBlendMode = BlendMode.normal;
-						alpha = 0;
-					}
-					blendMode = slotBlendMode;
-					blendMode.apply(batch, pmaBlendModes);
-				}
-
-				float red = r * color.r * multiplier;
-				float green = g * color.g * multiplier;
-				float blue = b * color.b * multiplier;
-				float light = NumberUtils.intToFloatColor((int)alpha << 24 //
-					| (int)(blue * lightColor.b) << 16 //
-					| (int)(green * lightColor.g) << 8 //
-					| (int)(red * lightColor.r));
-				Color darkColor = slot.getDarkColor();
-				float dark = darkColor == null ? 0
-					: NumberUtils.intToFloatColor((int)(blue * darkColor.b) << 16 //
-						| (int)(green * darkColor.g) << 8 //
-						| (int)(red * darkColor.r));
-
-				if (clipper.isClipping()) {
-					clipper.clipTriangles(vertices, verticesLength, triangles, triangles.length, uvs, light, dark, true);
-					FloatArray clippedVertices = clipper.getClippedVertices();
-					ShortArray clippedTriangles = clipper.getClippedTriangles();
-					if (vertexEffect != null) applyVertexEffect(clippedVertices.items, clippedVertices.size, 6, light, dark);
-					batch.drawTwoColor(texture, clippedVertices.items, 0, clippedVertices.size, clippedTriangles.items, 0,
-						clippedTriangles.size);
-				} else {
-					if (vertexEffect != null) {
-						tempLight1.set(NumberUtils.floatToIntColor(light));
-						tempDark1.set(NumberUtils.floatToIntColor(dark));
-						for (int v = 0, u = 0; v < verticesLength; v += 6, u += 2) {
-							tempPosition.x = vertices[v];
-							tempPosition.y = vertices[v + 1];
-							tempLight2.set(tempLight1);
-							tempDark2.set(tempDark1);
-							tempUV.x = uvs[u];
-							tempUV.y = uvs[u + 1];
-							vertexEffect.transform(tempPosition, tempUV, tempLight2, tempDark2);
-							vertices[v] = tempPosition.x;
-							vertices[v + 1] = tempPosition.y;
-							vertices[v + 2] = tempLight2.toFloatBits();
-							vertices[v + 3] = tempDark2.toFloatBits();
-							vertices[v + 4] = tempUV.x;
-							vertices[v + 5] = tempUV.y;
-						}
-					} else {
-						for (int v = 2, u = 0; v < verticesLength; v += 6, u += 2) {
-							vertices[v] = light;
-							vertices[v + 1] = dark;
-							vertices[v + 2] = uvs[u];
-							vertices[v + 3] = uvs[u + 1];
-						}
-					}
-					batch.drawTwoColor(texture, vertices, 0, verticesLength, triangles, 0, triangles.length);
-				}
-			}
-
-			clipper.clipEnd(slot);
-		}
-		clipper.clipEnd();
-		if (vertexEffect != null) vertexEffect.end();
-	}
-
-	private void applyVertexEffect (float[] vertices, int verticesLength, int stride, float light, float dark) {
-		Vector2 tempPosition = this.temp, tempUV = this.temp2;
-		Color tempLight1 = this.temp3, tempDark1 = this.temp4;
-		Color tempLight2 = this.temp5, tempDark2 = this.temp6;
-		VertexEffect vertexEffect = this.vertexEffect;
-		tempLight1.set(NumberUtils.floatToIntColor(light));
-		tempDark1.set(NumberUtils.floatToIntColor(dark));
-		if (stride == 5) {
-			for (int v = 0; v < verticesLength; v += stride) {
-				tempPosition.x = vertices[v];
-				tempPosition.y = vertices[v + 1];
-				tempUV.x = vertices[v + 3];
-				tempUV.y = vertices[v + 4];
-				tempLight2.set(tempLight1);
-				tempDark2.set(tempDark1);
-				vertexEffect.transform(tempPosition, tempUV, tempLight2, tempDark2);
-				vertices[v] = tempPosition.x;
-				vertices[v + 1] = tempPosition.y;
-				vertices[v + 2] = tempLight2.toFloatBits();
-				vertices[v + 3] = tempUV.x;
-				vertices[v + 4] = tempUV.y;
-			}
-		} else {
-			for (int v = 0; v < verticesLength; v += stride) {
-				tempPosition.x = vertices[v];
-				tempPosition.y = vertices[v + 1];
-				tempUV.x = vertices[v + 4];
-				tempUV.y = vertices[v + 5];
-				tempLight2.set(tempLight1);
-				tempDark2.set(tempDark1);
-				vertexEffect.transform(tempPosition, tempUV, tempLight2, tempDark2);
-				vertices[v] = tempPosition.x;
-				vertices[v + 1] = tempPosition.y;
-				vertices[v + 2] = tempLight2.toFloatBits();
-				vertices[v + 3] = tempDark2.toFloatBits();
-				vertices[v + 4] = tempUV.x;
-				vertices[v + 5] = tempUV.y;
-			}
-		}
-	}
-
-	public boolean getPremultipliedAlphaColors () {
-		return pmaColors;
-	}
-
-	/** If true, colors will be multiplied by their alpha before being sent to the GPU. Set to false if premultiplied alpha is not
-	 * being used or if the shader does the multiplication (libgdx's default batch shaders do not). Default is false. */
-	public void setPremultipliedAlphaColors (boolean pmaColors) {
-		this.pmaColors = pmaColors;
-	}
-
-	public boolean getPremultipliedAlphaBlendModes () {
-		return pmaBlendModes;
-	}
-
-	/** If true, blend modes for premultiplied alpha will be used. Set to false if premultiplied alpha is not being used. Default
-	 * is false. */
-	public void setPremultipliedAlphaBlendModes (boolean pmaBlendModes) {
-		this.pmaBlendModes = pmaBlendModes;
-	}
-
-	/** Sets {@link #setPremultipliedAlphaColors(boolean)} and {@link #setPremultipliedAlphaBlendModes(boolean)}. */
-	public void setPremultipliedAlpha (boolean pmaColorsAndBlendModes) {
-		pmaColors = pmaColorsAndBlendModes;
-		pmaBlendModes = pmaColorsAndBlendModes;
-	}
-
-	public @Null VertexEffect getVertexEffect () {
-		return vertexEffect;
-	}
-
-	public void setVertexEffect (@Null VertexEffect vertexEffect) {
-		this.vertexEffect = vertexEffect;
-	}
-
-	/** Modifies the skeleton or vertex positions, UVs, or colors during rendering. */
-	static public interface VertexEffect {
-		public void begin (Skeleton skeleton);
-
-		public void transform (Vector2 position, Vector2 uv, Color color, Color darkColor);
-
-		public void end ();
-	}
-}
+/******************************************************************************
+ * Spine Runtimes License Agreement
+ * Last updated January 1, 2020. Replaces all prior versions.
+ *
+ * Copyright (c) 2013-2020, Esoteric Software LLC
+ *
+ * Integration of the Spine Runtimes into software or otherwise creating
+ * derivative works of the Spine Runtimes is permitted under the terms and
+ * conditions of Section 2 of the Spine Editor License Agreement:
+ * http://esotericsoftware.com/spine-editor-license
+ *
+ * Otherwise, it is permitted to integrate the Spine Runtimes into software
+ * or otherwise create derivative works of the Spine Runtimes (collectively,
+ * "Products"), provided that each user of the Products must obtain their own
+ * Spine Editor license and redistribution of the Products in any form must
+ * include this license and copyright notice.
+ *
+ * THE SPINE RUNTIMES ARE PROVIDED BY ESOTERIC SOFTWARE LLC "AS IS" AND ANY
+ * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL ESOTERIC SOFTWARE LLC BE LIABLE FOR ANY
+ * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES,
+ * BUSINESS INTERRUPTION, OR LOSS OF USE, DATA, OR PROFITS) HOWEVER CAUSED AND
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
+ * THE SPINE RUNTIMES, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ *****************************************************************************/
+
+package com.esotericsoftware.spine;
+
+import com.badlogic.gdx.graphics.Color;
+import com.badlogic.gdx.graphics.Texture;
+import com.badlogic.gdx.graphics.g2d.Batch;
+import com.badlogic.gdx.graphics.g2d.PolygonSpriteBatch;
+import com.badlogic.gdx.math.Vector2;
+import com.badlogic.gdx.utils.FloatArray;
+import com.badlogic.gdx.utils.Null;
+import com.badlogic.gdx.utils.NumberUtils;
+import com.badlogic.gdx.utils.ShortArray;
+
+import com.esotericsoftware.spine.attachments.Attachment;
+import com.esotericsoftware.spine.attachments.ClippingAttachment;
+import com.esotericsoftware.spine.attachments.MeshAttachment;
+import com.esotericsoftware.spine.attachments.RegionAttachment;
+import com.esotericsoftware.spine.attachments.SkeletonAttachment;
+import com.esotericsoftware.spine.utils.SkeletonClipping;
+import com.esotericsoftware.spine.utils.TwoColorPolygonBatch;
+
+public class SkeletonRenderer {
+	static private final short[] quadTriangles = {0, 1, 2, 2, 3, 0};
+
+	private boolean pmaColors, pmaBlendModes;
+	private final FloatArray vertices = new FloatArray(32);
+	private final SkeletonClipping clipper = new SkeletonClipping();
+	private @Null VertexEffect vertexEffect;
+	private final Vector2 temp = new Vector2();
+	private final Vector2 temp2 = new Vector2();
+	private final Color temp3 = new Color();
+	private final Color temp4 = new Color();
+	private final Color temp5 = new Color();
+	private final Color temp6 = new Color();
+
+	/** Renders the specified skeleton. If the batch is a PolygonSpriteBatch, {@link #draw(PolygonSpriteBatch, Skeleton)} is
+	 * called. If the batch is a TwoColorPolygonBatch, {@link #draw(TwoColorPolygonBatch, Skeleton)} is called. Otherwise the
+	 * skeleton is rendered without two color tinting and any mesh attachments will throw an exception.
+	 * <p>
+	 * This method may change the batch's {@link Batch#setBlendFunctionSeparate(int, int, int, int) blending function}. The
+	 * previous blend function is not restored, since that could result in unnecessary flushes, depending on what is rendered
+	 * next. */
+	public void draw (Batch batch, Skeleton skeleton) {
+		if (batch instanceof TwoColorPolygonBatch) {
+			draw((TwoColorPolygonBatch)batch, skeleton);
+			return;
+		}
+		if (batch instanceof PolygonSpriteBatch) {
+			draw((PolygonSpriteBatch)batch, skeleton);
+			return;
+		}
+		if (batch == null) throw new IllegalArgumentException("batch cannot be null.");
+		if (skeleton == null) throw new IllegalArgumentException("skeleton cannot be null.");
+
+		VertexEffect vertexEffect = this.vertexEffect;
+		if (vertexEffect != null) vertexEffect.begin(skeleton);
+
+		boolean pmaColors = this.pmaColors, pmaBlendModes = this.pmaBlendModes;
+		BlendMode blendMode = null;
+		float[] vertices = this.vertices.items;
+		Color skeletonColor = skeleton.color;
+		float r = skeletonColor.r, g = skeletonColor.g, b = skeletonColor.b, a = skeletonColor.a;
+		Object[] drawOrder = skeleton.drawOrder.items;
+		for (int i = 0, n = skeleton.drawOrder.size; i < n; i++) {
+			Slot slot = (Slot)drawOrder[i];
+			if (!slot.bone.active) {
+				clipper.clipEnd(slot);
+				continue;
+			}
+			Attachment attachment = slot.attachment;
+			if (attachment instanceof RegionAttachment) {
+				RegionAttachment region = (RegionAttachment)attachment;
+				region.computeWorldVertices(slot.getBone(), vertices, 0, 5);
+				Color color = region.getColor(), slotColor = slot.getColor();
+				float alpha = a * slotColor.a * color.a * 255;
+				float multiplier = pmaColors ? alpha : 255;
+
+				BlendMode slotBlendMode = slot.data.getBlendMode();
+				if (slotBlendMode != blendMode) {
+					if (slotBlendMode == BlendMode.additive && pmaColors) {
+						slotBlendMode = BlendMode.normal;
+						alpha = 0;
+					}
+					blendMode = slotBlendMode;
+					blendMode.apply(batch, pmaBlendModes);
+				}
+
+				float c = NumberUtils.intToFloatColor((int)alpha << 24 //
+					| (int)(b * slotColor.b * color.b * multiplier) << 16 //
+					| (int)(g * slotColor.g * color.g * multiplier) << 8 //
+					| (int)(r * slotColor.r * color.r * multiplier));
+				float[] uvs = region.getUVs();
+				for (int u = 0, v = 2; u < 8; u += 2, v += 5) {
+					vertices[v] = c;
+					vertices[v + 1] = uvs[u];
+					vertices[v + 2] = uvs[u + 1];
+				}
+
+				if (vertexEffect != null) applyVertexEffect(vertices, 20, 5, c, 0);
+
+				batch.draw(region.getRegion().getTexture(), vertices, 0, 20);
+
+			} else if (attachment instanceof ClippingAttachment) {
+				clipper.clipStart(slot, (ClippingAttachment)attachment);
+				continue;
+
+			} else if (attachment instanceof MeshAttachment) {
+				throw new RuntimeException(batch.getClass().getSimpleName()
+					+ " cannot render meshes, PolygonSpriteBatch or TwoColorPolygonBatch is required.");
+
+			} else if (attachment instanceof SkeletonAttachment) {
+				Skeleton attachmentSkeleton = ((SkeletonAttachment)attachment).getSkeleton();
+				if (attachmentSkeleton != null) draw(batch, attachmentSkeleton);
+			}
+
+			clipper.clipEnd(slot);
+		}
+		clipper.clipEnd();
+		if (vertexEffect != null) vertexEffect.end();
+	}
+
+	/** Renders the specified skeleton, including meshes, but without two color tinting.
+	 * <p>
+	 * This method may change the batch's {@link Batch#setBlendFunctionSeparate(int, int, int, int) blending function}. The
+	 * previous blend function is not restored, since that could result in unnecessary flushes, depending on what is rendered
+	 * next. */
+	public void draw (PolygonSpriteBatch batch, Skeleton skeleton) {
+		if (batch == null) throw new IllegalArgumentException("batch cannot be null.");
+		if (skeleton == null) throw new IllegalArgumentException("skeleton cannot be null.");
+
+		Vector2 tempPosition = this.temp, tempUV = this.temp2;
+		Color tempLight1 = this.temp3, tempDark1 = this.temp4;
+		Color tempLight2 = this.temp5, tempDark2 = this.temp6;
+		VertexEffect vertexEffect = this.vertexEffect;
+		if (vertexEffect != null) vertexEffect.begin(skeleton);
+
+		boolean pmaColors = this.pmaColors, pmaBlendModes = this.pmaBlendModes;
+		BlendMode blendMode = null;
+		int verticesLength = 0;
+		float[] vertices = null, uvs = null;
+		short[] triangles = null;
+		Color color = null, skeletonColor = skeleton.color;
+		float r = skeletonColor.r, g = skeletonColor.g, b = skeletonColor.b, a = skeletonColor.a;
+		Object[] drawOrder = skeleton.drawOrder.items;
+		for (int i = 0, n = skeleton.drawOrder.size; i < n; i++) {
+			Slot slot = (Slot)drawOrder[i];
+			if (!slot.bone.active) {
+				clipper.clipEnd(slot);
+				continue;
+			}
+			Texture texture = null;
+			int vertexSize = clipper.isClipping() ? 2 : 5;
+			Attachment attachment = slot.attachment;
+			if (attachment instanceof RegionAttachment) {
+				RegionAttachment region = (RegionAttachment)attachment;
+				verticesLength = vertexSize << 2;
+				vertices = this.vertices.items;
+				region.computeWorldVertices(slot.getBone(), vertices, 0, vertexSize);
+				triangles = quadTriangles;
+				texture = region.getRegion().getTexture();
+				uvs = region.getUVs();
+				color = region.getColor();
+
+			} else if (attachment instanceof MeshAttachment) {
+				MeshAttachment mesh = (MeshAttachment)attachment;
+				int count = mesh.getWorldVerticesLength();
+				verticesLength = (count >> 1) * vertexSize;
+				vertices = this.vertices.setSize(verticesLength);
+				mesh.computeWorldVertices(slot, 0, count, vertices, 0, vertexSize);
+				triangles = mesh.getTriangles();
+				texture = mesh.getRegion().getTexture();
+				uvs = mesh.getUVs();
+				color = mesh.getColor();
+
+			} else if (attachment instanceof ClippingAttachment) {
+				ClippingAttachment clip = (ClippingAttachment)attachment;
+				clipper.clipStart(slot, clip);
+				continue;
+
+			} else if (attachment instanceof SkeletonAttachment) {
+				Skeleton attachmentSkeleton = ((SkeletonAttachment)attachment).getSkeleton();
+				if (attachmentSkeleton != null) draw(batch, attachmentSkeleton);
+			}
+
+			if (texture != null) {
+				Color slotColor = slot.getColor();
+				float alpha = a * slotColor.a * color.a * 255;
+				float multiplier = pmaColors ? alpha : 255;
+
+				BlendMode slotBlendMode = slot.data.getBlendMode();
+				if (slotBlendMode != blendMode) {
+					if (slotBlendMode == BlendMode.additive && pmaColors) {
+						slotBlendMode = BlendMode.normal;
+						alpha = 0;
+					}
+					blendMode = slotBlendMode;
+					blendMode.apply(batch, pmaBlendModes);
+				}
+
+				float c = NumberUtils.intToFloatColor((int)alpha << 24 //
+					| (int)(b * slotColor.b * color.b * multiplier) << 16 //
+					| (int)(g * slotColor.g * color.g * multiplier) << 8 //
+					| (int)(r * slotColor.r * color.r * multiplier));
+
+				if (clipper.isClipping()) {
+					clipper.clipTriangles(vertices, verticesLength, triangles, triangles.length, uvs, c, 0, false);
+					FloatArray clippedVertices = clipper.getClippedVertices();
+					ShortArray clippedTriangles = clipper.getClippedTriangles();
+					if (vertexEffect != null) applyVertexEffect(clippedVertices.items, clippedVertices.size, 5, c, 0);
+					batch.draw(texture, clippedVertices.items, 0, clippedVertices.size, clippedTriangles.items, 0,
+						clippedTriangles.size);
+				} else {
+					if (vertexEffect != null) {
+						tempLight1.set(NumberUtils.floatToIntColor(c));
+						tempDark1.set(0);
+						for (int v = 0, u = 0; v < verticesLength; v += 5, u += 2) {
+							tempPosition.x = vertices[v];
+							tempPosition.y = vertices[v + 1];
+							tempLight2.set(tempLight1);
+							tempDark2.set(tempDark1);
+							tempUV.x = uvs[u];
+							tempUV.y = uvs[u + 1];
+							vertexEffect.transform(tempPosition, tempUV, tempLight2, tempDark2);
+							vertices[v] = tempPosition.x;
+							vertices[v + 1] = tempPosition.y;
+							vertices[v + 2] = tempLight2.toFloatBits();
+							vertices[v + 3] = tempUV.x;
+							vertices[v + 4] = tempUV.y;
+						}
+					} else {
+						for (int v = 2, u = 0; v < verticesLength; v += 5, u += 2) {
+							vertices[v] = c;
+							vertices[v + 1] = uvs[u];
+							vertices[v + 2] = uvs[u + 1];
+						}
+					}
+					batch.draw(texture, vertices, 0, verticesLength, triangles, 0, triangles.length);
+				}
+			}
+
+			clipper.clipEnd(slot);
+		}
+		clipper.clipEnd();
+		if (vertexEffect != null) vertexEffect.end();
+	}
+
+	/** Renders the specified skeleton, including meshes and two color tinting.
+	 * <p>
+	 * This method may change the batch's {@link Batch#setBlendFunctionSeparate(int, int, int, int) blending function}. The
+	 * previous blend function is not restored, since that could result in unnecessary flushes, depending on what is rendered
+	 * next. */
+	public void draw (TwoColorPolygonBatch batch, Skeleton skeleton) {
+		if (batch == null) throw new IllegalArgumentException("batch cannot be null.");
+		if (skeleton == null) throw new IllegalArgumentException("skeleton cannot be null.");
+
+		Vector2 tempPosition = this.temp, tempUV = this.temp2;
+		Color tempLight1 = this.temp3, tempDark1 = this.temp4;
+		Color tempLight2 = this.temp5, tempDark2 = this.temp6;
+		VertexEffect vertexEffect = this.vertexEffect;
+		if (vertexEffect != null) vertexEffect.begin(skeleton);
+
+		boolean pmaColors = this.pmaColors, pmaBlendModes = this.pmaBlendModes;
+		batch.setPremultipliedAlpha(pmaColors);
+		BlendMode blendMode = null;
+		int verticesLength = 0;
+		float[] vertices = null, uvs = null;
+		short[] triangles = null;
+		Color color = null, skeletonColor = skeleton.color;
+		float r = skeletonColor.r, g = skeletonColor.g, b = skeletonColor.b, a = skeletonColor.a;
+		Object[] drawOrder = skeleton.drawOrder.items;
+		for (int i = 0, n = skeleton.drawOrder.size; i < n; i++) {
+			Slot slot = (Slot)drawOrder[i];
+			if (!slot.bone.active) {
+				clipper.clipEnd(slot);
+				continue;
+			}
+			Texture texture = null;
+			int vertexSize = clipper.isClipping() ? 2 : 6;
+			Attachment attachment = slot.attachment;
+			if (attachment instanceof RegionAttachment) {
+				RegionAttachment region = (RegionAttachment)attachment;
+				verticesLength = vertexSize << 2;
+				vertices = this.vertices.items;
+				region.computeWorldVertices(slot.getBone(), vertices, 0, vertexSize);
+				triangles = quadTriangles;
+				texture = region.getRegion().getTexture();
+				uvs = region.getUVs();
+				color = region.getColor();
+
+			} else if (attachment instanceof MeshAttachment) {
+				MeshAttachment mesh = (MeshAttachment)attachment;
+				int count = mesh.getWorldVerticesLength();
+				verticesLength = (count >> 1) * vertexSize;
+				vertices = this.vertices.setSize(verticesLength);
+				mesh.computeWorldVertices(slot, 0, count, vertices, 0, vertexSize);
+				triangles = mesh.getTriangles();
+				texture = mesh.getRegion().getTexture();
+				uvs = mesh.getUVs();
+				color = mesh.getColor();
+
+			} else if (attachment instanceof ClippingAttachment) {
+				ClippingAttachment clip = (ClippingAttachment)attachment;
+				clipper.clipStart(slot, clip);
+				continue;
+
+			} else if (attachment instanceof SkeletonAttachment) {
+				Skeleton attachmentSkeleton = ((SkeletonAttachment)attachment).getSkeleton();
+				if (attachmentSkeleton != null) draw(batch, attachmentSkeleton);
+			}
+
+			if (texture != null) {
+				Color lightColor = slot.getColor();
+				float alpha = a * lightColor.a * color.a * 255;
+				float multiplier = pmaColors ? alpha : 255;
+
+				BlendMode slotBlendMode = slot.data.getBlendMode();
+				if (slotBlendMode != blendMode) {
+					if (slotBlendMode == BlendMode.additive && pmaColors) {
+						slotBlendMode = BlendMode.normal;
+						alpha = 0;
+					}
+					blendMode = slotBlendMode;
+					blendMode.apply(batch, pmaBlendModes);
+				}
+
+				float red = r * color.r * multiplier;
+				float green = g * color.g * multiplier;
+				float blue = b * color.b * multiplier;
+				float light = NumberUtils.intToFloatColor((int)alpha << 24 //
+					| (int)(blue * lightColor.b) << 16 //
+					| (int)(green * lightColor.g) << 8 //
+					| (int)(red * lightColor.r));
+				Color darkColor = slot.getDarkColor();
+				float dark = darkColor == null ? 0
+					: NumberUtils.intToFloatColor((int)(blue * darkColor.b) << 16 //
+						| (int)(green * darkColor.g) << 8 //
+						| (int)(red * darkColor.r));
+
+				if (clipper.isClipping()) {
+					clipper.clipTriangles(vertices, verticesLength, triangles, triangles.length, uvs, light, dark, true);
+					FloatArray clippedVertices = clipper.getClippedVertices();
+					ShortArray clippedTriangles = clipper.getClippedTriangles();
+					if (vertexEffect != null) applyVertexEffect(clippedVertices.items, clippedVertices.size, 6, light, dark);
+					batch.drawTwoColor(texture, clippedVertices.items, 0, clippedVertices.size, clippedTriangles.items, 0,
+						clippedTriangles.size);
+				} else {
+					if (vertexEffect != null) {
+						tempLight1.set(NumberUtils.floatToIntColor(light));
+						tempDark1.set(NumberUtils.floatToIntColor(dark));
+						for (int v = 0, u = 0; v < verticesLength; v += 6, u += 2) {
+							tempPosition.x = vertices[v];
+							tempPosition.y = vertices[v + 1];
+							tempLight2.set(tempLight1);
+							tempDark2.set(tempDark1);
+							tempUV.x = uvs[u];
+							tempUV.y = uvs[u + 1];
+							vertexEffect.transform(tempPosition, tempUV, tempLight2, tempDark2);
+							vertices[v] = tempPosition.x;
+							vertices[v + 1] = tempPosition.y;
+							vertices[v + 2] = tempLight2.toFloatBits();
+							vertices[v + 3] = tempDark2.toFloatBits();
+							vertices[v + 4] = tempUV.x;
+							vertices[v + 5] = tempUV.y;
+						}
+					} else {
+						for (int v = 2, u = 0; v < verticesLength; v += 6, u += 2) {
+							vertices[v] = light;
+							vertices[v + 1] = dark;
+							vertices[v + 2] = uvs[u];
+							vertices[v + 3] = uvs[u + 1];
+						}
+					}
+					batch.drawTwoColor(texture, vertices, 0, verticesLength, triangles, 0, triangles.length);
+				}
+			}
+
+			clipper.clipEnd(slot);
+		}
+		clipper.clipEnd();
+		if (vertexEffect != null) vertexEffect.end();
+	}
+
+	private void applyVertexEffect (float[] vertices, int verticesLength, int stride, float light, float dark) {
+		Vector2 tempPosition = this.temp, tempUV = this.temp2;
+		Color tempLight1 = this.temp3, tempDark1 = this.temp4;
+		Color tempLight2 = this.temp5, tempDark2 = this.temp6;
+		VertexEffect vertexEffect = this.vertexEffect;
+		tempLight1.set(NumberUtils.floatToIntColor(light));
+		tempDark1.set(NumberUtils.floatToIntColor(dark));
+		if (stride == 5) {
+			for (int v = 0; v < verticesLength; v += stride) {
+				tempPosition.x = vertices[v];
+				tempPosition.y = vertices[v + 1];
+				tempUV.x = vertices[v + 3];
+				tempUV.y = vertices[v + 4];
+				tempLight2.set(tempLight1);
+				tempDark2.set(tempDark1);
+				vertexEffect.transform(tempPosition, tempUV, tempLight2, tempDark2);
+				vertices[v] = tempPosition.x;
+				vertices[v + 1] = tempPosition.y;
+				vertices[v + 2] = tempLight2.toFloatBits();
+				vertices[v + 3] = tempUV.x;
+				vertices[v + 4] = tempUV.y;
+			}
+		} else {
+			for (int v = 0; v < verticesLength; v += stride) {
+				tempPosition.x = vertices[v];
+				tempPosition.y = vertices[v + 1];
+				tempUV.x = vertices[v + 4];
+				tempUV.y = vertices[v + 5];
+				tempLight2.set(tempLight1);
+				tempDark2.set(tempDark1);
+				vertexEffect.transform(tempPosition, tempUV, tempLight2, tempDark2);
+				vertices[v] = tempPosition.x;
+				vertices[v + 1] = tempPosition.y;
+				vertices[v + 2] = tempLight2.toFloatBits();
+				vertices[v + 3] = tempDark2.toFloatBits();
+				vertices[v + 4] = tempUV.x;
+				vertices[v + 5] = tempUV.y;
+			}
+		}
+	}
+
+	public boolean getPremultipliedAlphaColors () {
+		return pmaColors;
+	}
+
+	/** If true, colors will be multiplied by their alpha before being sent to the GPU. Set to false if premultiplied alpha is not
+	 * being used or if the shader does the multiplication (libgdx's default batch shaders do not). Default is false. */
+	public void setPremultipliedAlphaColors (boolean pmaColors) {
+		this.pmaColors = pmaColors;
+	}
+
+	public boolean getPremultipliedAlphaBlendModes () {
+		return pmaBlendModes;
+	}
+
+	/** If true, blend modes for premultiplied alpha will be used. Set to false if premultiplied alpha is not being used. Default
+	 * is false. */
+	public void setPremultipliedAlphaBlendModes (boolean pmaBlendModes) {
+		this.pmaBlendModes = pmaBlendModes;
+	}
+
+	/** Sets {@link #setPremultipliedAlphaColors(boolean)} and {@link #setPremultipliedAlphaBlendModes(boolean)}. */
+	public void setPremultipliedAlpha (boolean pmaColorsAndBlendModes) {
+		pmaColors = pmaColorsAndBlendModes;
+		pmaBlendModes = pmaColorsAndBlendModes;
+	}
+
+	public @Null VertexEffect getVertexEffect () {
+		return vertexEffect;
+	}
+
+	public void setVertexEffect (@Null VertexEffect vertexEffect) {
+		this.vertexEffect = vertexEffect;
+	}
+
+	/** Modifies the skeleton or vertex positions, UVs, or colors during rendering. */
+	static public interface VertexEffect {
+		public void begin (Skeleton skeleton);
+
+		public void transform (Vector2 position, Vector2 uv, Color color, Color darkColor);
+
+		public void end ();
+	}
+}

+ 310 - 310
spine-libgdx/spine-libgdx/src/com/esotericsoftware/spine/SkeletonRendererDebug.java

@@ -1,310 +1,310 @@
-/******************************************************************************
- * Spine Runtimes License Agreement
- * Last updated January 1, 2020. Replaces all prior versions.
- *
- * Copyright (c) 2013-2020, Esoteric Software LLC
- *
- * Integration of the Spine Runtimes into software or otherwise creating
- * derivative works of the Spine Runtimes is permitted under the terms and
- * conditions of Section 2 of the Spine Editor License Agreement:
- * http://esotericsoftware.com/spine-editor-license
- *
- * Otherwise, it is permitted to integrate the Spine Runtimes into software
- * or otherwise create derivative works of the Spine Runtimes (collectively,
- * "Products"), provided that each user of the Products must obtain their own
- * Spine Editor license and redistribution of the Products in any form must
- * include this license and copyright notice.
- *
- * THE SPINE RUNTIMES ARE PROVIDED BY ESOTERIC SOFTWARE LLC "AS IS" AND ANY
- * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
- * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
- * DISCLAIMED. IN NO EVENT SHALL ESOTERIC SOFTWARE LLC BE LIABLE FOR ANY
- * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
- * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES,
- * BUSINESS INTERRUPTION, OR LOSS OF USE, DATA, OR PROFITS) HOWEVER CAUSED AND
- * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
- * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
- * THE SPINE RUNTIMES, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
- *****************************************************************************/
-
-package com.esotericsoftware.spine;
-
-import com.badlogic.gdx.Gdx;
-import com.badlogic.gdx.graphics.Color;
-import com.badlogic.gdx.graphics.GL20;
-import com.badlogic.gdx.graphics.glutils.ShapeRenderer;
-import com.badlogic.gdx.graphics.glutils.ShapeRenderer.ShapeType;
-import com.badlogic.gdx.math.Vector2;
-import com.badlogic.gdx.utils.Array;
-import com.badlogic.gdx.utils.FloatArray;
-
-import com.esotericsoftware.spine.attachments.Attachment;
-import com.esotericsoftware.spine.attachments.BoundingBoxAttachment;
-import com.esotericsoftware.spine.attachments.ClippingAttachment;
-import com.esotericsoftware.spine.attachments.MeshAttachment;
-import com.esotericsoftware.spine.attachments.PathAttachment;
-import com.esotericsoftware.spine.attachments.PointAttachment;
-import com.esotericsoftware.spine.attachments.RegionAttachment;
-
-public class SkeletonRendererDebug {
-	static public final Color boneLineColor = Color.RED;
-	static public final Color boneOriginColor = Color.GREEN;
-	static public final Color attachmentLineColor = new Color(0, 0, 1, 0.5f);
-	static public final Color triangleLineColor = new Color(1, 0.64f, 0, 0.5f); // ffa3007f
-	static public final Color aabbColor = new Color(0, 1, 0, 0.5f);
-
-	private final ShapeRenderer shapes;
-	private boolean drawBones = true, drawRegionAttachments = true, drawBoundingBoxes = true, drawPoints = true;
-	private boolean drawMeshHull = true, drawMeshTriangles = true, drawPaths = true, drawClipping = true;
-	private final SkeletonBounds bounds = new SkeletonBounds();
-	private final FloatArray vertices = new FloatArray(32);
-	private float scale = 1;
-	private float boneWidth = 2;
-	private boolean premultipliedAlpha;
-	private final Vector2 temp1 = new Vector2(), temp2 = new Vector2();
-
-	public SkeletonRendererDebug () {
-		shapes = new ShapeRenderer();
-	}
-
-	public SkeletonRendererDebug (ShapeRenderer shapes) {
-		if (shapes == null) throw new IllegalArgumentException("shapes cannot be null.");
-		this.shapes = shapes;
-	}
-
-	public void draw (Skeleton skeleton) {
-		if (skeleton == null) throw new IllegalArgumentException("skeleton cannot be null.");
-
-		Gdx.gl.glEnable(GL20.GL_BLEND);
-		int srcFunc = premultipliedAlpha ? GL20.GL_ONE : GL20.GL_SRC_ALPHA;
-		Gdx.gl.glBlendFunc(srcFunc, GL20.GL_ONE_MINUS_SRC_ALPHA);
-
-		ShapeRenderer shapes = this.shapes;
-		Array<Bone> bones = skeleton.getBones();
-		Array<Slot> slots = skeleton.getSlots();
-
-		shapes.begin(ShapeType.Filled);
-
-		if (drawBones) {
-			for (int i = 0, n = bones.size; i < n; i++) {
-				Bone bone = bones.get(i);
-				if (bone.parent == null || !bone.active) continue;
-				float length = bone.data.length, width = boneWidth;
-				if (length == 0) {
-					length = 8;
-					width /= 2;
-					shapes.setColor(boneOriginColor);
-				} else
-					shapes.setColor(boneLineColor);
-				float x = length * bone.a + bone.worldX;
-				float y = length * bone.c + bone.worldY;
-				shapes.rectLine(bone.worldX, bone.worldY, x, y, width * scale);
-			}
-			shapes.x(skeleton.getX(), skeleton.getY(), 4 * scale);
-		}
-
-		if (drawPoints) {
-			shapes.setColor(boneOriginColor);
-			for (int i = 0, n = slots.size; i < n; i++) {
-				Slot slot = slots.get(i);
-				Attachment attachment = slot.attachment;
-				if (!(attachment instanceof PointAttachment)) continue;
-				PointAttachment point = (PointAttachment)attachment;
-				point.computeWorldPosition(slot.getBone(), temp1);
-				temp2.set(8, 0).rotate(point.computeWorldRotation(slot.getBone()));
-				shapes.rectLine(temp1, temp2, boneWidth / 2 * scale);
-			}
-		}
-
-		shapes.end();
-		shapes.begin(ShapeType.Line);
-
-		if (drawRegionAttachments) {
-			shapes.setColor(attachmentLineColor);
-			for (int i = 0, n = slots.size; i < n; i++) {
-				Slot slot = slots.get(i);
-				Attachment attachment = slot.attachment;
-				if (attachment instanceof RegionAttachment) {
-					RegionAttachment region = (RegionAttachment)attachment;
-					float[] vertices = this.vertices.items;
-					region.computeWorldVertices(slot.getBone(), vertices, 0, 2);
-					shapes.line(vertices[0], vertices[1], vertices[2], vertices[3]);
-					shapes.line(vertices[2], vertices[3], vertices[4], vertices[5]);
-					shapes.line(vertices[4], vertices[5], vertices[6], vertices[7]);
-					shapes.line(vertices[6], vertices[7], vertices[0], vertices[1]);
-				}
-			}
-		}
-
-		if (drawMeshHull || drawMeshTriangles) {
-			for (int i = 0, n = slots.size; i < n; i++) {
-				Slot slot = slots.get(i);
-				Attachment attachment = slot.attachment;
-				if (!(attachment instanceof MeshAttachment)) continue;
-				MeshAttachment mesh = (MeshAttachment)attachment;
-				float[] vertices = this.vertices.setSize(mesh.getWorldVerticesLength());
-				mesh.computeWorldVertices(slot, 0, mesh.getWorldVerticesLength(), vertices, 0, 2);
-				short[] triangles = mesh.getTriangles();
-				int hullLength = mesh.getHullLength();
-				if (drawMeshTriangles) {
-					shapes.setColor(triangleLineColor);
-					for (int ii = 0, nn = triangles.length; ii < nn; ii += 3) {
-						int v1 = triangles[ii] * 2, v2 = triangles[ii + 1] * 2, v3 = triangles[ii + 2] * 2;
-						shapes.triangle(vertices[v1], vertices[v1 + 1], //
-							vertices[v2], vertices[v2 + 1], //
-							vertices[v3], vertices[v3 + 1] //
-						);
-					}
-				}
-				if (drawMeshHull && hullLength > 0) {
-					shapes.setColor(attachmentLineColor);
-					float lastX = vertices[hullLength - 2], lastY = vertices[hullLength - 1];
-					for (int ii = 0, nn = hullLength; ii < nn; ii += 2) {
-						float x = vertices[ii], y = vertices[ii + 1];
-						shapes.line(x, y, lastX, lastY);
-						lastX = x;
-						lastY = y;
-					}
-				}
-			}
-		}
-
-		if (drawBoundingBoxes) {
-			SkeletonBounds bounds = this.bounds;
-			bounds.update(skeleton, true);
-			shapes.setColor(aabbColor);
-			shapes.rect(bounds.getMinX(), bounds.getMinY(), bounds.getWidth(), bounds.getHeight());
-			Array<FloatArray> polygons = bounds.getPolygons();
-			Array<BoundingBoxAttachment> boxes = bounds.getBoundingBoxes();
-			for (int i = 0, n = polygons.size; i < n; i++) {
-				FloatArray polygon = polygons.get(i);
-				shapes.setColor(boxes.get(i).getColor());
-				shapes.polygon(polygon.items, 0, polygon.size);
-			}
-		}
-
-		if (drawClipping) {
-			for (int i = 0, n = slots.size; i < n; i++) {
-				Slot slot = slots.get(i);
-				Attachment attachment = slot.attachment;
-				if (!(attachment instanceof ClippingAttachment)) continue;
-				ClippingAttachment clip = (ClippingAttachment)attachment;
-				int nn = clip.getWorldVerticesLength();
-				float[] vertices = this.vertices.setSize(nn);
-				clip.computeWorldVertices(slot, 0, nn, vertices, 0, 2);
-				shapes.setColor(clip.getColor());
-				for (int ii = 2; ii < nn; ii += 2)
-					shapes.line(vertices[ii - 2], vertices[ii - 1], vertices[ii], vertices[ii + 1]);
-				shapes.line(vertices[0], vertices[1], vertices[nn - 2], vertices[nn - 1]);
-			}
-		}
-
-		if (drawPaths) {
-			for (int i = 0, n = slots.size; i < n; i++) {
-				Slot slot = slots.get(i);
-				Attachment attachment = slot.attachment;
-				if (!(attachment instanceof PathAttachment)) continue;
-				PathAttachment path = (PathAttachment)attachment;
-				int nn = path.getWorldVerticesLength();
-				float[] vertices = this.vertices.setSize(nn);
-				path.computeWorldVertices(slot, 0, nn, vertices, 0, 2);
-				Color color = path.getColor();
-				float x1 = vertices[2], y1 = vertices[3], x2 = 0, y2 = 0;
-				if (path.getClosed()) {
-					shapes.setColor(color);
-					float cx1 = vertices[0], cy1 = vertices[1], cx2 = vertices[nn - 2], cy2 = vertices[nn - 1];
-					x2 = vertices[nn - 4];
-					y2 = vertices[nn - 3];
-					shapes.curve(x1, y1, cx1, cy1, cx2, cy2, x2, y2, 32);
-					shapes.setColor(Color.LIGHT_GRAY);
-					shapes.line(x1, y1, cx1, cy1);
-					shapes.line(x2, y2, cx2, cy2);
-				}
-				nn -= 4;
-				for (int ii = 4; ii < nn; ii += 6) {
-					float cx1 = vertices[ii], cy1 = vertices[ii + 1], cx2 = vertices[ii + 2], cy2 = vertices[ii + 3];
-					x2 = vertices[ii + 4];
-					y2 = vertices[ii + 5];
-					shapes.setColor(color);
-					shapes.curve(x1, y1, cx1, cy1, cx2, cy2, x2, y2, 32);
-					shapes.setColor(Color.LIGHT_GRAY);
-					shapes.line(x1, y1, cx1, cy1);
-					shapes.line(x2, y2, cx2, cy2);
-					x1 = x2;
-					y1 = y2;
-				}
-			}
-		}
-
-		shapes.end();
-		shapes.begin(ShapeType.Filled);
-
-		if (drawBones) {
-			shapes.setColor(boneOriginColor);
-			for (int i = 0, n = bones.size; i < n; i++) {
-				Bone bone = bones.get(i);
-				if (!bone.active) continue;
-				shapes.circle(bone.worldX, bone.worldY, 3 * scale, 8);
-			}
-		}
-
-		if (drawPoints) {
-			shapes.setColor(boneOriginColor);
-			for (int i = 0, n = slots.size; i < n; i++) {
-				Slot slot = slots.get(i);
-				Attachment attachment = slot.attachment;
-				if (!(attachment instanceof PointAttachment)) continue;
-				PointAttachment point = (PointAttachment)attachment;
-				point.computeWorldPosition(slot.getBone(), temp1);
-				shapes.circle(temp1.x, temp1.y, 3 * scale, 8);
-			}
-		}
-
-		shapes.end();
-
-	}
-
-	public ShapeRenderer getShapeRenderer () {
-		return shapes;
-	}
-
-	public void setBones (boolean bones) {
-		this.drawBones = bones;
-	}
-
-	public void setScale (float scale) {
-		this.scale = scale;
-	}
-
-	public void setRegionAttachments (boolean regionAttachments) {
-		this.drawRegionAttachments = regionAttachments;
-	}
-
-	public void setBoundingBoxes (boolean boundingBoxes) {
-		this.drawBoundingBoxes = boundingBoxes;
-	}
-
-	public void setMeshHull (boolean meshHull) {
-		this.drawMeshHull = meshHull;
-	}
-
-	public void setMeshTriangles (boolean meshTriangles) {
-		this.drawMeshTriangles = meshTriangles;
-	}
-
-	public void setPaths (boolean paths) {
-		this.drawPaths = paths;
-	}
-
-	public void setPoints (boolean points) {
-		this.drawPoints = points;
-	}
-
-	public void setClipping (boolean clipping) {
-		this.drawClipping = clipping;
-	}
-
-	public void setPremultipliedAlpha (boolean premultipliedAlpha) {
-		this.premultipliedAlpha = premultipliedAlpha;
-	}
-}
+/******************************************************************************
+ * Spine Runtimes License Agreement
+ * Last updated January 1, 2020. Replaces all prior versions.
+ *
+ * Copyright (c) 2013-2020, Esoteric Software LLC
+ *
+ * Integration of the Spine Runtimes into software or otherwise creating
+ * derivative works of the Spine Runtimes is permitted under the terms and
+ * conditions of Section 2 of the Spine Editor License Agreement:
+ * http://esotericsoftware.com/spine-editor-license
+ *
+ * Otherwise, it is permitted to integrate the Spine Runtimes into software
+ * or otherwise create derivative works of the Spine Runtimes (collectively,
+ * "Products"), provided that each user of the Products must obtain their own
+ * Spine Editor license and redistribution of the Products in any form must
+ * include this license and copyright notice.
+ *
+ * THE SPINE RUNTIMES ARE PROVIDED BY ESOTERIC SOFTWARE LLC "AS IS" AND ANY
+ * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL ESOTERIC SOFTWARE LLC BE LIABLE FOR ANY
+ * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES,
+ * BUSINESS INTERRUPTION, OR LOSS OF USE, DATA, OR PROFITS) HOWEVER CAUSED AND
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
+ * THE SPINE RUNTIMES, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ *****************************************************************************/
+
+package com.esotericsoftware.spine;
+
+import com.badlogic.gdx.Gdx;
+import com.badlogic.gdx.graphics.Color;
+import com.badlogic.gdx.graphics.GL20;
+import com.badlogic.gdx.graphics.glutils.ShapeRenderer;
+import com.badlogic.gdx.graphics.glutils.ShapeRenderer.ShapeType;
+import com.badlogic.gdx.math.Vector2;
+import com.badlogic.gdx.utils.Array;
+import com.badlogic.gdx.utils.FloatArray;
+
+import com.esotericsoftware.spine.attachments.Attachment;
+import com.esotericsoftware.spine.attachments.BoundingBoxAttachment;
+import com.esotericsoftware.spine.attachments.ClippingAttachment;
+import com.esotericsoftware.spine.attachments.MeshAttachment;
+import com.esotericsoftware.spine.attachments.PathAttachment;
+import com.esotericsoftware.spine.attachments.PointAttachment;
+import com.esotericsoftware.spine.attachments.RegionAttachment;
+
+public class SkeletonRendererDebug {
+	static public final Color boneLineColor = Color.RED;
+	static public final Color boneOriginColor = Color.GREEN;
+	static public final Color attachmentLineColor = new Color(0, 0, 1, 0.5f);
+	static public final Color triangleLineColor = new Color(1, 0.64f, 0, 0.5f); // ffa3007f
+	static public final Color aabbColor = new Color(0, 1, 0, 0.5f);
+
+	private final ShapeRenderer shapes;
+	private boolean drawBones = true, drawRegionAttachments = true, drawBoundingBoxes = true, drawPoints = true;
+	private boolean drawMeshHull = true, drawMeshTriangles = true, drawPaths = true, drawClipping = true;
+	private final SkeletonBounds bounds = new SkeletonBounds();
+	private final FloatArray vertices = new FloatArray(32);
+	private float scale = 1;
+	private float boneWidth = 2;
+	private boolean premultipliedAlpha;
+	private final Vector2 temp1 = new Vector2(), temp2 = new Vector2();
+
+	public SkeletonRendererDebug () {
+		shapes = new ShapeRenderer();
+	}
+
+	public SkeletonRendererDebug (ShapeRenderer shapes) {
+		if (shapes == null) throw new IllegalArgumentException("shapes cannot be null.");
+		this.shapes = shapes;
+	}
+
+	public void draw (Skeleton skeleton) {
+		if (skeleton == null) throw new IllegalArgumentException("skeleton cannot be null.");
+
+		Gdx.gl.glEnable(GL20.GL_BLEND);
+		int srcFunc = premultipliedAlpha ? GL20.GL_ONE : GL20.GL_SRC_ALPHA;
+		Gdx.gl.glBlendFunc(srcFunc, GL20.GL_ONE_MINUS_SRC_ALPHA);
+
+		ShapeRenderer shapes = this.shapes;
+		Array<Bone> bones = skeleton.getBones();
+		Array<Slot> slots = skeleton.getSlots();
+
+		shapes.begin(ShapeType.Filled);
+
+		if (drawBones) {
+			for (int i = 0, n = bones.size; i < n; i++) {
+				Bone bone = bones.get(i);
+				if (bone.parent == null || !bone.active) continue;
+				float length = bone.data.length, width = boneWidth;
+				if (length == 0) {
+					length = 8;
+					width /= 2;
+					shapes.setColor(boneOriginColor);
+				} else
+					shapes.setColor(boneLineColor);
+				float x = length * bone.a + bone.worldX;
+				float y = length * bone.c + bone.worldY;
+				shapes.rectLine(bone.worldX, bone.worldY, x, y, width * scale);
+			}
+			shapes.x(skeleton.getX(), skeleton.getY(), 4 * scale);
+		}
+
+		if (drawPoints) {
+			shapes.setColor(boneOriginColor);
+			for (int i = 0, n = slots.size; i < n; i++) {
+				Slot slot = slots.get(i);
+				Attachment attachment = slot.attachment;
+				if (!(attachment instanceof PointAttachment)) continue;
+				PointAttachment point = (PointAttachment)attachment;
+				point.computeWorldPosition(slot.getBone(), temp1);
+				temp2.set(8, 0).rotate(point.computeWorldRotation(slot.getBone()));
+				shapes.rectLine(temp1, temp2, boneWidth / 2 * scale);
+			}
+		}
+
+		shapes.end();
+		shapes.begin(ShapeType.Line);
+
+		if (drawRegionAttachments) {
+			shapes.setColor(attachmentLineColor);
+			for (int i = 0, n = slots.size; i < n; i++) {
+				Slot slot = slots.get(i);
+				Attachment attachment = slot.attachment;
+				if (attachment instanceof RegionAttachment) {
+					RegionAttachment region = (RegionAttachment)attachment;
+					float[] vertices = this.vertices.items;
+					region.computeWorldVertices(slot.getBone(), vertices, 0, 2);
+					shapes.line(vertices[0], vertices[1], vertices[2], vertices[3]);
+					shapes.line(vertices[2], vertices[3], vertices[4], vertices[5]);
+					shapes.line(vertices[4], vertices[5], vertices[6], vertices[7]);
+					shapes.line(vertices[6], vertices[7], vertices[0], vertices[1]);
+				}
+			}
+		}
+
+		if (drawMeshHull || drawMeshTriangles) {
+			for (int i = 0, n = slots.size; i < n; i++) {
+				Slot slot = slots.get(i);
+				Attachment attachment = slot.attachment;
+				if (!(attachment instanceof MeshAttachment)) continue;
+				MeshAttachment mesh = (MeshAttachment)attachment;
+				float[] vertices = this.vertices.setSize(mesh.getWorldVerticesLength());
+				mesh.computeWorldVertices(slot, 0, mesh.getWorldVerticesLength(), vertices, 0, 2);
+				short[] triangles = mesh.getTriangles();
+				int hullLength = mesh.getHullLength();
+				if (drawMeshTriangles) {
+					shapes.setColor(triangleLineColor);
+					for (int ii = 0, nn = triangles.length; ii < nn; ii += 3) {
+						int v1 = triangles[ii] * 2, v2 = triangles[ii + 1] * 2, v3 = triangles[ii + 2] * 2;
+						shapes.triangle(vertices[v1], vertices[v1 + 1], //
+							vertices[v2], vertices[v2 + 1], //
+							vertices[v3], vertices[v3 + 1] //
+						);
+					}
+				}
+				if (drawMeshHull && hullLength > 0) {
+					shapes.setColor(attachmentLineColor);
+					float lastX = vertices[hullLength - 2], lastY = vertices[hullLength - 1];
+					for (int ii = 0, nn = hullLength; ii < nn; ii += 2) {
+						float x = vertices[ii], y = vertices[ii + 1];
+						shapes.line(x, y, lastX, lastY);
+						lastX = x;
+						lastY = y;
+					}
+				}
+			}
+		}
+
+		if (drawBoundingBoxes) {
+			SkeletonBounds bounds = this.bounds;
+			bounds.update(skeleton, true);
+			shapes.setColor(aabbColor);
+			shapes.rect(bounds.getMinX(), bounds.getMinY(), bounds.getWidth(), bounds.getHeight());
+			Array<FloatArray> polygons = bounds.getPolygons();
+			Array<BoundingBoxAttachment> boxes = bounds.getBoundingBoxes();
+			for (int i = 0, n = polygons.size; i < n; i++) {
+				FloatArray polygon = polygons.get(i);
+				shapes.setColor(boxes.get(i).getColor());
+				shapes.polygon(polygon.items, 0, polygon.size);
+			}
+		}
+
+		if (drawClipping) {
+			for (int i = 0, n = slots.size; i < n; i++) {
+				Slot slot = slots.get(i);
+				Attachment attachment = slot.attachment;
+				if (!(attachment instanceof ClippingAttachment)) continue;
+				ClippingAttachment clip = (ClippingAttachment)attachment;
+				int nn = clip.getWorldVerticesLength();
+				float[] vertices = this.vertices.setSize(nn);
+				clip.computeWorldVertices(slot, 0, nn, vertices, 0, 2);
+				shapes.setColor(clip.getColor());
+				for (int ii = 2; ii < nn; ii += 2)
+					shapes.line(vertices[ii - 2], vertices[ii - 1], vertices[ii], vertices[ii + 1]);
+				shapes.line(vertices[0], vertices[1], vertices[nn - 2], vertices[nn - 1]);
+			}
+		}
+
+		if (drawPaths) {
+			for (int i = 0, n = slots.size; i < n; i++) {
+				Slot slot = slots.get(i);
+				Attachment attachment = slot.attachment;
+				if (!(attachment instanceof PathAttachment)) continue;
+				PathAttachment path = (PathAttachment)attachment;
+				int nn = path.getWorldVerticesLength();
+				float[] vertices = this.vertices.setSize(nn);
+				path.computeWorldVertices(slot, 0, nn, vertices, 0, 2);
+				Color color = path.getColor();
+				float x1 = vertices[2], y1 = vertices[3], x2 = 0, y2 = 0;
+				if (path.getClosed()) {
+					shapes.setColor(color);
+					float cx1 = vertices[0], cy1 = vertices[1], cx2 = vertices[nn - 2], cy2 = vertices[nn - 1];
+					x2 = vertices[nn - 4];
+					y2 = vertices[nn - 3];
+					shapes.curve(x1, y1, cx1, cy1, cx2, cy2, x2, y2, 32);
+					shapes.setColor(Color.LIGHT_GRAY);
+					shapes.line(x1, y1, cx1, cy1);
+					shapes.line(x2, y2, cx2, cy2);
+				}
+				nn -= 4;
+				for (int ii = 4; ii < nn; ii += 6) {
+					float cx1 = vertices[ii], cy1 = vertices[ii + 1], cx2 = vertices[ii + 2], cy2 = vertices[ii + 3];
+					x2 = vertices[ii + 4];
+					y2 = vertices[ii + 5];
+					shapes.setColor(color);
+					shapes.curve(x1, y1, cx1, cy1, cx2, cy2, x2, y2, 32);
+					shapes.setColor(Color.LIGHT_GRAY);
+					shapes.line(x1, y1, cx1, cy1);
+					shapes.line(x2, y2, cx2, cy2);
+					x1 = x2;
+					y1 = y2;
+				}
+			}
+		}
+
+		shapes.end();
+		shapes.begin(ShapeType.Filled);
+
+		if (drawBones) {
+			shapes.setColor(boneOriginColor);
+			for (int i = 0, n = bones.size; i < n; i++) {
+				Bone bone = bones.get(i);
+				if (!bone.active) continue;
+				shapes.circle(bone.worldX, bone.worldY, 3 * scale, 8);
+			}
+		}
+
+		if (drawPoints) {
+			shapes.setColor(boneOriginColor);
+			for (int i = 0, n = slots.size; i < n; i++) {
+				Slot slot = slots.get(i);
+				Attachment attachment = slot.attachment;
+				if (!(attachment instanceof PointAttachment)) continue;
+				PointAttachment point = (PointAttachment)attachment;
+				point.computeWorldPosition(slot.getBone(), temp1);
+				shapes.circle(temp1.x, temp1.y, 3 * scale, 8);
+			}
+		}
+
+		shapes.end();
+
+	}
+
+	public ShapeRenderer getShapeRenderer () {
+		return shapes;
+	}
+
+	public void setBones (boolean bones) {
+		this.drawBones = bones;
+	}
+
+	public void setScale (float scale) {
+		this.scale = scale;
+	}
+
+	public void setRegionAttachments (boolean regionAttachments) {
+		this.drawRegionAttachments = regionAttachments;
+	}
+
+	public void setBoundingBoxes (boolean boundingBoxes) {
+		this.drawBoundingBoxes = boundingBoxes;
+	}
+
+	public void setMeshHull (boolean meshHull) {
+		this.drawMeshHull = meshHull;
+	}
+
+	public void setMeshTriangles (boolean meshTriangles) {
+		this.drawMeshTriangles = meshTriangles;
+	}
+
+	public void setPaths (boolean paths) {
+		this.drawPaths = paths;
+	}
+
+	public void setPoints (boolean points) {
+		this.drawPoints = points;
+	}
+
+	public void setClipping (boolean clipping) {
+		this.drawClipping = clipping;
+	}
+
+	public void setPremultipliedAlpha (boolean premultipliedAlpha) {
+		this.premultipliedAlpha = premultipliedAlpha;
+	}
+}

+ 207 - 207
spine-libgdx/spine-libgdx/src/com/esotericsoftware/spine/Skin.java

@@ -1,207 +1,207 @@
-/******************************************************************************
- * Spine Runtimes License Agreement
- * Last updated January 1, 2020. Replaces all prior versions.
- *
- * Copyright (c) 2013-2020, Esoteric Software LLC
- *
- * Integration of the Spine Runtimes into software or otherwise creating
- * derivative works of the Spine Runtimes is permitted under the terms and
- * conditions of Section 2 of the Spine Editor License Agreement:
- * http://esotericsoftware.com/spine-editor-license
- *
- * Otherwise, it is permitted to integrate the Spine Runtimes into software
- * or otherwise create derivative works of the Spine Runtimes (collectively,
- * "Products"), provided that each user of the Products must obtain their own
- * Spine Editor license and redistribution of the Products in any form must
- * include this license and copyright notice.
- *
- * THE SPINE RUNTIMES ARE PROVIDED BY ESOTERIC SOFTWARE LLC "AS IS" AND ANY
- * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
- * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
- * DISCLAIMED. IN NO EVENT SHALL ESOTERIC SOFTWARE LLC BE LIABLE FOR ANY
- * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
- * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES,
- * BUSINESS INTERRUPTION, OR LOSS OF USE, DATA, OR PROFITS) HOWEVER CAUSED AND
- * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
- * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
- * THE SPINE RUNTIMES, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
- *****************************************************************************/
-
-package com.esotericsoftware.spine;
-
-import com.badlogic.gdx.utils.Array;
-import com.badlogic.gdx.utils.Null;
-import com.badlogic.gdx.utils.OrderedSet;
-
-import com.esotericsoftware.spine.attachments.Attachment;
-import com.esotericsoftware.spine.attachments.MeshAttachment;
-
-/** Stores attachments by slot index and attachment name.
- * <p>
- * See SkeletonData {@link SkeletonData#defaultSkin}, Skeleton {@link Skeleton#skin}, and
- * <a href="http://esotericsoftware.com/spine-runtime-skins">Runtime skins</a> in the Spine Runtimes Guide. */
-public class Skin {
-	final String name;
-	final OrderedSet<SkinEntry> attachments = new OrderedSet();
-	final Array<BoneData> bones = new Array(0);
-	final Array<ConstraintData> constraints = new Array(0);
-	private final SkinEntry lookup = new SkinEntry(0, "", null);
-
-	public Skin (String name) {
-		if (name == null) throw new IllegalArgumentException("name cannot be null.");
-		this.name = name;
-		attachments.orderedItems().ordered = false;
-	}
-
-	/** Adds an attachment to the skin for the specified slot index and name. */
-	public void setAttachment (int slotIndex, String name, Attachment attachment) {
-		if (attachment == null) throw new IllegalArgumentException("attachment cannot be null.");
-		SkinEntry entry = new SkinEntry(slotIndex, name, attachment);
-		if (!attachments.add(entry)) attachments.get(entry).attachment = attachment;
-	}
-
-	/** Adds all attachments, bones, and constraints from the specified skin to this skin. */
-	public void addSkin (Skin skin) {
-		if (skin == null) throw new IllegalArgumentException("skin cannot be null.");
-
-		for (BoneData data : skin.bones)
-			if (!bones.contains(data, true)) bones.add(data);
-
-		for (ConstraintData data : skin.constraints)
-			if (!constraints.contains(data, true)) constraints.add(data);
-
-		for (SkinEntry entry : skin.attachments.orderedItems())
-			setAttachment(entry.slotIndex, entry.name, entry.attachment);
-	}
-
-	/** Adds all bones and constraints and copies of all attachments from the specified skin to this skin. Mesh attachments are not
-	 * copied, instead a new linked mesh is created. The attachment copies can be modified without affecting the originals. */
-	public void copySkin (Skin skin) {
-		if (skin == null) throw new IllegalArgumentException("skin cannot be null.");
-
-		for (BoneData data : skin.bones)
-			if (!bones.contains(data, true)) bones.add(data);
-
-		for (ConstraintData data : skin.constraints)
-			if (!constraints.contains(data, true)) constraints.add(data);
-
-		for (SkinEntry entry : skin.attachments.orderedItems()) {
-			if (entry.attachment instanceof MeshAttachment)
-				setAttachment(entry.slotIndex, entry.name, ((MeshAttachment)entry.attachment).newLinkedMesh());
-			else
-				setAttachment(entry.slotIndex, entry.name, entry.attachment != null ? entry.attachment.copy() : null);
-		}
-	}
-
-	/** Returns the attachment for the specified slot index and name, or null. */
-	public @Null Attachment getAttachment (int slotIndex, String name) {
-		lookup.set(slotIndex, name);
-		SkinEntry entry = attachments.get(lookup);
-		return entry != null ? entry.attachment : null;
-	}
-
-	/** Removes the attachment in the skin for the specified slot index and name, if any. */
-	public void removeAttachment (int slotIndex, String name) {
-		lookup.set(slotIndex, name);
-		attachments.remove(lookup);
-	}
-
-	/** Returns all attachments in this skin. */
-	public Array<SkinEntry> getAttachments () {
-		return attachments.orderedItems();
-	}
-
-	/** Returns all attachments in this skin for the specified slot index. */
-	public void getAttachments (int slotIndex, Array<SkinEntry> attachments) {
-		if (slotIndex < 0) throw new IllegalArgumentException("slotIndex must be >= 0.");
-		if (attachments == null) throw new IllegalArgumentException("attachments cannot be null.");
-		for (SkinEntry entry : this.attachments.orderedItems())
-			if (entry.slotIndex == slotIndex) attachments.add(entry);
-	}
-
-	/** Clears all attachments, bones, and constraints. */
-	public void clear () {
-		attachments.clear(1024);
-		bones.clear();
-		constraints.clear();
-	}
-
-	public Array<BoneData> getBones () {
-		return bones;
-	}
-
-	public Array<ConstraintData> getConstraints () {
-		return constraints;
-	}
-
-	/** The skin's name, which is unique across all skins in the skeleton. */
-	public String getName () {
-		return name;
-	}
-
-	public String toString () {
-		return name;
-	}
-
-	/** Attach each attachment in this skin if the corresponding attachment in the old skin is currently attached. */
-	void attachAll (Skeleton skeleton, Skin oldSkin) {
-		Object[] slots = skeleton.slots.items;
-		for (SkinEntry entry : oldSkin.attachments.orderedItems()) {
-			int slotIndex = entry.slotIndex;
-			Slot slot = (Slot)slots[slotIndex];
-			if (slot.attachment == entry.attachment) {
-				Attachment attachment = getAttachment(slotIndex, entry.name);
-				if (attachment != null) slot.setAttachment(attachment);
-			}
-		}
-	}
-
-	/** Stores an entry in the skin consisting of the slot index and the attachment name. */
-	static public class SkinEntry {
-		int slotIndex;
-		String name;
-		@Null Attachment attachment;
-		private int hashCode;
-
-		SkinEntry (int slotIndex, String name, @Null Attachment attachment) {
-			set(slotIndex, name);
-			this.attachment = attachment;
-		}
-
-		void set (int slotIndex, String name) {
-			if (slotIndex < 0) throw new IllegalArgumentException("slotIndex must be >= 0.");
-			if (name == null) throw new IllegalArgumentException("name cannot be null.");
-			this.slotIndex = slotIndex;
-			this.name = name;
-			hashCode = name.hashCode() + slotIndex * 37;
-		}
-
-		public int getSlotIndex () {
-			return slotIndex;
-		}
-
-		/** The name the attachment is associated with, equivalent to the skin placeholder name in the Spine editor. */
-		public String getName () {
-			return name;
-		}
-
-		public Attachment getAttachment () {
-			return attachment;
-		}
-
-		public int hashCode () {
-			return hashCode;
-		}
-
-		public boolean equals (Object object) {
-			if (object == null) return false;
-			SkinEntry other = (SkinEntry)object;
-			if (slotIndex != other.slotIndex) return false;
-			return name.equals(other.name);
-		}
-
-		public String toString () {
-			return slotIndex + ":" + name;
-		}
-	}
-}
+/******************************************************************************
+ * Spine Runtimes License Agreement
+ * Last updated January 1, 2020. Replaces all prior versions.
+ *
+ * Copyright (c) 2013-2020, Esoteric Software LLC
+ *
+ * Integration of the Spine Runtimes into software or otherwise creating
+ * derivative works of the Spine Runtimes is permitted under the terms and
+ * conditions of Section 2 of the Spine Editor License Agreement:
+ * http://esotericsoftware.com/spine-editor-license
+ *
+ * Otherwise, it is permitted to integrate the Spine Runtimes into software
+ * or otherwise create derivative works of the Spine Runtimes (collectively,
+ * "Products"), provided that each user of the Products must obtain their own
+ * Spine Editor license and redistribution of the Products in any form must
+ * include this license and copyright notice.
+ *
+ * THE SPINE RUNTIMES ARE PROVIDED BY ESOTERIC SOFTWARE LLC "AS IS" AND ANY
+ * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL ESOTERIC SOFTWARE LLC BE LIABLE FOR ANY
+ * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES,
+ * BUSINESS INTERRUPTION, OR LOSS OF USE, DATA, OR PROFITS) HOWEVER CAUSED AND
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
+ * THE SPINE RUNTIMES, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ *****************************************************************************/
+
+package com.esotericsoftware.spine;
+
+import com.badlogic.gdx.utils.Array;
+import com.badlogic.gdx.utils.Null;
+import com.badlogic.gdx.utils.OrderedSet;
+
+import com.esotericsoftware.spine.attachments.Attachment;
+import com.esotericsoftware.spine.attachments.MeshAttachment;
+
+/** Stores attachments by slot index and attachment name.
+ * <p>
+ * See SkeletonData {@link SkeletonData#defaultSkin}, Skeleton {@link Skeleton#skin}, and
+ * <a href="http://esotericsoftware.com/spine-runtime-skins">Runtime skins</a> in the Spine Runtimes Guide. */
+public class Skin {
+	final String name;
+	final OrderedSet<SkinEntry> attachments = new OrderedSet();
+	final Array<BoneData> bones = new Array(0);
+	final Array<ConstraintData> constraints = new Array(0);
+	private final SkinEntry lookup = new SkinEntry(0, "", null);
+
+	public Skin (String name) {
+		if (name == null) throw new IllegalArgumentException("name cannot be null.");
+		this.name = name;
+		attachments.orderedItems().ordered = false;
+	}
+
+	/** Adds an attachment to the skin for the specified slot index and name. */
+	public void setAttachment (int slotIndex, String name, Attachment attachment) {
+		if (attachment == null) throw new IllegalArgumentException("attachment cannot be null.");
+		SkinEntry entry = new SkinEntry(slotIndex, name, attachment);
+		if (!attachments.add(entry)) attachments.get(entry).attachment = attachment;
+	}
+
+	/** Adds all attachments, bones, and constraints from the specified skin to this skin. */
+	public void addSkin (Skin skin) {
+		if (skin == null) throw new IllegalArgumentException("skin cannot be null.");
+
+		for (BoneData data : skin.bones)
+			if (!bones.contains(data, true)) bones.add(data);
+
+		for (ConstraintData data : skin.constraints)
+			if (!constraints.contains(data, true)) constraints.add(data);
+
+		for (SkinEntry entry : skin.attachments.orderedItems())
+			setAttachment(entry.slotIndex, entry.name, entry.attachment);
+	}
+
+	/** Adds all bones and constraints and copies of all attachments from the specified skin to this skin. Mesh attachments are not
+	 * copied, instead a new linked mesh is created. The attachment copies can be modified without affecting the originals. */
+	public void copySkin (Skin skin) {
+		if (skin == null) throw new IllegalArgumentException("skin cannot be null.");
+
+		for (BoneData data : skin.bones)
+			if (!bones.contains(data, true)) bones.add(data);
+
+		for (ConstraintData data : skin.constraints)
+			if (!constraints.contains(data, true)) constraints.add(data);
+
+		for (SkinEntry entry : skin.attachments.orderedItems()) {
+			if (entry.attachment instanceof MeshAttachment)
+				setAttachment(entry.slotIndex, entry.name, ((MeshAttachment)entry.attachment).newLinkedMesh());
+			else
+				setAttachment(entry.slotIndex, entry.name, entry.attachment != null ? entry.attachment.copy() : null);
+		}
+	}
+
+	/** Returns the attachment for the specified slot index and name, or null. */
+	public @Null Attachment getAttachment (int slotIndex, String name) {
+		lookup.set(slotIndex, name);
+		SkinEntry entry = attachments.get(lookup);
+		return entry != null ? entry.attachment : null;
+	}
+
+	/** Removes the attachment in the skin for the specified slot index and name, if any. */
+	public void removeAttachment (int slotIndex, String name) {
+		lookup.set(slotIndex, name);
+		attachments.remove(lookup);
+	}
+
+	/** Returns all attachments in this skin. */
+	public Array<SkinEntry> getAttachments () {
+		return attachments.orderedItems();
+	}
+
+	/** Returns all attachments in this skin for the specified slot index. */
+	public void getAttachments (int slotIndex, Array<SkinEntry> attachments) {
+		if (slotIndex < 0) throw new IllegalArgumentException("slotIndex must be >= 0.");
+		if (attachments == null) throw new IllegalArgumentException("attachments cannot be null.");
+		for (SkinEntry entry : this.attachments.orderedItems())
+			if (entry.slotIndex == slotIndex) attachments.add(entry);
+	}
+
+	/** Clears all attachments, bones, and constraints. */
+	public void clear () {
+		attachments.clear(1024);
+		bones.clear();
+		constraints.clear();
+	}
+
+	public Array<BoneData> getBones () {
+		return bones;
+	}
+
+	public Array<ConstraintData> getConstraints () {
+		return constraints;
+	}
+
+	/** The skin's name, which is unique across all skins in the skeleton. */
+	public String getName () {
+		return name;
+	}
+
+	public String toString () {
+		return name;
+	}
+
+	/** Attach each attachment in this skin if the corresponding attachment in the old skin is currently attached. */
+	void attachAll (Skeleton skeleton, Skin oldSkin) {
+		Object[] slots = skeleton.slots.items;
+		for (SkinEntry entry : oldSkin.attachments.orderedItems()) {
+			int slotIndex = entry.slotIndex;
+			Slot slot = (Slot)slots[slotIndex];
+			if (slot.attachment == entry.attachment) {
+				Attachment attachment = getAttachment(slotIndex, entry.name);
+				if (attachment != null) slot.setAttachment(attachment);
+			}
+		}
+	}
+
+	/** Stores an entry in the skin consisting of the slot index and the attachment name. */
+	static public class SkinEntry {
+		int slotIndex;
+		String name;
+		@Null Attachment attachment;
+		private int hashCode;
+
+		SkinEntry (int slotIndex, String name, @Null Attachment attachment) {
+			set(slotIndex, name);
+			this.attachment = attachment;
+		}
+
+		void set (int slotIndex, String name) {
+			if (slotIndex < 0) throw new IllegalArgumentException("slotIndex must be >= 0.");
+			if (name == null) throw new IllegalArgumentException("name cannot be null.");
+			this.slotIndex = slotIndex;
+			this.name = name;
+			hashCode = name.hashCode() + slotIndex * 37;
+		}
+
+		public int getSlotIndex () {
+			return slotIndex;
+		}
+
+		/** The name the attachment is associated with, equivalent to the skin placeholder name in the Spine editor. */
+		public String getName () {
+			return name;
+		}
+
+		public Attachment getAttachment () {
+			return attachment;
+		}
+
+		public int hashCode () {
+			return hashCode;
+		}
+
+		public boolean equals (Object object) {
+			if (object == null) return false;
+			SkinEntry other = (SkinEntry)object;
+			if (slotIndex != other.slotIndex) return false;
+			return name.equals(other.name);
+		}
+
+		public String toString () {
+			return slotIndex + ":" + name;
+		}
+	}
+}

+ 159 - 159
spine-libgdx/spine-libgdx/src/com/esotericsoftware/spine/Slot.java

@@ -1,159 +1,159 @@
-/******************************************************************************
- * Spine Runtimes License Agreement
- * Last updated January 1, 2020. Replaces all prior versions.
- *
- * Copyright (c) 2013-2020, Esoteric Software LLC
- *
- * Integration of the Spine Runtimes into software or otherwise creating
- * derivative works of the Spine Runtimes is permitted under the terms and
- * conditions of Section 2 of the Spine Editor License Agreement:
- * http://esotericsoftware.com/spine-editor-license
- *
- * Otherwise, it is permitted to integrate the Spine Runtimes into software
- * or otherwise create derivative works of the Spine Runtimes (collectively,
- * "Products"), provided that each user of the Products must obtain their own
- * Spine Editor license and redistribution of the Products in any form must
- * include this license and copyright notice.
- *
- * THE SPINE RUNTIMES ARE PROVIDED BY ESOTERIC SOFTWARE LLC "AS IS" AND ANY
- * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
- * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
- * DISCLAIMED. IN NO EVENT SHALL ESOTERIC SOFTWARE LLC BE LIABLE FOR ANY
- * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
- * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES,
- * BUSINESS INTERRUPTION, OR LOSS OF USE, DATA, OR PROFITS) HOWEVER CAUSED AND
- * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
- * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
- * THE SPINE RUNTIMES, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
- *****************************************************************************/
-
-package com.esotericsoftware.spine;
-
-import com.badlogic.gdx.graphics.Color;
-import com.badlogic.gdx.utils.FloatArray;
-import com.badlogic.gdx.utils.Null;
-
-import com.esotericsoftware.spine.Animation.DeformTimeline;
-import com.esotericsoftware.spine.attachments.Attachment;
-import com.esotericsoftware.spine.attachments.VertexAttachment;
-
-/** Stores a slot's current pose. Slots organize attachments for {@link Skeleton#drawOrder} purposes and provide a place to store
- * state for an attachment. State cannot be stored in an attachment itself because attachments are stateless and may be shared
- * across multiple skeletons. */
-public class Slot {
-	final SlotData data;
-	final Bone bone;
-	final Color color = new Color();
-	@Null final Color darkColor;
-	@Null Attachment attachment;
-	private float attachmentTime;
-	private FloatArray deform = new FloatArray();
-
-	int attachmentState;
-
-	public Slot (SlotData data, Bone bone) {
-		if (data == null) throw new IllegalArgumentException("data cannot be null.");
-		if (bone == null) throw new IllegalArgumentException("bone cannot be null.");
-		this.data = data;
-		this.bone = bone;
-		darkColor = data.darkColor == null ? null : new Color();
-		setToSetupPose();
-	}
-
-	/** Copy constructor. */
-	public Slot (Slot slot, Bone bone) {
-		if (slot == null) throw new IllegalArgumentException("slot cannot be null.");
-		if (bone == null) throw new IllegalArgumentException("bone cannot be null.");
-		data = slot.data;
-		this.bone = bone;
-		color.set(slot.color);
-		darkColor = slot.darkColor == null ? null : new Color(slot.darkColor);
-		attachment = slot.attachment;
-		attachmentTime = slot.attachmentTime;
-		deform.addAll(slot.deform);
-	}
-
-	/** The slot's setup pose data. */
-	public SlotData getData () {
-		return data;
-	}
-
-	/** The bone this slot belongs to. */
-	public Bone getBone () {
-		return bone;
-	}
-
-	/** The skeleton this slot belongs to. */
-	public Skeleton getSkeleton () {
-		return bone.skeleton;
-	}
-
-	/** The color used to tint the slot's attachment. If {@link #getDarkColor()} is set, this is used as the light color for two
-	 * color tinting. */
-	public Color getColor () {
-		return color;
-	}
-
-	/** The dark color used to tint the slot's attachment for two color tinting, or null if two color tinting is not used. The dark
-	 * color's alpha is not used. */
-	public @Null Color getDarkColor () {
-		return darkColor;
-	}
-
-	/** The current attachment for the slot, or null if the slot has no attachment. */
-	public @Null Attachment getAttachment () {
-		return attachment;
-	}
-
-	/** Sets the slot's attachment and, if the attachment changed, resets {@link #attachmentTime} and clears the {@link #deform}.
-	 * The deform is not cleared if the old attachment has the same {@link VertexAttachment#getDeformAttachment()} as the specified
-	 * attachment. */
-	public void setAttachment (@Null Attachment attachment) {
-		if (this.attachment == attachment) return;
-		if (!(attachment instanceof VertexAttachment) || !(this.attachment instanceof VertexAttachment)
-			|| ((VertexAttachment)attachment).getDeformAttachment() != ((VertexAttachment)this.attachment).getDeformAttachment()) {
-			deform.clear();
-		}
-		this.attachment = attachment;
-		attachmentTime = bone.skeleton.time;
-	}
-
-	/** The time that has elapsed since the last time the attachment was set or cleared. Relies on Skeleton
-	 * {@link Skeleton#time}. */
-	public float getAttachmentTime () {
-		return bone.skeleton.time - attachmentTime;
-	}
-
-	public void setAttachmentTime (float time) {
-		attachmentTime = bone.skeleton.time - time;
-	}
-
-	/** Values to deform the slot's attachment. For an unweighted mesh, the entries are local positions for each vertex. For a
-	 * weighted mesh, the entries are an offset for each vertex which will be added to the mesh's local vertex positions.
-	 * <p>
-	 * See {@link VertexAttachment#computeWorldVertices(Slot, int, int, float[], int, int)} and {@link DeformTimeline}. */
-	public FloatArray getDeform () {
-		return deform;
-	}
-
-	public void setDeform (FloatArray deform) {
-		if (deform == null) throw new IllegalArgumentException("deform cannot be null.");
-		this.deform = deform;
-	}
-
-	/** Sets this slot to the setup pose. */
-	public void setToSetupPose () {
-		color.set(data.color);
-		if (darkColor != null) darkColor.set(data.darkColor);
-		if (data.attachmentName == null)
-			setAttachment(null);
-		else {
-			attachment = null;
-			setAttachment(bone.skeleton.getAttachment(data.index, data.attachmentName));
-		}
-	}
-
-	public String toString () {
-		return data.name;
-	}
-}
+/******************************************************************************
+ * Spine Runtimes License Agreement
+ * Last updated January 1, 2020. Replaces all prior versions.
+ *
+ * Copyright (c) 2013-2020, Esoteric Software LLC
+ *
+ * Integration of the Spine Runtimes into software or otherwise creating
+ * derivative works of the Spine Runtimes is permitted under the terms and
+ * conditions of Section 2 of the Spine Editor License Agreement:
+ * http://esotericsoftware.com/spine-editor-license
+ *
+ * Otherwise, it is permitted to integrate the Spine Runtimes into software
+ * or otherwise create derivative works of the Spine Runtimes (collectively,
+ * "Products"), provided that each user of the Products must obtain their own
+ * Spine Editor license and redistribution of the Products in any form must
+ * include this license and copyright notice.
+ *
+ * THE SPINE RUNTIMES ARE PROVIDED BY ESOTERIC SOFTWARE LLC "AS IS" AND ANY
+ * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL ESOTERIC SOFTWARE LLC BE LIABLE FOR ANY
+ * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES,
+ * BUSINESS INTERRUPTION, OR LOSS OF USE, DATA, OR PROFITS) HOWEVER CAUSED AND
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
+ * THE SPINE RUNTIMES, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ *****************************************************************************/
+
+package com.esotericsoftware.spine;
+
+import com.badlogic.gdx.graphics.Color;
+import com.badlogic.gdx.utils.FloatArray;
+import com.badlogic.gdx.utils.Null;
+
+import com.esotericsoftware.spine.Animation.DeformTimeline;
+import com.esotericsoftware.spine.attachments.Attachment;
+import com.esotericsoftware.spine.attachments.VertexAttachment;
+
+/** Stores a slot's current pose. Slots organize attachments for {@link Skeleton#drawOrder} purposes and provide a place to store
+ * state for an attachment. State cannot be stored in an attachment itself because attachments are stateless and may be shared
+ * across multiple skeletons. */
+public class Slot {
+	final SlotData data;
+	final Bone bone;
+	final Color color = new Color();
+	@Null final Color darkColor;
+	@Null Attachment attachment;
+	private float attachmentTime;
+	private FloatArray deform = new FloatArray();
+
+	int attachmentState;
+
+	public Slot (SlotData data, Bone bone) {
+		if (data == null) throw new IllegalArgumentException("data cannot be null.");
+		if (bone == null) throw new IllegalArgumentException("bone cannot be null.");
+		this.data = data;
+		this.bone = bone;
+		darkColor = data.darkColor == null ? null : new Color();
+		setToSetupPose();
+	}
+
+	/** Copy constructor. */
+	public Slot (Slot slot, Bone bone) {
+		if (slot == null) throw new IllegalArgumentException("slot cannot be null.");
+		if (bone == null) throw new IllegalArgumentException("bone cannot be null.");
+		data = slot.data;
+		this.bone = bone;
+		color.set(slot.color);
+		darkColor = slot.darkColor == null ? null : new Color(slot.darkColor);
+		attachment = slot.attachment;
+		attachmentTime = slot.attachmentTime;
+		deform.addAll(slot.deform);
+	}
+
+	/** The slot's setup pose data. */
+	public SlotData getData () {
+		return data;
+	}
+
+	/** The bone this slot belongs to. */
+	public Bone getBone () {
+		return bone;
+	}
+
+	/** The skeleton this slot belongs to. */
+	public Skeleton getSkeleton () {
+		return bone.skeleton;
+	}
+
+	/** The color used to tint the slot's attachment. If {@link #getDarkColor()} is set, this is used as the light color for two
+	 * color tinting. */
+	public Color getColor () {
+		return color;
+	}
+
+	/** The dark color used to tint the slot's attachment for two color tinting, or null if two color tinting is not used. The dark
+	 * color's alpha is not used. */
+	public @Null Color getDarkColor () {
+		return darkColor;
+	}
+
+	/** The current attachment for the slot, or null if the slot has no attachment. */
+	public @Null Attachment getAttachment () {
+		return attachment;
+	}
+
+	/** Sets the slot's attachment and, if the attachment changed, resets {@link #attachmentTime} and clears the {@link #deform}.
+	 * The deform is not cleared if the old attachment has the same {@link VertexAttachment#getDeformAttachment()} as the specified
+	 * attachment. */
+	public void setAttachment (@Null Attachment attachment) {
+		if (this.attachment == attachment) return;
+		if (!(attachment instanceof VertexAttachment) || !(this.attachment instanceof VertexAttachment)
+			|| ((VertexAttachment)attachment).getDeformAttachment() != ((VertexAttachment)this.attachment).getDeformAttachment()) {
+			deform.clear();
+		}
+		this.attachment = attachment;
+		attachmentTime = bone.skeleton.time;
+	}
+
+	/** The time that has elapsed since the last time the attachment was set or cleared. Relies on Skeleton
+	 * {@link Skeleton#time}. */
+	public float getAttachmentTime () {
+		return bone.skeleton.time - attachmentTime;
+	}
+
+	public void setAttachmentTime (float time) {
+		attachmentTime = bone.skeleton.time - time;
+	}
+
+	/** Values to deform the slot's attachment. For an unweighted mesh, the entries are local positions for each vertex. For a
+	 * weighted mesh, the entries are an offset for each vertex which will be added to the mesh's local vertex positions.
+	 * <p>
+	 * See {@link VertexAttachment#computeWorldVertices(Slot, int, int, float[], int, int)} and {@link DeformTimeline}. */
+	public FloatArray getDeform () {
+		return deform;
+	}
+
+	public void setDeform (FloatArray deform) {
+		if (deform == null) throw new IllegalArgumentException("deform cannot be null.");
+		this.deform = deform;
+	}
+
+	/** Sets this slot to the setup pose. */
+	public void setToSetupPose () {
+		color.set(data.color);
+		if (darkColor != null) darkColor.set(data.darkColor);
+		if (data.attachmentName == null)
+			setAttachment(null);
+		else {
+			attachment = null;
+			setAttachment(bone.skeleton.getAttachment(data.index, data.attachmentName));
+		}
+	}
+
+	public String toString () {
+		return data.name;
+	}
+}

+ 107 - 107
spine-libgdx/spine-libgdx/src/com/esotericsoftware/spine/SlotData.java

@@ -1,107 +1,107 @@
-/******************************************************************************
- * Spine Runtimes License Agreement
- * Last updated January 1, 2020. Replaces all prior versions.
- *
- * Copyright (c) 2013-2020, Esoteric Software LLC
- *
- * Integration of the Spine Runtimes into software or otherwise creating
- * derivative works of the Spine Runtimes is permitted under the terms and
- * conditions of Section 2 of the Spine Editor License Agreement:
- * http://esotericsoftware.com/spine-editor-license
- *
- * Otherwise, it is permitted to integrate the Spine Runtimes into software
- * or otherwise create derivative works of the Spine Runtimes (collectively,
- * "Products"), provided that each user of the Products must obtain their own
- * Spine Editor license and redistribution of the Products in any form must
- * include this license and copyright notice.
- *
- * THE SPINE RUNTIMES ARE PROVIDED BY ESOTERIC SOFTWARE LLC "AS IS" AND ANY
- * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
- * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
- * DISCLAIMED. IN NO EVENT SHALL ESOTERIC SOFTWARE LLC BE LIABLE FOR ANY
- * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
- * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES,
- * BUSINESS INTERRUPTION, OR LOSS OF USE, DATA, OR PROFITS) HOWEVER CAUSED AND
- * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
- * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
- * THE SPINE RUNTIMES, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
- *****************************************************************************/
-
-package com.esotericsoftware.spine;
-
-import com.badlogic.gdx.graphics.Color;
-import com.badlogic.gdx.utils.Null;
-
-/** Stores the setup pose for a {@link Slot}. */
-public class SlotData {
-	final int index;
-	final String name;
-	final BoneData boneData;
-	final Color color = new Color(1, 1, 1, 1);
-	@Null Color darkColor;
-	@Null String attachmentName;
-	BlendMode blendMode;
-
-	public SlotData (int index, String name, BoneData boneData) {
-		if (index < 0) throw new IllegalArgumentException("index must be >= 0.");
-		if (name == null) throw new IllegalArgumentException("name cannot be null.");
-		if (boneData == null) throw new IllegalArgumentException("boneData cannot be null.");
-		this.index = index;
-		this.name = name;
-		this.boneData = boneData;
-	}
-
-	/** The index of the slot in {@link Skeleton#getSlots()}. */
-	public int getIndex () {
-		return index;
-	}
-
-	/** The name of the slot, which is unique across all slots in the skeleton. */
-	public String getName () {
-		return name;
-	}
-
-	/** The bone this slot belongs to. */
-	public BoneData getBoneData () {
-		return boneData;
-	}
-
-	/** The color used to tint the slot's attachment. If {@link #getDarkColor()} is set, this is used as the light color for two
-	 * color tinting. */
-	public Color getColor () {
-		return color;
-	}
-
-	/** The dark color used to tint the slot's attachment for two color tinting, or null if two color tinting is not used. The dark
-	 * color's alpha is not used. */
-	public @Null Color getDarkColor () {
-		return darkColor;
-	}
-
-	public void setDarkColor (@Null Color darkColor) {
-		this.darkColor = darkColor;
-	}
-
-	public void setAttachmentName (@Null String attachmentName) {
-		this.attachmentName = attachmentName;
-	}
-
-	/** The name of the attachment that is visible for this slot in the setup pose, or null if no attachment is visible. */
-	public @Null String getAttachmentName () {
-		return attachmentName;
-	}
-
-	/** The blend mode for drawing the slot's attachment. */
-	public BlendMode getBlendMode () {
-		return blendMode;
-	}
-
-	public void setBlendMode (BlendMode blendMode) {
-		if (blendMode == null) throw new IllegalArgumentException("blendMode cannot be null.");
-		this.blendMode = blendMode;
-	}
-
-	public String toString () {
-		return name;
-	}
-}
+/******************************************************************************
+ * Spine Runtimes License Agreement
+ * Last updated January 1, 2020. Replaces all prior versions.
+ *
+ * Copyright (c) 2013-2020, Esoteric Software LLC
+ *
+ * Integration of the Spine Runtimes into software or otherwise creating
+ * derivative works of the Spine Runtimes is permitted under the terms and
+ * conditions of Section 2 of the Spine Editor License Agreement:
+ * http://esotericsoftware.com/spine-editor-license
+ *
+ * Otherwise, it is permitted to integrate the Spine Runtimes into software
+ * or otherwise create derivative works of the Spine Runtimes (collectively,
+ * "Products"), provided that each user of the Products must obtain their own
+ * Spine Editor license and redistribution of the Products in any form must
+ * include this license and copyright notice.
+ *
+ * THE SPINE RUNTIMES ARE PROVIDED BY ESOTERIC SOFTWARE LLC "AS IS" AND ANY
+ * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL ESOTERIC SOFTWARE LLC BE LIABLE FOR ANY
+ * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES,
+ * BUSINESS INTERRUPTION, OR LOSS OF USE, DATA, OR PROFITS) HOWEVER CAUSED AND
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
+ * THE SPINE RUNTIMES, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ *****************************************************************************/
+
+package com.esotericsoftware.spine;
+
+import com.badlogic.gdx.graphics.Color;
+import com.badlogic.gdx.utils.Null;
+
+/** Stores the setup pose for a {@link Slot}. */
+public class SlotData {
+	final int index;
+	final String name;
+	final BoneData boneData;
+	final Color color = new Color(1, 1, 1, 1);
+	@Null Color darkColor;
+	@Null String attachmentName;
+	BlendMode blendMode;
+
+	public SlotData (int index, String name, BoneData boneData) {
+		if (index < 0) throw new IllegalArgumentException("index must be >= 0.");
+		if (name == null) throw new IllegalArgumentException("name cannot be null.");
+		if (boneData == null) throw new IllegalArgumentException("boneData cannot be null.");
+		this.index = index;
+		this.name = name;
+		this.boneData = boneData;
+	}
+
+	/** The index of the slot in {@link Skeleton#getSlots()}. */
+	public int getIndex () {
+		return index;
+	}
+
+	/** The name of the slot, which is unique across all slots in the skeleton. */
+	public String getName () {
+		return name;
+	}
+
+	/** The bone this slot belongs to. */
+	public BoneData getBoneData () {
+		return boneData;
+	}
+
+	/** The color used to tint the slot's attachment. If {@link #getDarkColor()} is set, this is used as the light color for two
+	 * color tinting. */
+	public Color getColor () {
+		return color;
+	}
+
+	/** The dark color used to tint the slot's attachment for two color tinting, or null if two color tinting is not used. The dark
+	 * color's alpha is not used. */
+	public @Null Color getDarkColor () {
+		return darkColor;
+	}
+
+	public void setDarkColor (@Null Color darkColor) {
+		this.darkColor = darkColor;
+	}
+
+	public void setAttachmentName (@Null String attachmentName) {
+		this.attachmentName = attachmentName;
+	}
+
+	/** The name of the attachment that is visible for this slot in the setup pose, or null if no attachment is visible. */
+	public @Null String getAttachmentName () {
+		return attachmentName;
+	}
+
+	/** The blend mode for drawing the slot's attachment. */
+	public BlendMode getBlendMode () {
+		return blendMode;
+	}
+
+	public void setBlendMode (BlendMode blendMode) {
+		if (blendMode == null) throw new IllegalArgumentException("blendMode cannot be null.");
+		this.blendMode = blendMode;
+	}
+
+	public String toString () {
+		return name;
+	}
+}

+ 370 - 370
spine-libgdx/spine-libgdx/src/com/esotericsoftware/spine/TransformConstraint.java

@@ -1,370 +1,370 @@
-/******************************************************************************
- * Spine Runtimes License Agreement
- * Last updated January 1, 2020. Replaces all prior versions.
- *
- * Copyright (c) 2013-2020, Esoteric Software LLC
- *
- * Integration of the Spine Runtimes into software or otherwise creating
- * derivative works of the Spine Runtimes is permitted under the terms and
- * conditions of Section 2 of the Spine Editor License Agreement:
- * http://esotericsoftware.com/spine-editor-license
- *
- * Otherwise, it is permitted to integrate the Spine Runtimes into software
- * or otherwise create derivative works of the Spine Runtimes (collectively,
- * "Products"), provided that each user of the Products must obtain their own
- * Spine Editor license and redistribution of the Products in any form must
- * include this license and copyright notice.
- *
- * THE SPINE RUNTIMES ARE PROVIDED BY ESOTERIC SOFTWARE LLC "AS IS" AND ANY
- * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
- * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
- * DISCLAIMED. IN NO EVENT SHALL ESOTERIC SOFTWARE LLC BE LIABLE FOR ANY
- * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
- * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES,
- * BUSINESS INTERRUPTION, OR LOSS OF USE, DATA, OR PROFITS) HOWEVER CAUSED AND
- * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
- * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
- * THE SPINE RUNTIMES, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
- *****************************************************************************/
-
-package com.esotericsoftware.spine;
-
-import static com.esotericsoftware.spine.utils.SpineUtils.*;
-
-import com.badlogic.gdx.math.Vector2;
-import com.badlogic.gdx.utils.Array;
-
-/** Stores the current pose for a transform constraint. A transform constraint adjusts the world transform of the constrained
- * bones to match that of the target bone.
- * <p>
- * See <a href="http://esotericsoftware.com/spine-transform-constraints">Transform constraints</a> in the Spine User Guide. */
-public class TransformConstraint implements Updatable {
-	final TransformConstraintData data;
-	final Array<Bone> bones;
-	Bone target;
-	float mixRotate, mixX, mixY, mixScaleX, mixScaleY, mixShearY;
-
-	boolean active;
-	final Vector2 temp = new Vector2();
-
-	public TransformConstraint (TransformConstraintData data, Skeleton skeleton) {
-		if (data == null) throw new IllegalArgumentException("data cannot be null.");
-		if (skeleton == null) throw new IllegalArgumentException("skeleton cannot be null.");
-		this.data = data;
-		mixRotate = data.mixRotate;
-		mixX = data.mixX;
-		mixY = data.mixY;
-		mixScaleX = data.mixScaleX;
-		mixScaleY = data.mixScaleY;
-		mixShearY = data.mixShearY;
-		bones = new Array(data.bones.size);
-		for (BoneData boneData : data.bones)
-			bones.add(skeleton.findBone(boneData.name));
-		target = skeleton.findBone(data.target.name);
-	}
-
-	/** Copy constructor. */
-	public TransformConstraint (TransformConstraint constraint, Skeleton skeleton) {
-		if (constraint == null) throw new IllegalArgumentException("constraint cannot be null.");
-		if (skeleton == null) throw new IllegalArgumentException("skeleton cannot be null.");
-		data = constraint.data;
-		bones = new Array(constraint.bones.size);
-		for (Bone bone : constraint.bones)
-			bones.add(skeleton.bones.get(bone.data.index));
-		target = skeleton.bones.get(constraint.target.data.index);
-		mixRotate = constraint.mixRotate;
-		mixX = constraint.mixX;
-		mixY = constraint.mixY;
-		mixScaleX = constraint.mixScaleX;
-		mixScaleY = constraint.mixScaleY;
-		mixShearY = constraint.mixShearY;
-	}
-
-	/** Applies the constraint to the constrained bones. */
-	public void update () {
-		if (mixRotate == 0 && mixX == 0 && mixY == 0 && mixScaleX == 0 && mixScaleX == 0 && mixShearY == 0) return;
-		if (data.local) {
-			if (data.relative)
-				applyRelativeLocal();
-			else
-				applyAbsoluteLocal();
-		} else {
-			if (data.relative)
-				applyRelativeWorld();
-			else
-				applyAbsoluteWorld();
-		}
-	}
-
-	private void applyAbsoluteWorld () {
-		float mixRotate = this.mixRotate, mixX = this.mixX, mixY = this.mixY, mixScaleX = this.mixScaleX,
-			mixScaleY = this.mixScaleY, mixShearY = this.mixShearY;
-		boolean translate = mixX != 0 || mixY != 0;
-
-		Bone target = this.target;
-		float ta = target.a, tb = target.b, tc = target.c, td = target.d;
-		float degRadReflect = ta * td - tb * tc > 0 ? degRad : -degRad;
-		float offsetRotation = data.offsetRotation * degRadReflect, offsetShearY = data.offsetShearY * degRadReflect;
-
-		Object[] bones = this.bones.items;
-		for (int i = 0, n = this.bones.size; i < n; i++) {
-			Bone bone = (Bone)bones[i];
-
-			if (mixRotate != 0) {
-				float a = bone.a, b = bone.b, c = bone.c, d = bone.d;
-				float r = atan2(tc, ta) - atan2(c, a) + offsetRotation;
-				if (r > PI)
-					r -= PI2;
-				else if (r < -PI) //
-					r += PI2;
-				r *= mixRotate;
-				float cos = cos(r), sin = sin(r);
-				bone.a = cos * a - sin * c;
-				bone.b = cos * b - sin * d;
-				bone.c = sin * a + cos * c;
-				bone.d = sin * b + cos * d;
-			}
-
-			if (translate) {
-				Vector2 temp = this.temp;
-				target.localToWorld(temp.set(data.offsetX, data.offsetY));
-				bone.worldX += (temp.x - bone.worldX) * mixX;
-				bone.worldY += (temp.y - bone.worldY) * mixY;
-			}
-
-			if (mixScaleX != 0) {
-				float s = (float)Math.sqrt(bone.a * bone.a + bone.c * bone.c);
-				if (s != 0) s = (s + ((float)Math.sqrt(ta * ta + tc * tc) - s + data.offsetScaleX) * mixScaleX) / s;
-				bone.a *= s;
-				bone.c *= s;
-			}
-			if (mixScaleY != 0) {
-				float s = (float)Math.sqrt(bone.b * bone.b + bone.d * bone.d);
-				if (s != 0) s = (s + ((float)Math.sqrt(tb * tb + td * td) - s + data.offsetScaleY) * mixScaleY) / s;
-				bone.b *= s;
-				bone.d *= s;
-			}
-
-			if (mixShearY > 0) {
-				float b = bone.b, d = bone.d;
-				float by = atan2(d, b);
-				float r = atan2(td, tb) - atan2(tc, ta) - (by - atan2(bone.c, bone.a));
-				if (r > PI)
-					r -= PI2;
-				else if (r < -PI) //
-					r += PI2;
-				r = by + (r + offsetShearY) * mixShearY;
-				float s = (float)Math.sqrt(b * b + d * d);
-				bone.b = cos(r) * s;
-				bone.d = sin(r) * s;
-			}
-
-			bone.updateAppliedTransform();
-		}
-	}
-
-	private void applyRelativeWorld () {
-		float mixRotate = this.mixRotate, mixX = this.mixX, mixY = this.mixY, mixScaleX = this.mixScaleX,
-			mixScaleY = this.mixScaleY, mixShearY = this.mixShearY;
-		boolean translate = mixX != 0 || mixY != 0;
-
-		Bone target = this.target;
-		float ta = target.a, tb = target.b, tc = target.c, td = target.d;
-		float degRadReflect = ta * td - tb * tc > 0 ? degRad : -degRad;
-		float offsetRotation = data.offsetRotation * degRadReflect, offsetShearY = data.offsetShearY * degRadReflect;
-
-		Object[] bones = this.bones.items;
-		for (int i = 0, n = this.bones.size; i < n; i++) {
-			Bone bone = (Bone)bones[i];
-
-			if (mixRotate != 0) {
-				float a = bone.a, b = bone.b, c = bone.c, d = bone.d;
-				float r = atan2(tc, ta) + offsetRotation;
-				if (r > PI)
-					r -= PI2;
-				else if (r < -PI) //
-					r += PI2;
-				r *= mixRotate;
-				float cos = cos(r), sin = sin(r);
-				bone.a = cos * a - sin * c;
-				bone.b = cos * b - sin * d;
-				bone.c = sin * a + cos * c;
-				bone.d = sin * b + cos * d;
-			}
-
-			if (translate) {
-				Vector2 temp = this.temp;
-				target.localToWorld(temp.set(data.offsetX, data.offsetY));
-				bone.worldX += temp.x * mixX;
-				bone.worldY += temp.y * mixY;
-			}
-
-			if (mixScaleX != 0) {
-				float s = ((float)Math.sqrt(ta * ta + tc * tc) - 1 + data.offsetScaleX) * mixScaleX + 1;
-				bone.a *= s;
-				bone.c *= s;
-			}
-			if (mixScaleY != 0) {
-				float s = ((float)Math.sqrt(tb * tb + td * td) - 1 + data.offsetScaleY) * mixScaleY + 1;
-				bone.b *= s;
-				bone.d *= s;
-			}
-
-			if (mixShearY > 0) {
-				float r = atan2(td, tb) - atan2(tc, ta);
-				if (r > PI)
-					r -= PI2;
-				else if (r < -PI) //
-					r += PI2;
-				float b = bone.b, d = bone.d;
-				r = atan2(d, b) + (r - PI / 2 + offsetShearY) * mixShearY;
-				float s = (float)Math.sqrt(b * b + d * d);
-				bone.b = cos(r) * s;
-				bone.d = sin(r) * s;
-			}
-
-			bone.updateAppliedTransform();
-		}
-	}
-
-	private void applyAbsoluteLocal () {
-		float mixRotate = this.mixRotate, mixX = this.mixX, mixY = this.mixY, mixScaleX = this.mixScaleX,
-			mixScaleY = this.mixScaleY, mixShearY = this.mixShearY;
-
-		Bone target = this.target;
-
-		Object[] bones = this.bones.items;
-		for (int i = 0, n = this.bones.size; i < n; i++) {
-			Bone bone = (Bone)bones[i];
-
-			float rotation = bone.arotation;
-			if (mixRotate != 0) {
-				float r = target.arotation - rotation + data.offsetRotation;
-				r -= (16384 - (int)(16384.499999999996 - r / 360)) * 360;
-				rotation += r * mixRotate;
-			}
-
-			float x = bone.ax, y = bone.ay;
-			x += (target.ax - x + data.offsetX) * mixX;
-			y += (target.ay - y + data.offsetY) * mixY;
-
-			float scaleX = bone.ascaleX, scaleY = bone.ascaleY;
-			if (mixScaleX != 0 && scaleX != 0)
-				scaleX = (scaleX + (target.ascaleX - scaleX + data.offsetScaleX) * mixScaleX) / scaleX;
-			if (mixScaleY != 0 && scaleY != 0)
-				scaleY = (scaleY + (target.ascaleY - scaleY + data.offsetScaleY) * mixScaleY) / scaleY;
-
-			float shearY = bone.ashearY;
-			if (mixShearY != 0) {
-				float r = target.ashearY - shearY + data.offsetShearY;
-				r -= (16384 - (int)(16384.499999999996 - r / 360)) * 360;
-				shearY += r * mixShearY;
-			}
-
-			bone.updateWorldTransform(x, y, rotation, scaleX, scaleY, bone.ashearX, shearY);
-		}
-	}
-
-	private void applyRelativeLocal () {
-		float mixRotate = this.mixRotate, mixX = this.mixX, mixY = this.mixY, mixScaleX = this.mixScaleX,
-			mixScaleY = this.mixScaleY, mixShearY = this.mixShearY;
-
-		Bone target = this.target;
-
-		Object[] bones = this.bones.items;
-		for (int i = 0, n = this.bones.size; i < n; i++) {
-			Bone bone = (Bone)bones[i];
-
-			float rotation = bone.arotation + (target.arotation + data.offsetRotation) * mixRotate;
-			float x = bone.ax + (target.ax + data.offsetX) * mixX;
-			float y = bone.ay + (target.ay + data.offsetY) * mixY;
-			float scaleX = (bone.ascaleX * ((target.ascaleX - 1 + data.offsetScaleX) * mixScaleX) + 1);
-			float scaleY = (bone.ascaleY * ((target.ascaleY - 1 + data.offsetScaleY) * mixScaleY) + 1);
-			float shearY = bone.ashearY + (target.ashearY + data.offsetShearY) * mixShearY;
-
-			bone.updateWorldTransform(x, y, rotation, scaleX, scaleY, bone.ashearX, shearY);
-		}
-	}
-
-	/** The bones that will be modified by this transform constraint. */
-	public Array<Bone> getBones () {
-		return bones;
-	}
-
-	/** The target bone whose world transform will be copied to the constrained bones. */
-	public Bone getTarget () {
-		return target;
-	}
-
-	public void setTarget (Bone target) {
-		if (target == null) throw new IllegalArgumentException("target cannot be null.");
-		this.target = target;
-	}
-
-	/** A percentage (0-1) that controls the mix between the constrained and unconstrained rotation. */
-	public float getMixRotate () {
-		return mixRotate;
-	}
-
-	public void setMixRotate (float mixRotate) {
-		this.mixRotate = mixRotate;
-	}
-
-	/** A percentage (0-1) that controls the mix between the constrained and unconstrained translation X. */
-	public float getMixX () {
-		return mixX;
-	}
-
-	public void setMixX (float mixX) {
-		this.mixX = mixX;
-	}
-
-	/** A percentage (0-1) that controls the mix between the constrained and unconstrained translation Y. */
-	public float getMixY () {
-		return mixY;
-	}
-
-	public void setMixY (float mixY) {
-		this.mixY = mixY;
-	}
-
-	/** A percentage (0-1) that controls the mix between the constrained and unconstrained scale X. */
-	public float getMixScaleX () {
-		return mixScaleX;
-	}
-
-	public void setMixScaleX (float mixScaleX) {
-		this.mixScaleX = mixScaleX;
-	}
-
-	/** A percentage (0-1) that controls the mix between the constrained and unconstrained scale X. */
-	public float getMixScaleY () {
-		return mixScaleY;
-	}
-
-	public void setMixScaleY (float mixScaleY) {
-		this.mixScaleY = mixScaleY;
-	}
-
-	/** A percentage (0-1) that controls the mix between the constrained and unconstrained shear Y. */
-	public float getMixShearY () {
-		return mixShearY;
-	}
-
-	public void setMixShearY (float mixShearY) {
-		this.mixShearY = mixShearY;
-	}
-
-	public boolean isActive () {
-		return active;
-	}
-
-	/** The transform constraint's setup pose data. */
-	public TransformConstraintData getData () {
-		return data;
-	}
-
-	public String toString () {
-		return data.name;
-	}
-}
+/******************************************************************************
+ * Spine Runtimes License Agreement
+ * Last updated January 1, 2020. Replaces all prior versions.
+ *
+ * Copyright (c) 2013-2020, Esoteric Software LLC
+ *
+ * Integration of the Spine Runtimes into software or otherwise creating
+ * derivative works of the Spine Runtimes is permitted under the terms and
+ * conditions of Section 2 of the Spine Editor License Agreement:
+ * http://esotericsoftware.com/spine-editor-license
+ *
+ * Otherwise, it is permitted to integrate the Spine Runtimes into software
+ * or otherwise create derivative works of the Spine Runtimes (collectively,
+ * "Products"), provided that each user of the Products must obtain their own
+ * Spine Editor license and redistribution of the Products in any form must
+ * include this license and copyright notice.
+ *
+ * THE SPINE RUNTIMES ARE PROVIDED BY ESOTERIC SOFTWARE LLC "AS IS" AND ANY
+ * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL ESOTERIC SOFTWARE LLC BE LIABLE FOR ANY
+ * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES,
+ * BUSINESS INTERRUPTION, OR LOSS OF USE, DATA, OR PROFITS) HOWEVER CAUSED AND
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
+ * THE SPINE RUNTIMES, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ *****************************************************************************/
+
+package com.esotericsoftware.spine;
+
+import static com.esotericsoftware.spine.utils.SpineUtils.*;
+
+import com.badlogic.gdx.math.Vector2;
+import com.badlogic.gdx.utils.Array;
+
+/** Stores the current pose for a transform constraint. A transform constraint adjusts the world transform of the constrained
+ * bones to match that of the target bone.
+ * <p>
+ * See <a href="http://esotericsoftware.com/spine-transform-constraints">Transform constraints</a> in the Spine User Guide. */
+public class TransformConstraint implements Updatable {
+	final TransformConstraintData data;
+	final Array<Bone> bones;
+	Bone target;
+	float mixRotate, mixX, mixY, mixScaleX, mixScaleY, mixShearY;
+
+	boolean active;
+	final Vector2 temp = new Vector2();
+
+	public TransformConstraint (TransformConstraintData data, Skeleton skeleton) {
+		if (data == null) throw new IllegalArgumentException("data cannot be null.");
+		if (skeleton == null) throw new IllegalArgumentException("skeleton cannot be null.");
+		this.data = data;
+		mixRotate = data.mixRotate;
+		mixX = data.mixX;
+		mixY = data.mixY;
+		mixScaleX = data.mixScaleX;
+		mixScaleY = data.mixScaleY;
+		mixShearY = data.mixShearY;
+		bones = new Array(data.bones.size);
+		for (BoneData boneData : data.bones)
+			bones.add(skeleton.findBone(boneData.name));
+		target = skeleton.findBone(data.target.name);
+	}
+
+	/** Copy constructor. */
+	public TransformConstraint (TransformConstraint constraint, Skeleton skeleton) {
+		if (constraint == null) throw new IllegalArgumentException("constraint cannot be null.");
+		if (skeleton == null) throw new IllegalArgumentException("skeleton cannot be null.");
+		data = constraint.data;
+		bones = new Array(constraint.bones.size);
+		for (Bone bone : constraint.bones)
+			bones.add(skeleton.bones.get(bone.data.index));
+		target = skeleton.bones.get(constraint.target.data.index);
+		mixRotate = constraint.mixRotate;
+		mixX = constraint.mixX;
+		mixY = constraint.mixY;
+		mixScaleX = constraint.mixScaleX;
+		mixScaleY = constraint.mixScaleY;
+		mixShearY = constraint.mixShearY;
+	}
+
+	/** Applies the constraint to the constrained bones. */
+	public void update () {
+		if (mixRotate == 0 && mixX == 0 && mixY == 0 && mixScaleX == 0 && mixScaleX == 0 && mixShearY == 0) return;
+		if (data.local) {
+			if (data.relative)
+				applyRelativeLocal();
+			else
+				applyAbsoluteLocal();
+		} else {
+			if (data.relative)
+				applyRelativeWorld();
+			else
+				applyAbsoluteWorld();
+		}
+	}
+
+	private void applyAbsoluteWorld () {
+		float mixRotate = this.mixRotate, mixX = this.mixX, mixY = this.mixY, mixScaleX = this.mixScaleX,
+			mixScaleY = this.mixScaleY, mixShearY = this.mixShearY;
+		boolean translate = mixX != 0 || mixY != 0;
+
+		Bone target = this.target;
+		float ta = target.a, tb = target.b, tc = target.c, td = target.d;
+		float degRadReflect = ta * td - tb * tc > 0 ? degRad : -degRad;
+		float offsetRotation = data.offsetRotation * degRadReflect, offsetShearY = data.offsetShearY * degRadReflect;
+
+		Object[] bones = this.bones.items;
+		for (int i = 0, n = this.bones.size; i < n; i++) {
+			Bone bone = (Bone)bones[i];
+
+			if (mixRotate != 0) {
+				float a = bone.a, b = bone.b, c = bone.c, d = bone.d;
+				float r = atan2(tc, ta) - atan2(c, a) + offsetRotation;
+				if (r > PI)
+					r -= PI2;
+				else if (r < -PI) //
+					r += PI2;
+				r *= mixRotate;
+				float cos = cos(r), sin = sin(r);
+				bone.a = cos * a - sin * c;
+				bone.b = cos * b - sin * d;
+				bone.c = sin * a + cos * c;
+				bone.d = sin * b + cos * d;
+			}
+
+			if (translate) {
+				Vector2 temp = this.temp;
+				target.localToWorld(temp.set(data.offsetX, data.offsetY));
+				bone.worldX += (temp.x - bone.worldX) * mixX;
+				bone.worldY += (temp.y - bone.worldY) * mixY;
+			}
+
+			if (mixScaleX != 0) {
+				float s = (float)Math.sqrt(bone.a * bone.a + bone.c * bone.c);
+				if (s != 0) s = (s + ((float)Math.sqrt(ta * ta + tc * tc) - s + data.offsetScaleX) * mixScaleX) / s;
+				bone.a *= s;
+				bone.c *= s;
+			}
+			if (mixScaleY != 0) {
+				float s = (float)Math.sqrt(bone.b * bone.b + bone.d * bone.d);
+				if (s != 0) s = (s + ((float)Math.sqrt(tb * tb + td * td) - s + data.offsetScaleY) * mixScaleY) / s;
+				bone.b *= s;
+				bone.d *= s;
+			}
+
+			if (mixShearY > 0) {
+				float b = bone.b, d = bone.d;
+				float by = atan2(d, b);
+				float r = atan2(td, tb) - atan2(tc, ta) - (by - atan2(bone.c, bone.a));
+				if (r > PI)
+					r -= PI2;
+				else if (r < -PI) //
+					r += PI2;
+				r = by + (r + offsetShearY) * mixShearY;
+				float s = (float)Math.sqrt(b * b + d * d);
+				bone.b = cos(r) * s;
+				bone.d = sin(r) * s;
+			}
+
+			bone.updateAppliedTransform();
+		}
+	}
+
+	private void applyRelativeWorld () {
+		float mixRotate = this.mixRotate, mixX = this.mixX, mixY = this.mixY, mixScaleX = this.mixScaleX,
+			mixScaleY = this.mixScaleY, mixShearY = this.mixShearY;
+		boolean translate = mixX != 0 || mixY != 0;
+
+		Bone target = this.target;
+		float ta = target.a, tb = target.b, tc = target.c, td = target.d;
+		float degRadReflect = ta * td - tb * tc > 0 ? degRad : -degRad;
+		float offsetRotation = data.offsetRotation * degRadReflect, offsetShearY = data.offsetShearY * degRadReflect;
+
+		Object[] bones = this.bones.items;
+		for (int i = 0, n = this.bones.size; i < n; i++) {
+			Bone bone = (Bone)bones[i];
+
+			if (mixRotate != 0) {
+				float a = bone.a, b = bone.b, c = bone.c, d = bone.d;
+				float r = atan2(tc, ta) + offsetRotation;
+				if (r > PI)
+					r -= PI2;
+				else if (r < -PI) //
+					r += PI2;
+				r *= mixRotate;
+				float cos = cos(r), sin = sin(r);
+				bone.a = cos * a - sin * c;
+				bone.b = cos * b - sin * d;
+				bone.c = sin * a + cos * c;
+				bone.d = sin * b + cos * d;
+			}
+
+			if (translate) {
+				Vector2 temp = this.temp;
+				target.localToWorld(temp.set(data.offsetX, data.offsetY));
+				bone.worldX += temp.x * mixX;
+				bone.worldY += temp.y * mixY;
+			}
+
+			if (mixScaleX != 0) {
+				float s = ((float)Math.sqrt(ta * ta + tc * tc) - 1 + data.offsetScaleX) * mixScaleX + 1;
+				bone.a *= s;
+				bone.c *= s;
+			}
+			if (mixScaleY != 0) {
+				float s = ((float)Math.sqrt(tb * tb + td * td) - 1 + data.offsetScaleY) * mixScaleY + 1;
+				bone.b *= s;
+				bone.d *= s;
+			}
+
+			if (mixShearY > 0) {
+				float r = atan2(td, tb) - atan2(tc, ta);
+				if (r > PI)
+					r -= PI2;
+				else if (r < -PI) //
+					r += PI2;
+				float b = bone.b, d = bone.d;
+				r = atan2(d, b) + (r - PI / 2 + offsetShearY) * mixShearY;
+				float s = (float)Math.sqrt(b * b + d * d);
+				bone.b = cos(r) * s;
+				bone.d = sin(r) * s;
+			}
+
+			bone.updateAppliedTransform();
+		}
+	}
+
+	private void applyAbsoluteLocal () {
+		float mixRotate = this.mixRotate, mixX = this.mixX, mixY = this.mixY, mixScaleX = this.mixScaleX,
+			mixScaleY = this.mixScaleY, mixShearY = this.mixShearY;
+
+		Bone target = this.target;
+
+		Object[] bones = this.bones.items;
+		for (int i = 0, n = this.bones.size; i < n; i++) {
+			Bone bone = (Bone)bones[i];
+
+			float rotation = bone.arotation;
+			if (mixRotate != 0) {
+				float r = target.arotation - rotation + data.offsetRotation;
+				r -= (16384 - (int)(16384.499999999996 - r / 360)) * 360;
+				rotation += r * mixRotate;
+			}
+
+			float x = bone.ax, y = bone.ay;
+			x += (target.ax - x + data.offsetX) * mixX;
+			y += (target.ay - y + data.offsetY) * mixY;
+
+			float scaleX = bone.ascaleX, scaleY = bone.ascaleY;
+			if (mixScaleX != 0 && scaleX != 0)
+				scaleX = (scaleX + (target.ascaleX - scaleX + data.offsetScaleX) * mixScaleX) / scaleX;
+			if (mixScaleY != 0 && scaleY != 0)
+				scaleY = (scaleY + (target.ascaleY - scaleY + data.offsetScaleY) * mixScaleY) / scaleY;
+
+			float shearY = bone.ashearY;
+			if (mixShearY != 0) {
+				float r = target.ashearY - shearY + data.offsetShearY;
+				r -= (16384 - (int)(16384.499999999996 - r / 360)) * 360;
+				shearY += r * mixShearY;
+			}
+
+			bone.updateWorldTransform(x, y, rotation, scaleX, scaleY, bone.ashearX, shearY);
+		}
+	}
+
+	private void applyRelativeLocal () {
+		float mixRotate = this.mixRotate, mixX = this.mixX, mixY = this.mixY, mixScaleX = this.mixScaleX,
+			mixScaleY = this.mixScaleY, mixShearY = this.mixShearY;
+
+		Bone target = this.target;
+
+		Object[] bones = this.bones.items;
+		for (int i = 0, n = this.bones.size; i < n; i++) {
+			Bone bone = (Bone)bones[i];
+
+			float rotation = bone.arotation + (target.arotation + data.offsetRotation) * mixRotate;
+			float x = bone.ax + (target.ax + data.offsetX) * mixX;
+			float y = bone.ay + (target.ay + data.offsetY) * mixY;
+			float scaleX = (bone.ascaleX * ((target.ascaleX - 1 + data.offsetScaleX) * mixScaleX) + 1);
+			float scaleY = (bone.ascaleY * ((target.ascaleY - 1 + data.offsetScaleY) * mixScaleY) + 1);
+			float shearY = bone.ashearY + (target.ashearY + data.offsetShearY) * mixShearY;
+
+			bone.updateWorldTransform(x, y, rotation, scaleX, scaleY, bone.ashearX, shearY);
+		}
+	}
+
+	/** The bones that will be modified by this transform constraint. */
+	public Array<Bone> getBones () {
+		return bones;
+	}
+
+	/** The target bone whose world transform will be copied to the constrained bones. */
+	public Bone getTarget () {
+		return target;
+	}
+
+	public void setTarget (Bone target) {
+		if (target == null) throw new IllegalArgumentException("target cannot be null.");
+		this.target = target;
+	}
+
+	/** A percentage (0-1) that controls the mix between the constrained and unconstrained rotation. */
+	public float getMixRotate () {
+		return mixRotate;
+	}
+
+	public void setMixRotate (float mixRotate) {
+		this.mixRotate = mixRotate;
+	}
+
+	/** A percentage (0-1) that controls the mix between the constrained and unconstrained translation X. */
+	public float getMixX () {
+		return mixX;
+	}
+
+	public void setMixX (float mixX) {
+		this.mixX = mixX;
+	}
+
+	/** A percentage (0-1) that controls the mix between the constrained and unconstrained translation Y. */
+	public float getMixY () {
+		return mixY;
+	}
+
+	public void setMixY (float mixY) {
+		this.mixY = mixY;
+	}
+
+	/** A percentage (0-1) that controls the mix between the constrained and unconstrained scale X. */
+	public float getMixScaleX () {
+		return mixScaleX;
+	}
+
+	public void setMixScaleX (float mixScaleX) {
+		this.mixScaleX = mixScaleX;
+	}
+
+	/** A percentage (0-1) that controls the mix between the constrained and unconstrained scale X. */
+	public float getMixScaleY () {
+		return mixScaleY;
+	}
+
+	public void setMixScaleY (float mixScaleY) {
+		this.mixScaleY = mixScaleY;
+	}
+
+	/** A percentage (0-1) that controls the mix between the constrained and unconstrained shear Y. */
+	public float getMixShearY () {
+		return mixShearY;
+	}
+
+	public void setMixShearY (float mixShearY) {
+		this.mixShearY = mixShearY;
+	}
+
+	public boolean isActive () {
+		return active;
+	}
+
+	/** The transform constraint's setup pose data. */
+	public TransformConstraintData getData () {
+		return data;
+	}
+
+	public String toString () {
+		return data.name;
+	}
+}

+ 186 - 186
spine-libgdx/spine-libgdx/src/com/esotericsoftware/spine/TransformConstraintData.java

@@ -1,186 +1,186 @@
-/******************************************************************************
- * Spine Runtimes License Agreement
- * Last updated January 1, 2020. Replaces all prior versions.
- *
- * Copyright (c) 2013-2020, Esoteric Software LLC
- *
- * Integration of the Spine Runtimes into software or otherwise creating
- * derivative works of the Spine Runtimes is permitted under the terms and
- * conditions of Section 2 of the Spine Editor License Agreement:
- * http://esotericsoftware.com/spine-editor-license
- *
- * Otherwise, it is permitted to integrate the Spine Runtimes into software
- * or otherwise create derivative works of the Spine Runtimes (collectively,
- * "Products"), provided that each user of the Products must obtain their own
- * Spine Editor license and redistribution of the Products in any form must
- * include this license and copyright notice.
- *
- * THE SPINE RUNTIMES ARE PROVIDED BY ESOTERIC SOFTWARE LLC "AS IS" AND ANY
- * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
- * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
- * DISCLAIMED. IN NO EVENT SHALL ESOTERIC SOFTWARE LLC BE LIABLE FOR ANY
- * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
- * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES,
- * BUSINESS INTERRUPTION, OR LOSS OF USE, DATA, OR PROFITS) HOWEVER CAUSED AND
- * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
- * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
- * THE SPINE RUNTIMES, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
- *****************************************************************************/
-
-package com.esotericsoftware.spine;
-
-import com.badlogic.gdx.utils.Array;
-
-/** Stores the setup pose for a {@link TransformConstraint}.
- * <p>
- * See <a href="http://esotericsoftware.com/spine-transform-constraints">Transform constraints</a> in the Spine User Guide. */
-public class TransformConstraintData extends ConstraintData {
-	final Array<BoneData> bones = new Array();
-	BoneData target;
-	float mixRotate, mixX, mixY, mixScaleX, mixScaleY, mixShearY;
-	float offsetRotation, offsetX, offsetY, offsetScaleX, offsetScaleY, offsetShearY;
-	boolean relative, local;
-
-	public TransformConstraintData (String name) {
-		super(name);
-	}
-
-	/** The bones that will be modified by this transform constraint. */
-	public Array<BoneData> getBones () {
-		return bones;
-	}
-
-	/** The target bone whose world transform will be copied to the constrained bones. */
-	public BoneData getTarget () {
-		return target;
-	}
-
-	public void setTarget (BoneData target) {
-		if (target == null) throw new IllegalArgumentException("target cannot be null.");
-		this.target = target;
-	}
-
-	/** A percentage (0-1) that controls the mix between the constrained and unconstrained rotation. */
-	public float getMixRotate () {
-		return mixRotate;
-	}
-
-	public void setMixRotate (float mixRotate) {
-		this.mixRotate = mixRotate;
-	}
-
-	/** A percentage (0-1) that controls the mix between the constrained and unconstrained translation X. */
-	public float getMixX () {
-		return mixX;
-	}
-
-	public void setMixX (float mixX) {
-		this.mixX = mixX;
-	}
-
-	/** A percentage (0-1) that controls the mix between the constrained and unconstrained translation Y. */
-	public float getMixY () {
-		return mixY;
-	}
-
-	public void setMixY (float mixY) {
-		this.mixY = mixY;
-	}
-
-	/** A percentage (0-1) that controls the mix between the constrained and unconstrained scale X. */
-	public float getMixScaleX () {
-		return mixScaleX;
-	}
-
-	public void setMixScaleX (float mixScaleX) {
-		this.mixScaleX = mixScaleX;
-	}
-
-	/** A percentage (0-1) that controls the mix between the constrained and unconstrained scale Y. */
-	public float getMixScaleY () {
-		return mixScaleY;
-	}
-
-	public void setMixScaleY (float mixScaleY) {
-		this.mixScaleY = mixScaleY;
-	}
-
-	/** A percentage (0-1) that controls the mix between the constrained and unconstrained shear Y. */
-	public float getMixShearY () {
-		return mixShearY;
-	}
-
-	public void setMixShearY (float mixShearY) {
-		this.mixShearY = mixShearY;
-	}
-
-	/** An offset added to the constrained bone rotation. */
-	public float getOffsetRotation () {
-		return offsetRotation;
-	}
-
-	public void setOffsetRotation (float offsetRotation) {
-		this.offsetRotation = offsetRotation;
-	}
-
-	/** An offset added to the constrained bone X translation. */
-	public float getOffsetX () {
-		return offsetX;
-	}
-
-	public void setOffsetX (float offsetX) {
-		this.offsetX = offsetX;
-	}
-
-	/** An offset added to the constrained bone Y translation. */
-	public float getOffsetY () {
-		return offsetY;
-	}
-
-	public void setOffsetY (float offsetY) {
-		this.offsetY = offsetY;
-	}
-
-	/** An offset added to the constrained bone scaleX. */
-	public float getOffsetScaleX () {
-		return offsetScaleX;
-	}
-
-	public void setOffsetScaleX (float offsetScaleX) {
-		this.offsetScaleX = offsetScaleX;
-	}
-
-	/** An offset added to the constrained bone scaleY. */
-	public float getOffsetScaleY () {
-		return offsetScaleY;
-	}
-
-	public void setOffsetScaleY (float offsetScaleY) {
-		this.offsetScaleY = offsetScaleY;
-	}
-
-	/** An offset added to the constrained bone shearY. */
-	public float getOffsetShearY () {
-		return offsetShearY;
-	}
-
-	public void setOffsetShearY (float offsetShearY) {
-		this.offsetShearY = offsetShearY;
-	}
-
-	public boolean getRelative () {
-		return relative;
-	}
-
-	public void setRelative (boolean relative) {
-		this.relative = relative;
-	}
-
-	public boolean getLocal () {
-		return local;
-	}
-
-	public void setLocal (boolean local) {
-		this.local = local;
-	}
-}
+/******************************************************************************
+ * Spine Runtimes License Agreement
+ * Last updated January 1, 2020. Replaces all prior versions.
+ *
+ * Copyright (c) 2013-2020, Esoteric Software LLC
+ *
+ * Integration of the Spine Runtimes into software or otherwise creating
+ * derivative works of the Spine Runtimes is permitted under the terms and
+ * conditions of Section 2 of the Spine Editor License Agreement:
+ * http://esotericsoftware.com/spine-editor-license
+ *
+ * Otherwise, it is permitted to integrate the Spine Runtimes into software
+ * or otherwise create derivative works of the Spine Runtimes (collectively,
+ * "Products"), provided that each user of the Products must obtain their own
+ * Spine Editor license and redistribution of the Products in any form must
+ * include this license and copyright notice.
+ *
+ * THE SPINE RUNTIMES ARE PROVIDED BY ESOTERIC SOFTWARE LLC "AS IS" AND ANY
+ * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL ESOTERIC SOFTWARE LLC BE LIABLE FOR ANY
+ * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES,
+ * BUSINESS INTERRUPTION, OR LOSS OF USE, DATA, OR PROFITS) HOWEVER CAUSED AND
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
+ * THE SPINE RUNTIMES, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ *****************************************************************************/
+
+package com.esotericsoftware.spine;
+
+import com.badlogic.gdx.utils.Array;
+
+/** Stores the setup pose for a {@link TransformConstraint}.
+ * <p>
+ * See <a href="http://esotericsoftware.com/spine-transform-constraints">Transform constraints</a> in the Spine User Guide. */
+public class TransformConstraintData extends ConstraintData {
+	final Array<BoneData> bones = new Array();
+	BoneData target;
+	float mixRotate, mixX, mixY, mixScaleX, mixScaleY, mixShearY;
+	float offsetRotation, offsetX, offsetY, offsetScaleX, offsetScaleY, offsetShearY;
+	boolean relative, local;
+
+	public TransformConstraintData (String name) {
+		super(name);
+	}
+
+	/** The bones that will be modified by this transform constraint. */
+	public Array<BoneData> getBones () {
+		return bones;
+	}
+
+	/** The target bone whose world transform will be copied to the constrained bones. */
+	public BoneData getTarget () {
+		return target;
+	}
+
+	public void setTarget (BoneData target) {
+		if (target == null) throw new IllegalArgumentException("target cannot be null.");
+		this.target = target;
+	}
+
+	/** A percentage (0-1) that controls the mix between the constrained and unconstrained rotation. */
+	public float getMixRotate () {
+		return mixRotate;
+	}
+
+	public void setMixRotate (float mixRotate) {
+		this.mixRotate = mixRotate;
+	}
+
+	/** A percentage (0-1) that controls the mix between the constrained and unconstrained translation X. */
+	public float getMixX () {
+		return mixX;
+	}
+
+	public void setMixX (float mixX) {
+		this.mixX = mixX;
+	}
+
+	/** A percentage (0-1) that controls the mix between the constrained and unconstrained translation Y. */
+	public float getMixY () {
+		return mixY;
+	}
+
+	public void setMixY (float mixY) {
+		this.mixY = mixY;
+	}
+
+	/** A percentage (0-1) that controls the mix between the constrained and unconstrained scale X. */
+	public float getMixScaleX () {
+		return mixScaleX;
+	}
+
+	public void setMixScaleX (float mixScaleX) {
+		this.mixScaleX = mixScaleX;
+	}
+
+	/** A percentage (0-1) that controls the mix between the constrained and unconstrained scale Y. */
+	public float getMixScaleY () {
+		return mixScaleY;
+	}
+
+	public void setMixScaleY (float mixScaleY) {
+		this.mixScaleY = mixScaleY;
+	}
+
+	/** A percentage (0-1) that controls the mix between the constrained and unconstrained shear Y. */
+	public float getMixShearY () {
+		return mixShearY;
+	}
+
+	public void setMixShearY (float mixShearY) {
+		this.mixShearY = mixShearY;
+	}
+
+	/** An offset added to the constrained bone rotation. */
+	public float getOffsetRotation () {
+		return offsetRotation;
+	}
+
+	public void setOffsetRotation (float offsetRotation) {
+		this.offsetRotation = offsetRotation;
+	}
+
+	/** An offset added to the constrained bone X translation. */
+	public float getOffsetX () {
+		return offsetX;
+	}
+
+	public void setOffsetX (float offsetX) {
+		this.offsetX = offsetX;
+	}
+
+	/** An offset added to the constrained bone Y translation. */
+	public float getOffsetY () {
+		return offsetY;
+	}
+
+	public void setOffsetY (float offsetY) {
+		this.offsetY = offsetY;
+	}
+
+	/** An offset added to the constrained bone scaleX. */
+	public float getOffsetScaleX () {
+		return offsetScaleX;
+	}
+
+	public void setOffsetScaleX (float offsetScaleX) {
+		this.offsetScaleX = offsetScaleX;
+	}
+
+	/** An offset added to the constrained bone scaleY. */
+	public float getOffsetScaleY () {
+		return offsetScaleY;
+	}
+
+	public void setOffsetScaleY (float offsetScaleY) {
+		this.offsetScaleY = offsetScaleY;
+	}
+
+	/** An offset added to the constrained bone shearY. */
+	public float getOffsetShearY () {
+		return offsetShearY;
+	}
+
+	public void setOffsetShearY (float offsetShearY) {
+		this.offsetShearY = offsetShearY;
+	}
+
+	public boolean getRelative () {
+		return relative;
+	}
+
+	public void setRelative (boolean relative) {
+		this.relative = relative;
+	}
+
+	public boolean getLocal () {
+		return local;
+	}
+
+	public void setLocal (boolean local) {
+		this.local = local;
+	}
+}

+ 41 - 41
spine-libgdx/spine-libgdx/src/com/esotericsoftware/spine/Updatable.java

@@ -1,41 +1,41 @@
-/******************************************************************************
- * Spine Runtimes License Agreement
- * Last updated January 1, 2020. Replaces all prior versions.
- *
- * Copyright (c) 2013-2020, Esoteric Software LLC
- *
- * Integration of the Spine Runtimes into software or otherwise creating
- * derivative works of the Spine Runtimes is permitted under the terms and
- * conditions of Section 2 of the Spine Editor License Agreement:
- * http://esotericsoftware.com/spine-editor-license
- *
- * Otherwise, it is permitted to integrate the Spine Runtimes into software
- * or otherwise create derivative works of the Spine Runtimes (collectively,
- * "Products"), provided that each user of the Products must obtain their own
- * Spine Editor license and redistribution of the Products in any form must
- * include this license and copyright notice.
- *
- * THE SPINE RUNTIMES ARE PROVIDED BY ESOTERIC SOFTWARE LLC "AS IS" AND ANY
- * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
- * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
- * DISCLAIMED. IN NO EVENT SHALL ESOTERIC SOFTWARE LLC BE LIABLE FOR ANY
- * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
- * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES,
- * BUSINESS INTERRUPTION, OR LOSS OF USE, DATA, OR PROFITS) HOWEVER CAUSED AND
- * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
- * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
- * THE SPINE RUNTIMES, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
- *****************************************************************************/
-
-package com.esotericsoftware.spine;
-
-/** The interface for items updated by {@link Skeleton#updateWorldTransform()}. */
-public interface Updatable {
-	public void update ();
-
-	/** Returns false when this item has not been updated because a skin is required and the {@link Skeleton#getSkin() active skin}
-	 * does not contain this item.
-	 * @see Skin#getBones()
-	 * @see Skin#getConstraints() */
-	public boolean isActive ();
-}
+/******************************************************************************
+ * Spine Runtimes License Agreement
+ * Last updated January 1, 2020. Replaces all prior versions.
+ *
+ * Copyright (c) 2013-2020, Esoteric Software LLC
+ *
+ * Integration of the Spine Runtimes into software or otherwise creating
+ * derivative works of the Spine Runtimes is permitted under the terms and
+ * conditions of Section 2 of the Spine Editor License Agreement:
+ * http://esotericsoftware.com/spine-editor-license
+ *
+ * Otherwise, it is permitted to integrate the Spine Runtimes into software
+ * or otherwise create derivative works of the Spine Runtimes (collectively,
+ * "Products"), provided that each user of the Products must obtain their own
+ * Spine Editor license and redistribution of the Products in any form must
+ * include this license and copyright notice.
+ *
+ * THE SPINE RUNTIMES ARE PROVIDED BY ESOTERIC SOFTWARE LLC "AS IS" AND ANY
+ * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL ESOTERIC SOFTWARE LLC BE LIABLE FOR ANY
+ * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES,
+ * BUSINESS INTERRUPTION, OR LOSS OF USE, DATA, OR PROFITS) HOWEVER CAUSED AND
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
+ * THE SPINE RUNTIMES, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ *****************************************************************************/
+
+package com.esotericsoftware.spine;
+
+/** The interface for items updated by {@link Skeleton#updateWorldTransform()}. */
+public interface Updatable {
+	public void update ();
+
+	/** Returns false when this item has not been updated because a skin is required and the {@link Skeleton#getSkin() active skin}
+	 * does not contain this item.
+	 * @see Skin#getBones()
+	 * @see Skin#getConstraints() */
+	public boolean isActive ();
+}

+ 81 - 81
spine-libgdx/spine-libgdx/src/com/esotericsoftware/spine/attachments/AtlasAttachmentLoader.java

@@ -1,81 +1,81 @@
-/******************************************************************************
- * Spine Runtimes License Agreement
- * Last updated January 1, 2020. Replaces all prior versions.
- *
- * Copyright (c) 2013-2020, Esoteric Software LLC
- *
- * Integration of the Spine Runtimes into software or otherwise creating
- * derivative works of the Spine Runtimes is permitted under the terms and
- * conditions of Section 2 of the Spine Editor License Agreement:
- * http://esotericsoftware.com/spine-editor-license
- *
- * Otherwise, it is permitted to integrate the Spine Runtimes into software
- * or otherwise create derivative works of the Spine Runtimes (collectively,
- * "Products"), provided that each user of the Products must obtain their own
- * Spine Editor license and redistribution of the Products in any form must
- * include this license and copyright notice.
- *
- * THE SPINE RUNTIMES ARE PROVIDED BY ESOTERIC SOFTWARE LLC "AS IS" AND ANY
- * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
- * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
- * DISCLAIMED. IN NO EVENT SHALL ESOTERIC SOFTWARE LLC BE LIABLE FOR ANY
- * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
- * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES,
- * BUSINESS INTERRUPTION, OR LOSS OF USE, DATA, OR PROFITS) HOWEVER CAUSED AND
- * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
- * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
- * THE SPINE RUNTIMES, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
- *****************************************************************************/
-
-package com.esotericsoftware.spine.attachments;
-
-import com.badlogic.gdx.graphics.g2d.TextureAtlas;
-import com.badlogic.gdx.graphics.g2d.TextureAtlas.AtlasRegion;
-
-import com.esotericsoftware.spine.Skin;
-
-/** An {@link AttachmentLoader} that configures attachments using texture regions from an {@link Atlas}.
- * <p>
- * See <a href='http://esotericsoftware.com/spine-loading-skeleton-data#JSON-and-binary-data'>Loading skeleton data</a> in the
- * Spine Runtimes Guide. */
-@SuppressWarnings("javadoc")
-public class AtlasAttachmentLoader implements AttachmentLoader {
-	private TextureAtlas atlas;
-
-	public AtlasAttachmentLoader (TextureAtlas atlas) {
-		if (atlas == null) throw new IllegalArgumentException("atlas cannot be null.");
-		this.atlas = atlas;
-	}
-
-	public RegionAttachment newRegionAttachment (Skin skin, String name, String path) {
-		AtlasRegion region = atlas.findRegion(path);
-		if (region == null) throw new RuntimeException("Region not found in atlas: " + path + " (region attachment: " + name + ")");
-		RegionAttachment attachment = new RegionAttachment(name);
-		attachment.setRegion(region);
-		return attachment;
-	}
-
-	public MeshAttachment newMeshAttachment (Skin skin, String name, String path) {
-		AtlasRegion region = atlas.findRegion(path);
-		if (region == null) throw new RuntimeException("Region not found in atlas: " + path + " (mesh attachment: " + name + ")");
-		MeshAttachment attachment = new MeshAttachment(name);
-		attachment.setRegion(region);
-		return attachment;
-	}
-
-	public BoundingBoxAttachment newBoundingBoxAttachment (Skin skin, String name) {
-		return new BoundingBoxAttachment(name);
-	}
-
-	public ClippingAttachment newClippingAttachment (Skin skin, String name) {
-		return new ClippingAttachment(name);
-	}
-
-	public PathAttachment newPathAttachment (Skin skin, String name) {
-		return new PathAttachment(name);
-	}
-
-	public PointAttachment newPointAttachment (Skin skin, String name) {
-		return new PointAttachment(name);
-	}
-}
+/******************************************************************************
+ * Spine Runtimes License Agreement
+ * Last updated January 1, 2020. Replaces all prior versions.
+ *
+ * Copyright (c) 2013-2020, Esoteric Software LLC
+ *
+ * Integration of the Spine Runtimes into software or otherwise creating
+ * derivative works of the Spine Runtimes is permitted under the terms and
+ * conditions of Section 2 of the Spine Editor License Agreement:
+ * http://esotericsoftware.com/spine-editor-license
+ *
+ * Otherwise, it is permitted to integrate the Spine Runtimes into software
+ * or otherwise create derivative works of the Spine Runtimes (collectively,
+ * "Products"), provided that each user of the Products must obtain their own
+ * Spine Editor license and redistribution of the Products in any form must
+ * include this license and copyright notice.
+ *
+ * THE SPINE RUNTIMES ARE PROVIDED BY ESOTERIC SOFTWARE LLC "AS IS" AND ANY
+ * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL ESOTERIC SOFTWARE LLC BE LIABLE FOR ANY
+ * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES,
+ * BUSINESS INTERRUPTION, OR LOSS OF USE, DATA, OR PROFITS) HOWEVER CAUSED AND
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
+ * THE SPINE RUNTIMES, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ *****************************************************************************/
+
+package com.esotericsoftware.spine.attachments;
+
+import com.badlogic.gdx.graphics.g2d.TextureAtlas;
+import com.badlogic.gdx.graphics.g2d.TextureAtlas.AtlasRegion;
+
+import com.esotericsoftware.spine.Skin;
+
+/** An {@link AttachmentLoader} that configures attachments using texture regions from an {@link Atlas}.
+ * <p>
+ * See <a href='http://esotericsoftware.com/spine-loading-skeleton-data#JSON-and-binary-data'>Loading skeleton data</a> in the
+ * Spine Runtimes Guide. */
+@SuppressWarnings("javadoc")
+public class AtlasAttachmentLoader implements AttachmentLoader {
+	private TextureAtlas atlas;
+
+	public AtlasAttachmentLoader (TextureAtlas atlas) {
+		if (atlas == null) throw new IllegalArgumentException("atlas cannot be null.");
+		this.atlas = atlas;
+	}
+
+	public RegionAttachment newRegionAttachment (Skin skin, String name, String path) {
+		AtlasRegion region = atlas.findRegion(path);
+		if (region == null) throw new RuntimeException("Region not found in atlas: " + path + " (region attachment: " + name + ")");
+		RegionAttachment attachment = new RegionAttachment(name);
+		attachment.setRegion(region);
+		return attachment;
+	}
+
+	public MeshAttachment newMeshAttachment (Skin skin, String name, String path) {
+		AtlasRegion region = atlas.findRegion(path);
+		if (region == null) throw new RuntimeException("Region not found in atlas: " + path + " (mesh attachment: " + name + ")");
+		MeshAttachment attachment = new MeshAttachment(name);
+		attachment.setRegion(region);
+		return attachment;
+	}
+
+	public BoundingBoxAttachment newBoundingBoxAttachment (Skin skin, String name) {
+		return new BoundingBoxAttachment(name);
+	}
+
+	public ClippingAttachment newClippingAttachment (Skin skin, String name) {
+		return new ClippingAttachment(name);
+	}
+
+	public PathAttachment newPathAttachment (Skin skin, String name) {
+		return new PathAttachment(name);
+	}
+
+	public PointAttachment newPointAttachment (Skin skin, String name) {
+		return new PointAttachment(name);
+	}
+}

+ 52 - 52
spine-libgdx/spine-libgdx/src/com/esotericsoftware/spine/attachments/Attachment.java

@@ -1,52 +1,52 @@
-/******************************************************************************
- * Spine Runtimes License Agreement
- * Last updated January 1, 2020. Replaces all prior versions.
- *
- * Copyright (c) 2013-2020, Esoteric Software LLC
- *
- * Integration of the Spine Runtimes into software or otherwise creating
- * derivative works of the Spine Runtimes is permitted under the terms and
- * conditions of Section 2 of the Spine Editor License Agreement:
- * http://esotericsoftware.com/spine-editor-license
- *
- * Otherwise, it is permitted to integrate the Spine Runtimes into software
- * or otherwise create derivative works of the Spine Runtimes (collectively,
- * "Products"), provided that each user of the Products must obtain their own
- * Spine Editor license and redistribution of the Products in any form must
- * include this license and copyright notice.
- *
- * THE SPINE RUNTIMES ARE PROVIDED BY ESOTERIC SOFTWARE LLC "AS IS" AND ANY
- * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
- * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
- * DISCLAIMED. IN NO EVENT SHALL ESOTERIC SOFTWARE LLC BE LIABLE FOR ANY
- * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
- * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES,
- * BUSINESS INTERRUPTION, OR LOSS OF USE, DATA, OR PROFITS) HOWEVER CAUSED AND
- * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
- * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
- * THE SPINE RUNTIMES, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
- *****************************************************************************/
-
-package com.esotericsoftware.spine.attachments;
-
-/** The base class for all attachments. */
-abstract public class Attachment {
-	String name;
-
-	public Attachment (String name) {
-		if (name == null) throw new IllegalArgumentException("name cannot be null.");
-		this.name = name;
-	}
-
-	/** The attachment's name. */
-	public String getName () {
-		return name;
-	}
-
-	public String toString () {
-		return name;
-	}
-
-	/** Returns a copy of the attachment. */
-	abstract public Attachment copy ();
-}
+/******************************************************************************
+ * Spine Runtimes License Agreement
+ * Last updated January 1, 2020. Replaces all prior versions.
+ *
+ * Copyright (c) 2013-2020, Esoteric Software LLC
+ *
+ * Integration of the Spine Runtimes into software or otherwise creating
+ * derivative works of the Spine Runtimes is permitted under the terms and
+ * conditions of Section 2 of the Spine Editor License Agreement:
+ * http://esotericsoftware.com/spine-editor-license
+ *
+ * Otherwise, it is permitted to integrate the Spine Runtimes into software
+ * or otherwise create derivative works of the Spine Runtimes (collectively,
+ * "Products"), provided that each user of the Products must obtain their own
+ * Spine Editor license and redistribution of the Products in any form must
+ * include this license and copyright notice.
+ *
+ * THE SPINE RUNTIMES ARE PROVIDED BY ESOTERIC SOFTWARE LLC "AS IS" AND ANY
+ * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL ESOTERIC SOFTWARE LLC BE LIABLE FOR ANY
+ * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES,
+ * BUSINESS INTERRUPTION, OR LOSS OF USE, DATA, OR PROFITS) HOWEVER CAUSED AND
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
+ * THE SPINE RUNTIMES, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ *****************************************************************************/
+
+package com.esotericsoftware.spine.attachments;
+
+/** The base class for all attachments. */
+abstract public class Attachment {
+	String name;
+
+	public Attachment (String name) {
+		if (name == null) throw new IllegalArgumentException("name cannot be null.");
+		this.name = name;
+	}
+
+	/** The attachment's name. */
+	public String getName () {
+		return name;
+	}
+
+	public String toString () {
+		return name;
+	}
+
+	/** Returns a copy of the attachment. */
+	abstract public Attachment copy ();
+}

+ 58 - 58
spine-libgdx/spine-libgdx/src/com/esotericsoftware/spine/attachments/AttachmentLoader.java

@@ -1,58 +1,58 @@
-/******************************************************************************
- * Spine Runtimes License Agreement
- * Last updated January 1, 2020. Replaces all prior versions.
- *
- * Copyright (c) 2013-2020, Esoteric Software LLC
- *
- * Integration of the Spine Runtimes into software or otherwise creating
- * derivative works of the Spine Runtimes is permitted under the terms and
- * conditions of Section 2 of the Spine Editor License Agreement:
- * http://esotericsoftware.com/spine-editor-license
- *
- * Otherwise, it is permitted to integrate the Spine Runtimes into software
- * or otherwise create derivative works of the Spine Runtimes (collectively,
- * "Products"), provided that each user of the Products must obtain their own
- * Spine Editor license and redistribution of the Products in any form must
- * include this license and copyright notice.
- *
- * THE SPINE RUNTIMES ARE PROVIDED BY ESOTERIC SOFTWARE LLC "AS IS" AND ANY
- * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
- * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
- * DISCLAIMED. IN NO EVENT SHALL ESOTERIC SOFTWARE LLC BE LIABLE FOR ANY
- * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
- * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES,
- * BUSINESS INTERRUPTION, OR LOSS OF USE, DATA, OR PROFITS) HOWEVER CAUSED AND
- * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
- * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
- * THE SPINE RUNTIMES, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
- *****************************************************************************/
-
-package com.esotericsoftware.spine.attachments;
-
-import com.badlogic.gdx.utils.Null;
-
-import com.esotericsoftware.spine.Skin;
-
-/** The interface which can be implemented to customize creating and populating attachments.
- * <p>
- * See <a href='http://esotericsoftware.com/spine-loading-skeleton-data#AttachmentLoader'>Loading skeleton data</a> in the Spine
- * Runtimes Guide. */
-public interface AttachmentLoader {
-	/** @return May be null to not load the attachment. */
-	public @Null RegionAttachment newRegionAttachment (Skin skin, String name, String path);
-
-	/** @return May be null to not load the attachment. In that case null should also be returned for child meshes. */
-	public @Null MeshAttachment newMeshAttachment (Skin skin, String name, String path);
-
-	/** @return May be null to not load the attachment. */
-	public @Null BoundingBoxAttachment newBoundingBoxAttachment (Skin skin, String name);
-
-	/** @return May be null to not load the attachment. */
-	public @Null ClippingAttachment newClippingAttachment (Skin skin, String name);
-
-	/** @return May be null to not load the attachment. */
-	public @Null PathAttachment newPathAttachment (Skin skin, String name);
-
-	/** @return May be null to not load the attachment. */
-	public @Null PointAttachment newPointAttachment (Skin skin, String name);
-}
+/******************************************************************************
+ * Spine Runtimes License Agreement
+ * Last updated January 1, 2020. Replaces all prior versions.
+ *
+ * Copyright (c) 2013-2020, Esoteric Software LLC
+ *
+ * Integration of the Spine Runtimes into software or otherwise creating
+ * derivative works of the Spine Runtimes is permitted under the terms and
+ * conditions of Section 2 of the Spine Editor License Agreement:
+ * http://esotericsoftware.com/spine-editor-license
+ *
+ * Otherwise, it is permitted to integrate the Spine Runtimes into software
+ * or otherwise create derivative works of the Spine Runtimes (collectively,
+ * "Products"), provided that each user of the Products must obtain their own
+ * Spine Editor license and redistribution of the Products in any form must
+ * include this license and copyright notice.
+ *
+ * THE SPINE RUNTIMES ARE PROVIDED BY ESOTERIC SOFTWARE LLC "AS IS" AND ANY
+ * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL ESOTERIC SOFTWARE LLC BE LIABLE FOR ANY
+ * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES,
+ * BUSINESS INTERRUPTION, OR LOSS OF USE, DATA, OR PROFITS) HOWEVER CAUSED AND
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
+ * THE SPINE RUNTIMES, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ *****************************************************************************/
+
+package com.esotericsoftware.spine.attachments;
+
+import com.badlogic.gdx.utils.Null;
+
+import com.esotericsoftware.spine.Skin;
+
+/** The interface which can be implemented to customize creating and populating attachments.
+ * <p>
+ * See <a href='http://esotericsoftware.com/spine-loading-skeleton-data#AttachmentLoader'>Loading skeleton data</a> in the Spine
+ * Runtimes Guide. */
+public interface AttachmentLoader {
+	/** @return May be null to not load the attachment. */
+	public @Null RegionAttachment newRegionAttachment (Skin skin, String name, String path);
+
+	/** @return May be null to not load the attachment. In that case null should also be returned for child meshes. */
+	public @Null MeshAttachment newMeshAttachment (Skin skin, String name, String path);
+
+	/** @return May be null to not load the attachment. */
+	public @Null BoundingBoxAttachment newBoundingBoxAttachment (Skin skin, String name);
+
+	/** @return May be null to not load the attachment. */
+	public @Null ClippingAttachment newClippingAttachment (Skin skin, String name);
+
+	/** @return May be null to not load the attachment. */
+	public @Null PathAttachment newPathAttachment (Skin skin, String name);
+
+	/** @return May be null to not load the attachment. */
+	public @Null PointAttachment newPointAttachment (Skin skin, String name);
+}

+ 36 - 36
spine-libgdx/spine-libgdx/src/com/esotericsoftware/spine/attachments/AttachmentType.java

@@ -1,36 +1,36 @@
-/******************************************************************************
- * Spine Runtimes License Agreement
- * Last updated January 1, 2020. Replaces all prior versions.
- *
- * Copyright (c) 2013-2020, Esoteric Software LLC
- *
- * Integration of the Spine Runtimes into software or otherwise creating
- * derivative works of the Spine Runtimes is permitted under the terms and
- * conditions of Section 2 of the Spine Editor License Agreement:
- * http://esotericsoftware.com/spine-editor-license
- *
- * Otherwise, it is permitted to integrate the Spine Runtimes into software
- * or otherwise create derivative works of the Spine Runtimes (collectively,
- * "Products"), provided that each user of the Products must obtain their own
- * Spine Editor license and redistribution of the Products in any form must
- * include this license and copyright notice.
- *
- * THE SPINE RUNTIMES ARE PROVIDED BY ESOTERIC SOFTWARE LLC "AS IS" AND ANY
- * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
- * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
- * DISCLAIMED. IN NO EVENT SHALL ESOTERIC SOFTWARE LLC BE LIABLE FOR ANY
- * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
- * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES,
- * BUSINESS INTERRUPTION, OR LOSS OF USE, DATA, OR PROFITS) HOWEVER CAUSED AND
- * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
- * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
- * THE SPINE RUNTIMES, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
- *****************************************************************************/
-
-package com.esotericsoftware.spine.attachments;
-
-public enum AttachmentType {
-	region, boundingbox, mesh, linkedmesh, path, point, clipping;
-
-	static public final AttachmentType[] values = values();
-}
+/******************************************************************************
+ * Spine Runtimes License Agreement
+ * Last updated January 1, 2020. Replaces all prior versions.
+ *
+ * Copyright (c) 2013-2020, Esoteric Software LLC
+ *
+ * Integration of the Spine Runtimes into software or otherwise creating
+ * derivative works of the Spine Runtimes is permitted under the terms and
+ * conditions of Section 2 of the Spine Editor License Agreement:
+ * http://esotericsoftware.com/spine-editor-license
+ *
+ * Otherwise, it is permitted to integrate the Spine Runtimes into software
+ * or otherwise create derivative works of the Spine Runtimes (collectively,
+ * "Products"), provided that each user of the Products must obtain their own
+ * Spine Editor license and redistribution of the Products in any form must
+ * include this license and copyright notice.
+ *
+ * THE SPINE RUNTIMES ARE PROVIDED BY ESOTERIC SOFTWARE LLC "AS IS" AND ANY
+ * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL ESOTERIC SOFTWARE LLC BE LIABLE FOR ANY
+ * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES,
+ * BUSINESS INTERRUPTION, OR LOSS OF USE, DATA, OR PROFITS) HOWEVER CAUSED AND
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
+ * THE SPINE RUNTIMES, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ *****************************************************************************/
+
+package com.esotericsoftware.spine.attachments;
+
+public enum AttachmentType {
+	region, boundingbox, mesh, linkedmesh, path, point, clipping;
+
+	static public final AttachmentType[] values = values();
+}

+ 61 - 61
spine-libgdx/spine-libgdx/src/com/esotericsoftware/spine/attachments/BoundingBoxAttachment.java

@@ -1,61 +1,61 @@
-/******************************************************************************
- * Spine Runtimes License Agreement
- * Last updated January 1, 2020. Replaces all prior versions.
- *
- * Copyright (c) 2013-2020, Esoteric Software LLC
- *
- * Integration of the Spine Runtimes into software or otherwise creating
- * derivative works of the Spine Runtimes is permitted under the terms and
- * conditions of Section 2 of the Spine Editor License Agreement:
- * http://esotericsoftware.com/spine-editor-license
- *
- * Otherwise, it is permitted to integrate the Spine Runtimes into software
- * or otherwise create derivative works of the Spine Runtimes (collectively,
- * "Products"), provided that each user of the Products must obtain their own
- * Spine Editor license and redistribution of the Products in any form must
- * include this license and copyright notice.
- *
- * THE SPINE RUNTIMES ARE PROVIDED BY ESOTERIC SOFTWARE LLC "AS IS" AND ANY
- * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
- * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
- * DISCLAIMED. IN NO EVENT SHALL ESOTERIC SOFTWARE LLC BE LIABLE FOR ANY
- * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
- * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES,
- * BUSINESS INTERRUPTION, OR LOSS OF USE, DATA, OR PROFITS) HOWEVER CAUSED AND
- * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
- * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
- * THE SPINE RUNTIMES, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
- *****************************************************************************/
-
-package com.esotericsoftware.spine.attachments;
-
-import com.badlogic.gdx.graphics.Color;
-
-import com.esotericsoftware.spine.SkeletonBounds;
-
-/** An attachment with vertices that make up a polygon. Can be used for hit detection, creating physics bodies, spawning particle
- * effects, and more.
- * <p>
- * See {@link SkeletonBounds} and <a href="http://esotericsoftware.com/spine-bounding-boxes">Bounding Boxes</a> in the Spine User
- * Guide. */
-public class BoundingBoxAttachment extends VertexAttachment {
-	// Nonessential.
-	final Color color = new Color(0.38f, 0.94f, 0, 1); // 60f000ff
-
-	public BoundingBoxAttachment (String name) {
-		super(name);
-	}
-
-	/** The color of the bounding box as it was in Spine, or a default color if nonessential data was not exported. Bounding boxes
-	 * are not usually rendered at runtime. */
-	public Color getColor () {
-		return color;
-	}
-
-	public Attachment copy () {
-		BoundingBoxAttachment copy = new BoundingBoxAttachment(name);
-		copyTo(copy);
-		copy.color.set(color);
-		return copy;
-	}
-}
+/******************************************************************************
+ * Spine Runtimes License Agreement
+ * Last updated January 1, 2020. Replaces all prior versions.
+ *
+ * Copyright (c) 2013-2020, Esoteric Software LLC
+ *
+ * Integration of the Spine Runtimes into software or otherwise creating
+ * derivative works of the Spine Runtimes is permitted under the terms and
+ * conditions of Section 2 of the Spine Editor License Agreement:
+ * http://esotericsoftware.com/spine-editor-license
+ *
+ * Otherwise, it is permitted to integrate the Spine Runtimes into software
+ * or otherwise create derivative works of the Spine Runtimes (collectively,
+ * "Products"), provided that each user of the Products must obtain their own
+ * Spine Editor license and redistribution of the Products in any form must
+ * include this license and copyright notice.
+ *
+ * THE SPINE RUNTIMES ARE PROVIDED BY ESOTERIC SOFTWARE LLC "AS IS" AND ANY
+ * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL ESOTERIC SOFTWARE LLC BE LIABLE FOR ANY
+ * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES,
+ * BUSINESS INTERRUPTION, OR LOSS OF USE, DATA, OR PROFITS) HOWEVER CAUSED AND
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
+ * THE SPINE RUNTIMES, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ *****************************************************************************/
+
+package com.esotericsoftware.spine.attachments;
+
+import com.badlogic.gdx.graphics.Color;
+
+import com.esotericsoftware.spine.SkeletonBounds;
+
+/** An attachment with vertices that make up a polygon. Can be used for hit detection, creating physics bodies, spawning particle
+ * effects, and more.
+ * <p>
+ * See {@link SkeletonBounds} and <a href="http://esotericsoftware.com/spine-bounding-boxes">Bounding Boxes</a> in the Spine User
+ * Guide. */
+public class BoundingBoxAttachment extends VertexAttachment {
+	// Nonessential.
+	final Color color = new Color(0.38f, 0.94f, 0, 1); // 60f000ff
+
+	public BoundingBoxAttachment (String name) {
+		super(name);
+	}
+
+	/** The color of the bounding box as it was in Spine, or a default color if nonessential data was not exported. Bounding boxes
+	 * are not usually rendered at runtime. */
+	public Color getColor () {
+		return color;
+	}
+
+	public Attachment copy () {
+		BoundingBoxAttachment copy = new BoundingBoxAttachment(name);
+		copyTo(copy);
+		copy.color.set(color);
+		return copy;
+	}
+}

+ 275 - 275
spine-libgdx/spine-libgdx/src/com/esotericsoftware/spine/attachments/MeshAttachment.java

@@ -1,275 +1,275 @@
-/******************************************************************************
- * Spine Runtimes License Agreement
- * Last updated January 1, 2020. Replaces all prior versions.
- *
- * Copyright (c) 2013-2020, Esoteric Software LLC
- *
- * Integration of the Spine Runtimes into software or otherwise creating
- * derivative works of the Spine Runtimes is permitted under the terms and
- * conditions of Section 2 of the Spine Editor License Agreement:
- * http://esotericsoftware.com/spine-editor-license
- *
- * Otherwise, it is permitted to integrate the Spine Runtimes into software
- * or otherwise create derivative works of the Spine Runtimes (collectively,
- * "Products"), provided that each user of the Products must obtain their own
- * Spine Editor license and redistribution of the Products in any form must
- * include this license and copyright notice.
- *
- * THE SPINE RUNTIMES ARE PROVIDED BY ESOTERIC SOFTWARE LLC "AS IS" AND ANY
- * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
- * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
- * DISCLAIMED. IN NO EVENT SHALL ESOTERIC SOFTWARE LLC BE LIABLE FOR ANY
- * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
- * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES,
- * BUSINESS INTERRUPTION, OR LOSS OF USE, DATA, OR PROFITS) HOWEVER CAUSED AND
- * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
- * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
- * THE SPINE RUNTIMES, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
- *****************************************************************************/
-
-package com.esotericsoftware.spine.attachments;
-
-import static com.esotericsoftware.spine.utils.SpineUtils.*;
-
-import com.badlogic.gdx.graphics.Color;
-import com.badlogic.gdx.graphics.g2d.TextureAtlas.AtlasRegion;
-import com.badlogic.gdx.graphics.g2d.TextureRegion;
-import com.badlogic.gdx.utils.Null;
-
-/** An attachment that displays a textured mesh. A mesh has hull vertices and internal vertices within the hull. Holes are not
- * supported. Each vertex has UVs (texture coordinates) and triangles are used to map an image on to the mesh.
- * <p>
- * See <a href="http://esotericsoftware.com/spine-meshes">Mesh attachments</a> in the Spine User Guide. */
-public class MeshAttachment extends VertexAttachment {
-	private TextureRegion region;
-	private String path;
-	private float[] regionUVs, uvs;
-	private short[] triangles;
-	private final Color color = new Color(1, 1, 1, 1);
-	private int hullLength;
-	private @Null MeshAttachment parentMesh;
-
-	// Nonessential.
-	private @Null short[] edges;
-	private float width, height;
-
-	public MeshAttachment (String name) {
-		super(name);
-	}
-
-	public void setRegion (TextureRegion region) {
-		if (region == null) throw new IllegalArgumentException("region cannot be null.");
-		this.region = region;
-	}
-
-	public TextureRegion getRegion () {
-		if (region == null) throw new IllegalStateException("Region has not been set: " + this);
-		return region;
-	}
-
-	/** Calculates {@link #uvs} using {@link #regionUVs} and the {@link #region}. Must be called after changing the region UVs or
-	 * region. */
-	public void updateUVs () {
-		float[] regionUVs = this.regionUVs;
-		if (this.uvs == null || this.uvs.length != regionUVs.length) this.uvs = new float[regionUVs.length];
-		float[] uvs = this.uvs;
-		int n = uvs.length;
-		float u, v, width, height;
-		if (region instanceof AtlasRegion) {
-			u = region.getU();
-			v = region.getV();
-			AtlasRegion region = (AtlasRegion)this.region;
-			float textureWidth = region.getTexture().getWidth(), textureHeight = region.getTexture().getHeight();
-			switch (region.degrees) {
-			case 90:
-				u -= (region.originalHeight - region.offsetY - region.packedWidth) / textureWidth;
-				v -= (region.originalWidth - region.offsetX - region.packedHeight) / textureHeight;
-				width = region.originalHeight / textureWidth;
-				height = region.originalWidth / textureHeight;
-				for (int i = 0; i < n; i += 2) {
-					uvs[i] = u + regionUVs[i + 1] * width;
-					uvs[i + 1] = v + (1 - regionUVs[i]) * height;
-				}
-				return;
-			case 180:
-				u -= (region.originalWidth - region.offsetX - region.packedWidth) / textureWidth;
-				v -= region.offsetY / textureHeight;
-				width = region.originalWidth / textureWidth;
-				height = region.originalHeight / textureHeight;
-				for (int i = 0; i < n; i += 2) {
-					uvs[i] = u + (1 - regionUVs[i]) * width;
-					uvs[i + 1] = v + (1 - regionUVs[i + 1]) * height;
-				}
-				return;
-			case 270:
-				u -= region.offsetY / textureWidth;
-				v -= region.offsetX / textureHeight;
-				width = region.originalHeight / textureWidth;
-				height = region.originalWidth / textureHeight;
-				for (int i = 0; i < n; i += 2) {
-					uvs[i] = u + (1 - regionUVs[i + 1]) * width;
-					uvs[i + 1] = v + regionUVs[i] * height;
-				}
-				return;
-			}
-			u -= region.offsetX / textureWidth;
-			v -= (region.originalHeight - region.offsetY - region.packedHeight) / textureHeight;
-			width = region.originalWidth / textureWidth;
-			height = region.originalHeight / textureHeight;
-		} else if (region == null) {
-			u = v = 0;
-			width = height = 1;
-		} else {
-			u = region.getU();
-			v = region.getV();
-			width = region.getU2() - u;
-			height = region.getV2() - v;
-		}
-		for (int i = 0; i < n; i += 2) {
-			uvs[i] = u + regionUVs[i] * width;
-			uvs[i + 1] = v + regionUVs[i + 1] * height;
-		}
-	}
-
-	/** Triplets of vertex indices which describe the mesh's triangulation. */
-	public short[] getTriangles () {
-		return triangles;
-	}
-
-	public void setTriangles (short[] triangles) {
-		this.triangles = triangles;
-	}
-
-	/** The UV pair for each vertex, normalized within the texture region. */
-	public float[] getRegionUVs () {
-		return regionUVs;
-	}
-
-	/** Sets the texture coordinates for the region. The values are u,v pairs for each vertex. */
-	public void setRegionUVs (float[] regionUVs) {
-		this.regionUVs = regionUVs;
-	}
-
-	/** The UV pair for each vertex, normalized within the entire texture.
-	 * <p>
-	 * See {@link #updateUVs}. */
-	public float[] getUVs () {
-		return uvs;
-	}
-
-	public void setUVs (float[] uvs) {
-		this.uvs = uvs;
-	}
-
-	/** The color to tint the mesh. */
-	public Color getColor () {
-		return color;
-	}
-
-	/** The name of the texture region for this attachment. */
-	public String getPath () {
-		return path;
-	}
-
-	public void setPath (String path) {
-		this.path = path;
-	}
-
-	/** The number of entries at the beginning of {@link #vertices} that make up the mesh hull. */
-	public int getHullLength () {
-		return hullLength;
-	}
-
-	public void setHullLength (int hullLength) {
-		this.hullLength = hullLength;
-	}
-
-	public void setEdges (short[] edges) {
-		this.edges = edges;
-	}
-
-	/** Vertex index pairs describing edges for controlling triangulation, or be null if nonessential data was not exported. Mesh
-	 * triangles will never cross edges. Triangulation is not performed at runtime. */
-	public @Null short[] getEdges () {
-		return edges;
-	}
-
-	/** The width of the mesh's image, or zero if nonessential data was not exported. */
-	public float getWidth () {
-		return width;
-	}
-
-	public void setWidth (float width) {
-		this.width = width;
-	}
-
-	/** The height of the mesh's image, or zero if nonessential data was not exported. */
-	public float getHeight () {
-		return height;
-	}
-
-	public void setHeight (float height) {
-		this.height = height;
-	}
-
-	/** The parent mesh if this is a linked mesh, else null. A linked mesh shares the {@link #bones}, {@link #vertices},
-	 * {@link #regionUVs}, {@link #triangles}, {@link #hullLength}, {@link #edges}, {@link #width}, and {@link #height} with the
-	 * parent mesh, but may have a different {@link #name} or {@link #path} (and therefore a different texture). */
-	public @Null MeshAttachment getParentMesh () {
-		return parentMesh;
-	}
-
-	public void setParentMesh (@Null MeshAttachment parentMesh) {
-		this.parentMesh = parentMesh;
-		if (parentMesh != null) {
-			bones = parentMesh.bones;
-			vertices = parentMesh.vertices;
-			regionUVs = parentMesh.regionUVs;
-			triangles = parentMesh.triangles;
-			hullLength = parentMesh.hullLength;
-			worldVerticesLength = parentMesh.worldVerticesLength;
-			edges = parentMesh.edges;
-			width = parentMesh.width;
-			height = parentMesh.height;
-		}
-	}
-
-	public Attachment copy () {
-		if (parentMesh != null) return newLinkedMesh();
-
-		MeshAttachment copy = new MeshAttachment(name);
-		copy.region = region;
-		copy.path = path;
-		copy.color.set(color);
-
-		copyTo(copy);
-		copy.regionUVs = new float[regionUVs.length];
-		arraycopy(regionUVs, 0, copy.regionUVs, 0, regionUVs.length);
-		copy.uvs = new float[uvs.length];
-		arraycopy(uvs, 0, copy.uvs, 0, uvs.length);
-		copy.triangles = new short[triangles.length];
-		arraycopy(triangles, 0, copy.triangles, 0, triangles.length);
-		copy.hullLength = hullLength;
-
-		// Nonessential.
-		if (edges != null) {
-			copy.edges = new short[edges.length];
-			arraycopy(edges, 0, copy.edges, 0, edges.length);
-		}
-		copy.width = width;
-		copy.height = height;
-		return copy;
-	}
-
-	/** Returns a new mesh with the {@link #parentMesh} set to this mesh's parent mesh, if any, else to this mesh. */
-	public MeshAttachment newLinkedMesh () {
-		MeshAttachment mesh = new MeshAttachment(name);
-		mesh.region = region;
-		mesh.path = path;
-		mesh.color.set(color);
-		mesh.deformAttachment = deformAttachment;
-		mesh.setParentMesh(parentMesh != null ? parentMesh : this);
-		mesh.updateUVs();
-		return mesh;
-	}
-}
+/******************************************************************************
+ * Spine Runtimes License Agreement
+ * Last updated January 1, 2020. Replaces all prior versions.
+ *
+ * Copyright (c) 2013-2020, Esoteric Software LLC
+ *
+ * Integration of the Spine Runtimes into software or otherwise creating
+ * derivative works of the Spine Runtimes is permitted under the terms and
+ * conditions of Section 2 of the Spine Editor License Agreement:
+ * http://esotericsoftware.com/spine-editor-license
+ *
+ * Otherwise, it is permitted to integrate the Spine Runtimes into software
+ * or otherwise create derivative works of the Spine Runtimes (collectively,
+ * "Products"), provided that each user of the Products must obtain their own
+ * Spine Editor license and redistribution of the Products in any form must
+ * include this license and copyright notice.
+ *
+ * THE SPINE RUNTIMES ARE PROVIDED BY ESOTERIC SOFTWARE LLC "AS IS" AND ANY
+ * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL ESOTERIC SOFTWARE LLC BE LIABLE FOR ANY
+ * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES,
+ * BUSINESS INTERRUPTION, OR LOSS OF USE, DATA, OR PROFITS) HOWEVER CAUSED AND
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
+ * THE SPINE RUNTIMES, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ *****************************************************************************/
+
+package com.esotericsoftware.spine.attachments;
+
+import static com.esotericsoftware.spine.utils.SpineUtils.*;
+
+import com.badlogic.gdx.graphics.Color;
+import com.badlogic.gdx.graphics.g2d.TextureAtlas.AtlasRegion;
+import com.badlogic.gdx.graphics.g2d.TextureRegion;
+import com.badlogic.gdx.utils.Null;
+
+/** An attachment that displays a textured mesh. A mesh has hull vertices and internal vertices within the hull. Holes are not
+ * supported. Each vertex has UVs (texture coordinates) and triangles are used to map an image on to the mesh.
+ * <p>
+ * See <a href="http://esotericsoftware.com/spine-meshes">Mesh attachments</a> in the Spine User Guide. */
+public class MeshAttachment extends VertexAttachment {
+	private TextureRegion region;
+	private String path;
+	private float[] regionUVs, uvs;
+	private short[] triangles;
+	private final Color color = new Color(1, 1, 1, 1);
+	private int hullLength;
+	private @Null MeshAttachment parentMesh;
+
+	// Nonessential.
+	private @Null short[] edges;
+	private float width, height;
+
+	public MeshAttachment (String name) {
+		super(name);
+	}
+
+	public void setRegion (TextureRegion region) {
+		if (region == null) throw new IllegalArgumentException("region cannot be null.");
+		this.region = region;
+	}
+
+	public TextureRegion getRegion () {
+		if (region == null) throw new IllegalStateException("Region has not been set: " + this);
+		return region;
+	}
+
+	/** Calculates {@link #uvs} using {@link #regionUVs} and the {@link #region}. Must be called after changing the region UVs or
+	 * region. */
+	public void updateUVs () {
+		float[] regionUVs = this.regionUVs;
+		if (this.uvs == null || this.uvs.length != regionUVs.length) this.uvs = new float[regionUVs.length];
+		float[] uvs = this.uvs;
+		int n = uvs.length;
+		float u, v, width, height;
+		if (region instanceof AtlasRegion) {
+			u = region.getU();
+			v = region.getV();
+			AtlasRegion region = (AtlasRegion)this.region;
+			float textureWidth = region.getTexture().getWidth(), textureHeight = region.getTexture().getHeight();
+			switch (region.degrees) {
+			case 90:
+				u -= (region.originalHeight - region.offsetY - region.packedWidth) / textureWidth;
+				v -= (region.originalWidth - region.offsetX - region.packedHeight) / textureHeight;
+				width = region.originalHeight / textureWidth;
+				height = region.originalWidth / textureHeight;
+				for (int i = 0; i < n; i += 2) {
+					uvs[i] = u + regionUVs[i + 1] * width;
+					uvs[i + 1] = v + (1 - regionUVs[i]) * height;
+				}
+				return;
+			case 180:
+				u -= (region.originalWidth - region.offsetX - region.packedWidth) / textureWidth;
+				v -= region.offsetY / textureHeight;
+				width = region.originalWidth / textureWidth;
+				height = region.originalHeight / textureHeight;
+				for (int i = 0; i < n; i += 2) {
+					uvs[i] = u + (1 - regionUVs[i]) * width;
+					uvs[i + 1] = v + (1 - regionUVs[i + 1]) * height;
+				}
+				return;
+			case 270:
+				u -= region.offsetY / textureWidth;
+				v -= region.offsetX / textureHeight;
+				width = region.originalHeight / textureWidth;
+				height = region.originalWidth / textureHeight;
+				for (int i = 0; i < n; i += 2) {
+					uvs[i] = u + (1 - regionUVs[i + 1]) * width;
+					uvs[i + 1] = v + regionUVs[i] * height;
+				}
+				return;
+			}
+			u -= region.offsetX / textureWidth;
+			v -= (region.originalHeight - region.offsetY - region.packedHeight) / textureHeight;
+			width = region.originalWidth / textureWidth;
+			height = region.originalHeight / textureHeight;
+		} else if (region == null) {
+			u = v = 0;
+			width = height = 1;
+		} else {
+			u = region.getU();
+			v = region.getV();
+			width = region.getU2() - u;
+			height = region.getV2() - v;
+		}
+		for (int i = 0; i < n; i += 2) {
+			uvs[i] = u + regionUVs[i] * width;
+			uvs[i + 1] = v + regionUVs[i + 1] * height;
+		}
+	}
+
+	/** Triplets of vertex indices which describe the mesh's triangulation. */
+	public short[] getTriangles () {
+		return triangles;
+	}
+
+	public void setTriangles (short[] triangles) {
+		this.triangles = triangles;
+	}
+
+	/** The UV pair for each vertex, normalized within the texture region. */
+	public float[] getRegionUVs () {
+		return regionUVs;
+	}
+
+	/** Sets the texture coordinates for the region. The values are u,v pairs for each vertex. */
+	public void setRegionUVs (float[] regionUVs) {
+		this.regionUVs = regionUVs;
+	}
+
+	/** The UV pair for each vertex, normalized within the entire texture.
+	 * <p>
+	 * See {@link #updateUVs}. */
+	public float[] getUVs () {
+		return uvs;
+	}
+
+	public void setUVs (float[] uvs) {
+		this.uvs = uvs;
+	}
+
+	/** The color to tint the mesh. */
+	public Color getColor () {
+		return color;
+	}
+
+	/** The name of the texture region for this attachment. */
+	public String getPath () {
+		return path;
+	}
+
+	public void setPath (String path) {
+		this.path = path;
+	}
+
+	/** The number of entries at the beginning of {@link #vertices} that make up the mesh hull. */
+	public int getHullLength () {
+		return hullLength;
+	}
+
+	public void setHullLength (int hullLength) {
+		this.hullLength = hullLength;
+	}
+
+	public void setEdges (short[] edges) {
+		this.edges = edges;
+	}
+
+	/** Vertex index pairs describing edges for controlling triangulation, or be null if nonessential data was not exported. Mesh
+	 * triangles will never cross edges. Triangulation is not performed at runtime. */
+	public @Null short[] getEdges () {
+		return edges;
+	}
+
+	/** The width of the mesh's image, or zero if nonessential data was not exported. */
+	public float getWidth () {
+		return width;
+	}
+
+	public void setWidth (float width) {
+		this.width = width;
+	}
+
+	/** The height of the mesh's image, or zero if nonessential data was not exported. */
+	public float getHeight () {
+		return height;
+	}
+
+	public void setHeight (float height) {
+		this.height = height;
+	}
+
+	/** The parent mesh if this is a linked mesh, else null. A linked mesh shares the {@link #bones}, {@link #vertices},
+	 * {@link #regionUVs}, {@link #triangles}, {@link #hullLength}, {@link #edges}, {@link #width}, and {@link #height} with the
+	 * parent mesh, but may have a different {@link #name} or {@link #path} (and therefore a different texture). */
+	public @Null MeshAttachment getParentMesh () {
+		return parentMesh;
+	}
+
+	public void setParentMesh (@Null MeshAttachment parentMesh) {
+		this.parentMesh = parentMesh;
+		if (parentMesh != null) {
+			bones = parentMesh.bones;
+			vertices = parentMesh.vertices;
+			regionUVs = parentMesh.regionUVs;
+			triangles = parentMesh.triangles;
+			hullLength = parentMesh.hullLength;
+			worldVerticesLength = parentMesh.worldVerticesLength;
+			edges = parentMesh.edges;
+			width = parentMesh.width;
+			height = parentMesh.height;
+		}
+	}
+
+	public Attachment copy () {
+		if (parentMesh != null) return newLinkedMesh();
+
+		MeshAttachment copy = new MeshAttachment(name);
+		copy.region = region;
+		copy.path = path;
+		copy.color.set(color);
+
+		copyTo(copy);
+		copy.regionUVs = new float[regionUVs.length];
+		arraycopy(regionUVs, 0, copy.regionUVs, 0, regionUVs.length);
+		copy.uvs = new float[uvs.length];
+		arraycopy(uvs, 0, copy.uvs, 0, uvs.length);
+		copy.triangles = new short[triangles.length];
+		arraycopy(triangles, 0, copy.triangles, 0, triangles.length);
+		copy.hullLength = hullLength;
+
+		// Nonessential.
+		if (edges != null) {
+			copy.edges = new short[edges.length];
+			arraycopy(edges, 0, copy.edges, 0, edges.length);
+		}
+		copy.width = width;
+		copy.height = height;
+		return copy;
+	}
+
+	/** Returns a new mesh with the {@link #parentMesh} set to this mesh's parent mesh, if any, else to this mesh. */
+	public MeshAttachment newLinkedMesh () {
+		MeshAttachment mesh = new MeshAttachment(name);
+		mesh.region = region;
+		mesh.path = path;
+		mesh.color.set(color);
+		mesh.deformAttachment = deformAttachment;
+		mesh.setParentMesh(parentMesh != null ? parentMesh : this);
+		mesh.updateUVs();
+		return mesh;
+	}
+}

+ 96 - 96
spine-libgdx/spine-libgdx/src/com/esotericsoftware/spine/attachments/PathAttachment.java

@@ -1,96 +1,96 @@
-/******************************************************************************
- * Spine Runtimes License Agreement
- * Last updated January 1, 2020. Replaces all prior versions.
- *
- * Copyright (c) 2013-2020, Esoteric Software LLC
- *
- * Integration of the Spine Runtimes into software or otherwise creating
- * derivative works of the Spine Runtimes is permitted under the terms and
- * conditions of Section 2 of the Spine Editor License Agreement:
- * http://esotericsoftware.com/spine-editor-license
- *
- * Otherwise, it is permitted to integrate the Spine Runtimes into software
- * or otherwise create derivative works of the Spine Runtimes (collectively,
- * "Products"), provided that each user of the Products must obtain their own
- * Spine Editor license and redistribution of the Products in any form must
- * include this license and copyright notice.
- *
- * THE SPINE RUNTIMES ARE PROVIDED BY ESOTERIC SOFTWARE LLC "AS IS" AND ANY
- * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
- * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
- * DISCLAIMED. IN NO EVENT SHALL ESOTERIC SOFTWARE LLC BE LIABLE FOR ANY
- * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
- * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES,
- * BUSINESS INTERRUPTION, OR LOSS OF USE, DATA, OR PROFITS) HOWEVER CAUSED AND
- * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
- * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
- * THE SPINE RUNTIMES, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
- *****************************************************************************/
-
-package com.esotericsoftware.spine.attachments;
-
-import static com.esotericsoftware.spine.utils.SpineUtils.*;
-
-import com.badlogic.gdx.graphics.Color;
-
-import com.esotericsoftware.spine.PathConstraint;
-
-/** An attachment whose vertices make up a composite Bezier curve.
- * <p>
- * See {@link PathConstraint} and <a href="http://esotericsoftware.com/spine-paths">Paths</a> in the Spine User Guide. */
-public class PathAttachment extends VertexAttachment {
-	float[] lengths;
-	boolean closed, constantSpeed;
-
-	// Nonessential.
-	final Color color = new Color(1, 0.5f, 0, 1); // ff7f00ff
-
-	public PathAttachment (String name) {
-		super(name);
-	}
-
-	/** If true, the start and end knots are connected. */
-	public boolean getClosed () {
-		return closed;
-	}
-
-	public void setClosed (boolean closed) {
-		this.closed = closed;
-	}
-
-	/** If true, additional calculations are performed to make computing positions along the path more accurate and movement along
-	 * the path have a constant speed. */
-	public boolean getConstantSpeed () {
-		return constantSpeed;
-	}
-
-	public void setConstantSpeed (boolean constantSpeed) {
-		this.constantSpeed = constantSpeed;
-	}
-
-	/** The lengths along the path in the setup pose from the start of the path to the end of each Bezier curve. */
-	public float[] getLengths () {
-		return lengths;
-	}
-
-	public void setLengths (float[] lengths) {
-		this.lengths = lengths;
-	}
-
-	/** The color of the path as it was in Spine, or a default color if nonessential data was not exported. Paths are not usually
-	 * rendered at runtime. */
-	public Color getColor () {
-		return color;
-	}
-
-	public Attachment copy () {
-		PathAttachment copy = new PathAttachment(name);
-		copyTo(copy);
-		copy.lengths = new float[lengths.length];
-		arraycopy(lengths, 0, copy.lengths, 0, lengths.length);
-		copy.closed = closed;
-		copy.constantSpeed = constantSpeed;
-		copy.color.set(color);
-		return copy;
-	}
-}
+/******************************************************************************
+ * Spine Runtimes License Agreement
+ * Last updated January 1, 2020. Replaces all prior versions.
+ *
+ * Copyright (c) 2013-2020, Esoteric Software LLC
+ *
+ * Integration of the Spine Runtimes into software or otherwise creating
+ * derivative works of the Spine Runtimes is permitted under the terms and
+ * conditions of Section 2 of the Spine Editor License Agreement:
+ * http://esotericsoftware.com/spine-editor-license
+ *
+ * Otherwise, it is permitted to integrate the Spine Runtimes into software
+ * or otherwise create derivative works of the Spine Runtimes (collectively,
+ * "Products"), provided that each user of the Products must obtain their own
+ * Spine Editor license and redistribution of the Products in any form must
+ * include this license and copyright notice.
+ *
+ * THE SPINE RUNTIMES ARE PROVIDED BY ESOTERIC SOFTWARE LLC "AS IS" AND ANY
+ * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL ESOTERIC SOFTWARE LLC BE LIABLE FOR ANY
+ * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES,
+ * BUSINESS INTERRUPTION, OR LOSS OF USE, DATA, OR PROFITS) HOWEVER CAUSED AND
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
+ * THE SPINE RUNTIMES, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ *****************************************************************************/
+
+package com.esotericsoftware.spine.attachments;
+
+import static com.esotericsoftware.spine.utils.SpineUtils.*;
+
+import com.badlogic.gdx.graphics.Color;
+
+import com.esotericsoftware.spine.PathConstraint;
+
+/** An attachment whose vertices make up a composite Bezier curve.
+ * <p>
+ * See {@link PathConstraint} and <a href="http://esotericsoftware.com/spine-paths">Paths</a> in the Spine User Guide. */
+public class PathAttachment extends VertexAttachment {
+	float[] lengths;
+	boolean closed, constantSpeed;
+
+	// Nonessential.
+	final Color color = new Color(1, 0.5f, 0, 1); // ff7f00ff
+
+	public PathAttachment (String name) {
+		super(name);
+	}
+
+	/** If true, the start and end knots are connected. */
+	public boolean getClosed () {
+		return closed;
+	}
+
+	public void setClosed (boolean closed) {
+		this.closed = closed;
+	}
+
+	/** If true, additional calculations are performed to make computing positions along the path more accurate and movement along
+	 * the path have a constant speed. */
+	public boolean getConstantSpeed () {
+		return constantSpeed;
+	}
+
+	public void setConstantSpeed (boolean constantSpeed) {
+		this.constantSpeed = constantSpeed;
+	}
+
+	/** The lengths along the path in the setup pose from the start of the path to the end of each Bezier curve. */
+	public float[] getLengths () {
+		return lengths;
+	}
+
+	public void setLengths (float[] lengths) {
+		this.lengths = lengths;
+	}
+
+	/** The color of the path as it was in Spine, or a default color if nonessential data was not exported. Paths are not usually
+	 * rendered at runtime. */
+	public Color getColor () {
+		return color;
+	}
+
+	public Attachment copy () {
+		PathAttachment copy = new PathAttachment(name);
+		copyTo(copy);
+		copy.lengths = new float[lengths.length];
+		arraycopy(lengths, 0, copy.lengths, 0, lengths.length);
+		copy.closed = closed;
+		copy.constantSpeed = constantSpeed;
+		copy.color.set(color);
+		return copy;
+	}
+}

+ 285 - 285
spine-libgdx/spine-libgdx/src/com/esotericsoftware/spine/attachments/RegionAttachment.java

@@ -1,285 +1,285 @@
-/******************************************************************************
- * Spine Runtimes License Agreement
- * Last updated January 1, 2020. Replaces all prior versions.
- *
- * Copyright (c) 2013-2020, Esoteric Software LLC
- *
- * Integration of the Spine Runtimes into software or otherwise creating
- * derivative works of the Spine Runtimes is permitted under the terms and
- * conditions of Section 2 of the Spine Editor License Agreement:
- * http://esotericsoftware.com/spine-editor-license
- *
- * Otherwise, it is permitted to integrate the Spine Runtimes into software
- * or otherwise create derivative works of the Spine Runtimes (collectively,
- * "Products"), provided that each user of the Products must obtain their own
- * Spine Editor license and redistribution of the Products in any form must
- * include this license and copyright notice.
- *
- * THE SPINE RUNTIMES ARE PROVIDED BY ESOTERIC SOFTWARE LLC "AS IS" AND ANY
- * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
- * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
- * DISCLAIMED. IN NO EVENT SHALL ESOTERIC SOFTWARE LLC BE LIABLE FOR ANY
- * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
- * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES,
- * BUSINESS INTERRUPTION, OR LOSS OF USE, DATA, OR PROFITS) HOWEVER CAUSED AND
- * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
- * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
- * THE SPINE RUNTIMES, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
- *****************************************************************************/
-
-package com.esotericsoftware.spine.attachments;
-
-import static com.esotericsoftware.spine.utils.SpineUtils.*;
-
-import com.badlogic.gdx.graphics.Color;
-import com.badlogic.gdx.graphics.g2d.TextureAtlas.AtlasRegion;
-import com.badlogic.gdx.graphics.g2d.TextureRegion;
-
-import com.esotericsoftware.spine.Bone;
-
-/** An attachment that displays a textured quadrilateral.
- * <p>
- * See <a href="http://esotericsoftware.com/spine-regions">Region attachments</a> in the Spine User Guide. */
-public class RegionAttachment extends Attachment {
-	static public final int BLX = 0;
-	static public final int BLY = 1;
-	static public final int ULX = 2;
-	static public final int ULY = 3;
-	static public final int URX = 4;
-	static public final int URY = 5;
-	static public final int BRX = 6;
-	static public final int BRY = 7;
-
-	private TextureRegion region;
-	private String path;
-	private float x, y, scaleX = 1, scaleY = 1, rotation, width, height;
-	private final float[] uvs = new float[8];
-	private final float[] offset = new float[8];
-	private final Color color = new Color(1, 1, 1, 1);
-
-	public RegionAttachment (String name) {
-		super(name);
-	}
-
-	/** Calculates the {@link #offset} using the region settings. Must be called after changing region settings. */
-	public void updateOffset () {
-		float width = getWidth();
-		float height = getHeight();
-		float localX2 = width / 2;
-		float localY2 = height / 2;
-		float localX = -localX2;
-		float localY = -localY2;
-		if (region instanceof AtlasRegion) {
-			AtlasRegion region = (AtlasRegion)this.region;
-			localX += region.offsetX / region.originalWidth * width;
-			localY += region.offsetY / region.originalHeight * height;
-			if (region.degrees == 90) {
-				localX2 -= (region.originalWidth - region.offsetX - region.packedHeight) / region.originalWidth * width;
-				localY2 -= (region.originalHeight - region.offsetY - region.packedWidth) / region.originalHeight * height;
-			} else {
-				localX2 -= (region.originalWidth - region.offsetX - region.packedWidth) / region.originalWidth * width;
-				localY2 -= (region.originalHeight - region.offsetY - region.packedHeight) / region.originalHeight * height;
-			}
-		}
-		float scaleX = getScaleX();
-		float scaleY = getScaleY();
-		localX *= scaleX;
-		localY *= scaleY;
-		localX2 *= scaleX;
-		localY2 *= scaleY;
-		float rotation = getRotation();
-		float cos = (float)Math.cos(degRad * rotation);
-		float sin = (float)Math.sin(degRad * rotation);
-		float x = getX();
-		float y = getY();
-		float localXCos = localX * cos + x;
-		float localXSin = localX * sin;
-		float localYCos = localY * cos + y;
-		float localYSin = localY * sin;
-		float localX2Cos = localX2 * cos + x;
-		float localX2Sin = localX2 * sin;
-		float localY2Cos = localY2 * cos + y;
-		float localY2Sin = localY2 * sin;
-		float[] offset = this.offset;
-		offset[BLX] = localXCos - localYSin;
-		offset[BLY] = localYCos + localXSin;
-		offset[ULX] = localXCos - localY2Sin;
-		offset[ULY] = localY2Cos + localXSin;
-		offset[URX] = localX2Cos - localY2Sin;
-		offset[URY] = localY2Cos + localX2Sin;
-		offset[BRX] = localX2Cos - localYSin;
-		offset[BRY] = localYCos + localX2Sin;
-	}
-
-	public void setRegion (TextureRegion region) {
-		if (region == null) throw new IllegalArgumentException("region cannot be null.");
-		this.region = region;
-		float[] uvs = this.uvs;
-		if (region instanceof AtlasRegion && ((AtlasRegion)region).degrees == 90) {
-			uvs[URX] = region.getU();
-			uvs[URY] = region.getV2();
-			uvs[BRX] = region.getU();
-			uvs[BRY] = region.getV();
-			uvs[BLX] = region.getU2();
-			uvs[BLY] = region.getV();
-			uvs[ULX] = region.getU2();
-			uvs[ULY] = region.getV2();
-		} else {
-			uvs[ULX] = region.getU();
-			uvs[ULY] = region.getV2();
-			uvs[URX] = region.getU();
-			uvs[URY] = region.getV();
-			uvs[BRX] = region.getU2();
-			uvs[BRY] = region.getV();
-			uvs[BLX] = region.getU2();
-			uvs[BLY] = region.getV2();
-		}
-	}
-
-	public TextureRegion getRegion () {
-		if (region == null) throw new IllegalStateException("Region has not been set: " + this);
-		return region;
-	}
-
-	/** Transforms the attachment's four vertices to world coordinates.
-	 * <p>
-	 * See <a href="http://esotericsoftware.com/spine-runtime-skeletons#World-transforms">World transforms</a> in the Spine
-	 * Runtimes Guide.
-	 * @param worldVertices The output world vertices. Must have a length >= <code>offset</code> + 8.
-	 * @param offset The <code>worldVertices</code> index to begin writing values.
-	 * @param stride The number of <code>worldVertices</code> entries between the value pairs written. */
-	public void computeWorldVertices (Bone bone, float[] worldVertices, int offset, int stride) {
-		float[] vertexOffset = this.offset;
-		float x = bone.getWorldX(), y = bone.getWorldY();
-		float a = bone.getA(), b = bone.getB(), c = bone.getC(), d = bone.getD();
-		float offsetX, offsetY;
-
-		offsetX = vertexOffset[BRX];
-		offsetY = vertexOffset[BRY];
-		worldVertices[offset] = offsetX * a + offsetY * b + x; // br
-		worldVertices[offset + 1] = offsetX * c + offsetY * d + y;
-		offset += stride;
-
-		offsetX = vertexOffset[BLX];
-		offsetY = vertexOffset[BLY];
-		worldVertices[offset] = offsetX * a + offsetY * b + x; // bl
-		worldVertices[offset + 1] = offsetX * c + offsetY * d + y;
-		offset += stride;
-
-		offsetX = vertexOffset[ULX];
-		offsetY = vertexOffset[ULY];
-		worldVertices[offset] = offsetX * a + offsetY * b + x; // ul
-		worldVertices[offset + 1] = offsetX * c + offsetY * d + y;
-		offset += stride;
-
-		offsetX = vertexOffset[URX];
-		offsetY = vertexOffset[URY];
-		worldVertices[offset] = offsetX * a + offsetY * b + x; // ur
-		worldVertices[offset + 1] = offsetX * c + offsetY * d + y;
-	}
-
-	/** For each of the 4 vertices, a pair of <code>x,y</code> values that is the local position of the vertex.
-	 * <p>
-	 * See {@link #updateOffset()}. */
-	public float[] getOffset () {
-		return offset;
-	}
-
-	public float[] getUVs () {
-		return uvs;
-	}
-
-	/** The local x translation. */
-	public float getX () {
-		return x;
-	}
-
-	public void setX (float x) {
-		this.x = x;
-	}
-
-	/** The local y translation. */
-	public float getY () {
-		return y;
-	}
-
-	public void setY (float y) {
-		this.y = y;
-	}
-
-	/** The local scaleX. */
-	public float getScaleX () {
-		return scaleX;
-	}
-
-	public void setScaleX (float scaleX) {
-		this.scaleX = scaleX;
-	}
-
-	/** The local scaleY. */
-	public float getScaleY () {
-		return scaleY;
-	}
-
-	public void setScaleY (float scaleY) {
-		this.scaleY = scaleY;
-	}
-
-	/** The local rotation. */
-	public float getRotation () {
-		return rotation;
-	}
-
-	public void setRotation (float rotation) {
-		this.rotation = rotation;
-	}
-
-	/** The width of the region attachment in Spine. */
-	public float getWidth () {
-		return width;
-	}
-
-	public void setWidth (float width) {
-		this.width = width;
-	}
-
-	/** The height of the region attachment in Spine. */
-	public float getHeight () {
-		return height;
-	}
-
-	public void setHeight (float height) {
-		this.height = height;
-	}
-
-	/** The color to tint the region attachment. */
-	public Color getColor () {
-		return color;
-	}
-
-	/** The name of the texture region for this attachment. */
-	public String getPath () {
-		return path;
-	}
-
-	public void setPath (String path) {
-		this.path = path;
-	}
-
-	public Attachment copy () {
-		RegionAttachment copy = new RegionAttachment(name);
-		copy.region = region;
-		copy.path = path;
-		copy.x = x;
-		copy.y = y;
-		copy.scaleX = scaleX;
-		copy.scaleY = scaleY;
-		copy.rotation = rotation;
-		copy.width = width;
-		copy.height = height;
-		arraycopy(uvs, 0, copy.uvs, 0, 8);
-		arraycopy(offset, 0, copy.offset, 0, 8);
-		copy.color.set(color);
-		return copy;
-	}
-}
+/******************************************************************************
+ * Spine Runtimes License Agreement
+ * Last updated January 1, 2020. Replaces all prior versions.
+ *
+ * Copyright (c) 2013-2020, Esoteric Software LLC
+ *
+ * Integration of the Spine Runtimes into software or otherwise creating
+ * derivative works of the Spine Runtimes is permitted under the terms and
+ * conditions of Section 2 of the Spine Editor License Agreement:
+ * http://esotericsoftware.com/spine-editor-license
+ *
+ * Otherwise, it is permitted to integrate the Spine Runtimes into software
+ * or otherwise create derivative works of the Spine Runtimes (collectively,
+ * "Products"), provided that each user of the Products must obtain their own
+ * Spine Editor license and redistribution of the Products in any form must
+ * include this license and copyright notice.
+ *
+ * THE SPINE RUNTIMES ARE PROVIDED BY ESOTERIC SOFTWARE LLC "AS IS" AND ANY
+ * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL ESOTERIC SOFTWARE LLC BE LIABLE FOR ANY
+ * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES,
+ * BUSINESS INTERRUPTION, OR LOSS OF USE, DATA, OR PROFITS) HOWEVER CAUSED AND
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
+ * THE SPINE RUNTIMES, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ *****************************************************************************/
+
+package com.esotericsoftware.spine.attachments;
+
+import static com.esotericsoftware.spine.utils.SpineUtils.*;
+
+import com.badlogic.gdx.graphics.Color;
+import com.badlogic.gdx.graphics.g2d.TextureAtlas.AtlasRegion;
+import com.badlogic.gdx.graphics.g2d.TextureRegion;
+
+import com.esotericsoftware.spine.Bone;
+
+/** An attachment that displays a textured quadrilateral.
+ * <p>
+ * See <a href="http://esotericsoftware.com/spine-regions">Region attachments</a> in the Spine User Guide. */
+public class RegionAttachment extends Attachment {
+	static public final int BLX = 0;
+	static public final int BLY = 1;
+	static public final int ULX = 2;
+	static public final int ULY = 3;
+	static public final int URX = 4;
+	static public final int URY = 5;
+	static public final int BRX = 6;
+	static public final int BRY = 7;
+
+	private TextureRegion region;
+	private String path;
+	private float x, y, scaleX = 1, scaleY = 1, rotation, width, height;
+	private final float[] uvs = new float[8];
+	private final float[] offset = new float[8];
+	private final Color color = new Color(1, 1, 1, 1);
+
+	public RegionAttachment (String name) {
+		super(name);
+	}
+
+	/** Calculates the {@link #offset} using the region settings. Must be called after changing region settings. */
+	public void updateOffset () {
+		float width = getWidth();
+		float height = getHeight();
+		float localX2 = width / 2;
+		float localY2 = height / 2;
+		float localX = -localX2;
+		float localY = -localY2;
+		if (region instanceof AtlasRegion) {
+			AtlasRegion region = (AtlasRegion)this.region;
+			localX += region.offsetX / region.originalWidth * width;
+			localY += region.offsetY / region.originalHeight * height;
+			if (region.degrees == 90) {
+				localX2 -= (region.originalWidth - region.offsetX - region.packedHeight) / region.originalWidth * width;
+				localY2 -= (region.originalHeight - region.offsetY - region.packedWidth) / region.originalHeight * height;
+			} else {
+				localX2 -= (region.originalWidth - region.offsetX - region.packedWidth) / region.originalWidth * width;
+				localY2 -= (region.originalHeight - region.offsetY - region.packedHeight) / region.originalHeight * height;
+			}
+		}
+		float scaleX = getScaleX();
+		float scaleY = getScaleY();
+		localX *= scaleX;
+		localY *= scaleY;
+		localX2 *= scaleX;
+		localY2 *= scaleY;
+		float rotation = getRotation();
+		float cos = (float)Math.cos(degRad * rotation);
+		float sin = (float)Math.sin(degRad * rotation);
+		float x = getX();
+		float y = getY();
+		float localXCos = localX * cos + x;
+		float localXSin = localX * sin;
+		float localYCos = localY * cos + y;
+		float localYSin = localY * sin;
+		float localX2Cos = localX2 * cos + x;
+		float localX2Sin = localX2 * sin;
+		float localY2Cos = localY2 * cos + y;
+		float localY2Sin = localY2 * sin;
+		float[] offset = this.offset;
+		offset[BLX] = localXCos - localYSin;
+		offset[BLY] = localYCos + localXSin;
+		offset[ULX] = localXCos - localY2Sin;
+		offset[ULY] = localY2Cos + localXSin;
+		offset[URX] = localX2Cos - localY2Sin;
+		offset[URY] = localY2Cos + localX2Sin;
+		offset[BRX] = localX2Cos - localYSin;
+		offset[BRY] = localYCos + localX2Sin;
+	}
+
+	public void setRegion (TextureRegion region) {
+		if (region == null) throw new IllegalArgumentException("region cannot be null.");
+		this.region = region;
+		float[] uvs = this.uvs;
+		if (region instanceof AtlasRegion && ((AtlasRegion)region).degrees == 90) {
+			uvs[URX] = region.getU();
+			uvs[URY] = region.getV2();
+			uvs[BRX] = region.getU();
+			uvs[BRY] = region.getV();
+			uvs[BLX] = region.getU2();
+			uvs[BLY] = region.getV();
+			uvs[ULX] = region.getU2();
+			uvs[ULY] = region.getV2();
+		} else {
+			uvs[ULX] = region.getU();
+			uvs[ULY] = region.getV2();
+			uvs[URX] = region.getU();
+			uvs[URY] = region.getV();
+			uvs[BRX] = region.getU2();
+			uvs[BRY] = region.getV();
+			uvs[BLX] = region.getU2();
+			uvs[BLY] = region.getV2();
+		}
+	}
+
+	public TextureRegion getRegion () {
+		if (region == null) throw new IllegalStateException("Region has not been set: " + this);
+		return region;
+	}
+
+	/** Transforms the attachment's four vertices to world coordinates.
+	 * <p>
+	 * See <a href="http://esotericsoftware.com/spine-runtime-skeletons#World-transforms">World transforms</a> in the Spine
+	 * Runtimes Guide.
+	 * @param worldVertices The output world vertices. Must have a length >= <code>offset</code> + 8.
+	 * @param offset The <code>worldVertices</code> index to begin writing values.
+	 * @param stride The number of <code>worldVertices</code> entries between the value pairs written. */
+	public void computeWorldVertices (Bone bone, float[] worldVertices, int offset, int stride) {
+		float[] vertexOffset = this.offset;
+		float x = bone.getWorldX(), y = bone.getWorldY();
+		float a = bone.getA(), b = bone.getB(), c = bone.getC(), d = bone.getD();
+		float offsetX, offsetY;
+
+		offsetX = vertexOffset[BRX];
+		offsetY = vertexOffset[BRY];
+		worldVertices[offset] = offsetX * a + offsetY * b + x; // br
+		worldVertices[offset + 1] = offsetX * c + offsetY * d + y;
+		offset += stride;
+
+		offsetX = vertexOffset[BLX];
+		offsetY = vertexOffset[BLY];
+		worldVertices[offset] = offsetX * a + offsetY * b + x; // bl
+		worldVertices[offset + 1] = offsetX * c + offsetY * d + y;
+		offset += stride;
+
+		offsetX = vertexOffset[ULX];
+		offsetY = vertexOffset[ULY];
+		worldVertices[offset] = offsetX * a + offsetY * b + x; // ul
+		worldVertices[offset + 1] = offsetX * c + offsetY * d + y;
+		offset += stride;
+
+		offsetX = vertexOffset[URX];
+		offsetY = vertexOffset[URY];
+		worldVertices[offset] = offsetX * a + offsetY * b + x; // ur
+		worldVertices[offset + 1] = offsetX * c + offsetY * d + y;
+	}
+
+	/** For each of the 4 vertices, a pair of <code>x,y</code> values that is the local position of the vertex.
+	 * <p>
+	 * See {@link #updateOffset()}. */
+	public float[] getOffset () {
+		return offset;
+	}
+
+	public float[] getUVs () {
+		return uvs;
+	}
+
+	/** The local x translation. */
+	public float getX () {
+		return x;
+	}
+
+	public void setX (float x) {
+		this.x = x;
+	}
+
+	/** The local y translation. */
+	public float getY () {
+		return y;
+	}
+
+	public void setY (float y) {
+		this.y = y;
+	}
+
+	/** The local scaleX. */
+	public float getScaleX () {
+		return scaleX;
+	}
+
+	public void setScaleX (float scaleX) {
+		this.scaleX = scaleX;
+	}
+
+	/** The local scaleY. */
+	public float getScaleY () {
+		return scaleY;
+	}
+
+	public void setScaleY (float scaleY) {
+		this.scaleY = scaleY;
+	}
+
+	/** The local rotation. */
+	public float getRotation () {
+		return rotation;
+	}
+
+	public void setRotation (float rotation) {
+		this.rotation = rotation;
+	}
+
+	/** The width of the region attachment in Spine. */
+	public float getWidth () {
+		return width;
+	}
+
+	public void setWidth (float width) {
+		this.width = width;
+	}
+
+	/** The height of the region attachment in Spine. */
+	public float getHeight () {
+		return height;
+	}
+
+	public void setHeight (float height) {
+		this.height = height;
+	}
+
+	/** The color to tint the region attachment. */
+	public Color getColor () {
+		return color;
+	}
+
+	/** The name of the texture region for this attachment. */
+	public String getPath () {
+		return path;
+	}
+
+	public void setPath (String path) {
+		this.path = path;
+	}
+
+	public Attachment copy () {
+		RegionAttachment copy = new RegionAttachment(name);
+		copy.region = region;
+		copy.path = path;
+		copy.x = x;
+		copy.y = y;
+		copy.scaleX = scaleX;
+		copy.scaleY = scaleY;
+		copy.rotation = rotation;
+		copy.width = width;
+		copy.height = height;
+		arraycopy(uvs, 0, copy.uvs, 0, 8);
+		arraycopy(offset, 0, copy.offset, 0, 8);
+		copy.color.set(color);
+		return copy;
+	}
+}

+ 58 - 58
spine-libgdx/spine-libgdx/src/com/esotericsoftware/spine/attachments/SkeletonAttachment.java

@@ -1,58 +1,58 @@
-/******************************************************************************
- * Spine Runtimes License Agreement
- * Last updated January 1, 2020. Replaces all prior versions.
- *
- * Copyright (c) 2013-2020, Esoteric Software LLC
- *
- * Integration of the Spine Runtimes into software or otherwise creating
- * derivative works of the Spine Runtimes is permitted under the terms and
- * conditions of Section 2 of the Spine Editor License Agreement:
- * http://esotericsoftware.com/spine-editor-license
- *
- * Otherwise, it is permitted to integrate the Spine Runtimes into software
- * or otherwise create derivative works of the Spine Runtimes (collectively,
- * "Products"), provided that each user of the Products must obtain their own
- * Spine Editor license and redistribution of the Products in any form must
- * include this license and copyright notice.
- *
- * THE SPINE RUNTIMES ARE PROVIDED BY ESOTERIC SOFTWARE LLC "AS IS" AND ANY
- * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
- * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
- * DISCLAIMED. IN NO EVENT SHALL ESOTERIC SOFTWARE LLC BE LIABLE FOR ANY
- * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
- * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES,
- * BUSINESS INTERRUPTION, OR LOSS OF USE, DATA, OR PROFITS) HOWEVER CAUSED AND
- * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
- * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
- * THE SPINE RUNTIMES, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
- *****************************************************************************/
-
-package com.esotericsoftware.spine.attachments;
-
-import com.badlogic.gdx.utils.Null;
-
-import com.esotericsoftware.spine.Skeleton;
-
-/** Attachment that displays a skeleton. */
-public class SkeletonAttachment extends Attachment {
-	private @Null Skeleton skeleton;
-
-	public SkeletonAttachment (String name) {
-		super(name);
-	}
-
-	/** @return May return null. */
-	public Skeleton getSkeleton () {
-		return skeleton;
-	}
-
-	public void setSkeleton (@Null Skeleton skeleton) {
-		this.skeleton = skeleton;
-	}
-
-	public Attachment copy () {
-		SkeletonAttachment copy = new SkeletonAttachment(name);
-		copy.skeleton = skeleton;
-		return copy;
-	}
-}
+/******************************************************************************
+ * Spine Runtimes License Agreement
+ * Last updated January 1, 2020. Replaces all prior versions.
+ *
+ * Copyright (c) 2013-2020, Esoteric Software LLC
+ *
+ * Integration of the Spine Runtimes into software or otherwise creating
+ * derivative works of the Spine Runtimes is permitted under the terms and
+ * conditions of Section 2 of the Spine Editor License Agreement:
+ * http://esotericsoftware.com/spine-editor-license
+ *
+ * Otherwise, it is permitted to integrate the Spine Runtimes into software
+ * or otherwise create derivative works of the Spine Runtimes (collectively,
+ * "Products"), provided that each user of the Products must obtain their own
+ * Spine Editor license and redistribution of the Products in any form must
+ * include this license and copyright notice.
+ *
+ * THE SPINE RUNTIMES ARE PROVIDED BY ESOTERIC SOFTWARE LLC "AS IS" AND ANY
+ * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL ESOTERIC SOFTWARE LLC BE LIABLE FOR ANY
+ * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES,
+ * BUSINESS INTERRUPTION, OR LOSS OF USE, DATA, OR PROFITS) HOWEVER CAUSED AND
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
+ * THE SPINE RUNTIMES, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ *****************************************************************************/
+
+package com.esotericsoftware.spine.attachments;
+
+import com.badlogic.gdx.utils.Null;
+
+import com.esotericsoftware.spine.Skeleton;
+
+/** Attachment that displays a skeleton. */
+public class SkeletonAttachment extends Attachment {
+	private @Null Skeleton skeleton;
+
+	public SkeletonAttachment (String name) {
+		super(name);
+	}
+
+	/** @return May return null. */
+	public Skeleton getSkeleton () {
+		return skeleton;
+	}
+
+	public void setSkeleton (@Null Skeleton skeleton) {
+		this.skeleton = skeleton;
+	}
+
+	public Attachment copy () {
+		SkeletonAttachment copy = new SkeletonAttachment(name);
+		copy.skeleton = skeleton;
+		return copy;
+	}
+}

+ 193 - 193
spine-libgdx/spine-libgdx/src/com/esotericsoftware/spine/attachments/VertexAttachment.java

@@ -1,193 +1,193 @@
-/******************************************************************************
- * Spine Runtimes License Agreement
- * Last updated January 1, 2020. Replaces all prior versions.
- *
- * Copyright (c) 2013-2020, Esoteric Software LLC
- *
- * Integration of the Spine Runtimes into software or otherwise creating
- * derivative works of the Spine Runtimes is permitted under the terms and
- * conditions of Section 2 of the Spine Editor License Agreement:
- * http://esotericsoftware.com/spine-editor-license
- *
- * Otherwise, it is permitted to integrate the Spine Runtimes into software
- * or otherwise create derivative works of the Spine Runtimes (collectively,
- * "Products"), provided that each user of the Products must obtain their own
- * Spine Editor license and redistribution of the Products in any form must
- * include this license and copyright notice.
- *
- * THE SPINE RUNTIMES ARE PROVIDED BY ESOTERIC SOFTWARE LLC "AS IS" AND ANY
- * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
- * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
- * DISCLAIMED. IN NO EVENT SHALL ESOTERIC SOFTWARE LLC BE LIABLE FOR ANY
- * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
- * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES,
- * BUSINESS INTERRUPTION, OR LOSS OF USE, DATA, OR PROFITS) HOWEVER CAUSED AND
- * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
- * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
- * THE SPINE RUNTIMES, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
- *****************************************************************************/
-
-package com.esotericsoftware.spine.attachments;
-
-import static com.esotericsoftware.spine.utils.SpineUtils.*;
-
-import com.badlogic.gdx.utils.FloatArray;
-import com.badlogic.gdx.utils.Null;
-
-import com.esotericsoftware.spine.Bone;
-import com.esotericsoftware.spine.Skeleton;
-import com.esotericsoftware.spine.Slot;
-
-/** Base class for an attachment with vertices that are transformed by one or more bones and can be deformed by a slot's
- * {@link Slot#getDeform()}. */
-abstract public class VertexAttachment extends Attachment {
-	static private int nextID;
-
-	private final int id = nextID();
-	@Null int[] bones;
-	float[] vertices;
-	int worldVerticesLength;
-	@Null VertexAttachment deformAttachment = this;
-
-	public VertexAttachment (String name) {
-		super(name);
-	}
-
-	/** Transforms the attachment's local {@link #getVertices()} to world coordinates. If the slot's {@link Slot#getDeform()} is
-	 * not empty, it is used to deform the vertices.
-	 * <p>
-	 * See <a href="http://esotericsoftware.com/spine-runtime-skeletons#World-transforms">World transforms</a> in the Spine
-	 * Runtimes Guide.
-	 * @param start The index of the first {@link #getVertices()} value to transform. Each vertex has 2 values, x and y.
-	 * @param count The number of world vertex values to output. Must be <= {@link #getWorldVerticesLength()} - <code>start</code>.
-	 * @param worldVertices The output world vertices. Must have a length >= <code>offset</code> + <code>count</code> *
-	 *           <code>stride</code> / 2.
-	 * @param offset The <code>worldVertices</code> index to begin writing values.
-	 * @param stride The number of <code>worldVertices</code> entries between the value pairs written. */
-	public void computeWorldVertices (Slot slot, int start, int count, float[] worldVertices, int offset, int stride) {
-		count = offset + (count >> 1) * stride;
-		FloatArray deformArray = slot.getDeform();
-		float[] vertices = this.vertices;
-		int[] bones = this.bones;
-		if (bones == null) {
-			if (deformArray.size > 0) vertices = deformArray.items;
-			Bone bone = slot.getBone();
-			float x = bone.getWorldX(), y = bone.getWorldY();
-			float a = bone.getA(), b = bone.getB(), c = bone.getC(), d = bone.getD();
-			for (int v = start, w = offset; w < count; v += 2, w += stride) {
-				float vx = vertices[v], vy = vertices[v + 1];
-				worldVertices[w] = vx * a + vy * b + x;
-				worldVertices[w + 1] = vx * c + vy * d + y;
-			}
-			return;
-		}
-		int v = 0, skip = 0;
-		for (int i = 0; i < start; i += 2) {
-			int n = bones[v];
-			v += n + 1;
-			skip += n;
-		}
-		Object[] skeletonBones = slot.getSkeleton().getBones().items;
-		if (deformArray.size == 0) {
-			for (int w = offset, b = skip * 3; w < count; w += stride) {
-				float wx = 0, wy = 0;
-				int n = bones[v++];
-				n += v;
-				for (; v < n; v++, b += 3) {
-					Bone bone = (Bone)skeletonBones[bones[v]];
-					float vx = vertices[b], vy = vertices[b + 1], weight = vertices[b + 2];
-					wx += (vx * bone.getA() + vy * bone.getB() + bone.getWorldX()) * weight;
-					wy += (vx * bone.getC() + vy * bone.getD() + bone.getWorldY()) * weight;
-				}
-				worldVertices[w] = wx;
-				worldVertices[w + 1] = wy;
-			}
-		} else {
-			float[] deform = deformArray.items;
-			for (int w = offset, b = skip * 3, f = skip << 1; w < count; w += stride) {
-				float wx = 0, wy = 0;
-				int n = bones[v++];
-				n += v;
-				for (; v < n; v++, b += 3, f += 2) {
-					Bone bone = (Bone)skeletonBones[bones[v]];
-					float vx = vertices[b] + deform[f], vy = vertices[b + 1] + deform[f + 1], weight = vertices[b + 2];
-					wx += (vx * bone.getA() + vy * bone.getB() + bone.getWorldX()) * weight;
-					wy += (vx * bone.getC() + vy * bone.getD() + bone.getWorldY()) * weight;
-				}
-				worldVertices[w] = wx;
-				worldVertices[w + 1] = wy;
-			}
-		}
-	}
-
-	/** Deform keys for the deform attachment are also applied to this attachment.
-	 * @return May be null if no deform keys should be applied. */
-	public @Null VertexAttachment getDeformAttachment () {
-		return deformAttachment;
-	}
-
-	/** @param deformAttachment May be null if no deform keys should be applied. */
-	public void setDeformAttachment (@Null VertexAttachment deformAttachment) {
-		this.deformAttachment = deformAttachment;
-	}
-
-	/** The bones which affect the {@link #getVertices()}. The array entries are, for each vertex, the number of bones affecting
-	 * the vertex followed by that many bone indices, which is the index of the bone in {@link Skeleton#getBones()}. Will be null
-	 * if this attachment has no weights. */
-	public @Null int[] getBones () {
-		return bones;
-	}
-
-	/** @param bones May be null if this attachment has no weights. */
-	public void setBones (@Null int[] bones) {
-		this.bones = bones;
-	}
-
-	/** The vertex positions in the bone's coordinate system. For a non-weighted attachment, the values are <code>x,y</code>
-	 * entries for each vertex. For a weighted attachment, the values are <code>x,y,weight</code> entries for each bone affecting
-	 * each vertex. */
-	public float[] getVertices () {
-		return vertices;
-	}
-
-	public void setVertices (float[] vertices) {
-		this.vertices = vertices;
-	}
-
-	/** The maximum number of world vertex values that can be output by
-	 * {@link #computeWorldVertices(Slot, int, int, float[], int, int)} using the <code>count</code> parameter. */
-	public int getWorldVerticesLength () {
-		return worldVerticesLength;
-	}
-
-	public void setWorldVerticesLength (int worldVerticesLength) {
-		this.worldVerticesLength = worldVerticesLength;
-	}
-
-	/** Returns a unique ID for this attachment. */
-	public int getId () {
-		return id;
-	}
-
-	/** Does not copy id (generated) or name (set on construction). */
-	void copyTo (VertexAttachment attachment) {
-		if (bones != null) {
-			attachment.bones = new int[bones.length];
-			arraycopy(bones, 0, attachment.bones, 0, bones.length);
-		} else
-			attachment.bones = null;
-
-		if (vertices != null) {
-			attachment.vertices = new float[vertices.length];
-			arraycopy(vertices, 0, attachment.vertices, 0, vertices.length);
-		} else
-			attachment.vertices = null;
-
-		attachment.worldVerticesLength = worldVerticesLength;
-		attachment.deformAttachment = deformAttachment;
-	}
-
-	static private synchronized int nextID () {
-		return nextID++;
-	}
-}
+/******************************************************************************
+ * Spine Runtimes License Agreement
+ * Last updated January 1, 2020. Replaces all prior versions.
+ *
+ * Copyright (c) 2013-2020, Esoteric Software LLC
+ *
+ * Integration of the Spine Runtimes into software or otherwise creating
+ * derivative works of the Spine Runtimes is permitted under the terms and
+ * conditions of Section 2 of the Spine Editor License Agreement:
+ * http://esotericsoftware.com/spine-editor-license
+ *
+ * Otherwise, it is permitted to integrate the Spine Runtimes into software
+ * or otherwise create derivative works of the Spine Runtimes (collectively,
+ * "Products"), provided that each user of the Products must obtain their own
+ * Spine Editor license and redistribution of the Products in any form must
+ * include this license and copyright notice.
+ *
+ * THE SPINE RUNTIMES ARE PROVIDED BY ESOTERIC SOFTWARE LLC "AS IS" AND ANY
+ * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL ESOTERIC SOFTWARE LLC BE LIABLE FOR ANY
+ * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES,
+ * BUSINESS INTERRUPTION, OR LOSS OF USE, DATA, OR PROFITS) HOWEVER CAUSED AND
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
+ * THE SPINE RUNTIMES, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ *****************************************************************************/
+
+package com.esotericsoftware.spine.attachments;
+
+import static com.esotericsoftware.spine.utils.SpineUtils.*;
+
+import com.badlogic.gdx.utils.FloatArray;
+import com.badlogic.gdx.utils.Null;
+
+import com.esotericsoftware.spine.Bone;
+import com.esotericsoftware.spine.Skeleton;
+import com.esotericsoftware.spine.Slot;
+
+/** Base class for an attachment with vertices that are transformed by one or more bones and can be deformed by a slot's
+ * {@link Slot#getDeform()}. */
+abstract public class VertexAttachment extends Attachment {
+	static private int nextID;
+
+	private final int id = nextID();
+	@Null int[] bones;
+	float[] vertices;
+	int worldVerticesLength;
+	@Null VertexAttachment deformAttachment = this;
+
+	public VertexAttachment (String name) {
+		super(name);
+	}
+
+	/** Transforms the attachment's local {@link #getVertices()} to world coordinates. If the slot's {@link Slot#getDeform()} is
+	 * not empty, it is used to deform the vertices.
+	 * <p>
+	 * See <a href="http://esotericsoftware.com/spine-runtime-skeletons#World-transforms">World transforms</a> in the Spine
+	 * Runtimes Guide.
+	 * @param start The index of the first {@link #getVertices()} value to transform. Each vertex has 2 values, x and y.
+	 * @param count The number of world vertex values to output. Must be <= {@link #getWorldVerticesLength()} - <code>start</code>.
+	 * @param worldVertices The output world vertices. Must have a length >= <code>offset</code> + <code>count</code> *
+	 *           <code>stride</code> / 2.
+	 * @param offset The <code>worldVertices</code> index to begin writing values.
+	 * @param stride The number of <code>worldVertices</code> entries between the value pairs written. */
+	public void computeWorldVertices (Slot slot, int start, int count, float[] worldVertices, int offset, int stride) {
+		count = offset + (count >> 1) * stride;
+		FloatArray deformArray = slot.getDeform();
+		float[] vertices = this.vertices;
+		int[] bones = this.bones;
+		if (bones == null) {
+			if (deformArray.size > 0) vertices = deformArray.items;
+			Bone bone = slot.getBone();
+			float x = bone.getWorldX(), y = bone.getWorldY();
+			float a = bone.getA(), b = bone.getB(), c = bone.getC(), d = bone.getD();
+			for (int v = start, w = offset; w < count; v += 2, w += stride) {
+				float vx = vertices[v], vy = vertices[v + 1];
+				worldVertices[w] = vx * a + vy * b + x;
+				worldVertices[w + 1] = vx * c + vy * d + y;
+			}
+			return;
+		}
+		int v = 0, skip = 0;
+		for (int i = 0; i < start; i += 2) {
+			int n = bones[v];
+			v += n + 1;
+			skip += n;
+		}
+		Object[] skeletonBones = slot.getSkeleton().getBones().items;
+		if (deformArray.size == 0) {
+			for (int w = offset, b = skip * 3; w < count; w += stride) {
+				float wx = 0, wy = 0;
+				int n = bones[v++];
+				n += v;
+				for (; v < n; v++, b += 3) {
+					Bone bone = (Bone)skeletonBones[bones[v]];
+					float vx = vertices[b], vy = vertices[b + 1], weight = vertices[b + 2];
+					wx += (vx * bone.getA() + vy * bone.getB() + bone.getWorldX()) * weight;
+					wy += (vx * bone.getC() + vy * bone.getD() + bone.getWorldY()) * weight;
+				}
+				worldVertices[w] = wx;
+				worldVertices[w + 1] = wy;
+			}
+		} else {
+			float[] deform = deformArray.items;
+			for (int w = offset, b = skip * 3, f = skip << 1; w < count; w += stride) {
+				float wx = 0, wy = 0;
+				int n = bones[v++];
+				n += v;
+				for (; v < n; v++, b += 3, f += 2) {
+					Bone bone = (Bone)skeletonBones[bones[v]];
+					float vx = vertices[b] + deform[f], vy = vertices[b + 1] + deform[f + 1], weight = vertices[b + 2];
+					wx += (vx * bone.getA() + vy * bone.getB() + bone.getWorldX()) * weight;
+					wy += (vx * bone.getC() + vy * bone.getD() + bone.getWorldY()) * weight;
+				}
+				worldVertices[w] = wx;
+				worldVertices[w + 1] = wy;
+			}
+		}
+	}
+
+	/** Deform keys for the deform attachment are also applied to this attachment.
+	 * @return May be null if no deform keys should be applied. */
+	public @Null VertexAttachment getDeformAttachment () {
+		return deformAttachment;
+	}
+
+	/** @param deformAttachment May be null if no deform keys should be applied. */
+	public void setDeformAttachment (@Null VertexAttachment deformAttachment) {
+		this.deformAttachment = deformAttachment;
+	}
+
+	/** The bones which affect the {@link #getVertices()}. The array entries are, for each vertex, the number of bones affecting
+	 * the vertex followed by that many bone indices, which is the index of the bone in {@link Skeleton#getBones()}. Will be null
+	 * if this attachment has no weights. */
+	public @Null int[] getBones () {
+		return bones;
+	}
+
+	/** @param bones May be null if this attachment has no weights. */
+	public void setBones (@Null int[] bones) {
+		this.bones = bones;
+	}
+
+	/** The vertex positions in the bone's coordinate system. For a non-weighted attachment, the values are <code>x,y</code>
+	 * entries for each vertex. For a weighted attachment, the values are <code>x,y,weight</code> entries for each bone affecting
+	 * each vertex. */
+	public float[] getVertices () {
+		return vertices;
+	}
+
+	public void setVertices (float[] vertices) {
+		this.vertices = vertices;
+	}
+
+	/** The maximum number of world vertex values that can be output by
+	 * {@link #computeWorldVertices(Slot, int, int, float[], int, int)} using the <code>count</code> parameter. */
+	public int getWorldVerticesLength () {
+		return worldVerticesLength;
+	}
+
+	public void setWorldVerticesLength (int worldVerticesLength) {
+		this.worldVerticesLength = worldVerticesLength;
+	}
+
+	/** Returns a unique ID for this attachment. */
+	public int getId () {
+		return id;
+	}
+
+	/** Does not copy id (generated) or name (set on construction). */
+	void copyTo (VertexAttachment attachment) {
+		if (bones != null) {
+			attachment.bones = new int[bones.length];
+			arraycopy(bones, 0, attachment.bones, 0, bones.length);
+		} else
+			attachment.bones = null;
+
+		if (vertices != null) {
+			attachment.vertices = new float[vertices.length];
+			arraycopy(vertices, 0, attachment.vertices, 0, vertices.length);
+		} else
+			attachment.vertices = null;
+
+		attachment.worldVerticesLength = worldVerticesLength;
+		attachment.deformAttachment = deformAttachment;
+	}
+
+	static private synchronized int nextID () {
+		return nextID++;
+	}
+}

+ 113 - 113
spine-libgdx/spine-libgdx/src/com/esotericsoftware/spine/utils/SkeletonActor.java

@@ -1,113 +1,113 @@
-/******************************************************************************
- * Spine Runtimes License Agreement
- * Last updated January 1, 2020. Replaces all prior versions.
- *
- * Copyright (c) 2013-2020, Esoteric Software LLC
- *
- * Integration of the Spine Runtimes into software or otherwise creating
- * derivative works of the Spine Runtimes is permitted under the terms and
- * conditions of Section 2 of the Spine Editor License Agreement:
- * http://esotericsoftware.com/spine-editor-license
- *
- * Otherwise, it is permitted to integrate the Spine Runtimes into software
- * or otherwise create derivative works of the Spine Runtimes (collectively,
- * "Products"), provided that each user of the Products must obtain their own
- * Spine Editor license and redistribution of the Products in any form must
- * include this license and copyright notice.
- *
- * THE SPINE RUNTIMES ARE PROVIDED BY ESOTERIC SOFTWARE LLC "AS IS" AND ANY
- * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
- * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
- * DISCLAIMED. IN NO EVENT SHALL ESOTERIC SOFTWARE LLC BE LIABLE FOR ANY
- * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
- * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES,
- * BUSINESS INTERRUPTION, OR LOSS OF USE, DATA, OR PROFITS) HOWEVER CAUSED AND
- * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
- * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
- * THE SPINE RUNTIMES, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
- *****************************************************************************/
-
-package com.esotericsoftware.spine.utils;
-
-import com.badlogic.gdx.graphics.Color;
-import com.badlogic.gdx.graphics.g2d.Batch;
-import com.badlogic.gdx.scenes.scene2d.Actor;
-
-import com.esotericsoftware.spine.AnimationState;
-import com.esotericsoftware.spine.Skeleton;
-import com.esotericsoftware.spine.SkeletonRenderer;
-
-/** A scene2d actor that draws a skeleton. */
-public class SkeletonActor extends Actor {
-	private SkeletonRenderer renderer;
-	private Skeleton skeleton;
-	AnimationState state;
-	private boolean resetBlendFunction = true;
-
-	/** Creates an uninitialized SkeletonActor. The renderer, skeleton, and animation state must be set before use. */
-	public SkeletonActor () {
-	}
-
-	public SkeletonActor (SkeletonRenderer renderer, Skeleton skeleton, AnimationState state) {
-		this.renderer = renderer;
-		this.skeleton = skeleton;
-		this.state = state;
-	}
-
-	public void act (float delta) {
-		state.update(delta);
-		state.apply(skeleton);
-		super.act(delta);
-	}
-
-	public void draw (Batch batch, float parentAlpha) {
-		int blendSrc = batch.getBlendSrcFunc(), blendDst = batch.getBlendDstFunc();
-		int blendSrcAlpha = batch.getBlendSrcFuncAlpha(), blendDstAlpha = batch.getBlendDstFuncAlpha();
-
-		Color color = skeleton.getColor();
-		float oldAlpha = color.a;
-		skeleton.getColor().a *= parentAlpha;
-
-		skeleton.setPosition(getX(), getY());
-		skeleton.updateWorldTransform();
-		renderer.draw(batch, skeleton);
-
-		if (resetBlendFunction) batch.setBlendFunctionSeparate(blendSrc, blendDst, blendSrcAlpha, blendDstAlpha);
-
-		color.a = oldAlpha;
-	}
-
-	public SkeletonRenderer getRenderer () {
-		return renderer;
-	}
-
-	public void setRenderer (SkeletonRenderer renderer) {
-		this.renderer = renderer;
-	}
-
-	public Skeleton getSkeleton () {
-		return skeleton;
-	}
-
-	public void setSkeleton (Skeleton skeleton) {
-		this.skeleton = skeleton;
-	}
-
-	public AnimationState getAnimationState () {
-		return state;
-	}
-
-	public void setAnimationState (AnimationState state) {
-		this.state = state;
-	}
-
-	public boolean getResetBlendFunction () {
-		return resetBlendFunction;
-	}
-
-	/** If false, the blend function will be left as whatever {@link SkeletonRenderer#draw(Batch, Skeleton)} set. This can reduce
-	 * batch flushes in some cases, but means other rendering may need to first set the blend function. Default is true. */
-	public void setResetBlendFunction (boolean resetBlendFunction) {
-		this.resetBlendFunction = resetBlendFunction;
-	}
-}
+/******************************************************************************
+ * Spine Runtimes License Agreement
+ * Last updated January 1, 2020. Replaces all prior versions.
+ *
+ * Copyright (c) 2013-2020, Esoteric Software LLC
+ *
+ * Integration of the Spine Runtimes into software or otherwise creating
+ * derivative works of the Spine Runtimes is permitted under the terms and
+ * conditions of Section 2 of the Spine Editor License Agreement:
+ * http://esotericsoftware.com/spine-editor-license
+ *
+ * Otherwise, it is permitted to integrate the Spine Runtimes into software
+ * or otherwise create derivative works of the Spine Runtimes (collectively,
+ * "Products"), provided that each user of the Products must obtain their own
+ * Spine Editor license and redistribution of the Products in any form must
+ * include this license and copyright notice.
+ *
+ * THE SPINE RUNTIMES ARE PROVIDED BY ESOTERIC SOFTWARE LLC "AS IS" AND ANY
+ * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL ESOTERIC SOFTWARE LLC BE LIABLE FOR ANY
+ * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES,
+ * BUSINESS INTERRUPTION, OR LOSS OF USE, DATA, OR PROFITS) HOWEVER CAUSED AND
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
+ * THE SPINE RUNTIMES, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ *****************************************************************************/
+
+package com.esotericsoftware.spine.utils;
+
+import com.badlogic.gdx.graphics.Color;
+import com.badlogic.gdx.graphics.g2d.Batch;
+import com.badlogic.gdx.scenes.scene2d.Actor;
+
+import com.esotericsoftware.spine.AnimationState;
+import com.esotericsoftware.spine.Skeleton;
+import com.esotericsoftware.spine.SkeletonRenderer;
+
+/** A scene2d actor that draws a skeleton. */
+public class SkeletonActor extends Actor {
+	private SkeletonRenderer renderer;
+	private Skeleton skeleton;
+	AnimationState state;
+	private boolean resetBlendFunction = true;
+
+	/** Creates an uninitialized SkeletonActor. The renderer, skeleton, and animation state must be set before use. */
+	public SkeletonActor () {
+	}
+
+	public SkeletonActor (SkeletonRenderer renderer, Skeleton skeleton, AnimationState state) {
+		this.renderer = renderer;
+		this.skeleton = skeleton;
+		this.state = state;
+	}
+
+	public void act (float delta) {
+		state.update(delta);
+		state.apply(skeleton);
+		super.act(delta);
+	}
+
+	public void draw (Batch batch, float parentAlpha) {
+		int blendSrc = batch.getBlendSrcFunc(), blendDst = batch.getBlendDstFunc();
+		int blendSrcAlpha = batch.getBlendSrcFuncAlpha(), blendDstAlpha = batch.getBlendDstFuncAlpha();
+
+		Color color = skeleton.getColor();
+		float oldAlpha = color.a;
+		skeleton.getColor().a *= parentAlpha;
+
+		skeleton.setPosition(getX(), getY());
+		skeleton.updateWorldTransform();
+		renderer.draw(batch, skeleton);
+
+		if (resetBlendFunction) batch.setBlendFunctionSeparate(blendSrc, blendDst, blendSrcAlpha, blendDstAlpha);
+
+		color.a = oldAlpha;
+	}
+
+	public SkeletonRenderer getRenderer () {
+		return renderer;
+	}
+
+	public void setRenderer (SkeletonRenderer renderer) {
+		this.renderer = renderer;
+	}
+
+	public Skeleton getSkeleton () {
+		return skeleton;
+	}
+
+	public void setSkeleton (Skeleton skeleton) {
+		this.skeleton = skeleton;
+	}
+
+	public AnimationState getAnimationState () {
+		return state;
+	}
+
+	public void setAnimationState (AnimationState state) {
+		this.state = state;
+	}
+
+	public boolean getResetBlendFunction () {
+		return resetBlendFunction;
+	}
+
+	/** If false, the blend function will be left as whatever {@link SkeletonRenderer#draw(Batch, Skeleton)} set. This can reduce
+	 * batch flushes in some cases, but means other rendering may need to first set the blend function. Default is true. */
+	public void setResetBlendFunction (boolean resetBlendFunction) {
+		this.resetBlendFunction = resetBlendFunction;
+	}
+}

+ 131 - 131
spine-libgdx/spine-libgdx/src/com/esotericsoftware/spine/utils/SkeletonActorPool.java

@@ -1,131 +1,131 @@
-/******************************************************************************
- * Spine Runtimes License Agreement
- * Last updated January 1, 2020. Replaces all prior versions.
- *
- * Copyright (c) 2013-2020, Esoteric Software LLC
- *
- * Integration of the Spine Runtimes into software or otherwise creating
- * derivative works of the Spine Runtimes is permitted under the terms and
- * conditions of Section 2 of the Spine Editor License Agreement:
- * http://esotericsoftware.com/spine-editor-license
- *
- * Otherwise, it is permitted to integrate the Spine Runtimes into software
- * or otherwise create derivative works of the Spine Runtimes (collectively,
- * "Products"), provided that each user of the Products must obtain their own
- * Spine Editor license and redistribution of the Products in any form must
- * include this license and copyright notice.
- *
- * THE SPINE RUNTIMES ARE PROVIDED BY ESOTERIC SOFTWARE LLC "AS IS" AND ANY
- * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
- * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
- * DISCLAIMED. IN NO EVENT SHALL ESOTERIC SOFTWARE LLC BE LIABLE FOR ANY
- * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
- * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES,
- * BUSINESS INTERRUPTION, OR LOSS OF USE, DATA, OR PROFITS) HOWEVER CAUSED AND
- * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
- * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
- * THE SPINE RUNTIMES, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
- *****************************************************************************/
-
-package com.esotericsoftware.spine.utils;
-
-import com.badlogic.gdx.graphics.Color;
-import com.badlogic.gdx.utils.Array;
-import com.badlogic.gdx.utils.Pool;
-
-import com.esotericsoftware.spine.AnimationState;
-import com.esotericsoftware.spine.AnimationState.TrackEntry;
-import com.esotericsoftware.spine.AnimationStateData;
-import com.esotericsoftware.spine.Skeleton;
-import com.esotericsoftware.spine.SkeletonData;
-import com.esotericsoftware.spine.SkeletonRenderer;
-import com.esotericsoftware.spine.Skin;
-
-public class SkeletonActorPool extends Pool<SkeletonActor> {
-	private SkeletonRenderer renderer;
-	SkeletonData skeletonData;
-	AnimationStateData stateData;
-	private final Pool<Skeleton> skeletonPool;
-	private final Pool<AnimationState> statePool;
-	private final Array<SkeletonActor> obtained;
-
-	public SkeletonActorPool (SkeletonRenderer renderer, SkeletonData skeletonData, AnimationStateData stateData) {
-		this(renderer, skeletonData, stateData, 16, Integer.MAX_VALUE);
-	}
-
-	public SkeletonActorPool (SkeletonRenderer renderer, SkeletonData skeletonData, AnimationStateData stateData,
-		int initialCapacity, int max) {
-		super(initialCapacity, max);
-
-		this.renderer = renderer;
-		this.skeletonData = skeletonData;
-		this.stateData = stateData;
-
-		obtained = new Array(false, initialCapacity);
-
-		skeletonPool = new Pool<Skeleton>(initialCapacity, max) {
-			protected Skeleton newObject () {
-				return new Skeleton(SkeletonActorPool.this.skeletonData);
-			}
-
-			protected void reset (Skeleton skeleton) {
-				skeleton.setColor(Color.WHITE);
-				skeleton.setScale(1, 1);
-				skeleton.setSkin((Skin)null);
-				skeleton.setSkin(SkeletonActorPool.this.skeletonData.getDefaultSkin());
-				skeleton.setToSetupPose();
-			}
-		};
-
-		statePool = new Pool<AnimationState>(initialCapacity, max) {
-			protected AnimationState newObject () {
-				return new AnimationState(SkeletonActorPool.this.stateData);
-			}
-
-			protected void reset (AnimationState state) {
-				state.clearTracks();
-				state.clearListeners();
-			}
-		};
-	}
-
-	/** Each obtained skeleton actor that is no longer playing an animation is removed from the stage and returned to the pool. */
-	public void freeComplete () {
-		Object[] obtained = this.obtained.items;
-		outer:
-		for (int i = this.obtained.size - 1; i >= 0; i--) {
-			SkeletonActor actor = (SkeletonActor)obtained[i];
-			Array<TrackEntry> tracks = actor.state.getTracks();
-			for (int ii = 0, nn = tracks.size; ii < nn; ii++)
-				if (tracks.get(ii) != null) continue outer;
-			free(actor);
-		}
-	}
-
-	protected SkeletonActor newObject () {
-		SkeletonActor actor = new SkeletonActor();
-		actor.setRenderer(renderer);
-		return actor;
-	}
-
-	/** This pool keeps a reference to the obtained instance, so it should be returned to the pool via {@link #free(SkeletonActor)}
-	 * , {@link #freeAll(Array)} or {@link #freeComplete()} to avoid leaking memory. */
-	public SkeletonActor obtain () {
-		SkeletonActor actor = super.obtain();
-		actor.setSkeleton(skeletonPool.obtain());
-		actor.setAnimationState(statePool.obtain());
-		obtained.add(actor);
-		return actor;
-	}
-
-	protected void reset (SkeletonActor actor) {
-		actor.remove();
-		obtained.removeValue(actor, true);
-		skeletonPool.free(actor.getSkeleton());
-		statePool.free(actor.getAnimationState());
-	}
-
-	public Array<SkeletonActor> getObtained () {
-		return obtained;
-	}
-}
+/******************************************************************************
+ * Spine Runtimes License Agreement
+ * Last updated January 1, 2020. Replaces all prior versions.
+ *
+ * Copyright (c) 2013-2020, Esoteric Software LLC
+ *
+ * Integration of the Spine Runtimes into software or otherwise creating
+ * derivative works of the Spine Runtimes is permitted under the terms and
+ * conditions of Section 2 of the Spine Editor License Agreement:
+ * http://esotericsoftware.com/spine-editor-license
+ *
+ * Otherwise, it is permitted to integrate the Spine Runtimes into software
+ * or otherwise create derivative works of the Spine Runtimes (collectively,
+ * "Products"), provided that each user of the Products must obtain their own
+ * Spine Editor license and redistribution of the Products in any form must
+ * include this license and copyright notice.
+ *
+ * THE SPINE RUNTIMES ARE PROVIDED BY ESOTERIC SOFTWARE LLC "AS IS" AND ANY
+ * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL ESOTERIC SOFTWARE LLC BE LIABLE FOR ANY
+ * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES,
+ * BUSINESS INTERRUPTION, OR LOSS OF USE, DATA, OR PROFITS) HOWEVER CAUSED AND
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
+ * THE SPINE RUNTIMES, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ *****************************************************************************/
+
+package com.esotericsoftware.spine.utils;
+
+import com.badlogic.gdx.graphics.Color;
+import com.badlogic.gdx.utils.Array;
+import com.badlogic.gdx.utils.Pool;
+
+import com.esotericsoftware.spine.AnimationState;
+import com.esotericsoftware.spine.AnimationState.TrackEntry;
+import com.esotericsoftware.spine.AnimationStateData;
+import com.esotericsoftware.spine.Skeleton;
+import com.esotericsoftware.spine.SkeletonData;
+import com.esotericsoftware.spine.SkeletonRenderer;
+import com.esotericsoftware.spine.Skin;
+
+public class SkeletonActorPool extends Pool<SkeletonActor> {
+	private SkeletonRenderer renderer;
+	SkeletonData skeletonData;
+	AnimationStateData stateData;
+	private final Pool<Skeleton> skeletonPool;
+	private final Pool<AnimationState> statePool;
+	private final Array<SkeletonActor> obtained;
+
+	public SkeletonActorPool (SkeletonRenderer renderer, SkeletonData skeletonData, AnimationStateData stateData) {
+		this(renderer, skeletonData, stateData, 16, Integer.MAX_VALUE);
+	}
+
+	public SkeletonActorPool (SkeletonRenderer renderer, SkeletonData skeletonData, AnimationStateData stateData,
+		int initialCapacity, int max) {
+		super(initialCapacity, max);
+
+		this.renderer = renderer;
+		this.skeletonData = skeletonData;
+		this.stateData = stateData;
+
+		obtained = new Array(false, initialCapacity);
+
+		skeletonPool = new Pool<Skeleton>(initialCapacity, max) {
+			protected Skeleton newObject () {
+				return new Skeleton(SkeletonActorPool.this.skeletonData);
+			}
+
+			protected void reset (Skeleton skeleton) {
+				skeleton.setColor(Color.WHITE);
+				skeleton.setScale(1, 1);
+				skeleton.setSkin((Skin)null);
+				skeleton.setSkin(SkeletonActorPool.this.skeletonData.getDefaultSkin());
+				skeleton.setToSetupPose();
+			}
+		};
+
+		statePool = new Pool<AnimationState>(initialCapacity, max) {
+			protected AnimationState newObject () {
+				return new AnimationState(SkeletonActorPool.this.stateData);
+			}
+
+			protected void reset (AnimationState state) {
+				state.clearTracks();
+				state.clearListeners();
+			}
+		};
+	}
+
+	/** Each obtained skeleton actor that is no longer playing an animation is removed from the stage and returned to the pool. */
+	public void freeComplete () {
+		Object[] obtained = this.obtained.items;
+		outer:
+		for (int i = this.obtained.size - 1; i >= 0; i--) {
+			SkeletonActor actor = (SkeletonActor)obtained[i];
+			Array<TrackEntry> tracks = actor.state.getTracks();
+			for (int ii = 0, nn = tracks.size; ii < nn; ii++)
+				if (tracks.get(ii) != null) continue outer;
+			free(actor);
+		}
+	}
+
+	protected SkeletonActor newObject () {
+		SkeletonActor actor = new SkeletonActor();
+		actor.setRenderer(renderer);
+		return actor;
+	}
+
+	/** This pool keeps a reference to the obtained instance, so it should be returned to the pool via {@link #free(SkeletonActor)}
+	 * , {@link #freeAll(Array)} or {@link #freeComplete()} to avoid leaking memory. */
+	public SkeletonActor obtain () {
+		SkeletonActor actor = super.obtain();
+		actor.setSkeleton(skeletonPool.obtain());
+		actor.setAnimationState(statePool.obtain());
+		obtained.add(actor);
+		return actor;
+	}
+
+	protected void reset (SkeletonActor actor) {
+		actor.remove();
+		obtained.removeValue(actor, true);
+		skeletonPool.free(actor.getSkeleton());
+		statePool.free(actor.getAnimationState());
+	}
+
+	public Array<SkeletonActor> getObtained () {
+		return obtained;
+	}
+}

+ 57 - 57
spine-libgdx/spine-libgdx/src/com/esotericsoftware/spine/utils/SkeletonPool.java

@@ -1,57 +1,57 @@
-/******************************************************************************
- * Spine Runtimes License Agreement
- * Last updated January 1, 2020. Replaces all prior versions.
- *
- * Copyright (c) 2013-2020, Esoteric Software LLC
- *
- * Integration of the Spine Runtimes into software or otherwise creating
- * derivative works of the Spine Runtimes is permitted under the terms and
- * conditions of Section 2 of the Spine Editor License Agreement:
- * http://esotericsoftware.com/spine-editor-license
- *
- * Otherwise, it is permitted to integrate the Spine Runtimes into software
- * or otherwise create derivative works of the Spine Runtimes (collectively,
- * "Products"), provided that each user of the Products must obtain their own
- * Spine Editor license and redistribution of the Products in any form must
- * include this license and copyright notice.
- *
- * THE SPINE RUNTIMES ARE PROVIDED BY ESOTERIC SOFTWARE LLC "AS IS" AND ANY
- * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
- * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
- * DISCLAIMED. IN NO EVENT SHALL ESOTERIC SOFTWARE LLC BE LIABLE FOR ANY
- * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
- * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES,
- * BUSINESS INTERRUPTION, OR LOSS OF USE, DATA, OR PROFITS) HOWEVER CAUSED AND
- * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
- * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
- * THE SPINE RUNTIMES, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
- *****************************************************************************/
-
-package com.esotericsoftware.spine.utils;
-
-import com.badlogic.gdx.utils.Pool;
-
-import com.esotericsoftware.spine.Skeleton;
-import com.esotericsoftware.spine.SkeletonData;
-
-public class SkeletonPool extends Pool<Skeleton> {
-	private SkeletonData skeletonData;
-
-	public SkeletonPool (SkeletonData skeletonData) {
-		this.skeletonData = skeletonData;
-	}
-
-	public SkeletonPool (SkeletonData skeletonData, int initialCapacity) {
-		super(initialCapacity);
-		this.skeletonData = skeletonData;
-	}
-
-	public SkeletonPool (SkeletonData skeletonData, int initialCapacity, int max) {
-		super(initialCapacity, max);
-		this.skeletonData = skeletonData;
-	}
-
-	protected Skeleton newObject () {
-		return new Skeleton(skeletonData);
-	}
-}
+/******************************************************************************
+ * Spine Runtimes License Agreement
+ * Last updated January 1, 2020. Replaces all prior versions.
+ *
+ * Copyright (c) 2013-2020, Esoteric Software LLC
+ *
+ * Integration of the Spine Runtimes into software or otherwise creating
+ * derivative works of the Spine Runtimes is permitted under the terms and
+ * conditions of Section 2 of the Spine Editor License Agreement:
+ * http://esotericsoftware.com/spine-editor-license
+ *
+ * Otherwise, it is permitted to integrate the Spine Runtimes into software
+ * or otherwise create derivative works of the Spine Runtimes (collectively,
+ * "Products"), provided that each user of the Products must obtain their own
+ * Spine Editor license and redistribution of the Products in any form must
+ * include this license and copyright notice.
+ *
+ * THE SPINE RUNTIMES ARE PROVIDED BY ESOTERIC SOFTWARE LLC "AS IS" AND ANY
+ * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL ESOTERIC SOFTWARE LLC BE LIABLE FOR ANY
+ * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES,
+ * BUSINESS INTERRUPTION, OR LOSS OF USE, DATA, OR PROFITS) HOWEVER CAUSED AND
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
+ * THE SPINE RUNTIMES, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ *****************************************************************************/
+
+package com.esotericsoftware.spine.utils;
+
+import com.badlogic.gdx.utils.Pool;
+
+import com.esotericsoftware.spine.Skeleton;
+import com.esotericsoftware.spine.SkeletonData;
+
+public class SkeletonPool extends Pool<Skeleton> {
+	private SkeletonData skeletonData;
+
+	public SkeletonPool (SkeletonData skeletonData) {
+		this.skeletonData = skeletonData;
+	}
+
+	public SkeletonPool (SkeletonData skeletonData, int initialCapacity) {
+		super(initialCapacity);
+		this.skeletonData = skeletonData;
+	}
+
+	public SkeletonPool (SkeletonData skeletonData, int initialCapacity, int max) {
+		super(initialCapacity, max);
+		this.skeletonData = skeletonData;
+	}
+
+	protected Skeleton newObject () {
+		return new Skeleton(skeletonData);
+	}
+}

+ 281 - 281
spine-libgdx/spine-skeletonviewer/src/com/esotericsoftware/spine/JsonRollback.java

@@ -1,281 +1,281 @@
-/******************************************************************************
- * Spine Runtimes License Agreement
- * Last updated January 1, 2020. Replaces all prior versions.
- *
- * Copyright (c) 2013-2020, Esoteric Software LLC
- *
- * Integration of the Spine Runtimes into software or otherwise creating
- * derivative works of the Spine Runtimes is permitted under the terms and
- * conditions of Section 2 of the Spine Editor License Agreement:
- * http://esotericsoftware.com/spine-editor-license
- *
- * Otherwise, it is permitted to integrate the Spine Runtimes into software
- * or otherwise create derivative works of the Spine Runtimes (collectively,
- * "Products"), provided that each user of the Products must obtain their own
- * Spine Editor license and redistribution of the Products in any form must
- * include this license and copyright notice.
- *
- * THE SPINE RUNTIMES ARE PROVIDED BY ESOTERIC SOFTWARE LLC "AS IS" AND ANY
- * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
- * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
- * DISCLAIMED. IN NO EVENT SHALL ESOTERIC SOFTWARE LLC BE LIABLE FOR ANY
- * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
- * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES,
- * BUSINESS INTERRUPTION, OR LOSS OF USE, DATA, OR PROFITS) HOWEVER CAUSED AND
- * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
- * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
- * THE SPINE RUNTIMES, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
- *****************************************************************************/
-
-package com.esotericsoftware.spine;
-
-import java.io.BufferedWriter;
-
-import com.badlogic.gdx.files.FileHandle;
-import com.badlogic.gdx.utils.Array;
-import com.badlogic.gdx.utils.Json;
-import com.badlogic.gdx.utils.JsonValue;
-import com.badlogic.gdx.utils.JsonValue.ValueType;
-import com.badlogic.gdx.utils.JsonWriter.OutputType;
-
-/** Takes Spine JSON data and transforms it to work with an older version of Spine. Target versions:<br>
- * 2.1: supports going from version 3.3.xx to 2.1.27.<br>
- * 3.7: supports going from version 3.8.xx to 3.7.94.<br>
- * 3.8: supports going from version 4.0.xx to 3.8.99 (all curves become linear, separate timelines are lost, constraint translate
- * Y and shear Y are lost).
- * <p>
- * Data can be exported from a Spine project, processed with JsonRollback, then imported into an older version of Spine. However,
- * JsonRollback may remove data for features not supported by the older Spine version. Because of this, JsonRollback is only
- * intended for situations were work was accidentally done with a newer Spine version and now needs to be imported into an older
- * Spine version (eg, if runtime support for the new version is not yet available).
- * <p>
- * Animators should freeze their Spine editor version to match the Spine version supported by the runtime being used. Only when
- * the runtime is updated to support a newer Spine version should animators update their Spine editor version to match. */
-public class JsonRollback {
-	static public void main (String[] args) throws Exception {
-		if (args.length != 2 && args.length != 3) {
-			System.out.println("Usage: <inputFile> <targetVersion> [outputFile]");
-			System.exit(0);
-		}
-
-		String version = args[1];
-		if (!version.equals("2.1") && !version.equals("3.7") && !version.equals("3.8")) {
-			System.out.println("ERROR: Target version must be: 2.1, 3.7, or 3.8");
-			System.out.println("Usage: <inputFile> <toVersion> [outputFile]");
-			System.exit(0);
-		}
-
-		JsonValue root = new Json().fromJson(null, new FileHandle(args[0]));
-
-		// Update Spine version.
-		JsonValue skeleton = root.get("skeleton");
-		if (skeleton == null) {
-			skeleton = new JsonValue(ValueType.object);
-			skeleton.name = "skeleton";
-			JsonValue first = root.child;
-			root.child = skeleton;
-			skeleton.next = first;
-		}
-		JsonValue spine = skeleton.get("spine");
-		if (spine != null)
-			spine.set(version + "-from-" + spine.asString());
-		else
-			skeleton.addChild("spine", new JsonValue(version));
-
-		if (version.equals("2.1")) {
-			// In 3.2 skinnedmesh was renamed to weightedmesh.
-			setValue(root, "skinnedmesh", "skins", "*", "*", "*", "type", "weightedmesh");
-
-			// In 3.2 shear was added.
-			delete(root, "animations", "*", "bones", "*", "shear");
-
-			// In 3.3 ffd was renamed to deform.
-			rename(root, "ffd", "animations", "*", "deform");
-
-			// In 3.3 mesh is now a single type, previously they were skinnedmesh if they had weights.
-			for (JsonValue value : find(root, new Array(), 0, "skins", "*", "*", "*", "type", "mesh"))
-				if (value.parent.get("uvs").size != value.parent.get("vertices").size) value.set("skinnedmesh");
-
-			// In 3.3 linkedmesh is now a single type, previously they were linkedweightedmesh if they had weights.
-			for (JsonValue value : find(root, new Array(), 0, "skins", "*", "*", "*", "type", "linkedmesh")) {
-				String slot = value.parent.parent.name.replaceAll("", "");
-				String skinName = value.parent.getString("skin", "default");
-				String parentName = value.parent.getString("parent");
-				if (find(root, new Array(), 0,
-					("skins~~" + skinName + "~~" + slot + "~~" + parentName + "~~type~~skinnedmesh").split("~~")).size > 0)
-					value.set("weightedlinkedmesh");
-			}
-
-			// In 3.3 bounding boxes can be weighted.
-			for (JsonValue value : find(root, new Array(), 0, "skins", "*", "*", "*", "type", "boundingbox"))
-				if (value.parent.getInt("vertexCount") * 2 != value.parent.get("vertices").size)
-					value.parent.parent.remove(value.parent.name);
-
-			// In 3.3 paths were added.
-			for (JsonValue value : find(root, new Array(), 0, "skins", "*", "*", "*", "type", "path")) {
-				String attachment = value.parent.name;
-				value.parent.parent.remove(attachment);
-				String slot = value.parent.parent.name;
-				// Also remove path deform timelines.
-				delete(root, "animations", "*", "ffd", "*", slot, attachment);
-			}
-
-			// In 3.3 IK constraint timelines no longer require bendPositive.
-			for (JsonValue value : find(root, new Array(), 0, "animations", "*", "ik", "*"))
-				for (JsonValue child = value.child; child != null; child = child.next)
-					if (!child.has("bendPositive")) child.addChild("bendPositive", new JsonValue(true));
-
-			// In 3.3 transform constraints can have more than 1 bone.
-			for (JsonValue child = root.getChild("transform"); child != null; child = child.next) {
-				JsonValue bones = child.remove("bones");
-				if (bones != null) child.addChild("bone", new JsonValue(bones.child.asString()));
-			}
-		} else if (version.equals("3.7")) {
-			JsonValue skins = root.get("skins");
-			if (skins != null && skins.isArray()) {
-				JsonValue newSkins = new JsonValue(ValueType.object);
-				for (JsonValue skinMap = skins.child; skinMap != null; skinMap = skinMap.next) {
-					JsonValue attachments = skinMap.get("attachments");
-					if (attachments != null) newSkins.addChild(skinMap.getString("name"), skinMap.get("attachments"));
-				}
-				root.remove("skins");
-				root.addChild("skins", newSkins);
-			}
-
-			rollbackCurves(root.get("animations"));
-		} else if (version.equals("3.8")) {
-			linearCurves(root.get("animations"));
-			rename(root, "angle", "animations", "*", "bones", "*", "rotate", "value");
-			constraintNames(root, "transform");
-			constraintNames(root, "path");
-			constraintNames(root, "animations", "*", "transform", "*");
-			constraintNames(root, "animations", "*", "path", "*");
-		}
-
-		if (args.length == 3) {
-			System.out.println("Writing: " + args[2]);
-			BufferedWriter fileWriter = new BufferedWriter(new FileHandle(args[2]).writer(false, "UTF-8"), 16 * 1024);
-			root.prettyPrint(OutputType.json, fileWriter);
-			fileWriter.close();
-		} else
-			System.out.println(root.prettyPrint(OutputType.json, 130));
-	}
-
-	static private void log (String message) {
-		System.out.println(message);
-	}
-
-	static private void constraintNames (JsonValue root, String... path) {
-		for (JsonValue map : find(root, new Array(), 0, path)) {
-			for (JsonValue constraint = map.child; constraint != null; constraint = constraint.next) {
-				for (JsonValue child = constraint.child; child != null; child = child.next) {
-					if (child.name.equals("mixRotate"))
-						child.name = "rotateMix";
-					else if (child.name.equals("mixX") || child.name.equals("mixY"))
-						child.name = "translateMix";
-					else if (child.name.equals("mixScaleX") || child.name.equals("mixScaleY")) {
-						child.name = "scaleMix";
-					} else if (child.name.equals("mixShearX") || child.name.equals("mixShearY")) //
-						child.name = "shearMix";
-				}
-			}
-		}
-	}
-
-	static private void linearCurves (JsonValue map) {
-		if (map == null) return;
-
-		if (map.isObject() && map.parent.isArray()) { // Probably a key.
-			if (map.parent.name != null) {
-				String name = map.parent.name;
-				if (name.equals("translatex") || name.equals("translatey") //
-					|| name.equals("scalex") || name.equals("scaley") //
-					|| name.equals("shearx") || name.equals("sheary") //
-					|| name.equals("rgb") || name.equals("rgb2") || name.equals("alpha")) {
-					map.parent.remove();
-					log("Separate timelines removed: " + name);
-				}
-				if (name.equals("rgba"))
-					map.parent.name = "color";
-				else if (name.equals("rgba")) //
-					map.parent.name = "twoColor";
-			}
-		}
-
-		JsonValue curve = map.get("curve");
-		if (curve == null) {
-			for (JsonValue child = map.child; child != null; child = child.next)
-				linearCurves(child);
-			return;
-		}
-		if (!curve.isString()) {
-			curve.remove();
-			log("Bezier curve changed to linear.");
-		}
-	}
-
-	static private void rollbackCurves (JsonValue map) {
-		if (map == null) return;
-
-		if (map.isObject() && map.parent.isArray()) { // Probably a key.
-			if (!map.has("time")) map.addChild("time", new JsonValue(0f));
-			if (map.parent.name != null) {
-				if (map.parent.name.equals("rotate") && !map.has("angle"))
-					map.addChild("angle", new JsonValue(0f));
-				else if (map.parent.name.equals("scale")) {
-					if (!map.has("x")) map.addChild("x", new JsonValue(1f));
-					if (!map.has("y")) map.addChild("y", new JsonValue(1f));
-				}
-			}
-		}
-
-		JsonValue curve = map.get("curve");
-		if (curve == null) {
-			for (JsonValue child = map.child; child != null; child = child.next)
-				rollbackCurves(child);
-			return;
-		}
-		if (curve.isNumber()) {
-			curve.addChild(new JsonValue(curve.asFloat()));
-			curve.setType(ValueType.array);
-			curve.addChild(new JsonValue(map.getFloat("c2", 0)));
-			curve.addChild(new JsonValue(map.getFloat("c3", 1)));
-			curve.addChild(new JsonValue(map.getFloat("c4", 1)));
-			map.remove("c2");
-			map.remove("c3");
-			map.remove("c4");
-		}
-	}
-
-	static void setValue (JsonValue root, String newValue, String... path) {
-		for (JsonValue value : find(root, new Array(), 0, path))
-			value.set(newValue);
-	}
-
-	static void rename (JsonValue root, String newName, String... path) {
-		for (JsonValue value : find(root, new Array(), 0, path))
-			value.name = newName;
-	}
-
-	static void delete (JsonValue root, String... path) {
-		for (JsonValue value : find(root, new Array(), 0, path))
-			value.parent.remove(value.name);
-	}
-
-	static Array<JsonValue> find (JsonValue current, Array<JsonValue> values, int index, String... path) {
-		String name = path[index];
-		if (current.name == null) {
-			if (name.equals("*") && index == path.length - 1)
-				values.add(current);
-			else if (current.has(name)) return find(current.get(name), values, index, path);
-		} else if (name.equals("*") || current.name.equals(name)) {
-			if (++index == path.length || (index == path.length - 1 && current.isString() && current.asString().equals(path[index])))
-				values.add(current);
-			else {
-				for (JsonValue child = current.child; child != null; child = child.next)
-					find(child, values, index, path);
-			}
-		}
-		return values;
-	}
-}
+/******************************************************************************
+ * Spine Runtimes License Agreement
+ * Last updated January 1, 2020. Replaces all prior versions.
+ *
+ * Copyright (c) 2013-2020, Esoteric Software LLC
+ *
+ * Integration of the Spine Runtimes into software or otherwise creating
+ * derivative works of the Spine Runtimes is permitted under the terms and
+ * conditions of Section 2 of the Spine Editor License Agreement:
+ * http://esotericsoftware.com/spine-editor-license
+ *
+ * Otherwise, it is permitted to integrate the Spine Runtimes into software
+ * or otherwise create derivative works of the Spine Runtimes (collectively,
+ * "Products"), provided that each user of the Products must obtain their own
+ * Spine Editor license and redistribution of the Products in any form must
+ * include this license and copyright notice.
+ *
+ * THE SPINE RUNTIMES ARE PROVIDED BY ESOTERIC SOFTWARE LLC "AS IS" AND ANY
+ * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL ESOTERIC SOFTWARE LLC BE LIABLE FOR ANY
+ * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES,
+ * BUSINESS INTERRUPTION, OR LOSS OF USE, DATA, OR PROFITS) HOWEVER CAUSED AND
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
+ * THE SPINE RUNTIMES, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ *****************************************************************************/
+
+package com.esotericsoftware.spine;
+
+import java.io.BufferedWriter;
+
+import com.badlogic.gdx.files.FileHandle;
+import com.badlogic.gdx.utils.Array;
+import com.badlogic.gdx.utils.Json;
+import com.badlogic.gdx.utils.JsonValue;
+import com.badlogic.gdx.utils.JsonValue.ValueType;
+import com.badlogic.gdx.utils.JsonWriter.OutputType;
+
+/** Takes Spine JSON data and transforms it to work with an older version of Spine. Target versions:<br>
+ * 2.1: supports going from version 3.3.xx to 2.1.27.<br>
+ * 3.7: supports going from version 3.8.xx to 3.7.94.<br>
+ * 3.8: supports going from version 4.0.xx to 3.8.99 (all curves become linear, separate timelines are lost, constraint translate
+ * Y and shear Y are lost).
+ * <p>
+ * Data can be exported from a Spine project, processed with JsonRollback, then imported into an older version of Spine. However,
+ * JsonRollback may remove data for features not supported by the older Spine version. Because of this, JsonRollback is only
+ * intended for situations were work was accidentally done with a newer Spine version and now needs to be imported into an older
+ * Spine version (eg, if runtime support for the new version is not yet available).
+ * <p>
+ * Animators should freeze their Spine editor version to match the Spine version supported by the runtime being used. Only when
+ * the runtime is updated to support a newer Spine version should animators update their Spine editor version to match. */
+public class JsonRollback {
+	static public void main (String[] args) throws Exception {
+		if (args.length != 2 && args.length != 3) {
+			System.out.println("Usage: <inputFile> <targetVersion> [outputFile]");
+			System.exit(0);
+		}
+
+		String version = args[1];
+		if (!version.equals("2.1") && !version.equals("3.7") && !version.equals("3.8")) {
+			System.out.println("ERROR: Target version must be: 2.1, 3.7, or 3.8");
+			System.out.println("Usage: <inputFile> <toVersion> [outputFile]");
+			System.exit(0);
+		}
+
+		JsonValue root = new Json().fromJson(null, new FileHandle(args[0]));
+
+		// Update Spine version.
+		JsonValue skeleton = root.get("skeleton");
+		if (skeleton == null) {
+			skeleton = new JsonValue(ValueType.object);
+			skeleton.name = "skeleton";
+			JsonValue first = root.child;
+			root.child = skeleton;
+			skeleton.next = first;
+		}
+		JsonValue spine = skeleton.get("spine");
+		if (spine != null)
+			spine.set(version + "-from-" + spine.asString());
+		else
+			skeleton.addChild("spine", new JsonValue(version));
+
+		if (version.equals("2.1")) {
+			// In 3.2 skinnedmesh was renamed to weightedmesh.
+			setValue(root, "skinnedmesh", "skins", "*", "*", "*", "type", "weightedmesh");
+
+			// In 3.2 shear was added.
+			delete(root, "animations", "*", "bones", "*", "shear");
+
+			// In 3.3 ffd was renamed to deform.
+			rename(root, "ffd", "animations", "*", "deform");
+
+			// In 3.3 mesh is now a single type, previously they were skinnedmesh if they had weights.
+			for (JsonValue value : find(root, new Array(), 0, "skins", "*", "*", "*", "type", "mesh"))
+				if (value.parent.get("uvs").size != value.parent.get("vertices").size) value.set("skinnedmesh");
+
+			// In 3.3 linkedmesh is now a single type, previously they were linkedweightedmesh if they had weights.
+			for (JsonValue value : find(root, new Array(), 0, "skins", "*", "*", "*", "type", "linkedmesh")) {
+				String slot = value.parent.parent.name.replaceAll("", "");
+				String skinName = value.parent.getString("skin", "default");
+				String parentName = value.parent.getString("parent");
+				if (find(root, new Array(), 0,
+					("skins~~" + skinName + "~~" + slot + "~~" + parentName + "~~type~~skinnedmesh").split("~~")).size > 0)
+					value.set("weightedlinkedmesh");
+			}
+
+			// In 3.3 bounding boxes can be weighted.
+			for (JsonValue value : find(root, new Array(), 0, "skins", "*", "*", "*", "type", "boundingbox"))
+				if (value.parent.getInt("vertexCount") * 2 != value.parent.get("vertices").size)
+					value.parent.parent.remove(value.parent.name);
+
+			// In 3.3 paths were added.
+			for (JsonValue value : find(root, new Array(), 0, "skins", "*", "*", "*", "type", "path")) {
+				String attachment = value.parent.name;
+				value.parent.parent.remove(attachment);
+				String slot = value.parent.parent.name;
+				// Also remove path deform timelines.
+				delete(root, "animations", "*", "ffd", "*", slot, attachment);
+			}
+
+			// In 3.3 IK constraint timelines no longer require bendPositive.
+			for (JsonValue value : find(root, new Array(), 0, "animations", "*", "ik", "*"))
+				for (JsonValue child = value.child; child != null; child = child.next)
+					if (!child.has("bendPositive")) child.addChild("bendPositive", new JsonValue(true));
+
+			// In 3.3 transform constraints can have more than 1 bone.
+			for (JsonValue child = root.getChild("transform"); child != null; child = child.next) {
+				JsonValue bones = child.remove("bones");
+				if (bones != null) child.addChild("bone", new JsonValue(bones.child.asString()));
+			}
+		} else if (version.equals("3.7")) {
+			JsonValue skins = root.get("skins");
+			if (skins != null && skins.isArray()) {
+				JsonValue newSkins = new JsonValue(ValueType.object);
+				for (JsonValue skinMap = skins.child; skinMap != null; skinMap = skinMap.next) {
+					JsonValue attachments = skinMap.get("attachments");
+					if (attachments != null) newSkins.addChild(skinMap.getString("name"), skinMap.get("attachments"));
+				}
+				root.remove("skins");
+				root.addChild("skins", newSkins);
+			}
+
+			rollbackCurves(root.get("animations"));
+		} else if (version.equals("3.8")) {
+			linearCurves(root.get("animations"));
+			rename(root, "angle", "animations", "*", "bones", "*", "rotate", "value");
+			constraintNames(root, "transform");
+			constraintNames(root, "path");
+			constraintNames(root, "animations", "*", "transform", "*");
+			constraintNames(root, "animations", "*", "path", "*");
+		}
+
+		if (args.length == 3) {
+			System.out.println("Writing: " + args[2]);
+			BufferedWriter fileWriter = new BufferedWriter(new FileHandle(args[2]).writer(false, "UTF-8"), 16 * 1024);
+			root.prettyPrint(OutputType.json, fileWriter);
+			fileWriter.close();
+		} else
+			System.out.println(root.prettyPrint(OutputType.json, 130));
+	}
+
+	static private void log (String message) {
+		System.out.println(message);
+	}
+
+	static private void constraintNames (JsonValue root, String... path) {
+		for (JsonValue map : find(root, new Array(), 0, path)) {
+			for (JsonValue constraint = map.child; constraint != null; constraint = constraint.next) {
+				for (JsonValue child = constraint.child; child != null; child = child.next) {
+					if (child.name.equals("mixRotate"))
+						child.name = "rotateMix";
+					else if (child.name.equals("mixX") || child.name.equals("mixY"))
+						child.name = "translateMix";
+					else if (child.name.equals("mixScaleX") || child.name.equals("mixScaleY")) {
+						child.name = "scaleMix";
+					} else if (child.name.equals("mixShearX") || child.name.equals("mixShearY")) //
+						child.name = "shearMix";
+				}
+			}
+		}
+	}
+
+	static private void linearCurves (JsonValue map) {
+		if (map == null) return;
+
+		if (map.isObject() && map.parent.isArray()) { // Probably a key.
+			if (map.parent.name != null) {
+				String name = map.parent.name;
+				if (name.equals("translatex") || name.equals("translatey") //
+					|| name.equals("scalex") || name.equals("scaley") //
+					|| name.equals("shearx") || name.equals("sheary") //
+					|| name.equals("rgb") || name.equals("rgb2") || name.equals("alpha")) {
+					map.parent.remove();
+					log("Separate timelines removed: " + name);
+				}
+				if (name.equals("rgba"))
+					map.parent.name = "color";
+				else if (name.equals("rgba")) //
+					map.parent.name = "twoColor";
+			}
+		}
+
+		JsonValue curve = map.get("curve");
+		if (curve == null) {
+			for (JsonValue child = map.child; child != null; child = child.next)
+				linearCurves(child);
+			return;
+		}
+		if (!curve.isString()) {
+			curve.remove();
+			log("Bezier curve changed to linear.");
+		}
+	}
+
+	static private void rollbackCurves (JsonValue map) {
+		if (map == null) return;
+
+		if (map.isObject() && map.parent.isArray()) { // Probably a key.
+			if (!map.has("time")) map.addChild("time", new JsonValue(0f));
+			if (map.parent.name != null) {
+				if (map.parent.name.equals("rotate") && !map.has("angle"))
+					map.addChild("angle", new JsonValue(0f));
+				else if (map.parent.name.equals("scale")) {
+					if (!map.has("x")) map.addChild("x", new JsonValue(1f));
+					if (!map.has("y")) map.addChild("y", new JsonValue(1f));
+				}
+			}
+		}
+
+		JsonValue curve = map.get("curve");
+		if (curve == null) {
+			for (JsonValue child = map.child; child != null; child = child.next)
+				rollbackCurves(child);
+			return;
+		}
+		if (curve.isNumber()) {
+			curve.addChild(new JsonValue(curve.asFloat()));
+			curve.setType(ValueType.array);
+			curve.addChild(new JsonValue(map.getFloat("c2", 0)));
+			curve.addChild(new JsonValue(map.getFloat("c3", 1)));
+			curve.addChild(new JsonValue(map.getFloat("c4", 1)));
+			map.remove("c2");
+			map.remove("c3");
+			map.remove("c4");
+		}
+	}
+
+	static void setValue (JsonValue root, String newValue, String... path) {
+		for (JsonValue value : find(root, new Array(), 0, path))
+			value.set(newValue);
+	}
+
+	static void rename (JsonValue root, String newName, String... path) {
+		for (JsonValue value : find(root, new Array(), 0, path))
+			value.name = newName;
+	}
+
+	static void delete (JsonValue root, String... path) {
+		for (JsonValue value : find(root, new Array(), 0, path))
+			value.parent.remove(value.name);
+	}
+
+	static Array<JsonValue> find (JsonValue current, Array<JsonValue> values, int index, String... path) {
+		String name = path[index];
+		if (current.name == null) {
+			if (name.equals("*") && index == path.length - 1)
+				values.add(current);
+			else if (current.has(name)) return find(current.get(name), values, index, path);
+		} else if (name.equals("*") || current.name.equals(name)) {
+			if (++index == path.length || (index == path.length - 1 && current.isString() && current.asString().equals(path[index])))
+				values.add(current);
+			else {
+				for (JsonValue child = current.child; child != null; child = child.next)
+					find(child, values, index, path);
+			}
+		}
+		return values;
+	}
+}

+ 377 - 377
spine-libgdx/spine-skeletonviewer/src/com/esotericsoftware/spine/SkeletonViewer.java

@@ -1,377 +1,377 @@
-/******************************************************************************
- * Spine Runtimes License Agreement
- * Last updated January 1, 2020. Replaces all prior versions.
- *
- * Copyright (c) 2013-2020, Esoteric Software LLC
- *
- * Integration of the Spine Runtimes into software or otherwise creating
- * derivative works of the Spine Runtimes is permitted under the terms and
- * conditions of Section 2 of the Spine Editor License Agreement:
- * http://esotericsoftware.com/spine-editor-license
- *
- * Otherwise, it is permitted to integrate the Spine Runtimes into software
- * or otherwise create derivative works of the Spine Runtimes (collectively,
- * "Products"), provided that each user of the Products must obtain their own
- * Spine Editor license and redistribution of the Products in any form must
- * include this license and copyright notice.
- *
- * THE SPINE RUNTIMES ARE PROVIDED BY ESOTERIC SOFTWARE LLC "AS IS" AND ANY
- * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
- * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
- * DISCLAIMED. IN NO EVENT SHALL ESOTERIC SOFTWARE LLC BE LIABLE FOR ANY
- * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
- * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES,
- * BUSINESS INTERRUPTION, OR LOSS OF USE, DATA, OR PROFITS) HOWEVER CAUSED AND
- * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
- * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
- * THE SPINE RUNTIMES, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
- *****************************************************************************/
-
-package com.esotericsoftware.spine;
-
-import java.lang.Thread.UncaughtExceptionHandler;
-import java.lang.reflect.Field;
-
-import com.badlogic.gdx.ApplicationAdapter;
-import com.badlogic.gdx.Gdx;
-import com.badlogic.gdx.Preferences;
-import com.badlogic.gdx.backends.lwjgl.LwjglApplication;
-import com.badlogic.gdx.backends.lwjgl.LwjglApplicationConfiguration;
-import com.badlogic.gdx.files.FileHandle;
-import com.badlogic.gdx.graphics.Color;
-import com.badlogic.gdx.graphics.GL20;
-import com.badlogic.gdx.graphics.OrthographicCamera;
-import com.badlogic.gdx.graphics.glutils.ShapeRenderer;
-import com.badlogic.gdx.graphics.glutils.ShapeRenderer.ShapeType;
-import com.badlogic.gdx.utils.Array;
-import com.badlogic.gdx.utils.Null;
-import com.badlogic.gdx.utils.StringBuilder;
-import com.badlogic.gdx.utils.viewport.ScreenViewport;
-
-import com.esotericsoftware.spine.Animation.MixBlend;
-import com.esotericsoftware.spine.AnimationState.AnimationStateAdapter;
-import com.esotericsoftware.spine.AnimationState.TrackEntry;
-import com.esotericsoftware.spine.utils.TwoColorPolygonBatch;
-
-import java.awt.Toolkit;
-
-public class SkeletonViewer extends ApplicationAdapter {
-	static final String version = ""; // Replaced by build.
-	static final float checkModifiedInterval = 0.250f;
-	static final float reloadDelay = 1;
-	static final String[] startSuffixes = {"", "-pro", "-ess"};
-	static final String[] dataSuffixes = {".json", ".skel"};
-	static final String[] endSuffixes = {"", ".txt", ".bytes"};
-	static final String[] atlasSuffixes = {".atlas", "-pma.atlas"};
-	static String[] args;
-	static float uiScale = 1;
-
-	Preferences prefs;
-	TwoColorPolygonBatch batch;
-	OrthographicCamera camera;
-	SkeletonRenderer renderer;
-	SkeletonRendererDebug debugRenderer;
-	SkeletonViewerUI ui;
-
-	SkeletonViewerAtlas atlas;
-	SkeletonData skeletonData;
-	Skeleton skeleton;
-	AnimationState state;
-	FileHandle skeletonFile;
-	long skeletonModified, atlasModified;
-	float lastModifiedCheck, reloadTimer;
-	final StringBuilder status = new StringBuilder();
-
-	public void create () {
-		Thread.setDefaultUncaughtExceptionHandler(new UncaughtExceptionHandler() {
-			public void uncaughtException (Thread thread, Throwable ex) {
-				System.out.println("Uncaught exception:");
-				ex.printStackTrace();
-				Runtime.getRuntime().halt(0); // Prevent Swing from keeping JVM alive.
-			}
-		});
-
-		prefs = Gdx.app.getPreferences("spine-skeletonviewer");
-		batch = new TwoColorPolygonBatch(3100);
-		camera = new OrthographicCamera();
-		renderer = new SkeletonRenderer();
-		debugRenderer = new SkeletonRendererDebug();
-		ui = new SkeletonViewerUI(this);
-		resetCameraPosition();
-		ui.loadPrefs();
-
-		if (args.length == 0) {
-			loadSkeleton(
-				Gdx.files.internal(Gdx.app.getPreferences("spine-skeletonviewer").getString("lastFile", "spineboy/spineboy.json")));
-		} else
-			loadSkeleton(Gdx.files.internal(args[0]));
-
-		ui.loadPrefs();
-		ui.prefsLoaded = true;
-		setAnimation(true);
-
-		if (false) {
-			ui.animationList.clearListeners();
-			// Test code:
-			// state.setAnimation(0, "walk", true);
-		}
-	}
-
-	boolean loadSkeleton (final @Null FileHandle skeletonFile) {
-		if (skeletonFile == null) return false;
-		FileHandle oldSkeletonFile = this.skeletonFile;
-		this.skeletonFile = skeletonFile;
-		reloadTimer = 0;
-
-		try {
-			atlas = new SkeletonViewerAtlas(this, skeletonFile);
-
-			// Load skeleton data.
-			String extension = skeletonFile.extension();
-			SkeletonLoader loader;
-			if (extension.equalsIgnoreCase("json") || extension.equalsIgnoreCase("txt"))
-				loader = new SkeletonJson(atlas);
-			else
-				loader = new SkeletonBinary(atlas);
-			loader.setScale(ui.loadScaleSlider.getValue());
-			skeletonData = loader.readSkeletonData(skeletonFile);
-			if (skeletonData.getBones().size == 0) throw new Exception("No bones in skeleton data.");
-		} catch (Throwable ex) {
-			System.out.println("Error loading skeleton: " + skeletonFile.file().getAbsolutePath());
-			ex.printStackTrace();
-			ui.toast("Error loading skeleton: " + skeletonFile.name());
-			this.skeletonFile = oldSkeletonFile;
-			return false;
-		}
-
-		skeleton = new Skeleton(skeletonData);
-		skeleton.updateWorldTransform();
-		skeleton.setToSetupPose();
-		skeleton = new Skeleton(skeleton); // Tests copy constructors.
-		skeleton.updateWorldTransform();
-
-		state = new AnimationState(new AnimationStateData(skeletonData));
-		state.addListener(new AnimationStateAdapter() {
-			public void event (TrackEntry entry, Event event) {
-				ui.toast(event.getData().getName());
-			}
-		});
-
-		skeletonModified = skeletonFile.lastModified();
-		atlasModified = atlas.lastModified();
-		lastModifiedCheck = checkModifiedInterval;
-		prefs.putString("lastFile", skeletonFile.path());
-		prefs.flush();
-
-		// Populate UI.
-
-		ui.window.getTitleLabel().setText(skeletonFile.name());
-		{
-			Array<String> items = new Array();
-			for (Skin skin : skeletonData.getSkins())
-				items.add(skin.getName());
-			ui.skinList.setItems(items);
-		}
-		{
-			Array<String> items = new Array();
-			for (Animation animation : skeletonData.getAnimations())
-				items.add(animation.getName());
-			ui.animationList.setItems(items);
-		}
-		ui.trackButtons.getButtons().first().setChecked(true);
-
-		// Configure skeleton from UI.
-
-		if (ui.skinList.getSelected() != null) skeleton.setSkin(ui.skinList.getSelected());
-		setAnimation(true);
-		return true;
-	}
-
-	void setAnimation (boolean first) {
-		if (!ui.prefsLoaded) return;
-		if (ui.animationList.getSelected() == null) return;
-		int track = ui.trackButtons.getCheckedIndex();
-		TrackEntry entry;
-		if (!first && state.getCurrent(track) == null) {
-			state.setEmptyAnimation(track, 0);
-			entry = state.addAnimation(track, ui.animationList.getSelected(), ui.loopCheckbox.isChecked(), 0);
-			entry.setMixDuration(ui.mixSlider.getValue());
-		} else {
-			entry = state.setAnimation(track, ui.animationList.getSelected(), ui.loopCheckbox.isChecked());
-			entry.setHoldPrevious(track > 0 && ui.holdPrevCheckbox.isChecked());
-		}
-		entry.setMixBlend(ui.addCheckbox.isChecked() ? MixBlend.add : MixBlend.replace);
-		entry.setReverse(ui.reverseCheckbox.isChecked());
-		entry.setAlpha(ui.alphaSlider.getValue());
-	}
-
-	public void render () {
-		Gdx.gl.glClearColor(112 / 255f, 111 / 255f, 118 / 255f, 1);
-		Gdx.gl.glClear(GL20.GL_COLOR_BUFFER_BIT);
-
-		float delta = Gdx.graphics.getDeltaTime();
-		camera.update();
-		batch.getProjectionMatrix().set(camera.combined);
-		debugRenderer.getShapeRenderer().setProjectionMatrix(camera.combined);
-
-		// Draw skeleton origin lines.
-		ShapeRenderer shapes = debugRenderer.getShapeRenderer();
-		if (state != null) {
-			shapes.setColor(Color.DARK_GRAY);
-			shapes.begin(ShapeType.Line);
-			shapes.line(0, -99999, 0, 99999);
-			shapes.line(-99999, 0, 99999, 0);
-			shapes.end();
-		}
-
-		if (skeleton != null) {
-			// Reload if skeleton file was modified.
-			if (reloadTimer <= 0) {
-				lastModifiedCheck -= delta;
-				if (lastModifiedCheck < 0) {
-					lastModifiedCheck = checkModifiedInterval;
-					long time = skeletonFile.lastModified();
-					if (time != 0 && skeletonModified != time) reloadTimer = reloadDelay;
-					time = atlas.lastModified();
-					if (time != 0 && atlasModified != 0 && atlasModified != time) reloadTimer = reloadDelay;
-				}
-			} else {
-				reloadTimer -= delta;
-				if (reloadTimer <= 0) {
-					loadSkeleton(skeletonFile);
-					ui.toast("Reloaded.");
-				}
-			}
-
-			// Pose and render skeleton.
-			state.getData().setDefaultMix(ui.mixSlider.getValue());
-			renderer.setPremultipliedAlpha(ui.pmaCheckbox.isChecked());
-			batch.setPremultipliedAlpha(ui.pmaCheckbox.isChecked());
-
-			float scaleX = ui.xScaleSlider.getValue(), scaleY = ui.yScaleSlider.getValue();
-			if (skeleton.scaleX == 0) skeleton.scaleX = 0.01f;
-			if (skeleton.scaleY == 0) skeleton.scaleY = 0.01f;
-			skeleton.setScale(scaleX, scaleY);
-
-			delta = Math.min(delta, 0.032f) * ui.speedSlider.getValue();
-			skeleton.update(delta);
-			state.update(delta);
-			state.apply(skeleton);
-			skeleton.updateWorldTransform();
-
-			batch.begin();
-			renderer.draw(batch, skeleton);
-			batch.end();
-
-			debugRenderer.setBones(ui.debugBonesCheckbox.isChecked());
-			debugRenderer.setRegionAttachments(ui.debugRegionsCheckbox.isChecked());
-			debugRenderer.setBoundingBoxes(ui.debugBoundingBoxesCheckbox.isChecked());
-			debugRenderer.setMeshHull(ui.debugMeshHullCheckbox.isChecked());
-			debugRenderer.setMeshTriangles(ui.debugMeshTrianglesCheckbox.isChecked());
-			debugRenderer.setPaths(ui.debugPathsCheckbox.isChecked());
-			debugRenderer.setPoints(ui.debugPointsCheckbox.isChecked());
-			debugRenderer.setClipping(ui.debugClippingCheckbox.isChecked());
-			debugRenderer.draw(skeleton);
-		}
-
-		if (state != null) {
-			// AnimationState status.
-			status.setLength(0);
-			for (int i = state.getTracks().size - 1; i >= 0; i--) {
-				TrackEntry entry = state.getTracks().get(i);
-				if (entry == null) continue;
-				status.append(i);
-				status.append(": [LIGHT_GRAY]");
-				status(entry);
-				status.append("[WHITE]");
-				status.append(entry.animation.name);
-				status.append('\n');
-			}
-			ui.statusLabel.setText(status);
-		}
-
-		// Render UI.
-		ui.render();
-
-		// Draw indicator lines for animation and mix times.
-		if (state != null) {
-			TrackEntry entry = state.getCurrent(0);
-			if (entry != null) {
-				shapes.getProjectionMatrix().setToOrtho2D(0, 0, Gdx.graphics.getWidth(), Gdx.graphics.getHeight());
-				shapes.updateMatrices();
-				shapes.begin(ShapeType.Line);
-
-				float percent = entry.getAnimationTime() / entry.getAnimationEnd();
-				float x = ui.window.getRight() * uiScale + (Gdx.graphics.getWidth() - ui.window.getRight() * uiScale) * percent;
-				shapes.setColor(Color.CYAN);
-				shapes.line(x, 0, x, 12);
-
-				percent = entry.getMixDuration() == 0 ? 1 : Math.min(1, entry.getMixTime() / entry.getMixDuration());
-				x = ui.window.getRight() * uiScale + (Gdx.graphics.getWidth() - ui.window.getRight() * uiScale) * percent;
-				shapes.setColor(Color.RED);
-				shapes.line(x, 0, x, 12);
-
-				shapes.end();
-			}
-		}
-	}
-
-	void status (TrackEntry entry) {
-		TrackEntry from = entry.mixingFrom;
-		if (from == null) return;
-		status(from);
-		status.append(from.animation.name);
-		status.append(' ');
-		status.append(Math.min(100, (int)(entry.mixTime / entry.mixDuration * 100)));
-		status.append("% -> ");
-	}
-
-	void resetCameraPosition () {
-		camera.position.x = -ui.window.getWidth() / 2 * uiScale;
-		camera.position.y = Gdx.graphics.getHeight() / 4;
-	}
-
-	public void resize (int width, int height) {
-		float x = camera.position.x, y = camera.position.y;
-		camera.setToOrtho(false);
-		camera.position.set(x, y, 0);
-		((ScreenViewport)ui.stage.getViewport()).setUnitsPerPixel(1 / uiScale);
-		ui.stage.getViewport().update(width, height, true);
-		if (!ui.minimizeButton.isChecked()) ui.window.setHeight(height / uiScale + 8);
-	}
-
-	static public void main (String[] args) throws Exception {
-		try { // Try to turn off illegal access log messages.
-			Class loggerClass = Class.forName("jdk.internal.module.IllegalAccessLogger");
-			Field loggerField = loggerClass.getDeclaredField("logger");
-			Class unsafeClass = Class.forName("sun.misc.Unsafe");
-			Field unsafeField = unsafeClass.getDeclaredField("theUnsafe");
-			unsafeField.setAccessible(true);
-			Object unsafe = unsafeField.get(null);
-			Long offset = (Long)unsafeClass.getMethod("staticFieldOffset", Field.class).invoke(unsafe, loggerField);
-			unsafeClass.getMethod("putObjectVolatile", Object.class, long.class, Object.class) //
-				.invoke(unsafe, loggerClass, offset, null);
-		} catch (Throwable ex) {
-		}
-
-		SkeletonViewer.args = args;
-
-		String os = System.getProperty("os.name");
-		float dpiScale = 1;
-		if (os.contains("Windows")) dpiScale = Toolkit.getDefaultToolkit().getScreenResolution() / 96f;
-		if (os.contains("OS X")) {
-			Object object = Toolkit.getDefaultToolkit().getDesktopProperty("apple.awt.contentScaleFactor");
-			if (object instanceof Float && ((Float)object).intValue() >= 2) dpiScale = 2;
-		}
-		if (dpiScale >= 2.0f) uiScale = 2;
-
-		LwjglApplicationConfiguration.disableAudio = true;
-		LwjglApplicationConfiguration config = new LwjglApplicationConfiguration();
-		config.width = (int)(800 * uiScale);
-		config.height = (int)(600 * uiScale);
-		config.title = "Skeleton Viewer";
-		config.allowSoftwareMode = true;
-		config.samples = 2;
-		new LwjglApplication(new SkeletonViewer(), config);
-	}
-}
+/******************************************************************************
+ * Spine Runtimes License Agreement
+ * Last updated January 1, 2020. Replaces all prior versions.
+ *
+ * Copyright (c) 2013-2020, Esoteric Software LLC
+ *
+ * Integration of the Spine Runtimes into software or otherwise creating
+ * derivative works of the Spine Runtimes is permitted under the terms and
+ * conditions of Section 2 of the Spine Editor License Agreement:
+ * http://esotericsoftware.com/spine-editor-license
+ *
+ * Otherwise, it is permitted to integrate the Spine Runtimes into software
+ * or otherwise create derivative works of the Spine Runtimes (collectively,
+ * "Products"), provided that each user of the Products must obtain their own
+ * Spine Editor license and redistribution of the Products in any form must
+ * include this license and copyright notice.
+ *
+ * THE SPINE RUNTIMES ARE PROVIDED BY ESOTERIC SOFTWARE LLC "AS IS" AND ANY
+ * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL ESOTERIC SOFTWARE LLC BE LIABLE FOR ANY
+ * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES,
+ * BUSINESS INTERRUPTION, OR LOSS OF USE, DATA, OR PROFITS) HOWEVER CAUSED AND
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
+ * THE SPINE RUNTIMES, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ *****************************************************************************/
+
+package com.esotericsoftware.spine;
+
+import java.lang.Thread.UncaughtExceptionHandler;
+import java.lang.reflect.Field;
+
+import com.badlogic.gdx.ApplicationAdapter;
+import com.badlogic.gdx.Gdx;
+import com.badlogic.gdx.Preferences;
+import com.badlogic.gdx.backends.lwjgl.LwjglApplication;
+import com.badlogic.gdx.backends.lwjgl.LwjglApplicationConfiguration;
+import com.badlogic.gdx.files.FileHandle;
+import com.badlogic.gdx.graphics.Color;
+import com.badlogic.gdx.graphics.GL20;
+import com.badlogic.gdx.graphics.OrthographicCamera;
+import com.badlogic.gdx.graphics.glutils.ShapeRenderer;
+import com.badlogic.gdx.graphics.glutils.ShapeRenderer.ShapeType;
+import com.badlogic.gdx.utils.Array;
+import com.badlogic.gdx.utils.Null;
+import com.badlogic.gdx.utils.StringBuilder;
+import com.badlogic.gdx.utils.viewport.ScreenViewport;
+
+import com.esotericsoftware.spine.Animation.MixBlend;
+import com.esotericsoftware.spine.AnimationState.AnimationStateAdapter;
+import com.esotericsoftware.spine.AnimationState.TrackEntry;
+import com.esotericsoftware.spine.utils.TwoColorPolygonBatch;
+
+import java.awt.Toolkit;
+
+public class SkeletonViewer extends ApplicationAdapter {
+	static final String version = ""; // Replaced by build.
+	static final float checkModifiedInterval = 0.250f;
+	static final float reloadDelay = 1;
+	static final String[] startSuffixes = {"", "-pro", "-ess"};
+	static final String[] dataSuffixes = {".json", ".skel"};
+	static final String[] endSuffixes = {"", ".txt", ".bytes"};
+	static final String[] atlasSuffixes = {".atlas", "-pma.atlas"};
+	static String[] args;
+	static float uiScale = 1;
+
+	Preferences prefs;
+	TwoColorPolygonBatch batch;
+	OrthographicCamera camera;
+	SkeletonRenderer renderer;
+	SkeletonRendererDebug debugRenderer;
+	SkeletonViewerUI ui;
+
+	SkeletonViewerAtlas atlas;
+	SkeletonData skeletonData;
+	Skeleton skeleton;
+	AnimationState state;
+	FileHandle skeletonFile;
+	long skeletonModified, atlasModified;
+	float lastModifiedCheck, reloadTimer;
+	final StringBuilder status = new StringBuilder();
+
+	public void create () {
+		Thread.setDefaultUncaughtExceptionHandler(new UncaughtExceptionHandler() {
+			public void uncaughtException (Thread thread, Throwable ex) {
+				System.out.println("Uncaught exception:");
+				ex.printStackTrace();
+				Runtime.getRuntime().halt(0); // Prevent Swing from keeping JVM alive.
+			}
+		});
+
+		prefs = Gdx.app.getPreferences("spine-skeletonviewer");
+		batch = new TwoColorPolygonBatch(3100);
+		camera = new OrthographicCamera();
+		renderer = new SkeletonRenderer();
+		debugRenderer = new SkeletonRendererDebug();
+		ui = new SkeletonViewerUI(this);
+		resetCameraPosition();
+		ui.loadPrefs();
+
+		if (args.length == 0) {
+			loadSkeleton(
+				Gdx.files.internal(Gdx.app.getPreferences("spine-skeletonviewer").getString("lastFile", "spineboy/spineboy.json")));
+		} else
+			loadSkeleton(Gdx.files.internal(args[0]));
+
+		ui.loadPrefs();
+		ui.prefsLoaded = true;
+		setAnimation(true);
+
+		if (false) {
+			ui.animationList.clearListeners();
+			// Test code:
+			// state.setAnimation(0, "walk", true);
+		}
+	}
+
+	boolean loadSkeleton (final @Null FileHandle skeletonFile) {
+		if (skeletonFile == null) return false;
+		FileHandle oldSkeletonFile = this.skeletonFile;
+		this.skeletonFile = skeletonFile;
+		reloadTimer = 0;
+
+		try {
+			atlas = new SkeletonViewerAtlas(this, skeletonFile);
+
+			// Load skeleton data.
+			String extension = skeletonFile.extension();
+			SkeletonLoader loader;
+			if (extension.equalsIgnoreCase("json") || extension.equalsIgnoreCase("txt"))
+				loader = new SkeletonJson(atlas);
+			else
+				loader = new SkeletonBinary(atlas);
+			loader.setScale(ui.loadScaleSlider.getValue());
+			skeletonData = loader.readSkeletonData(skeletonFile);
+			if (skeletonData.getBones().size == 0) throw new Exception("No bones in skeleton data.");
+		} catch (Throwable ex) {
+			System.out.println("Error loading skeleton: " + skeletonFile.file().getAbsolutePath());
+			ex.printStackTrace();
+			ui.toast("Error loading skeleton: " + skeletonFile.name());
+			this.skeletonFile = oldSkeletonFile;
+			return false;
+		}
+
+		skeleton = new Skeleton(skeletonData);
+		skeleton.updateWorldTransform();
+		skeleton.setToSetupPose();
+		skeleton = new Skeleton(skeleton); // Tests copy constructors.
+		skeleton.updateWorldTransform();
+
+		state = new AnimationState(new AnimationStateData(skeletonData));
+		state.addListener(new AnimationStateAdapter() {
+			public void event (TrackEntry entry, Event event) {
+				ui.toast(event.getData().getName());
+			}
+		});
+
+		skeletonModified = skeletonFile.lastModified();
+		atlasModified = atlas.lastModified();
+		lastModifiedCheck = checkModifiedInterval;
+		prefs.putString("lastFile", skeletonFile.path());
+		prefs.flush();
+
+		// Populate UI.
+
+		ui.window.getTitleLabel().setText(skeletonFile.name());
+		{
+			Array<String> items = new Array();
+			for (Skin skin : skeletonData.getSkins())
+				items.add(skin.getName());
+			ui.skinList.setItems(items);
+		}
+		{
+			Array<String> items = new Array();
+			for (Animation animation : skeletonData.getAnimations())
+				items.add(animation.getName());
+			ui.animationList.setItems(items);
+		}
+		ui.trackButtons.getButtons().first().setChecked(true);
+
+		// Configure skeleton from UI.
+
+		if (ui.skinList.getSelected() != null) skeleton.setSkin(ui.skinList.getSelected());
+		setAnimation(true);
+		return true;
+	}
+
+	void setAnimation (boolean first) {
+		if (!ui.prefsLoaded) return;
+		if (ui.animationList.getSelected() == null) return;
+		int track = ui.trackButtons.getCheckedIndex();
+		TrackEntry entry;
+		if (!first && state.getCurrent(track) == null) {
+			state.setEmptyAnimation(track, 0);
+			entry = state.addAnimation(track, ui.animationList.getSelected(), ui.loopCheckbox.isChecked(), 0);
+			entry.setMixDuration(ui.mixSlider.getValue());
+		} else {
+			entry = state.setAnimation(track, ui.animationList.getSelected(), ui.loopCheckbox.isChecked());
+			entry.setHoldPrevious(track > 0 && ui.holdPrevCheckbox.isChecked());
+		}
+		entry.setMixBlend(ui.addCheckbox.isChecked() ? MixBlend.add : MixBlend.replace);
+		entry.setReverse(ui.reverseCheckbox.isChecked());
+		entry.setAlpha(ui.alphaSlider.getValue());
+	}
+
+	public void render () {
+		Gdx.gl.glClearColor(112 / 255f, 111 / 255f, 118 / 255f, 1);
+		Gdx.gl.glClear(GL20.GL_COLOR_BUFFER_BIT);
+
+		float delta = Gdx.graphics.getDeltaTime();
+		camera.update();
+		batch.getProjectionMatrix().set(camera.combined);
+		debugRenderer.getShapeRenderer().setProjectionMatrix(camera.combined);
+
+		// Draw skeleton origin lines.
+		ShapeRenderer shapes = debugRenderer.getShapeRenderer();
+		if (state != null) {
+			shapes.setColor(Color.DARK_GRAY);
+			shapes.begin(ShapeType.Line);
+			shapes.line(0, -99999, 0, 99999);
+			shapes.line(-99999, 0, 99999, 0);
+			shapes.end();
+		}
+
+		if (skeleton != null) {
+			// Reload if skeleton file was modified.
+			if (reloadTimer <= 0) {
+				lastModifiedCheck -= delta;
+				if (lastModifiedCheck < 0) {
+					lastModifiedCheck = checkModifiedInterval;
+					long time = skeletonFile.lastModified();
+					if (time != 0 && skeletonModified != time) reloadTimer = reloadDelay;
+					time = atlas.lastModified();
+					if (time != 0 && atlasModified != 0 && atlasModified != time) reloadTimer = reloadDelay;
+				}
+			} else {
+				reloadTimer -= delta;
+				if (reloadTimer <= 0) {
+					loadSkeleton(skeletonFile);
+					ui.toast("Reloaded.");
+				}
+			}
+
+			// Pose and render skeleton.
+			state.getData().setDefaultMix(ui.mixSlider.getValue());
+			renderer.setPremultipliedAlpha(ui.pmaCheckbox.isChecked());
+			batch.setPremultipliedAlpha(ui.pmaCheckbox.isChecked());
+
+			float scaleX = ui.xScaleSlider.getValue(), scaleY = ui.yScaleSlider.getValue();
+			if (skeleton.scaleX == 0) skeleton.scaleX = 0.01f;
+			if (skeleton.scaleY == 0) skeleton.scaleY = 0.01f;
+			skeleton.setScale(scaleX, scaleY);
+
+			delta = Math.min(delta, 0.032f) * ui.speedSlider.getValue();
+			skeleton.update(delta);
+			state.update(delta);
+			state.apply(skeleton);
+			skeleton.updateWorldTransform();
+
+			batch.begin();
+			renderer.draw(batch, skeleton);
+			batch.end();
+
+			debugRenderer.setBones(ui.debugBonesCheckbox.isChecked());
+			debugRenderer.setRegionAttachments(ui.debugRegionsCheckbox.isChecked());
+			debugRenderer.setBoundingBoxes(ui.debugBoundingBoxesCheckbox.isChecked());
+			debugRenderer.setMeshHull(ui.debugMeshHullCheckbox.isChecked());
+			debugRenderer.setMeshTriangles(ui.debugMeshTrianglesCheckbox.isChecked());
+			debugRenderer.setPaths(ui.debugPathsCheckbox.isChecked());
+			debugRenderer.setPoints(ui.debugPointsCheckbox.isChecked());
+			debugRenderer.setClipping(ui.debugClippingCheckbox.isChecked());
+			debugRenderer.draw(skeleton);
+		}
+
+		if (state != null) {
+			// AnimationState status.
+			status.setLength(0);
+			for (int i = state.getTracks().size - 1; i >= 0; i--) {
+				TrackEntry entry = state.getTracks().get(i);
+				if (entry == null) continue;
+				status.append(i);
+				status.append(": [LIGHT_GRAY]");
+				status(entry);
+				status.append("[WHITE]");
+				status.append(entry.animation.name);
+				status.append('\n');
+			}
+			ui.statusLabel.setText(status);
+		}
+
+		// Render UI.
+		ui.render();
+
+		// Draw indicator lines for animation and mix times.
+		if (state != null) {
+			TrackEntry entry = state.getCurrent(0);
+			if (entry != null) {
+				shapes.getProjectionMatrix().setToOrtho2D(0, 0, Gdx.graphics.getWidth(), Gdx.graphics.getHeight());
+				shapes.updateMatrices();
+				shapes.begin(ShapeType.Line);
+
+				float percent = entry.getAnimationTime() / entry.getAnimationEnd();
+				float x = ui.window.getRight() * uiScale + (Gdx.graphics.getWidth() - ui.window.getRight() * uiScale) * percent;
+				shapes.setColor(Color.CYAN);
+				shapes.line(x, 0, x, 12);
+
+				percent = entry.getMixDuration() == 0 ? 1 : Math.min(1, entry.getMixTime() / entry.getMixDuration());
+				x = ui.window.getRight() * uiScale + (Gdx.graphics.getWidth() - ui.window.getRight() * uiScale) * percent;
+				shapes.setColor(Color.RED);
+				shapes.line(x, 0, x, 12);
+
+				shapes.end();
+			}
+		}
+	}
+
+	void status (TrackEntry entry) {
+		TrackEntry from = entry.mixingFrom;
+		if (from == null) return;
+		status(from);
+		status.append(from.animation.name);
+		status.append(' ');
+		status.append(Math.min(100, (int)(entry.mixTime / entry.mixDuration * 100)));
+		status.append("% -> ");
+	}
+
+	void resetCameraPosition () {
+		camera.position.x = -ui.window.getWidth() / 2 * uiScale;
+		camera.position.y = Gdx.graphics.getHeight() / 4;
+	}
+
+	public void resize (int width, int height) {
+		float x = camera.position.x, y = camera.position.y;
+		camera.setToOrtho(false);
+		camera.position.set(x, y, 0);
+		((ScreenViewport)ui.stage.getViewport()).setUnitsPerPixel(1 / uiScale);
+		ui.stage.getViewport().update(width, height, true);
+		if (!ui.minimizeButton.isChecked()) ui.window.setHeight(height / uiScale + 8);
+	}
+
+	static public void main (String[] args) throws Exception {
+		try { // Try to turn off illegal access log messages.
+			Class loggerClass = Class.forName("jdk.internal.module.IllegalAccessLogger");
+			Field loggerField = loggerClass.getDeclaredField("logger");
+			Class unsafeClass = Class.forName("sun.misc.Unsafe");
+			Field unsafeField = unsafeClass.getDeclaredField("theUnsafe");
+			unsafeField.setAccessible(true);
+			Object unsafe = unsafeField.get(null);
+			Long offset = (Long)unsafeClass.getMethod("staticFieldOffset", Field.class).invoke(unsafe, loggerField);
+			unsafeClass.getMethod("putObjectVolatile", Object.class, long.class, Object.class) //
+				.invoke(unsafe, loggerClass, offset, null);
+		} catch (Throwable ex) {
+		}
+
+		SkeletonViewer.args = args;
+
+		String os = System.getProperty("os.name");
+		float dpiScale = 1;
+		if (os.contains("Windows")) dpiScale = Toolkit.getDefaultToolkit().getScreenResolution() / 96f;
+		if (os.contains("OS X")) {
+			Object object = Toolkit.getDefaultToolkit().getDesktopProperty("apple.awt.contentScaleFactor");
+			if (object instanceof Float && ((Float)object).intValue() >= 2) dpiScale = 2;
+		}
+		if (dpiScale >= 2.0f) uiScale = 2;
+
+		LwjglApplicationConfiguration.disableAudio = true;
+		LwjglApplicationConfiguration config = new LwjglApplicationConfiguration();
+		config.width = (int)(800 * uiScale);
+		config.height = (int)(600 * uiScale);
+		config.title = "Skeleton Viewer";
+		config.allowSoftwareMode = true;
+		config.samples = 2;
+		new LwjglApplication(new SkeletonViewer(), config);
+	}
+}