Browse Source

[runtimes] Merged with master, updated license headers

badlogic 9 years ago
parent
commit
06e7037503
32 changed files with 10902 additions and 10784 deletions
  1. 781 782
      spine-libgdx/spine-libgdx-tests/src/com/esotericsoftware/spine/AnimationStateTests.java
  2. 72 73
      spine-libgdx/spine-libgdx-tests/src/com/esotericsoftware/spine/BonePlotting.java
  3. 245 246
      spine-libgdx/spine-libgdx-tests/src/com/esotericsoftware/spine/Box2DExample.java
  4. 224 225
      spine-libgdx/spine-libgdx-tests/src/com/esotericsoftware/spine/EventTimelineTests.java
  5. 138 139
      spine-libgdx/spine-libgdx-tests/src/com/esotericsoftware/spine/MixTest.java
  6. 381 382
      spine-libgdx/spine-libgdx-tests/src/com/esotericsoftware/spine/NormalMapTest.java
  7. 172 173
      spine-libgdx/spine-libgdx-tests/src/com/esotericsoftware/spine/SimpleTest2.java
  8. 1167 1168
      spine-libgdx/spine-libgdx/src/com/esotericsoftware/spine/Animation.java
  9. 1059 1060
      spine-libgdx/spine-libgdx/src/com/esotericsoftware/spine/AnimationState.java
  10. 105 106
      spine-libgdx/spine-libgdx/src/com/esotericsoftware/spine/AnimationStateData.java
  11. 466 467
      spine-libgdx/spine-libgdx/src/com/esotericsoftware/spine/Bone.java
  12. 177 178
      spine-libgdx/spine-libgdx/src/com/esotericsoftware/spine/BoneData.java
  13. 33 4
      spine-libgdx/spine-libgdx/src/com/esotericsoftware/spine/Constraint.java
  14. 277 278
      spine-libgdx/spine-libgdx/src/com/esotericsoftware/spine/IkConstraint.java
  15. 89 90
      spine-libgdx/spine-libgdx/src/com/esotericsoftware/spine/IkConstraintData.java
  16. 467 438
      spine-libgdx/spine-libgdx/src/com/esotericsoftware/spine/PathConstraint.java
  17. 159 130
      spine-libgdx/spine-libgdx/src/com/esotericsoftware/spine/PathConstraintData.java
  18. 643 644
      spine-libgdx/spine-libgdx/src/com/esotericsoftware/spine/Skeleton.java
  19. 782 783
      spine-libgdx/spine-libgdx/src/com/esotericsoftware/spine/SkeletonBinary.java
  20. 225 226
      spine-libgdx/spine-libgdx/src/com/esotericsoftware/spine/SkeletonBounds.java
  21. 287 288
      spine-libgdx/spine-libgdx/src/com/esotericsoftware/spine/SkeletonData.java
  22. 703 704
      spine-libgdx/spine-libgdx/src/com/esotericsoftware/spine/SkeletonJson.java
  23. 98 99
      spine-libgdx/spine-libgdx/src/com/esotericsoftware/spine/SkeletonMeshRenderer.java
  24. 90 91
      spine-libgdx/spine-libgdx/src/com/esotericsoftware/spine/SkeletonRenderer.java
  25. 254 255
      spine-libgdx/spine-libgdx/src/com/esotericsoftware/spine/SkeletonRendererDebug.java
  26. 136 137
      spine-libgdx/spine-libgdx/src/com/esotericsoftware/spine/Skin.java
  27. 196 167
      spine-libgdx/spine-libgdx/src/com/esotericsoftware/spine/TransformConstraint.java
  28. 155 126
      spine-libgdx/spine-libgdx/src/com/esotericsoftware/spine/TransformConstraintData.java
  29. 257 258
      spine-libgdx/spine-libgdx/src/com/esotericsoftware/spine/attachments/MeshAttachment.java
  30. 267 268
      spine-libgdx/spine-libgdx/src/com/esotericsoftware/spine/attachments/RegionAttachment.java
  31. 144 145
      spine-libgdx/spine-libgdx/src/com/esotericsoftware/spine/attachments/VertexAttachment.java
  32. 653 654
      spine-libgdx/spine-skeletonviewer/src/com/esotericsoftware/spine/SkeletonViewer.java

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

@@ -1,785 +1,784 @@
-/******************************************************************************
- * Spine Runtimes Software License
- * Version 2.3
- * 
- * Copyright (c) 2013-2015, Esoteric Software
- * All rights reserved.
- * 
- * You are granted a perpetual, non-exclusive, non-sublicensable and
- * non-transferable license to use, install, execute and perform the Spine
- * Runtimes Software (the "Software") and derivative works solely for personal
- * or internal use. Without the written permission of Esoteric Software (see
- * Section 2 of the Spine Software License Agreement), you may not (a) modify,
- * translate, adapt or otherwise create derivative works, improvements of the
- * Software or develop new applications using the Software or (b) remove,
- * delete, alter or obscure any trademarks or any copyright, trademark, patent
- * or other intellectual property or proprietary rights notices on or in the
- * Software, including any copy thereof. Redistributions in binary or source
- * form must include this license and terms.
- * 
- * THIS SOFTWARE IS PROVIDED BY ESOTERIC SOFTWARE "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 BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
- * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
- * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS;
- * OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
- * WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR
- * OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
- * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+/******************************************************************************
+ * Spine Runtimes Software License v2.5
+ * 
+ * Copyright (c) 2013-2016, Esoteric Software
+ * All rights reserved.
+ * 
+ * You are granted a perpetual, non-exclusive, non-sublicensable, and
+ * non-transferable license to use, install, execute, and perform the Spine
+ * Runtimes software and derivative works solely for personal or internal
+ * use. Without the written permission of Esoteric Software (see Section 2 of
+ * the Spine Software License Agreement), you may not (a) modify, translate,
+ * adapt, or develop new applications using the Spine Runtimes or otherwise
+ * create derivative works or improvements of the Spine Runtimes or (b) remove,
+ * delete, alter, or obscure any trademarks or any copyright, trademark, patent,
+ * or other intellectual property or proprietary rights notices on or in the
+ * Software, including any copy thereof. Redistributions in binary or source
+ * form must include this license and terms.
+ * 
+ * THIS SOFTWARE IS PROVIDED BY ESOTERIC SOFTWARE "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 BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
+ * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES, BUSINESS INTERRUPTION, OR LOSS OF
+ * USE, DATA, OR PROFITS) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER
+ * IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+ * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
+ * POSSIBILITY OF SUCH DAMAGE.
  *****************************************************************************/
 
-package com.esotericsoftware.spine;
-
-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.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.MeshAttachment;
-import com.esotericsoftware.spine.attachments.PathAttachment;
-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 PathAttachment newPathAttachment (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) {
-			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;
-	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, "events1", false);
-		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), //
-
-			// First 2 set/addAnimation calls are done.
-
-			expect(0, "start", 0, 0), //
-			expect(0, "event 0", 0, 0), //
-			expect(0, "event 14", 0.467f, 0.467f), //
-			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, "events1", false);
-		state.addAnimation(0, "events2", false, 0);
-		state.addAnimation(0, "events1", false, 0);
-		state.addAnimation(0, "events2", false, 0);
-		state.setAnimation(0, "events1", false);
-		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, "events1", false);
-		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, "events1", false);
-		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, "events1", false);
-		state.addAnimation(0, "events2", false, 0);
-		state.addAnimation(0, "events1", false, 0);
-		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, "events1", false);
-		state.addAnimation(0, "events2", false, 0.5f);
-		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("events1", "events2", 0.7f);
-		state.setAnimation(0, "events1", true);
-		state.addAnimation(0, "events2", false, 0.9f);
-		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, "events1", false);
-		state.addAnimation(0, "events2", false, 0.4f);
-		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("events1", "events2", 0.7f);
-		state.setAnimation(0, "events1", false).setEventThreshold(0.5f);
-		state.addAnimation(0, "events2", false, 0.4f);
-		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, "events1", true).setEventThreshold(1);
-		state.addAnimation(0, "events2", false, 0.8f).setMixDuration(0.7f);
-		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) //
-		);
-		state.setAnimation(0, "events1", 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, "events1", false);
-		state.addAnimation(0, "events2", false, 2);
-		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, "events1", true);
-		run(0.1f, 6, new TestListener() {
-			public void frame (float time) {
-				if (MathUtils.isEqual(time, 1.4f)) state.addAnimation(0, "events2", false, 0);
-			}
-		});
-
-		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, "events1", false, 0);
-		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, "events1", 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) //
-		);
-		entry = state.setAnimation(0, "events1", 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) //
-		);
-		entry = state.setAnimation(0, "events1", 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, "events1", false);
-		entry.setAnimationStart(0.2f);
-		entry.setAnimationLast(0.2f);
-		entry.setAnimationEnd(0.8f);
-		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, "events1", true);
-		entry.setAnimationStart(0.2f);
-		entry.setAnimationLast(0.2f);
-		entry.setAnimationEnd(0.8f);
-		entry.setEventThreshold(1);
-		state.addAnimation(0, "events2", false, 0.7f).setMixDuration(0.7f);
-		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, "events1", true);
-		run(0.1f, 1000, new TestListener() {
-			public void frame (float time) {
-				if (MathUtils.isEqual(time, 1f)) state.setAnimation(0, "events2", false).setMixDuration(0.7f);
-			}
-		});
-
-		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), //
-
-			// 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(1, "start", 0, 0.8f), //
-			expect(1, "event 0", 0.1f, 0.9f), //
-			expect(1, "event 14", 0.5f, 1.3f), //
-			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, "events1", false); // First should be ignored.
-		state.setAnimation(0, "events2", false);
-		run(0.1f, 1000, new TestListener() {
-			public void frame (float time) {
-				if (MathUtils.isEqual(time, 0.8f)) {
-					state.setAnimation(0, "events1", false); // First should be ignored.
-					state.setAnimation(0, "events2", false);
-				}
-			}
-		});
-
-		setup("addAnimation with delay on empty track", // 22
-			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, "events1", false, 5);
-		run(0.1f, 10, null);
-
-		setup("setAnimation during AnimationStateListener"); // 23
-		state.addListener(new AnimationStateListener() {
-			public void start (TrackEntry entry) {
-				if (entry.getAnimation().getName().equals("events1")) state.setAnimation(1, "events2", false);
-			}
-
-			public void interrupt (TrackEntry entry) {
-				state.addAnimation(3, "events2", false, 0);
-			}
-
-			public void end (TrackEntry entry) {
-				if (entry.getAnimation().getName().equals("events1")) state.setAnimation(0, "events2", false);
-			}
-
-			public void dispose (TrackEntry entry) {
-				if (entry.getAnimation().getName().equals("events1")) state.setAnimation(1, "events2", false);
-			}
-
-			public void complete (TrackEntry entry) {
-				if (entry.getAnimation().getName().equals("events1")) state.setAnimation(1, "events2", false);
-			}
-
-			public void event (TrackEntry entry, Event event) {
-				if (entry.getTrackIndex() != 2) state.setAnimation(2, "events2", false);
-			}
-		});
-		state.addAnimation(0, "events1", false, 0);
-		state.addAnimation(0, "events2", false, 0);
-		state.setAnimation(1, "events2", false);
-		run(0.1f, 10, null);
-
-		setup("clearTrack", // 24
-			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, "events1", false, 0);
-		run(0.1f, 10, new TestListener() {
-			public void frame (float time) {
-				if (MathUtils.isEqual(time, 0.7f)) state.clearTrack(0);
-			}
-		});
-
-		setup("setEmptyAnimation", // 25
-			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.1f, 0.9f), //
-			expect(-1, "dispose", 0.1f, 0.9f) //
-		);
-		state.addAnimation(0, "events1", false, 0);
-		run(0.1f, 10, new TestListener() {
-			public void frame (float time) {
-				if (MathUtils.isEqual(time, 0.7f)) state.setEmptyAnimation(0, 0);
-			}
-		});
-
-		System.out.println("AnimationState tests passed.");
-	}
-
-	void setup (String description, Result... expectedArray) {
-		test++;
-		expected.addAll(expectedArray);
-		stateData = new AnimationStateData(skeletonData);
-		state = new AnimationState(stateData);
-		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);
-		}
-		// 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;
-		}
-		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;
-	}
-
-	void log (String message) {
-		System.out.println(message);
-	}
-
-	class Result {
-		String name;
-		int animationIndex;
-		float trackTime, totalTime;
-
-		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();
-	}
+package com.esotericsoftware.spine;
+
+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.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.MeshAttachment;
+import com.esotericsoftware.spine.attachments.PathAttachment;
+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 PathAttachment newPathAttachment (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) {
+			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;
+	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, "events1", false);
+		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), //
+
+			// First 2 set/addAnimation calls are done.
+
+			expect(0, "start", 0, 0), //
+			expect(0, "event 0", 0, 0), //
+			expect(0, "event 14", 0.467f, 0.467f), //
+			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, "events1", false);
+		state.addAnimation(0, "events2", false, 0);
+		state.addAnimation(0, "events1", false, 0);
+		state.addAnimation(0, "events2", false, 0);
+		state.setAnimation(0, "events1", false);
+		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, "events1", false);
+		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, "events1", false);
+		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, "events1", false);
+		state.addAnimation(0, "events2", false, 0);
+		state.addAnimation(0, "events1", false, 0);
+		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, "events1", false);
+		state.addAnimation(0, "events2", false, 0.5f);
+		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("events1", "events2", 0.7f);
+		state.setAnimation(0, "events1", true);
+		state.addAnimation(0, "events2", false, 0.9f);
+		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, "events1", false);
+		state.addAnimation(0, "events2", false, 0.4f);
+		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("events1", "events2", 0.7f);
+		state.setAnimation(0, "events1", false).setEventThreshold(0.5f);
+		state.addAnimation(0, "events2", false, 0.4f);
+		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, "events1", true).setEventThreshold(1);
+		state.addAnimation(0, "events2", false, 0.8f).setMixDuration(0.7f);
+		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) //
+		);
+		state.setAnimation(0, "events1", 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, "events1", false);
+		state.addAnimation(0, "events2", false, 2);
+		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, "events1", true);
+		run(0.1f, 6, new TestListener() {
+			public void frame (float time) {
+				if (MathUtils.isEqual(time, 1.4f)) state.addAnimation(0, "events2", false, 0);
+			}
+		});
+
+		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, "events1", false, 0);
+		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, "events1", 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) //
+		);
+		entry = state.setAnimation(0, "events1", 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) //
+		);
+		entry = state.setAnimation(0, "events1", 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, "events1", false);
+		entry.setAnimationStart(0.2f);
+		entry.setAnimationLast(0.2f);
+		entry.setAnimationEnd(0.8f);
+		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, "events1", true);
+		entry.setAnimationStart(0.2f);
+		entry.setAnimationLast(0.2f);
+		entry.setAnimationEnd(0.8f);
+		entry.setEventThreshold(1);
+		state.addAnimation(0, "events2", false, 0.7f).setMixDuration(0.7f);
+		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, "events1", true);
+		run(0.1f, 1000, new TestListener() {
+			public void frame (float time) {
+				if (MathUtils.isEqual(time, 1f)) state.setAnimation(0, "events2", false).setMixDuration(0.7f);
+			}
+		});
+
+		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), //
+
+			// 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(1, "start", 0, 0.8f), //
+			expect(1, "event 0", 0.1f, 0.9f), //
+			expect(1, "event 14", 0.5f, 1.3f), //
+			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, "events1", false); // First should be ignored.
+		state.setAnimation(0, "events2", false);
+		run(0.1f, 1000, new TestListener() {
+			public void frame (float time) {
+				if (MathUtils.isEqual(time, 0.8f)) {
+					state.setAnimation(0, "events1", false); // First should be ignored.
+					state.setAnimation(0, "events2", false);
+				}
+			}
+		});
+
+		setup("addAnimation with delay on empty track", // 22
+			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, "events1", false, 5);
+		run(0.1f, 10, null);
+
+		setup("setAnimation during AnimationStateListener"); // 23
+		state.addListener(new AnimationStateListener() {
+			public void start (TrackEntry entry) {
+				if (entry.getAnimation().getName().equals("events1")) state.setAnimation(1, "events2", false);
+			}
+
+			public void interrupt (TrackEntry entry) {
+				state.addAnimation(3, "events2", false, 0);
+			}
+
+			public void end (TrackEntry entry) {
+				if (entry.getAnimation().getName().equals("events1")) state.setAnimation(0, "events2", false);
+			}
+
+			public void dispose (TrackEntry entry) {
+				if (entry.getAnimation().getName().equals("events1")) state.setAnimation(1, "events2", false);
+			}
+
+			public void complete (TrackEntry entry) {
+				if (entry.getAnimation().getName().equals("events1")) state.setAnimation(1, "events2", false);
+			}
+
+			public void event (TrackEntry entry, Event event) {
+				if (entry.getTrackIndex() != 2) state.setAnimation(2, "events2", false);
+			}
+		});
+		state.addAnimation(0, "events1", false, 0);
+		state.addAnimation(0, "events2", false, 0);
+		state.setAnimation(1, "events2", false);
+		run(0.1f, 10, null);
+
+		setup("clearTrack", // 24
+			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, "events1", false, 0);
+		run(0.1f, 10, new TestListener() {
+			public void frame (float time) {
+				if (MathUtils.isEqual(time, 0.7f)) state.clearTrack(0);
+			}
+		});
+
+		setup("setEmptyAnimation", // 25
+			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.1f, 0.9f), //
+			expect(-1, "dispose", 0.1f, 0.9f) //
+		);
+		state.addAnimation(0, "events1", false, 0);
+		run(0.1f, 10, new TestListener() {
+			public void frame (float time) {
+				if (MathUtils.isEqual(time, 0.7f)) state.setEmptyAnimation(0, 0);
+			}
+		});
+
+		System.out.println("AnimationState tests passed.");
+	}
+
+	void setup (String description, Result... expectedArray) {
+		test++;
+		expected.addAll(expectedArray);
+		stateData = new AnimationStateData(skeletonData);
+		state = new AnimationState(stateData);
+		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);
+		}
+		// 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;
+		}
+		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;
+	}
+
+	void log (String message) {
+		System.out.println(message);
+	}
+
+	class Result {
+		String name;
+		int animationIndex;
+		float trackTime, totalTime;
+
+		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();
+	}
 }

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

@@ -1,76 +1,75 @@
-/******************************************************************************
- * Spine Runtimes Software License
- * Version 2.3
- * 
- * Copyright (c) 2013-2015, Esoteric Software
- * All rights reserved.
- * 
- * You are granted a perpetual, non-exclusive, non-sublicensable and
- * non-transferable license to use, install, execute and perform the Spine
- * Runtimes Software (the "Software") and derivative works solely for personal
- * or internal use. Without the written permission of Esoteric Software (see
- * Section 2 of the Spine Software License Agreement), you may not (a) modify,
- * translate, adapt or otherwise create derivative works, improvements of the
- * Software or develop new applications using the Software or (b) remove,
- * delete, alter or obscure any trademarks or any copyright, trademark, patent
- * or other intellectual property or proprietary rights notices on or in the
- * Software, including any copy thereof. Redistributions in binary or source
- * form must include this license and terms.
- * 
- * THIS SOFTWARE IS PROVIDED BY ESOTERIC SOFTWARE "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 BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
- * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
- * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS;
- * OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
- * WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR
- * OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
- * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+/******************************************************************************
+ * Spine Runtimes Software License v2.5
+ * 
+ * Copyright (c) 2013-2016, Esoteric Software
+ * All rights reserved.
+ * 
+ * You are granted a perpetual, non-exclusive, non-sublicensable, and
+ * non-transferable license to use, install, execute, and perform the Spine
+ * Runtimes software and derivative works solely for personal or internal
+ * use. Without the written permission of Esoteric Software (see Section 2 of
+ * the Spine Software License Agreement), you may not (a) modify, translate,
+ * adapt, or develop new applications using the Spine Runtimes or otherwise
+ * create derivative works or improvements of the Spine Runtimes or (b) remove,
+ * delete, alter, or obscure any trademarks or any copyright, trademark, patent,
+ * or other intellectual property or proprietary rights notices on or in the
+ * Software, including any copy thereof. Redistributions in binary or source
+ * form must include this license and terms.
+ * 
+ * THIS SOFTWARE IS PROVIDED BY ESOTERIC SOFTWARE "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 BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
+ * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES, BUSINESS INTERRUPTION, OR LOSS OF
+ * USE, DATA, OR PROFITS) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER
+ * IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+ * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
+ * POSSIBILITY OF SUCH DAMAGE.
  *****************************************************************************/
 
-package com.esotericsoftware.spine;
-
-import com.badlogic.gdx.files.FileHandle;
-import com.esotericsoftware.spine.attachments.AttachmentLoader;
-import com.esotericsoftware.spine.attachments.BoundingBoxAttachment;
-import com.esotericsoftware.spine.attachments.RegionAttachment;
-import com.esotericsoftware.spine.attachments.MeshAttachment;
-import com.esotericsoftware.spine.attachments.PathAttachment;
-
-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 PathAttachment newPathAttachment (Skin skin, String name) {
-				return null;
-			}
-		});
-		SkeletonData skeletonData = json.readSkeletonData(new FileHandle("assets/spineboy/spineboy.json"));
-		Skeleton skeleton = new Skeleton(skeletonData);
-		Bone bone = skeleton.findBone("gunTip");
-		float fps = 1 / 15f;
-		for (Animation animation : skeletonData.getAnimations()) {
-			float time = 0;
-			while (time < animation.getDuration()) {
-				animation.apply(skeleton, time, time, false, null, 1, false, false);
-				skeleton.updateWorldTransform();
-				System.out
-					.println(animation.getName() + "," + bone.getWorldX() + "," + bone.getWorldY() + "," + bone.getWorldRotationX());
-				time += fps;
-			}
-		}
-	}
+package com.esotericsoftware.spine;
+
+import com.badlogic.gdx.files.FileHandle;
+import com.esotericsoftware.spine.attachments.AttachmentLoader;
+import com.esotericsoftware.spine.attachments.BoundingBoxAttachment;
+import com.esotericsoftware.spine.attachments.RegionAttachment;
+import com.esotericsoftware.spine.attachments.MeshAttachment;
+import com.esotericsoftware.spine.attachments.PathAttachment;
+
+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 PathAttachment newPathAttachment (Skin skin, String name) {
+				return null;
+			}
+		});
+		SkeletonData skeletonData = json.readSkeletonData(new FileHandle("assets/spineboy/spineboy.json"));
+		Skeleton skeleton = new Skeleton(skeletonData);
+		Bone bone = skeleton.findBone("gunTip");
+		float fps = 1 / 15f;
+		for (Animation animation : skeletonData.getAnimations()) {
+			float time = 0;
+			while (time < animation.getDuration()) {
+				animation.apply(skeleton, time, time, false, null, 1, false, false);
+				skeleton.updateWorldTransform();
+				System.out
+					.println(animation.getName() + "," + bone.getWorldX() + "," + bone.getWorldY() + "," + bone.getWorldRotationX());
+				time += fps;
+			}
+		}
+	}
 }

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

@@ -1,249 +1,248 @@
-/******************************************************************************
- * Spine Runtimes Software License
- * Version 2.3
- * 
- * Copyright (c) 2013-2015, Esoteric Software
- * All rights reserved.
- * 
- * You are granted a perpetual, non-exclusive, non-sublicensable and
- * non-transferable license to use, install, execute and perform the Spine
- * Runtimes Software (the "Software") and derivative works solely for personal
- * or internal use. Without the written permission of Esoteric Software (see
- * Section 2 of the Spine Software License Agreement), you may not (a) modify,
- * translate, adapt or otherwise create derivative works, improvements of the
- * Software or develop new applications using the Software or (b) remove,
- * delete, alter or obscure any trademarks or any copyright, trademark, patent
- * or other intellectual property or proprietary rights notices on or in the
- * Software, including any copy thereof. Redistributions in binary or source
- * form must include this license and terms.
- * 
- * THIS SOFTWARE IS PROVIDED BY ESOTERIC SOFTWARE "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 BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
- * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
- * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS;
- * OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
- * WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR
- * OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
- * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+/******************************************************************************
+ * Spine Runtimes Software License v2.5
+ * 
+ * Copyright (c) 2013-2016, Esoteric Software
+ * All rights reserved.
+ * 
+ * You are granted a perpetual, non-exclusive, non-sublicensable, and
+ * non-transferable license to use, install, execute, and perform the Spine
+ * Runtimes software and derivative works solely for personal or internal
+ * use. Without the written permission of Esoteric Software (see Section 2 of
+ * the Spine Software License Agreement), you may not (a) modify, translate,
+ * adapt, or develop new applications using the Spine Runtimes or otherwise
+ * create derivative works or improvements of the Spine Runtimes or (b) remove,
+ * delete, alter, or obscure any trademarks or any copyright, trademark, patent,
+ * or other intellectual property or proprietary rights notices on or in the
+ * Software, including any copy thereof. Redistributions in binary or source
+ * form must include this license and terms.
+ * 
+ * THIS SOFTWARE IS PROVIDED BY ESOTERIC SOFTWARE "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 BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
+ * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES, BUSINESS INTERRUPTION, OR LOSS OF
+ * USE, DATA, OR PROFITS) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER
+ * IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+ * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
+ * POSSIBILITY OF SUCH DAMAGE.
  *****************************************************************************/
 
-package com.esotericsoftware.spine;
-
-import com.esotericsoftware.spine.attachments.AtlasAttachmentLoader;
-import com.esotericsoftware.spine.attachments.RegionAttachment;
-
-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.GL20;
-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;
-
-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.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();
-
-		Gdx.gl.glClear(GL20.GL_COLOR_BUFFER_BIT);
-		batch.setProjectionMatrix(camera.projection);
-		batch.setTransformMatrix(camera.view);
-		batch.begin();
-
-		animation.apply(skeleton, time, time, true, events, 1, false, false);
-		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 = skeleton.x + slot.getBone().getWorldX();
-			float y = skeleton.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);
-	}
+package com.esotericsoftware.spine;
+
+import com.esotericsoftware.spine.attachments.AtlasAttachmentLoader;
+import com.esotericsoftware.spine.attachments.RegionAttachment;
+
+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.GL20;
+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;
+
+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.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();
+
+		Gdx.gl.glClear(GL20.GL_COLOR_BUFFER_BIT);
+		batch.setProjectionMatrix(camera.projection);
+		batch.setTransformMatrix(camera.view);
+		batch.begin();
+
+		animation.apply(skeleton, time, time, true, events, 1, false, false);
+		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 = skeleton.x + slot.getBone().getWorldX();
+			float y = skeleton.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);
+	}
 }

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

@@ -1,228 +1,227 @@
-/******************************************************************************
- * Spine Runtimes Software License
- * Version 2.3
- * 
- * Copyright (c) 2013-2015, Esoteric Software
- * All rights reserved.
- * 
- * You are granted a perpetual, non-exclusive, non-sublicensable and
- * non-transferable license to use, install, execute and perform the Spine
- * Runtimes Software (the "Software") and derivative works solely for personal
- * or internal use. Without the written permission of Esoteric Software (see
- * Section 2 of the Spine Software License Agreement), you may not (a) modify,
- * translate, adapt or otherwise create derivative works, improvements of the
- * Software or develop new applications using the Software or (b) remove,
- * delete, alter or obscure any trademarks or any copyright, trademark, patent
- * or other intellectual property or proprietary rights notices on or in the
- * Software, including any copy thereof. Redistributions in binary or source
- * form must include this license and terms.
- * 
- * THIS SOFTWARE IS PROVIDED BY ESOTERIC SOFTWARE "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 BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
- * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
- * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS;
- * OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
- * WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR
- * OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
- * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+/******************************************************************************
+ * Spine Runtimes Software License v2.5
+ * 
+ * Copyright (c) 2013-2016, Esoteric Software
+ * All rights reserved.
+ * 
+ * You are granted a perpetual, non-exclusive, non-sublicensable, and
+ * non-transferable license to use, install, execute, and perform the Spine
+ * Runtimes software and derivative works solely for personal or internal
+ * use. Without the written permission of Esoteric Software (see Section 2 of
+ * the Spine Software License Agreement), you may not (a) modify, translate,
+ * adapt, or develop new applications using the Spine Runtimes or otherwise
+ * create derivative works or improvements of the Spine Runtimes or (b) remove,
+ * delete, alter, or obscure any trademarks or any copyright, trademark, patent,
+ * or other intellectual property or proprietary rights notices on or in the
+ * Software, including any copy thereof. Redistributions in binary or source
+ * form must include this license and terms.
+ * 
+ * THIS SOFTWARE IS PROVIDED BY ESOTERIC SOFTWARE "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 BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
+ * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES, BUSINESS INTERRUPTION, OR LOSS OF
+ * USE, DATA, OR PROFITS) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER
+ * IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+ * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
+ * POSSIBILITY OF SUCH DAMAGE.
  *****************************************************************************/
 
-package com.esotericsoftware.spine;
-
-import com.esotericsoftware.spine.Animation.EventTimeline;
-
-import com.badlogic.gdx.utils.Array;
-import com.badlogic.gdx.utils.StringBuilder;
-
-import java.util.Arrays;
-
-/** 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(0);
-	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, false, false);
-
-			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, false, false);
-						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, false, false);
-					fail("Wrong event fired.");
-				}
-				eventIndex++;
-				beforeCount++;
-			}
-
-			if (time >= timeEnd) break;
-			lastTime = time;
-			i++;
-		}
-		if (firedEvents.size < eventsCount) {
-			timeline.apply(skeleton, lastTimeLooped, timeLooped, firedEvents, 1, false, false);
-			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();
-	}
+package com.esotericsoftware.spine;
+
+import com.esotericsoftware.spine.Animation.EventTimeline;
+
+import com.badlogic.gdx.utils.Array;
+import com.badlogic.gdx.utils.StringBuilder;
+
+import java.util.Arrays;
+
+/** 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(0);
+	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, false, false);
+
+			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, false, false);
+						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, false, false);
+					fail("Wrong event fired.");
+				}
+				eventIndex++;
+				beforeCount++;
+			}
+
+			if (time >= timeEnd) break;
+			lastTime = time;
+			i++;
+		}
+		if (firedEvents.size < eventsCount) {
+			timeline.apply(skeleton, lastTimeLooped, timeLooped, firedEvents, 1, false, false);
+			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();
+	}
 }

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

@@ -1,142 +1,141 @@
-/******************************************************************************
- * Spine Runtimes Software License
- * Version 2.3
- * 
- * Copyright (c) 2013-2015, Esoteric Software
- * All rights reserved.
- * 
- * You are granted a perpetual, non-exclusive, non-sublicensable and
- * non-transferable license to use, install, execute and perform the Spine
- * Runtimes Software (the "Software") and derivative works solely for personal
- * or internal use. Without the written permission of Esoteric Software (see
- * Section 2 of the Spine Software License Agreement), you may not (a) modify,
- * translate, adapt or otherwise create derivative works, improvements of the
- * Software or develop new applications using the Software or (b) remove,
- * delete, alter or obscure any trademarks or any copyright, trademark, patent
- * or other intellectual property or proprietary rights notices on or in the
- * Software, including any copy thereof. Redistributions in binary or source
- * form must include this license and terms.
- * 
- * THIS SOFTWARE IS PROVIDED BY ESOTERIC SOFTWARE "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 BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
- * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
- * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS;
- * OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
- * WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR
- * OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
- * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+/******************************************************************************
+ * Spine Runtimes Software License v2.5
+ * 
+ * Copyright (c) 2013-2016, Esoteric Software
+ * All rights reserved.
+ * 
+ * You are granted a perpetual, non-exclusive, non-sublicensable, and
+ * non-transferable license to use, install, execute, and perform the Spine
+ * Runtimes software and derivative works solely for personal or internal
+ * use. Without the written permission of Esoteric Software (see Section 2 of
+ * the Spine Software License Agreement), you may not (a) modify, translate,
+ * adapt, or develop new applications using the Spine Runtimes or otherwise
+ * create derivative works or improvements of the Spine Runtimes or (b) remove,
+ * delete, alter, or obscure any trademarks or any copyright, trademark, patent,
+ * or other intellectual property or proprietary rights notices on or in the
+ * Software, including any copy thereof. Redistributions in binary or source
+ * form must include this license and terms.
+ * 
+ * THIS SOFTWARE IS PROVIDED BY ESOTERIC SOFTWARE "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 BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
+ * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES, BUSINESS INTERRUPTION, OR LOSS OF
+ * USE, DATA, OR PROFITS) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER
+ * IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+ * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
+ * POSSIBILITY OF SUCH DAMAGE.
  *****************************************************************************/
 
-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.g2d.SpriteBatch;
-import com.badlogic.gdx.graphics.g2d.TextureAtlas;
-import com.badlogic.gdx.utils.Array;
-
-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 + ".json"));
-		} else {
-			SkeletonBinary binary = new SkeletonBinary(atlas);
-			binary.setScale(0.6f);
-			skeletonData = binary.readSkeletonData(Gdx.files.internal(name + ".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);
-
-		Gdx.gl.glClear(GL20.GL_COLOR_BUFFER_BIT);
-
-		// 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, false, false);
-		} else if (time > blendOutStart) {
-			// blend out jump
-			walkAnimation.apply(skeleton, time, time, true, events, 1, false, false);
-			jumpAnimation.apply(skeleton, time - beforeJump, time - beforeJump, false, events, 1 - (time - blendOutStart) / blendOut, false, false);
-		} else if (time > beforeJump + blendIn) {
-			// just jump
-			jumpAnimation.apply(skeleton, time - beforeJump, time - beforeJump, false, events, 1, false, false);
-		} else if (time > beforeJump) {
-			// blend in jump
-			walkAnimation.apply(skeleton, time, time, true, events, 1, false, false);
-			jumpAnimation.apply(skeleton, time - beforeJump, time - beforeJump, false, events, (time - beforeJump) / blendIn, false, false);
-		} else {
-			// just walk before jump
-			walkAnimation.apply(skeleton, time, time, true, events, 1, false, false);
-		}
-
-		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());
-	}
+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.g2d.SpriteBatch;
+import com.badlogic.gdx.graphics.g2d.TextureAtlas;
+import com.badlogic.gdx.utils.Array;
+
+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 + ".json"));
+		} else {
+			SkeletonBinary binary = new SkeletonBinary(atlas);
+			binary.setScale(0.6f);
+			skeletonData = binary.readSkeletonData(Gdx.files.internal(name + ".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);
+
+		Gdx.gl.glClear(GL20.GL_COLOR_BUFFER_BIT);
+
+		// 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, false, false);
+		} else if (time > blendOutStart) {
+			// blend out jump
+			walkAnimation.apply(skeleton, time, time, true, events, 1, false, false);
+			jumpAnimation.apply(skeleton, time - beforeJump, time - beforeJump, false, events, 1 - (time - blendOutStart) / blendOut, false, false);
+		} else if (time > beforeJump + blendIn) {
+			// just jump
+			jumpAnimation.apply(skeleton, time - beforeJump, time - beforeJump, false, events, 1, false, false);
+		} else if (time > beforeJump) {
+			// blend in jump
+			walkAnimation.apply(skeleton, time, time, true, events, 1, false, false);
+			jumpAnimation.apply(skeleton, time - beforeJump, time - beforeJump, false, events, (time - beforeJump) / blendIn, false, false);
+		} else {
+			// just walk before jump
+			walkAnimation.apply(skeleton, time, time, true, events, 1, false, false);
+		}
+
+		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());
+	}
 }

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

@@ -1,385 +1,384 @@
-/******************************************************************************
- * Spine Runtimes Software License
- * Version 2.3
- * 
- * Copyright (c) 2013-2015, Esoteric Software
- * All rights reserved.
- * 
- * You are granted a perpetual, non-exclusive, non-sublicensable and
- * non-transferable license to use, install, execute and perform the Spine
- * Runtimes Software (the "Software") and derivative works solely for personal
- * or internal use. Without the written permission of Esoteric Software (see
- * Section 2 of the Spine Software License Agreement), you may not (a) modify,
- * translate, adapt or otherwise create derivative works, improvements of the
- * Software or develop new applications using the Software or (b) remove,
- * delete, alter or obscure any trademarks or any copyright, trademark, patent
- * or other intellectual property or proprietary rights notices on or in the
- * Software, including any copy thereof. Redistributions in binary or source
- * form must include this license and terms.
- * 
- * THIS SOFTWARE IS PROVIDED BY ESOTERIC SOFTWARE "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 BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
- * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
- * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS;
- * OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
- * WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR
- * OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
- * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+/******************************************************************************
+ * Spine Runtimes Software License v2.5
+ * 
+ * Copyright (c) 2013-2016, Esoteric Software
+ * All rights reserved.
+ * 
+ * You are granted a perpetual, non-exclusive, non-sublicensable, and
+ * non-transferable license to use, install, execute, and perform the Spine
+ * Runtimes software and derivative works solely for personal or internal
+ * use. Without the written permission of Esoteric Software (see Section 2 of
+ * the Spine Software License Agreement), you may not (a) modify, translate,
+ * adapt, or develop new applications using the Spine Runtimes or otherwise
+ * create derivative works or improvements of the Spine Runtimes or (b) remove,
+ * delete, alter, or obscure any trademarks or any copyright, trademark, patent,
+ * or other intellectual property or proprietary rights notices on or in the
+ * Software, including any copy thereof. Redistributions in binary or source
+ * form must include this license and terms.
+ * 
+ * THIS SOFTWARE IS PROVIDED BY ESOTERIC SOFTWARE "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 BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
+ * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES, BUSINESS INTERRUPTION, OR LOSS OF
+ * USE, DATA, OR PROFITS) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER
+ * IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+ * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
+ * POSSIBILITY OF SUCH DAMAGE.
  *****************************************************************************/
 
-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.GL20;
-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;
-
-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() - 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, false, false);
-		skeleton.updateWorldTransform();
-		skeleton.update(Gdx.graphics.getDeltaTime());
-
-		lightPosition.x = Gdx.input.getX();
-		lightPosition.y = (Gdx.graphics.getHeight() - Gdx.input.getY());
-
-		Gdx.gl.glClear(GL20.GL_COLOR_BUFFER_BIT);
-
-		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.begin();
-		program.setUniformi("u_texture", 0);
-		program.setUniformi("u_normals", 1);
-		program.end();
-
-		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]));
-	}
+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.GL20;
+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;
+
+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() - 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, false, false);
+		skeleton.updateWorldTransform();
+		skeleton.update(Gdx.graphics.getDeltaTime());
+
+		lightPosition.x = Gdx.input.getX();
+		lightPosition.y = (Gdx.graphics.getHeight() - Gdx.input.getY());
+
+		Gdx.gl.glClear(GL20.GL_COLOR_BUFFER_BIT);
+
+		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.begin();
+		program.setUniformi("u_texture", 0);
+		program.setUniformi("u_normals", 1);
+		program.end();
+
+		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]));
+	}
 }

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

@@ -1,176 +1,175 @@
-/******************************************************************************
- * Spine Runtimes Software License
- * Version 2.3
- * 
- * Copyright (c) 2013-2015, Esoteric Software
- * All rights reserved.
- * 
- * You are granted a perpetual, non-exclusive, non-sublicensable and
- * non-transferable license to use, install, execute and perform the Spine
- * Runtimes Software (the "Software") and derivative works solely for personal
- * or internal use. Without the written permission of Esoteric Software (see
- * Section 2 of the Spine Software License Agreement), you may not (a) modify,
- * translate, adapt or otherwise create derivative works, improvements of the
- * Software or develop new applications using the Software or (b) remove,
- * delete, alter or obscure any trademarks or any copyright, trademark, patent
- * or other intellectual property or proprietary rights notices on or in the
- * Software, including any copy thereof. Redistributions in binary or source
- * form must include this license and terms.
- * 
- * THIS SOFTWARE IS PROVIDED BY ESOTERIC SOFTWARE "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 BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
- * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
- * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS;
- * OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
- * WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR
- * OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
- * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+/******************************************************************************
+ * Spine Runtimes Software License v2.5
+ * 
+ * Copyright (c) 2013-2016, Esoteric Software
+ * All rights reserved.
+ * 
+ * You are granted a perpetual, non-exclusive, non-sublicensable, and
+ * non-transferable license to use, install, execute, and perform the Spine
+ * Runtimes software and derivative works solely for personal or internal
+ * use. Without the written permission of Esoteric Software (see Section 2 of
+ * the Spine Software License Agreement), you may not (a) modify, translate,
+ * adapt, or develop new applications using the Spine Runtimes or otherwise
+ * create derivative works or improvements of the Spine Runtimes or (b) remove,
+ * delete, alter, or obscure any trademarks or any copyright, trademark, patent,
+ * or other intellectual property or proprietary rights notices on or in the
+ * Software, including any copy thereof. Redistributions in binary or source
+ * form must include this license and terms.
+ * 
+ * THIS SOFTWARE IS PROVIDED BY ESOTERIC SOFTWARE "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 BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
+ * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES, BUSINESS INTERRUPTION, OR LOSS OF
+ * USE, DATA, OR PROFITS) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER
+ * IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+ * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
+ * POSSIBILITY OF SUCH DAMAGE.
  *****************************************************************************/
 
-package com.esotericsoftware.spine;
-
-import com.esotericsoftware.spine.AnimationState.AnimationStateListener;
-import com.esotericsoftware.spine.AnimationState.TrackEntry;
-import com.esotericsoftware.spine.attachments.BoundingBoxAttachment;
-
-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.SpriteBatch;
-import com.badlogic.gdx.graphics.g2d.TextureAtlas;
-import com.badlogic.gdx.math.Vector3;
-
-public class SimpleTest2 extends ApplicationAdapter {
-	OrthographicCamera camera;
-	SpriteBatch batch;
-	SkeletonRenderer renderer;
-	SkeletonRendererDebug debugRenderer;
-
-	TextureAtlas atlas;
-	Skeleton skeleton;
-	SkeletonBounds bounds;
-	AnimationState state;
-
-	public void create () {
-		camera = new OrthographicCamera();
-		batch = new SpriteBatch();
-		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.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);
-
-		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());
-	}
+package com.esotericsoftware.spine;
+
+import com.esotericsoftware.spine.AnimationState.AnimationStateListener;
+import com.esotericsoftware.spine.AnimationState.TrackEntry;
+import com.esotericsoftware.spine.attachments.BoundingBoxAttachment;
+
+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.SpriteBatch;
+import com.badlogic.gdx.graphics.g2d.TextureAtlas;
+import com.badlogic.gdx.math.Vector3;
+
+public class SimpleTest2 extends ApplicationAdapter {
+	OrthographicCamera camera;
+	SpriteBatch batch;
+	SkeletonRenderer renderer;
+	SkeletonRendererDebug debugRenderer;
+
+	TextureAtlas atlas;
+	Skeleton skeleton;
+	SkeletonBounds bounds;
+	AnimationState state;
+
+	public void create () {
+		camera = new OrthographicCamera();
+		batch = new SpriteBatch();
+		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.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);
+
+		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());
+	}
 }

+ 1167 - 1168
spine-libgdx/spine-libgdx/src/com/esotericsoftware/spine/Animation.java

@@ -1,1171 +1,1170 @@
-/******************************************************************************
- * Spine Runtimes Software License
- * Version 2.3
- * 
- * Copyright (c) 2013-2015, Esoteric Software
- * All rights reserved.
- * 
- * You are granted a perpetual, non-exclusive, non-sublicensable and
- * non-transferable license to use, install, execute and perform the Spine
- * Runtimes Software (the "Software") and derivative works solely for personal
- * or internal use. Without the written permission of Esoteric Software (see
- * Section 2 of the Spine Software License Agreement), you may not (a) modify,
- * translate, adapt or otherwise create derivative works, improvements of the
- * Software or develop new applications using the Software or (b) remove,
- * delete, alter or obscure any trademarks or any copyright, trademark, patent
- * or other intellectual property or proprietary rights notices on or in the
- * Software, including any copy thereof. Redistributions in binary or source
- * form must include this license and terms.
- * 
- * THIS SOFTWARE IS PROVIDED BY ESOTERIC SOFTWARE "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 BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
- * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
- * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS;
- * OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
- * WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR
- * OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
- * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+/******************************************************************************
+ * Spine Runtimes Software License v2.5
+ * 
+ * Copyright (c) 2013-2016, Esoteric Software
+ * All rights reserved.
+ * 
+ * You are granted a perpetual, non-exclusive, non-sublicensable, and
+ * non-transferable license to use, install, execute, and perform the Spine
+ * Runtimes software and derivative works solely for personal or internal
+ * use. Without the written permission of Esoteric Software (see Section 2 of
+ * the Spine Software License Agreement), you may not (a) modify, translate,
+ * adapt, or develop new applications using the Spine Runtimes or otherwise
+ * create derivative works or improvements of the Spine Runtimes or (b) remove,
+ * delete, alter, or obscure any trademarks or any copyright, trademark, patent,
+ * or other intellectual property or proprietary rights notices on or in the
+ * Software, including any copy thereof. Redistributions in binary or source
+ * form must include this license and terms.
+ * 
+ * THIS SOFTWARE IS PROVIDED BY ESOTERIC SOFTWARE "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 BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
+ * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES, BUSINESS INTERRUPTION, OR LOSS OF
+ * USE, DATA, OR PROFITS) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER
+ * IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+ * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
+ * POSSIBILITY OF SUCH DAMAGE.
  *****************************************************************************/
 
-package com.esotericsoftware.spine;
-
-import com.badlogic.gdx.graphics.Color;
-import com.badlogic.gdx.math.MathUtils;
-import com.badlogic.gdx.utils.Array;
-import com.badlogic.gdx.utils.FloatArray;
-import com.esotericsoftware.spine.attachments.Attachment;
-import com.esotericsoftware.spine.attachments.VertexAttachment;
-
-public class Animation {
-	final String name;
-	final Array<Timeline> timelines;
-	float duration;
-
-	public Animation (String name, Array<Timeline> timelines, float duration) {
-		if (name == null) throw new IllegalArgumentException("name cannot be null.");
-		if (timelines == null) throw new IllegalArgumentException("timelines cannot be null.");
-		this.name = name;
-		this.timelines = timelines;
-		this.duration = duration;
-	}
-
-	public Array<Timeline> getTimelines () {
-		return timelines;
-	}
-
-	/** Returns the duration of the animation in seconds. */
-	public float getDuration () {
-		return duration;
-	}
-
-	public void setDuration (float duration) {
-		this.duration = duration;
-	}
-
-	/** Poses the skeleton at the specified time for this animation mixed with the current or setup pose.
-	 * @param lastTime The last time the animation was applied.
-	 * @param events Any triggered events are added. May be null.
-	 * @param alpha The percentage between this animation's pose and the current pose.
-	 * @param setupPose If true, the animation is mixed with the setup pose, else it is mixed with the current pose. Passing true
-	 *           when alpha is 1 is slightly more efficient.
-	 * @param mixingOut True when mixing over time toward the setup or current pose, false when mixing toward the keyed pose.
-	 *           Irrelevant when alpha is 1. */
-	public void apply (Skeleton skeleton, float lastTime, float time, boolean loop, Array<Event> events, float alpha,
-		boolean setupPose, boolean mixingOut) {
-		if (skeleton == null) throw new IllegalArgumentException("skeleton cannot be null.");
-
-		if (loop && duration != 0) {
-			time %= duration;
-			if (lastTime > 0) lastTime %= duration;
-		}
-
-		Array<Timeline> timelines = this.timelines;
-		for (int i = 0, n = timelines.size; i < n; i++)
-			timelines.get(i).apply(skeleton, lastTime, time, events, alpha, setupPose, false);
-	}
-
-	public String getName () {
-		return name;
-	}
-
-	public String toString () {
-		return name;
-	}
-
-	/** @param target After the first and before the last value.
-	 * @return index of first value greater than the target. */
-	static int binarySearch (float[] values, float target, int step) {
-		int low = 0;
-		int high = values.length / step - 2;
-		if (high == 0) return step;
-		int current = high >>> 1;
-		while (true) {
-			if (values[(current + 1) * step] <= target)
-				low = current + 1;
-			else
-				high = current;
-			if (low == high) return (low + 1) * step;
-			current = (low + high) >>> 1;
-		}
-	}
-
-	/** @param target After the first and before the last value.
-	 * @return index of first value greater than the target. */
-	static int binarySearch (float[] values, float target) {
-		int low = 0;
-		int high = values.length - 2;
-		if (high == 0) return 1;
-		int current = high >>> 1;
-		while (true) {
-			if (values[current + 1] <= target)
-				low = current + 1;
-			else
-				high = current;
-			if (low == high) return low + 1;
-			current = (low + high) >>> 1;
-		}
-	}
-
-	static int linearSearch (float[] values, float target, int step) {
-		for (int i = 0, last = values.length - step; i <= last; i += step)
-			if (values[i] > target) return i;
-		return -1;
-	}
-
-	static public interface Timeline {
-		/** Sets the value(s) for the specified time.
-		 * @param events May be null to not collect fired events.
-		 * @param setupPose If true, the timeline is mixed with the setup pose, else it is mixed with the current pose. Passing true
-		 *           when alpha is 1 is slightly more efficient.
-		 * @param mixingOut True when mixing over time toward the setup or current pose, false when mixing toward the keyed pose.
-		 *           Irrelevant when alpha is 1. */
-		public void apply (Skeleton skeleton, float lastTime, float time, Array<Event> events, float alpha, boolean setupPose,
-			boolean mixingOut);
-
-		public int getPropertyId ();
-	}
-
-	static private enum TimelineType {
-		rotate, translate, scale, shear, //
-		attachment, color, deform, //
-		event, drawOrder, //
-		ikConstraint, transformConstraint, //
-		pathConstraintPosition, pathConstraintSpacing, pathConstraintMix
-	}
-
-	/** Base class for frames that use an interpolation bezier curve. */
-	abstract static public class CurveTimeline implements Timeline {
-		static public final float LINEAR = 0, STEPPED = 1, BEZIER = 2;
-		static private final int BEZIER_SIZE = 10 * 2 - 1;
-
-		private final float[] curves; // type, x, y, ...
-
-		public CurveTimeline (int frameCount) {
-			if (frameCount <= 0) throw new IllegalArgumentException("frameCount must be > 0: " + frameCount);
-			curves = new float[(frameCount - 1) * BEZIER_SIZE];
-		}
-
-		public int getFrameCount () {
-			return curves.length / BEZIER_SIZE + 1;
-		}
-
-		public void setLinear (int frameIndex) {
-			curves[frameIndex * BEZIER_SIZE] = LINEAR;
-		}
-
-		public void setStepped (int frameIndex) {
-			curves[frameIndex * BEZIER_SIZE] = STEPPED;
-		}
-
-		public float getCurveType (int frameIndex) {
-			int index = frameIndex * BEZIER_SIZE;
-			if (index == curves.length) return LINEAR;
-			float type = curves[index];
-			if (type == LINEAR) return LINEAR;
-			if (type == STEPPED) return STEPPED;
-			return BEZIER;
-		}
-
-		/** Sets the control handle positions for an interpolation bezier curve used to transition from this keyframe to the next.
-		 * cx1 and cx2 are from 0 to 1, representing the percent of time between the two keyframes. cy1 and cy2 are the percent of
-		 * the difference between the keyframe's values. */
-		public void setCurve (int frameIndex, float cx1, float cy1, float cx2, float cy2) {
-			float tmpx = (-cx1 * 2 + cx2) * 0.03f, tmpy = (-cy1 * 2 + cy2) * 0.03f;
-			float dddfx = ((cx1 - cx2) * 3 + 1) * 0.006f, dddfy = ((cy1 - cy2) * 3 + 1) * 0.006f;
-			float ddfx = tmpx * 2 + dddfx, ddfy = tmpy * 2 + dddfy;
-			float dfx = cx1 * 0.3f + tmpx + dddfx * 0.16666667f, dfy = cy1 * 0.3f + tmpy + dddfy * 0.16666667f;
-
-			int i = frameIndex * BEZIER_SIZE;
-			float[] curves = this.curves;
-			curves[i++] = BEZIER;
-
-			float x = dfx, y = dfy;
-			for (int n = i + BEZIER_SIZE - 1; i < n; i += 2) {
-				curves[i] = x;
-				curves[i + 1] = y;
-				dfx += ddfx;
-				dfy += ddfy;
-				ddfx += dddfx;
-				ddfy += dddfy;
-				x += dfx;
-				y += dfy;
-			}
-		}
-
-		public float getCurvePercent (int frameIndex, float percent) {
-			percent = MathUtils.clamp(percent, 0, 1);
-			float[] curves = this.curves;
-			int i = frameIndex * BEZIER_SIZE;
-			float type = curves[i];
-			if (type == LINEAR) return percent;
-			if (type == STEPPED) return 0;
-			i++;
-			float x = 0;
-			for (int start = i, n = i + BEZIER_SIZE - 1; i < n; i += 2) {
-				x = curves[i];
-				if (x >= percent) {
-					float prevX, prevY;
-					if (i == start) {
-						prevX = 0;
-						prevY = 0;
-					} else {
-						prevX = curves[i - 2];
-						prevY = curves[i - 1];
-					}
-					return prevY + (curves[i + 1] - prevY) * (percent - prevX) / (x - prevX);
-				}
-			}
-			float y = curves[i - 1];
-			return y + (1 - y) * (percent - x) / (1 - x); // Last point is 1,1.
-		}
-	}
-
-	static public class RotateTimeline extends CurveTimeline {
-		static public final int ENTRIES = 2;
-		static final int PREV_TIME = -2, PREV_ROTATION = -1;
-		static final int ROTATION = 1;
-
-		int boneIndex;
-		final float[] frames; // time, degrees, ...
-
-		public RotateTimeline (int frameCount) {
-			super(frameCount);
-			frames = new float[frameCount << 1];
-		}
-
-		public int getPropertyId () {
-			return (TimelineType.rotate.ordinal() << 24) + boneIndex;
-		}
-
-		public void setBoneIndex (int index) {
-			if (index < 0) throw new IllegalArgumentException("index must be >= 0.");
-			this.boneIndex = index;
-		}
-
-		public int getBoneIndex () {
-			return boneIndex;
-		}
-
-		public float[] getFrames () {
-			return frames;
-		}
-
-		/** Sets the time and angle of the specified keyframe. */
-		public void setFrame (int frameIndex, float time, float degrees) {
-			frameIndex <<= 1;
-			frames[frameIndex] = time;
-			frames[frameIndex + ROTATION] = degrees;
-		}
-
-		public void apply (Skeleton skeleton, float lastTime, float time, Array<Event> events, float alpha, boolean setupPose,
-			boolean mixingOut) {
-			float[] frames = this.frames;
-			if (time < frames[0]) return; // Time is before first frame.
-
-			Bone bone = skeleton.bones.get(boneIndex);
-
-			if (time >= frames[frames.length - ENTRIES]) { // Time is after last frame.
-				if (setupPose)
-					bone.rotation = bone.data.rotation + frames[frames.length + PREV_ROTATION] * alpha;
-				else {
-					float r = bone.data.rotation + frames[frames.length + PREV_ROTATION] - bone.rotation;
-					r -= (16384 - (int)(16384.499999999996 - r / 360)) * 360;
-					bone.rotation += r * alpha;
-				}
-				return;
-			}
-
-			// Interpolate between the previous frame and the current frame.
-			int frame = binarySearch(frames, time, ENTRIES);
-			float prevRotation = frames[frame + PREV_ROTATION];
-			float frameTime = frames[frame];
-			float percent = getCurvePercent((frame >> 1) - 1, 1 - (time - frameTime) / (frames[frame + PREV_TIME] - frameTime));
-
-			float r = frames[frame + ROTATION] - prevRotation;
-			r -= (16384 - (int)(16384.499999999996 - r / 360)) * 360;
-			r = prevRotation + r * percent;
-			if (setupPose) {
-				r -= (16384 - (int)(16384.499999999996 - r / 360)) * 360;
-				bone.rotation = bone.data.rotation + r * alpha;
-			} else {
-				r = bone.data.rotation + r - bone.rotation;
-				r -= (16384 - (int)(16384.499999999996 - r / 360)) * 360;
-				bone.rotation += r * alpha;
-			}
-		}
-	}
-
-	static public class TranslateTimeline extends CurveTimeline {
-		static public final int ENTRIES = 3;
-		static final int PREV_TIME = -3, PREV_X = -2, PREV_Y = -1;
-		static final int X = 1, Y = 2;
-
-		int boneIndex;
-		final float[] frames; // time, x, y, ...
-
-		public TranslateTimeline (int frameCount) {
-			super(frameCount);
-			frames = new float[frameCount * ENTRIES];
-		}
-
-		public int getPropertyId () {
-			return (TimelineType.translate.ordinal() << 24) + boneIndex;
-		}
-
-		public void setBoneIndex (int index) {
-			if (index < 0) throw new IllegalArgumentException("index must be >= 0.");
-			this.boneIndex = index;
-		}
-
-		public int getBoneIndex () {
-			return boneIndex;
-		}
-
-		public float[] getFrames () {
-			return frames;
-		}
-
-		/** Sets the time and value of the specified keyframe. */
-		public void setFrame (int frameIndex, float time, float x, float y) {
-			frameIndex *= ENTRIES;
-			frames[frameIndex] = time;
-			frames[frameIndex + X] = x;
-			frames[frameIndex + Y] = y;
-		}
-
-		public void apply (Skeleton skeleton, float lastTime, float time, Array<Event> events, float alpha, boolean setupPose,
-			boolean mixingOut) {
-			float[] frames = this.frames;
-			if (time < frames[0]) return; // Time is before first frame.
-
-			Bone bone = skeleton.bones.get(boneIndex);
-
-			if (time >= frames[frames.length - ENTRIES]) { // Time is after last frame.
-				if (setupPose) {
-					bone.x = bone.data.x + frames[frames.length + PREV_X] * alpha;
-					bone.y = bone.data.y + frames[frames.length + PREV_Y] * alpha;
-				} else {
-					bone.x += (bone.data.x + frames[frames.length + PREV_X] - bone.x) * alpha;
-					bone.y += (bone.data.y + frames[frames.length + PREV_Y] - bone.y) * alpha;
-				}
-			} else {
-				// Interpolate between the previous frame and the current frame.
-				int frame = binarySearch(frames, time, ENTRIES);
-				float prevX = frames[frame + PREV_X];
-				float prevY = frames[frame + PREV_Y];
-				float frameTime = frames[frame];
-				float percent = getCurvePercent(frame / ENTRIES - 1,
-					1 - (time - frameTime) / (frames[frame + PREV_TIME] - frameTime));
-
-				float x = prevX + (frames[frame + X] - prevX) * percent;
-				float y = prevY + (frames[frame + Y] - prevY) * percent;
-				if (setupPose) {
-					bone.x = bone.data.x + x * alpha;
-					bone.y = bone.data.y + y * alpha;
-				} else {
-					bone.x += (bone.data.x + x - bone.x) * alpha;
-					bone.y += (bone.data.y + y - bone.y) * alpha;
-				}
-			}
-		}
-	}
-
-	static public class ScaleTimeline extends TranslateTimeline {
-		public ScaleTimeline (int frameCount) {
-			super(frameCount);
-		}
-
-		public int getPropertyId () {
-			return (TimelineType.scale.ordinal() << 24) + boneIndex;
-		}
-
-		public void apply (Skeleton skeleton, float lastTime, float time, Array<Event> events, float alpha, boolean setupPose,
-			boolean mixingOut) {
-			float[] frames = this.frames;
-			if (time < frames[0]) return; // Time is before first frame.
-
-			Bone bone = skeleton.bones.get(boneIndex);
-
-			float x, y;
-			if (time >= frames[frames.length - ENTRIES]) { // Time is after last frame.
-				x = frames[frames.length + PREV_X] * bone.data.scaleX;
-				y = frames[frames.length + PREV_Y] * bone.data.scaleY;
-			} else {
-				// Interpolate between the previous frame and the current frame.
-				int frame = binarySearch(frames, time, ENTRIES);
-				float prevX = frames[frame + PREV_X];
-				float prevY = frames[frame + PREV_Y];
-				float frameTime = frames[frame];
-				float percent = getCurvePercent(frame / ENTRIES - 1,
-					1 - (time - frameTime) / (frames[frame + PREV_TIME] - frameTime));
-
-				x = (prevX + (frames[frame + X] - prevX) * percent) * bone.data.scaleX;
-				y = (prevY + (frames[frame + Y] - prevY) * percent) * bone.data.scaleY;
-			}
-			if (alpha == 1) {
-				bone.scaleX = x;
-				bone.scaleY = y;
-			} else {
-				float bx, by;
-				if (setupPose) {
-					bx = bone.data.scaleX;
-					by = bone.data.scaleY;
-				} else {
-					bx = bone.scaleX;
-					by = bone.scaleY;
-				}
-				// Mixing out uses sign of setup or current pose, else use sign of key.
-				if (mixingOut) {
-					x = Math.abs(x) * Math.signum(bx);
-					y = Math.abs(y) * Math.signum(by);
-				} else {
-					bx = Math.abs(bx) * Math.signum(x);
-					by = Math.abs(by) * Math.signum(y);
-				}
-				bone.scaleX = bx + (x - bx) * alpha;
-				bone.scaleY = by + (y - by) * alpha;
-			}
-		}
-	}
-
-	static public class ShearTimeline extends TranslateTimeline {
-		public ShearTimeline (int frameCount) {
-			super(frameCount);
-		}
-
-		public int getPropertyId () {
-			return (TimelineType.shear.ordinal() << 24) + boneIndex;
-		}
-
-		public void apply (Skeleton skeleton, float lastTime, float time, Array<Event> events, float alpha, boolean setupPose,
-			boolean mixingOut) {
-			float[] frames = this.frames;
-			if (time < frames[0]) return; // Time is before first frame.
-
-			Bone bone = skeleton.bones.get(boneIndex);
-			if (time >= frames[frames.length - ENTRIES]) { // Time is after last frame.
-				if (setupPose) {
-					bone.shearX = bone.data.shearX + frames[frames.length + PREV_X] * alpha;
-					bone.shearY = bone.data.shearY + frames[frames.length + PREV_Y] * alpha;
-				} else {
-					bone.shearX += (bone.data.shearX + frames[frames.length + PREV_X] - bone.shearX) * alpha;
-					bone.shearY += (bone.data.shearY + frames[frames.length + PREV_Y] - bone.shearY) * alpha;
-				}
-				return;
-			}
-
-			// Interpolate between the previous frame and the current frame.
-			int frame = binarySearch(frames, time, ENTRIES);
-			float prevX = frames[frame + PREV_X];
-			float prevY = frames[frame + PREV_Y];
-			float frameTime = frames[frame];
-			float percent = getCurvePercent(frame / ENTRIES - 1, 1 - (time - frameTime) / (frames[frame + PREV_TIME] - frameTime));
-
-			float x = prevX + (frames[frame + X] - prevX) * percent;
-			float y = prevY + (frames[frame + Y] - prevY) * percent;
-			if (setupPose) {
-				bone.shearX = bone.data.shearX + x * alpha;
-				bone.shearY = bone.data.shearY + y * alpha;
-			} else {
-				bone.shearX += (bone.data.shearX + x - bone.shearX) * alpha;
-				bone.shearY += (bone.data.shearY + y - bone.shearY) * alpha;
-			}
-		}
-	}
-
-	static public class ColorTimeline extends CurveTimeline {
-		static public final int ENTRIES = 5;
-		static private final int PREV_TIME = -5, PREV_R = -4, PREV_G = -3, PREV_B = -2, PREV_A = -1;
-		static private final int R = 1, G = 2, B = 3, A = 4;
-
-		int slotIndex;
-		private final float[] frames; // time, r, g, b, a, ...
-
-		public ColorTimeline (int frameCount) {
-			super(frameCount);
-			frames = new float[frameCount * ENTRIES];
-		}
-
-		public int getPropertyId () {
-			return (TimelineType.color.ordinal() << 24) + slotIndex;
-		}
-
-		public void setSlotIndex (int index) {
-			if (index < 0) throw new IllegalArgumentException("index must be >= 0.");
-			this.slotIndex = index;
-		}
-
-		public int getSlotIndex () {
-			return slotIndex;
-		}
-
-		public float[] getFrames () {
-			return frames;
-		}
-
-		/** Sets the time and value of the specified keyframe. */
-		public void setFrame (int frameIndex, float time, float r, float g, float b, float a) {
-			frameIndex *= ENTRIES;
-			frames[frameIndex] = time;
-			frames[frameIndex + R] = r;
-			frames[frameIndex + G] = g;
-			frames[frameIndex + B] = b;
-			frames[frameIndex + A] = a;
-		}
-
-		public void apply (Skeleton skeleton, float lastTime, float time, Array<Event> events, float alpha, boolean setupPose,
-			boolean mixingOut) {
-			float[] frames = this.frames;
-			if (time < frames[0]) return; // Time is before first frame.
-
-			float r, g, b, a;
-			if (time >= frames[frames.length - ENTRIES]) { // Time is after last frame.
-				int i = frames.length;
-				r = frames[i + PREV_R];
-				g = frames[i + PREV_G];
-				b = frames[i + PREV_B];
-				a = frames[i + PREV_A];
-			} else {
-				// Interpolate between the previous frame and the current frame.
-				int frame = binarySearch(frames, time, ENTRIES);
-				r = frames[frame + PREV_R];
-				g = frames[frame + PREV_G];
-				b = frames[frame + PREV_B];
-				a = frames[frame + PREV_A];
-				float frameTime = frames[frame];
-				float percent = getCurvePercent(frame / ENTRIES - 1,
-					1 - (time - frameTime) / (frames[frame + PREV_TIME] - frameTime));
-
-				r += (frames[frame + R] - r) * percent;
-				g += (frames[frame + G] - g) * percent;
-				b += (frames[frame + B] - b) * percent;
-				a += (frames[frame + A] - a) * percent;
-			}
-			Slot slot = skeleton.slots.get(slotIndex);
-			if (alpha == 1)
-				slot.color.set(r, g, b, a);
-			else {
-				Color color = slot.color;
-				if (setupPose) color.set(slot.data.color);
-				color.add((r - color.r) * alpha, (g - color.g) * alpha, (b - color.b) * alpha, (a - color.a) * alpha);
-			}
-		}
-	}
-
-	static public class AttachmentTimeline implements Timeline {
-		int slotIndex;
-		final float[] frames; // time, ...
-		final String[] attachmentNames;
-
-		public AttachmentTimeline (int frameCount) {
-			frames = new float[frameCount];
-			attachmentNames = new String[frameCount];
-		}
-
-		public int getPropertyId () {
-			return (TimelineType.attachment.ordinal() << 24) + slotIndex;
-		}
-
-		public int getFrameCount () {
-			return frames.length;
-		}
-
-		public void setSlotIndex (int index) {
-			if (index < 0) throw new IllegalArgumentException("index must be >= 0.");
-			this.slotIndex = index;
-		}
-
-		public int getSlotIndex () {
-			return slotIndex;
-		}
-
-		public float[] getFrames () {
-			return frames;
-		}
-
-		public String[] getAttachmentNames () {
-			return attachmentNames;
-		}
-
-		/** Sets the time and value of the specified keyframe. */
-		public void setFrame (int frameIndex, float time, String attachmentName) {
-			frames[frameIndex] = time;
-			attachmentNames[frameIndex] = attachmentName;
-		}
-
-		public void apply (Skeleton skeleton, float lastTime, float time, Array<Event> events, float alpha, boolean setupPose,
-			boolean mixingOut) {
-			if (mixingOut && setupPose) {
-				Slot slot = skeleton.slots.get(slotIndex);
-				String attachmentName = slot.data.attachmentName;
-				slot.setAttachment(attachmentName == null ? null : skeleton.getAttachment(slotIndex, attachmentName));
-				return;
-			}
-
-			float[] frames = this.frames;
-			if (time < frames[0]) return; // Time is before first frame.
-
-			int frameIndex;
-			if (time >= frames[frames.length - 1]) // Time is after last frame.
-				frameIndex = frames.length - 1;
-			else
-				frameIndex = binarySearch(frames, time, 1) - 1;
-
-			String attachmentName = attachmentNames[frameIndex];
-			skeleton.slots.get(slotIndex)
-				.setAttachment(attachmentName == null ? null : skeleton.getAttachment(slotIndex, attachmentName));
-		}
-	}
-
-	static public class DeformTimeline extends CurveTimeline {
-		int slotIndex;
-		VertexAttachment attachment;
-		private final float[] frames; // time, ...
-		private final float[][] frameVertices;
-
-		public DeformTimeline (int frameCount) {
-			super(frameCount);
-			frames = new float[frameCount];
-			frameVertices = new float[frameCount][];
-		}
-
-		public int getPropertyId () {
-			return (TimelineType.deform.ordinal() << 24) + slotIndex;
-		}
-
-		public void setSlotIndex (int index) {
-			if (index < 0) throw new IllegalArgumentException("index must be >= 0.");
-			this.slotIndex = index;
-		}
-
-		public int getSlotIndex () {
-			return slotIndex;
-		}
-
-		public void setAttachment (VertexAttachment attachment) {
-			this.attachment = attachment;
-		}
-
-		public VertexAttachment getAttachment () {
-			return attachment;
-		}
-
-		public float[] getFrames () {
-			return frames;
-		}
-
-		public float[][] getVertices () {
-			return frameVertices;
-		}
-
-		/** Sets the time of the specified keyframe. */
-		public void setFrame (int frameIndex, float time, float[] vertices) {
-			frames[frameIndex] = time;
-			frameVertices[frameIndex] = vertices;
-		}
-
-		public void apply (Skeleton skeleton, float lastTime, float time, Array<Event> firedEvents, float alpha, boolean setupPose,
-			boolean mixingOut) {
-			Slot slot = skeleton.slots.get(slotIndex);
-			Attachment slotAttachment = slot.attachment;
-			if (!(slotAttachment instanceof VertexAttachment) || !((VertexAttachment)slotAttachment).applyDeform(attachment)) return;
-
-			float[] frames = this.frames;
-			if (time < frames[0]) return; // Time is before first frame.
-
-			float[][] frameVertices = this.frameVertices;
-			int vertexCount = frameVertices[0].length;
-
-			FloatArray verticesArray = slot.getAttachmentVertices();
-			if (verticesArray.size != vertexCount) alpha = 1; // Don't mix from uninitialized slot vertices.
-			float[] vertices = verticesArray.setSize(vertexCount);
-
-			if (time >= frames[frames.length - 1]) { // Time is after last frame.
-				float[] lastVertices = frameVertices[frames.length - 1];
-				if (alpha < 1) {
-					for (int i = 0; i < vertexCount; i++)
-						vertices[i] += (lastVertices[i] - vertices[i]) * alpha;
-				} else
-					System.arraycopy(lastVertices, 0, vertices, 0, vertexCount);
-				return;
-			}
-
-			// Interpolate between the previous frame and the current frame.
-			int frame = binarySearch(frames, time);
-			float[] prevVertices = frameVertices[frame - 1];
-			float[] nextVertices = frameVertices[frame];
-			float frameTime = frames[frame];
-			float percent = getCurvePercent(frame - 1, 1 - (time - frameTime) / (frames[frame - 1] - frameTime));
-
-			// BOZO - Test.
-			if (alpha == 1) {
-				// Vertex positions or deform offsets, no alpha.
-				for (int i = 0; i < vertexCount; i++) {
-					float prev = prevVertices[i];
-					vertices[i] = prev + (nextVertices[i] - prev) * percent;
-				}
-			} else if (setupPose) {
-				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];
-						vertices[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];
-						vertices[i] = (prev + (nextVertices[i] - prev) * percent) * alpha;
-					}
-				}
-			} else {
-				// Vertex positions or deform offsets, with alpha.
-				for (int i = 0; i < vertexCount; i++) {
-					float prev = prevVertices[i];
-					vertices[i] += (prev + (nextVertices[i] - prev) * percent - vertices[i]) * alpha;
-				}
-			}
-		}
-	}
-
-	static public class EventTimeline implements Timeline {
-		private final float[] frames; // time, ...
-		private final Event[] events;
-
-		public EventTimeline (int frameCount) {
-			frames = new float[frameCount];
-			events = new Event[frameCount];
-		}
-
-		public int getPropertyId () {
-			return TimelineType.event.ordinal() << 24;
-		}
-
-		public int getFrameCount () {
-			return frames.length;
-		}
-
-		public float[] getFrames () {
-			return frames;
-		}
-
-		public Event[] getEvents () {
-			return events;
-		}
-
-		/** Sets the time of the specified keyframe. */
-		public void setFrame (int frameIndex, Event event) {
-			frames[frameIndex] = event.time;
-			events[frameIndex] = event;
-		}
-
-		/** Fires events for frames > lastTime and <= time. */
-		public void apply (Skeleton skeleton, float lastTime, float time, Array<Event> firedEvents, float alpha, boolean setupPose,
-			boolean mixingOut) {
-			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, setupPose, mixingOut);
-				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 frame;
-			if (lastTime < frames[0])
-				frame = 0;
-			else {
-				frame = binarySearch(frames, lastTime);
-				float frameTime = frames[frame];
-				while (frame > 0) { // Fire multiple events with the same frame.
-					if (frames[frame - 1] != frameTime) break;
-					frame--;
-				}
-			}
-			for (; frame < frameCount && time >= frames[frame]; frame++)
-				firedEvents.add(events[frame]);
-		}
-	}
-
-	static public class DrawOrderTimeline implements Timeline {
-		private final float[] frames; // time, ...
-		private final int[][] drawOrders;
-
-		public DrawOrderTimeline (int frameCount) {
-			frames = new float[frameCount];
-			drawOrders = new int[frameCount][];
-		}
-
-		public int getPropertyId () {
-			return TimelineType.drawOrder.ordinal() << 24;
-		}
-
-		public int getFrameCount () {
-			return frames.length;
-		}
-
-		public float[] getFrames () {
-			return frames;
-		}
-
-		public int[][] getDrawOrders () {
-			return drawOrders;
-		}
-
-		/** Sets the time of the specified keyframe.
-		 * @param drawOrder May be null to use bind pose draw order. */
-		public void setFrame (int frameIndex, float time, int[] drawOrder) {
-			frames[frameIndex] = time;
-			drawOrders[frameIndex] = drawOrder;
-		}
-
-		public void apply (Skeleton skeleton, float lastTime, float time, Array<Event> firedEvents, float alpha, boolean setupPose,
-			boolean mixingOut) {
-			if (mixingOut && setupPose) {
-				System.arraycopy(skeleton.slots.items, 0, skeleton.drawOrder.items, 0, skeleton.slots.size);
-				return;
-			}
-
-			float[] frames = this.frames;
-			if (time < frames[0]) return; // Time is before first frame.
-
-			int frame;
-			if (time >= frames[frames.length - 1]) // Time is after last frame.
-				frame = frames.length - 1;
-			else
-				frame = binarySearch(frames, time) - 1;
-
-			Array<Slot> drawOrder = skeleton.drawOrder;
-			Array<Slot> slots = skeleton.slots;
-			int[] drawOrderToSetupIndex = drawOrders[frame];
-			if (drawOrderToSetupIndex == null)
-				System.arraycopy(slots.items, 0, drawOrder.items, 0, slots.size);
-			else {
-				for (int i = 0, n = drawOrderToSetupIndex.length; i < n; i++)
-					drawOrder.set(i, slots.get(drawOrderToSetupIndex[i]));
-			}
-		}
-	}
-
-	static public class IkConstraintTimeline extends CurveTimeline {
-		static public final int ENTRIES = 3;
-		static private final int PREV_TIME = -3, PREV_MIX = -2, PREV_BEND_DIRECTION = -1;
-		static private final int MIX = 1, BEND_DIRECTION = 2;
-
-		int ikConstraintIndex;
-		private final float[] frames; // time, mix, bendDirection, ...
-
-		public IkConstraintTimeline (int frameCount) {
-			super(frameCount);
-			frames = new float[frameCount * ENTRIES];
-		}
-
-		public int getPropertyId () {
-			return (TimelineType.ikConstraint.ordinal() << 24) + ikConstraintIndex;
-		}
-
-		public void setIkConstraintIndex (int index) {
-			if (index < 0) throw new IllegalArgumentException("index must be >= 0.");
-			this.ikConstraintIndex = index;
-		}
-
-		public int getIkConstraintIndex () {
-			return ikConstraintIndex;
-		}
-
-		public float[] getFrames () {
-			return frames;
-		}
-
-		/** Sets the time, mix and bend direction of the specified keyframe. */
-		public void setFrame (int frameIndex, float time, float mix, int bendDirection) {
-			frameIndex *= ENTRIES;
-			frames[frameIndex] = time;
-			frames[frameIndex + MIX] = mix;
-			frames[frameIndex + BEND_DIRECTION] = bendDirection;
-		}
-
-		public void apply (Skeleton skeleton, float lastTime, float time, Array<Event> events, float alpha, boolean setupPose,
-			boolean mixingOut) {
-			float[] frames = this.frames;
-			if (time < frames[0]) return; // Time is before first frame.
-
-			// BOZO - Finish timelines handling setupPose and mixingOut from here down.
-			
-			IkConstraint constraint = skeleton.ikConstraints.get(ikConstraintIndex);
-
-			if (time >= frames[frames.length - ENTRIES]) { // Time is after last frame.
-				constraint.mix += (frames[frames.length + PREV_MIX] - constraint.mix) * alpha;
-				constraint.bendDirection = (int)frames[frames.length + PREV_BEND_DIRECTION];
-				return;
-			}
-
-			// Interpolate between the previous frame and the current frame.
-			int frame = binarySearch(frames, time, ENTRIES);
-			float mix = frames[frame + PREV_MIX];
-			float frameTime = frames[frame];
-			float percent = getCurvePercent(frame / ENTRIES - 1, 1 - (time - frameTime) / (frames[frame + PREV_TIME] - frameTime));
-
-			constraint.mix += (mix + (frames[frame + MIX] - mix) * percent - constraint.mix) * alpha;
-			constraint.bendDirection = (int)frames[frame + PREV_BEND_DIRECTION];
-		}
-	}
-
-	static public class TransformConstraintTimeline extends CurveTimeline {
-		static public final int ENTRIES = 5;
-		static private final int PREV_TIME = -5, PREV_ROTATE = -4, PREV_TRANSLATE = -3, PREV_SCALE = -2, PREV_SHEAR = -1;
-		static private final int ROTATE = 1, TRANSLATE = 2, SCALE = 3, SHEAR = 4;
-
-		int transformConstraintIndex;
-		private final float[] frames; // time, rotate mix, translate mix, scale mix, shear mix, ...
-
-		public TransformConstraintTimeline (int frameCount) {
-			super(frameCount);
-			frames = new float[frameCount * ENTRIES];
-		}
-
-		public int getPropertyId () {
-			return (TimelineType.transformConstraint.ordinal() << 24) + transformConstraintIndex;
-		}
-
-		public void setTransformConstraintIndex (int index) {
-			if (index < 0) throw new IllegalArgumentException("index must be >= 0.");
-			this.transformConstraintIndex = index;
-		}
-
-		public int getTransformConstraintIndex () {
-			return transformConstraintIndex;
-		}
-
-		public float[] getFrames () {
-			return frames;
-		}
-
-		/** Sets the time and mixes of the specified keyframe. */
-		public void setFrame (int frameIndex, float time, float rotateMix, float translateMix, float scaleMix, float shearMix) {
-			frameIndex *= ENTRIES;
-			frames[frameIndex] = time;
-			frames[frameIndex + ROTATE] = rotateMix;
-			frames[frameIndex + TRANSLATE] = translateMix;
-			frames[frameIndex + SCALE] = scaleMix;
-			frames[frameIndex + SHEAR] = shearMix;
-		}
-
-		public void apply (Skeleton skeleton, float lastTime, float time, Array<Event> events, float alpha, boolean setupPose,
-			boolean mixingOut) {
-			float[] frames = this.frames;
-			if (time < frames[0]) return; // Time is before first frame.
-
-			TransformConstraint constraint = skeleton.transformConstraints.get(transformConstraintIndex);
-
-			if (time >= frames[frames.length - ENTRIES]) { // Time is after last frame.
-				int i = frames.length;
-				constraint.rotateMix += (frames[i + PREV_ROTATE] - constraint.rotateMix) * alpha;
-				constraint.translateMix += (frames[i + PREV_TRANSLATE] - constraint.translateMix) * alpha;
-				constraint.scaleMix += (frames[i + PREV_SCALE] - constraint.scaleMix) * alpha;
-				constraint.shearMix += (frames[i + PREV_SHEAR] - constraint.shearMix) * alpha;
-				return;
-			}
-
-			// Interpolate between the previous frame and the current frame.
-			int frame = binarySearch(frames, time, ENTRIES);
-			float frameTime = frames[frame];
-			float percent = getCurvePercent(frame / ENTRIES - 1, 1 - (time - frameTime) / (frames[frame + PREV_TIME] - frameTime));
-
-			float rotate = frames[frame + PREV_ROTATE];
-			float translate = frames[frame + PREV_TRANSLATE];
-			float scale = frames[frame + PREV_SCALE];
-			float shear = frames[frame + PREV_SHEAR];
-			constraint.rotateMix += (rotate + (frames[frame + ROTATE] - rotate) * percent - constraint.rotateMix) * alpha;
-			constraint.translateMix += (translate + (frames[frame + TRANSLATE] - translate) * percent - constraint.translateMix)
-				* alpha;
-			constraint.scaleMix += (scale + (frames[frame + SCALE] - scale) * percent - constraint.scaleMix) * alpha;
-			constraint.shearMix += (shear + (frames[frame + SHEAR] - shear) * percent - constraint.shearMix) * alpha;
-		}
-	}
-
-	static public class PathConstraintPositionTimeline extends CurveTimeline {
-		static public final int ENTRIES = 2;
-		static final int PREV_TIME = -2, PREV_VALUE = -1;
-		static final int VALUE = 1;
-
-		int pathConstraintIndex;
-
-		final float[] frames; // time, position, ...
-
-		public PathConstraintPositionTimeline (int frameCount) {
-			super(frameCount);
-			frames = new float[frameCount * ENTRIES];
-		}
-
-		public int getPropertyId () {
-			return (TimelineType.pathConstraintPosition.ordinal() << 24) + pathConstraintIndex;
-		}
-
-		public void setPathConstraintIndex (int index) {
-			if (index < 0) throw new IllegalArgumentException("index must be >= 0.");
-			this.pathConstraintIndex = index;
-		}
-
-		public int getPathConstraintIndex () {
-			return pathConstraintIndex;
-		}
-
-		public float[] getFrames () {
-			return frames;
-		}
-
-		/** Sets the time and value of the specified keyframe. */
-		public void setFrame (int frameIndex, float time, float value) {
-			frameIndex *= ENTRIES;
-			frames[frameIndex] = time;
-			frames[frameIndex + VALUE] = value;
-		}
-
-		public void apply (Skeleton skeleton, float lastTime, float time, Array<Event> events, float alpha, boolean setupPose,
-			boolean mixingOut) {
-			float[] frames = this.frames;
-			if (time < frames[0]) return; // Time is before first frame.
-
-			PathConstraint constraint = skeleton.pathConstraints.get(pathConstraintIndex);
-
-			if (time >= frames[frames.length - ENTRIES]) { // Time is after last frame.
-				int i = frames.length;
-				constraint.position += (frames[i + PREV_VALUE] - constraint.position) * alpha;
-				return;
-			}
-
-			// Interpolate between the previous frame and the current frame.
-			int frame = binarySearch(frames, time, ENTRIES);
-			float position = frames[frame + PREV_VALUE];
-			float frameTime = frames[frame];
-			float percent = getCurvePercent(frame / ENTRIES - 1, 1 - (time - frameTime) / (frames[frame + PREV_TIME] - frameTime));
-
-			constraint.position += (position + (frames[frame + VALUE] - position) * percent - constraint.position) * alpha;
-		}
-	}
-
-	static public class PathConstraintSpacingTimeline extends PathConstraintPositionTimeline {
-		public PathConstraintSpacingTimeline (int frameCount) {
-			super(frameCount);
-		}
-
-		public int getPropertyId () {
-			return (TimelineType.pathConstraintSpacing.ordinal() << 24) + pathConstraintIndex;
-		}
-
-		public void apply (Skeleton skeleton, float lastTime, float time, Array<Event> events, float alpha, boolean setupPose,
-			boolean mixingOut) {
-			float[] frames = this.frames;
-			if (time < frames[0]) return; // Time is before first frame.
-
-			PathConstraint constraint = skeleton.pathConstraints.get(pathConstraintIndex);
-
-			if (time >= frames[frames.length - ENTRIES]) { // Time is after last frame.
-				int i = frames.length;
-				constraint.spacing += (frames[i + PREV_VALUE] - constraint.spacing) * alpha;
-				return;
-			}
-
-			// Interpolate between the previous frame and the current frame.
-			int frame = binarySearch(frames, time, ENTRIES);
-			float spacing = frames[frame + PREV_VALUE];
-			float frameTime = frames[frame];
-			float percent = getCurvePercent(frame / ENTRIES - 1, 1 - (time - frameTime) / (frames[frame + PREV_TIME] - frameTime));
-
-			constraint.spacing += (spacing + (frames[frame + VALUE] - spacing) * percent - constraint.spacing) * alpha;
-		}
-	}
-
-	static public class PathConstraintMixTimeline extends CurveTimeline {
-		static public final int ENTRIES = 3;
-		static private final int PREV_TIME = -3, PREV_ROTATE = -2, PREV_TRANSLATE = -1;
-		static private final int ROTATE = 1, TRANSLATE = 2;
-
-		int pathConstraintIndex;
-
-		private final float[] frames; // time, rotate mix, translate mix, ...
-
-		public PathConstraintMixTimeline (int frameCount) {
-			super(frameCount);
-			frames = new float[frameCount * ENTRIES];
-		}
-
-		public int getPropertyId () {
-			return (TimelineType.pathConstraintMix.ordinal() << 24) + pathConstraintIndex;
-		}
-
-		public void setPathConstraintIndex (int index) {
-			if (index < 0) throw new IllegalArgumentException("index must be >= 0.");
-			this.pathConstraintIndex = index;
-		}
-
-		public int getPathConstraintIndex () {
-			return pathConstraintIndex;
-		}
-
-		public float[] getFrames () {
-			return frames;
-		}
-
-		/** Sets the time and mixes of the specified keyframe. */
-		public void setFrame (int frameIndex, float time, float rotateMix, float translateMix) {
-			frameIndex *= ENTRIES;
-			frames[frameIndex] = time;
-			frames[frameIndex + ROTATE] = rotateMix;
-			frames[frameIndex + TRANSLATE] = translateMix;
-		}
-
-		public void apply (Skeleton skeleton, float lastTime, float time, Array<Event> events, float alpha, boolean setupPose,
-			boolean mixingOut) {
-			float[] frames = this.frames;
-			if (time < frames[0]) return; // Time is before first frame.
-
-			PathConstraint constraint = skeleton.pathConstraints.get(pathConstraintIndex);
-
-			if (time >= frames[frames.length - ENTRIES]) { // Time is after last frame.
-				int i = frames.length;
-				constraint.rotateMix += (frames[i + PREV_ROTATE] - constraint.rotateMix) * alpha;
-				constraint.translateMix += (frames[i + PREV_TRANSLATE] - constraint.translateMix) * alpha;
-				return;
-			}
-
-			// Interpolate between the previous frame and the current frame.
-			int frame = binarySearch(frames, time, ENTRIES);
-			float rotate = frames[frame + PREV_ROTATE];
-			float translate = frames[frame + PREV_TRANSLATE];
-			float frameTime = frames[frame];
-			float percent = getCurvePercent(frame / ENTRIES - 1, 1 - (time - frameTime) / (frames[frame + PREV_TIME] - frameTime));
-
-			constraint.rotateMix += (rotate + (frames[frame + ROTATE] - rotate) * percent - constraint.rotateMix) * alpha;
-			constraint.translateMix += (translate + (frames[frame + TRANSLATE] - translate) * percent - constraint.translateMix)
-				* alpha;
-		}
-	}
+package com.esotericsoftware.spine;
+
+import com.badlogic.gdx.graphics.Color;
+import com.badlogic.gdx.math.MathUtils;
+import com.badlogic.gdx.utils.Array;
+import com.badlogic.gdx.utils.FloatArray;
+import com.esotericsoftware.spine.attachments.Attachment;
+import com.esotericsoftware.spine.attachments.VertexAttachment;
+
+public class Animation {
+	final String name;
+	final Array<Timeline> timelines;
+	float duration;
+
+	public Animation (String name, Array<Timeline> timelines, float duration) {
+		if (name == null) throw new IllegalArgumentException("name cannot be null.");
+		if (timelines == null) throw new IllegalArgumentException("timelines cannot be null.");
+		this.name = name;
+		this.timelines = timelines;
+		this.duration = duration;
+	}
+
+	public Array<Timeline> getTimelines () {
+		return timelines;
+	}
+
+	/** Returns the duration of the animation in seconds. */
+	public float getDuration () {
+		return duration;
+	}
+
+	public void setDuration (float duration) {
+		this.duration = duration;
+	}
+
+	/** Poses the skeleton at the specified time for this animation mixed with the current or setup pose.
+	 * @param lastTime The last time the animation was applied.
+	 * @param events Any triggered events are added. May be null.
+	 * @param alpha The percentage between this animation's pose and the current pose.
+	 * @param setupPose If true, the animation is mixed with the setup pose, else it is mixed with the current pose. Passing true
+	 *           when alpha is 1 is slightly more efficient.
+	 * @param mixingOut True when mixing over time toward the setup or current pose, false when mixing toward the keyed pose.
+	 *           Irrelevant when alpha is 1. */
+	public void apply (Skeleton skeleton, float lastTime, float time, boolean loop, Array<Event> events, float alpha,
+		boolean setupPose, boolean mixingOut) {
+		if (skeleton == null) throw new IllegalArgumentException("skeleton cannot be null.");
+
+		if (loop && duration != 0) {
+			time %= duration;
+			if (lastTime > 0) lastTime %= duration;
+		}
+
+		Array<Timeline> timelines = this.timelines;
+		for (int i = 0, n = timelines.size; i < n; i++)
+			timelines.get(i).apply(skeleton, lastTime, time, events, alpha, setupPose, false);
+	}
+
+	public String getName () {
+		return name;
+	}
+
+	public String toString () {
+		return name;
+	}
+
+	/** @param target After the first and before the last value.
+	 * @return index of first value greater than the target. */
+	static int binarySearch (float[] values, float target, int step) {
+		int low = 0;
+		int high = values.length / step - 2;
+		if (high == 0) return step;
+		int current = high >>> 1;
+		while (true) {
+			if (values[(current + 1) * step] <= target)
+				low = current + 1;
+			else
+				high = current;
+			if (low == high) return (low + 1) * step;
+			current = (low + high) >>> 1;
+		}
+	}
+
+	/** @param target After the first and before the last value.
+	 * @return index of first value greater than the target. */
+	static int binarySearch (float[] values, float target) {
+		int low = 0;
+		int high = values.length - 2;
+		if (high == 0) return 1;
+		int current = high >>> 1;
+		while (true) {
+			if (values[current + 1] <= target)
+				low = current + 1;
+			else
+				high = current;
+			if (low == high) return low + 1;
+			current = (low + high) >>> 1;
+		}
+	}
+
+	static int linearSearch (float[] values, float target, int step) {
+		for (int i = 0, last = values.length - step; i <= last; i += step)
+			if (values[i] > target) return i;
+		return -1;
+	}
+
+	static public interface Timeline {
+		/** Sets the value(s) for the specified time.
+		 * @param events May be null to not collect fired events.
+		 * @param setupPose If true, the timeline is mixed with the setup pose, else it is mixed with the current pose. Passing true
+		 *           when alpha is 1 is slightly more efficient.
+		 * @param mixingOut True when mixing over time toward the setup or current pose, false when mixing toward the keyed pose.
+		 *           Irrelevant when alpha is 1. */
+		public void apply (Skeleton skeleton, float lastTime, float time, Array<Event> events, float alpha, boolean setupPose,
+			boolean mixingOut);
+
+		public int getPropertyId ();
+	}
+
+	static private enum TimelineType {
+		rotate, translate, scale, shear, //
+		attachment, color, deform, //
+		event, drawOrder, //
+		ikConstraint, transformConstraint, //
+		pathConstraintPosition, pathConstraintSpacing, pathConstraintMix
+	}
+
+	/** Base class for frames that use an interpolation bezier curve. */
+	abstract static public class CurveTimeline implements Timeline {
+		static public final float LINEAR = 0, STEPPED = 1, BEZIER = 2;
+		static private final int BEZIER_SIZE = 10 * 2 - 1;
+
+		private final float[] curves; // type, x, y, ...
+
+		public CurveTimeline (int frameCount) {
+			if (frameCount <= 0) throw new IllegalArgumentException("frameCount must be > 0: " + frameCount);
+			curves = new float[(frameCount - 1) * BEZIER_SIZE];
+		}
+
+		public int getFrameCount () {
+			return curves.length / BEZIER_SIZE + 1;
+		}
+
+		public void setLinear (int frameIndex) {
+			curves[frameIndex * BEZIER_SIZE] = LINEAR;
+		}
+
+		public void setStepped (int frameIndex) {
+			curves[frameIndex * BEZIER_SIZE] = STEPPED;
+		}
+
+		public float getCurveType (int frameIndex) {
+			int index = frameIndex * BEZIER_SIZE;
+			if (index == curves.length) return LINEAR;
+			float type = curves[index];
+			if (type == LINEAR) return LINEAR;
+			if (type == STEPPED) return STEPPED;
+			return BEZIER;
+		}
+
+		/** Sets the control handle positions for an interpolation bezier curve used to transition from this keyframe to the next.
+		 * cx1 and cx2 are from 0 to 1, representing the percent of time between the two keyframes. cy1 and cy2 are the percent of
+		 * the difference between the keyframe's values. */
+		public void setCurve (int frameIndex, float cx1, float cy1, float cx2, float cy2) {
+			float tmpx = (-cx1 * 2 + cx2) * 0.03f, tmpy = (-cy1 * 2 + cy2) * 0.03f;
+			float dddfx = ((cx1 - cx2) * 3 + 1) * 0.006f, dddfy = ((cy1 - cy2) * 3 + 1) * 0.006f;
+			float ddfx = tmpx * 2 + dddfx, ddfy = tmpy * 2 + dddfy;
+			float dfx = cx1 * 0.3f + tmpx + dddfx * 0.16666667f, dfy = cy1 * 0.3f + tmpy + dddfy * 0.16666667f;
+
+			int i = frameIndex * BEZIER_SIZE;
+			float[] curves = this.curves;
+			curves[i++] = BEZIER;
+
+			float x = dfx, y = dfy;
+			for (int n = i + BEZIER_SIZE - 1; i < n; i += 2) {
+				curves[i] = x;
+				curves[i + 1] = y;
+				dfx += ddfx;
+				dfy += ddfy;
+				ddfx += dddfx;
+				ddfy += dddfy;
+				x += dfx;
+				y += dfy;
+			}
+		}
+
+		public float getCurvePercent (int frameIndex, float percent) {
+			percent = MathUtils.clamp(percent, 0, 1);
+			float[] curves = this.curves;
+			int i = frameIndex * BEZIER_SIZE;
+			float type = curves[i];
+			if (type == LINEAR) return percent;
+			if (type == STEPPED) return 0;
+			i++;
+			float x = 0;
+			for (int start = i, n = i + BEZIER_SIZE - 1; i < n; i += 2) {
+				x = curves[i];
+				if (x >= percent) {
+					float prevX, prevY;
+					if (i == start) {
+						prevX = 0;
+						prevY = 0;
+					} else {
+						prevX = curves[i - 2];
+						prevY = curves[i - 1];
+					}
+					return prevY + (curves[i + 1] - prevY) * (percent - prevX) / (x - prevX);
+				}
+			}
+			float y = curves[i - 1];
+			return y + (1 - y) * (percent - x) / (1 - x); // Last point is 1,1.
+		}
+	}
+
+	static public class RotateTimeline extends CurveTimeline {
+		static public final int ENTRIES = 2;
+		static final int PREV_TIME = -2, PREV_ROTATION = -1;
+		static final int ROTATION = 1;
+
+		int boneIndex;
+		final float[] frames; // time, degrees, ...
+
+		public RotateTimeline (int frameCount) {
+			super(frameCount);
+			frames = new float[frameCount << 1];
+		}
+
+		public int getPropertyId () {
+			return (TimelineType.rotate.ordinal() << 24) + boneIndex;
+		}
+
+		public void setBoneIndex (int index) {
+			if (index < 0) throw new IllegalArgumentException("index must be >= 0.");
+			this.boneIndex = index;
+		}
+
+		public int getBoneIndex () {
+			return boneIndex;
+		}
+
+		public float[] getFrames () {
+			return frames;
+		}
+
+		/** Sets the time and angle of the specified keyframe. */
+		public void setFrame (int frameIndex, float time, float degrees) {
+			frameIndex <<= 1;
+			frames[frameIndex] = time;
+			frames[frameIndex + ROTATION] = degrees;
+		}
+
+		public void apply (Skeleton skeleton, float lastTime, float time, Array<Event> events, float alpha, boolean setupPose,
+			boolean mixingOut) {
+			float[] frames = this.frames;
+			if (time < frames[0]) return; // Time is before first frame.
+
+			Bone bone = skeleton.bones.get(boneIndex);
+
+			if (time >= frames[frames.length - ENTRIES]) { // Time is after last frame.
+				if (setupPose)
+					bone.rotation = bone.data.rotation + frames[frames.length + PREV_ROTATION] * alpha;
+				else {
+					float r = bone.data.rotation + frames[frames.length + PREV_ROTATION] - bone.rotation;
+					r -= (16384 - (int)(16384.499999999996 - r / 360)) * 360;
+					bone.rotation += r * alpha;
+				}
+				return;
+			}
+
+			// Interpolate between the previous frame and the current frame.
+			int frame = binarySearch(frames, time, ENTRIES);
+			float prevRotation = frames[frame + PREV_ROTATION];
+			float frameTime = frames[frame];
+			float percent = getCurvePercent((frame >> 1) - 1, 1 - (time - frameTime) / (frames[frame + PREV_TIME] - frameTime));
+
+			float r = frames[frame + ROTATION] - prevRotation;
+			r -= (16384 - (int)(16384.499999999996 - r / 360)) * 360;
+			r = prevRotation + r * percent;
+			if (setupPose) {
+				r -= (16384 - (int)(16384.499999999996 - r / 360)) * 360;
+				bone.rotation = bone.data.rotation + r * alpha;
+			} else {
+				r = bone.data.rotation + r - bone.rotation;
+				r -= (16384 - (int)(16384.499999999996 - r / 360)) * 360;
+				bone.rotation += r * alpha;
+			}
+		}
+	}
+
+	static public class TranslateTimeline extends CurveTimeline {
+		static public final int ENTRIES = 3;
+		static final int PREV_TIME = -3, PREV_X = -2, PREV_Y = -1;
+		static final int X = 1, Y = 2;
+
+		int boneIndex;
+		final float[] frames; // time, x, y, ...
+
+		public TranslateTimeline (int frameCount) {
+			super(frameCount);
+			frames = new float[frameCount * ENTRIES];
+		}
+
+		public int getPropertyId () {
+			return (TimelineType.translate.ordinal() << 24) + boneIndex;
+		}
+
+		public void setBoneIndex (int index) {
+			if (index < 0) throw new IllegalArgumentException("index must be >= 0.");
+			this.boneIndex = index;
+		}
+
+		public int getBoneIndex () {
+			return boneIndex;
+		}
+
+		public float[] getFrames () {
+			return frames;
+		}
+
+		/** Sets the time and value of the specified keyframe. */
+		public void setFrame (int frameIndex, float time, float x, float y) {
+			frameIndex *= ENTRIES;
+			frames[frameIndex] = time;
+			frames[frameIndex + X] = x;
+			frames[frameIndex + Y] = y;
+		}
+
+		public void apply (Skeleton skeleton, float lastTime, float time, Array<Event> events, float alpha, boolean setupPose,
+			boolean mixingOut) {
+			float[] frames = this.frames;
+			if (time < frames[0]) return; // Time is before first frame.
+
+			Bone bone = skeleton.bones.get(boneIndex);
+
+			if (time >= frames[frames.length - ENTRIES]) { // Time is after last frame.
+				if (setupPose) {
+					bone.x = bone.data.x + frames[frames.length + PREV_X] * alpha;
+					bone.y = bone.data.y + frames[frames.length + PREV_Y] * alpha;
+				} else {
+					bone.x += (bone.data.x + frames[frames.length + PREV_X] - bone.x) * alpha;
+					bone.y += (bone.data.y + frames[frames.length + PREV_Y] - bone.y) * alpha;
+				}
+			} else {
+				// Interpolate between the previous frame and the current frame.
+				int frame = binarySearch(frames, time, ENTRIES);
+				float prevX = frames[frame + PREV_X];
+				float prevY = frames[frame + PREV_Y];
+				float frameTime = frames[frame];
+				float percent = getCurvePercent(frame / ENTRIES - 1,
+					1 - (time - frameTime) / (frames[frame + PREV_TIME] - frameTime));
+
+				float x = prevX + (frames[frame + X] - prevX) * percent;
+				float y = prevY + (frames[frame + Y] - prevY) * percent;
+				if (setupPose) {
+					bone.x = bone.data.x + x * alpha;
+					bone.y = bone.data.y + y * alpha;
+				} else {
+					bone.x += (bone.data.x + x - bone.x) * alpha;
+					bone.y += (bone.data.y + y - bone.y) * alpha;
+				}
+			}
+		}
+	}
+
+	static public class ScaleTimeline extends TranslateTimeline {
+		public ScaleTimeline (int frameCount) {
+			super(frameCount);
+		}
+
+		public int getPropertyId () {
+			return (TimelineType.scale.ordinal() << 24) + boneIndex;
+		}
+
+		public void apply (Skeleton skeleton, float lastTime, float time, Array<Event> events, float alpha, boolean setupPose,
+			boolean mixingOut) {
+			float[] frames = this.frames;
+			if (time < frames[0]) return; // Time is before first frame.
+
+			Bone bone = skeleton.bones.get(boneIndex);
+
+			float x, y;
+			if (time >= frames[frames.length - ENTRIES]) { // Time is after last frame.
+				x = frames[frames.length + PREV_X] * bone.data.scaleX;
+				y = frames[frames.length + PREV_Y] * bone.data.scaleY;
+			} else {
+				// Interpolate between the previous frame and the current frame.
+				int frame = binarySearch(frames, time, ENTRIES);
+				float prevX = frames[frame + PREV_X];
+				float prevY = frames[frame + PREV_Y];
+				float frameTime = frames[frame];
+				float percent = getCurvePercent(frame / ENTRIES - 1,
+					1 - (time - frameTime) / (frames[frame + PREV_TIME] - frameTime));
+
+				x = (prevX + (frames[frame + X] - prevX) * percent) * bone.data.scaleX;
+				y = (prevY + (frames[frame + Y] - prevY) * percent) * bone.data.scaleY;
+			}
+			if (alpha == 1) {
+				bone.scaleX = x;
+				bone.scaleY = y;
+			} else {
+				float bx, by;
+				if (setupPose) {
+					bx = bone.data.scaleX;
+					by = bone.data.scaleY;
+				} else {
+					bx = bone.scaleX;
+					by = bone.scaleY;
+				}
+				// Mixing out uses sign of setup or current pose, else use sign of key.
+				if (mixingOut) {
+					x = Math.abs(x) * Math.signum(bx);
+					y = Math.abs(y) * Math.signum(by);
+				} else {
+					bx = Math.abs(bx) * Math.signum(x);
+					by = Math.abs(by) * Math.signum(y);
+				}
+				bone.scaleX = bx + (x - bx) * alpha;
+				bone.scaleY = by + (y - by) * alpha;
+			}
+		}
+	}
+
+	static public class ShearTimeline extends TranslateTimeline {
+		public ShearTimeline (int frameCount) {
+			super(frameCount);
+		}
+
+		public int getPropertyId () {
+			return (TimelineType.shear.ordinal() << 24) + boneIndex;
+		}
+
+		public void apply (Skeleton skeleton, float lastTime, float time, Array<Event> events, float alpha, boolean setupPose,
+			boolean mixingOut) {
+			float[] frames = this.frames;
+			if (time < frames[0]) return; // Time is before first frame.
+
+			Bone bone = skeleton.bones.get(boneIndex);
+			if (time >= frames[frames.length - ENTRIES]) { // Time is after last frame.
+				if (setupPose) {
+					bone.shearX = bone.data.shearX + frames[frames.length + PREV_X] * alpha;
+					bone.shearY = bone.data.shearY + frames[frames.length + PREV_Y] * alpha;
+				} else {
+					bone.shearX += (bone.data.shearX + frames[frames.length + PREV_X] - bone.shearX) * alpha;
+					bone.shearY += (bone.data.shearY + frames[frames.length + PREV_Y] - bone.shearY) * alpha;
+				}
+				return;
+			}
+
+			// Interpolate between the previous frame and the current frame.
+			int frame = binarySearch(frames, time, ENTRIES);
+			float prevX = frames[frame + PREV_X];
+			float prevY = frames[frame + PREV_Y];
+			float frameTime = frames[frame];
+			float percent = getCurvePercent(frame / ENTRIES - 1, 1 - (time - frameTime) / (frames[frame + PREV_TIME] - frameTime));
+
+			float x = prevX + (frames[frame + X] - prevX) * percent;
+			float y = prevY + (frames[frame + Y] - prevY) * percent;
+			if (setupPose) {
+				bone.shearX = bone.data.shearX + x * alpha;
+				bone.shearY = bone.data.shearY + y * alpha;
+			} else {
+				bone.shearX += (bone.data.shearX + x - bone.shearX) * alpha;
+				bone.shearY += (bone.data.shearY + y - bone.shearY) * alpha;
+			}
+		}
+	}
+
+	static public class ColorTimeline extends CurveTimeline {
+		static public final int ENTRIES = 5;
+		static private final int PREV_TIME = -5, PREV_R = -4, PREV_G = -3, PREV_B = -2, PREV_A = -1;
+		static private final int R = 1, G = 2, B = 3, A = 4;
+
+		int slotIndex;
+		private final float[] frames; // time, r, g, b, a, ...
+
+		public ColorTimeline (int frameCount) {
+			super(frameCount);
+			frames = new float[frameCount * ENTRIES];
+		}
+
+		public int getPropertyId () {
+			return (TimelineType.color.ordinal() << 24) + slotIndex;
+		}
+
+		public void setSlotIndex (int index) {
+			if (index < 0) throw new IllegalArgumentException("index must be >= 0.");
+			this.slotIndex = index;
+		}
+
+		public int getSlotIndex () {
+			return slotIndex;
+		}
+
+		public float[] getFrames () {
+			return frames;
+		}
+
+		/** Sets the time and value of the specified keyframe. */
+		public void setFrame (int frameIndex, float time, float r, float g, float b, float a) {
+			frameIndex *= ENTRIES;
+			frames[frameIndex] = time;
+			frames[frameIndex + R] = r;
+			frames[frameIndex + G] = g;
+			frames[frameIndex + B] = b;
+			frames[frameIndex + A] = a;
+		}
+
+		public void apply (Skeleton skeleton, float lastTime, float time, Array<Event> events, float alpha, boolean setupPose,
+			boolean mixingOut) {
+			float[] frames = this.frames;
+			if (time < frames[0]) return; // Time is before first frame.
+
+			float r, g, b, a;
+			if (time >= frames[frames.length - ENTRIES]) { // Time is after last frame.
+				int i = frames.length;
+				r = frames[i + PREV_R];
+				g = frames[i + PREV_G];
+				b = frames[i + PREV_B];
+				a = frames[i + PREV_A];
+			} else {
+				// Interpolate between the previous frame and the current frame.
+				int frame = binarySearch(frames, time, ENTRIES);
+				r = frames[frame + PREV_R];
+				g = frames[frame + PREV_G];
+				b = frames[frame + PREV_B];
+				a = frames[frame + PREV_A];
+				float frameTime = frames[frame];
+				float percent = getCurvePercent(frame / ENTRIES - 1,
+					1 - (time - frameTime) / (frames[frame + PREV_TIME] - frameTime));
+
+				r += (frames[frame + R] - r) * percent;
+				g += (frames[frame + G] - g) * percent;
+				b += (frames[frame + B] - b) * percent;
+				a += (frames[frame + A] - a) * percent;
+			}
+			Slot slot = skeleton.slots.get(slotIndex);
+			if (alpha == 1)
+				slot.color.set(r, g, b, a);
+			else {
+				Color color = slot.color;
+				if (setupPose) color.set(slot.data.color);
+				color.add((r - color.r) * alpha, (g - color.g) * alpha, (b - color.b) * alpha, (a - color.a) * alpha);
+			}
+		}
+	}
+
+	static public class AttachmentTimeline implements Timeline {
+		int slotIndex;
+		final float[] frames; // time, ...
+		final String[] attachmentNames;
+
+		public AttachmentTimeline (int frameCount) {
+			frames = new float[frameCount];
+			attachmentNames = new String[frameCount];
+		}
+
+		public int getPropertyId () {
+			return (TimelineType.attachment.ordinal() << 24) + slotIndex;
+		}
+
+		public int getFrameCount () {
+			return frames.length;
+		}
+
+		public void setSlotIndex (int index) {
+			if (index < 0) throw new IllegalArgumentException("index must be >= 0.");
+			this.slotIndex = index;
+		}
+
+		public int getSlotIndex () {
+			return slotIndex;
+		}
+
+		public float[] getFrames () {
+			return frames;
+		}
+
+		public String[] getAttachmentNames () {
+			return attachmentNames;
+		}
+
+		/** Sets the time and value of the specified keyframe. */
+		public void setFrame (int frameIndex, float time, String attachmentName) {
+			frames[frameIndex] = time;
+			attachmentNames[frameIndex] = attachmentName;
+		}
+
+		public void apply (Skeleton skeleton, float lastTime, float time, Array<Event> events, float alpha, boolean setupPose,
+			boolean mixingOut) {
+			if (mixingOut && setupPose) {
+				Slot slot = skeleton.slots.get(slotIndex);
+				String attachmentName = slot.data.attachmentName;
+				slot.setAttachment(attachmentName == null ? null : skeleton.getAttachment(slotIndex, attachmentName));
+				return;
+			}
+
+			float[] frames = this.frames;
+			if (time < frames[0]) return; // Time is before first frame.
+
+			int frameIndex;
+			if (time >= frames[frames.length - 1]) // Time is after last frame.
+				frameIndex = frames.length - 1;
+			else
+				frameIndex = binarySearch(frames, time, 1) - 1;
+
+			String attachmentName = attachmentNames[frameIndex];
+			skeleton.slots.get(slotIndex)
+				.setAttachment(attachmentName == null ? null : skeleton.getAttachment(slotIndex, attachmentName));
+		}
+	}
+
+	static public class DeformTimeline extends CurveTimeline {
+		int slotIndex;
+		VertexAttachment attachment;
+		private final float[] frames; // time, ...
+		private final float[][] frameVertices;
+
+		public DeformTimeline (int frameCount) {
+			super(frameCount);
+			frames = new float[frameCount];
+			frameVertices = new float[frameCount][];
+		}
+
+		public int getPropertyId () {
+			return (TimelineType.deform.ordinal() << 24) + slotIndex;
+		}
+
+		public void setSlotIndex (int index) {
+			if (index < 0) throw new IllegalArgumentException("index must be >= 0.");
+			this.slotIndex = index;
+		}
+
+		public int getSlotIndex () {
+			return slotIndex;
+		}
+
+		public void setAttachment (VertexAttachment attachment) {
+			this.attachment = attachment;
+		}
+
+		public VertexAttachment getAttachment () {
+			return attachment;
+		}
+
+		public float[] getFrames () {
+			return frames;
+		}
+
+		public float[][] getVertices () {
+			return frameVertices;
+		}
+
+		/** Sets the time of the specified keyframe. */
+		public void setFrame (int frameIndex, float time, float[] vertices) {
+			frames[frameIndex] = time;
+			frameVertices[frameIndex] = vertices;
+		}
+
+		public void apply (Skeleton skeleton, float lastTime, float time, Array<Event> firedEvents, float alpha, boolean setupPose,
+			boolean mixingOut) {
+			Slot slot = skeleton.slots.get(slotIndex);
+			Attachment slotAttachment = slot.attachment;
+			if (!(slotAttachment instanceof VertexAttachment) || !((VertexAttachment)slotAttachment).applyDeform(attachment)) return;
+
+			float[] frames = this.frames;
+			if (time < frames[0]) return; // Time is before first frame.
+
+			float[][] frameVertices = this.frameVertices;
+			int vertexCount = frameVertices[0].length;
+
+			FloatArray verticesArray = slot.getAttachmentVertices();
+			if (verticesArray.size != vertexCount) alpha = 1; // Don't mix from uninitialized slot vertices.
+			float[] vertices = verticesArray.setSize(vertexCount);
+
+			if (time >= frames[frames.length - 1]) { // Time is after last frame.
+				float[] lastVertices = frameVertices[frames.length - 1];
+				if (alpha < 1) {
+					for (int i = 0; i < vertexCount; i++)
+						vertices[i] += (lastVertices[i] - vertices[i]) * alpha;
+				} else
+					System.arraycopy(lastVertices, 0, vertices, 0, vertexCount);
+				return;
+			}
+
+			// Interpolate between the previous frame and the current frame.
+			int frame = binarySearch(frames, time);
+			float[] prevVertices = frameVertices[frame - 1];
+			float[] nextVertices = frameVertices[frame];
+			float frameTime = frames[frame];
+			float percent = getCurvePercent(frame - 1, 1 - (time - frameTime) / (frames[frame - 1] - frameTime));
+
+			// BOZO - Test.
+			if (alpha == 1) {
+				// Vertex positions or deform offsets, no alpha.
+				for (int i = 0; i < vertexCount; i++) {
+					float prev = prevVertices[i];
+					vertices[i] = prev + (nextVertices[i] - prev) * percent;
+				}
+			} else if (setupPose) {
+				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];
+						vertices[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];
+						vertices[i] = (prev + (nextVertices[i] - prev) * percent) * alpha;
+					}
+				}
+			} else {
+				// Vertex positions or deform offsets, with alpha.
+				for (int i = 0; i < vertexCount; i++) {
+					float prev = prevVertices[i];
+					vertices[i] += (prev + (nextVertices[i] - prev) * percent - vertices[i]) * alpha;
+				}
+			}
+		}
+	}
+
+	static public class EventTimeline implements Timeline {
+		private final float[] frames; // time, ...
+		private final Event[] events;
+
+		public EventTimeline (int frameCount) {
+			frames = new float[frameCount];
+			events = new Event[frameCount];
+		}
+
+		public int getPropertyId () {
+			return TimelineType.event.ordinal() << 24;
+		}
+
+		public int getFrameCount () {
+			return frames.length;
+		}
+
+		public float[] getFrames () {
+			return frames;
+		}
+
+		public Event[] getEvents () {
+			return events;
+		}
+
+		/** Sets the time of the specified keyframe. */
+		public void setFrame (int frameIndex, Event event) {
+			frames[frameIndex] = event.time;
+			events[frameIndex] = event;
+		}
+
+		/** Fires events for frames > lastTime and <= time. */
+		public void apply (Skeleton skeleton, float lastTime, float time, Array<Event> firedEvents, float alpha, boolean setupPose,
+			boolean mixingOut) {
+			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, setupPose, mixingOut);
+				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 frame;
+			if (lastTime < frames[0])
+				frame = 0;
+			else {
+				frame = binarySearch(frames, lastTime);
+				float frameTime = frames[frame];
+				while (frame > 0) { // Fire multiple events with the same frame.
+					if (frames[frame - 1] != frameTime) break;
+					frame--;
+				}
+			}
+			for (; frame < frameCount && time >= frames[frame]; frame++)
+				firedEvents.add(events[frame]);
+		}
+	}
+
+	static public class DrawOrderTimeline implements Timeline {
+		private final float[] frames; // time, ...
+		private final int[][] drawOrders;
+
+		public DrawOrderTimeline (int frameCount) {
+			frames = new float[frameCount];
+			drawOrders = new int[frameCount][];
+		}
+
+		public int getPropertyId () {
+			return TimelineType.drawOrder.ordinal() << 24;
+		}
+
+		public int getFrameCount () {
+			return frames.length;
+		}
+
+		public float[] getFrames () {
+			return frames;
+		}
+
+		public int[][] getDrawOrders () {
+			return drawOrders;
+		}
+
+		/** Sets the time of the specified keyframe.
+		 * @param drawOrder May be null to use bind pose draw order. */
+		public void setFrame (int frameIndex, float time, int[] drawOrder) {
+			frames[frameIndex] = time;
+			drawOrders[frameIndex] = drawOrder;
+		}
+
+		public void apply (Skeleton skeleton, float lastTime, float time, Array<Event> firedEvents, float alpha, boolean setupPose,
+			boolean mixingOut) {
+			if (mixingOut && setupPose) {
+				System.arraycopy(skeleton.slots.items, 0, skeleton.drawOrder.items, 0, skeleton.slots.size);
+				return;
+			}
+
+			float[] frames = this.frames;
+			if (time < frames[0]) return; // Time is before first frame.
+
+			int frame;
+			if (time >= frames[frames.length - 1]) // Time is after last frame.
+				frame = frames.length - 1;
+			else
+				frame = binarySearch(frames, time) - 1;
+
+			Array<Slot> drawOrder = skeleton.drawOrder;
+			Array<Slot> slots = skeleton.slots;
+			int[] drawOrderToSetupIndex = drawOrders[frame];
+			if (drawOrderToSetupIndex == null)
+				System.arraycopy(slots.items, 0, drawOrder.items, 0, slots.size);
+			else {
+				for (int i = 0, n = drawOrderToSetupIndex.length; i < n; i++)
+					drawOrder.set(i, slots.get(drawOrderToSetupIndex[i]));
+			}
+		}
+	}
+
+	static public class IkConstraintTimeline extends CurveTimeline {
+		static public final int ENTRIES = 3;
+		static private final int PREV_TIME = -3, PREV_MIX = -2, PREV_BEND_DIRECTION = -1;
+		static private final int MIX = 1, BEND_DIRECTION = 2;
+
+		int ikConstraintIndex;
+		private final float[] frames; // time, mix, bendDirection, ...
+
+		public IkConstraintTimeline (int frameCount) {
+			super(frameCount);
+			frames = new float[frameCount * ENTRIES];
+		}
+
+		public int getPropertyId () {
+			return (TimelineType.ikConstraint.ordinal() << 24) + ikConstraintIndex;
+		}
+
+		public void setIkConstraintIndex (int index) {
+			if (index < 0) throw new IllegalArgumentException("index must be >= 0.");
+			this.ikConstraintIndex = index;
+		}
+
+		public int getIkConstraintIndex () {
+			return ikConstraintIndex;
+		}
+
+		public float[] getFrames () {
+			return frames;
+		}
+
+		/** Sets the time, mix and bend direction of the specified keyframe. */
+		public void setFrame (int frameIndex, float time, float mix, int bendDirection) {
+			frameIndex *= ENTRIES;
+			frames[frameIndex] = time;
+			frames[frameIndex + MIX] = mix;
+			frames[frameIndex + BEND_DIRECTION] = bendDirection;
+		}
+
+		public void apply (Skeleton skeleton, float lastTime, float time, Array<Event> events, float alpha, boolean setupPose,
+			boolean mixingOut) {
+			float[] frames = this.frames;
+			if (time < frames[0]) return; // Time is before first frame.
+
+			// BOZO - Finish timelines handling setupPose and mixingOut from here down.
+			
+			IkConstraint constraint = skeleton.ikConstraints.get(ikConstraintIndex);
+
+			if (time >= frames[frames.length - ENTRIES]) { // Time is after last frame.
+				constraint.mix += (frames[frames.length + PREV_MIX] - constraint.mix) * alpha;
+				constraint.bendDirection = (int)frames[frames.length + PREV_BEND_DIRECTION];
+				return;
+			}
+
+			// Interpolate between the previous frame and the current frame.
+			int frame = binarySearch(frames, time, ENTRIES);
+			float mix = frames[frame + PREV_MIX];
+			float frameTime = frames[frame];
+			float percent = getCurvePercent(frame / ENTRIES - 1, 1 - (time - frameTime) / (frames[frame + PREV_TIME] - frameTime));
+
+			constraint.mix += (mix + (frames[frame + MIX] - mix) * percent - constraint.mix) * alpha;
+			constraint.bendDirection = (int)frames[frame + PREV_BEND_DIRECTION];
+		}
+	}
+
+	static public class TransformConstraintTimeline extends CurveTimeline {
+		static public final int ENTRIES = 5;
+		static private final int PREV_TIME = -5, PREV_ROTATE = -4, PREV_TRANSLATE = -3, PREV_SCALE = -2, PREV_SHEAR = -1;
+		static private final int ROTATE = 1, TRANSLATE = 2, SCALE = 3, SHEAR = 4;
+
+		int transformConstraintIndex;
+		private final float[] frames; // time, rotate mix, translate mix, scale mix, shear mix, ...
+
+		public TransformConstraintTimeline (int frameCount) {
+			super(frameCount);
+			frames = new float[frameCount * ENTRIES];
+		}
+
+		public int getPropertyId () {
+			return (TimelineType.transformConstraint.ordinal() << 24) + transformConstraintIndex;
+		}
+
+		public void setTransformConstraintIndex (int index) {
+			if (index < 0) throw new IllegalArgumentException("index must be >= 0.");
+			this.transformConstraintIndex = index;
+		}
+
+		public int getTransformConstraintIndex () {
+			return transformConstraintIndex;
+		}
+
+		public float[] getFrames () {
+			return frames;
+		}
+
+		/** Sets the time and mixes of the specified keyframe. */
+		public void setFrame (int frameIndex, float time, float rotateMix, float translateMix, float scaleMix, float shearMix) {
+			frameIndex *= ENTRIES;
+			frames[frameIndex] = time;
+			frames[frameIndex + ROTATE] = rotateMix;
+			frames[frameIndex + TRANSLATE] = translateMix;
+			frames[frameIndex + SCALE] = scaleMix;
+			frames[frameIndex + SHEAR] = shearMix;
+		}
+
+		public void apply (Skeleton skeleton, float lastTime, float time, Array<Event> events, float alpha, boolean setupPose,
+			boolean mixingOut) {
+			float[] frames = this.frames;
+			if (time < frames[0]) return; // Time is before first frame.
+
+			TransformConstraint constraint = skeleton.transformConstraints.get(transformConstraintIndex);
+
+			if (time >= frames[frames.length - ENTRIES]) { // Time is after last frame.
+				int i = frames.length;
+				constraint.rotateMix += (frames[i + PREV_ROTATE] - constraint.rotateMix) * alpha;
+				constraint.translateMix += (frames[i + PREV_TRANSLATE] - constraint.translateMix) * alpha;
+				constraint.scaleMix += (frames[i + PREV_SCALE] - constraint.scaleMix) * alpha;
+				constraint.shearMix += (frames[i + PREV_SHEAR] - constraint.shearMix) * alpha;
+				return;
+			}
+
+			// Interpolate between the previous frame and the current frame.
+			int frame = binarySearch(frames, time, ENTRIES);
+			float frameTime = frames[frame];
+			float percent = getCurvePercent(frame / ENTRIES - 1, 1 - (time - frameTime) / (frames[frame + PREV_TIME] - frameTime));
+
+			float rotate = frames[frame + PREV_ROTATE];
+			float translate = frames[frame + PREV_TRANSLATE];
+			float scale = frames[frame + PREV_SCALE];
+			float shear = frames[frame + PREV_SHEAR];
+			constraint.rotateMix += (rotate + (frames[frame + ROTATE] - rotate) * percent - constraint.rotateMix) * alpha;
+			constraint.translateMix += (translate + (frames[frame + TRANSLATE] - translate) * percent - constraint.translateMix)
+				* alpha;
+			constraint.scaleMix += (scale + (frames[frame + SCALE] - scale) * percent - constraint.scaleMix) * alpha;
+			constraint.shearMix += (shear + (frames[frame + SHEAR] - shear) * percent - constraint.shearMix) * alpha;
+		}
+	}
+
+	static public class PathConstraintPositionTimeline extends CurveTimeline {
+		static public final int ENTRIES = 2;
+		static final int PREV_TIME = -2, PREV_VALUE = -1;
+		static final int VALUE = 1;
+
+		int pathConstraintIndex;
+
+		final float[] frames; // time, position, ...
+
+		public PathConstraintPositionTimeline (int frameCount) {
+			super(frameCount);
+			frames = new float[frameCount * ENTRIES];
+		}
+
+		public int getPropertyId () {
+			return (TimelineType.pathConstraintPosition.ordinal() << 24) + pathConstraintIndex;
+		}
+
+		public void setPathConstraintIndex (int index) {
+			if (index < 0) throw new IllegalArgumentException("index must be >= 0.");
+			this.pathConstraintIndex = index;
+		}
+
+		public int getPathConstraintIndex () {
+			return pathConstraintIndex;
+		}
+
+		public float[] getFrames () {
+			return frames;
+		}
+
+		/** Sets the time and value of the specified keyframe. */
+		public void setFrame (int frameIndex, float time, float value) {
+			frameIndex *= ENTRIES;
+			frames[frameIndex] = time;
+			frames[frameIndex + VALUE] = value;
+		}
+
+		public void apply (Skeleton skeleton, float lastTime, float time, Array<Event> events, float alpha, boolean setupPose,
+			boolean mixingOut) {
+			float[] frames = this.frames;
+			if (time < frames[0]) return; // Time is before first frame.
+
+			PathConstraint constraint = skeleton.pathConstraints.get(pathConstraintIndex);
+
+			if (time >= frames[frames.length - ENTRIES]) { // Time is after last frame.
+				int i = frames.length;
+				constraint.position += (frames[i + PREV_VALUE] - constraint.position) * alpha;
+				return;
+			}
+
+			// Interpolate between the previous frame and the current frame.
+			int frame = binarySearch(frames, time, ENTRIES);
+			float position = frames[frame + PREV_VALUE];
+			float frameTime = frames[frame];
+			float percent = getCurvePercent(frame / ENTRIES - 1, 1 - (time - frameTime) / (frames[frame + PREV_TIME] - frameTime));
+
+			constraint.position += (position + (frames[frame + VALUE] - position) * percent - constraint.position) * alpha;
+		}
+	}
+
+	static public class PathConstraintSpacingTimeline extends PathConstraintPositionTimeline {
+		public PathConstraintSpacingTimeline (int frameCount) {
+			super(frameCount);
+		}
+
+		public int getPropertyId () {
+			return (TimelineType.pathConstraintSpacing.ordinal() << 24) + pathConstraintIndex;
+		}
+
+		public void apply (Skeleton skeleton, float lastTime, float time, Array<Event> events, float alpha, boolean setupPose,
+			boolean mixingOut) {
+			float[] frames = this.frames;
+			if (time < frames[0]) return; // Time is before first frame.
+
+			PathConstraint constraint = skeleton.pathConstraints.get(pathConstraintIndex);
+
+			if (time >= frames[frames.length - ENTRIES]) { // Time is after last frame.
+				int i = frames.length;
+				constraint.spacing += (frames[i + PREV_VALUE] - constraint.spacing) * alpha;
+				return;
+			}
+
+			// Interpolate between the previous frame and the current frame.
+			int frame = binarySearch(frames, time, ENTRIES);
+			float spacing = frames[frame + PREV_VALUE];
+			float frameTime = frames[frame];
+			float percent = getCurvePercent(frame / ENTRIES - 1, 1 - (time - frameTime) / (frames[frame + PREV_TIME] - frameTime));
+
+			constraint.spacing += (spacing + (frames[frame + VALUE] - spacing) * percent - constraint.spacing) * alpha;
+		}
+	}
+
+	static public class PathConstraintMixTimeline extends CurveTimeline {
+		static public final int ENTRIES = 3;
+		static private final int PREV_TIME = -3, PREV_ROTATE = -2, PREV_TRANSLATE = -1;
+		static private final int ROTATE = 1, TRANSLATE = 2;
+
+		int pathConstraintIndex;
+
+		private final float[] frames; // time, rotate mix, translate mix, ...
+
+		public PathConstraintMixTimeline (int frameCount) {
+			super(frameCount);
+			frames = new float[frameCount * ENTRIES];
+		}
+
+		public int getPropertyId () {
+			return (TimelineType.pathConstraintMix.ordinal() << 24) + pathConstraintIndex;
+		}
+
+		public void setPathConstraintIndex (int index) {
+			if (index < 0) throw new IllegalArgumentException("index must be >= 0.");
+			this.pathConstraintIndex = index;
+		}
+
+		public int getPathConstraintIndex () {
+			return pathConstraintIndex;
+		}
+
+		public float[] getFrames () {
+			return frames;
+		}
+
+		/** Sets the time and mixes of the specified keyframe. */
+		public void setFrame (int frameIndex, float time, float rotateMix, float translateMix) {
+			frameIndex *= ENTRIES;
+			frames[frameIndex] = time;
+			frames[frameIndex + ROTATE] = rotateMix;
+			frames[frameIndex + TRANSLATE] = translateMix;
+		}
+
+		public void apply (Skeleton skeleton, float lastTime, float time, Array<Event> events, float alpha, boolean setupPose,
+			boolean mixingOut) {
+			float[] frames = this.frames;
+			if (time < frames[0]) return; // Time is before first frame.
+
+			PathConstraint constraint = skeleton.pathConstraints.get(pathConstraintIndex);
+
+			if (time >= frames[frames.length - ENTRIES]) { // Time is after last frame.
+				int i = frames.length;
+				constraint.rotateMix += (frames[i + PREV_ROTATE] - constraint.rotateMix) * alpha;
+				constraint.translateMix += (frames[i + PREV_TRANSLATE] - constraint.translateMix) * alpha;
+				return;
+			}
+
+			// Interpolate between the previous frame and the current frame.
+			int frame = binarySearch(frames, time, ENTRIES);
+			float rotate = frames[frame + PREV_ROTATE];
+			float translate = frames[frame + PREV_TRANSLATE];
+			float frameTime = frames[frame];
+			float percent = getCurvePercent(frame / ENTRIES - 1, 1 - (time - frameTime) / (frames[frame + PREV_TIME] - frameTime));
+
+			constraint.rotateMix += (rotate + (frames[frame + ROTATE] - rotate) * percent - constraint.rotateMix) * alpha;
+			constraint.translateMix += (translate + (frames[frame + TRANSLATE] - translate) * percent - constraint.translateMix)
+				* alpha;
+		}
+	}
 }

+ 1059 - 1060
spine-libgdx/spine-libgdx/src/com/esotericsoftware/spine/AnimationState.java

@@ -1,1063 +1,1062 @@
-/******************************************************************************
- * Spine Runtimes Software License
- * Version 2.3
- * 
- * Copyright (c) 2013-2015, Esoteric Software
- * All rights reserved.
- * 
- * You are granted a perpetual, non-exclusive, non-sublicensable and
- * non-transferable license to use, install, execute and perform the Spine
- * Runtimes Software (the "Software") and derivative works solely for personal
- * or internal use. Without the written permission of Esoteric Software (see
- * Section 2 of the Spine Software License Agreement), you may not (a) modify,
- * translate, adapt or otherwise create derivative works, improvements of the
- * Software or develop new applications using the Software or (b) remove,
- * delete, alter or obscure any trademarks or any copyright, trademark, patent
- * or other intellectual property or proprietary rights notices on or in the
- * Software, including any copy thereof. Redistributions in binary or source
- * form must include this license and terms.
- * 
- * THIS SOFTWARE IS PROVIDED BY ESOTERIC SOFTWARE "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 BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
- * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
- * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS;
- * OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
- * WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR
- * OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
- * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+/******************************************************************************
+ * Spine Runtimes Software License v2.5
+ * 
+ * Copyright (c) 2013-2016, Esoteric Software
+ * All rights reserved.
+ * 
+ * You are granted a perpetual, non-exclusive, non-sublicensable, and
+ * non-transferable license to use, install, execute, and perform the Spine
+ * Runtimes software and derivative works solely for personal or internal
+ * use. Without the written permission of Esoteric Software (see Section 2 of
+ * the Spine Software License Agreement), you may not (a) modify, translate,
+ * adapt, or develop new applications using the Spine Runtimes or otherwise
+ * create derivative works or improvements of the Spine Runtimes or (b) remove,
+ * delete, alter, or obscure any trademarks or any copyright, trademark, patent,
+ * or other intellectual property or proprietary rights notices on or in the
+ * Software, including any copy thereof. Redistributions in binary or source
+ * form must include this license and terms.
+ * 
+ * THIS SOFTWARE IS PROVIDED BY ESOTERIC SOFTWARE "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 BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
+ * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES, BUSINESS INTERRUPTION, OR LOSS OF
+ * USE, DATA, OR PROFITS) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER
+ * IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+ * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
+ * POSSIBILITY OF SUCH DAMAGE.
  *****************************************************************************/
 
-package com.esotericsoftware.spine;
-
-import static com.esotericsoftware.spine.Animation.RotateTimeline.*;
-
-import com.badlogic.gdx.utils.Array;
-import com.badlogic.gdx.utils.BooleanArray;
-import com.badlogic.gdx.utils.FloatArray;
-import com.badlogic.gdx.utils.IntSet;
-import com.badlogic.gdx.utils.Pool;
-import com.badlogic.gdx.utils.Pool.Poolable;
-import com.esotericsoftware.spine.Animation.AttachmentTimeline;
-import com.esotericsoftware.spine.Animation.DrawOrderTimeline;
-import com.esotericsoftware.spine.Animation.RotateTimeline;
-import com.esotericsoftware.spine.Animation.Timeline;
-
-/** Stores state for applying one or more animations over time and mixing (crossfading) between animations.
- * <p>
- * Animations on different tracks are applied sequentially each frame, from lowest to highest track index. This enables animations
- * to be layered, where higher tracks either key only a subset of the skeleton pose or use alpha < 1 to mix with the pose on the
- * lower track. */
-public class AnimationState {
-	static private final Animation emptyAnimation = new Animation("<empty>", new Array(0), 0);
-
-	private AnimationStateData data;
-	private final Array<TrackEntry> tracks = new Array();
-	private final Array<Event> events = new Array();
-	final Array<AnimationStateListener> listeners = new Array();
-	private final EventQueue queue = new EventQueue();
-	private final IntSet propertyIDs = new IntSet();
-	boolean animationsChanged;
-	private float timeScale = 1;
-
-	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 the track entry times, setting queued animations as current if needed. */
-	public void update (float delta) {
-		delta *= timeScale;
-		for (int i = 0, n = tracks.size; i < n; i++) {
-			TrackEntry current = tracks.get(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 = nextTime + delta * next.timeScale;
-					current.trackTime += currentDelta;
-					setCurrent(i, next);
-					while (next.mixingFrom != null) {
-						next.mixTime += currentDelta;
-						next = next.mixingFrom;
-					}
-					continue;
-				}
-				updateMixingFrom(current, delta);
-			} else {
-				updateMixingFrom(current, delta);
-				// Clear the track when there is no next entry, the track end time is reached, and there is no mixingFrom.
-				if (current.trackLast >= current.trackEnd && current.mixingFrom == null) {
-					tracks.set(i, null);
-					queue.end(current);
-					disposeNext(current);
-					continue;
-				}
-			}
-
-			current.trackTime += currentDelta;
-		}
-
-		queue.drain();
-	}
-
-	private void updateMixingFrom (TrackEntry entry, float delta) {
-		TrackEntry from = entry.mixingFrom;
-		if (from == null) return;
-
-		if (entry.mixTime >= entry.mixDuration && entry.mixTime > 0) {
-			queue.end(from);
-			TrackEntry newFrom = from.mixingFrom;
-			entry.mixingFrom = newFrom;
-			if (newFrom == null) return;
-			entry.mixTime = from.mixTime;
-			entry.mixDuration = from.mixDuration;
-			from = newFrom;
-		}
-
-		from.animationLast = from.nextAnimationLast;
-		from.trackLast = from.nextTrackLast;
-		float mixingFromDelta = delta * from.timeScale;
-		from.trackTime += mixingFromDelta;
-		entry.mixTime += mixingFromDelta;
-
-		updateMixingFrom(from, delta);
-	}
-
-	/** Poses the skeleton using the track entry animations. There are no side effects other than invoking listeners, so the
-	 * animation state can be applied to multiple skeletons to pose them identically. */
-	public void apply (Skeleton skeleton) {
-		if (skeleton == null) throw new IllegalArgumentException("skeleton cannot be null.");
-		if (animationsChanged) animationsChanged();
-
-		Array<Event> events = this.events;
-
-		for (int i = 0; i < tracks.size; i++) {
-			TrackEntry current = tracks.get(i);
-			if (current == null || current.delay > 0) continue;
-
-			// Apply mixing from entries first.
-			float mix = current.alpha;
-			if (current.mixingFrom != null) mix = applyMixingFrom(current, skeleton, mix);
-
-			// Apply current entry.
-			float animationLast = current.animationLast, animationTime = current.getAnimationTime();
-			Array<Timeline> timelines = current.animation.timelines;
-			if (mix == 1) {
-				for (int ii = 0, n = timelines.size; ii < n; ii++)
-					timelines.get(ii).apply(skeleton, animationLast, animationTime, events, 1, false, false);
-			} else {
-				boolean firstFrame = current.timelinesRotation.size == 0;
-				if (firstFrame) current.timelinesRotation.setSize(timelines.size << 1);
-				float[] timelinesRotation = current.timelinesRotation.items;
-				boolean[] timelinesFirst = current.timelinesFirst.items;
-				for (int ii = 0, n = timelines.size; ii < n; ii++) {
-					Timeline timeline = timelines.get(ii);
-					if (timeline instanceof RotateTimeline) {
-						applyRotateTimeline((RotateTimeline)timeline, skeleton, animationLast, animationTime, events, mix,
-							timelinesFirst[ii], false, timelinesRotation, ii << 1, firstFrame);
-					} else {
-						timeline.apply(skeleton, animationLast, animationTime, events, mix, timelinesFirst[ii], false);
-					}
-				}
-			}
-			queueEvents(current, animationTime);
-			current.nextAnimationLast = animationTime;
-			current.nextTrackLast = current.trackTime;
-		}
-
-		queue.drain();
-
-		System.out.println();
-	}
-
-	private float applyMixingFrom (TrackEntry entry, Skeleton skeleton, float alpha) {
-		float mix;
-		if (entry.mixDuration == 0) // Single frame mix to undo mixingFrom changes.
-			mix = 1;
-		else {
-			mix = alpha * entry.mixTime / entry.mixDuration;
-			if (mix > 1) mix = 1;
-		}
-
-		TrackEntry from = entry.mixingFrom;
-		if (from.mixingFrom != null) applyMixingFrom(from, skeleton, alpha);
-
-		Array<Event> events = mix < from.eventThreshold ? this.events : null;
-		boolean attachments = mix < from.attachmentThreshold, drawOrder = mix < from.drawOrderThreshold;
-
-		float animationLast = from.animationLast, animationTime = from.getAnimationTime();
-		Array<Timeline> timelines = from.animation.timelines;
-		int timelineCount = timelines.size;
-		boolean[] timelinesFirst = from.timelinesFirst.items, timelinesLast = from.timelinesLast.items;
-		float alphaFull = from.alpha, alphaMix = alphaFull * (1 - mix);
-
-		boolean firstFrame = from.timelinesRotation.size == 0;
-		if (firstFrame) from.timelinesRotation.setSize(timelineCount << 1);
-		float[] timelinesRotation = from.timelinesRotation.items;
-
-		System.out.println(entry.mixingFrom + " -> " + entry + ": " + entry.mixTime / entry.mixDuration);
-
-		for (int i = 0; i < timelineCount; i++) {
-			Timeline timeline = timelines.get(i);
-			boolean setupPose = timelinesFirst[i];
-			float a = timelinesLast[i] ? alphaMix : alphaFull;
-			if (timeline instanceof RotateTimeline) {
-				applyRotateTimeline((RotateTimeline)timeline, skeleton, animationLast, animationTime, events, a, setupPose, setupPose,
-					timelinesRotation, i << 1, firstFrame);
-			} else {
-				if (!setupPose) {
-					if (!attachments && timeline instanceof AttachmentTimeline) continue;
-					if (!drawOrder && timeline instanceof DrawOrderTimeline) continue;
-				}
-				timeline.apply(skeleton, animationLast, animationTime, events, a, setupPose, setupPose);
-			}
-		}
-
-		queueEvents(from, animationTime);
-		from.nextAnimationLast = animationTime;
-		from.nextTrackLast = from.trackTime;
-
-		return mix;
-	}
-
-	/** @param events May be null. */
-	private void applyRotateTimeline (RotateTimeline timeline, Skeleton skeleton, float lastTime, float time, Array<Event> events,
-		float alpha, boolean setupPose, boolean mixingOut, float[] timelinesRotation, int i, boolean firstFrame) {
-		if (alpha == 1) {
-			timeline.apply(skeleton, lastTime, time, events, 1, setupPose, setupPose);
-			return;
-		}
-
-		float[] frames = timeline.frames;
-		if (time < frames[0]) return; // Time is before first frame.
-
-		Bone bone = skeleton.bones.get(timeline.boneIndex);
-
-		float r2;
-		if (time >= frames[frames.length - ENTRIES]) // Time is after last frame.
-			r2 = bone.data.rotation + frames[frames.length + PREV_ROTATION];
-		else {
-			// Interpolate between the previous frame and the current frame.
-			int frame = Animation.binarySearch(frames, time, ENTRIES);
-			float prevRotation = frames[frame + PREV_ROTATION];
-			float frameTime = frames[frame];
-			float percent = timeline.getCurvePercent((frame >> 1) - 1,
-				1 - (time - frameTime) / (frames[frame + PREV_TIME] - frameTime));
-
-			r2 = frames[frame + ROTATION] - prevRotation;
-			r2 -= (16384 - (int)(16384.499999999996 - r2 / 360)) * 360;
-			r2 = prevRotation + r2 * percent + bone.data.rotation;
-			r2 -= (16384 - (int)(16384.499999999996 - r2 / 360)) * 360;
-		}
-
-		// Mix between two rotations using the direction of the shortest route on the first frame while detecting crosses.
-		float r1 = setupPose ? bone.data.rotation : bone.rotation;
-		float total, diff = r2 - r1;
-		if (diff == 0) {
-			if (firstFrame) {
-				timelinesRotation[i] = 0;
-				total = 0;
-			} else
-				total = timelinesRotation[i];
-		} else {
-			diff -= (16384 - (int)(16384.499999999996 - diff / 360)) * 360;
-			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; // Keep loops part of lastTotal.
-			if (dir != current) total += 360 * Math.signum(lastTotal);
-			timelinesRotation[i] = total;
-		}
-		timelinesRotation[i + 1] = diff;
-		r1 += total * alpha;
-		bone.rotation = r1 - (16384 - (int)(16384.499999999996 - r1 / 360)) * 360;
-	}
-
-	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.
-		Array<Event> events = this.events;
-		int i = 0, n = events.size;
-		for (; i < n; i++) {
-			Event event = events.get(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.
-		if (entry.loop ? (trackLastWrapped > entry.trackTime % duration)
-			: (animationTime >= animationEnd && entry.animationLast < animationEnd)) {
-			queue.complete(entry);
-		}
-
-		// Queue events after complete.
-		for (; i < n; i++) {
-			Event event = events.get(i);
-			if (event.time < animationStart) continue; // Discard events outside animation start/end.
-			queue.event(entry, events.get(i));
-		}
-		events.clear();
-	}
-
-	/** Removes all animations from all tracks, leaving skeletons in their last 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 last pose. */
-	public void clearTracks () {
-		queue.drainDisabled = true;
-		for (int i = 0, n = tracks.size; i < n; i++)
-			clearTrack(i);
-		tracks.clear();
-		queue.drainDisabled = false;
-		queue.drain();
-	}
-
-	/** Removes all animations from the track, leaving skeletons in their last 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 last pose. */
-	public void clearTrack (int trackIndex) {
-		if (trackIndex >= tracks.size) return;
-		TrackEntry current = tracks.get(trackIndex);
-		if (current == null) return;
-
-		queue.end(current);
-
-		disposeNext(current);
-
-		TrackEntry entry = current;
-		while (true) {
-			TrackEntry from = entry.mixingFrom;
-			if (from == null) break;
-			queue.end(from);
-			entry.mixingFrom = null;
-			entry = from;
-		}
-
-		tracks.set(current.trackIndex, null);
-
-		queue.drain();
-	}
-
-	private void setCurrent (int index, TrackEntry entry) {
-		TrackEntry current = expandToIndex(index);
-		tracks.set(index, entry);
-
-		if (current != null) {
-			queue.interrupt(current);
-			entry.mixingFrom = current;
-			entry.mixTime = Math.max(0, entry.mixDuration - current.trackTime);
-			current.timelinesRotation.clear(); // BOZO - Needed? Recursive?
-		}
-
-		queue.start(entry);
-	}
-
-	/** @see #setAnimation(int, Animation, boolean) */
-	public TrackEntry setAnimation (int trackIndex, String animationName, boolean loop) {
-		Animation animation = data.getSkeletonData().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.
-	 * @return A track entry to allow further customization of animation playback. References to the track entry must not be kept
-	 *         after {@link AnimationStateListener#dispose(TrackEntry)}. */
-	public TrackEntry setAnimation (int trackIndex, Animation animation, boolean loop) {
-		if (animation == null) throw new IllegalArgumentException("animation cannot be null.");
-		TrackEntry current = expandToIndex(trackIndex);
-		if (current != null) {
-			if (current.nextTrackLast == -1) {
-				// Don't mix from an entry that was never applied.
-				tracks.set(trackIndex, null);
-				queue.interrupt(current);
-				queue.end(current);
-				disposeNext(current);
-				current = null;
-			} else
-				disposeNext(current);
-		}
-		TrackEntry entry = trackEntry(trackIndex, animation, loop, current);
-		setCurrent(trackIndex, entry);
-		queue.drain();
-		return entry;
-	}
-
-	/** {@link #addAnimation(int, Animation, boolean, float)} */
-	public TrackEntry addAnimation (int trackIndex, String animationName, boolean loop, float delay) {
-		Animation animation = data.getSkeletonData().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.
-	 * @param delay Seconds to begin this animation after the start of the previous animation. May be <= 0 to use the animation
-	 *           duration of the previous track minus any mix duration plus the negative delay.
-	 * @return A track entry to allow further customization of animation playback. References to the track entry must not be kept
-	 *         after {@link AnimationStateListener#dispose(TrackEntry)}. */
-	public TrackEntry addAnimation (int trackIndex, Animation animation, boolean loop, float delay) {
-		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);
-			queue.drain();
-		} else {
-			last.next = entry;
-			if (delay <= 0) {
-				float duration = last.animationEnd - last.animationStart;
-				if (duration != 0)
-					delay += duration * (1 + (int)(last.trackTime / duration)) - data.getMix(last.animation, animation);
-				else
-					delay = 0;
-			}
-		}
-
-		entry.delay = delay;
-		return entry;
-	}
-
-	/** Sets an empty animation for a track, discarding any queued animations, and mixes to it over the specified mix duration. */
-	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 mixes to it over the
-	 * specified mix duration.
-	 * @param delay Seconds to begin this animation after the start of the previous animation. May be <= 0 to use the animation
-	 *           duration of the previous track minus any mix duration plus the negative delay.
-	 * @return A track entry to allow further customization of animation playback. References to the track entry must not be kept
-	 *         after {@link AnimationStateListener#dispose(TrackEntry)}. */
-	public TrackEntry addEmptyAnimation (int trackIndex, float mixDuration, float delay) {
-		if (delay <= 0) delay -= mixDuration;
-		TrackEntry entry = addAnimation(trackIndex, emptyAnimation, false, delay);
-		entry.mixDuration = mixDuration;
-		entry.trackEnd = mixDuration;
-		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) {
-		queue.drainDisabled = true;
-		for (int i = 0, n = tracks.size; i < n; i++) {
-			TrackEntry current = tracks.get(i);
-			if (current != null) setEmptyAnimation(current.trackIndex, mixDuration);
-		}
-		queue.drainDisabled = false;
-		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;
-	}
-
-	/** @param last May be null. */
-	private TrackEntry trackEntry (int trackIndex, Animation animation, boolean loop, TrackEntry last) {
-		TrackEntry entry = trackEntryPool.obtain();
-		entry.trackIndex = trackIndex;
-		entry.animation = animation;
-		entry.loop = loop;
-
-		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 = loop ? Integer.MAX_VALUE : entry.animationEnd;
-		entry.timeScale = 1;
-
-		entry.alpha = 1;
-		entry.mixTime = 0;
-		entry.mixDuration = last == null ? 0 : data.getMix(last.animation, animation);
-		return entry;
-	}
-
-	private void disposeNext (TrackEntry entry) {
-		TrackEntry next = entry.next;
-		while (next != null) {
-			queue.dispose(next);
-			next = next.next;
-		}
-		entry.next = null;
-	}
-
-	private void animationsChanged () {
-		animationsChanged = false;
-
-		IntSet propertyIDs = this.propertyIDs;
-
-		// Compute timelinesFirst from lowest to highest track entries.
-		int i = 0, n = tracks.size;
-		propertyIDs.clear();
-		for (; i < n; i++) { // Find first non-null entry.
-			TrackEntry entry = tracks.get(i);
-			if (entry == null) continue;
-			setTimelinesFirst(entry);
-			i++;
-			break;
-		}
-		for (; i < n; i++) { // Rest of entries.
-			TrackEntry entry = tracks.get(i);
-			if (entry != null) checkTimelinesFirst(entry);
-		}
-
-		// Compute timelinesLast from highest to lowest track entries that have mixingFrom.
-		propertyIDs.clear();
-		int lowestMixingFrom = n;
-		for (i = 0; i < n; i++) { // Find lowest with a mixingFrom entry.
-			TrackEntry entry = tracks.get(i);
-			if (entry == null) continue;
-			if (entry.mixingFrom != null) {
-				lowestMixingFrom = i;
-				break;
-			}
-		}
-		for (i = n - 1; i >= lowestMixingFrom; i--) {
-			TrackEntry entry = tracks.get(i);
-			if (entry == null) continue;
-
-			Array<Timeline> timelines = entry.animation.timelines;
-			for (int ii = 0, nn = timelines.size; ii < nn; ii++)
-				propertyIDs.add(timelines.get(ii).getPropertyId());
-
-			entry = entry.mixingFrom;
-			while (entry != null) {
-				checkTimelinesUsage(entry, entry.timelinesLast);
-				entry = entry.mixingFrom;
-			}
-		}
-	}
-
-	/** From last to first mixingFrom entries, sets timelinesFirst to true on last, calls checkTimelineUsage on rest. */
-	private void setTimelinesFirst (TrackEntry entry) {
-		if (entry.mixingFrom != null) {
-			setTimelinesFirst(entry.mixingFrom);
-			checkTimelinesUsage(entry, entry.timelinesFirst);
-			return;
-		}
-		IntSet propertyIDs = this.propertyIDs;
-		Array<Timeline> timelines = entry.animation.timelines;
-		int n = timelines.size;
-		boolean[] usage = entry.timelinesFirst.setSize(n);
-		for (int i = 0; i < n; i++) {
-			propertyIDs.add(timelines.get(i).getPropertyId());
-			usage[i] = true;
-		}
-	}
-
-	/** From last to first mixingFrom entries, calls checkTimelineUsage. */
-	private void checkTimelinesFirst (TrackEntry entry) {
-		if (entry.mixingFrom != null) checkTimelinesFirst(entry.mixingFrom);
-		checkTimelinesUsage(entry, entry.timelinesFirst);
-	}
-
-	private void checkTimelinesUsage (TrackEntry entry, BooleanArray usageArray) {
-		IntSet propertyIDs = this.propertyIDs;
-		Array<Timeline> timelines = entry.animation.timelines;
-		int n = timelines.size;
-		boolean[] usage = usageArray.setSize(n);
-		for (int i = 0; i < n; i++)
-			usage[i] = propertyIDs.add(timelines.get(i).getPropertyId());
-	}
-
-	/** Returns the track entry for the animation currently playing on the track, or null. */
-	public TrackEntry getCurrent (int trackIndex) {
-		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);
-	}
-
-	public void clearListeners () {
-		listeners.clear();
-	}
-
-	/** Discards all {@link #addListener(AnimationStateListener) 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 to play slower or
-	 * faster. Defaults to 1. */
-	public float getTimeScale () {
-		return timeScale;
-	}
-
-	public void setTimeScale (float timeScale) {
-		this.timeScale = timeScale;
-	}
-
-	public AnimationStateData getData () {
-		return data;
-	}
-
-	public void setData (AnimationStateData data) {
-		if (data == null) throw new IllegalArgumentException("data cannot be null.");
-		this.data = data;
-	}
-
-	/** Returns the list of tracks that have animations, which may contain null entries. */
-	public Array<TrackEntry> getTracks () {
-		return tracks;
-	}
-
-	public String toString () {
-		StringBuilder buffer = new StringBuilder(64);
-		for (int i = 0, n = tracks.size; i < n; i++) {
-			TrackEntry entry = tracks.get(i);
-			if (entry == null) continue;
-			if (buffer.length() > 0) buffer.append(", ");
-			buffer.append(entry.toString());
-		}
-		if (buffer.length() == 0) return "<none>";
-		return buffer.toString();
-	}
-
-	/** State for the playback of an animation. */
-	static public class TrackEntry implements Poolable {
-		Animation animation;
-		TrackEntry next, mixingFrom;
-		AnimationStateListener listener;
-		int trackIndex;
-		boolean loop;
-		float eventThreshold, attachmentThreshold, drawOrderThreshold;
-		float animationStart, animationEnd, animationLast, nextAnimationLast;
-		float delay, trackTime, trackLast, nextTrackLast, trackEnd, timeScale;
-		float alpha, mixTime, mixDuration;
-		final BooleanArray timelinesFirst = new BooleanArray(), timelinesLast = new BooleanArray();
-		final FloatArray timelinesRotation = new FloatArray();
-
-		public void reset () {
-			next = null;
-			mixingFrom = null;
-			animation = null;
-			listener = null;
-			timelinesFirst.clear();
-			timelinesLast.clear();
-			timelinesRotation.clear();
-		}
-
-		public int getTrackIndex () {
-			return trackIndex;
-		}
-
-		public Animation getAnimation () {
-			return animation;
-		}
-
-		public void setAnimation (Animation animation) {
-			this.animation = animation;
-		}
-
-		public boolean getLoop () {
-			return loop;
-		}
-
-		public void setLoop (boolean loop) {
-			this.loop = loop;
-		}
-
-		/** Seconds to postpone playing the animation. When a track entry is the current track entry, delay postpones incrementing
-		 * the track time. When a track entry is queued, delay is the time from the start of the previous animation to when the
-		 * track entry will become the current track entry. */
-		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 animation duration for
-		 * non-looping animations and to {@link Integer#MAX_VALUE} for looping animations. If the track end time is reached, no
-		 * other animations are queued for playback, and mixing from any previous animations is complete, then the track is cleared,
-		 * leaving skeletons in their last pose.
-		 * <p>
-		 * It may be desired to use {@link AnimationState#addEmptyAnimation(int, float, float)} to mix the skeletons back to the
-		 * setup pose, rather than leaving them in their last pose. */
-		public float getTrackEnd () {
-			return trackEnd;
-		}
-
-		public void setTrackEnd (float trackEnd) {
-			this.trackEnd = trackEnd;
-		}
-
-		/** Seconds when this animation starts, both initially and after looping. Defaults to 0.
-		 * <p>
-		 * When changing the animation start 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 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 animation last time (exclusive) and animation time
-		 * (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 animation time between {@link #getAnimationStart()} and
-		 * {@link #getAnimationEnd()}. When the track time is 0, the animation time is equal to the animation start 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 the animation state is updated, causing time for this animation to play slower or
-		 * faster. Defaults to 1. */
-		public float getTimeScale () {
-			return timeScale;
-		}
-
-		public void setTimeScale (float timeScale) {
-			this.timeScale = timeScale;
-		}
-
-		/** The listener for events generated by this track entry, or null. */
-		public AnimationStateListener getListener () {
-			return listener;
-		}
-
-		/** @param listener May be null. */
-		public void setListener (AnimationStateListener listener) {
-			this.listener = listener;
-		}
-
-		/** Values < 1 mix this animation with the last skeleton pose. Defaults to 1, which overwrites the last skeleton pose with
-		 * this animation.
-		 * <p>
-		 * Typically track 0 is used to completely pose the skeleton, then alpha can be 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 (mix time / mix duration) is less than the event threshold, event timelines for the animation
-		 * being mixed out will be applied. Defaults to 0, so event timelines are not applied for an animation being mixed out. */
-		public float getEventThreshold () {
-			return eventThreshold;
-		}
-
-		public void setEventThreshold (float eventThreshold) {
-			this.eventThreshold = eventThreshold;
-		}
-
-		/** When the mix percentage (mix time / mix duration) is less than the attachment threshold, attachment timelines for the
-		 * animation being mixed out will be applied. Defaults to 0, so attachment timelines are not applied for an animation being
-		 * mixed out. */
-		public float getAttachmentThreshold () {
-			return attachmentThreshold;
-		}
-
-		public void setAttachmentThreshold (float attachmentThreshold) {
-			this.attachmentThreshold = attachmentThreshold;
-		}
-
-		/** When the mix percentage (mix time / mix duration) is less than the draw order threshold, draw order timelines for the
-		 * animation being mixed out will be applied. Defaults to 0, so draw order timelines are not applied for an animation 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. */
-		public TrackEntry getNext () {
-			return next;
-		}
-
-		/** Returns true if at least one loop has been completed. */
-		public boolean isComplete () {
-			return trackTime >= animationEnd - animationStart;
-		}
-
-		/** Seconds from 0 to the mix duration when mixing from the previous animation to this animation. May be slightly more than
-		 * {@link #getMixDuration()}. */
-		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
-		 * {@link AnimationStateData} based on the animation before this animation (if any).
-		 * <p>
-		 * The mix duration must be set before {@link AnimationState#update(float)} is next called. */
-		public float getMixDuration () {
-			return mixDuration;
-		}
-
-		public void setMixDuration (float mixDuration) {
-			this.mixDuration = mixDuration;
-		}
-
-		/** The track entry for the previous animation when mixing from the previous animation to this animation, or null if no
-		 * mixing is currently occuring. */
-		public TrackEntry getMixingFrom () {
-			return mixingFrom;
-		}
-
-		public String toString () {
-			return animation == null ? "<none>" : animation.name;
-		}
-	}
-
-	class EventQueue {
-		private final Array objects = new Array();
-		boolean drainDisabled;
-
-		public void start (TrackEntry entry) {
-			objects.add(EventType.start);
-			objects.add(entry);
-			animationsChanged = true;
-		}
-
-		public void interrupt (TrackEntry entry) {
-			objects.add(EventType.interrupt);
-			objects.add(entry);
-		}
-
-		public void end (TrackEntry entry) {
-			objects.add(EventType.end);
-			objects.add(entry);
-			animationsChanged = true;
-		}
-
-		public void dispose (TrackEntry entry) {
-			objects.add(EventType.dispose);
-			objects.add(entry);
-		}
-
-		public void complete (TrackEntry entry) {
-			objects.add(EventType.complete);
-			objects.add(entry);
-		}
-
-		public void event (TrackEntry entry, Event event) {
-			objects.add(EventType.event);
-			objects.add(entry);
-			objects.add(event);
-		}
-
-		public void drain () {
-			if (drainDisabled) return; // Not reentrant.
-			drainDisabled = true;
-
-			Array objects = this.objects;
-			Array<AnimationStateListener> listeners = AnimationState.this.listeners;
-			for (int i = 0; i < objects.size; i += 2) {
-				EventType type = (EventType)objects.get(i);
-				TrackEntry entry = (TrackEntry)objects.get(i + 1);
-				switch (type) {
-				case start:
-					if (entry.listener != null) entry.listener.end(entry);
-					for (int ii = 0; ii < listeners.size; ii++)
-						listeners.get(ii).start(entry);
-					break;
-				case interrupt:
-					if (entry.listener != null) entry.listener.end(entry);
-					for (int ii = 0; ii < listeners.size; ii++)
-						listeners.get(ii).interrupt(entry);
-					break;
-				case end:
-					if (entry.listener != null) entry.listener.end(entry);
-					for (int ii = 0; ii < listeners.size; ii++)
-						listeners.get(ii).end(entry);
-					// Fall through.
-				case dispose:
-					if (entry.listener != null) entry.listener.end(entry);
-					for (int ii = 0; ii < listeners.size; ii++)
-						listeners.get(ii).dispose(entry);
-					trackEntryPool.free(entry);
-					break;
-				case complete:
-					if (entry.listener != null) entry.listener.complete(entry);
-					for (int ii = 0; ii < listeners.size; ii++)
-						listeners.get(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 < listeners.size; ii++)
-						listeners.get(ii).event(entry, event);
-					break;
-				}
-			}
-			clear();
-
-			drainDisabled = false;
-		}
-
-		public void clear () {
-			objects.clear();
-		}
-	}
-
-	static private enum EventType {
-		start, interrupt, end, dispose, complete, event
-	}
-
-	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. References to the entry should not be kept after dispose is called, as it may
-		 * be destroyed or reused. */
-		public void dispose (TrackEntry entry);
-
-		/** Invoked every time this entry's animation completes a loop. */
-		public void complete (TrackEntry entry);
-
-		/** Invoked when this entry's animation triggers an event. */
-		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) {
-		}
-	}
+package com.esotericsoftware.spine;
+
+import static com.esotericsoftware.spine.Animation.RotateTimeline.*;
+
+import com.badlogic.gdx.utils.Array;
+import com.badlogic.gdx.utils.BooleanArray;
+import com.badlogic.gdx.utils.FloatArray;
+import com.badlogic.gdx.utils.IntSet;
+import com.badlogic.gdx.utils.Pool;
+import com.badlogic.gdx.utils.Pool.Poolable;
+import com.esotericsoftware.spine.Animation.AttachmentTimeline;
+import com.esotericsoftware.spine.Animation.DrawOrderTimeline;
+import com.esotericsoftware.spine.Animation.RotateTimeline;
+import com.esotericsoftware.spine.Animation.Timeline;
+
+/** Stores state for applying one or more animations over time and mixing (crossfading) between animations.
+ * <p>
+ * Animations on different tracks are applied sequentially each frame, from lowest to highest track index. This enables animations
+ * to be layered, where higher tracks either key only a subset of the skeleton pose or use alpha < 1 to mix with the pose on the
+ * lower track. */
+public class AnimationState {
+	static private final Animation emptyAnimation = new Animation("<empty>", new Array(0), 0);
+
+	private AnimationStateData data;
+	private final Array<TrackEntry> tracks = new Array();
+	private final Array<Event> events = new Array();
+	final Array<AnimationStateListener> listeners = new Array();
+	private final EventQueue queue = new EventQueue();
+	private final IntSet propertyIDs = new IntSet();
+	boolean animationsChanged;
+	private float timeScale = 1;
+
+	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 the track entry times, setting queued animations as current if needed. */
+	public void update (float delta) {
+		delta *= timeScale;
+		for (int i = 0, n = tracks.size; i < n; i++) {
+			TrackEntry current = tracks.get(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 = nextTime + delta * next.timeScale;
+					current.trackTime += currentDelta;
+					setCurrent(i, next);
+					while (next.mixingFrom != null) {
+						next.mixTime += currentDelta;
+						next = next.mixingFrom;
+					}
+					continue;
+				}
+				updateMixingFrom(current, delta);
+			} else {
+				updateMixingFrom(current, delta);
+				// Clear the track when there is no next entry, the track end time is reached, and there is no mixingFrom.
+				if (current.trackLast >= current.trackEnd && current.mixingFrom == null) {
+					tracks.set(i, null);
+					queue.end(current);
+					disposeNext(current);
+					continue;
+				}
+			}
+
+			current.trackTime += currentDelta;
+		}
+
+		queue.drain();
+	}
+
+	private void updateMixingFrom (TrackEntry entry, float delta) {
+		TrackEntry from = entry.mixingFrom;
+		if (from == null) return;
+
+		if (entry.mixTime >= entry.mixDuration && entry.mixTime > 0) {
+			queue.end(from);
+			TrackEntry newFrom = from.mixingFrom;
+			entry.mixingFrom = newFrom;
+			if (newFrom == null) return;
+			entry.mixTime = from.mixTime;
+			entry.mixDuration = from.mixDuration;
+			from = newFrom;
+		}
+
+		from.animationLast = from.nextAnimationLast;
+		from.trackLast = from.nextTrackLast;
+		float mixingFromDelta = delta * from.timeScale;
+		from.trackTime += mixingFromDelta;
+		entry.mixTime += mixingFromDelta;
+
+		updateMixingFrom(from, delta);
+	}
+
+	/** Poses the skeleton using the track entry animations. There are no side effects other than invoking listeners, so the
+	 * animation state can be applied to multiple skeletons to pose them identically. */
+	public void apply (Skeleton skeleton) {
+		if (skeleton == null) throw new IllegalArgumentException("skeleton cannot be null.");
+		if (animationsChanged) animationsChanged();
+
+		Array<Event> events = this.events;
+
+		for (int i = 0; i < tracks.size; i++) {
+			TrackEntry current = tracks.get(i);
+			if (current == null || current.delay > 0) continue;
+
+			// Apply mixing from entries first.
+			float mix = current.alpha;
+			if (current.mixingFrom != null) mix = applyMixingFrom(current, skeleton, mix);
+
+			// Apply current entry.
+			float animationLast = current.animationLast, animationTime = current.getAnimationTime();
+			Array<Timeline> timelines = current.animation.timelines;
+			if (mix == 1) {
+				for (int ii = 0, n = timelines.size; ii < n; ii++)
+					timelines.get(ii).apply(skeleton, animationLast, animationTime, events, 1, false, false);
+			} else {
+				boolean firstFrame = current.timelinesRotation.size == 0;
+				if (firstFrame) current.timelinesRotation.setSize(timelines.size << 1);
+				float[] timelinesRotation = current.timelinesRotation.items;
+				boolean[] timelinesFirst = current.timelinesFirst.items;
+				for (int ii = 0, n = timelines.size; ii < n; ii++) {
+					Timeline timeline = timelines.get(ii);
+					if (timeline instanceof RotateTimeline) {
+						applyRotateTimeline((RotateTimeline)timeline, skeleton, animationLast, animationTime, events, mix,
+							timelinesFirst[ii], false, timelinesRotation, ii << 1, firstFrame);
+					} else {
+						timeline.apply(skeleton, animationLast, animationTime, events, mix, timelinesFirst[ii], false);
+					}
+				}
+			}
+			queueEvents(current, animationTime);
+			current.nextAnimationLast = animationTime;
+			current.nextTrackLast = current.trackTime;
+		}
+
+		queue.drain();
+
+		System.out.println();
+	}
+
+	private float applyMixingFrom (TrackEntry entry, Skeleton skeleton, float alpha) {
+		float mix;
+		if (entry.mixDuration == 0) // Single frame mix to undo mixingFrom changes.
+			mix = 1;
+		else {
+			mix = alpha * entry.mixTime / entry.mixDuration;
+			if (mix > 1) mix = 1;
+		}
+
+		TrackEntry from = entry.mixingFrom;
+		if (from.mixingFrom != null) applyMixingFrom(from, skeleton, alpha);
+
+		Array<Event> events = mix < from.eventThreshold ? this.events : null;
+		boolean attachments = mix < from.attachmentThreshold, drawOrder = mix < from.drawOrderThreshold;
+
+		float animationLast = from.animationLast, animationTime = from.getAnimationTime();
+		Array<Timeline> timelines = from.animation.timelines;
+		int timelineCount = timelines.size;
+		boolean[] timelinesFirst = from.timelinesFirst.items, timelinesLast = from.timelinesLast.items;
+		float alphaFull = from.alpha, alphaMix = alphaFull * (1 - mix);
+
+		boolean firstFrame = from.timelinesRotation.size == 0;
+		if (firstFrame) from.timelinesRotation.setSize(timelineCount << 1);
+		float[] timelinesRotation = from.timelinesRotation.items;
+
+		System.out.println(entry.mixingFrom + " -> " + entry + ": " + entry.mixTime / entry.mixDuration);
+
+		for (int i = 0; i < timelineCount; i++) {
+			Timeline timeline = timelines.get(i);
+			boolean setupPose = timelinesFirst[i];
+			float a = timelinesLast[i] ? alphaMix : alphaFull;
+			if (timeline instanceof RotateTimeline) {
+				applyRotateTimeline((RotateTimeline)timeline, skeleton, animationLast, animationTime, events, a, setupPose, setupPose,
+					timelinesRotation, i << 1, firstFrame);
+			} else {
+				if (!setupPose) {
+					if (!attachments && timeline instanceof AttachmentTimeline) continue;
+					if (!drawOrder && timeline instanceof DrawOrderTimeline) continue;
+				}
+				timeline.apply(skeleton, animationLast, animationTime, events, a, setupPose, setupPose);
+			}
+		}
+
+		queueEvents(from, animationTime);
+		from.nextAnimationLast = animationTime;
+		from.nextTrackLast = from.trackTime;
+
+		return mix;
+	}
+
+	/** @param events May be null. */
+	private void applyRotateTimeline (RotateTimeline timeline, Skeleton skeleton, float lastTime, float time, Array<Event> events,
+		float alpha, boolean setupPose, boolean mixingOut, float[] timelinesRotation, int i, boolean firstFrame) {
+		if (alpha == 1) {
+			timeline.apply(skeleton, lastTime, time, events, 1, setupPose, setupPose);
+			return;
+		}
+
+		float[] frames = timeline.frames;
+		if (time < frames[0]) return; // Time is before first frame.
+
+		Bone bone = skeleton.bones.get(timeline.boneIndex);
+
+		float r2;
+		if (time >= frames[frames.length - ENTRIES]) // Time is after last frame.
+			r2 = bone.data.rotation + frames[frames.length + PREV_ROTATION];
+		else {
+			// Interpolate between the previous frame and the current frame.
+			int frame = Animation.binarySearch(frames, time, ENTRIES);
+			float prevRotation = frames[frame + PREV_ROTATION];
+			float frameTime = frames[frame];
+			float percent = timeline.getCurvePercent((frame >> 1) - 1,
+				1 - (time - frameTime) / (frames[frame + PREV_TIME] - frameTime));
+
+			r2 = frames[frame + ROTATION] - prevRotation;
+			r2 -= (16384 - (int)(16384.499999999996 - r2 / 360)) * 360;
+			r2 = prevRotation + r2 * percent + bone.data.rotation;
+			r2 -= (16384 - (int)(16384.499999999996 - r2 / 360)) * 360;
+		}
+
+		// Mix between two rotations using the direction of the shortest route on the first frame while detecting crosses.
+		float r1 = setupPose ? bone.data.rotation : bone.rotation;
+		float total, diff = r2 - r1;
+		if (diff == 0) {
+			if (firstFrame) {
+				timelinesRotation[i] = 0;
+				total = 0;
+			} else
+				total = timelinesRotation[i];
+		} else {
+			diff -= (16384 - (int)(16384.499999999996 - diff / 360)) * 360;
+			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; // Keep loops part of lastTotal.
+			if (dir != current) total += 360 * Math.signum(lastTotal);
+			timelinesRotation[i] = total;
+		}
+		timelinesRotation[i + 1] = diff;
+		r1 += total * alpha;
+		bone.rotation = r1 - (16384 - (int)(16384.499999999996 - r1 / 360)) * 360;
+	}
+
+	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.
+		Array<Event> events = this.events;
+		int i = 0, n = events.size;
+		for (; i < n; i++) {
+			Event event = events.get(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.
+		if (entry.loop ? (trackLastWrapped > entry.trackTime % duration)
+			: (animationTime >= animationEnd && entry.animationLast < animationEnd)) {
+			queue.complete(entry);
+		}
+
+		// Queue events after complete.
+		for (; i < n; i++) {
+			Event event = events.get(i);
+			if (event.time < animationStart) continue; // Discard events outside animation start/end.
+			queue.event(entry, events.get(i));
+		}
+		events.clear();
+	}
+
+	/** Removes all animations from all tracks, leaving skeletons in their last 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 last pose. */
+	public void clearTracks () {
+		queue.drainDisabled = true;
+		for (int i = 0, n = tracks.size; i < n; i++)
+			clearTrack(i);
+		tracks.clear();
+		queue.drainDisabled = false;
+		queue.drain();
+	}
+
+	/** Removes all animations from the track, leaving skeletons in their last 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 last pose. */
+	public void clearTrack (int trackIndex) {
+		if (trackIndex >= tracks.size) return;
+		TrackEntry current = tracks.get(trackIndex);
+		if (current == null) return;
+
+		queue.end(current);
+
+		disposeNext(current);
+
+		TrackEntry entry = current;
+		while (true) {
+			TrackEntry from = entry.mixingFrom;
+			if (from == null) break;
+			queue.end(from);
+			entry.mixingFrom = null;
+			entry = from;
+		}
+
+		tracks.set(current.trackIndex, null);
+
+		queue.drain();
+	}
+
+	private void setCurrent (int index, TrackEntry entry) {
+		TrackEntry current = expandToIndex(index);
+		tracks.set(index, entry);
+
+		if (current != null) {
+			queue.interrupt(current);
+			entry.mixingFrom = current;
+			entry.mixTime = Math.max(0, entry.mixDuration - current.trackTime);
+			current.timelinesRotation.clear(); // BOZO - Needed? Recursive?
+		}
+
+		queue.start(entry);
+	}
+
+	/** @see #setAnimation(int, Animation, boolean) */
+	public TrackEntry setAnimation (int trackIndex, String animationName, boolean loop) {
+		Animation animation = data.getSkeletonData().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.
+	 * @return A track entry to allow further customization of animation playback. References to the track entry must not be kept
+	 *         after {@link AnimationStateListener#dispose(TrackEntry)}. */
+	public TrackEntry setAnimation (int trackIndex, Animation animation, boolean loop) {
+		if (animation == null) throw new IllegalArgumentException("animation cannot be null.");
+		TrackEntry current = expandToIndex(trackIndex);
+		if (current != null) {
+			if (current.nextTrackLast == -1) {
+				// Don't mix from an entry that was never applied.
+				tracks.set(trackIndex, null);
+				queue.interrupt(current);
+				queue.end(current);
+				disposeNext(current);
+				current = null;
+			} else
+				disposeNext(current);
+		}
+		TrackEntry entry = trackEntry(trackIndex, animation, loop, current);
+		setCurrent(trackIndex, entry);
+		queue.drain();
+		return entry;
+	}
+
+	/** {@link #addAnimation(int, Animation, boolean, float)} */
+	public TrackEntry addAnimation (int trackIndex, String animationName, boolean loop, float delay) {
+		Animation animation = data.getSkeletonData().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.
+	 * @param delay Seconds to begin this animation after the start of the previous animation. May be <= 0 to use the animation
+	 *           duration of the previous track minus any mix duration plus the negative delay.
+	 * @return A track entry to allow further customization of animation playback. References to the track entry must not be kept
+	 *         after {@link AnimationStateListener#dispose(TrackEntry)}. */
+	public TrackEntry addAnimation (int trackIndex, Animation animation, boolean loop, float delay) {
+		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);
+			queue.drain();
+		} else {
+			last.next = entry;
+			if (delay <= 0) {
+				float duration = last.animationEnd - last.animationStart;
+				if (duration != 0)
+					delay += duration * (1 + (int)(last.trackTime / duration)) - data.getMix(last.animation, animation);
+				else
+					delay = 0;
+			}
+		}
+
+		entry.delay = delay;
+		return entry;
+	}
+
+	/** Sets an empty animation for a track, discarding any queued animations, and mixes to it over the specified mix duration. */
+	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 mixes to it over the
+	 * specified mix duration.
+	 * @param delay Seconds to begin this animation after the start of the previous animation. May be <= 0 to use the animation
+	 *           duration of the previous track minus any mix duration plus the negative delay.
+	 * @return A track entry to allow further customization of animation playback. References to the track entry must not be kept
+	 *         after {@link AnimationStateListener#dispose(TrackEntry)}. */
+	public TrackEntry addEmptyAnimation (int trackIndex, float mixDuration, float delay) {
+		if (delay <= 0) delay -= mixDuration;
+		TrackEntry entry = addAnimation(trackIndex, emptyAnimation, false, delay);
+		entry.mixDuration = mixDuration;
+		entry.trackEnd = mixDuration;
+		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) {
+		queue.drainDisabled = true;
+		for (int i = 0, n = tracks.size; i < n; i++) {
+			TrackEntry current = tracks.get(i);
+			if (current != null) setEmptyAnimation(current.trackIndex, mixDuration);
+		}
+		queue.drainDisabled = false;
+		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;
+	}
+
+	/** @param last May be null. */
+	private TrackEntry trackEntry (int trackIndex, Animation animation, boolean loop, TrackEntry last) {
+		TrackEntry entry = trackEntryPool.obtain();
+		entry.trackIndex = trackIndex;
+		entry.animation = animation;
+		entry.loop = loop;
+
+		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 = loop ? Integer.MAX_VALUE : entry.animationEnd;
+		entry.timeScale = 1;
+
+		entry.alpha = 1;
+		entry.mixTime = 0;
+		entry.mixDuration = last == null ? 0 : data.getMix(last.animation, animation);
+		return entry;
+	}
+
+	private void disposeNext (TrackEntry entry) {
+		TrackEntry next = entry.next;
+		while (next != null) {
+			queue.dispose(next);
+			next = next.next;
+		}
+		entry.next = null;
+	}
+
+	private void animationsChanged () {
+		animationsChanged = false;
+
+		IntSet propertyIDs = this.propertyIDs;
+
+		// Compute timelinesFirst from lowest to highest track entries.
+		int i = 0, n = tracks.size;
+		propertyIDs.clear();
+		for (; i < n; i++) { // Find first non-null entry.
+			TrackEntry entry = tracks.get(i);
+			if (entry == null) continue;
+			setTimelinesFirst(entry);
+			i++;
+			break;
+		}
+		for (; i < n; i++) { // Rest of entries.
+			TrackEntry entry = tracks.get(i);
+			if (entry != null) checkTimelinesFirst(entry);
+		}
+
+		// Compute timelinesLast from highest to lowest track entries that have mixingFrom.
+		propertyIDs.clear();
+		int lowestMixingFrom = n;
+		for (i = 0; i < n; i++) { // Find lowest with a mixingFrom entry.
+			TrackEntry entry = tracks.get(i);
+			if (entry == null) continue;
+			if (entry.mixingFrom != null) {
+				lowestMixingFrom = i;
+				break;
+			}
+		}
+		for (i = n - 1; i >= lowestMixingFrom; i--) {
+			TrackEntry entry = tracks.get(i);
+			if (entry == null) continue;
+
+			Array<Timeline> timelines = entry.animation.timelines;
+			for (int ii = 0, nn = timelines.size; ii < nn; ii++)
+				propertyIDs.add(timelines.get(ii).getPropertyId());
+
+			entry = entry.mixingFrom;
+			while (entry != null) {
+				checkTimelinesUsage(entry, entry.timelinesLast);
+				entry = entry.mixingFrom;
+			}
+		}
+	}
+
+	/** From last to first mixingFrom entries, sets timelinesFirst to true on last, calls checkTimelineUsage on rest. */
+	private void setTimelinesFirst (TrackEntry entry) {
+		if (entry.mixingFrom != null) {
+			setTimelinesFirst(entry.mixingFrom);
+			checkTimelinesUsage(entry, entry.timelinesFirst);
+			return;
+		}
+		IntSet propertyIDs = this.propertyIDs;
+		Array<Timeline> timelines = entry.animation.timelines;
+		int n = timelines.size;
+		boolean[] usage = entry.timelinesFirst.setSize(n);
+		for (int i = 0; i < n; i++) {
+			propertyIDs.add(timelines.get(i).getPropertyId());
+			usage[i] = true;
+		}
+	}
+
+	/** From last to first mixingFrom entries, calls checkTimelineUsage. */
+	private void checkTimelinesFirst (TrackEntry entry) {
+		if (entry.mixingFrom != null) checkTimelinesFirst(entry.mixingFrom);
+		checkTimelinesUsage(entry, entry.timelinesFirst);
+	}
+
+	private void checkTimelinesUsage (TrackEntry entry, BooleanArray usageArray) {
+		IntSet propertyIDs = this.propertyIDs;
+		Array<Timeline> timelines = entry.animation.timelines;
+		int n = timelines.size;
+		boolean[] usage = usageArray.setSize(n);
+		for (int i = 0; i < n; i++)
+			usage[i] = propertyIDs.add(timelines.get(i).getPropertyId());
+	}
+
+	/** Returns the track entry for the animation currently playing on the track, or null. */
+	public TrackEntry getCurrent (int trackIndex) {
+		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);
+	}
+
+	public void clearListeners () {
+		listeners.clear();
+	}
+
+	/** Discards all {@link #addListener(AnimationStateListener) 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 to play slower or
+	 * faster. Defaults to 1. */
+	public float getTimeScale () {
+		return timeScale;
+	}
+
+	public void setTimeScale (float timeScale) {
+		this.timeScale = timeScale;
+	}
+
+	public AnimationStateData getData () {
+		return data;
+	}
+
+	public void setData (AnimationStateData data) {
+		if (data == null) throw new IllegalArgumentException("data cannot be null.");
+		this.data = data;
+	}
+
+	/** Returns the list of tracks that have animations, which may contain null entries. */
+	public Array<TrackEntry> getTracks () {
+		return tracks;
+	}
+
+	public String toString () {
+		StringBuilder buffer = new StringBuilder(64);
+		for (int i = 0, n = tracks.size; i < n; i++) {
+			TrackEntry entry = tracks.get(i);
+			if (entry == null) continue;
+			if (buffer.length() > 0) buffer.append(", ");
+			buffer.append(entry.toString());
+		}
+		if (buffer.length() == 0) return "<none>";
+		return buffer.toString();
+	}
+
+	/** State for the playback of an animation. */
+	static public class TrackEntry implements Poolable {
+		Animation animation;
+		TrackEntry next, mixingFrom;
+		AnimationStateListener listener;
+		int trackIndex;
+		boolean loop;
+		float eventThreshold, attachmentThreshold, drawOrderThreshold;
+		float animationStart, animationEnd, animationLast, nextAnimationLast;
+		float delay, trackTime, trackLast, nextTrackLast, trackEnd, timeScale;
+		float alpha, mixTime, mixDuration;
+		final BooleanArray timelinesFirst = new BooleanArray(), timelinesLast = new BooleanArray();
+		final FloatArray timelinesRotation = new FloatArray();
+
+		public void reset () {
+			next = null;
+			mixingFrom = null;
+			animation = null;
+			listener = null;
+			timelinesFirst.clear();
+			timelinesLast.clear();
+			timelinesRotation.clear();
+		}
+
+		public int getTrackIndex () {
+			return trackIndex;
+		}
+
+		public Animation getAnimation () {
+			return animation;
+		}
+
+		public void setAnimation (Animation animation) {
+			this.animation = animation;
+		}
+
+		public boolean getLoop () {
+			return loop;
+		}
+
+		public void setLoop (boolean loop) {
+			this.loop = loop;
+		}
+
+		/** Seconds to postpone playing the animation. When a track entry is the current track entry, delay postpones incrementing
+		 * the track time. When a track entry is queued, delay is the time from the start of the previous animation to when the
+		 * track entry will become the current track entry. */
+		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 animation duration for
+		 * non-looping animations and to {@link Integer#MAX_VALUE} for looping animations. If the track end time is reached, no
+		 * other animations are queued for playback, and mixing from any previous animations is complete, then the track is cleared,
+		 * leaving skeletons in their last pose.
+		 * <p>
+		 * It may be desired to use {@link AnimationState#addEmptyAnimation(int, float, float)} to mix the skeletons back to the
+		 * setup pose, rather than leaving them in their last pose. */
+		public float getTrackEnd () {
+			return trackEnd;
+		}
+
+		public void setTrackEnd (float trackEnd) {
+			this.trackEnd = trackEnd;
+		}
+
+		/** Seconds when this animation starts, both initially and after looping. Defaults to 0.
+		 * <p>
+		 * When changing the animation start 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 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 animation last time (exclusive) and animation time
+		 * (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 animation time between {@link #getAnimationStart()} and
+		 * {@link #getAnimationEnd()}. When the track time is 0, the animation time is equal to the animation start 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 the animation state is updated, causing time for this animation to play slower or
+		 * faster. Defaults to 1. */
+		public float getTimeScale () {
+			return timeScale;
+		}
+
+		public void setTimeScale (float timeScale) {
+			this.timeScale = timeScale;
+		}
+
+		/** The listener for events generated by this track entry, or null. */
+		public AnimationStateListener getListener () {
+			return listener;
+		}
+
+		/** @param listener May be null. */
+		public void setListener (AnimationStateListener listener) {
+			this.listener = listener;
+		}
+
+		/** Values < 1 mix this animation with the last skeleton pose. Defaults to 1, which overwrites the last skeleton pose with
+		 * this animation.
+		 * <p>
+		 * Typically track 0 is used to completely pose the skeleton, then alpha can be 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 (mix time / mix duration) is less than the event threshold, event timelines for the animation
+		 * being mixed out will be applied. Defaults to 0, so event timelines are not applied for an animation being mixed out. */
+		public float getEventThreshold () {
+			return eventThreshold;
+		}
+
+		public void setEventThreshold (float eventThreshold) {
+			this.eventThreshold = eventThreshold;
+		}
+
+		/** When the mix percentage (mix time / mix duration) is less than the attachment threshold, attachment timelines for the
+		 * animation being mixed out will be applied. Defaults to 0, so attachment timelines are not applied for an animation being
+		 * mixed out. */
+		public float getAttachmentThreshold () {
+			return attachmentThreshold;
+		}
+
+		public void setAttachmentThreshold (float attachmentThreshold) {
+			this.attachmentThreshold = attachmentThreshold;
+		}
+
+		/** When the mix percentage (mix time / mix duration) is less than the draw order threshold, draw order timelines for the
+		 * animation being mixed out will be applied. Defaults to 0, so draw order timelines are not applied for an animation 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. */
+		public TrackEntry getNext () {
+			return next;
+		}
+
+		/** Returns true if at least one loop has been completed. */
+		public boolean isComplete () {
+			return trackTime >= animationEnd - animationStart;
+		}
+
+		/** Seconds from 0 to the mix duration when mixing from the previous animation to this animation. May be slightly more than
+		 * {@link #getMixDuration()}. */
+		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
+		 * {@link AnimationStateData} based on the animation before this animation (if any).
+		 * <p>
+		 * The mix duration must be set before {@link AnimationState#update(float)} is next called. */
+		public float getMixDuration () {
+			return mixDuration;
+		}
+
+		public void setMixDuration (float mixDuration) {
+			this.mixDuration = mixDuration;
+		}
+
+		/** The track entry for the previous animation when mixing from the previous animation to this animation, or null if no
+		 * mixing is currently occuring. */
+		public TrackEntry getMixingFrom () {
+			return mixingFrom;
+		}
+
+		public String toString () {
+			return animation == null ? "<none>" : animation.name;
+		}
+	}
+
+	class EventQueue {
+		private final Array objects = new Array();
+		boolean drainDisabled;
+
+		public void start (TrackEntry entry) {
+			objects.add(EventType.start);
+			objects.add(entry);
+			animationsChanged = true;
+		}
+
+		public void interrupt (TrackEntry entry) {
+			objects.add(EventType.interrupt);
+			objects.add(entry);
+		}
+
+		public void end (TrackEntry entry) {
+			objects.add(EventType.end);
+			objects.add(entry);
+			animationsChanged = true;
+		}
+
+		public void dispose (TrackEntry entry) {
+			objects.add(EventType.dispose);
+			objects.add(entry);
+		}
+
+		public void complete (TrackEntry entry) {
+			objects.add(EventType.complete);
+			objects.add(entry);
+		}
+
+		public void event (TrackEntry entry, Event event) {
+			objects.add(EventType.event);
+			objects.add(entry);
+			objects.add(event);
+		}
+
+		public void drain () {
+			if (drainDisabled) return; // Not reentrant.
+			drainDisabled = true;
+
+			Array objects = this.objects;
+			Array<AnimationStateListener> listeners = AnimationState.this.listeners;
+			for (int i = 0; i < objects.size; i += 2) {
+				EventType type = (EventType)objects.get(i);
+				TrackEntry entry = (TrackEntry)objects.get(i + 1);
+				switch (type) {
+				case start:
+					if (entry.listener != null) entry.listener.end(entry);
+					for (int ii = 0; ii < listeners.size; ii++)
+						listeners.get(ii).start(entry);
+					break;
+				case interrupt:
+					if (entry.listener != null) entry.listener.end(entry);
+					for (int ii = 0; ii < listeners.size; ii++)
+						listeners.get(ii).interrupt(entry);
+					break;
+				case end:
+					if (entry.listener != null) entry.listener.end(entry);
+					for (int ii = 0; ii < listeners.size; ii++)
+						listeners.get(ii).end(entry);
+					// Fall through.
+				case dispose:
+					if (entry.listener != null) entry.listener.end(entry);
+					for (int ii = 0; ii < listeners.size; ii++)
+						listeners.get(ii).dispose(entry);
+					trackEntryPool.free(entry);
+					break;
+				case complete:
+					if (entry.listener != null) entry.listener.complete(entry);
+					for (int ii = 0; ii < listeners.size; ii++)
+						listeners.get(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 < listeners.size; ii++)
+						listeners.get(ii).event(entry, event);
+					break;
+				}
+			}
+			clear();
+
+			drainDisabled = false;
+		}
+
+		public void clear () {
+			objects.clear();
+		}
+	}
+
+	static private enum EventType {
+		start, interrupt, end, dispose, complete, event
+	}
+
+	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. References to the entry should not be kept after dispose is called, as it may
+		 * be destroyed or reused. */
+		public void dispose (TrackEntry entry);
+
+		/** Invoked every time this entry's animation completes a loop. */
+		public void complete (TrackEntry entry);
+
+		/** Invoked when this entry's animation triggers an event. */
+		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) {
+		}
+	}
 }

+ 105 - 106
spine-libgdx/spine-libgdx/src/com/esotericsoftware/spine/AnimationStateData.java

@@ -1,109 +1,108 @@
-/******************************************************************************
- * Spine Runtimes Software License
- * Version 2.3
- * 
- * Copyright (c) 2013-2015, Esoteric Software
- * All rights reserved.
- * 
- * You are granted a perpetual, non-exclusive, non-sublicensable and
- * non-transferable license to use, install, execute and perform the Spine
- * Runtimes Software (the "Software") and derivative works solely for personal
- * or internal use. Without the written permission of Esoteric Software (see
- * Section 2 of the Spine Software License Agreement), you may not (a) modify,
- * translate, adapt or otherwise create derivative works, improvements of the
- * Software or develop new applications using the Software or (b) remove,
- * delete, alter or obscure any trademarks or any copyright, trademark, patent
- * or other intellectual property or proprietary rights notices on or in the
- * Software, including any copy thereof. Redistributions in binary or source
- * form must include this license and terms.
- * 
- * THIS SOFTWARE IS PROVIDED BY ESOTERIC SOFTWARE "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 BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
- * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
- * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS;
- * OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
- * WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR
- * OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
- * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+/******************************************************************************
+ * Spine Runtimes Software License v2.5
+ * 
+ * Copyright (c) 2013-2016, Esoteric Software
+ * All rights reserved.
+ * 
+ * You are granted a perpetual, non-exclusive, non-sublicensable, and
+ * non-transferable license to use, install, execute, and perform the Spine
+ * Runtimes software and derivative works solely for personal or internal
+ * use. Without the written permission of Esoteric Software (see Section 2 of
+ * the Spine Software License Agreement), you may not (a) modify, translate,
+ * adapt, or develop new applications using the Spine Runtimes or otherwise
+ * create derivative works or improvements of the Spine Runtimes or (b) remove,
+ * delete, alter, or obscure any trademarks or any copyright, trademark, patent,
+ * or other intellectual property or proprietary rights notices on or in the
+ * Software, including any copy thereof. Redistributions in binary or source
+ * form must include this license and terms.
+ * 
+ * THIS SOFTWARE IS PROVIDED BY ESOTERIC SOFTWARE "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 BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
+ * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES, BUSINESS INTERRUPTION, OR LOSS OF
+ * USE, DATA, OR PROFITS) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER
+ * IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+ * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
+ * POSSIBILITY OF SUCH DAMAGE.
  *****************************************************************************/
 
-package com.esotericsoftware.spine;
-
-import com.badlogic.gdx.utils.ObjectFloatMap;
-
-/** Stores mixing times between animations. */
-public class AnimationStateData {
-	private final SkeletonData skeletonData;
-	final ObjectFloatMap<Key> animationToMixTime = new ObjectFloatMap();
-	final Key tempKey = new Key();
-	float defaultMix;
-
-	public AnimationStateData (SkeletonData skeletonData) {
-		if (skeletonData == null) throw new IllegalArgumentException("skeletonData cannot be null.");
-		this.skeletonData = skeletonData;
-	}
-
-	public SkeletonData getSkeletonData () {
-		return skeletonData;
-	}
-
-	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);
-	}
-
-	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);
-	}
-
-	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);
-	}
-
-	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;
-		}
-	}
+package com.esotericsoftware.spine;
+
+import com.badlogic.gdx.utils.ObjectFloatMap;
+
+/** Stores mixing times between animations. */
+public class AnimationStateData {
+	private final SkeletonData skeletonData;
+	final ObjectFloatMap<Key> animationToMixTime = new ObjectFloatMap();
+	final Key tempKey = new Key();
+	float defaultMix;
+
+	public AnimationStateData (SkeletonData skeletonData) {
+		if (skeletonData == null) throw new IllegalArgumentException("skeletonData cannot be null.");
+		this.skeletonData = skeletonData;
+	}
+
+	public SkeletonData getSkeletonData () {
+		return skeletonData;
+	}
+
+	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);
+	}
+
+	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);
+	}
+
+	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);
+	}
+
+	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;
+		}
+	}
 }

+ 466 - 467
spine-libgdx/spine-libgdx/src/com/esotericsoftware/spine/Bone.java

@@ -1,470 +1,469 @@
-/******************************************************************************
- * Spine Runtimes Software License
- * Version 2.3
- * 
- * Copyright (c) 2013-2015, Esoteric Software
- * All rights reserved.
- * 
- * You are granted a perpetual, non-exclusive, non-sublicensable and
- * non-transferable license to use, install, execute and perform the Spine
- * Runtimes Software (the "Software") and derivative works solely for personal
- * or internal use. Without the written permission of Esoteric Software (see
- * Section 2 of the Spine Software License Agreement), you may not (a) modify,
- * translate, adapt or otherwise create derivative works, improvements of the
- * Software or develop new applications using the Software or (b) remove,
- * delete, alter or obscure any trademarks or any copyright, trademark, patent
- * or other intellectual property or proprietary rights notices on or in the
- * Software, including any copy thereof. Redistributions in binary or source
- * form must include this license and terms.
- * 
- * THIS SOFTWARE IS PROVIDED BY ESOTERIC SOFTWARE "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 BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
- * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
- * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS;
- * OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
- * WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR
- * OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
- * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+/******************************************************************************
+ * Spine Runtimes Software License v2.5
+ * 
+ * Copyright (c) 2013-2016, Esoteric Software
+ * All rights reserved.
+ * 
+ * You are granted a perpetual, non-exclusive, non-sublicensable, and
+ * non-transferable license to use, install, execute, and perform the Spine
+ * Runtimes software and derivative works solely for personal or internal
+ * use. Without the written permission of Esoteric Software (see Section 2 of
+ * the Spine Software License Agreement), you may not (a) modify, translate,
+ * adapt, or develop new applications using the Spine Runtimes or otherwise
+ * create derivative works or improvements of the Spine Runtimes or (b) remove,
+ * delete, alter, or obscure any trademarks or any copyright, trademark, patent,
+ * or other intellectual property or proprietary rights notices on or in the
+ * Software, including any copy thereof. Redistributions in binary or source
+ * form must include this license and terms.
+ * 
+ * THIS SOFTWARE IS PROVIDED BY ESOTERIC SOFTWARE "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 BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
+ * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES, BUSINESS INTERRUPTION, OR LOSS OF
+ * USE, DATA, OR PROFITS) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER
+ * IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+ * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
+ * POSSIBILITY OF SUCH DAMAGE.
  *****************************************************************************/
 
-package com.esotericsoftware.spine;
-
-import static com.badlogic.gdx.math.MathUtils.*;
-import static com.badlogic.gdx.math.Matrix3.*;
-
-import com.badlogic.gdx.math.Matrix3;
-import com.badlogic.gdx.math.Vector2;
-import com.badlogic.gdx.utils.Array;
-import com.esotericsoftware.spine.BoneData.TransformMode;
-
-public class Bone implements Updatable {
-	final BoneData data;
-	final Skeleton skeleton;
-	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;
-	boolean appliedValid;
-
-	float a, b, worldX;
-	float c, d, worldY;
-
-	boolean sorted;
-
-	/** @param parent May be null. */
-	public Bone (BoneData data, Skeleton skeleton, 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 children bones.
-	 * @param parent May be null. */
-	public Bone (Bone bone, Skeleton skeleton, 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;
-	}
-
-	/** Same as {@link #updateWorldTransform()}. This method exists for Bone to implement {@link Updatable}. */
-	public void update () {
-		updateWorldTransform(x, y, rotation, scaleX, scaleY, shearX, shearY);
-	}
-
-	/** Computes the world transform using the parent bone and this bone's local transform. */
-	public void updateWorldTransform () {
-		updateWorldTransform(x, y, rotation, scaleX, scaleY, shearX, shearY);
-	}
-
-	/** Computes the world transform using the parent bone and the specified local transform. */
-	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;
-		appliedValid = true;
-
-		Bone parent = this.parent;
-		if (parent == null) { // Root bone.
-			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;
-			Skeleton skeleton = this.skeleton;
-			if (skeleton.flipX) {
-				x = -x;
-				la = -la;
-				lb = -lb;
-			}
-			if (skeleton.flipY) {
-				y = -y;
-				lc = -lc;
-				ld = -ld;
-			}
-			a = la;
-			b = lb;
-			c = lc;
-			d = ld;
-			worldX = x + skeleton.x;
-			worldY = y + 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 psx = (float)Math.sqrt(pa * pa + pc * pc), psy, prx;
-			if (psx > 0.0001f) {
-				psy = Math.abs((pa * pd - pb * pc) / psx);
-				prx = atan2(pc, pa) * radDeg;
-			} else {
-				psx = 0;
-				psy = (float)Math.sqrt(pb * pb + pd * pd);
-				prx = 90 - atan2(pd, pb) * radDeg;
-			}
-			pa = cosDeg(prx) * psx;
-			pb = cosDeg(prx + 90) * psy;
-			pc = sinDeg(prx) * psx;
-			pd = sinDeg(prx + 90) * psy;
-			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;
-			float za = pa * la + pb * lc;
-			float zb = pa * lb + pb * ld;
-			float zc = pc * la + pd * lc;
-			float zd = pc * lb + pd * ld;
-			pa = cosDeg(-prx);
-			pb = cosDeg(90 - prx);
-			pc = sinDeg(-prx);
-			pd = sinDeg(90 - prx);
-			a = za * pa + zb * pc;
-			b = za * pb + zb * pd;
-			c = zc * pa + zd * pc;
-			d = zc * pb + zd * pd;
-			break;
-		}
-		case noScale:
-		case noScaleOrReflection: {
-			float cos = cosDeg(rotation), sin = sinDeg(rotation);
-			float za = pa * cos + pb * sin;
-			float zc = pc * cos + pd * sin;
-			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);
-			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;
-			if (data.transformMode != TransformMode.noScaleOrReflection ? pa * pd - pb * pc < 0 : skeleton.flipX != skeleton.flipY) {
-				b = -b;
-				d = -d;
-			}
-			return;
-		}
-		}
-		if (skeleton.flipX) {
-			a = -a;
-			b = -b;
-		}
-		if (skeleton.flipY) {
-			c = -c;
-			d = -d;
-		}
-	}
-
-	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;
-	}
-
-	public BoneData getData () {
-		return data;
-	}
-
-	public Skeleton getSkeleton () {
-		return skeleton;
-	}
-
-	public Bone getParent () {
-		return parent;
-	}
-
-	public Array<Bone> getChildren () {
-		return children;
-	}
-
-	public float getX () {
-		return x;
-	}
-
-	public void setX (float x) {
-		this.x = x;
-	}
-
-	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;
-	}
-
-	public float getRotation () {
-		return rotation;
-	}
-
-	public void setRotation (float rotation) {
-		this.rotation = rotation;
-	}
-
-	public float getScaleX () {
-		return scaleX;
-	}
-
-	public void setScaleX (float scaleX) {
-		this.scaleX = scaleX;
-	}
-
-	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;
-	}
-
-	public float getShearX () {
-		return shearX;
-	}
-
-	public void setShearX (float shearX) {
-		this.shearX = shearX;
-	}
-
-	public float getShearY () {
-		return shearY;
-	}
-
-	public void setShearY (float shearY) {
-		this.shearY = shearY;
-	}
-
-	public float getA () {
-		return a;
-	}
-
-	public float getB () {
-		return b;
-	}
-
-	public float getC () {
-		return c;
-	}
-
-	public float getD () {
-		return d;
-	}
-
-	public float getWorldX () {
-		return worldX;
-	}
-
-	public float getWorldY () {
-		return worldY;
-	}
-
-	public float getWorldRotationX () {
-		return atan2(c, a) * radDeg;
-	}
-
-	public float getWorldRotationY () {
-		return atan2(d, b) * radDeg;
-	}
-
-	/** Returns the magnitude (always positive) of the world scale X. */
-	public float getWorldScaleX () {
-		return (float)Math.sqrt(a * a + c * c);
-	}
-
-	/** Returns the magnitude (always positive) of the world scale Y. */
-	public float getWorldScaleY () {
-		return (float)Math.sqrt(b * b + d * d);
-	}
-
-	public float worldToLocalRotationX () {
-		Bone parent = this.parent;
-		if (parent == null) return arotation;
-		float pa = parent.a, pb = parent.b, pc = parent.c, pd = parent.d, a = this.a, c = this.c;
-		return atan2(pa * c - pc * a, pd * a - pb * c) * radDeg;
-	}
-
-	public float worldToLocalRotationY () {
-		Bone parent = this.parent;
-		if (parent == null) return arotation;
-		float pa = parent.a, pb = parent.b, pc = parent.c, pd = parent.d, b = this.b, d = this.d;
-		return atan2(pa * d - pc * b, pd * b - pb * d) * radDeg;
-	}
-
-	public void rotateWorld (float degrees) {
-		float a = this.a, b = this.b, c = this.c, d = this.d;
-		float cos = cosDeg(degrees), sin = sinDeg(degrees);
-		this.a = cos * a - sin * c;
-		this.b = cos * b - sin * d;
-		this.c = sin * a + cos * c;
-		this.d = sin * b + cos * d;
-		appliedValid = false;
-	}
-
-	/** Computes the individual applied transform values from the world transform. This can be useful to perform processing using
-	 * the applied transform after the world transform has been modified directly (eg, by a constraint).
-	 * <p>
-	 * Some information is ambiguous in the world transform, such as -1,-1 scale versus 180 rotation. */
-	public void updateAppliedTransform () {
-		appliedValid = true;
-		Bone parent = this.parent;
-		if (parent == null) {
-			ax = worldX;
-			ay = worldY;
-			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;
-		}
-	}
-
-	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;
-	}
-
-	public Vector2 worldToLocal (Vector2 world) {
-		float a = this.a, b = this.b, c = this.c, d = this.d;
-		float invDet = 1 / (a * d - b * c);
-		float x = world.x - worldX, y = world.y - worldY;
-		world.x = (x * d * invDet - y * b * invDet);
-		world.y = (y * a * invDet - x * c * invDet);
-		return world;
-	}
-
-	public Vector2 localToWorld (Vector2 local) {
-		float x = local.x, y = local.y;
-		local.x = x * a + y * b + worldX;
-		local.y = x * c + y * d + worldY;
-		return local;
-	}
-
-	public String toString () {
-		return data.name;
-	}
+package com.esotericsoftware.spine;
+
+import static com.badlogic.gdx.math.MathUtils.*;
+import static com.badlogic.gdx.math.Matrix3.*;
+
+import com.badlogic.gdx.math.Matrix3;
+import com.badlogic.gdx.math.Vector2;
+import com.badlogic.gdx.utils.Array;
+import com.esotericsoftware.spine.BoneData.TransformMode;
+
+public class Bone implements Updatable {
+	final BoneData data;
+	final Skeleton skeleton;
+	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;
+	boolean appliedValid;
+
+	float a, b, worldX;
+	float c, d, worldY;
+
+	boolean sorted;
+
+	/** @param parent May be null. */
+	public Bone (BoneData data, Skeleton skeleton, 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 children bones.
+	 * @param parent May be null. */
+	public Bone (Bone bone, Skeleton skeleton, 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;
+	}
+
+	/** Same as {@link #updateWorldTransform()}. This method exists for Bone to implement {@link Updatable}. */
+	public void update () {
+		updateWorldTransform(x, y, rotation, scaleX, scaleY, shearX, shearY);
+	}
+
+	/** Computes the world transform using the parent bone and this bone's local transform. */
+	public void updateWorldTransform () {
+		updateWorldTransform(x, y, rotation, scaleX, scaleY, shearX, shearY);
+	}
+
+	/** Computes the world transform using the parent bone and the specified local transform. */
+	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;
+		appliedValid = true;
+
+		Bone parent = this.parent;
+		if (parent == null) { // Root bone.
+			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;
+			Skeleton skeleton = this.skeleton;
+			if (skeleton.flipX) {
+				x = -x;
+				la = -la;
+				lb = -lb;
+			}
+			if (skeleton.flipY) {
+				y = -y;
+				lc = -lc;
+				ld = -ld;
+			}
+			a = la;
+			b = lb;
+			c = lc;
+			d = ld;
+			worldX = x + skeleton.x;
+			worldY = y + 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 psx = (float)Math.sqrt(pa * pa + pc * pc), psy, prx;
+			if (psx > 0.0001f) {
+				psy = Math.abs((pa * pd - pb * pc) / psx);
+				prx = atan2(pc, pa) * radDeg;
+			} else {
+				psx = 0;
+				psy = (float)Math.sqrt(pb * pb + pd * pd);
+				prx = 90 - atan2(pd, pb) * radDeg;
+			}
+			pa = cosDeg(prx) * psx;
+			pb = cosDeg(prx + 90) * psy;
+			pc = sinDeg(prx) * psx;
+			pd = sinDeg(prx + 90) * psy;
+			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;
+			float za = pa * la + pb * lc;
+			float zb = pa * lb + pb * ld;
+			float zc = pc * la + pd * lc;
+			float zd = pc * lb + pd * ld;
+			pa = cosDeg(-prx);
+			pb = cosDeg(90 - prx);
+			pc = sinDeg(-prx);
+			pd = sinDeg(90 - prx);
+			a = za * pa + zb * pc;
+			b = za * pb + zb * pd;
+			c = zc * pa + zd * pc;
+			d = zc * pb + zd * pd;
+			break;
+		}
+		case noScale:
+		case noScaleOrReflection: {
+			float cos = cosDeg(rotation), sin = sinDeg(rotation);
+			float za = pa * cos + pb * sin;
+			float zc = pc * cos + pd * sin;
+			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);
+			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;
+			if (data.transformMode != TransformMode.noScaleOrReflection ? pa * pd - pb * pc < 0 : skeleton.flipX != skeleton.flipY) {
+				b = -b;
+				d = -d;
+			}
+			return;
+		}
+		}
+		if (skeleton.flipX) {
+			a = -a;
+			b = -b;
+		}
+		if (skeleton.flipY) {
+			c = -c;
+			d = -d;
+		}
+	}
+
+	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;
+	}
+
+	public BoneData getData () {
+		return data;
+	}
+
+	public Skeleton getSkeleton () {
+		return skeleton;
+	}
+
+	public Bone getParent () {
+		return parent;
+	}
+
+	public Array<Bone> getChildren () {
+		return children;
+	}
+
+	public float getX () {
+		return x;
+	}
+
+	public void setX (float x) {
+		this.x = x;
+	}
+
+	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;
+	}
+
+	public float getRotation () {
+		return rotation;
+	}
+
+	public void setRotation (float rotation) {
+		this.rotation = rotation;
+	}
+
+	public float getScaleX () {
+		return scaleX;
+	}
+
+	public void setScaleX (float scaleX) {
+		this.scaleX = scaleX;
+	}
+
+	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;
+	}
+
+	public float getShearX () {
+		return shearX;
+	}
+
+	public void setShearX (float shearX) {
+		this.shearX = shearX;
+	}
+
+	public float getShearY () {
+		return shearY;
+	}
+
+	public void setShearY (float shearY) {
+		this.shearY = shearY;
+	}
+
+	public float getA () {
+		return a;
+	}
+
+	public float getB () {
+		return b;
+	}
+
+	public float getC () {
+		return c;
+	}
+
+	public float getD () {
+		return d;
+	}
+
+	public float getWorldX () {
+		return worldX;
+	}
+
+	public float getWorldY () {
+		return worldY;
+	}
+
+	public float getWorldRotationX () {
+		return atan2(c, a) * radDeg;
+	}
+
+	public float getWorldRotationY () {
+		return atan2(d, b) * radDeg;
+	}
+
+	/** Returns the magnitude (always positive) of the world scale X. */
+	public float getWorldScaleX () {
+		return (float)Math.sqrt(a * a + c * c);
+	}
+
+	/** Returns the magnitude (always positive) of the world scale Y. */
+	public float getWorldScaleY () {
+		return (float)Math.sqrt(b * b + d * d);
+	}
+
+	public float worldToLocalRotationX () {
+		Bone parent = this.parent;
+		if (parent == null) return arotation;
+		float pa = parent.a, pb = parent.b, pc = parent.c, pd = parent.d, a = this.a, c = this.c;
+		return atan2(pa * c - pc * a, pd * a - pb * c) * radDeg;
+	}
+
+	public float worldToLocalRotationY () {
+		Bone parent = this.parent;
+		if (parent == null) return arotation;
+		float pa = parent.a, pb = parent.b, pc = parent.c, pd = parent.d, b = this.b, d = this.d;
+		return atan2(pa * d - pc * b, pd * b - pb * d) * radDeg;
+	}
+
+	public void rotateWorld (float degrees) {
+		float a = this.a, b = this.b, c = this.c, d = this.d;
+		float cos = cosDeg(degrees), sin = sinDeg(degrees);
+		this.a = cos * a - sin * c;
+		this.b = cos * b - sin * d;
+		this.c = sin * a + cos * c;
+		this.d = sin * b + cos * d;
+		appliedValid = false;
+	}
+
+	/** Computes the individual applied transform values from the world transform. This can be useful to perform processing using
+	 * the applied transform after the world transform has been modified directly (eg, by a constraint).
+	 * <p>
+	 * Some information is ambiguous in the world transform, such as -1,-1 scale versus 180 rotation. */
+	public void updateAppliedTransform () {
+		appliedValid = true;
+		Bone parent = this.parent;
+		if (parent == null) {
+			ax = worldX;
+			ay = worldY;
+			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;
+		}
+	}
+
+	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;
+	}
+
+	public Vector2 worldToLocal (Vector2 world) {
+		float a = this.a, b = this.b, c = this.c, d = this.d;
+		float invDet = 1 / (a * d - b * c);
+		float x = world.x - worldX, y = world.y - worldY;
+		world.x = (x * d * invDet - y * b * invDet);
+		world.y = (y * a * invDet - x * c * invDet);
+		return world;
+	}
+
+	public Vector2 localToWorld (Vector2 local) {
+		float x = local.x, y = local.y;
+		local.x = x * a + y * b + worldX;
+		local.y = x * c + y * d + worldY;
+		return local;
+	}
+
+	public String toString () {
+		return data.name;
+	}
 }

+ 177 - 178
spine-libgdx/spine-libgdx/src/com/esotericsoftware/spine/BoneData.java

@@ -1,181 +1,180 @@
-/******************************************************************************
- * Spine Runtimes Software License
- * Version 2.3
- * 
- * Copyright (c) 2013-2015, Esoteric Software
- * All rights reserved.
- * 
- * You are granted a perpetual, non-exclusive, non-sublicensable and
- * non-transferable license to use, install, execute and perform the Spine
- * Runtimes Software (the "Software") and derivative works solely for personal
- * or internal use. Without the written permission of Esoteric Software (see
- * Section 2 of the Spine Software License Agreement), you may not (a) modify,
- * translate, adapt or otherwise create derivative works, improvements of the
- * Software or develop new applications using the Software or (b) remove,
- * delete, alter or obscure any trademarks or any copyright, trademark, patent
- * or other intellectual property or proprietary rights notices on or in the
- * Software, including any copy thereof. Redistributions in binary or source
- * form must include this license and terms.
- * 
- * THIS SOFTWARE IS PROVIDED BY ESOTERIC SOFTWARE "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 BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
- * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
- * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS;
- * OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
- * WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR
- * OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
- * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+/******************************************************************************
+ * Spine Runtimes Software License v2.5
+ * 
+ * Copyright (c) 2013-2016, Esoteric Software
+ * All rights reserved.
+ * 
+ * You are granted a perpetual, non-exclusive, non-sublicensable, and
+ * non-transferable license to use, install, execute, and perform the Spine
+ * Runtimes software and derivative works solely for personal or internal
+ * use. Without the written permission of Esoteric Software (see Section 2 of
+ * the Spine Software License Agreement), you may not (a) modify, translate,
+ * adapt, or develop new applications using the Spine Runtimes or otherwise
+ * create derivative works or improvements of the Spine Runtimes or (b) remove,
+ * delete, alter, or obscure any trademarks or any copyright, trademark, patent,
+ * or other intellectual property or proprietary rights notices on or in the
+ * Software, including any copy thereof. Redistributions in binary or source
+ * form must include this license and terms.
+ * 
+ * THIS SOFTWARE IS PROVIDED BY ESOTERIC SOFTWARE "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 BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
+ * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES, BUSINESS INTERRUPTION, OR LOSS OF
+ * USE, DATA, OR PROFITS) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER
+ * IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+ * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
+ * POSSIBILITY OF SUCH DAMAGE.
  *****************************************************************************/
 
-package com.esotericsoftware.spine;
-
-import com.badlogic.gdx.graphics.Color;
-
-public class BoneData {
-	final int index;
-	final String name;
-	final BoneData parent;
-	float length;
-	float x, y, rotation, scaleX = 1, scaleY = 1, shearX, shearY;
-	TransformMode transformMode = TransformMode.normal;
-
-	// Nonessential.
-	final Color color = new Color(0.61f, 0.61f, 0.61f, 1);
-
-	/** @param parent May be null. */
-	public BoneData (int index, String name, 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.
-	 * @param parent May be null. */
-	public BoneData (BoneData bone, 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;
-	}
-
-	public int getIndex () {
-		return index;
-	}
-
-	public String getName () {
-		return name;
-	}
-
-	/** @return May be null. */
-	public BoneData getParent () {
-		return parent;
-	}
-
-	public float getLength () {
-		return length;
-	}
-
-	public void setLength (float length) {
-		this.length = length;
-	}
-
-	public float getX () {
-		return x;
-	}
-
-	public void setX (float x) {
-		this.x = x;
-	}
-
-	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;
-	}
-
-	public float getRotation () {
-		return rotation;
-	}
-
-	public void setRotation (float rotation) {
-		this.rotation = rotation;
-	}
-
-	public float getScaleX () {
-		return scaleX;
-	}
-
-	public void setScaleX (float scaleX) {
-		this.scaleX = scaleX;
-	}
-
-	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 float getShearX () {
-		return shearX;
-	}
-
-	public void setShearX (float shearX) {
-		this.shearX = shearX;
-	}
-
-	public float getShearY () {
-		return shearY;
-	}
-
-	public void setShearY (float shearY) {
-		this.shearY = shearY;
-	}
-
-	public TransformMode getTransformMode () {
-		return transformMode;
-	}
-
-	public void setTransformMode (TransformMode transformMode) {
-		this.transformMode = transformMode;
-	}
-
-	public Color getColor () {
-		return color;
-	}
-
-	public String toString () {
-		return name;
-	}
-
-	static public enum TransformMode {
-		normal, onlyTranslation, noRotationOrReflection, noScale, noScaleOrReflection;
-
-		static public final TransformMode[] values = TransformMode.values();
-	}
+package com.esotericsoftware.spine;
+
+import com.badlogic.gdx.graphics.Color;
+
+public class BoneData {
+	final int index;
+	final String name;
+	final BoneData parent;
+	float length;
+	float x, y, rotation, scaleX = 1, scaleY = 1, shearX, shearY;
+	TransformMode transformMode = TransformMode.normal;
+
+	// Nonessential.
+	final Color color = new Color(0.61f, 0.61f, 0.61f, 1);
+
+	/** @param parent May be null. */
+	public BoneData (int index, String name, 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.
+	 * @param parent May be null. */
+	public BoneData (BoneData bone, 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;
+	}
+
+	public int getIndex () {
+		return index;
+	}
+
+	public String getName () {
+		return name;
+	}
+
+	/** @return May be null. */
+	public BoneData getParent () {
+		return parent;
+	}
+
+	public float getLength () {
+		return length;
+	}
+
+	public void setLength (float length) {
+		this.length = length;
+	}
+
+	public float getX () {
+		return x;
+	}
+
+	public void setX (float x) {
+		this.x = x;
+	}
+
+	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;
+	}
+
+	public float getRotation () {
+		return rotation;
+	}
+
+	public void setRotation (float rotation) {
+		this.rotation = rotation;
+	}
+
+	public float getScaleX () {
+		return scaleX;
+	}
+
+	public void setScaleX (float scaleX) {
+		this.scaleX = scaleX;
+	}
+
+	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 float getShearX () {
+		return shearX;
+	}
+
+	public void setShearX (float shearX) {
+		this.shearX = shearX;
+	}
+
+	public float getShearY () {
+		return shearY;
+	}
+
+	public void setShearY (float shearY) {
+		this.shearY = shearY;
+	}
+
+	public TransformMode getTransformMode () {
+		return transformMode;
+	}
+
+	public void setTransformMode (TransformMode transformMode) {
+		this.transformMode = transformMode;
+	}
+
+	public Color getColor () {
+		return color;
+	}
+
+	public String toString () {
+		return name;
+	}
+
+	static public enum TransformMode {
+		normal, onlyTranslation, noRotationOrReflection, noScale, noScaleOrReflection;
+
+		static public final TransformMode[] values = TransformMode.values();
+	}
 }

+ 33 - 4
spine-libgdx/spine-libgdx/src/com/esotericsoftware/spine/Constraint.java

@@ -1,6 +1,35 @@
+/******************************************************************************
+ * Spine Runtimes Software License v2.5
+ * 
+ * Copyright (c) 2013-2016, Esoteric Software
+ * All rights reserved.
+ * 
+ * You are granted a perpetual, non-exclusive, non-sublicensable, and
+ * non-transferable license to use, install, execute, and perform the Spine
+ * Runtimes software and derivative works solely for personal or internal
+ * use. Without the written permission of Esoteric Software (see Section 2 of
+ * the Spine Software License Agreement), you may not (a) modify, translate,
+ * adapt, or develop new applications using the Spine Runtimes or otherwise
+ * create derivative works or improvements of the Spine Runtimes or (b) remove,
+ * delete, alter, or obscure any trademarks or any copyright, trademark, patent,
+ * or other intellectual property or proprietary rights notices on or in the
+ * Software, including any copy thereof. Redistributions in binary or source
+ * form must include this license and terms.
+ * 
+ * THIS SOFTWARE IS PROVIDED BY ESOTERIC SOFTWARE "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 BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
+ * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES, BUSINESS INTERRUPTION, OR LOSS OF
+ * USE, DATA, OR PROFITS) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER
+ * IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+ * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
+ * POSSIBILITY OF SUCH DAMAGE.
+ *****************************************************************************/
 
-package com.esotericsoftware.spine;
-
-public interface Constraint extends Updatable {
-	public int getOrder ();
+package com.esotericsoftware.spine;
+
+public interface Constraint extends Updatable {
+	public int getOrder ();
 }

+ 277 - 278
spine-libgdx/spine-libgdx/src/com/esotericsoftware/spine/IkConstraint.java

@@ -1,281 +1,280 @@
-/******************************************************************************
- * Spine Runtimes Software License
- * Version 2.3
- * 
- * Copyright (c) 2013-2015, Esoteric Software
- * All rights reserved.
- * 
- * You are granted a perpetual, non-exclusive, non-sublicensable and
- * non-transferable license to use, install, execute and perform the Spine
- * Runtimes Software (the "Software") and derivative works solely for personal
- * or internal use. Without the written permission of Esoteric Software (see
- * Section 2 of the Spine Software License Agreement), you may not (a) modify,
- * translate, adapt or otherwise create derivative works, improvements of the
- * Software or develop new applications using the Software or (b) remove,
- * delete, alter or obscure any trademarks or any copyright, trademark, patent
- * or other intellectual property or proprietary rights notices on or in the
- * Software, including any copy thereof. Redistributions in binary or source
- * form must include this license and terms.
- * 
- * THIS SOFTWARE IS PROVIDED BY ESOTERIC SOFTWARE "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 BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
- * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
- * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS;
- * OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
- * WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR
- * OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
- * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+/******************************************************************************
+ * Spine Runtimes Software License v2.5
+ * 
+ * Copyright (c) 2013-2016, Esoteric Software
+ * All rights reserved.
+ * 
+ * You are granted a perpetual, non-exclusive, non-sublicensable, and
+ * non-transferable license to use, install, execute, and perform the Spine
+ * Runtimes software and derivative works solely for personal or internal
+ * use. Without the written permission of Esoteric Software (see Section 2 of
+ * the Spine Software License Agreement), you may not (a) modify, translate,
+ * adapt, or develop new applications using the Spine Runtimes or otherwise
+ * create derivative works or improvements of the Spine Runtimes or (b) remove,
+ * delete, alter, or obscure any trademarks or any copyright, trademark, patent,
+ * or other intellectual property or proprietary rights notices on or in the
+ * Software, including any copy thereof. Redistributions in binary or source
+ * form must include this license and terms.
+ * 
+ * THIS SOFTWARE IS PROVIDED BY ESOTERIC SOFTWARE "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 BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
+ * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES, BUSINESS INTERRUPTION, OR LOSS OF
+ * USE, DATA, OR PROFITS) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER
+ * IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+ * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
+ * POSSIBILITY OF SUCH DAMAGE.
  *****************************************************************************/
 
-package com.esotericsoftware.spine;
-
-import static com.badlogic.gdx.math.MathUtils.*;
-
-import com.badlogic.gdx.utils.Array;
-
-public class IkConstraint implements Constraint {
-	final IkConstraintData data;
-	final Array<Bone> bones;
-	Bone target;
-	float mix = 1;
-	int bendDirection;
-
-	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;
-		bendDirection = data.bendDirection;
-
-		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;
-		bendDirection = constraint.bendDirection;
-	}
-
-	public void apply () {
-		update();
-	}
-
-	public void update () {
-		Bone target = this.target;
-		Array<Bone> bones = this.bones;
-		switch (bones.size) {
-		case 1:
-			apply(bones.first(), target.worldX, target.worldY, mix);
-			break;
-		case 2:
-			apply(bones.first(), bones.get(1), target.worldX, target.worldY, bendDirection, mix);
-			break;
-		}
-	}
-
-	public int getOrder () {
-		return data.order;
-	}
-
-	public Array<Bone> getBones () {
-		return bones;
-	}
-
-	public Bone getTarget () {
-		return target;
-	}
-
-	public void setTarget (Bone target) {
-		this.target = target;
-	}
-
-	public float getMix () {
-		return mix;
-	}
-
-	public void setMix (float mix) {
-		this.mix = mix;
-	}
-
-	public int getBendDirection () {
-		return bendDirection;
-	}
-
-	public void setBendDirection (int bendDirection) {
-		this.bendDirection = bendDirection;
-	}
-
-	public IkConstraintData getData () {
-		return data;
-	}
-
-	public String toString () {
-		return data.name;
-	}
-
-	/** Adjusts the bone rotation so the tip is as close to the target position as possible. The target is specified in the world
-	 * coordinate system. */
-	static public void apply (Bone bone, float targetX, float targetY, float alpha) {
-		if (!bone.appliedValid) bone.updateAppliedTransform();
-		Bone p = bone.parent;
-		float id = 1 / (p.a * p.d - p.b * p.c);
-		float x = targetX - p.worldX, y = targetY - p.worldY;
-		float tx = (x * p.d - y * p.b) * id - bone.ax, ty = (y * p.a - x * p.c) * id - bone.ay;
-		float rotationIK = atan2(ty, tx) * radDeg - bone.ashearX - bone.arotation;
-		if (bone.ascaleX < 0) rotationIK += 180;
-		if (rotationIK > 180)
-			rotationIK -= 360;
-		else if (rotationIK < -180) rotationIK += 360;
-		bone.updateWorldTransform(bone.ax, bone.ay, bone.arotation + rotationIK * alpha, bone.ascaleX, bone.ascaleY, bone.ashearX,
-			bone.ashearY);
-	}
-
-	/** Adjusts the parent and child bone rotations so the tip of the child is as close to the target position as possible. 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, float alpha) {
-		if (alpha == 0) {
-			child.updateWorldTransform();
-			return;
-		}
-		if (!parent.appliedValid) parent.updateAppliedTransform();
-		if (!child.appliedValid) child.updateAppliedTransform();
-		float px = parent.ax, py = parent.ay, psx = parent.ascaleX, psy = parent.ascaleY, 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) {
-			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 = targetX - pp.worldX, y = targetY - pp.worldY;
-		float tx = (x * d - y * b) * id - px, ty = (y * a - x * c) * id - py;
-		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;
-		outer:
-		if (u) {
-			l2 *= psx;
-			float cos = (tx * tx + ty * ty - l1 * l1 - l2 * l2) / (2 * l1 * l2);
-			if (cos < -1)
-				cos = -1;
-			else if (cos > 1) cos = 1;
-			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, dd = tx * tx + ty * ty, 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) / 2;
-				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 = 0, minDist = Float.MAX_VALUE, minX = 0, minY = 0;
-			float maxAngle = 0, maxDist = 0, maxX = 0, maxY = 0;
-			x = l1 + a;
-			d = x * x;
-			if (d > maxDist) {
-				maxAngle = 0;
-				maxDist = d;
-				maxX = x;
-			}
-			x = l1 - a;
-			d = x * x;
-			if (d < minDist) {
-				minAngle = PI;
-				minDist = d;
-				minX = x;
-			}
-			float angle = (float)Math.acos(-a * l1 / (aa - bb));
-			x = a * cos(angle) + l1;
-			y = b * sin(angle);
-			d = x * x + y * y;
-			if (d < minDist) {
-				minAngle = angle;
-				minDist = d;
-				minX = x;
-				minY = y;
-			}
-			if (d > maxDist) {
-				maxAngle = angle;
-				maxDist = d;
-				maxX = x;
-				maxY = y;
-			}
-			if (dd <= (minDist + maxDist) / 2) {
-				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, parent.ascaleX, parent.ascaleY, 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);
-	}
+package com.esotericsoftware.spine;
+
+import static com.badlogic.gdx.math.MathUtils.*;
+
+import com.badlogic.gdx.utils.Array;
+
+public class IkConstraint implements Constraint {
+	final IkConstraintData data;
+	final Array<Bone> bones;
+	Bone target;
+	float mix = 1;
+	int bendDirection;
+
+	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;
+		bendDirection = data.bendDirection;
+
+		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;
+		bendDirection = constraint.bendDirection;
+	}
+
+	public void apply () {
+		update();
+	}
+
+	public void update () {
+		Bone target = this.target;
+		Array<Bone> bones = this.bones;
+		switch (bones.size) {
+		case 1:
+			apply(bones.first(), target.worldX, target.worldY, mix);
+			break;
+		case 2:
+			apply(bones.first(), bones.get(1), target.worldX, target.worldY, bendDirection, mix);
+			break;
+		}
+	}
+
+	public int getOrder () {
+		return data.order;
+	}
+
+	public Array<Bone> getBones () {
+		return bones;
+	}
+
+	public Bone getTarget () {
+		return target;
+	}
+
+	public void setTarget (Bone target) {
+		this.target = target;
+	}
+
+	public float getMix () {
+		return mix;
+	}
+
+	public void setMix (float mix) {
+		this.mix = mix;
+	}
+
+	public int getBendDirection () {
+		return bendDirection;
+	}
+
+	public void setBendDirection (int bendDirection) {
+		this.bendDirection = bendDirection;
+	}
+
+	public IkConstraintData getData () {
+		return data;
+	}
+
+	public String toString () {
+		return data.name;
+	}
+
+	/** Adjusts the bone rotation so the tip is as close to the target position as possible. The target is specified in the world
+	 * coordinate system. */
+	static public void apply (Bone bone, float targetX, float targetY, float alpha) {
+		if (!bone.appliedValid) bone.updateAppliedTransform();
+		Bone p = bone.parent;
+		float id = 1 / (p.a * p.d - p.b * p.c);
+		float x = targetX - p.worldX, y = targetY - p.worldY;
+		float tx = (x * p.d - y * p.b) * id - bone.ax, ty = (y * p.a - x * p.c) * id - bone.ay;
+		float rotationIK = atan2(ty, tx) * radDeg - bone.ashearX - bone.arotation;
+		if (bone.ascaleX < 0) rotationIK += 180;
+		if (rotationIK > 180)
+			rotationIK -= 360;
+		else if (rotationIK < -180) rotationIK += 360;
+		bone.updateWorldTransform(bone.ax, bone.ay, bone.arotation + rotationIK * alpha, bone.ascaleX, bone.ascaleY, bone.ashearX,
+			bone.ashearY);
+	}
+
+	/** Adjusts the parent and child bone rotations so the tip of the child is as close to the target position as possible. 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, float alpha) {
+		if (alpha == 0) {
+			child.updateWorldTransform();
+			return;
+		}
+		if (!parent.appliedValid) parent.updateAppliedTransform();
+		if (!child.appliedValid) child.updateAppliedTransform();
+		float px = parent.ax, py = parent.ay, psx = parent.ascaleX, psy = parent.ascaleY, 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) {
+			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 = targetX - pp.worldX, y = targetY - pp.worldY;
+		float tx = (x * d - y * b) * id - px, ty = (y * a - x * c) * id - py;
+		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;
+		outer:
+		if (u) {
+			l2 *= psx;
+			float cos = (tx * tx + ty * ty - l1 * l1 - l2 * l2) / (2 * l1 * l2);
+			if (cos < -1)
+				cos = -1;
+			else if (cos > 1) cos = 1;
+			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, dd = tx * tx + ty * ty, 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) / 2;
+				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 = 0, minDist = Float.MAX_VALUE, minX = 0, minY = 0;
+			float maxAngle = 0, maxDist = 0, maxX = 0, maxY = 0;
+			x = l1 + a;
+			d = x * x;
+			if (d > maxDist) {
+				maxAngle = 0;
+				maxDist = d;
+				maxX = x;
+			}
+			x = l1 - a;
+			d = x * x;
+			if (d < minDist) {
+				minAngle = PI;
+				minDist = d;
+				minX = x;
+			}
+			float angle = (float)Math.acos(-a * l1 / (aa - bb));
+			x = a * cos(angle) + l1;
+			y = b * sin(angle);
+			d = x * x + y * y;
+			if (d < minDist) {
+				minAngle = angle;
+				minDist = d;
+				minX = x;
+				minY = y;
+			}
+			if (d > maxDist) {
+				maxAngle = angle;
+				maxDist = d;
+				maxX = x;
+				maxY = y;
+			}
+			if (dd <= (minDist + maxDist) / 2) {
+				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, parent.ascaleX, parent.ascaleY, 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);
+	}
 }

+ 89 - 90
spine-libgdx/spine-libgdx/src/com/esotericsoftware/spine/IkConstraintData.java

@@ -1,93 +1,92 @@
-/******************************************************************************
- * Spine Runtimes Software License
- * Version 2.3
- * 
- * Copyright (c) 2013-2015, Esoteric Software
- * All rights reserved.
- * 
- * You are granted a perpetual, non-exclusive, non-sublicensable and
- * non-transferable license to use, install, execute and perform the Spine
- * Runtimes Software (the "Software") and derivative works solely for personal
- * or internal use. Without the written permission of Esoteric Software (see
- * Section 2 of the Spine Software License Agreement), you may not (a) modify,
- * translate, adapt or otherwise create derivative works, improvements of the
- * Software or develop new applications using the Software or (b) remove,
- * delete, alter or obscure any trademarks or any copyright, trademark, patent
- * or other intellectual property or proprietary rights notices on or in the
- * Software, including any copy thereof. Redistributions in binary or source
- * form must include this license and terms.
- * 
- * THIS SOFTWARE IS PROVIDED BY ESOTERIC SOFTWARE "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 BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
- * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
- * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS;
- * OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
- * WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR
- * OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
- * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+/******************************************************************************
+ * Spine Runtimes Software License v2.5
+ * 
+ * Copyright (c) 2013-2016, Esoteric Software
+ * All rights reserved.
+ * 
+ * You are granted a perpetual, non-exclusive, non-sublicensable, and
+ * non-transferable license to use, install, execute, and perform the Spine
+ * Runtimes software and derivative works solely for personal or internal
+ * use. Without the written permission of Esoteric Software (see Section 2 of
+ * the Spine Software License Agreement), you may not (a) modify, translate,
+ * adapt, or develop new applications using the Spine Runtimes or otherwise
+ * create derivative works or improvements of the Spine Runtimes or (b) remove,
+ * delete, alter, or obscure any trademarks or any copyright, trademark, patent,
+ * or other intellectual property or proprietary rights notices on or in the
+ * Software, including any copy thereof. Redistributions in binary or source
+ * form must include this license and terms.
+ * 
+ * THIS SOFTWARE IS PROVIDED BY ESOTERIC SOFTWARE "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 BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
+ * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES, BUSINESS INTERRUPTION, OR LOSS OF
+ * USE, DATA, OR PROFITS) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER
+ * IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+ * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
+ * POSSIBILITY OF SUCH DAMAGE.
  *****************************************************************************/
 
-package com.esotericsoftware.spine;
-
-import com.badlogic.gdx.utils.Array;
-
-public class IkConstraintData {
-	final String name;
-	int order;
-	final Array<BoneData> bones = new Array();
-	BoneData target;
-	int bendDirection = 1;
-	float mix = 1;
-
-	public IkConstraintData (String name) {
-		if (name == null) throw new IllegalArgumentException("name cannot be null.");
-		this.name = name;
-	}
-
-	public String getName () {
-		return name;
-	}
-
-	public int getOrder () {
-		return order;
-	}
-
-	public void setOrder (int order) {
-		this.order = order;
-	}
-
-	public Array<BoneData> getBones () {
-		return bones;
-	}
-
-	public BoneData getTarget () {
-		return target;
-	}
-
-	public void setTarget (BoneData target) {
-		if (target == null) throw new IllegalArgumentException("target cannot be null.");
-		this.target = target;
-	}
-
-	public int getBendDirection () {
-		return bendDirection;
-	}
-
-	public void setBendDirection (int bendDirection) {
-		this.bendDirection = bendDirection;
-	}
-
-	public float getMix () {
-		return mix;
-	}
-
-	public void setMix (float mix) {
-		this.mix = mix;
-	}
-
-	public String toString () {
-		return name;
-	}
+package com.esotericsoftware.spine;
+
+import com.badlogic.gdx.utils.Array;
+
+public class IkConstraintData {
+	final String name;
+	int order;
+	final Array<BoneData> bones = new Array();
+	BoneData target;
+	int bendDirection = 1;
+	float mix = 1;
+
+	public IkConstraintData (String name) {
+		if (name == null) throw new IllegalArgumentException("name cannot be null.");
+		this.name = name;
+	}
+
+	public String getName () {
+		return name;
+	}
+
+	public int getOrder () {
+		return order;
+	}
+
+	public void setOrder (int order) {
+		this.order = order;
+	}
+
+	public Array<BoneData> getBones () {
+		return bones;
+	}
+
+	public BoneData getTarget () {
+		return target;
+	}
+
+	public void setTarget (BoneData target) {
+		if (target == null) throw new IllegalArgumentException("target cannot be null.");
+		this.target = target;
+	}
+
+	public int getBendDirection () {
+		return bendDirection;
+	}
+
+	public void setBendDirection (int bendDirection) {
+		this.bendDirection = bendDirection;
+	}
+
+	public float getMix () {
+		return mix;
+	}
+
+	public void setMix (float mix) {
+		this.mix = mix;
+	}
+
+	public String toString () {
+		return name;
+	}
 }

+ 467 - 438
spine-libgdx/spine-libgdx/src/com/esotericsoftware/spine/PathConstraint.java

@@ -1,439 +1,468 @@
-
-package com.esotericsoftware.spine;
-
-import static com.badlogic.gdx.math.MathUtils.*;
-
-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;
-
-public class PathConstraint implements Constraint {
-	static private final int NONE = -1, BEFORE = -2, AFTER = -3;
-
-	final PathConstraintData data;
-	final Array<Bone> bones;
-	Slot target;
-	float position, spacing, rotateMix, translateMix;
-
-	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;
-		rotateMix = data.rotateMix;
-		translateMix = data.translateMix;
-	}
-
-	/** 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;
-		rotateMix = constraint.rotateMix;
-		translateMix = constraint.translateMix;
-	}
-
-	public void apply () {
-		update();
-	}
-
-	@SuppressWarnings("null")
-	public void update () {
-		Attachment attachment = target.attachment;
-		if (!(attachment instanceof PathAttachment)) return;
-
-		float rotateMix = this.rotateMix, translateMix = this.translateMix;
-		boolean translate = translateMix > 0, rotate = rotateMix > 0;
-		if (!translate && !rotate) return;
-
-		PathConstraintData data = this.data;
-		SpacingMode spacingMode = data.spacingMode;
-		boolean lengthSpacing = spacingMode == SpacingMode.length;
-		RotateMode rotateMode = data.rotateMode;
-		boolean tangents = rotateMode == RotateMode.tangent, scale = 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 = null;
-		float spacing = this.spacing;
-		if (scale || lengthSpacing) {
-			if (scale) lengths = this.lengths.setSize(boneCount);
-			for (int i = 0, n = spacesCount - 1; i < n;) {
-				Bone bone = (Bone)bones[i];
-				float length = bone.data.length, x = length * bone.a, y = length * bone.c;
-				length = (float)Math.sqrt(x * x + y * y);
-				if (scale) lengths[i] = length;
-				spaces[++i] = lengthSpacing ? Math.max(0, length + spacing) : spacing;
-			}
-		} else {
-			for (int i = 1; i < spacesCount; i++)
-				spaces[i] = spacing;
-		}
-
-		float[] positions = computeWorldPositions((PathAttachment)attachment, spacesCount, tangents,
-			data.positionMode == PositionMode.percent, spacingMode == SpacingMode.percent);
-		float boneX = positions[0], boneY = positions[1], offsetRotation = data.offsetRotation;
-		boolean tip = rotateMode == RotateMode.chain && offsetRotation == 0;
-		for (int i = 0, p = 3; i < boneCount; i++, p += 3) {
-			Bone bone = (Bone)bones[i];
-			bone.worldX += (boneX - bone.worldX) * translateMix;
-			bone.worldY += (boneY - bone.worldY) * translateMix;
-			float x = positions[p], y = positions[p + 1], dx = x - boneX, dy = y - boneY;
-			if (scale) {
-				float length = lengths[i];
-				if (length != 0) {
-					float s = ((float)Math.sqrt(dx * dx + dy * dy) / length - 1) * rotateMix + 1;
-					bone.a *= s;
-					bone.c *= s;
-				}
-			}
-			boneX = x;
-			boneY = y;
-			if (rotate) {
-				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] == 0)
-					r = positions[p + 2];
-				else
-					r = atan2(dy, dx);
-				r -= atan2(c, a) - offsetRotation * degRad;
-				if (tip) {
-					cos = cos(r);
-					sin = sin(r);
-					float length = bone.data.length;
-					boneX += (length * (cos * a - sin * c) - dx) * rotateMix;
-					boneY += (length * (sin * a + cos * c) - dy) * rotateMix;
-				}
-				if (r > PI)
-					r -= PI2;
-				else if (r < -PI) //
-					r += PI2;
-				r *= rotateMix;
-				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;
-			}
-			bone.appliedValid = false;
-		}
-	}
-
-	float[] computeWorldPositions (PathAttachment path, int spacesCount, boolean tangents, boolean percentPosition,
-		boolean percentSpacing) {
-		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 (percentPosition) position *= pathLength;
-			if (percentSpacing) {
-				for (int i = 0; i < spacesCount; i++)
-					spaces[i] *= pathLength;
-			}
-			world = this.world.setSize(8);
-			for (int i = 0, o = 0, curve = 0; i < spacesCount; i++, o += 3) {
-				float space = spaces[i];
-				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);
-					}
-					addBeforePosition(p, world, 0, out, o);
-					continue;
-				} else if (p > pathLength) {
-					if (prevCurve != AFTER) {
-						prevCurve = AFTER;
-						path.computeWorldVertices(target, verticesLength - 6, 4, world, 0);
-					}
-					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);
-						path.computeWorldVertices(target, 0, 4, world, 4);
-					} else
-						path.computeWorldVertices(target, curve * 6 + 2, 8, world, 0);
-				}
-				addCurvePosition(p, world[0], world[1], world[2], world[3], world[4], world[5], world[6], world[7], out, o,
-					tangents || (i > 0 && space == 0));
-			}
-			return out;
-		}
-
-		// World vertices.
-		if (closed) {
-			verticesLength += 2;
-			world = this.world.setSize(verticesLength);
-			path.computeWorldVertices(target, 2, verticesLength - 4, world, 0);
-			path.computeWorldVertices(target, 0, 2, world, verticesLength - 4);
-			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);
-		}
-
-		// 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 (percentPosition) position *= pathLength;
-		if (percentSpacing) {
-			for (int i = 0; i < spacesCount; i++)
-				spaces[i] *= pathLength;
-		}
-
-		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];
-			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 == 0));
-		}
-		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 = atan2(dy, dx);
-		out[o] = x1 + p * cos(r);
-		out[o + 1] = y1 + p * 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 = atan2(dy, dx);
-		out[o] = x1 + p * cos(r);
-		out[o + 1] = y1 + p * 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 == 0 || Float.isNaN(p)) p = 0.0001f;
-		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) out[o + 2] = atan2(y - (y1 * uu + cy1 * ut * 2 + cy2 * tt), x - (x1 * uu + cx1 * ut * 2 + cx2 * tt));
-	}
-
-	public int getOrder () {
-		return data.order;
-	}
-
-	public float getPosition () {
-		return position;
-	}
-
-	public void setPosition (float position) {
-		this.position = position;
-	}
-
-	public float getSpacing () {
-		return spacing;
-	}
-
-	public void setSpacing (float spacing) {
-		this.spacing = spacing;
-	}
-
-	public float getRotateMix () {
-		return rotateMix;
-	}
-
-	public void setRotateMix (float rotateMix) {
-		this.rotateMix = rotateMix;
-	}
-
-	public float getTranslateMix () {
-		return translateMix;
-	}
-
-	public void setTranslateMix (float translateMix) {
-		this.translateMix = translateMix;
-	}
-
-	public Array<Bone> getBones () {
-		return bones;
-	}
-
-	public Slot getTarget () {
-		return target;
-	}
-
-	public void setTarget (Slot target) {
-		this.target = target;
-	}
-
-	public PathConstraintData getData () {
-		return data;
-	}
-
-	public String toString () {
-		return data.name;
-	}
+/******************************************************************************
+ * Spine Runtimes Software License v2.5
+ * 
+ * Copyright (c) 2013-2016, Esoteric Software
+ * All rights reserved.
+ * 
+ * You are granted a perpetual, non-exclusive, non-sublicensable, and
+ * non-transferable license to use, install, execute, and perform the Spine
+ * Runtimes software and derivative works solely for personal or internal
+ * use. Without the written permission of Esoteric Software (see Section 2 of
+ * the Spine Software License Agreement), you may not (a) modify, translate,
+ * adapt, or develop new applications using the Spine Runtimes or otherwise
+ * create derivative works or improvements of the Spine Runtimes or (b) remove,
+ * delete, alter, or obscure any trademarks or any copyright, trademark, patent,
+ * or other intellectual property or proprietary rights notices on or in the
+ * Software, including any copy thereof. Redistributions in binary or source
+ * form must include this license and terms.
+ * 
+ * THIS SOFTWARE IS PROVIDED BY ESOTERIC SOFTWARE "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 BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
+ * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES, BUSINESS INTERRUPTION, OR LOSS OF
+ * USE, DATA, OR PROFITS) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER
+ * IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+ * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
+ * POSSIBILITY OF SUCH DAMAGE.
+ *****************************************************************************/
+
+package com.esotericsoftware.spine;
+
+import static com.badlogic.gdx.math.MathUtils.*;
+
+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;
+
+public class PathConstraint implements Constraint {
+	static private final int NONE = -1, BEFORE = -2, AFTER = -3;
+
+	final PathConstraintData data;
+	final Array<Bone> bones;
+	Slot target;
+	float position, spacing, rotateMix, translateMix;
+
+	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;
+		rotateMix = data.rotateMix;
+		translateMix = data.translateMix;
+	}
+
+	/** 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;
+		rotateMix = constraint.rotateMix;
+		translateMix = constraint.translateMix;
+	}
+
+	public void apply () {
+		update();
+	}
+
+	@SuppressWarnings("null")
+	public void update () {
+		Attachment attachment = target.attachment;
+		if (!(attachment instanceof PathAttachment)) return;
+
+		float rotateMix = this.rotateMix, translateMix = this.translateMix;
+		boolean translate = translateMix > 0, rotate = rotateMix > 0;
+		if (!translate && !rotate) return;
+
+		PathConstraintData data = this.data;
+		SpacingMode spacingMode = data.spacingMode;
+		boolean lengthSpacing = spacingMode == SpacingMode.length;
+		RotateMode rotateMode = data.rotateMode;
+		boolean tangents = rotateMode == RotateMode.tangent, scale = 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 = null;
+		float spacing = this.spacing;
+		if (scale || lengthSpacing) {
+			if (scale) lengths = this.lengths.setSize(boneCount);
+			for (int i = 0, n = spacesCount - 1; i < n;) {
+				Bone bone = (Bone)bones[i];
+				float length = bone.data.length, x = length * bone.a, y = length * bone.c;
+				length = (float)Math.sqrt(x * x + y * y);
+				if (scale) lengths[i] = length;
+				spaces[++i] = lengthSpacing ? Math.max(0, length + spacing) : spacing;
+			}
+		} else {
+			for (int i = 1; i < spacesCount; i++)
+				spaces[i] = spacing;
+		}
+
+		float[] positions = computeWorldPositions((PathAttachment)attachment, spacesCount, tangents,
+			data.positionMode == PositionMode.percent, spacingMode == SpacingMode.percent);
+		float boneX = positions[0], boneY = positions[1], offsetRotation = data.offsetRotation;
+		boolean tip = rotateMode == RotateMode.chain && offsetRotation == 0;
+		for (int i = 0, p = 3; i < boneCount; i++, p += 3) {
+			Bone bone = (Bone)bones[i];
+			bone.worldX += (boneX - bone.worldX) * translateMix;
+			bone.worldY += (boneY - bone.worldY) * translateMix;
+			float x = positions[p], y = positions[p + 1], dx = x - boneX, dy = y - boneY;
+			if (scale) {
+				float length = lengths[i];
+				if (length != 0) {
+					float s = ((float)Math.sqrt(dx * dx + dy * dy) / length - 1) * rotateMix + 1;
+					bone.a *= s;
+					bone.c *= s;
+				}
+			}
+			boneX = x;
+			boneY = y;
+			if (rotate) {
+				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] == 0)
+					r = positions[p + 2];
+				else
+					r = atan2(dy, dx);
+				r -= atan2(c, a) - offsetRotation * degRad;
+				if (tip) {
+					cos = cos(r);
+					sin = sin(r);
+					float length = bone.data.length;
+					boneX += (length * (cos * a - sin * c) - dx) * rotateMix;
+					boneY += (length * (sin * a + cos * c) - dy) * rotateMix;
+				}
+				if (r > PI)
+					r -= PI2;
+				else if (r < -PI) //
+					r += PI2;
+				r *= rotateMix;
+				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;
+			}
+			bone.appliedValid = false;
+		}
+	}
+
+	float[] computeWorldPositions (PathAttachment path, int spacesCount, boolean tangents, boolean percentPosition,
+		boolean percentSpacing) {
+		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 (percentPosition) position *= pathLength;
+			if (percentSpacing) {
+				for (int i = 0; i < spacesCount; i++)
+					spaces[i] *= pathLength;
+			}
+			world = this.world.setSize(8);
+			for (int i = 0, o = 0, curve = 0; i < spacesCount; i++, o += 3) {
+				float space = spaces[i];
+				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);
+					}
+					addBeforePosition(p, world, 0, out, o);
+					continue;
+				} else if (p > pathLength) {
+					if (prevCurve != AFTER) {
+						prevCurve = AFTER;
+						path.computeWorldVertices(target, verticesLength - 6, 4, world, 0);
+					}
+					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);
+						path.computeWorldVertices(target, 0, 4, world, 4);
+					} else
+						path.computeWorldVertices(target, curve * 6 + 2, 8, world, 0);
+				}
+				addCurvePosition(p, world[0], world[1], world[2], world[3], world[4], world[5], world[6], world[7], out, o,
+					tangents || (i > 0 && space == 0));
+			}
+			return out;
+		}
+
+		// World vertices.
+		if (closed) {
+			verticesLength += 2;
+			world = this.world.setSize(verticesLength);
+			path.computeWorldVertices(target, 2, verticesLength - 4, world, 0);
+			path.computeWorldVertices(target, 0, 2, world, verticesLength - 4);
+			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);
+		}
+
+		// 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 (percentPosition) position *= pathLength;
+		if (percentSpacing) {
+			for (int i = 0; i < spacesCount; i++)
+				spaces[i] *= pathLength;
+		}
+
+		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];
+			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 == 0));
+		}
+		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 = atan2(dy, dx);
+		out[o] = x1 + p * cos(r);
+		out[o + 1] = y1 + p * 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 = atan2(dy, dx);
+		out[o] = x1 + p * cos(r);
+		out[o + 1] = y1 + p * 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 == 0 || Float.isNaN(p)) p = 0.0001f;
+		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) out[o + 2] = atan2(y - (y1 * uu + cy1 * ut * 2 + cy2 * tt), x - (x1 * uu + cx1 * ut * 2 + cx2 * tt));
+	}
+
+	public int getOrder () {
+		return data.order;
+	}
+
+	public float getPosition () {
+		return position;
+	}
+
+	public void setPosition (float position) {
+		this.position = position;
+	}
+
+	public float getSpacing () {
+		return spacing;
+	}
+
+	public void setSpacing (float spacing) {
+		this.spacing = spacing;
+	}
+
+	public float getRotateMix () {
+		return rotateMix;
+	}
+
+	public void setRotateMix (float rotateMix) {
+		this.rotateMix = rotateMix;
+	}
+
+	public float getTranslateMix () {
+		return translateMix;
+	}
+
+	public void setTranslateMix (float translateMix) {
+		this.translateMix = translateMix;
+	}
+
+	public Array<Bone> getBones () {
+		return bones;
+	}
+
+	public Slot getTarget () {
+		return target;
+	}
+
+	public void setTarget (Slot target) {
+		this.target = target;
+	}
+
+	public PathConstraintData getData () {
+		return data;
+	}
+
+	public String toString () {
+		return data.name;
+	}
 }

+ 159 - 130
spine-libgdx/spine-libgdx/src/com/esotericsoftware/spine/PathConstraintData.java

@@ -1,131 +1,160 @@
-
-package com.esotericsoftware.spine;
-
-import com.badlogic.gdx.utils.Array;
-
-public class PathConstraintData {
-	final String name;
-	int order;
-	final Array<BoneData> bones = new Array();
-	SlotData target;
-	PositionMode positionMode;
-	SpacingMode spacingMode;
-	RotateMode rotateMode;
-	float offsetRotation;
-	float position, spacing, rotateMix, translateMix;
-
-	public PathConstraintData (String name) {
-		if (name == null) throw new IllegalArgumentException("name cannot be null.");
-		this.name = name;
-	}
-
-	public String getName () {
-		return name;
-	}
-
-	public int getOrder () {
-		return order;
-	}
-
-	public void setOrder (int order) {
-		this.order = order;
-	}
-
-	public Array<BoneData> getBones () {
-		return bones;
-	}
-
-	public SlotData getTarget () {
-		return target;
-	}
-
-	public void setTarget (SlotData target) {
-		this.target = target;
-	}
-
-	public PositionMode getPositionMode () {
-		return positionMode;
-	}
-
-	public void setPositionMode (PositionMode positionMode) {
-		this.positionMode = positionMode;
-	}
-
-	public SpacingMode getSpacingMode () {
-		return spacingMode;
-	}
-
-	public void setSpacingMode (SpacingMode spacingMode) {
-		this.spacingMode = spacingMode;
-	}
-
-	public RotateMode getRotateMode () {
-		return rotateMode;
-	}
-
-	public void setRotateMode (RotateMode rotateMode) {
-		this.rotateMode = rotateMode;
-	}
-
-	public float getOffsetRotation () {
-		return offsetRotation;
-	}
-
-	public void setOffsetRotation (float offsetRotation) {
-		this.offsetRotation = offsetRotation;
-	}
-
-	public float getPosition () {
-		return position;
-	}
-
-	public void setPosition (float position) {
-		this.position = position;
-	}
-
-	public float getSpacing () {
-		return spacing;
-	}
-
-	public void setSpacing (float spacing) {
-		this.spacing = spacing;
-	}
-
-	public float getRotateMix () {
-		return rotateMix;
-	}
-
-	public void setRotateMix (float rotateMix) {
-		this.rotateMix = rotateMix;
-	}
-
-	public float getTranslateMix () {
-		return translateMix;
-	}
-
-	public void setTranslateMix (float translateMix) {
-		this.translateMix = translateMix;
-	}
-
-	public String toString () {
-		return name;
-	}
-
-	static public enum PositionMode {
-		fixed, percent;
-
-		static public final PositionMode[] values = PositionMode.values();
-	}
-
-	static public enum SpacingMode {
-		length, fixed, percent;
-
-		static public final SpacingMode[] values = SpacingMode.values();
-	}
-
-	static public enum RotateMode {
-		tangent, chain, chainScale;
-
-		static public final RotateMode[] values = RotateMode.values();
-	}
+/******************************************************************************
+ * Spine Runtimes Software License v2.5
+ * 
+ * Copyright (c) 2013-2016, Esoteric Software
+ * All rights reserved.
+ * 
+ * You are granted a perpetual, non-exclusive, non-sublicensable, and
+ * non-transferable license to use, install, execute, and perform the Spine
+ * Runtimes software and derivative works solely for personal or internal
+ * use. Without the written permission of Esoteric Software (see Section 2 of
+ * the Spine Software License Agreement), you may not (a) modify, translate,
+ * adapt, or develop new applications using the Spine Runtimes or otherwise
+ * create derivative works or improvements of the Spine Runtimes or (b) remove,
+ * delete, alter, or obscure any trademarks or any copyright, trademark, patent,
+ * or other intellectual property or proprietary rights notices on or in the
+ * Software, including any copy thereof. Redistributions in binary or source
+ * form must include this license and terms.
+ * 
+ * THIS SOFTWARE IS PROVIDED BY ESOTERIC SOFTWARE "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 BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
+ * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES, BUSINESS INTERRUPTION, OR LOSS OF
+ * USE, DATA, OR PROFITS) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER
+ * IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+ * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
+ * POSSIBILITY OF SUCH DAMAGE.
+ *****************************************************************************/
+
+package com.esotericsoftware.spine;
+
+import com.badlogic.gdx.utils.Array;
+
+public class PathConstraintData {
+	final String name;
+	int order;
+	final Array<BoneData> bones = new Array();
+	SlotData target;
+	PositionMode positionMode;
+	SpacingMode spacingMode;
+	RotateMode rotateMode;
+	float offsetRotation;
+	float position, spacing, rotateMix, translateMix;
+
+	public PathConstraintData (String name) {
+		if (name == null) throw new IllegalArgumentException("name cannot be null.");
+		this.name = name;
+	}
+
+	public String getName () {
+		return name;
+	}
+
+	public int getOrder () {
+		return order;
+	}
+
+	public void setOrder (int order) {
+		this.order = order;
+	}
+
+	public Array<BoneData> getBones () {
+		return bones;
+	}
+
+	public SlotData getTarget () {
+		return target;
+	}
+
+	public void setTarget (SlotData target) {
+		this.target = target;
+	}
+
+	public PositionMode getPositionMode () {
+		return positionMode;
+	}
+
+	public void setPositionMode (PositionMode positionMode) {
+		this.positionMode = positionMode;
+	}
+
+	public SpacingMode getSpacingMode () {
+		return spacingMode;
+	}
+
+	public void setSpacingMode (SpacingMode spacingMode) {
+		this.spacingMode = spacingMode;
+	}
+
+	public RotateMode getRotateMode () {
+		return rotateMode;
+	}
+
+	public void setRotateMode (RotateMode rotateMode) {
+		this.rotateMode = rotateMode;
+	}
+
+	public float getOffsetRotation () {
+		return offsetRotation;
+	}
+
+	public void setOffsetRotation (float offsetRotation) {
+		this.offsetRotation = offsetRotation;
+	}
+
+	public float getPosition () {
+		return position;
+	}
+
+	public void setPosition (float position) {
+		this.position = position;
+	}
+
+	public float getSpacing () {
+		return spacing;
+	}
+
+	public void setSpacing (float spacing) {
+		this.spacing = spacing;
+	}
+
+	public float getRotateMix () {
+		return rotateMix;
+	}
+
+	public void setRotateMix (float rotateMix) {
+		this.rotateMix = rotateMix;
+	}
+
+	public float getTranslateMix () {
+		return translateMix;
+	}
+
+	public void setTranslateMix (float translateMix) {
+		this.translateMix = translateMix;
+	}
+
+	public String toString () {
+		return name;
+	}
+
+	static public enum PositionMode {
+		fixed, percent;
+
+		static public final PositionMode[] values = PositionMode.values();
+	}
+
+	static public enum SpacingMode {
+		length, fixed, percent;
+
+		static public final SpacingMode[] values = SpacingMode.values();
+	}
+
+	static public enum RotateMode {
+		tangent, chain, chainScale;
+
+		static public final RotateMode[] values = RotateMode.values();
+	}
 }

+ 643 - 644
spine-libgdx/spine-libgdx/src/com/esotericsoftware/spine/Skeleton.java

@@ -1,647 +1,646 @@
-/******************************************************************************
- * Spine Runtimes Software License
- * Version 2.3
- * 
- * Copyright (c) 2013-2015, Esoteric Software
- * All rights reserved.
- * 
- * You are granted a perpetual, non-exclusive, non-sublicensable and
- * non-transferable license to use, install, execute and perform the Spine
- * Runtimes Software (the "Software") and derivative works solely for personal
- * or internal use. Without the written permission of Esoteric Software (see
- * Section 2 of the Spine Software License Agreement), you may not (a) modify,
- * translate, adapt or otherwise create derivative works, improvements of the
- * Software or develop new applications using the Software or (b) remove,
- * delete, alter or obscure any trademarks or any copyright, trademark, patent
- * or other intellectual property or proprietary rights notices on or in the
- * Software, including any copy thereof. Redistributions in binary or source
- * form must include this license and terms.
- * 
- * THIS SOFTWARE IS PROVIDED BY ESOTERIC SOFTWARE "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 BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
- * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
- * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS;
- * OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
- * WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR
- * OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
- * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+/******************************************************************************
+ * Spine Runtimes Software License v2.5
+ * 
+ * Copyright (c) 2013-2016, Esoteric Software
+ * All rights reserved.
+ * 
+ * You are granted a perpetual, non-exclusive, non-sublicensable, and
+ * non-transferable license to use, install, execute, and perform the Spine
+ * Runtimes software and derivative works solely for personal or internal
+ * use. Without the written permission of Esoteric Software (see Section 2 of
+ * the Spine Software License Agreement), you may not (a) modify, translate,
+ * adapt, or develop new applications using the Spine Runtimes or otherwise
+ * create derivative works or improvements of the Spine Runtimes or (b) remove,
+ * delete, alter, or obscure any trademarks or any copyright, trademark, patent,
+ * or other intellectual property or proprietary rights notices on or in the
+ * Software, including any copy thereof. Redistributions in binary or source
+ * form must include this license and terms.
+ * 
+ * THIS SOFTWARE IS PROVIDED BY ESOTERIC SOFTWARE "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 BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
+ * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES, BUSINESS INTERRUPTION, OR LOSS OF
+ * USE, DATA, OR PROFITS) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER
+ * IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+ * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
+ * POSSIBILITY OF SUCH DAMAGE.
  *****************************************************************************/
 
-package com.esotericsoftware.spine;
-
-import com.badlogic.gdx.graphics.Color;
-import com.badlogic.gdx.math.Vector2;
-import com.badlogic.gdx.utils.Array;
-import com.badlogic.gdx.utils.ObjectMap.Entry;
-import com.esotericsoftware.spine.Skin.Key;
-import com.esotericsoftware.spine.attachments.Attachment;
-import com.esotericsoftware.spine.attachments.MeshAttachment;
-import com.esotericsoftware.spine.attachments.PathAttachment;
-import com.esotericsoftware.spine.attachments.RegionAttachment;
-
-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();
-	final Array<Bone> updateCacheReset = new Array();
-	Skin skin;
-	final Color color;
-	float time;
-	boolean flipX, flipY;
-	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);
-		for (BoneData boneData : data.bones) {
-			Bone bone;
-			if (boneData.parent == null)
-				bone = new Bone(boneData, this, null);
-			else {
-				Bone parent = bones.get(boneData.parent.index);
-				bone = new Bone(boneData, this, parent);
-				parent.children.add(bone);
-			}
-			bones.add(bone);
-		}
-
-		slots = new Array(data.slots.size);
-		drawOrder = new Array(data.slots.size);
-		for (SlotData slotData : data.slots) {
-			Bone bone = bones.get(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;
-		flipX = skeleton.flipX;
-		flipY = skeleton.flipY;
-
-		updateCache();
-	}
-
-	/** Caches information about bones and constraints. Must be called if bones, constraints, or weighted path attachments are
-	 * added or removed. */
-	public void updateCache () {
-		Array<Updatable> updateCache = this.updateCache;
-		updateCache.clear();
-
-		Array<Bone> bones = this.bones;
-		for (int i = 0, n = bones.size; i < n; i++)
-			bones.get(i).sorted = false;
-
-		Array<IkConstraint> ikConstraints = this.ikConstraints;
-		Array<TransformConstraint> transformConstraints = this.transformConstraints;
-		Array<PathConstraint> pathConstraints = this.pathConstraints;
-		int ikCount = ikConstraints.size, transformCount = transformConstraints.size, pathCount = pathConstraints.size;
-		int constraintCount = ikCount + transformCount + pathCount;
-		outer:
-		for (int i = 0; i < constraintCount; i++) {
-			for (int ii = 0; ii < ikCount; ii++) {
-				IkConstraint constraint = ikConstraints.get(ii);
-				if (constraint.data.order == i) {
-					sortIkConstraint(constraint);
-					continue outer;
-				}
-			}
-			for (int ii = 0; ii < transformCount; ii++) {
-				TransformConstraint constraint = transformConstraints.get(ii);
-				if (constraint.data.order == i) {
-					sortTransformConstraint(constraint);
-					continue outer;
-				}
-			}
-			for (int ii = 0; ii < pathCount; ii++) {
-				PathConstraint constraint = pathConstraints.get(ii);
-				if (constraint.data.order == i) {
-					sortPathConstraint(constraint);
-					continue outer;
-				}
-			}
-		}
-
-		for (int i = 0, n = bones.size; i < n; i++)
-			sortBone(bones.get(i));
-	}
-
-	private void sortIkConstraint (IkConstraint constraint) {
-		Bone target = constraint.target;
-		sortBone(target);
-
-		Array<Bone> constrained = constraint.bones;
-		Bone parent = constrained.first();
-		sortBone(parent);
-
-		if (constrained.size > 1) {
-			Bone child = constrained.peek();
-			if (!updateCache.contains(child, true)) updateCacheReset.add(child);
-		}
-
-		updateCache.add(constraint);
-
-		sortReset(parent.children);
-		constrained.peek().sorted = true;
-	}
-
-	private void sortPathConstraint (PathConstraint constraint) {
-		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);
-		for (int ii = 0, nn = data.skins.size; ii < nn; ii++)
-			sortPathConstraintAttachment(data.skins.get(ii), slotIndex, slotBone);
-
-		Attachment attachment = slot.attachment;
-		if (attachment instanceof PathAttachment) sortPathConstraintAttachment(attachment, slotBone);
-
-		Array<Bone> constrained = constraint.bones;
-		int boneCount = constrained.size;
-		for (int ii = 0; ii < boneCount; ii++)
-			sortBone(constrained.get(ii));
-
-		updateCache.add(constraint);
-
-		for (int ii = 0; ii < boneCount; ii++)
-			sortReset(constrained.get(ii).children);
-		for (int ii = 0; ii < boneCount; ii++)
-			constrained.get(ii).sorted = true;
-	}
-
-	private void sortTransformConstraint (TransformConstraint constraint) {
-		sortBone(constraint.target);
-
-		Array<Bone> constrained = constraint.bones;
-		int boneCount = constrained.size;
-		for (int ii = 0; ii < boneCount; ii++)
-			sortBone(constrained.get(ii));
-
-		updateCache.add(constraint);
-
-		for (int ii = 0; ii < boneCount; ii++)
-			sortReset(constrained.get(ii).children);
-		for (int ii = 0; ii < boneCount; ii++)
-			constrained.get(ii).sorted = true;
-	}
-
-	private void sortPathConstraintAttachment (Skin skin, int slotIndex, Bone slotBone) {
-		for (Entry<Key, Attachment> entry : skin.attachments.entries())
-			if (entry.key.slotIndex == slotIndex) sortPathConstraintAttachment(entry.value, slotBone);
-	}
-
-	private void sortPathConstraintAttachment (Attachment attachment, Bone slotBone) {
-		if (!(attachment instanceof PathAttachment)) return;
-		int[] pathBones = ((PathAttachment)attachment).getBones();
-		if (pathBones == null)
-			sortBone(slotBone);
-		else {
-			Array<Bone> bones = this.bones;
-			for (int i = 0, n = pathBones.length; i < n;) {
-				int nn = pathBones[i++];
-				nn += i;
-				while (i < nn)
-					sortBone(bones.get(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) {
-		for (int i = 0, n = bones.size; i < n; i++) {
-			Bone bone = bones.get(i);
-			if (bone.sorted) sortReset(bone.children);
-			bone.sorted = false;
-		}
-	}
-
-	/** Updates the world transform for each bone and applies constraints. */
-	public void updateWorldTransform () {
-		Array<Bone> updateCacheReset = this.updateCacheReset;
-		for (int i = 0, n = updateCacheReset.size; i < n; i++) {
-			Bone bone = updateCacheReset.get(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;
-			bone.appliedValid = true;
-		}
-		Array<Updatable> updateCache = this.updateCache;
-		for (int i = 0, n = updateCache.size; i < n; i++)
-			updateCache.get(i).update();
-	}
-
-	/** Sets the bones, constraints, and slots to their setup pose values. */
-	public void setToSetupPose () {
-		setBonesToSetupPose();
-		setSlotsToSetupPose();
-	}
-
-	/** Sets the bones and constraints to their setup pose values. */
-	public void setBonesToSetupPose () {
-		Array<Bone> bones = this.bones;
-		for (int i = 0, n = bones.size; i < n; i++)
-			bones.get(i).setToSetupPose();
-
-		Array<IkConstraint> ikConstraints = this.ikConstraints;
-		for (int i = 0, n = ikConstraints.size; i < n; i++) {
-			IkConstraint constraint = ikConstraints.get(i);
-			constraint.bendDirection = constraint.data.bendDirection;
-			constraint.mix = constraint.data.mix;
-		}
-
-		Array<TransformConstraint> transformConstraints = this.transformConstraints;
-		for (int i = 0, n = transformConstraints.size; i < n; i++) {
-			TransformConstraint constraint = transformConstraints.get(i);
-			TransformConstraintData data = constraint.data;
-			constraint.rotateMix = data.rotateMix;
-			constraint.translateMix = data.translateMix;
-			constraint.scaleMix = data.scaleMix;
-			constraint.shearMix = data.shearMix;
-		}
-
-		Array<PathConstraint> pathConstraints = this.pathConstraints;
-		for (int i = 0, n = pathConstraints.size; i < n; i++) {
-			PathConstraint constraint = pathConstraints.get(i);
-			PathConstraintData data = constraint.data;
-			constraint.position = data.position;
-			constraint.spacing = data.spacing;
-			constraint.rotateMix = data.rotateMix;
-			constraint.translateMix = data.translateMix;
-		}
-	}
-
-	public void setSlotsToSetupPose () {
-		Array<Slot> slots = this.slots;
-		System.arraycopy(slots.items, 0, drawOrder.items, 0, slots.size);
-		for (int i = 0, n = slots.size; i < n; i++)
-			slots.get(i).setToSetupPose();
-	}
-
-	public SkeletonData getData () {
-		return data;
-	}
-
-	public Array<Bone> getBones () {
-		return bones;
-	}
-
-	public Array<Updatable> getUpdateCache () {
-		return updateCache;
-	}
-
-	/** @return May return null. */
-	public Bone getRootBone () {
-		if (bones.size == 0) return null;
-		return bones.first();
-	}
-
-	/** @return May be null. */
-	public Bone findBone (String boneName) {
-		if (boneName == null) throw new IllegalArgumentException("boneName cannot be null.");
-		Array<Bone> bones = this.bones;
-		for (int i = 0, n = bones.size; i < n; i++) {
-			Bone bone = bones.get(i);
-			if (bone.data.name.equals(boneName)) return bone;
-		}
-		return null;
-	}
-
-	/** @return -1 if the bone was not found. */
-	public int findBoneIndex (String boneName) {
-		if (boneName == null) throw new IllegalArgumentException("boneName cannot be null.");
-		Array<Bone> bones = this.bones;
-		for (int i = 0, n = bones.size; i < n; i++)
-			if (bones.get(i).data.name.equals(boneName)) return i;
-		return -1;
-	}
-
-	public Array<Slot> getSlots () {
-		return slots;
-	}
-
-	/** @return May be null. */
-	public Slot findSlot (String slotName) {
-		if (slotName == null) throw new IllegalArgumentException("slotName cannot be null.");
-		Array<Slot> slots = this.slots;
-		for (int i = 0, n = slots.size; i < n; i++) {
-			Slot slot = slots.get(i);
-			if (slot.data.name.equals(slotName)) return slot;
-		}
-		return null;
-	}
-
-	/** @return -1 if the bone was not found. */
-	public int findSlotIndex (String slotName) {
-		if (slotName == null) throw new IllegalArgumentException("slotName cannot be null.");
-		Array<Slot> slots = this.slots;
-		for (int i = 0, n = slots.size; i < n; i++)
-			if (slots.get(i).data.name.equals(slotName)) return i;
-		return -1;
-	}
-
-	/** Returns the slots in the order they will be drawn. The returned array may be modified to change the draw order. */
-	public Array<Slot> getDrawOrder () {
-		return drawOrder;
-	}
-
-	/** Sets the slots and the order they will be drawn. */
-	public void setDrawOrder (Array<Slot> drawOrder) {
-		if (drawOrder == null) throw new IllegalArgumentException("drawOrder cannot be null.");
-		this.drawOrder = drawOrder;
-	}
-
-	/** @return May be null. */
-	public Skin getSkin () {
-		return skin;
-	}
-
-	/** Sets a skin by name.
-	 * @see #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}.
-	 * 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.
-	 * @param newSkin May be null. */
-	public void setSkin (Skin newSkin) {
-		if (newSkin != null) {
-			if (skin != null)
-				newSkin.attachAll(this, skin);
-			else {
-				Array<Slot> slots = this.slots;
-				for (int i = 0, n = slots.size; i < n; i++) {
-					Slot slot = slots.get(i);
-					String name = slot.data.attachmentName;
-					if (name != null) {
-						Attachment attachment = newSkin.getAttachment(i, name);
-						if (attachment != null) slot.setAttachment(attachment);
-					}
-				}
-			}
-		}
-		skin = newSkin;
-	}
-
-	/** @return May be null. */
-	public Attachment getAttachment (String slotName, String attachmentName) {
-		return getAttachment(data.findSlotIndex(slotName), attachmentName);
-	}
-
-	/** @return May be null. */
-	public 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;
-	}
-
-	/** @param attachmentName May be null. */
-	public void setAttachment (String slotName, String attachmentName) {
-		if (slotName == null) throw new IllegalArgumentException("slotName cannot be null.");
-		Array<Slot> slots = this.slots;
-		for (int i = 0, n = slots.size; i < n; i++) {
-			Slot slot = slots.get(i);
-			if (slot.data.name.equals(slotName)) {
-				Attachment attachment = null;
-				if (attachmentName != null) {
-					attachment = getAttachment(i, attachmentName);
-					if (attachment == null)
-						throw new IllegalArgumentException("Attachment not found: " + attachmentName + ", for slot: " + slotName);
-				}
-				slot.setAttachment(attachment);
-				return;
-			}
-		}
-		throw new IllegalArgumentException("Slot not found: " + slotName);
-	}
-
-	public Array<IkConstraint> getIkConstraints () {
-		return ikConstraints;
-	}
-
-	/** @return May be null. */
-	public IkConstraint findIkConstraint (String constraintName) {
-		if (constraintName == null) throw new IllegalArgumentException("constraintName cannot be null.");
-		Array<IkConstraint> ikConstraints = this.ikConstraints;
-		for (int i = 0, n = ikConstraints.size; i < n; i++) {
-			IkConstraint ikConstraint = ikConstraints.get(i);
-			if (ikConstraint.data.name.equals(constraintName)) return ikConstraint;
-		}
-		return null;
-	}
-
-	public Array<TransformConstraint> getTransformConstraints () {
-		return transformConstraints;
-	}
-
-	/** @return May be null. */
-	public TransformConstraint findTransformConstraint (String constraintName) {
-		if (constraintName == null) throw new IllegalArgumentException("constraintName cannot be null.");
-		Array<TransformConstraint> transformConstraints = this.transformConstraints;
-		for (int i = 0, n = transformConstraints.size; i < n; i++) {
-			TransformConstraint constraint = transformConstraints.get(i);
-			if (constraint.data.name.equals(constraintName)) return constraint;
-		}
-		return null;
-	}
-
-	public Array<PathConstraint> getPathConstraints () {
-		return pathConstraints;
-	}
-
-	/** @return May be null. */
-	public PathConstraint findPathConstraint (String constraintName) {
-		if (constraintName == null) throw new IllegalArgumentException("constraintName cannot be null.");
-		Array<PathConstraint> pathConstraints = this.pathConstraints;
-		for (int i = 0, n = pathConstraints.size; i < n; i++) {
-			PathConstraint constraint = pathConstraints.get(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 The distance from the skeleton origin to the bottom left corner of the AABB.
-	 * @param size The width and height of the AABB. */
-	public void getBounds (Vector2 offset, Vector2 size) {
-		if (offset == null) throw new IllegalArgumentException("offset cannot be null.");
-		if (size == null) throw new IllegalArgumentException("size cannot be null.");
-		Array<Slot> drawOrder = this.drawOrder;
-		float minX = Integer.MAX_VALUE, minY = Integer.MAX_VALUE, maxX = Integer.MIN_VALUE, maxY = Integer.MIN_VALUE;
-		for (int i = 0, n = drawOrder.size; i < n; i++) {
-			Slot slot = drawOrder.get(i);
-			float[] vertices = null;
-			Attachment attachment = slot.getAttachment();
-			if (attachment instanceof RegionAttachment)
-				vertices = ((RegionAttachment)attachment).updateWorldVertices(slot, false);
-			else if (attachment instanceof MeshAttachment) //
-				vertices = ((MeshAttachment)attachment).updateWorldVertices(slot, true);
-			if (vertices != null) {
-				for (int ii = 0, nn = vertices.length; ii < nn; ii += 5) {
-					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);
-	}
-
-	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);
-	}
-
-	public boolean getFlipX () {
-		return flipX;
-	}
-
-	public void setFlipX (boolean flipX) {
-		this.flipX = flipX;
-	}
-
-	public boolean getFlipY () {
-		return flipY;
-	}
-
-	public void setFlipY (boolean flipY) {
-		this.flipY = flipY;
-	}
-
-	public void setFlip (boolean flipX, boolean flipY) {
-		this.flipX = flipX;
-		this.flipY = flipY;
-	}
-
-	public float getX () {
-		return x;
-	}
-
-	public void setX (float x) {
-		this.x = x;
-	}
-
-	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;
-	}
-
-	public float getTime () {
-		return time;
-	}
-
-	public void setTime (float time) {
-		this.time = time;
-	}
-
-	public void update (float delta) {
-		time += delta;
-	}
-
-	public String toString () {
-		return data.name != null ? data.name : super.toString();
-	}
+package com.esotericsoftware.spine;
+
+import com.badlogic.gdx.graphics.Color;
+import com.badlogic.gdx.math.Vector2;
+import com.badlogic.gdx.utils.Array;
+import com.badlogic.gdx.utils.ObjectMap.Entry;
+import com.esotericsoftware.spine.Skin.Key;
+import com.esotericsoftware.spine.attachments.Attachment;
+import com.esotericsoftware.spine.attachments.MeshAttachment;
+import com.esotericsoftware.spine.attachments.PathAttachment;
+import com.esotericsoftware.spine.attachments.RegionAttachment;
+
+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();
+	final Array<Bone> updateCacheReset = new Array();
+	Skin skin;
+	final Color color;
+	float time;
+	boolean flipX, flipY;
+	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);
+		for (BoneData boneData : data.bones) {
+			Bone bone;
+			if (boneData.parent == null)
+				bone = new Bone(boneData, this, null);
+			else {
+				Bone parent = bones.get(boneData.parent.index);
+				bone = new Bone(boneData, this, parent);
+				parent.children.add(bone);
+			}
+			bones.add(bone);
+		}
+
+		slots = new Array(data.slots.size);
+		drawOrder = new Array(data.slots.size);
+		for (SlotData slotData : data.slots) {
+			Bone bone = bones.get(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;
+		flipX = skeleton.flipX;
+		flipY = skeleton.flipY;
+
+		updateCache();
+	}
+
+	/** Caches information about bones and constraints. Must be called if bones, constraints, or weighted path attachments are
+	 * added or removed. */
+	public void updateCache () {
+		Array<Updatable> updateCache = this.updateCache;
+		updateCache.clear();
+
+		Array<Bone> bones = this.bones;
+		for (int i = 0, n = bones.size; i < n; i++)
+			bones.get(i).sorted = false;
+
+		Array<IkConstraint> ikConstraints = this.ikConstraints;
+		Array<TransformConstraint> transformConstraints = this.transformConstraints;
+		Array<PathConstraint> pathConstraints = this.pathConstraints;
+		int ikCount = ikConstraints.size, transformCount = transformConstraints.size, pathCount = pathConstraints.size;
+		int constraintCount = ikCount + transformCount + pathCount;
+		outer:
+		for (int i = 0; i < constraintCount; i++) {
+			for (int ii = 0; ii < ikCount; ii++) {
+				IkConstraint constraint = ikConstraints.get(ii);
+				if (constraint.data.order == i) {
+					sortIkConstraint(constraint);
+					continue outer;
+				}
+			}
+			for (int ii = 0; ii < transformCount; ii++) {
+				TransformConstraint constraint = transformConstraints.get(ii);
+				if (constraint.data.order == i) {
+					sortTransformConstraint(constraint);
+					continue outer;
+				}
+			}
+			for (int ii = 0; ii < pathCount; ii++) {
+				PathConstraint constraint = pathConstraints.get(ii);
+				if (constraint.data.order == i) {
+					sortPathConstraint(constraint);
+					continue outer;
+				}
+			}
+		}
+
+		for (int i = 0, n = bones.size; i < n; i++)
+			sortBone(bones.get(i));
+	}
+
+	private void sortIkConstraint (IkConstraint constraint) {
+		Bone target = constraint.target;
+		sortBone(target);
+
+		Array<Bone> constrained = constraint.bones;
+		Bone parent = constrained.first();
+		sortBone(parent);
+
+		if (constrained.size > 1) {
+			Bone child = constrained.peek();
+			if (!updateCache.contains(child, true)) updateCacheReset.add(child);
+		}
+
+		updateCache.add(constraint);
+
+		sortReset(parent.children);
+		constrained.peek().sorted = true;
+	}
+
+	private void sortPathConstraint (PathConstraint constraint) {
+		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);
+		for (int ii = 0, nn = data.skins.size; ii < nn; ii++)
+			sortPathConstraintAttachment(data.skins.get(ii), slotIndex, slotBone);
+
+		Attachment attachment = slot.attachment;
+		if (attachment instanceof PathAttachment) sortPathConstraintAttachment(attachment, slotBone);
+
+		Array<Bone> constrained = constraint.bones;
+		int boneCount = constrained.size;
+		for (int ii = 0; ii < boneCount; ii++)
+			sortBone(constrained.get(ii));
+
+		updateCache.add(constraint);
+
+		for (int ii = 0; ii < boneCount; ii++)
+			sortReset(constrained.get(ii).children);
+		for (int ii = 0; ii < boneCount; ii++)
+			constrained.get(ii).sorted = true;
+	}
+
+	private void sortTransformConstraint (TransformConstraint constraint) {
+		sortBone(constraint.target);
+
+		Array<Bone> constrained = constraint.bones;
+		int boneCount = constrained.size;
+		for (int ii = 0; ii < boneCount; ii++)
+			sortBone(constrained.get(ii));
+
+		updateCache.add(constraint);
+
+		for (int ii = 0; ii < boneCount; ii++)
+			sortReset(constrained.get(ii).children);
+		for (int ii = 0; ii < boneCount; ii++)
+			constrained.get(ii).sorted = true;
+	}
+
+	private void sortPathConstraintAttachment (Skin skin, int slotIndex, Bone slotBone) {
+		for (Entry<Key, Attachment> entry : skin.attachments.entries())
+			if (entry.key.slotIndex == slotIndex) sortPathConstraintAttachment(entry.value, slotBone);
+	}
+
+	private void sortPathConstraintAttachment (Attachment attachment, Bone slotBone) {
+		if (!(attachment instanceof PathAttachment)) return;
+		int[] pathBones = ((PathAttachment)attachment).getBones();
+		if (pathBones == null)
+			sortBone(slotBone);
+		else {
+			Array<Bone> bones = this.bones;
+			for (int i = 0, n = pathBones.length; i < n;) {
+				int nn = pathBones[i++];
+				nn += i;
+				while (i < nn)
+					sortBone(bones.get(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) {
+		for (int i = 0, n = bones.size; i < n; i++) {
+			Bone bone = bones.get(i);
+			if (bone.sorted) sortReset(bone.children);
+			bone.sorted = false;
+		}
+	}
+
+	/** Updates the world transform for each bone and applies constraints. */
+	public void updateWorldTransform () {
+		Array<Bone> updateCacheReset = this.updateCacheReset;
+		for (int i = 0, n = updateCacheReset.size; i < n; i++) {
+			Bone bone = updateCacheReset.get(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;
+			bone.appliedValid = true;
+		}
+		Array<Updatable> updateCache = this.updateCache;
+		for (int i = 0, n = updateCache.size; i < n; i++)
+			updateCache.get(i).update();
+	}
+
+	/** Sets the bones, constraints, and slots to their setup pose values. */
+	public void setToSetupPose () {
+		setBonesToSetupPose();
+		setSlotsToSetupPose();
+	}
+
+	/** Sets the bones and constraints to their setup pose values. */
+	public void setBonesToSetupPose () {
+		Array<Bone> bones = this.bones;
+		for (int i = 0, n = bones.size; i < n; i++)
+			bones.get(i).setToSetupPose();
+
+		Array<IkConstraint> ikConstraints = this.ikConstraints;
+		for (int i = 0, n = ikConstraints.size; i < n; i++) {
+			IkConstraint constraint = ikConstraints.get(i);
+			constraint.bendDirection = constraint.data.bendDirection;
+			constraint.mix = constraint.data.mix;
+		}
+
+		Array<TransformConstraint> transformConstraints = this.transformConstraints;
+		for (int i = 0, n = transformConstraints.size; i < n; i++) {
+			TransformConstraint constraint = transformConstraints.get(i);
+			TransformConstraintData data = constraint.data;
+			constraint.rotateMix = data.rotateMix;
+			constraint.translateMix = data.translateMix;
+			constraint.scaleMix = data.scaleMix;
+			constraint.shearMix = data.shearMix;
+		}
+
+		Array<PathConstraint> pathConstraints = this.pathConstraints;
+		for (int i = 0, n = pathConstraints.size; i < n; i++) {
+			PathConstraint constraint = pathConstraints.get(i);
+			PathConstraintData data = constraint.data;
+			constraint.position = data.position;
+			constraint.spacing = data.spacing;
+			constraint.rotateMix = data.rotateMix;
+			constraint.translateMix = data.translateMix;
+		}
+	}
+
+	public void setSlotsToSetupPose () {
+		Array<Slot> slots = this.slots;
+		System.arraycopy(slots.items, 0, drawOrder.items, 0, slots.size);
+		for (int i = 0, n = slots.size; i < n; i++)
+			slots.get(i).setToSetupPose();
+	}
+
+	public SkeletonData getData () {
+		return data;
+	}
+
+	public Array<Bone> getBones () {
+		return bones;
+	}
+
+	public Array<Updatable> getUpdateCache () {
+		return updateCache;
+	}
+
+	/** @return May return null. */
+	public Bone getRootBone () {
+		if (bones.size == 0) return null;
+		return bones.first();
+	}
+
+	/** @return May be null. */
+	public Bone findBone (String boneName) {
+		if (boneName == null) throw new IllegalArgumentException("boneName cannot be null.");
+		Array<Bone> bones = this.bones;
+		for (int i = 0, n = bones.size; i < n; i++) {
+			Bone bone = bones.get(i);
+			if (bone.data.name.equals(boneName)) return bone;
+		}
+		return null;
+	}
+
+	/** @return -1 if the bone was not found. */
+	public int findBoneIndex (String boneName) {
+		if (boneName == null) throw new IllegalArgumentException("boneName cannot be null.");
+		Array<Bone> bones = this.bones;
+		for (int i = 0, n = bones.size; i < n; i++)
+			if (bones.get(i).data.name.equals(boneName)) return i;
+		return -1;
+	}
+
+	public Array<Slot> getSlots () {
+		return slots;
+	}
+
+	/** @return May be null. */
+	public Slot findSlot (String slotName) {
+		if (slotName == null) throw new IllegalArgumentException("slotName cannot be null.");
+		Array<Slot> slots = this.slots;
+		for (int i = 0, n = slots.size; i < n; i++) {
+			Slot slot = slots.get(i);
+			if (slot.data.name.equals(slotName)) return slot;
+		}
+		return null;
+	}
+
+	/** @return -1 if the bone was not found. */
+	public int findSlotIndex (String slotName) {
+		if (slotName == null) throw new IllegalArgumentException("slotName cannot be null.");
+		Array<Slot> slots = this.slots;
+		for (int i = 0, n = slots.size; i < n; i++)
+			if (slots.get(i).data.name.equals(slotName)) return i;
+		return -1;
+	}
+
+	/** Returns the slots in the order they will be drawn. The returned array may be modified to change the draw order. */
+	public Array<Slot> getDrawOrder () {
+		return drawOrder;
+	}
+
+	/** Sets the slots and the order they will be drawn. */
+	public void setDrawOrder (Array<Slot> drawOrder) {
+		if (drawOrder == null) throw new IllegalArgumentException("drawOrder cannot be null.");
+		this.drawOrder = drawOrder;
+	}
+
+	/** @return May be null. */
+	public Skin getSkin () {
+		return skin;
+	}
+
+	/** Sets a skin by name.
+	 * @see #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}.
+	 * 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.
+	 * @param newSkin May be null. */
+	public void setSkin (Skin newSkin) {
+		if (newSkin != null) {
+			if (skin != null)
+				newSkin.attachAll(this, skin);
+			else {
+				Array<Slot> slots = this.slots;
+				for (int i = 0, n = slots.size; i < n; i++) {
+					Slot slot = slots.get(i);
+					String name = slot.data.attachmentName;
+					if (name != null) {
+						Attachment attachment = newSkin.getAttachment(i, name);
+						if (attachment != null) slot.setAttachment(attachment);
+					}
+				}
+			}
+		}
+		skin = newSkin;
+	}
+
+	/** @return May be null. */
+	public Attachment getAttachment (String slotName, String attachmentName) {
+		return getAttachment(data.findSlotIndex(slotName), attachmentName);
+	}
+
+	/** @return May be null. */
+	public 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;
+	}
+
+	/** @param attachmentName May be null. */
+	public void setAttachment (String slotName, String attachmentName) {
+		if (slotName == null) throw new IllegalArgumentException("slotName cannot be null.");
+		Array<Slot> slots = this.slots;
+		for (int i = 0, n = slots.size; i < n; i++) {
+			Slot slot = slots.get(i);
+			if (slot.data.name.equals(slotName)) {
+				Attachment attachment = null;
+				if (attachmentName != null) {
+					attachment = getAttachment(i, attachmentName);
+					if (attachment == null)
+						throw new IllegalArgumentException("Attachment not found: " + attachmentName + ", for slot: " + slotName);
+				}
+				slot.setAttachment(attachment);
+				return;
+			}
+		}
+		throw new IllegalArgumentException("Slot not found: " + slotName);
+	}
+
+	public Array<IkConstraint> getIkConstraints () {
+		return ikConstraints;
+	}
+
+	/** @return May be null. */
+	public IkConstraint findIkConstraint (String constraintName) {
+		if (constraintName == null) throw new IllegalArgumentException("constraintName cannot be null.");
+		Array<IkConstraint> ikConstraints = this.ikConstraints;
+		for (int i = 0, n = ikConstraints.size; i < n; i++) {
+			IkConstraint ikConstraint = ikConstraints.get(i);
+			if (ikConstraint.data.name.equals(constraintName)) return ikConstraint;
+		}
+		return null;
+	}
+
+	public Array<TransformConstraint> getTransformConstraints () {
+		return transformConstraints;
+	}
+
+	/** @return May be null. */
+	public TransformConstraint findTransformConstraint (String constraintName) {
+		if (constraintName == null) throw new IllegalArgumentException("constraintName cannot be null.");
+		Array<TransformConstraint> transformConstraints = this.transformConstraints;
+		for (int i = 0, n = transformConstraints.size; i < n; i++) {
+			TransformConstraint constraint = transformConstraints.get(i);
+			if (constraint.data.name.equals(constraintName)) return constraint;
+		}
+		return null;
+	}
+
+	public Array<PathConstraint> getPathConstraints () {
+		return pathConstraints;
+	}
+
+	/** @return May be null. */
+	public PathConstraint findPathConstraint (String constraintName) {
+		if (constraintName == null) throw new IllegalArgumentException("constraintName cannot be null.");
+		Array<PathConstraint> pathConstraints = this.pathConstraints;
+		for (int i = 0, n = pathConstraints.size; i < n; i++) {
+			PathConstraint constraint = pathConstraints.get(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 The distance from the skeleton origin to the bottom left corner of the AABB.
+	 * @param size The width and height of the AABB. */
+	public void getBounds (Vector2 offset, Vector2 size) {
+		if (offset == null) throw new IllegalArgumentException("offset cannot be null.");
+		if (size == null) throw new IllegalArgumentException("size cannot be null.");
+		Array<Slot> drawOrder = this.drawOrder;
+		float minX = Integer.MAX_VALUE, minY = Integer.MAX_VALUE, maxX = Integer.MIN_VALUE, maxY = Integer.MIN_VALUE;
+		for (int i = 0, n = drawOrder.size; i < n; i++) {
+			Slot slot = drawOrder.get(i);
+			float[] vertices = null;
+			Attachment attachment = slot.getAttachment();
+			if (attachment instanceof RegionAttachment)
+				vertices = ((RegionAttachment)attachment).updateWorldVertices(slot, false);
+			else if (attachment instanceof MeshAttachment) //
+				vertices = ((MeshAttachment)attachment).updateWorldVertices(slot, true);
+			if (vertices != null) {
+				for (int ii = 0, nn = vertices.length; ii < nn; ii += 5) {
+					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);
+	}
+
+	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);
+	}
+
+	public boolean getFlipX () {
+		return flipX;
+	}
+
+	public void setFlipX (boolean flipX) {
+		this.flipX = flipX;
+	}
+
+	public boolean getFlipY () {
+		return flipY;
+	}
+
+	public void setFlipY (boolean flipY) {
+		this.flipY = flipY;
+	}
+
+	public void setFlip (boolean flipX, boolean flipY) {
+		this.flipX = flipX;
+		this.flipY = flipY;
+	}
+
+	public float getX () {
+		return x;
+	}
+
+	public void setX (float x) {
+		this.x = x;
+	}
+
+	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;
+	}
+
+	public float getTime () {
+		return time;
+	}
+
+	public void setTime (float time) {
+		this.time = time;
+	}
+
+	public void update (float delta) {
+		time += delta;
+	}
+
+	public String toString () {
+		return data.name != null ? data.name : super.toString();
+	}
 }

+ 782 - 783
spine-libgdx/spine-libgdx/src/com/esotericsoftware/spine/SkeletonBinary.java

@@ -1,786 +1,785 @@
-/******************************************************************************
- * Spine Runtimes Software License
- * Version 2.3
- * 
- * Copyright (c) 2013-2015, Esoteric Software
- * All rights reserved.
- * 
- * You are granted a perpetual, non-exclusive, non-sublicensable and
- * non-transferable license to use, install, execute and perform the Spine
- * Runtimes Software (the "Software") and derivative works solely for personal
- * or internal use. Without the written permission of Esoteric Software (see
- * Section 2 of the Spine Software License Agreement), you may not (a) modify,
- * translate, adapt or otherwise create derivative works, improvements of the
- * Software or develop new applications using the Software or (b) remove,
- * delete, alter or obscure any trademarks or any copyright, trademark, patent
- * or other intellectual property or proprietary rights notices on or in the
- * Software, including any copy thereof. Redistributions in binary or source
- * form must include this license and terms.
- * 
- * THIS SOFTWARE IS PROVIDED BY ESOTERIC SOFTWARE "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 BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
- * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
- * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS;
- * OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
- * WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR
- * OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
- * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+/******************************************************************************
+ * Spine Runtimes Software License v2.5
+ * 
+ * Copyright (c) 2013-2016, Esoteric Software
+ * All rights reserved.
+ * 
+ * You are granted a perpetual, non-exclusive, non-sublicensable, and
+ * non-transferable license to use, install, execute, and perform the Spine
+ * Runtimes software and derivative works solely for personal or internal
+ * use. Without the written permission of Esoteric Software (see Section 2 of
+ * the Spine Software License Agreement), you may not (a) modify, translate,
+ * adapt, or develop new applications using the Spine Runtimes or otherwise
+ * create derivative works or improvements of the Spine Runtimes or (b) remove,
+ * delete, alter, or obscure any trademarks or any copyright, trademark, patent,
+ * or other intellectual property or proprietary rights notices on or in the
+ * Software, including any copy thereof. Redistributions in binary or source
+ * form must include this license and terms.
+ * 
+ * THIS SOFTWARE IS PROVIDED BY ESOTERIC SOFTWARE "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 BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
+ * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES, BUSINESS INTERRUPTION, OR LOSS OF
+ * USE, DATA, OR PROFITS) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER
+ * IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+ * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
+ * POSSIBILITY OF SUCH DAMAGE.
  *****************************************************************************/
 
-package com.esotericsoftware.spine;
-
-import java.io.EOFException;
-import java.io.IOException;
-
-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.SerializationException;
-import com.esotericsoftware.spine.Animation.AttachmentTimeline;
-import com.esotericsoftware.spine.Animation.ColorTimeline;
-import com.esotericsoftware.spine.Animation.CurveTimeline;
-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.RotateTimeline;
-import com.esotericsoftware.spine.Animation.ScaleTimeline;
-import com.esotericsoftware.spine.Animation.ShearTimeline;
-import com.esotericsoftware.spine.Animation.Timeline;
-import com.esotericsoftware.spine.Animation.TransformConstraintTimeline;
-import com.esotericsoftware.spine.Animation.TranslateTimeline;
-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.AtlasAttachmentLoader;
-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.MeshAttachment;
-import com.esotericsoftware.spine.attachments.PathAttachment;
-import com.esotericsoftware.spine.attachments.RegionAttachment;
-import com.esotericsoftware.spine.attachments.VertexAttachment;
-
-public class SkeletonBinary {
-	static public final int BONE_ROTATE = 0;
-	static public final int BONE_TRANSLATE = 1;
-	static public final int BONE_SCALE = 2;
-	static public final int BONE_SHEAR = 3;
-
-	static public final int SLOT_ATTACHMENT = 0;
-	static public final int SLOT_COLOR = 1;
-
-	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;
-
-	static private final Color tempColor = new Color();
-
-	private final AttachmentLoader attachmentLoader;
-	private float scale = 1;
-	private Array<LinkedMesh> linkedMeshes = new Array();
-
-	public SkeletonBinary (TextureAtlas atlas) {
-		attachmentLoader = new AtlasAttachmentLoader(atlas);
-	}
-
-	public SkeletonBinary (AttachmentLoader attachmentLoader) {
-		if (attachmentLoader == null) throw new IllegalArgumentException("attachmentLoader cannot be null.");
-		this.attachmentLoader = attachmentLoader;
-	}
-
-	public float getScale () {
-		return scale;
-	}
-
-	/** Scales the bones, images, and animations as they are loaded. */
-	public void setScale (float scale) {
-		this.scale = scale;
-	}
-
-	public SkeletonData readSkeletonData (FileHandle file) {
-		if (file == null) throw new IllegalArgumentException("file cannot be null.");
-
-		float scale = this.scale;
-
-		SkeletonData skeletonData = new SkeletonData();
-		skeletonData.name = file.nameWithoutExtension();
-
-		DataInput input = new DataInput(file.read(512)) {
-			private char[] chars = new char[32];
-
-			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);
-			}
-		};
-		try {
-			skeletonData.hash = input.readString();
-			if (skeletonData.hash.isEmpty()) skeletonData.hash = null;
-			skeletonData.version = input.readString();
-			if (skeletonData.version.isEmpty()) skeletonData.version = null;
-			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;
-			}
-
-			// Bones.
-			for (int i = 0, n = input.readInt(true); i < n; i++) {
-				String name = input.readString();
-				BoneData parent = i == 0 ? null : skeletonData.bones.get(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)];
-				if (nonessential) Color.rgba8888ToColor(data.color, input.readInt());
-				skeletonData.bones.add(data);
-			}
-
-			// Slots.
-			for (int i = 0, n = input.readInt(true); i < n; i++) {
-				String slotName = input.readString();
-				BoneData boneData = skeletonData.bones.get(input.readInt(true));
-				SlotData data = new SlotData(i, slotName, boneData);
-				Color.rgba8888ToColor(data.color, input.readInt());
-				data.attachmentName = input.readString();
-				data.blendMode = BlendMode.values[input.readInt(true)];
-				skeletonData.slots.add(data);
-			}
-
-			// IK constraints.
-			for (int i = 0, n = input.readInt(true); i < n; i++) {
-				IkConstraintData data = new IkConstraintData(input.readString());
-				data.order = input.readInt(true);
-				for (int ii = 0, nn = input.readInt(true); ii < nn; ii++)
-					data.bones.add(skeletonData.bones.get(input.readInt(true)));
-				data.target = skeletonData.bones.get(input.readInt(true));
-				data.mix = input.readFloat();
-				data.bendDirection = input.readByte();
-				skeletonData.ikConstraints.add(data);
-			}
-
-			// Transform constraints.
-			for (int i = 0, n = input.readInt(true); i < n; i++) {
-				TransformConstraintData data = new TransformConstraintData(input.readString());
-				data.order = input.readInt(true);
-				for (int ii = 0, nn = input.readInt(true); ii < nn; ii++)
-					data.bones.add(skeletonData.bones.get(input.readInt(true)));
-				data.target = skeletonData.bones.get(input.readInt(true));
-				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.rotateMix = input.readFloat();
-				data.translateMix = input.readFloat();
-				data.scaleMix = input.readFloat();
-				data.shearMix = input.readFloat();
-				skeletonData.transformConstraints.add(data);
-			}
-
-			// Path constraints.
-			for (int i = 0, n = input.readInt(true); i < n; i++) {
-				PathConstraintData data = new PathConstraintData(input.readString());
-				data.order = input.readInt(true);
-				for (int ii = 0, nn = input.readInt(true); ii < nn; ii++)
-					data.bones.add(skeletonData.bones.get(input.readInt(true)));
-				data.target = skeletonData.slots.get(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.rotateMix = input.readFloat();
-				data.translateMix = input.readFloat();
-				skeletonData.pathConstraints.add(data);
-			}
-
-			// Default skin.
-			Skin defaultSkin = readSkin(input, "default", nonessential);
-			if (defaultSkin != null) {
-				skeletonData.defaultSkin = defaultSkin;
-				skeletonData.skins.add(defaultSkin);
-			}
-
-			// Skins.
-			for (int i = 0, n = input.readInt(true); i < n; i++)
-				skeletonData.skins.add(readSkin(input, input.readString(), nonessential));
-
-			// Linked meshes.
-			for (int i = 0, n = linkedMeshes.size; i < n; i++) {
-				LinkedMesh linkedMesh = linkedMeshes.get(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.setParentMesh((MeshAttachment)parent);
-				linkedMesh.mesh.updateUVs();
-			}
-			linkedMeshes.clear();
-
-			// Events.
-			for (int i = 0, n = input.readInt(true); i < n; i++) {
-				EventData data = new EventData(input.readString());
-				data.intValue = input.readInt(false);
-				data.floatValue = input.readFloat();
-				data.stringValue = input.readString();
-				skeletonData.events.add(data);
-			}
-
-			// Animations.
-			for (int i = 0, n = input.readInt(true); i < n; i++)
-				readAnimation(input.readString(), input, skeletonData);
-
-		} catch (IOException ex) {
-			throw new SerializationException("Error reading skeleton file.", ex);
-		} finally {
-			try {
-				input.close();
-			} catch (IOException ignored) {
-			}
-		}
-
-		skeletonData.bones.shrink();
-		skeletonData.slots.shrink();
-		skeletonData.skins.shrink();
-		skeletonData.events.shrink();
-		skeletonData.animations.shrink();
-		skeletonData.ikConstraints.shrink();
-		return skeletonData;
-	}
-
-	/** @return May be null. */
-	private Skin readSkin (DataInput input, String skinName, boolean nonessential) throws IOException {
-		int slotCount = input.readInt(true);
-		if (slotCount == 0) return null;
-		Skin skin = new Skin(skinName);
-		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.readString();
-				Attachment attachment = readAttachment(input, skin, slotIndex, name, nonessential);
-				if (attachment != null) skin.addAttachment(slotIndex, name, attachment);
-			}
-		}
-		return skin;
-	}
-
-	private Attachment readAttachment (DataInput input, Skin skin, int slotIndex, String attachmentName, boolean nonessential)
-		throws IOException {
-		float scale = this.scale;
-
-		String name = input.readString();
-		if (name == null) name = attachmentName;
-
-		AttachmentType type = AttachmentType.values[input.readByte()];
-		switch (type) {
-		case region: {
-			String path = input.readString();
-			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.readString();
-			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.readString();
-			int color = input.readInt();
-			String skinName = input.readString();
-			String parent = input.readString();
-			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);
-			mesh.setInheritDeform(inheritDeform);
-			if (nonessential) {
-				mesh.setWidth(width * scale);
-				mesh.setHeight(height * scale);
-			}
-			linkedMeshes.add(new LinkedMesh(mesh, skinName, slotIndex, parent));
-			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;
-		}
-		}
-		return null;
-	}
-
-	private Vertices readVertices (DataInput input, int vertexCount) throws IOException {
-		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 (DataInput 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 (DataInput 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 void readAnimation (String name, DataInput input, SkeletonData skeletonData) {
-		Array<Timeline> timelines = new Array();
-		float scale = this.scale;
-		float duration = 0;
-
-		try {
-			// 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();
-					int frameCount = input.readInt(true);
-					switch (timelineType) {
-					case SLOT_COLOR: {
-						ColorTimeline timeline = new ColorTimeline(frameCount);
-						timeline.slotIndex = slotIndex;
-						for (int frameIndex = 0; frameIndex < frameCount; frameIndex++) {
-							float time = input.readFloat();
-							Color.rgba8888ToColor(tempColor, input.readInt());
-							timeline.setFrame(frameIndex, time, tempColor.r, tempColor.g, tempColor.b, tempColor.a);
-							if (frameIndex < frameCount - 1) readCurve(input, frameIndex, timeline);
-						}
-						timelines.add(timeline);
-						duration = Math.max(duration, timeline.getFrames()[(frameCount - 1) * ColorTimeline.ENTRIES]);
-						break;
-					}
-					case SLOT_ATTACHMENT:
-						AttachmentTimeline timeline = new AttachmentTimeline(frameCount);
-						timeline.slotIndex = slotIndex;
-						for (int frameIndex = 0; frameIndex < frameCount; frameIndex++)
-							timeline.setFrame(frameIndex, input.readFloat(), input.readString());
-						timelines.add(timeline);
-						duration = Math.max(duration, timeline.getFrames()[frameCount - 1]);
-						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 timelineType = input.readByte();
-					int frameCount = input.readInt(true);
-					switch (timelineType) {
-					case BONE_ROTATE: {
-						RotateTimeline timeline = new RotateTimeline(frameCount);
-						timeline.boneIndex = boneIndex;
-						for (int frameIndex = 0; frameIndex < frameCount; frameIndex++) {
-							timeline.setFrame(frameIndex, input.readFloat(), input.readFloat());
-							if (frameIndex < frameCount - 1) readCurve(input, frameIndex, timeline);
-						}
-						timelines.add(timeline);
-						duration = Math.max(duration, timeline.getFrames()[(frameCount - 1) * RotateTimeline.ENTRIES]);
-						break;
-					}
-					case BONE_TRANSLATE:
-					case BONE_SCALE:
-					case BONE_SHEAR: {
-						TranslateTimeline timeline;
-						float timelineScale = 1;
-						if (timelineType == BONE_SCALE)
-							timeline = new ScaleTimeline(frameCount);
-						else if (timelineType == BONE_SHEAR)
-							timeline = new ShearTimeline(frameCount);
-						else {
-							timeline = new TranslateTimeline(frameCount);
-							timelineScale = scale;
-						}
-						timeline.boneIndex = boneIndex;
-						for (int frameIndex = 0; frameIndex < frameCount; frameIndex++) {
-							timeline.setFrame(frameIndex, input.readFloat(), input.readFloat() * timelineScale,
-								input.readFloat() * timelineScale);
-							if (frameIndex < frameCount - 1) readCurve(input, frameIndex, timeline);
-						}
-						timelines.add(timeline);
-						duration = Math.max(duration, timeline.getFrames()[(frameCount - 1) * TranslateTimeline.ENTRIES]);
-						break;
-					}
-					}
-				}
-			}
-
-			// IK constraint timelines.
-			for (int i = 0, n = input.readInt(true); i < n; i++) {
-				int index = input.readInt(true);
-				int frameCount = input.readInt(true);
-				IkConstraintTimeline timeline = new IkConstraintTimeline(frameCount);
-				timeline.ikConstraintIndex = index;
-				for (int frameIndex = 0; frameIndex < frameCount; frameIndex++) {
-					timeline.setFrame(frameIndex, input.readFloat(), input.readFloat(), input.readByte());
-					if (frameIndex < frameCount - 1) readCurve(input, frameIndex, timeline);
-				}
-				timelines.add(timeline);
-				duration = Math.max(duration, timeline.getFrames()[(frameCount - 1) * IkConstraintTimeline.ENTRIES]);
-			}
-
-			// Transform constraint timelines.
-			for (int i = 0, n = input.readInt(true); i < n; i++) {
-				int index = input.readInt(true);
-				int frameCount = input.readInt(true);
-				TransformConstraintTimeline timeline = new TransformConstraintTimeline(frameCount);
-				timeline.transformConstraintIndex = index;
-				for (int frameIndex = 0; frameIndex < frameCount; frameIndex++) {
-					timeline.setFrame(frameIndex, input.readFloat(), input.readFloat(), input.readFloat(), input.readFloat(),
-						input.readFloat());
-					if (frameIndex < frameCount - 1) readCurve(input, frameIndex, timeline);
-				}
-				timelines.add(timeline);
-				duration = Math.max(duration, timeline.getFrames()[(frameCount - 1) * TransformConstraintTimeline.ENTRIES]);
-			}
-
-			// Path constraint timelines.
-			for (int i = 0, n = input.readInt(true); i < n; i++) {
-				int index = input.readInt(true);
-				PathConstraintData data = skeletonData.getPathConstraints().get(index);
-				for (int ii = 0, nn = input.readInt(true); ii < nn; ii++) {
-					int timelineType = input.readByte();
-					int frameCount = input.readInt(true);
-					switch (timelineType) {
-					case PATH_POSITION:
-					case PATH_SPACING: {
-						PathConstraintPositionTimeline timeline;
-						float timelineScale = 1;
-						if (timelineType == PATH_SPACING) {
-							timeline = new PathConstraintSpacingTimeline(frameCount);
-							if (data.spacingMode == SpacingMode.length || data.spacingMode == SpacingMode.fixed) timelineScale = scale;
-						} else {
-							timeline = new PathConstraintPositionTimeline(frameCount);
-							if (data.positionMode == PositionMode.fixed) timelineScale = scale;
-						}
-						timeline.pathConstraintIndex = index;
-						for (int frameIndex = 0; frameIndex < frameCount; frameIndex++) {
-							timeline.setFrame(frameIndex, input.readFloat(), input.readFloat() * timelineScale);
-							if (frameIndex < frameCount - 1) readCurve(input, frameIndex, timeline);
-						}
-						timelines.add(timeline);
-						duration = Math.max(duration, timeline.getFrames()[(frameCount - 1) * PathConstraintPositionTimeline.ENTRIES]);
-						break;
-					}
-					case PATH_MIX: {
-						PathConstraintMixTimeline timeline = new PathConstraintMixTimeline(frameCount);
-						timeline.pathConstraintIndex = index;
-						for (int frameIndex = 0; frameIndex < frameCount; frameIndex++) {
-							timeline.setFrame(frameIndex, input.readFloat(), input.readFloat(), input.readFloat());
-							if (frameIndex < frameCount - 1) readCurve(input, frameIndex, timeline);
-						}
-						timelines.add(timeline);
-						duration = Math.max(duration, timeline.getFrames()[(frameCount - 1) * PathConstraintMixTimeline.ENTRIES]);
-						break;
-					}
-					}
-				}
-			}
-
-			// 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++) {
-						VertexAttachment attachment = (VertexAttachment)skin.getAttachment(slotIndex, input.readString());
-						boolean weighted = attachment.getBones() != null;
-						float[] vertices = attachment.getVertices();
-						int deformLength = weighted ? vertices.length / 3 * 2 : vertices.length;
-
-						int frameCount = input.readInt(true);
-						DeformTimeline timeline = new DeformTimeline(frameCount);
-						timeline.slotIndex = slotIndex;
-						timeline.attachment = attachment;
-
-						for (int frameIndex = 0; frameIndex < frameCount; frameIndex++) {
-							float time = input.readFloat();
-							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(frameIndex, time, deform);
-							if (frameIndex < frameCount - 1) readCurve(input, frameIndex, timeline);
-						}
-						timelines.add(timeline);
-						duration = Math.max(duration, timeline.getFrames()[frameCount - 1]);
-					}
-				}
-			}
-
-			// 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);
-				duration = Math.max(duration, timeline.getFrames()[drawOrderCount - 1]);
-			}
-
-			// 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;
-					timeline.setFrame(i, event);
-				}
-				timelines.add(timeline);
-				duration = Math.max(duration, timeline.getFrames()[eventCount - 1]);
-			}
-		} catch (IOException ex) {
-			throw new SerializationException("Error reading skeleton file.", ex);
-		}
-
-		timelines.shrink();
-		skeletonData.animations.add(new Animation(name, timelines, duration));
-
-	}
-
-	private void readCurve (DataInput input, int frameIndex, CurveTimeline timeline) throws IOException {
-		switch (input.readByte()) {
-		case CURVE_STEPPED:
-			timeline.setStepped(frameIndex);
-			break;
-		case CURVE_BEZIER:
-			setCurve(timeline, frameIndex, input.readFloat(), input.readFloat(), input.readFloat(), input.readFloat());
-			break;
-		}
-	}
-
-	void setCurve (CurveTimeline timeline, int frameIndex, float cx1, float cy1, float cx2, float cy2) {
-		timeline.setCurve(frameIndex, cx1, cy1, cx2, cy2);
-	}
-
-	static class Vertices {
-		int[] bones;
-		float[] vertices;
-	}
+package com.esotericsoftware.spine;
+
+import java.io.EOFException;
+import java.io.IOException;
+
+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.SerializationException;
+import com.esotericsoftware.spine.Animation.AttachmentTimeline;
+import com.esotericsoftware.spine.Animation.ColorTimeline;
+import com.esotericsoftware.spine.Animation.CurveTimeline;
+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.RotateTimeline;
+import com.esotericsoftware.spine.Animation.ScaleTimeline;
+import com.esotericsoftware.spine.Animation.ShearTimeline;
+import com.esotericsoftware.spine.Animation.Timeline;
+import com.esotericsoftware.spine.Animation.TransformConstraintTimeline;
+import com.esotericsoftware.spine.Animation.TranslateTimeline;
+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.AtlasAttachmentLoader;
+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.MeshAttachment;
+import com.esotericsoftware.spine.attachments.PathAttachment;
+import com.esotericsoftware.spine.attachments.RegionAttachment;
+import com.esotericsoftware.spine.attachments.VertexAttachment;
+
+public class SkeletonBinary {
+	static public final int BONE_ROTATE = 0;
+	static public final int BONE_TRANSLATE = 1;
+	static public final int BONE_SCALE = 2;
+	static public final int BONE_SHEAR = 3;
+
+	static public final int SLOT_ATTACHMENT = 0;
+	static public final int SLOT_COLOR = 1;
+
+	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;
+
+	static private final Color tempColor = new Color();
+
+	private final AttachmentLoader attachmentLoader;
+	private float scale = 1;
+	private Array<LinkedMesh> linkedMeshes = new Array();
+
+	public SkeletonBinary (TextureAtlas atlas) {
+		attachmentLoader = new AtlasAttachmentLoader(atlas);
+	}
+
+	public SkeletonBinary (AttachmentLoader attachmentLoader) {
+		if (attachmentLoader == null) throw new IllegalArgumentException("attachmentLoader cannot be null.");
+		this.attachmentLoader = attachmentLoader;
+	}
+
+	public float getScale () {
+		return scale;
+	}
+
+	/** Scales the bones, images, and animations as they are loaded. */
+	public void setScale (float scale) {
+		this.scale = scale;
+	}
+
+	public SkeletonData readSkeletonData (FileHandle file) {
+		if (file == null) throw new IllegalArgumentException("file cannot be null.");
+
+		float scale = this.scale;
+
+		SkeletonData skeletonData = new SkeletonData();
+		skeletonData.name = file.nameWithoutExtension();
+
+		DataInput input = new DataInput(file.read(512)) {
+			private char[] chars = new char[32];
+
+			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);
+			}
+		};
+		try {
+			skeletonData.hash = input.readString();
+			if (skeletonData.hash.isEmpty()) skeletonData.hash = null;
+			skeletonData.version = input.readString();
+			if (skeletonData.version.isEmpty()) skeletonData.version = null;
+			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;
+			}
+
+			// Bones.
+			for (int i = 0, n = input.readInt(true); i < n; i++) {
+				String name = input.readString();
+				BoneData parent = i == 0 ? null : skeletonData.bones.get(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)];
+				if (nonessential) Color.rgba8888ToColor(data.color, input.readInt());
+				skeletonData.bones.add(data);
+			}
+
+			// Slots.
+			for (int i = 0, n = input.readInt(true); i < n; i++) {
+				String slotName = input.readString();
+				BoneData boneData = skeletonData.bones.get(input.readInt(true));
+				SlotData data = new SlotData(i, slotName, boneData);
+				Color.rgba8888ToColor(data.color, input.readInt());
+				data.attachmentName = input.readString();
+				data.blendMode = BlendMode.values[input.readInt(true)];
+				skeletonData.slots.add(data);
+			}
+
+			// IK constraints.
+			for (int i = 0, n = input.readInt(true); i < n; i++) {
+				IkConstraintData data = new IkConstraintData(input.readString());
+				data.order = input.readInt(true);
+				for (int ii = 0, nn = input.readInt(true); ii < nn; ii++)
+					data.bones.add(skeletonData.bones.get(input.readInt(true)));
+				data.target = skeletonData.bones.get(input.readInt(true));
+				data.mix = input.readFloat();
+				data.bendDirection = input.readByte();
+				skeletonData.ikConstraints.add(data);
+			}
+
+			// Transform constraints.
+			for (int i = 0, n = input.readInt(true); i < n; i++) {
+				TransformConstraintData data = new TransformConstraintData(input.readString());
+				data.order = input.readInt(true);
+				for (int ii = 0, nn = input.readInt(true); ii < nn; ii++)
+					data.bones.add(skeletonData.bones.get(input.readInt(true)));
+				data.target = skeletonData.bones.get(input.readInt(true));
+				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.rotateMix = input.readFloat();
+				data.translateMix = input.readFloat();
+				data.scaleMix = input.readFloat();
+				data.shearMix = input.readFloat();
+				skeletonData.transformConstraints.add(data);
+			}
+
+			// Path constraints.
+			for (int i = 0, n = input.readInt(true); i < n; i++) {
+				PathConstraintData data = new PathConstraintData(input.readString());
+				data.order = input.readInt(true);
+				for (int ii = 0, nn = input.readInt(true); ii < nn; ii++)
+					data.bones.add(skeletonData.bones.get(input.readInt(true)));
+				data.target = skeletonData.slots.get(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.rotateMix = input.readFloat();
+				data.translateMix = input.readFloat();
+				skeletonData.pathConstraints.add(data);
+			}
+
+			// Default skin.
+			Skin defaultSkin = readSkin(input, "default", nonessential);
+			if (defaultSkin != null) {
+				skeletonData.defaultSkin = defaultSkin;
+				skeletonData.skins.add(defaultSkin);
+			}
+
+			// Skins.
+			for (int i = 0, n = input.readInt(true); i < n; i++)
+				skeletonData.skins.add(readSkin(input, input.readString(), nonessential));
+
+			// Linked meshes.
+			for (int i = 0, n = linkedMeshes.size; i < n; i++) {
+				LinkedMesh linkedMesh = linkedMeshes.get(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.setParentMesh((MeshAttachment)parent);
+				linkedMesh.mesh.updateUVs();
+			}
+			linkedMeshes.clear();
+
+			// Events.
+			for (int i = 0, n = input.readInt(true); i < n; i++) {
+				EventData data = new EventData(input.readString());
+				data.intValue = input.readInt(false);
+				data.floatValue = input.readFloat();
+				data.stringValue = input.readString();
+				skeletonData.events.add(data);
+			}
+
+			// Animations.
+			for (int i = 0, n = input.readInt(true); i < n; i++)
+				readAnimation(input.readString(), input, skeletonData);
+
+		} catch (IOException ex) {
+			throw new SerializationException("Error reading skeleton file.", ex);
+		} finally {
+			try {
+				input.close();
+			} catch (IOException ignored) {
+			}
+		}
+
+		skeletonData.bones.shrink();
+		skeletonData.slots.shrink();
+		skeletonData.skins.shrink();
+		skeletonData.events.shrink();
+		skeletonData.animations.shrink();
+		skeletonData.ikConstraints.shrink();
+		return skeletonData;
+	}
+
+	/** @return May be null. */
+	private Skin readSkin (DataInput input, String skinName, boolean nonessential) throws IOException {
+		int slotCount = input.readInt(true);
+		if (slotCount == 0) return null;
+		Skin skin = new Skin(skinName);
+		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.readString();
+				Attachment attachment = readAttachment(input, skin, slotIndex, name, nonessential);
+				if (attachment != null) skin.addAttachment(slotIndex, name, attachment);
+			}
+		}
+		return skin;
+	}
+
+	private Attachment readAttachment (DataInput input, Skin skin, int slotIndex, String attachmentName, boolean nonessential)
+		throws IOException {
+		float scale = this.scale;
+
+		String name = input.readString();
+		if (name == null) name = attachmentName;
+
+		AttachmentType type = AttachmentType.values[input.readByte()];
+		switch (type) {
+		case region: {
+			String path = input.readString();
+			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.readString();
+			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.readString();
+			int color = input.readInt();
+			String skinName = input.readString();
+			String parent = input.readString();
+			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);
+			mesh.setInheritDeform(inheritDeform);
+			if (nonessential) {
+				mesh.setWidth(width * scale);
+				mesh.setHeight(height * scale);
+			}
+			linkedMeshes.add(new LinkedMesh(mesh, skinName, slotIndex, parent));
+			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;
+		}
+		}
+		return null;
+	}
+
+	private Vertices readVertices (DataInput input, int vertexCount) throws IOException {
+		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 (DataInput 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 (DataInput 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 void readAnimation (String name, DataInput input, SkeletonData skeletonData) {
+		Array<Timeline> timelines = new Array();
+		float scale = this.scale;
+		float duration = 0;
+
+		try {
+			// 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();
+					int frameCount = input.readInt(true);
+					switch (timelineType) {
+					case SLOT_COLOR: {
+						ColorTimeline timeline = new ColorTimeline(frameCount);
+						timeline.slotIndex = slotIndex;
+						for (int frameIndex = 0; frameIndex < frameCount; frameIndex++) {
+							float time = input.readFloat();
+							Color.rgba8888ToColor(tempColor, input.readInt());
+							timeline.setFrame(frameIndex, time, tempColor.r, tempColor.g, tempColor.b, tempColor.a);
+							if (frameIndex < frameCount - 1) readCurve(input, frameIndex, timeline);
+						}
+						timelines.add(timeline);
+						duration = Math.max(duration, timeline.getFrames()[(frameCount - 1) * ColorTimeline.ENTRIES]);
+						break;
+					}
+					case SLOT_ATTACHMENT:
+						AttachmentTimeline timeline = new AttachmentTimeline(frameCount);
+						timeline.slotIndex = slotIndex;
+						for (int frameIndex = 0; frameIndex < frameCount; frameIndex++)
+							timeline.setFrame(frameIndex, input.readFloat(), input.readString());
+						timelines.add(timeline);
+						duration = Math.max(duration, timeline.getFrames()[frameCount - 1]);
+						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 timelineType = input.readByte();
+					int frameCount = input.readInt(true);
+					switch (timelineType) {
+					case BONE_ROTATE: {
+						RotateTimeline timeline = new RotateTimeline(frameCount);
+						timeline.boneIndex = boneIndex;
+						for (int frameIndex = 0; frameIndex < frameCount; frameIndex++) {
+							timeline.setFrame(frameIndex, input.readFloat(), input.readFloat());
+							if (frameIndex < frameCount - 1) readCurve(input, frameIndex, timeline);
+						}
+						timelines.add(timeline);
+						duration = Math.max(duration, timeline.getFrames()[(frameCount - 1) * RotateTimeline.ENTRIES]);
+						break;
+					}
+					case BONE_TRANSLATE:
+					case BONE_SCALE:
+					case BONE_SHEAR: {
+						TranslateTimeline timeline;
+						float timelineScale = 1;
+						if (timelineType == BONE_SCALE)
+							timeline = new ScaleTimeline(frameCount);
+						else if (timelineType == BONE_SHEAR)
+							timeline = new ShearTimeline(frameCount);
+						else {
+							timeline = new TranslateTimeline(frameCount);
+							timelineScale = scale;
+						}
+						timeline.boneIndex = boneIndex;
+						for (int frameIndex = 0; frameIndex < frameCount; frameIndex++) {
+							timeline.setFrame(frameIndex, input.readFloat(), input.readFloat() * timelineScale,
+								input.readFloat() * timelineScale);
+							if (frameIndex < frameCount - 1) readCurve(input, frameIndex, timeline);
+						}
+						timelines.add(timeline);
+						duration = Math.max(duration, timeline.getFrames()[(frameCount - 1) * TranslateTimeline.ENTRIES]);
+						break;
+					}
+					}
+				}
+			}
+
+			// IK constraint timelines.
+			for (int i = 0, n = input.readInt(true); i < n; i++) {
+				int index = input.readInt(true);
+				int frameCount = input.readInt(true);
+				IkConstraintTimeline timeline = new IkConstraintTimeline(frameCount);
+				timeline.ikConstraintIndex = index;
+				for (int frameIndex = 0; frameIndex < frameCount; frameIndex++) {
+					timeline.setFrame(frameIndex, input.readFloat(), input.readFloat(), input.readByte());
+					if (frameIndex < frameCount - 1) readCurve(input, frameIndex, timeline);
+				}
+				timelines.add(timeline);
+				duration = Math.max(duration, timeline.getFrames()[(frameCount - 1) * IkConstraintTimeline.ENTRIES]);
+			}
+
+			// Transform constraint timelines.
+			for (int i = 0, n = input.readInt(true); i < n; i++) {
+				int index = input.readInt(true);
+				int frameCount = input.readInt(true);
+				TransformConstraintTimeline timeline = new TransformConstraintTimeline(frameCount);
+				timeline.transformConstraintIndex = index;
+				for (int frameIndex = 0; frameIndex < frameCount; frameIndex++) {
+					timeline.setFrame(frameIndex, input.readFloat(), input.readFloat(), input.readFloat(), input.readFloat(),
+						input.readFloat());
+					if (frameIndex < frameCount - 1) readCurve(input, frameIndex, timeline);
+				}
+				timelines.add(timeline);
+				duration = Math.max(duration, timeline.getFrames()[(frameCount - 1) * TransformConstraintTimeline.ENTRIES]);
+			}
+
+			// Path constraint timelines.
+			for (int i = 0, n = input.readInt(true); i < n; i++) {
+				int index = input.readInt(true);
+				PathConstraintData data = skeletonData.getPathConstraints().get(index);
+				for (int ii = 0, nn = input.readInt(true); ii < nn; ii++) {
+					int timelineType = input.readByte();
+					int frameCount = input.readInt(true);
+					switch (timelineType) {
+					case PATH_POSITION:
+					case PATH_SPACING: {
+						PathConstraintPositionTimeline timeline;
+						float timelineScale = 1;
+						if (timelineType == PATH_SPACING) {
+							timeline = new PathConstraintSpacingTimeline(frameCount);
+							if (data.spacingMode == SpacingMode.length || data.spacingMode == SpacingMode.fixed) timelineScale = scale;
+						} else {
+							timeline = new PathConstraintPositionTimeline(frameCount);
+							if (data.positionMode == PositionMode.fixed) timelineScale = scale;
+						}
+						timeline.pathConstraintIndex = index;
+						for (int frameIndex = 0; frameIndex < frameCount; frameIndex++) {
+							timeline.setFrame(frameIndex, input.readFloat(), input.readFloat() * timelineScale);
+							if (frameIndex < frameCount - 1) readCurve(input, frameIndex, timeline);
+						}
+						timelines.add(timeline);
+						duration = Math.max(duration, timeline.getFrames()[(frameCount - 1) * PathConstraintPositionTimeline.ENTRIES]);
+						break;
+					}
+					case PATH_MIX: {
+						PathConstraintMixTimeline timeline = new PathConstraintMixTimeline(frameCount);
+						timeline.pathConstraintIndex = index;
+						for (int frameIndex = 0; frameIndex < frameCount; frameIndex++) {
+							timeline.setFrame(frameIndex, input.readFloat(), input.readFloat(), input.readFloat());
+							if (frameIndex < frameCount - 1) readCurve(input, frameIndex, timeline);
+						}
+						timelines.add(timeline);
+						duration = Math.max(duration, timeline.getFrames()[(frameCount - 1) * PathConstraintMixTimeline.ENTRIES]);
+						break;
+					}
+					}
+				}
+			}
+
+			// 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++) {
+						VertexAttachment attachment = (VertexAttachment)skin.getAttachment(slotIndex, input.readString());
+						boolean weighted = attachment.getBones() != null;
+						float[] vertices = attachment.getVertices();
+						int deformLength = weighted ? vertices.length / 3 * 2 : vertices.length;
+
+						int frameCount = input.readInt(true);
+						DeformTimeline timeline = new DeformTimeline(frameCount);
+						timeline.slotIndex = slotIndex;
+						timeline.attachment = attachment;
+
+						for (int frameIndex = 0; frameIndex < frameCount; frameIndex++) {
+							float time = input.readFloat();
+							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(frameIndex, time, deform);
+							if (frameIndex < frameCount - 1) readCurve(input, frameIndex, timeline);
+						}
+						timelines.add(timeline);
+						duration = Math.max(duration, timeline.getFrames()[frameCount - 1]);
+					}
+				}
+			}
+
+			// 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);
+				duration = Math.max(duration, timeline.getFrames()[drawOrderCount - 1]);
+			}
+
+			// 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;
+					timeline.setFrame(i, event);
+				}
+				timelines.add(timeline);
+				duration = Math.max(duration, timeline.getFrames()[eventCount - 1]);
+			}
+		} catch (IOException ex) {
+			throw new SerializationException("Error reading skeleton file.", ex);
+		}
+
+		timelines.shrink();
+		skeletonData.animations.add(new Animation(name, timelines, duration));
+
+	}
+
+	private void readCurve (DataInput input, int frameIndex, CurveTimeline timeline) throws IOException {
+		switch (input.readByte()) {
+		case CURVE_STEPPED:
+			timeline.setStepped(frameIndex);
+			break;
+		case CURVE_BEZIER:
+			setCurve(timeline, frameIndex, input.readFloat(), input.readFloat(), input.readFloat(), input.readFloat());
+			break;
+		}
+	}
+
+	void setCurve (CurveTimeline timeline, int frameIndex, float cx1, float cy1, float cx2, float cy2) {
+		timeline.setCurve(frameIndex, cx1, cy1, cx2, cy2);
+	}
+
+	static class Vertices {
+		int[] bones;
+		float[] vertices;
+	}
 }

+ 225 - 226
spine-libgdx/spine-libgdx/src/com/esotericsoftware/spine/SkeletonBounds.java

@@ -1,229 +1,228 @@
-/******************************************************************************
- * Spine Runtimes Software License
- * Version 2.3
- * 
- * Copyright (c) 2013-2015, Esoteric Software
- * All rights reserved.
- * 
- * You are granted a perpetual, non-exclusive, non-sublicensable and
- * non-transferable license to use, install, execute and perform the Spine
- * Runtimes Software (the "Software") and derivative works solely for personal
- * or internal use. Without the written permission of Esoteric Software (see
- * Section 2 of the Spine Software License Agreement), you may not (a) modify,
- * translate, adapt or otherwise create derivative works, improvements of the
- * Software or develop new applications using the Software or (b) remove,
- * delete, alter or obscure any trademarks or any copyright, trademark, patent
- * or other intellectual property or proprietary rights notices on or in the
- * Software, including any copy thereof. Redistributions in binary or source
- * form must include this license and terms.
- * 
- * THIS SOFTWARE IS PROVIDED BY ESOTERIC SOFTWARE "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 BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
- * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
- * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS;
- * OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
- * WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR
- * OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
- * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+/******************************************************************************
+ * Spine Runtimes Software License v2.5
+ * 
+ * Copyright (c) 2013-2016, Esoteric Software
+ * All rights reserved.
+ * 
+ * You are granted a perpetual, non-exclusive, non-sublicensable, and
+ * non-transferable license to use, install, execute, and perform the Spine
+ * Runtimes software and derivative works solely for personal or internal
+ * use. Without the written permission of Esoteric Software (see Section 2 of
+ * the Spine Software License Agreement), you may not (a) modify, translate,
+ * adapt, or develop new applications using the Spine Runtimes or otherwise
+ * create derivative works or improvements of the Spine Runtimes or (b) remove,
+ * delete, alter, or obscure any trademarks or any copyright, trademark, patent,
+ * or other intellectual property or proprietary rights notices on or in the
+ * Software, including any copy thereof. Redistributions in binary or source
+ * form must include this license and terms.
+ * 
+ * THIS SOFTWARE IS PROVIDED BY ESOTERIC SOFTWARE "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 BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
+ * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES, BUSINESS INTERRUPTION, OR LOSS OF
+ * USE, DATA, OR PROFITS) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER
+ * IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+ * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
+ * POSSIBILITY OF SUCH DAMAGE.
  *****************************************************************************/
 
-package com.esotericsoftware.spine;
-
-import com.esotericsoftware.spine.attachments.Attachment;
-import com.esotericsoftware.spine.attachments.BoundingBoxAttachment;
-
-import com.badlogic.gdx.utils.Array;
-import com.badlogic.gdx.utils.FloatArray;
-import com.badlogic.gdx.utils.Pool;
-
-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();
-		}
-	};
-
-	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;
-		Array<Slot> slots = skeleton.slots;
-		int slotCount = slots.size;
-
-		boundingBoxes.clear();
-		polygonPool.freeAll(polygons);
-		polygons.clear();
-
-		for (int i = 0; i < slotCount; i++) {
-			Slot slot = slots.get(i);
-			Attachment attachment = slot.getAttachment();
-			if (attachment instanceof BoundingBoxAttachment) {
-				BoundingBoxAttachment boundingBox = (BoundingBoxAttachment)attachment;
-				boundingBoxes.add(boundingBox);
-
-				FloatArray polygon = polygonPool.obtain();
-				polygons.add(polygon);
-				boundingBox.computeWorldVertices(slot, polygon.setSize(boundingBox.getWorldVerticesLength()));
-			}
-		}
-
-		if (updateAabb) aabbCompute();
-	}
-
-	private void aabbCompute () {
-		float minX = Integer.MAX_VALUE, minY = Integer.MAX_VALUE, maxX = Integer.MIN_VALUE, maxY = Integer.MIN_VALUE;
-		Array<FloatArray> polygons = this.polygons;
-		for (int i = 0, n = polygons.size; i < n; i++) {
-			FloatArray polygon = polygons.get(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) {
-		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 BoundingBoxAttachment containsPoint (float x, float y) {
-		Array<FloatArray> polygons = this.polygons;
-		for (int i = 0, n = polygons.size; i < n; i++)
-			if (containsPoint(polygons.get(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) {
-		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 BoundingBoxAttachment intersectsSegment (float x1, float y1, float x2, float y2) {
-		Array<FloatArray> polygons = this.polygons;
-		for (int i = 0, n = polygons.size; i < n; i++)
-			if (intersectsSegment(polygons.get(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) {
-		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;
-	}
-
-	public float getMinX () {
-		return minX;
-	}
-
-	public float getMinY () {
-		return minY;
-	}
-
-	public float getMaxX () {
-		return maxX;
-	}
-
-	public float getMaxY () {
-		return maxY;
-	}
-
-	public float getWidth () {
-		return maxX - minX;
-	}
-
-	public float getHeight () {
-		return maxY - minY;
-	}
-
-	public Array<BoundingBoxAttachment> getBoundingBoxes () {
-		return boundingBoxes;
-	}
-
-	public Array<FloatArray> getPolygons () {
-		return polygons;
-	}
-
-	/** Returns the polygon for the specified bounding box, or null. */
-	public 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);
-	}
+package com.esotericsoftware.spine;
+
+import com.esotericsoftware.spine.attachments.Attachment;
+import com.esotericsoftware.spine.attachments.BoundingBoxAttachment;
+
+import com.badlogic.gdx.utils.Array;
+import com.badlogic.gdx.utils.FloatArray;
+import com.badlogic.gdx.utils.Pool;
+
+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();
+		}
+	};
+
+	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;
+		Array<Slot> slots = skeleton.slots;
+		int slotCount = slots.size;
+
+		boundingBoxes.clear();
+		polygonPool.freeAll(polygons);
+		polygons.clear();
+
+		for (int i = 0; i < slotCount; i++) {
+			Slot slot = slots.get(i);
+			Attachment attachment = slot.getAttachment();
+			if (attachment instanceof BoundingBoxAttachment) {
+				BoundingBoxAttachment boundingBox = (BoundingBoxAttachment)attachment;
+				boundingBoxes.add(boundingBox);
+
+				FloatArray polygon = polygonPool.obtain();
+				polygons.add(polygon);
+				boundingBox.computeWorldVertices(slot, polygon.setSize(boundingBox.getWorldVerticesLength()));
+			}
+		}
+
+		if (updateAabb) aabbCompute();
+	}
+
+	private void aabbCompute () {
+		float minX = Integer.MAX_VALUE, minY = Integer.MAX_VALUE, maxX = Integer.MIN_VALUE, maxY = Integer.MIN_VALUE;
+		Array<FloatArray> polygons = this.polygons;
+		for (int i = 0, n = polygons.size; i < n; i++) {
+			FloatArray polygon = polygons.get(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) {
+		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 BoundingBoxAttachment containsPoint (float x, float y) {
+		Array<FloatArray> polygons = this.polygons;
+		for (int i = 0, n = polygons.size; i < n; i++)
+			if (containsPoint(polygons.get(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) {
+		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 BoundingBoxAttachment intersectsSegment (float x1, float y1, float x2, float y2) {
+		Array<FloatArray> polygons = this.polygons;
+		for (int i = 0, n = polygons.size; i < n; i++)
+			if (intersectsSegment(polygons.get(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) {
+		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;
+	}
+
+	public float getMinX () {
+		return minX;
+	}
+
+	public float getMinY () {
+		return minY;
+	}
+
+	public float getMaxX () {
+		return maxX;
+	}
+
+	public float getMaxY () {
+		return maxY;
+	}
+
+	public float getWidth () {
+		return maxX - minX;
+	}
+
+	public float getHeight () {
+		return maxY - minY;
+	}
+
+	public Array<BoundingBoxAttachment> getBoundingBoxes () {
+		return boundingBoxes;
+	}
+
+	public Array<FloatArray> getPolygons () {
+		return polygons;
+	}
+
+	/** Returns the polygon for the specified bounding box, or null. */
+	public 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);
+	}
 }

+ 287 - 288
spine-libgdx/spine-libgdx/src/com/esotericsoftware/spine/SkeletonData.java

@@ -1,291 +1,290 @@
-/******************************************************************************
- * Spine Runtimes Software License
- * Version 2.3
- * 
- * Copyright (c) 2013-2015, Esoteric Software
- * All rights reserved.
- * 
- * You are granted a perpetual, non-exclusive, non-sublicensable and
- * non-transferable license to use, install, execute and perform the Spine
- * Runtimes Software (the "Software") and derivative works solely for personal
- * or internal use. Without the written permission of Esoteric Software (see
- * Section 2 of the Spine Software License Agreement), you may not (a) modify,
- * translate, adapt or otherwise create derivative works, improvements of the
- * Software or develop new applications using the Software or (b) remove,
- * delete, alter or obscure any trademarks or any copyright, trademark, patent
- * or other intellectual property or proprietary rights notices on or in the
- * Software, including any copy thereof. Redistributions in binary or source
- * form must include this license and terms.
- * 
- * THIS SOFTWARE IS PROVIDED BY ESOTERIC SOFTWARE "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 BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
- * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
- * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS;
- * OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
- * WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR
- * OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
- * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+/******************************************************************************
+ * Spine Runtimes Software License v2.5
+ * 
+ * Copyright (c) 2013-2016, Esoteric Software
+ * All rights reserved.
+ * 
+ * You are granted a perpetual, non-exclusive, non-sublicensable, and
+ * non-transferable license to use, install, execute, and perform the Spine
+ * Runtimes software and derivative works solely for personal or internal
+ * use. Without the written permission of Esoteric Software (see Section 2 of
+ * the Spine Software License Agreement), you may not (a) modify, translate,
+ * adapt, or develop new applications using the Spine Runtimes or otherwise
+ * create derivative works or improvements of the Spine Runtimes or (b) remove,
+ * delete, alter, or obscure any trademarks or any copyright, trademark, patent,
+ * or other intellectual property or proprietary rights notices on or in the
+ * Software, including any copy thereof. Redistributions in binary or source
+ * form must include this license and terms.
+ * 
+ * THIS SOFTWARE IS PROVIDED BY ESOTERIC SOFTWARE "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 BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
+ * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES, BUSINESS INTERRUPTION, OR LOSS OF
+ * USE, DATA, OR PROFITS) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER
+ * IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+ * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
+ * POSSIBILITY OF SUCH DAMAGE.
  *****************************************************************************/
 
-package com.esotericsoftware.spine;
-
-import com.badlogic.gdx.utils.Array;
-
-public class SkeletonData {
-	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();
-	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 width, height;
-	String version, hash;
-
-	// Nonessential.
-	float fps;
-	String imagesPath;
-
-	// --- Bones.
-
-	public Array<BoneData> getBones () {
-		return bones;
-	}
-
-	/** @return May be null. */
-	public BoneData findBone (String boneName) {
-		if (boneName == null) throw new IllegalArgumentException("boneName cannot be null.");
-		Array<BoneData> bones = this.bones;
-		for (int i = 0, n = bones.size; i < n; i++) {
-			BoneData bone = bones.get(i);
-			if (bone.name.equals(boneName)) return bone;
-		}
-		return null;
-	}
-
-	/** @return -1 if the bone was not found. */
-	public int findBoneIndex (String boneName) {
-		if (boneName == null) throw new IllegalArgumentException("boneName cannot be null.");
-		Array<BoneData> bones = this.bones;
-		for (int i = 0, n = bones.size; i < n; i++)
-			if (bones.get(i).name.equals(boneName)) return i;
-		return -1;
-	}
-
-	// --- Slots.
-
-	public Array<SlotData> getSlots () {
-		return slots;
-	}
-
-	/** @return May be null. */
-	public SlotData findSlot (String slotName) {
-		if (slotName == null) throw new IllegalArgumentException("slotName cannot be null.");
-		Array<SlotData> slots = this.slots;
-		for (int i = 0, n = slots.size; i < n; i++) {
-			SlotData slot = slots.get(i);
-			if (slot.name.equals(slotName)) return slot;
-		}
-		return null;
-	}
-
-	/** @return -1 if the slot was not found. */
-	public int findSlotIndex (String slotName) {
-		if (slotName == null) throw new IllegalArgumentException("slotName cannot be null.");
-		Array<SlotData> slots = this.slots;
-		for (int i = 0, n = slots.size; i < n; i++)
-			if (slots.get(i).name.equals(slotName)) return i;
-		return -1;
-	}
-
-	// --- Skins.
-
-	/** @return May be null. */
-	public Skin getDefaultSkin () {
-		return defaultSkin;
-	}
-
-	/** @param defaultSkin May be null. */
-	public void setDefaultSkin (Skin defaultSkin) {
-		this.defaultSkin = defaultSkin;
-	}
-
-	/** @return May be null. */
-	public 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;
-	}
-
-	/** Returns all skins, including the default skin. */
-	public Array<Skin> getSkins () {
-		return skins;
-	}
-
-	// --- Events.
-
-	/** @return May be null. */
-	public 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;
-	}
-
-	public Array<EventData> getEvents () {
-		return events;
-	}
-
-	// --- Animations.
-
-	public Array<Animation> getAnimations () {
-		return animations;
-	}
-
-	/** @return May be null. */
-	public Animation findAnimation (String animationName) {
-		if (animationName == null) throw new IllegalArgumentException("animationName cannot be null.");
-		Array<Animation> animations = this.animations;
-		for (int i = 0, n = animations.size; i < n; i++) {
-			Animation animation = animations.get(i);
-			if (animation.name.equals(animationName)) return animation;
-		}
-		return null;
-	}
-
-	// --- IK constraints
-
-	public Array<IkConstraintData> getIkConstraints () {
-		return ikConstraints;
-	}
-
-	/** @return May be null. */
-	public IkConstraintData findIkConstraint (String constraintName) {
-		if (constraintName == null) throw new IllegalArgumentException("constraintName cannot be null.");
-		Array<IkConstraintData> ikConstraints = this.ikConstraints;
-		for (int i = 0, n = ikConstraints.size; i < n; i++) {
-			IkConstraintData constraint = ikConstraints.get(i);
-			if (constraint.name.equals(constraintName)) return constraint;
-		}
-		return null;
-	}
-
-	// --- Transform constraints
-
-	public Array<TransformConstraintData> getTransformConstraints () {
-		return transformConstraints;
-	}
-
-	/** @return May be null. */
-	public TransformConstraintData findTransformConstraint (String constraintName) {
-		if (constraintName == null) throw new IllegalArgumentException("constraintName cannot be null.");
-		Array<TransformConstraintData> transformConstraints = this.transformConstraints;
-		for (int i = 0, n = transformConstraints.size; i < n; i++) {
-			TransformConstraintData constraint = transformConstraints.get(i);
-			if (constraint.name.equals(constraintName)) return constraint;
-		}
-		return null;
-	}
-
-	// --- Path constraints
-
-	public Array<PathConstraintData> getPathConstraints () {
-		return pathConstraints;
-	}
-
-	/** @return May be null. */
-	public PathConstraintData findPathConstraint (String constraintName) {
-		if (constraintName == null) throw new IllegalArgumentException("constraintName cannot be null.");
-		Array<PathConstraintData> pathConstraints = this.pathConstraints;
-		for (int i = 0, n = pathConstraints.size; i < n; i++) {
-			PathConstraintData constraint = pathConstraints.get(i);
-			if (constraint.name.equals(constraintName)) return constraint;
-		}
-		return null;
-	}
-
-	/** @return -1 if the path constraint was not found. */
-	public int findPathConstraintIndex (String pathConstraintName) {
-		if (pathConstraintName == null) throw new IllegalArgumentException("pathConstraintName cannot be null.");
-		Array<PathConstraintData> pathConstraints = this.pathConstraints;
-		for (int i = 0, n = pathConstraints.size; i < n; i++)
-			if (pathConstraints.get(i).name.equals(pathConstraintName)) return i;
-		return -1;
-	}
-
-	// ---
-
-	/** @return May be null. */
-	public String getName () {
-		return name;
-	}
-
-	/** @param name May be null. */
-	public void setName (String name) {
-		this.name = name;
-	}
-
-	public float getWidth () {
-		return width;
-	}
-
-	public void setWidth (float width) {
-		this.width = width;
-	}
-
-	public float getHeight () {
-		return height;
-	}
-
-	public void setHeight (float height) {
-		this.height = height;
-	}
-
-	/** Returns the Spine version used to export this data, or null. */
-	public String getVersion () {
-		return version;
-	}
-
-	/** @param version May be null. */
-	public void setVersion (String version) {
-		this.version = version;
-	}
-
-	/** @return May be null. */
-	public String getHash () {
-		return hash;
-	}
-
-	/** @param hash May be null. */
-	public void setHash (String hash) {
-		this.hash = hash;
-	}
-
-	/** @return May be null. */
-	public String getImagesPath () {
-		return imagesPath;
-	}
-
-	/** @param imagesPath May be null. */
-	public void setImagesPath (String imagesPath) {
-		this.imagesPath = imagesPath;
-	}
-
-	public float getFps () {
-		return fps;
-	}
-
-	public void setFps (float fps) {
-		this.fps = fps;
-	}
-
-	public String toString () {
-		return name != null ? name : super.toString();
-	}
+package com.esotericsoftware.spine;
+
+import com.badlogic.gdx.utils.Array;
+
+public class SkeletonData {
+	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();
+	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 width, height;
+	String version, hash;
+
+	// Nonessential.
+	float fps;
+	String imagesPath;
+
+	// --- Bones.
+
+	public Array<BoneData> getBones () {
+		return bones;
+	}
+
+	/** @return May be null. */
+	public BoneData findBone (String boneName) {
+		if (boneName == null) throw new IllegalArgumentException("boneName cannot be null.");
+		Array<BoneData> bones = this.bones;
+		for (int i = 0, n = bones.size; i < n; i++) {
+			BoneData bone = bones.get(i);
+			if (bone.name.equals(boneName)) return bone;
+		}
+		return null;
+	}
+
+	/** @return -1 if the bone was not found. */
+	public int findBoneIndex (String boneName) {
+		if (boneName == null) throw new IllegalArgumentException("boneName cannot be null.");
+		Array<BoneData> bones = this.bones;
+		for (int i = 0, n = bones.size; i < n; i++)
+			if (bones.get(i).name.equals(boneName)) return i;
+		return -1;
+	}
+
+	// --- Slots.
+
+	public Array<SlotData> getSlots () {
+		return slots;
+	}
+
+	/** @return May be null. */
+	public SlotData findSlot (String slotName) {
+		if (slotName == null) throw new IllegalArgumentException("slotName cannot be null.");
+		Array<SlotData> slots = this.slots;
+		for (int i = 0, n = slots.size; i < n; i++) {
+			SlotData slot = slots.get(i);
+			if (slot.name.equals(slotName)) return slot;
+		}
+		return null;
+	}
+
+	/** @return -1 if the slot was not found. */
+	public int findSlotIndex (String slotName) {
+		if (slotName == null) throw new IllegalArgumentException("slotName cannot be null.");
+		Array<SlotData> slots = this.slots;
+		for (int i = 0, n = slots.size; i < n; i++)
+			if (slots.get(i).name.equals(slotName)) return i;
+		return -1;
+	}
+
+	// --- Skins.
+
+	/** @return May be null. */
+	public Skin getDefaultSkin () {
+		return defaultSkin;
+	}
+
+	/** @param defaultSkin May be null. */
+	public void setDefaultSkin (Skin defaultSkin) {
+		this.defaultSkin = defaultSkin;
+	}
+
+	/** @return May be null. */
+	public 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;
+	}
+
+	/** Returns all skins, including the default skin. */
+	public Array<Skin> getSkins () {
+		return skins;
+	}
+
+	// --- Events.
+
+	/** @return May be null. */
+	public 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;
+	}
+
+	public Array<EventData> getEvents () {
+		return events;
+	}
+
+	// --- Animations.
+
+	public Array<Animation> getAnimations () {
+		return animations;
+	}
+
+	/** @return May be null. */
+	public Animation findAnimation (String animationName) {
+		if (animationName == null) throw new IllegalArgumentException("animationName cannot be null.");
+		Array<Animation> animations = this.animations;
+		for (int i = 0, n = animations.size; i < n; i++) {
+			Animation animation = animations.get(i);
+			if (animation.name.equals(animationName)) return animation;
+		}
+		return null;
+	}
+
+	// --- IK constraints
+
+	public Array<IkConstraintData> getIkConstraints () {
+		return ikConstraints;
+	}
+
+	/** @return May be null. */
+	public IkConstraintData findIkConstraint (String constraintName) {
+		if (constraintName == null) throw new IllegalArgumentException("constraintName cannot be null.");
+		Array<IkConstraintData> ikConstraints = this.ikConstraints;
+		for (int i = 0, n = ikConstraints.size; i < n; i++) {
+			IkConstraintData constraint = ikConstraints.get(i);
+			if (constraint.name.equals(constraintName)) return constraint;
+		}
+		return null;
+	}
+
+	// --- Transform constraints
+
+	public Array<TransformConstraintData> getTransformConstraints () {
+		return transformConstraints;
+	}
+
+	/** @return May be null. */
+	public TransformConstraintData findTransformConstraint (String constraintName) {
+		if (constraintName == null) throw new IllegalArgumentException("constraintName cannot be null.");
+		Array<TransformConstraintData> transformConstraints = this.transformConstraints;
+		for (int i = 0, n = transformConstraints.size; i < n; i++) {
+			TransformConstraintData constraint = transformConstraints.get(i);
+			if (constraint.name.equals(constraintName)) return constraint;
+		}
+		return null;
+	}
+
+	// --- Path constraints
+
+	public Array<PathConstraintData> getPathConstraints () {
+		return pathConstraints;
+	}
+
+	/** @return May be null. */
+	public PathConstraintData findPathConstraint (String constraintName) {
+		if (constraintName == null) throw new IllegalArgumentException("constraintName cannot be null.");
+		Array<PathConstraintData> pathConstraints = this.pathConstraints;
+		for (int i = 0, n = pathConstraints.size; i < n; i++) {
+			PathConstraintData constraint = pathConstraints.get(i);
+			if (constraint.name.equals(constraintName)) return constraint;
+		}
+		return null;
+	}
+
+	/** @return -1 if the path constraint was not found. */
+	public int findPathConstraintIndex (String pathConstraintName) {
+		if (pathConstraintName == null) throw new IllegalArgumentException("pathConstraintName cannot be null.");
+		Array<PathConstraintData> pathConstraints = this.pathConstraints;
+		for (int i = 0, n = pathConstraints.size; i < n; i++)
+			if (pathConstraints.get(i).name.equals(pathConstraintName)) return i;
+		return -1;
+	}
+
+	// ---
+
+	/** @return May be null. */
+	public String getName () {
+		return name;
+	}
+
+	/** @param name May be null. */
+	public void setName (String name) {
+		this.name = name;
+	}
+
+	public float getWidth () {
+		return width;
+	}
+
+	public void setWidth (float width) {
+		this.width = width;
+	}
+
+	public float getHeight () {
+		return height;
+	}
+
+	public void setHeight (float height) {
+		this.height = height;
+	}
+
+	/** Returns the Spine version used to export this data, or null. */
+	public String getVersion () {
+		return version;
+	}
+
+	/** @param version May be null. */
+	public void setVersion (String version) {
+		this.version = version;
+	}
+
+	/** @return May be null. */
+	public String getHash () {
+		return hash;
+	}
+
+	/** @param hash May be null. */
+	public void setHash (String hash) {
+		this.hash = hash;
+	}
+
+	/** @return May be null. */
+	public String getImagesPath () {
+		return imagesPath;
+	}
+
+	/** @param imagesPath May be null. */
+	public void setImagesPath (String imagesPath) {
+		this.imagesPath = imagesPath;
+	}
+
+	public float getFps () {
+		return fps;
+	}
+
+	public void setFps (float fps) {
+		this.fps = fps;
+	}
+
+	public String toString () {
+		return name != null ? name : super.toString();
+	}
 }

+ 703 - 704
spine-libgdx/spine-libgdx/src/com/esotericsoftware/spine/SkeletonJson.java

@@ -1,707 +1,706 @@
-/******************************************************************************
- * Spine Runtimes Software License
- * Version 2.3
- * 
- * Copyright (c) 2013-2015, Esoteric Software
- * All rights reserved.
- * 
- * You are granted a perpetual, non-exclusive, non-sublicensable and
- * non-transferable license to use, install, execute and perform the Spine
- * Runtimes Software (the "Software") and derivative works solely for personal
- * or internal use. Without the written permission of Esoteric Software (see
- * Section 2 of the Spine Software License Agreement), you may not (a) modify,
- * translate, adapt or otherwise create derivative works, improvements of the
- * Software or develop new applications using the Software or (b) remove,
- * delete, alter or obscure any trademarks or any copyright, trademark, patent
- * or other intellectual property or proprietary rights notices on or in the
- * Software, including any copy thereof. Redistributions in binary or source
- * form must include this license and terms.
- * 
- * THIS SOFTWARE IS PROVIDED BY ESOTERIC SOFTWARE "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 BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
- * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
- * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS;
- * OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
- * WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR
- * OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
- * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+/******************************************************************************
+ * Spine Runtimes Software License v2.5
+ * 
+ * Copyright (c) 2013-2016, Esoteric Software
+ * All rights reserved.
+ * 
+ * You are granted a perpetual, non-exclusive, non-sublicensable, and
+ * non-transferable license to use, install, execute, and perform the Spine
+ * Runtimes software and derivative works solely for personal or internal
+ * use. Without the written permission of Esoteric Software (see Section 2 of
+ * the Spine Software License Agreement), you may not (a) modify, translate,
+ * adapt, or develop new applications using the Spine Runtimes or otherwise
+ * create derivative works or improvements of the Spine Runtimes or (b) remove,
+ * delete, alter, or obscure any trademarks or any copyright, trademark, patent,
+ * or other intellectual property or proprietary rights notices on or in the
+ * Software, including any copy thereof. Redistributions in binary or source
+ * form must include this license and terms.
+ * 
+ * THIS SOFTWARE IS PROVIDED BY ESOTERIC SOFTWARE "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 BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
+ * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES, BUSINESS INTERRUPTION, OR LOSS OF
+ * USE, DATA, OR PROFITS) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER
+ * IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+ * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
+ * POSSIBILITY OF SUCH DAMAGE.
  *****************************************************************************/
 
-package com.esotericsoftware.spine;
-
-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.AttachmentTimeline;
-import com.esotericsoftware.spine.Animation.ColorTimeline;
-import com.esotericsoftware.spine.Animation.CurveTimeline;
-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.RotateTimeline;
-import com.esotericsoftware.spine.Animation.ScaleTimeline;
-import com.esotericsoftware.spine.Animation.ShearTimeline;
-import com.esotericsoftware.spine.Animation.Timeline;
-import com.esotericsoftware.spine.Animation.TransformConstraintTimeline;
-import com.esotericsoftware.spine.Animation.TranslateTimeline;
-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.AtlasAttachmentLoader;
-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.MeshAttachment;
-import com.esotericsoftware.spine.attachments.PathAttachment;
-import com.esotericsoftware.spine.attachments.RegionAttachment;
-import com.esotericsoftware.spine.attachments.VertexAttachment;
-
-public class SkeletonJson {
-	private final AttachmentLoader attachmentLoader;
-	private float scale = 1;
-	private Array<LinkedMesh> linkedMeshes = new Array();
-
-	public SkeletonJson (TextureAtlas atlas) {
-		attachmentLoader = new AtlasAttachmentLoader(atlas);
-	}
-
-	public SkeletonJson (AttachmentLoader attachmentLoader) {
-		if (attachmentLoader == null) throw new IllegalArgumentException("attachmentLoader cannot be null.");
-		this.attachmentLoader = attachmentLoader;
-	}
-
-	public float getScale () {
-		return scale;
-	}
-
-	/** Scales the bones, images, and animations as they are loaded. */
-	public void setScale (float scale) {
-		this.scale = scale;
-	}
-
-	public SkeletonData readSkeletonData (FileHandle file) {
-		if (file == null) throw new IllegalArgumentException("file cannot be null.");
-
-		float scale = this.scale;
-
-		SkeletonData skeletonData = new SkeletonData();
-		skeletonData.name = file.nameWithoutExtension();
-
-		JsonValue root = new JsonReader().parse(file);
-
-		// Skeleton.
-		JsonValue skeletonMap = root.get("skeleton");
-		if (skeletonMap != null) {
-			skeletonData.hash = skeletonMap.getString("hash", null);
-			skeletonData.version = skeletonMap.getString("spine", null);
-			skeletonData.width = skeletonMap.getFloat("width", 0);
-			skeletonData.height = skeletonMap.getFloat("height", 0);
-			skeletonData.fps = skeletonMap.getFloat("fps", 0);
-			skeletonData.imagesPath = skeletonMap.getString("images", 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()));
-
-			String color = boneMap.getString("color", null);
-			if (color != null) data.getColor().set(Color.valueOf(color));
-
-			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) data.getColor().set(Color.valueOf(color));
-
-			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);
-
-			for (JsonValue boneMap = constraintMap.getChild("bones"); boneMap != null; boneMap = boneMap.next) {
-				String boneName = boneMap.asString();
-				BoneData bone = skeletonData.findBone(boneName);
-				if (bone == null) throw new SerializationException("IK bone not found: " + boneName);
-				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.bendDirection = constraintMap.getBoolean("bendPositive", true) ? 1 : -1;
-			data.mix = constraintMap.getFloat("mix", 1);
-
-			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);
-
-			for (JsonValue boneMap = constraintMap.getChild("bones"); boneMap != null; boneMap = boneMap.next) {
-				String boneName = boneMap.asString();
-				BoneData bone = skeletonData.findBone(boneName);
-				if (bone == null) throw new SerializationException("Transform constraint bone not found: " + boneName);
-				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.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.rotateMix = constraintMap.getFloat("rotateMix", 1);
-			data.translateMix = constraintMap.getFloat("translateMix", 1);
-			data.scaleMix = constraintMap.getFloat("scaleMix", 1);
-			data.shearMix = constraintMap.getFloat("shearMix", 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);
-
-			for (JsonValue boneMap = constraintMap.getChild("bones"); boneMap != null; boneMap = boneMap.next) {
-				String boneName = boneMap.asString();
-				BoneData bone = skeletonData.findBone(boneName);
-				if (bone == null) throw new SerializationException("Path bone not found: " + boneName);
-				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.rotateMix = constraintMap.getFloat("rotateMix", 1);
-			data.translateMix = constraintMap.getFloat("translateMix", 1);
-
-			skeletonData.pathConstraints.add(data);
-		}
-
-		// Skins.
-		for (JsonValue skinMap = root.getChild("skins"); skinMap != null; skinMap = skinMap.next) {
-			Skin skin = new Skin(skinMap.name);
-			for (JsonValue slotEntry = skinMap.child; slotEntry != null; slotEntry = slotEntry.next) {
-				int slotIndex = skeletonData.findSlotIndex(slotEntry.name);
-				if (slotIndex == -1) throw new SerializationException("Slot not found: " + slotEntry.name);
-				for (JsonValue entry = slotEntry.child; entry != null; entry = entry.next) {
-					try {
-						Attachment attachment = readAttachment(entry, skin, slotIndex, entry.name);
-						if (attachment != null) skin.addAttachment(slotIndex, entry.name, attachment);
-					} catch (Exception 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.
-		for (int i = 0, n = linkedMeshes.size; i < n; i++) {
-			LinkedMesh linkedMesh = linkedMeshes.get(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.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", null);
-			skeletonData.events.add(data);
-		}
-
-		// Animations.
-		for (JsonValue animationMap = root.getChild("animations"); animationMap != null; animationMap = animationMap.next) {
-			try {
-				readAnimation(animationMap, animationMap.name, skeletonData);
-			} catch (Exception 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) {
-		float scale = this.scale;
-		name = map.getString("name", name);
-
-		String type = map.getString("type", AttachmentType.region.name());
-
-		switch (AttachmentType.valueOf(type)) {
-		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) region.getColor().set(Color.valueOf(color));
-
-			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) box.getColor().set(Color.valueOf(color));
-			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) mesh.getColor().set(Color.valueOf(color));
-
-			mesh.setWidth(map.getFloat("width", 0) * scale);
-			mesh.setHeight(map.getFloat("height", 0) * scale);
-
-			String parent = map.getString("parent", null);
-			if (parent != null) {
-				mesh.setInheritDeform(map.getBoolean("deform", true));
-				linkedMeshes.add(new LinkedMesh(mesh, map.getString("skin", null), slotIndex, parent));
-				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() * 2);
-			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) path.getColor().set(Color.valueOf(color));
-			return path;
-		}
-		}
-		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 * 4; 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();
-		float duration = 0;
-
-		// Slot timelines.
-		for (JsonValue slotMap = map.getChild("slots"); slotMap != null; slotMap = slotMap.next) {
-			int slotIndex = skeletonData.findSlotIndex(slotMap.name);
-			if (slotIndex == -1) throw new SerializationException("Slot not found: " + slotMap.name);
-			for (JsonValue timelineMap = slotMap.child; timelineMap != null; timelineMap = timelineMap.next) {
-				String timelineName = timelineMap.name;
-				if (timelineName.equals("color")) {
-					ColorTimeline timeline = new ColorTimeline(timelineMap.size);
-					timeline.slotIndex = slotIndex;
-
-					int frameIndex = 0;
-					for (JsonValue valueMap = timelineMap.child; valueMap != null; valueMap = valueMap.next) {
-						Color color = Color.valueOf(valueMap.getString("color"));
-						timeline.setFrame(frameIndex, valueMap.getFloat("time"), color.r, color.g, color.b, color.a);
-						readCurve(valueMap, timeline, frameIndex);
-						frameIndex++;
-					}
-					timelines.add(timeline);
-					duration = Math.max(duration, timeline.getFrames()[(timeline.getFrameCount() - 1) * ColorTimeline.ENTRIES]);
-
-				} else if (timelineName.equals("attachment")) {
-					AttachmentTimeline timeline = new AttachmentTimeline(timelineMap.size);
-					timeline.slotIndex = slotIndex;
-
-					int frameIndex = 0;
-					for (JsonValue valueMap = timelineMap.child; valueMap != null; valueMap = valueMap.next)
-						timeline.setFrame(frameIndex++, valueMap.getFloat("time"), valueMap.getString("name"));
-					timelines.add(timeline);
-					duration = Math.max(duration, timeline.getFrames()[timeline.getFrameCount() - 1]);
-				} 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) {
-			int boneIndex = skeletonData.findBoneIndex(boneMap.name);
-			if (boneIndex == -1) throw new SerializationException("Bone not found: " + boneMap.name);
-			for (JsonValue timelineMap = boneMap.child; timelineMap != null; timelineMap = timelineMap.next) {
-				String timelineName = timelineMap.name;
-				if (timelineName.equals("rotate")) {
-					RotateTimeline timeline = new RotateTimeline(timelineMap.size);
-					timeline.boneIndex = boneIndex;
-
-					int frameIndex = 0;
-					for (JsonValue valueMap = timelineMap.child; valueMap != null; valueMap = valueMap.next) {
-						timeline.setFrame(frameIndex, valueMap.getFloat("time"), valueMap.getFloat("angle"));
-						readCurve(valueMap, timeline, frameIndex);
-						frameIndex++;
-					}
-					timelines.add(timeline);
-					duration = Math.max(duration, timeline.getFrames()[(timeline.getFrameCount() - 1) * RotateTimeline.ENTRIES]);
-
-				} else if (timelineName.equals("translate") || timelineName.equals("scale") || timelineName.equals("shear")) {
-					TranslateTimeline timeline;
-					float timelineScale = 1;
-					if (timelineName.equals("scale"))
-						timeline = new ScaleTimeline(timelineMap.size);
-					else if (timelineName.equals("shear"))
-						timeline = new ShearTimeline(timelineMap.size);
-					else {
-						timeline = new TranslateTimeline(timelineMap.size);
-						timelineScale = scale;
-					}
-					timeline.boneIndex = boneIndex;
-
-					int frameIndex = 0;
-					for (JsonValue valueMap = timelineMap.child; valueMap != null; valueMap = valueMap.next) {
-						float x = valueMap.getFloat("x", 0), y = valueMap.getFloat("y", 0);
-						timeline.setFrame(frameIndex, valueMap.getFloat("time"), x * timelineScale, y * timelineScale);
-						readCurve(valueMap, timeline, frameIndex);
-						frameIndex++;
-					}
-					timelines.add(timeline);
-					duration = Math.max(duration, timeline.getFrames()[(timeline.getFrameCount() - 1) * TranslateTimeline.ENTRIES]);
-
-				} else
-					throw new RuntimeException("Invalid timeline type for a bone: " + timelineName + " (" + boneMap.name + ")");
-			}
-		}
-
-		// IK constraint timelines.
-		for (JsonValue constraintMap = map.getChild("ik"); constraintMap != null; constraintMap = constraintMap.next) {
-			IkConstraintData constraint = skeletonData.findIkConstraint(constraintMap.name);
-			IkConstraintTimeline timeline = new IkConstraintTimeline(constraintMap.size);
-			timeline.ikConstraintIndex = skeletonData.getIkConstraints().indexOf(constraint, true);
-			int frameIndex = 0;
-			for (JsonValue valueMap = constraintMap.child; valueMap != null; valueMap = valueMap.next) {
-				timeline.setFrame(frameIndex, valueMap.getFloat("time"), valueMap.getFloat("mix", 1),
-					valueMap.getBoolean("bendPositive", true) ? 1 : -1);
-				readCurve(valueMap, timeline, frameIndex);
-				frameIndex++;
-			}
-			timelines.add(timeline);
-			duration = Math.max(duration, timeline.getFrames()[(timeline.getFrameCount() - 1) * IkConstraintTimeline.ENTRIES]);
-		}
-
-		// Transform constraint timelines.
-		for (JsonValue constraintMap = map.getChild("transform"); constraintMap != null; constraintMap = constraintMap.next) {
-			TransformConstraintData constraint = skeletonData.findTransformConstraint(constraintMap.name);
-			TransformConstraintTimeline timeline = new TransformConstraintTimeline(constraintMap.size);
-			timeline.transformConstraintIndex = skeletonData.getTransformConstraints().indexOf(constraint, true);
-			int frameIndex = 0;
-			for (JsonValue valueMap = constraintMap.child; valueMap != null; valueMap = valueMap.next) {
-				timeline.setFrame(frameIndex, valueMap.getFloat("time"), valueMap.getFloat("rotateMix", 1),
-					valueMap.getFloat("translateMix", 1), valueMap.getFloat("scaleMix", 1), valueMap.getFloat("shearMix", 1));
-				readCurve(valueMap, timeline, frameIndex);
-				frameIndex++;
-			}
-			timelines.add(timeline);
-			duration = Math.max(duration,
-				timeline.getFrames()[(timeline.getFrameCount() - 1) * TransformConstraintTimeline.ENTRIES]);
-		}
-
-		// Path constraint timelines.
-		for (JsonValue constraintMap = map.getChild("paths"); constraintMap != null; constraintMap = constraintMap.next) {
-			int index = skeletonData.findPathConstraintIndex(constraintMap.name);
-			if (index == -1) throw new SerializationException("Path constraint not found: " + constraintMap.name);
-			PathConstraintData data = skeletonData.getPathConstraints().get(index);
-			for (JsonValue timelineMap = constraintMap.child; timelineMap != null; timelineMap = timelineMap.next) {
-				String timelineName = timelineMap.name;
-				if (timelineName.equals("position") || timelineName.equals("spacing")) {
-					PathConstraintPositionTimeline timeline;
-					float timelineScale = 1;
-					if (timelineName.equals("spacing")) {
-						timeline = new PathConstraintSpacingTimeline(timelineMap.size);
-						if (data.spacingMode == SpacingMode.length || data.spacingMode == SpacingMode.fixed) timelineScale = scale;
-					} else {
-						timeline = new PathConstraintPositionTimeline(timelineMap.size);
-						if (data.positionMode == PositionMode.fixed) timelineScale = scale;
-					}
-					timeline.pathConstraintIndex = index;
-					int frameIndex = 0;
-					for (JsonValue valueMap = timelineMap.child; valueMap != null; valueMap = valueMap.next) {
-						timeline.setFrame(frameIndex, valueMap.getFloat("time"), valueMap.getFloat(timelineName, 0) * timelineScale);
-						readCurve(valueMap, timeline, frameIndex);
-						frameIndex++;
-					}
-					timelines.add(timeline);
-					duration = Math.max(duration,
-						timeline.getFrames()[(timeline.getFrameCount() - 1) * PathConstraintPositionTimeline.ENTRIES]);
-				} else if (timelineName.equals("mix")) {
-					PathConstraintMixTimeline timeline = new PathConstraintMixTimeline(timelineMap.size);
-					timeline.pathConstraintIndex = index;
-					int frameIndex = 0;
-					for (JsonValue valueMap = timelineMap.child; valueMap != null; valueMap = valueMap.next) {
-						timeline.setFrame(frameIndex, valueMap.getFloat("time"), valueMap.getFloat("rotateMix", 1),
-							valueMap.getFloat("translateMix", 1));
-						readCurve(valueMap, timeline, frameIndex);
-						frameIndex++;
-					}
-					timelines.add(timeline);
-					duration = Math.max(duration,
-						timeline.getFrames()[(timeline.getFrameCount() - 1) * PathConstraintMixTimeline.ENTRIES]);
-				}
-			}
-		}
-
-		// 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) {
-				int slotIndex = skeletonData.findSlotIndex(slotMap.name);
-				if (slotIndex == -1) throw new SerializationException("Slot not found: " + slotMap.name);
-				for (JsonValue timelineMap = slotMap.child; timelineMap != null; timelineMap = timelineMap.next) {
-					VertexAttachment attachment = (VertexAttachment)skin.getAttachment(slotIndex, 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 * 2 : vertices.length;
-
-					DeformTimeline timeline = new DeformTimeline(timelineMap.size);
-					timeline.slotIndex = slotIndex;
-					timeline.attachment = attachment;
-
-					int frameIndex = 0;
-					for (JsonValue valueMap = timelineMap.child; valueMap != null; valueMap = valueMap.next) {
-						float[] deform;
-						JsonValue verticesValue = valueMap.get("vertices");
-						if (verticesValue == null)
-							deform = weighted ? new float[deformLength] : vertices;
-						else {
-							deform = new float[deformLength];
-							int start = valueMap.getInt("offset", 0);
-							System.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(frameIndex, valueMap.getFloat("time"), deform);
-						readCurve(valueMap, timeline, frameIndex);
-						frameIndex++;
-					}
-					timelines.add(timeline);
-					duration = Math.max(duration, timeline.getFrames()[timeline.getFrameCount() - 1]);
-				}
-			}
-		}
-
-		// Draw order timeline.
-		JsonValue drawOrdersMap = map.get("drawOrder");
-		if (drawOrdersMap == null) drawOrdersMap = map.get("draworder");
-		if (drawOrdersMap != null) {
-			DrawOrderTimeline timeline = new DrawOrderTimeline(drawOrdersMap.size);
-			int slotCount = skeletonData.slots.size;
-			int frameIndex = 0;
-			for (JsonValue drawOrderMap = drawOrdersMap.child; drawOrderMap != null; drawOrderMap = drawOrderMap.next) {
-				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) {
-						int slotIndex = skeletonData.findSlotIndex(offsetMap.getString("slot"));
-						if (slotIndex == -1) throw new SerializationException("Slot not found: " + offsetMap.getString("slot"));
-						// Collect unchanged items.
-						while (originalIndex != slotIndex)
-							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(frameIndex++, drawOrderMap.getFloat("time"), drawOrder);
-			}
-			timelines.add(timeline);
-			duration = Math.max(duration, timeline.getFrames()[timeline.getFrameCount() - 1]);
-		}
-
-		// Event timeline.
-		JsonValue eventsMap = map.get("events");
-		if (eventsMap != null) {
-			EventTimeline timeline = new EventTimeline(eventsMap.size);
-			int frameIndex = 0;
-			for (JsonValue eventMap = eventsMap.child; eventMap != null; eventMap = eventMap.next) {
-				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"), eventData);
-				event.intValue = eventMap.getInt("int", eventData.getInt());
-				event.floatValue = eventMap.getFloat("float", eventData.getFloat());
-				event.stringValue = eventMap.getString("string", eventData.getString());
-				timeline.setFrame(frameIndex++, event);
-			}
-			timelines.add(timeline);
-			duration = Math.max(duration, timeline.getFrames()[timeline.getFrameCount() - 1]);
-		}
-
-		timelines.shrink();
-		skeletonData.animations.add(new Animation(name, timelines, duration));
-	}
-
-	void readCurve (JsonValue map, CurveTimeline timeline, int frameIndex) {
-		JsonValue curve = map.get("curve");
-		if (curve == null) return;
-		if (curve.isString() && curve.asString().equals("stepped"))
-			timeline.setStepped(frameIndex);
-		else if (curve.isArray()) {
-			timeline.setCurve(frameIndex, curve.getFloat(0), curve.getFloat(1), curve.getFloat(2), curve.getFloat(3));
-		}
-	}
-
-	static class LinkedMesh {
-		String parent, skin;
-		int slotIndex;
-		MeshAttachment mesh;
-
-		public LinkedMesh (MeshAttachment mesh, String skin, int slotIndex, String parent) {
-			this.mesh = mesh;
-			this.skin = skin;
-			this.slotIndex = slotIndex;
-			this.parent = parent;
-		}
-	}
+package com.esotericsoftware.spine;
+
+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.AttachmentTimeline;
+import com.esotericsoftware.spine.Animation.ColorTimeline;
+import com.esotericsoftware.spine.Animation.CurveTimeline;
+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.RotateTimeline;
+import com.esotericsoftware.spine.Animation.ScaleTimeline;
+import com.esotericsoftware.spine.Animation.ShearTimeline;
+import com.esotericsoftware.spine.Animation.Timeline;
+import com.esotericsoftware.spine.Animation.TransformConstraintTimeline;
+import com.esotericsoftware.spine.Animation.TranslateTimeline;
+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.AtlasAttachmentLoader;
+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.MeshAttachment;
+import com.esotericsoftware.spine.attachments.PathAttachment;
+import com.esotericsoftware.spine.attachments.RegionAttachment;
+import com.esotericsoftware.spine.attachments.VertexAttachment;
+
+public class SkeletonJson {
+	private final AttachmentLoader attachmentLoader;
+	private float scale = 1;
+	private Array<LinkedMesh> linkedMeshes = new Array();
+
+	public SkeletonJson (TextureAtlas atlas) {
+		attachmentLoader = new AtlasAttachmentLoader(atlas);
+	}
+
+	public SkeletonJson (AttachmentLoader attachmentLoader) {
+		if (attachmentLoader == null) throw new IllegalArgumentException("attachmentLoader cannot be null.");
+		this.attachmentLoader = attachmentLoader;
+	}
+
+	public float getScale () {
+		return scale;
+	}
+
+	/** Scales the bones, images, and animations as they are loaded. */
+	public void setScale (float scale) {
+		this.scale = scale;
+	}
+
+	public SkeletonData readSkeletonData (FileHandle file) {
+		if (file == null) throw new IllegalArgumentException("file cannot be null.");
+
+		float scale = this.scale;
+
+		SkeletonData skeletonData = new SkeletonData();
+		skeletonData.name = file.nameWithoutExtension();
+
+		JsonValue root = new JsonReader().parse(file);
+
+		// Skeleton.
+		JsonValue skeletonMap = root.get("skeleton");
+		if (skeletonMap != null) {
+			skeletonData.hash = skeletonMap.getString("hash", null);
+			skeletonData.version = skeletonMap.getString("spine", null);
+			skeletonData.width = skeletonMap.getFloat("width", 0);
+			skeletonData.height = skeletonMap.getFloat("height", 0);
+			skeletonData.fps = skeletonMap.getFloat("fps", 0);
+			skeletonData.imagesPath = skeletonMap.getString("images", 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()));
+
+			String color = boneMap.getString("color", null);
+			if (color != null) data.getColor().set(Color.valueOf(color));
+
+			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) data.getColor().set(Color.valueOf(color));
+
+			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);
+
+			for (JsonValue boneMap = constraintMap.getChild("bones"); boneMap != null; boneMap = boneMap.next) {
+				String boneName = boneMap.asString();
+				BoneData bone = skeletonData.findBone(boneName);
+				if (bone == null) throw new SerializationException("IK bone not found: " + boneName);
+				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.bendDirection = constraintMap.getBoolean("bendPositive", true) ? 1 : -1;
+			data.mix = constraintMap.getFloat("mix", 1);
+
+			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);
+
+			for (JsonValue boneMap = constraintMap.getChild("bones"); boneMap != null; boneMap = boneMap.next) {
+				String boneName = boneMap.asString();
+				BoneData bone = skeletonData.findBone(boneName);
+				if (bone == null) throw new SerializationException("Transform constraint bone not found: " + boneName);
+				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.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.rotateMix = constraintMap.getFloat("rotateMix", 1);
+			data.translateMix = constraintMap.getFloat("translateMix", 1);
+			data.scaleMix = constraintMap.getFloat("scaleMix", 1);
+			data.shearMix = constraintMap.getFloat("shearMix", 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);
+
+			for (JsonValue boneMap = constraintMap.getChild("bones"); boneMap != null; boneMap = boneMap.next) {
+				String boneName = boneMap.asString();
+				BoneData bone = skeletonData.findBone(boneName);
+				if (bone == null) throw new SerializationException("Path bone not found: " + boneName);
+				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.rotateMix = constraintMap.getFloat("rotateMix", 1);
+			data.translateMix = constraintMap.getFloat("translateMix", 1);
+
+			skeletonData.pathConstraints.add(data);
+		}
+
+		// Skins.
+		for (JsonValue skinMap = root.getChild("skins"); skinMap != null; skinMap = skinMap.next) {
+			Skin skin = new Skin(skinMap.name);
+			for (JsonValue slotEntry = skinMap.child; slotEntry != null; slotEntry = slotEntry.next) {
+				int slotIndex = skeletonData.findSlotIndex(slotEntry.name);
+				if (slotIndex == -1) throw new SerializationException("Slot not found: " + slotEntry.name);
+				for (JsonValue entry = slotEntry.child; entry != null; entry = entry.next) {
+					try {
+						Attachment attachment = readAttachment(entry, skin, slotIndex, entry.name);
+						if (attachment != null) skin.addAttachment(slotIndex, entry.name, attachment);
+					} catch (Exception 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.
+		for (int i = 0, n = linkedMeshes.size; i < n; i++) {
+			LinkedMesh linkedMesh = linkedMeshes.get(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.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", null);
+			skeletonData.events.add(data);
+		}
+
+		// Animations.
+		for (JsonValue animationMap = root.getChild("animations"); animationMap != null; animationMap = animationMap.next) {
+			try {
+				readAnimation(animationMap, animationMap.name, skeletonData);
+			} catch (Exception 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) {
+		float scale = this.scale;
+		name = map.getString("name", name);
+
+		String type = map.getString("type", AttachmentType.region.name());
+
+		switch (AttachmentType.valueOf(type)) {
+		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) region.getColor().set(Color.valueOf(color));
+
+			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) box.getColor().set(Color.valueOf(color));
+			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) mesh.getColor().set(Color.valueOf(color));
+
+			mesh.setWidth(map.getFloat("width", 0) * scale);
+			mesh.setHeight(map.getFloat("height", 0) * scale);
+
+			String parent = map.getString("parent", null);
+			if (parent != null) {
+				mesh.setInheritDeform(map.getBoolean("deform", true));
+				linkedMeshes.add(new LinkedMesh(mesh, map.getString("skin", null), slotIndex, parent));
+				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() * 2);
+			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) path.getColor().set(Color.valueOf(color));
+			return path;
+		}
+		}
+		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 * 4; 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();
+		float duration = 0;
+
+		// Slot timelines.
+		for (JsonValue slotMap = map.getChild("slots"); slotMap != null; slotMap = slotMap.next) {
+			int slotIndex = skeletonData.findSlotIndex(slotMap.name);
+			if (slotIndex == -1) throw new SerializationException("Slot not found: " + slotMap.name);
+			for (JsonValue timelineMap = slotMap.child; timelineMap != null; timelineMap = timelineMap.next) {
+				String timelineName = timelineMap.name;
+				if (timelineName.equals("color")) {
+					ColorTimeline timeline = new ColorTimeline(timelineMap.size);
+					timeline.slotIndex = slotIndex;
+
+					int frameIndex = 0;
+					for (JsonValue valueMap = timelineMap.child; valueMap != null; valueMap = valueMap.next) {
+						Color color = Color.valueOf(valueMap.getString("color"));
+						timeline.setFrame(frameIndex, valueMap.getFloat("time"), color.r, color.g, color.b, color.a);
+						readCurve(valueMap, timeline, frameIndex);
+						frameIndex++;
+					}
+					timelines.add(timeline);
+					duration = Math.max(duration, timeline.getFrames()[(timeline.getFrameCount() - 1) * ColorTimeline.ENTRIES]);
+
+				} else if (timelineName.equals("attachment")) {
+					AttachmentTimeline timeline = new AttachmentTimeline(timelineMap.size);
+					timeline.slotIndex = slotIndex;
+
+					int frameIndex = 0;
+					for (JsonValue valueMap = timelineMap.child; valueMap != null; valueMap = valueMap.next)
+						timeline.setFrame(frameIndex++, valueMap.getFloat("time"), valueMap.getString("name"));
+					timelines.add(timeline);
+					duration = Math.max(duration, timeline.getFrames()[timeline.getFrameCount() - 1]);
+				} 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) {
+			int boneIndex = skeletonData.findBoneIndex(boneMap.name);
+			if (boneIndex == -1) throw new SerializationException("Bone not found: " + boneMap.name);
+			for (JsonValue timelineMap = boneMap.child; timelineMap != null; timelineMap = timelineMap.next) {
+				String timelineName = timelineMap.name;
+				if (timelineName.equals("rotate")) {
+					RotateTimeline timeline = new RotateTimeline(timelineMap.size);
+					timeline.boneIndex = boneIndex;
+
+					int frameIndex = 0;
+					for (JsonValue valueMap = timelineMap.child; valueMap != null; valueMap = valueMap.next) {
+						timeline.setFrame(frameIndex, valueMap.getFloat("time"), valueMap.getFloat("angle"));
+						readCurve(valueMap, timeline, frameIndex);
+						frameIndex++;
+					}
+					timelines.add(timeline);
+					duration = Math.max(duration, timeline.getFrames()[(timeline.getFrameCount() - 1) * RotateTimeline.ENTRIES]);
+
+				} else if (timelineName.equals("translate") || timelineName.equals("scale") || timelineName.equals("shear")) {
+					TranslateTimeline timeline;
+					float timelineScale = 1;
+					if (timelineName.equals("scale"))
+						timeline = new ScaleTimeline(timelineMap.size);
+					else if (timelineName.equals("shear"))
+						timeline = new ShearTimeline(timelineMap.size);
+					else {
+						timeline = new TranslateTimeline(timelineMap.size);
+						timelineScale = scale;
+					}
+					timeline.boneIndex = boneIndex;
+
+					int frameIndex = 0;
+					for (JsonValue valueMap = timelineMap.child; valueMap != null; valueMap = valueMap.next) {
+						float x = valueMap.getFloat("x", 0), y = valueMap.getFloat("y", 0);
+						timeline.setFrame(frameIndex, valueMap.getFloat("time"), x * timelineScale, y * timelineScale);
+						readCurve(valueMap, timeline, frameIndex);
+						frameIndex++;
+					}
+					timelines.add(timeline);
+					duration = Math.max(duration, timeline.getFrames()[(timeline.getFrameCount() - 1) * TranslateTimeline.ENTRIES]);
+
+				} else
+					throw new RuntimeException("Invalid timeline type for a bone: " + timelineName + " (" + boneMap.name + ")");
+			}
+		}
+
+		// IK constraint timelines.
+		for (JsonValue constraintMap = map.getChild("ik"); constraintMap != null; constraintMap = constraintMap.next) {
+			IkConstraintData constraint = skeletonData.findIkConstraint(constraintMap.name);
+			IkConstraintTimeline timeline = new IkConstraintTimeline(constraintMap.size);
+			timeline.ikConstraintIndex = skeletonData.getIkConstraints().indexOf(constraint, true);
+			int frameIndex = 0;
+			for (JsonValue valueMap = constraintMap.child; valueMap != null; valueMap = valueMap.next) {
+				timeline.setFrame(frameIndex, valueMap.getFloat("time"), valueMap.getFloat("mix", 1),
+					valueMap.getBoolean("bendPositive", true) ? 1 : -1);
+				readCurve(valueMap, timeline, frameIndex);
+				frameIndex++;
+			}
+			timelines.add(timeline);
+			duration = Math.max(duration, timeline.getFrames()[(timeline.getFrameCount() - 1) * IkConstraintTimeline.ENTRIES]);
+		}
+
+		// Transform constraint timelines.
+		for (JsonValue constraintMap = map.getChild("transform"); constraintMap != null; constraintMap = constraintMap.next) {
+			TransformConstraintData constraint = skeletonData.findTransformConstraint(constraintMap.name);
+			TransformConstraintTimeline timeline = new TransformConstraintTimeline(constraintMap.size);
+			timeline.transformConstraintIndex = skeletonData.getTransformConstraints().indexOf(constraint, true);
+			int frameIndex = 0;
+			for (JsonValue valueMap = constraintMap.child; valueMap != null; valueMap = valueMap.next) {
+				timeline.setFrame(frameIndex, valueMap.getFloat("time"), valueMap.getFloat("rotateMix", 1),
+					valueMap.getFloat("translateMix", 1), valueMap.getFloat("scaleMix", 1), valueMap.getFloat("shearMix", 1));
+				readCurve(valueMap, timeline, frameIndex);
+				frameIndex++;
+			}
+			timelines.add(timeline);
+			duration = Math.max(duration,
+				timeline.getFrames()[(timeline.getFrameCount() - 1) * TransformConstraintTimeline.ENTRIES]);
+		}
+
+		// Path constraint timelines.
+		for (JsonValue constraintMap = map.getChild("paths"); constraintMap != null; constraintMap = constraintMap.next) {
+			int index = skeletonData.findPathConstraintIndex(constraintMap.name);
+			if (index == -1) throw new SerializationException("Path constraint not found: " + constraintMap.name);
+			PathConstraintData data = skeletonData.getPathConstraints().get(index);
+			for (JsonValue timelineMap = constraintMap.child; timelineMap != null; timelineMap = timelineMap.next) {
+				String timelineName = timelineMap.name;
+				if (timelineName.equals("position") || timelineName.equals("spacing")) {
+					PathConstraintPositionTimeline timeline;
+					float timelineScale = 1;
+					if (timelineName.equals("spacing")) {
+						timeline = new PathConstraintSpacingTimeline(timelineMap.size);
+						if (data.spacingMode == SpacingMode.length || data.spacingMode == SpacingMode.fixed) timelineScale = scale;
+					} else {
+						timeline = new PathConstraintPositionTimeline(timelineMap.size);
+						if (data.positionMode == PositionMode.fixed) timelineScale = scale;
+					}
+					timeline.pathConstraintIndex = index;
+					int frameIndex = 0;
+					for (JsonValue valueMap = timelineMap.child; valueMap != null; valueMap = valueMap.next) {
+						timeline.setFrame(frameIndex, valueMap.getFloat("time"), valueMap.getFloat(timelineName, 0) * timelineScale);
+						readCurve(valueMap, timeline, frameIndex);
+						frameIndex++;
+					}
+					timelines.add(timeline);
+					duration = Math.max(duration,
+						timeline.getFrames()[(timeline.getFrameCount() - 1) * PathConstraintPositionTimeline.ENTRIES]);
+				} else if (timelineName.equals("mix")) {
+					PathConstraintMixTimeline timeline = new PathConstraintMixTimeline(timelineMap.size);
+					timeline.pathConstraintIndex = index;
+					int frameIndex = 0;
+					for (JsonValue valueMap = timelineMap.child; valueMap != null; valueMap = valueMap.next) {
+						timeline.setFrame(frameIndex, valueMap.getFloat("time"), valueMap.getFloat("rotateMix", 1),
+							valueMap.getFloat("translateMix", 1));
+						readCurve(valueMap, timeline, frameIndex);
+						frameIndex++;
+					}
+					timelines.add(timeline);
+					duration = Math.max(duration,
+						timeline.getFrames()[(timeline.getFrameCount() - 1) * PathConstraintMixTimeline.ENTRIES]);
+				}
+			}
+		}
+
+		// 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) {
+				int slotIndex = skeletonData.findSlotIndex(slotMap.name);
+				if (slotIndex == -1) throw new SerializationException("Slot not found: " + slotMap.name);
+				for (JsonValue timelineMap = slotMap.child; timelineMap != null; timelineMap = timelineMap.next) {
+					VertexAttachment attachment = (VertexAttachment)skin.getAttachment(slotIndex, 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 * 2 : vertices.length;
+
+					DeformTimeline timeline = new DeformTimeline(timelineMap.size);
+					timeline.slotIndex = slotIndex;
+					timeline.attachment = attachment;
+
+					int frameIndex = 0;
+					for (JsonValue valueMap = timelineMap.child; valueMap != null; valueMap = valueMap.next) {
+						float[] deform;
+						JsonValue verticesValue = valueMap.get("vertices");
+						if (verticesValue == null)
+							deform = weighted ? new float[deformLength] : vertices;
+						else {
+							deform = new float[deformLength];
+							int start = valueMap.getInt("offset", 0);
+							System.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(frameIndex, valueMap.getFloat("time"), deform);
+						readCurve(valueMap, timeline, frameIndex);
+						frameIndex++;
+					}
+					timelines.add(timeline);
+					duration = Math.max(duration, timeline.getFrames()[timeline.getFrameCount() - 1]);
+				}
+			}
+		}
+
+		// Draw order timeline.
+		JsonValue drawOrdersMap = map.get("drawOrder");
+		if (drawOrdersMap == null) drawOrdersMap = map.get("draworder");
+		if (drawOrdersMap != null) {
+			DrawOrderTimeline timeline = new DrawOrderTimeline(drawOrdersMap.size);
+			int slotCount = skeletonData.slots.size;
+			int frameIndex = 0;
+			for (JsonValue drawOrderMap = drawOrdersMap.child; drawOrderMap != null; drawOrderMap = drawOrderMap.next) {
+				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) {
+						int slotIndex = skeletonData.findSlotIndex(offsetMap.getString("slot"));
+						if (slotIndex == -1) throw new SerializationException("Slot not found: " + offsetMap.getString("slot"));
+						// Collect unchanged items.
+						while (originalIndex != slotIndex)
+							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(frameIndex++, drawOrderMap.getFloat("time"), drawOrder);
+			}
+			timelines.add(timeline);
+			duration = Math.max(duration, timeline.getFrames()[timeline.getFrameCount() - 1]);
+		}
+
+		// Event timeline.
+		JsonValue eventsMap = map.get("events");
+		if (eventsMap != null) {
+			EventTimeline timeline = new EventTimeline(eventsMap.size);
+			int frameIndex = 0;
+			for (JsonValue eventMap = eventsMap.child; eventMap != null; eventMap = eventMap.next) {
+				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"), eventData);
+				event.intValue = eventMap.getInt("int", eventData.getInt());
+				event.floatValue = eventMap.getFloat("float", eventData.getFloat());
+				event.stringValue = eventMap.getString("string", eventData.getString());
+				timeline.setFrame(frameIndex++, event);
+			}
+			timelines.add(timeline);
+			duration = Math.max(duration, timeline.getFrames()[timeline.getFrameCount() - 1]);
+		}
+
+		timelines.shrink();
+		skeletonData.animations.add(new Animation(name, timelines, duration));
+	}
+
+	void readCurve (JsonValue map, CurveTimeline timeline, int frameIndex) {
+		JsonValue curve = map.get("curve");
+		if (curve == null) return;
+		if (curve.isString() && curve.asString().equals("stepped"))
+			timeline.setStepped(frameIndex);
+		else if (curve.isArray()) {
+			timeline.setCurve(frameIndex, curve.getFloat(0), curve.getFloat(1), curve.getFloat(2), curve.getFloat(3));
+		}
+	}
+
+	static class LinkedMesh {
+		String parent, skin;
+		int slotIndex;
+		MeshAttachment mesh;
+
+		public LinkedMesh (MeshAttachment mesh, String skin, int slotIndex, String parent) {
+			this.mesh = mesh;
+			this.skin = skin;
+			this.slotIndex = slotIndex;
+			this.parent = parent;
+		}
+	}
 }

+ 98 - 99
spine-libgdx/spine-libgdx/src/com/esotericsoftware/spine/SkeletonMeshRenderer.java

@@ -1,102 +1,101 @@
-/******************************************************************************
- * Spine Runtimes Software License
- * Version 2.3
- * 
- * Copyright (c) 2013-2015, Esoteric Software
- * All rights reserved.
- * 
- * You are granted a perpetual, non-exclusive, non-sublicensable and
- * non-transferable license to use, install, execute and perform the Spine
- * Runtimes Software (the "Software") and derivative works solely for personal
- * or internal use. Without the written permission of Esoteric Software (see
- * Section 2 of the Spine Software License Agreement), you may not (a) modify,
- * translate, adapt or otherwise create derivative works, improvements of the
- * Software or develop new applications using the Software or (b) remove,
- * delete, alter or obscure any trademarks or any copyright, trademark, patent
- * or other intellectual property or proprietary rights notices on or in the
- * Software, including any copy thereof. Redistributions in binary or source
- * form must include this license and terms.
- * 
- * THIS SOFTWARE IS PROVIDED BY ESOTERIC SOFTWARE "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 BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
- * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
- * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS;
- * OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
- * WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR
- * OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
- * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+/******************************************************************************
+ * Spine Runtimes Software License v2.5
+ * 
+ * Copyright (c) 2013-2016, Esoteric Software
+ * All rights reserved.
+ * 
+ * You are granted a perpetual, non-exclusive, non-sublicensable, and
+ * non-transferable license to use, install, execute, and perform the Spine
+ * Runtimes software and derivative works solely for personal or internal
+ * use. Without the written permission of Esoteric Software (see Section 2 of
+ * the Spine Software License Agreement), you may not (a) modify, translate,
+ * adapt, or develop new applications using the Spine Runtimes or otherwise
+ * create derivative works or improvements of the Spine Runtimes or (b) remove,
+ * delete, alter, or obscure any trademarks or any copyright, trademark, patent,
+ * or other intellectual property or proprietary rights notices on or in the
+ * Software, including any copy thereof. Redistributions in binary or source
+ * form must include this license and terms.
+ * 
+ * THIS SOFTWARE IS PROVIDED BY ESOTERIC SOFTWARE "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 BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
+ * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES, BUSINESS INTERRUPTION, OR LOSS OF
+ * USE, DATA, OR PROFITS) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER
+ * IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+ * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
+ * POSSIBILITY OF SUCH DAMAGE.
  *****************************************************************************/
 
-package com.esotericsoftware.spine;
-
-import com.badlogic.gdx.graphics.Texture;
-import com.badlogic.gdx.graphics.g2d.PolygonSpriteBatch;
-import com.badlogic.gdx.utils.Array;
-import com.esotericsoftware.spine.attachments.Attachment;
-import com.esotericsoftware.spine.attachments.RegionAttachment;
-import com.esotericsoftware.spine.attachments.SkeletonAttachment;
-import com.esotericsoftware.spine.attachments.MeshAttachment;
-
-public class SkeletonMeshRenderer extends SkeletonRenderer<PolygonSpriteBatch> {
-	static private final short[] quadTriangles = {0, 1, 2, 2, 3, 0};
-
-	@SuppressWarnings("null")
-	public void draw (PolygonSpriteBatch batch, Skeleton skeleton) {
-		boolean premultipliedAlpha = this.premultipliedAlpha;
-		BlendMode blendMode = null;
-
-		float[] vertices = null;
-		short[] triangles = null;
-		Array<Slot> drawOrder = skeleton.drawOrder;
-		for (int i = 0, n = drawOrder.size; i < n; i++) {
-			Slot slot = drawOrder.get(i);
-			Attachment attachment = slot.getAttachment();
-			Texture texture = null;
-			if (attachment instanceof RegionAttachment) {
-				RegionAttachment region = (RegionAttachment)attachment;
-				vertices = region.updateWorldVertices(slot, premultipliedAlpha);
-				triangles = quadTriangles;
-				texture = region.getRegion().getTexture();
-
-			} else if (attachment instanceof MeshAttachment) {
-				MeshAttachment mesh = (MeshAttachment)attachment;
-				vertices = mesh.updateWorldVertices(slot, premultipliedAlpha);
-				triangles = mesh.getTriangles();
-				texture = mesh.getRegion().getTexture();
-
-			} else if (attachment instanceof SkeletonAttachment) {
-				Skeleton attachmentSkeleton = ((SkeletonAttachment)attachment).getSkeleton();
-				if (attachmentSkeleton == null) continue;
-				Bone bone = slot.getBone();
-				Bone rootBone = attachmentSkeleton.getRootBone();
-				float oldScaleX = rootBone.getScaleX();
-				float oldScaleY = rootBone.getScaleY();
-				float oldRotation = rootBone.getRotation();
-				attachmentSkeleton.setPosition(bone.getWorldX(), bone.getWorldY());
-				// rootBone.setScaleX(1 + bone.getWorldScaleX() - oldScaleX);
-				// rootBone.setScaleY(1 + bone.getWorldScaleY() - oldScaleY);
-				// Set shear.
-				rootBone.setRotation(oldRotation + bone.getWorldRotationX());
-				attachmentSkeleton.updateWorldTransform();
-
-				draw(batch, attachmentSkeleton);
-
-				attachmentSkeleton.setPosition(0, 0);
-				rootBone.setScaleX(oldScaleX);
-				rootBone.setScaleY(oldScaleY);
-				rootBone.setRotation(oldRotation);
-			}
-
-			if (texture != null) {
-				BlendMode slotBlendMode = slot.data.getBlendMode();
-				if (slotBlendMode != blendMode) {
-					blendMode = slotBlendMode;
-					batch.setBlendFunction(blendMode.getSource(premultipliedAlpha), blendMode.getDest());
-				}
-				batch.draw(texture, vertices, 0, vertices.length, triangles, 0, triangles.length);
-			}
-		}
-	}
+package com.esotericsoftware.spine;
+
+import com.badlogic.gdx.graphics.Texture;
+import com.badlogic.gdx.graphics.g2d.PolygonSpriteBatch;
+import com.badlogic.gdx.utils.Array;
+import com.esotericsoftware.spine.attachments.Attachment;
+import com.esotericsoftware.spine.attachments.RegionAttachment;
+import com.esotericsoftware.spine.attachments.SkeletonAttachment;
+import com.esotericsoftware.spine.attachments.MeshAttachment;
+
+public class SkeletonMeshRenderer extends SkeletonRenderer<PolygonSpriteBatch> {
+	static private final short[] quadTriangles = {0, 1, 2, 2, 3, 0};
+
+	@SuppressWarnings("null")
+	public void draw (PolygonSpriteBatch batch, Skeleton skeleton) {
+		boolean premultipliedAlpha = this.premultipliedAlpha;
+		BlendMode blendMode = null;
+
+		float[] vertices = null;
+		short[] triangles = null;
+		Array<Slot> drawOrder = skeleton.drawOrder;
+		for (int i = 0, n = drawOrder.size; i < n; i++) {
+			Slot slot = drawOrder.get(i);
+			Attachment attachment = slot.getAttachment();
+			Texture texture = null;
+			if (attachment instanceof RegionAttachment) {
+				RegionAttachment region = (RegionAttachment)attachment;
+				vertices = region.updateWorldVertices(slot, premultipliedAlpha);
+				triangles = quadTriangles;
+				texture = region.getRegion().getTexture();
+
+			} else if (attachment instanceof MeshAttachment) {
+				MeshAttachment mesh = (MeshAttachment)attachment;
+				vertices = mesh.updateWorldVertices(slot, premultipliedAlpha);
+				triangles = mesh.getTriangles();
+				texture = mesh.getRegion().getTexture();
+
+			} else if (attachment instanceof SkeletonAttachment) {
+				Skeleton attachmentSkeleton = ((SkeletonAttachment)attachment).getSkeleton();
+				if (attachmentSkeleton == null) continue;
+				Bone bone = slot.getBone();
+				Bone rootBone = attachmentSkeleton.getRootBone();
+				float oldScaleX = rootBone.getScaleX();
+				float oldScaleY = rootBone.getScaleY();
+				float oldRotation = rootBone.getRotation();
+				attachmentSkeleton.setPosition(bone.getWorldX(), bone.getWorldY());
+				// rootBone.setScaleX(1 + bone.getWorldScaleX() - oldScaleX);
+				// rootBone.setScaleY(1 + bone.getWorldScaleY() - oldScaleY);
+				// Set shear.
+				rootBone.setRotation(oldRotation + bone.getWorldRotationX());
+				attachmentSkeleton.updateWorldTransform();
+
+				draw(batch, attachmentSkeleton);
+
+				attachmentSkeleton.setPosition(0, 0);
+				rootBone.setScaleX(oldScaleX);
+				rootBone.setScaleY(oldScaleY);
+				rootBone.setRotation(oldRotation);
+			}
+
+			if (texture != null) {
+				BlendMode slotBlendMode = slot.data.getBlendMode();
+				if (slotBlendMode != blendMode) {
+					blendMode = slotBlendMode;
+					batch.setBlendFunction(blendMode.getSource(premultipliedAlpha), blendMode.getDest());
+				}
+				batch.draw(texture, vertices, 0, vertices.length, triangles, 0, triangles.length);
+			}
+		}
+	}
 }

+ 90 - 91
spine-libgdx/spine-libgdx/src/com/esotericsoftware/spine/SkeletonRenderer.java

@@ -1,94 +1,93 @@
-/******************************************************************************
- * Spine Runtimes Software License
- * Version 2.3
- * 
- * Copyright (c) 2013-2015, Esoteric Software
- * All rights reserved.
- * 
- * You are granted a perpetual, non-exclusive, non-sublicensable and
- * non-transferable license to use, install, execute and perform the Spine
- * Runtimes Software (the "Software") and derivative works solely for personal
- * or internal use. Without the written permission of Esoteric Software (see
- * Section 2 of the Spine Software License Agreement), you may not (a) modify,
- * translate, adapt or otherwise create derivative works, improvements of the
- * Software or develop new applications using the Software or (b) remove,
- * delete, alter or obscure any trademarks or any copyright, trademark, patent
- * or other intellectual property or proprietary rights notices on or in the
- * Software, including any copy thereof. Redistributions in binary or source
- * form must include this license and terms.
- * 
- * THIS SOFTWARE IS PROVIDED BY ESOTERIC SOFTWARE "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 BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
- * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
- * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS;
- * OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
- * WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR
- * OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
- * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+/******************************************************************************
+ * Spine Runtimes Software License v2.5
+ * 
+ * Copyright (c) 2013-2016, Esoteric Software
+ * All rights reserved.
+ * 
+ * You are granted a perpetual, non-exclusive, non-sublicensable, and
+ * non-transferable license to use, install, execute, and perform the Spine
+ * Runtimes software and derivative works solely for personal or internal
+ * use. Without the written permission of Esoteric Software (see Section 2 of
+ * the Spine Software License Agreement), you may not (a) modify, translate,
+ * adapt, or develop new applications using the Spine Runtimes or otherwise
+ * create derivative works or improvements of the Spine Runtimes or (b) remove,
+ * delete, alter, or obscure any trademarks or any copyright, trademark, patent,
+ * or other intellectual property or proprietary rights notices on or in the
+ * Software, including any copy thereof. Redistributions in binary or source
+ * form must include this license and terms.
+ * 
+ * THIS SOFTWARE IS PROVIDED BY ESOTERIC SOFTWARE "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 BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
+ * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES, BUSINESS INTERRUPTION, OR LOSS OF
+ * USE, DATA, OR PROFITS) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER
+ * IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+ * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
+ * POSSIBILITY OF SUCH DAMAGE.
  *****************************************************************************/
 
-package com.esotericsoftware.spine;
-
-import com.badlogic.gdx.graphics.g2d.Batch;
-import com.badlogic.gdx.utils.Array;
-import com.esotericsoftware.spine.attachments.Attachment;
-import com.esotericsoftware.spine.attachments.RegionAttachment;
-import com.esotericsoftware.spine.attachments.SkeletonAttachment;
-import com.esotericsoftware.spine.attachments.MeshAttachment;
-
-public class SkeletonRenderer<T extends Batch> {
-	boolean premultipliedAlpha;
-
-	public void draw (T batch, Skeleton skeleton) {
-		boolean premultipliedAlpha = this.premultipliedAlpha;
-		BlendMode blendMode = null;
-
-		Array<Slot> drawOrder = skeleton.drawOrder;
-		for (int i = 0, n = drawOrder.size; i < n; i++) {
-			Slot slot = drawOrder.get(i);
-			Attachment attachment = slot.getAttachment();
-			if (attachment instanceof RegionAttachment) {
-				RegionAttachment regionAttachment = (RegionAttachment)attachment;
-				float[] vertices = regionAttachment.updateWorldVertices(slot, premultipliedAlpha);
-				BlendMode slotBlendMode = slot.data.getBlendMode();
-				if (slotBlendMode != blendMode) {
-					blendMode = slotBlendMode;
-					batch.setBlendFunction(blendMode.getSource(premultipliedAlpha), blendMode.getDest());
-				}
-				batch.draw(regionAttachment.getRegion().getTexture(), vertices, 0, 20);
-
-			} else if (attachment instanceof MeshAttachment) {
-				throw new RuntimeException("SkeletonMeshRenderer is required to render meshes.");
-
-			} else if (attachment instanceof SkeletonAttachment) {
-				Skeleton attachmentSkeleton = ((SkeletonAttachment)attachment).getSkeleton();
-				if (attachmentSkeleton == null) continue;
-				Bone bone = slot.getBone();
-				Bone rootBone = attachmentSkeleton.getRootBone();
-				float oldScaleX = rootBone.getScaleX();
-				float oldScaleY = rootBone.getScaleY();
-				float oldRotation = rootBone.getRotation();
-				attachmentSkeleton.setPosition(bone.getWorldX(), bone.getWorldY());
-				// rootBone.setScaleX(1 + bone.getWorldScaleX() - oldScaleX);
-				// rootBone.setScaleY(1 + bone.getWorldScaleY() - oldScaleY);
-				// Set shear.
-				rootBone.setRotation(oldRotation + bone.getWorldRotationX());
-				attachmentSkeleton.updateWorldTransform();
-
-				draw(batch, attachmentSkeleton);
-
-				attachmentSkeleton.setX(0);
-				attachmentSkeleton.setY(0);
-				rootBone.setScaleX(oldScaleX);
-				rootBone.setScaleY(oldScaleY);
-				rootBone.setRotation(oldRotation);
-			}
-		}
-	}
-
-	public void setPremultipliedAlpha (boolean premultipliedAlpha) {
-		this.premultipliedAlpha = premultipliedAlpha;
-	}
+package com.esotericsoftware.spine;
+
+import com.badlogic.gdx.graphics.g2d.Batch;
+import com.badlogic.gdx.utils.Array;
+import com.esotericsoftware.spine.attachments.Attachment;
+import com.esotericsoftware.spine.attachments.RegionAttachment;
+import com.esotericsoftware.spine.attachments.SkeletonAttachment;
+import com.esotericsoftware.spine.attachments.MeshAttachment;
+
+public class SkeletonRenderer<T extends Batch> {
+	boolean premultipliedAlpha;
+
+	public void draw (T batch, Skeleton skeleton) {
+		boolean premultipliedAlpha = this.premultipliedAlpha;
+		BlendMode blendMode = null;
+
+		Array<Slot> drawOrder = skeleton.drawOrder;
+		for (int i = 0, n = drawOrder.size; i < n; i++) {
+			Slot slot = drawOrder.get(i);
+			Attachment attachment = slot.getAttachment();
+			if (attachment instanceof RegionAttachment) {
+				RegionAttachment regionAttachment = (RegionAttachment)attachment;
+				float[] vertices = regionAttachment.updateWorldVertices(slot, premultipliedAlpha);
+				BlendMode slotBlendMode = slot.data.getBlendMode();
+				if (slotBlendMode != blendMode) {
+					blendMode = slotBlendMode;
+					batch.setBlendFunction(blendMode.getSource(premultipliedAlpha), blendMode.getDest());
+				}
+				batch.draw(regionAttachment.getRegion().getTexture(), vertices, 0, 20);
+
+			} else if (attachment instanceof MeshAttachment) {
+				throw new RuntimeException("SkeletonMeshRenderer is required to render meshes.");
+
+			} else if (attachment instanceof SkeletonAttachment) {
+				Skeleton attachmentSkeleton = ((SkeletonAttachment)attachment).getSkeleton();
+				if (attachmentSkeleton == null) continue;
+				Bone bone = slot.getBone();
+				Bone rootBone = attachmentSkeleton.getRootBone();
+				float oldScaleX = rootBone.getScaleX();
+				float oldScaleY = rootBone.getScaleY();
+				float oldRotation = rootBone.getRotation();
+				attachmentSkeleton.setPosition(bone.getWorldX(), bone.getWorldY());
+				// rootBone.setScaleX(1 + bone.getWorldScaleX() - oldScaleX);
+				// rootBone.setScaleY(1 + bone.getWorldScaleY() - oldScaleY);
+				// Set shear.
+				rootBone.setRotation(oldRotation + bone.getWorldRotationX());
+				attachmentSkeleton.updateWorldTransform();
+
+				draw(batch, attachmentSkeleton);
+
+				attachmentSkeleton.setX(0);
+				attachmentSkeleton.setY(0);
+				rootBone.setScaleX(oldScaleX);
+				rootBone.setScaleY(oldScaleY);
+				rootBone.setRotation(oldRotation);
+			}
+		}
+	}
+
+	public void setPremultipliedAlpha (boolean premultipliedAlpha) {
+		this.premultipliedAlpha = premultipliedAlpha;
+	}
 }

+ 254 - 255
spine-libgdx/spine-libgdx/src/com/esotericsoftware/spine/SkeletonRendererDebug.java

@@ -1,258 +1,257 @@
-/******************************************************************************
- * Spine Runtimes Software License
- * Version 2.3
- * 
- * Copyright (c) 2013-2015, Esoteric Software
- * All rights reserved.
- * 
- * You are granted a perpetual, non-exclusive, non-sublicensable and
- * non-transferable license to use, install, execute and perform the Spine
- * Runtimes Software (the "Software") and derivative works solely for personal
- * or internal use. Without the written permission of Esoteric Software (see
- * Section 2 of the Spine Software License Agreement), you may not (a) modify,
- * translate, adapt or otherwise create derivative works, improvements of the
- * Software or develop new applications using the Software or (b) remove,
- * delete, alter or obscure any trademarks or any copyright, trademark, patent
- * or other intellectual property or proprietary rights notices on or in the
- * Software, including any copy thereof. Redistributions in binary or source
- * form must include this license and terms.
- * 
- * THIS SOFTWARE IS PROVIDED BY ESOTERIC SOFTWARE "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 BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
- * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
- * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS;
- * OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
- * WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR
- * OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
- * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+/******************************************************************************
+ * Spine Runtimes Software License v2.5
+ * 
+ * Copyright (c) 2013-2016, Esoteric Software
+ * All rights reserved.
+ * 
+ * You are granted a perpetual, non-exclusive, non-sublicensable, and
+ * non-transferable license to use, install, execute, and perform the Spine
+ * Runtimes software and derivative works solely for personal or internal
+ * use. Without the written permission of Esoteric Software (see Section 2 of
+ * the Spine Software License Agreement), you may not (a) modify, translate,
+ * adapt, or develop new applications using the Spine Runtimes or otherwise
+ * create derivative works or improvements of the Spine Runtimes or (b) remove,
+ * delete, alter, or obscure any trademarks or any copyright, trademark, patent,
+ * or other intellectual property or proprietary rights notices on or in the
+ * Software, including any copy thereof. Redistributions in binary or source
+ * form must include this license and terms.
+ * 
+ * THIS SOFTWARE IS PROVIDED BY ESOTERIC SOFTWARE "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 BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
+ * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES, BUSINESS INTERRUPTION, OR LOSS OF
+ * USE, DATA, OR PROFITS) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER
+ * IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+ * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
+ * POSSIBILITY OF SUCH DAMAGE.
  *****************************************************************************/
 
-package com.esotericsoftware.spine;
-
-import static com.badlogic.gdx.graphics.g2d.Batch.*;
-
-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.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.MeshAttachment;
-import com.esotericsoftware.spine.attachments.PathAttachment;
-import com.esotericsoftware.spine.attachments.RegionAttachment;
-
-public class SkeletonRendererDebug {
-	static private final Color boneLineColor = Color.RED;
-	static private final Color boneOriginColor = Color.GREEN;
-	static private final Color attachmentLineColor = new Color(0, 0, 1, 0.5f);
-	static private final Color triangleLineColor = new Color(1, 0.64f, 0, 0.5f);
-	static private final Color aabbColor = new Color(0, 1, 0, 0.5f);
-
-	private final ShapeRenderer shapes;
-	private boolean drawBones = true, drawRegionAttachments = true, drawBoundingBoxes = true;
-	private boolean drawMeshHull = true, drawMeshTriangles = true, drawPaths = true;
-	private final SkeletonBounds bounds = new SkeletonBounds();
-	private final FloatArray temp = new FloatArray();
-	private float scale = 1;
-	private float boneWidth = 2;
-	private boolean premultipliedAlpha;
-
-	public SkeletonRendererDebug () {
-		shapes = new ShapeRenderer();
-	}
-
-	public SkeletonRendererDebug (ShapeRenderer shapes) {
-		this.shapes = shapes;
-	}
-
-	public void draw (Skeleton skeleton) {
-		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();
-		if (drawBones) {
-			shapes.begin(ShapeType.Filled);
-			for (int i = 0, n = bones.size; i < n; i++) {
-				Bone bone = bones.get(i);
-				if (bone.parent == null) 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.end();
-			shapes.begin(ShapeType.Line);
-			shapes.x(skeleton.getX(), skeleton.getY(), 4 * scale);
-		} else
-			shapes.begin(ShapeType.Line);
-
-		if (drawRegionAttachments) {
-			shapes.setColor(attachmentLineColor);
-			Array<Slot> slots = skeleton.getSlots();
-			for (int i = 0, n = slots.size; i < n; i++) {
-				Slot slot = slots.get(i);
-				Attachment attachment = slot.getAttachment();
-				if (attachment instanceof RegionAttachment) {
-					RegionAttachment regionAttachment = (RegionAttachment)attachment;
-					float[] vertices = regionAttachment.updateWorldVertices(slot, false);
-					shapes.line(vertices[X1], vertices[Y1], vertices[X2], vertices[Y2]);
-					shapes.line(vertices[X2], vertices[Y2], vertices[X3], vertices[Y3]);
-					shapes.line(vertices[X3], vertices[Y3], vertices[X4], vertices[Y4]);
-					shapes.line(vertices[X4], vertices[Y4], vertices[X1], vertices[Y1]);
-				}
-			}
-		}
-
-		if (drawMeshHull || drawMeshTriangles) {
-			Array<Slot> slots = skeleton.getSlots();
-			for (int i = 0, n = slots.size; i < n; i++) {
-				Slot slot = slots.get(i);
-				Attachment attachment = slot.getAttachment();
-				if (!(attachment instanceof MeshAttachment)) continue;
-				MeshAttachment mesh = (MeshAttachment)attachment;
-				mesh.updateWorldVertices(slot, false);
-				float[] vertices = mesh.getWorldVertices();
-				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] * 5, v2 = triangles[ii + 1] * 5, v3 = triangles[ii + 2] * 5;
-						shapes.triangle(vertices[v1], vertices[v1 + 1], //
-							vertices[v2], vertices[v2 + 1], //
-							vertices[v3], vertices[v3 + 1] //
-						);
-					}
-				}
-				if (drawMeshHull && hullLength > 0) {
-					shapes.setColor(attachmentLineColor);
-					hullLength = (hullLength >> 1) * 5;
-					float lastX = vertices[hullLength - 5], lastY = vertices[hullLength - 4];
-					for (int ii = 0, nn = hullLength; ii < nn; ii += 5) {
-						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 (drawPaths) {
-			Array<Slot> slots = skeleton.getSlots();
-			for (int i = 0, n = slots.size; i < n; i++) {
-				Slot slot = slots.get(i);
-				Attachment attachment = slot.getAttachment();
-				if (!(attachment instanceof PathAttachment)) continue;
-				PathAttachment path = (PathAttachment)attachment;
-				int nn = path.getWorldVerticesLength();
-				float[] world = temp.setSize(nn);
-				path.computeWorldVertices(slot, world);
-				Color color = path.getColor();
-				float x1 = world[2], y1 = world[3], x2 = 0, y2 = 0;
-				if (path.getClosed()) {
-					shapes.setColor(color);
-					float cx1 = world[0], cy1 = world[1], cx2 = world[nn - 2], cy2 = world[nn - 1];
-					x2 = world[nn - 4];
-					y2 = world[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 = world[ii], cy1 = world[ii + 1], cx2 = world[ii + 2], cy2 = world[ii + 3];
-					x2 = world[ii + 4];
-					y2 = world[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);
-				shapes.setColor(Color.GREEN);
-				shapes.circle(bone.worldX, bone.worldY, 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 setPremultipliedAlpha (boolean premultipliedAlpha) {
-		this.premultipliedAlpha = premultipliedAlpha;
-	}
+package com.esotericsoftware.spine;
+
+import static com.badlogic.gdx.graphics.g2d.Batch.*;
+
+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.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.MeshAttachment;
+import com.esotericsoftware.spine.attachments.PathAttachment;
+import com.esotericsoftware.spine.attachments.RegionAttachment;
+
+public class SkeletonRendererDebug {
+	static private final Color boneLineColor = Color.RED;
+	static private final Color boneOriginColor = Color.GREEN;
+	static private final Color attachmentLineColor = new Color(0, 0, 1, 0.5f);
+	static private final Color triangleLineColor = new Color(1, 0.64f, 0, 0.5f);
+	static private final Color aabbColor = new Color(0, 1, 0, 0.5f);
+
+	private final ShapeRenderer shapes;
+	private boolean drawBones = true, drawRegionAttachments = true, drawBoundingBoxes = true;
+	private boolean drawMeshHull = true, drawMeshTriangles = true, drawPaths = true;
+	private final SkeletonBounds bounds = new SkeletonBounds();
+	private final FloatArray temp = new FloatArray();
+	private float scale = 1;
+	private float boneWidth = 2;
+	private boolean premultipliedAlpha;
+
+	public SkeletonRendererDebug () {
+		shapes = new ShapeRenderer();
+	}
+
+	public SkeletonRendererDebug (ShapeRenderer shapes) {
+		this.shapes = shapes;
+	}
+
+	public void draw (Skeleton skeleton) {
+		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();
+		if (drawBones) {
+			shapes.begin(ShapeType.Filled);
+			for (int i = 0, n = bones.size; i < n; i++) {
+				Bone bone = bones.get(i);
+				if (bone.parent == null) 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.end();
+			shapes.begin(ShapeType.Line);
+			shapes.x(skeleton.getX(), skeleton.getY(), 4 * scale);
+		} else
+			shapes.begin(ShapeType.Line);
+
+		if (drawRegionAttachments) {
+			shapes.setColor(attachmentLineColor);
+			Array<Slot> slots = skeleton.getSlots();
+			for (int i = 0, n = slots.size; i < n; i++) {
+				Slot slot = slots.get(i);
+				Attachment attachment = slot.getAttachment();
+				if (attachment instanceof RegionAttachment) {
+					RegionAttachment regionAttachment = (RegionAttachment)attachment;
+					float[] vertices = regionAttachment.updateWorldVertices(slot, false);
+					shapes.line(vertices[X1], vertices[Y1], vertices[X2], vertices[Y2]);
+					shapes.line(vertices[X2], vertices[Y2], vertices[X3], vertices[Y3]);
+					shapes.line(vertices[X3], vertices[Y3], vertices[X4], vertices[Y4]);
+					shapes.line(vertices[X4], vertices[Y4], vertices[X1], vertices[Y1]);
+				}
+			}
+		}
+
+		if (drawMeshHull || drawMeshTriangles) {
+			Array<Slot> slots = skeleton.getSlots();
+			for (int i = 0, n = slots.size; i < n; i++) {
+				Slot slot = slots.get(i);
+				Attachment attachment = slot.getAttachment();
+				if (!(attachment instanceof MeshAttachment)) continue;
+				MeshAttachment mesh = (MeshAttachment)attachment;
+				mesh.updateWorldVertices(slot, false);
+				float[] vertices = mesh.getWorldVertices();
+				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] * 5, v2 = triangles[ii + 1] * 5, v3 = triangles[ii + 2] * 5;
+						shapes.triangle(vertices[v1], vertices[v1 + 1], //
+							vertices[v2], vertices[v2 + 1], //
+							vertices[v3], vertices[v3 + 1] //
+						);
+					}
+				}
+				if (drawMeshHull && hullLength > 0) {
+					shapes.setColor(attachmentLineColor);
+					hullLength = (hullLength >> 1) * 5;
+					float lastX = vertices[hullLength - 5], lastY = vertices[hullLength - 4];
+					for (int ii = 0, nn = hullLength; ii < nn; ii += 5) {
+						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 (drawPaths) {
+			Array<Slot> slots = skeleton.getSlots();
+			for (int i = 0, n = slots.size; i < n; i++) {
+				Slot slot = slots.get(i);
+				Attachment attachment = slot.getAttachment();
+				if (!(attachment instanceof PathAttachment)) continue;
+				PathAttachment path = (PathAttachment)attachment;
+				int nn = path.getWorldVerticesLength();
+				float[] world = temp.setSize(nn);
+				path.computeWorldVertices(slot, world);
+				Color color = path.getColor();
+				float x1 = world[2], y1 = world[3], x2 = 0, y2 = 0;
+				if (path.getClosed()) {
+					shapes.setColor(color);
+					float cx1 = world[0], cy1 = world[1], cx2 = world[nn - 2], cy2 = world[nn - 1];
+					x2 = world[nn - 4];
+					y2 = world[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 = world[ii], cy1 = world[ii + 1], cx2 = world[ii + 2], cy2 = world[ii + 3];
+					x2 = world[ii + 4];
+					y2 = world[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);
+				shapes.setColor(Color.GREEN);
+				shapes.circle(bone.worldX, bone.worldY, 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 setPremultipliedAlpha (boolean premultipliedAlpha) {
+		this.premultipliedAlpha = premultipliedAlpha;
+	}
 }

+ 136 - 137
spine-libgdx/spine-libgdx/src/com/esotericsoftware/spine/Skin.java

@@ -1,140 +1,139 @@
-/******************************************************************************
- * Spine Runtimes Software License
- * Version 2.3
- * 
- * Copyright (c) 2013-2015, Esoteric Software
- * All rights reserved.
- * 
- * You are granted a perpetual, non-exclusive, non-sublicensable and
- * non-transferable license to use, install, execute and perform the Spine
- * Runtimes Software (the "Software") and derivative works solely for personal
- * or internal use. Without the written permission of Esoteric Software (see
- * Section 2 of the Spine Software License Agreement), you may not (a) modify,
- * translate, adapt or otherwise create derivative works, improvements of the
- * Software or develop new applications using the Software or (b) remove,
- * delete, alter or obscure any trademarks or any copyright, trademark, patent
- * or other intellectual property or proprietary rights notices on or in the
- * Software, including any copy thereof. Redistributions in binary or source
- * form must include this license and terms.
- * 
- * THIS SOFTWARE IS PROVIDED BY ESOTERIC SOFTWARE "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 BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
- * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
- * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS;
- * OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
- * WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR
- * OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
- * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+/******************************************************************************
+ * Spine Runtimes Software License v2.5
+ * 
+ * Copyright (c) 2013-2016, Esoteric Software
+ * All rights reserved.
+ * 
+ * You are granted a perpetual, non-exclusive, non-sublicensable, and
+ * non-transferable license to use, install, execute, and perform the Spine
+ * Runtimes software and derivative works solely for personal or internal
+ * use. Without the written permission of Esoteric Software (see Section 2 of
+ * the Spine Software License Agreement), you may not (a) modify, translate,
+ * adapt, or develop new applications using the Spine Runtimes or otherwise
+ * create derivative works or improvements of the Spine Runtimes or (b) remove,
+ * delete, alter, or obscure any trademarks or any copyright, trademark, patent,
+ * or other intellectual property or proprietary rights notices on or in the
+ * Software, including any copy thereof. Redistributions in binary or source
+ * form must include this license and terms.
+ * 
+ * THIS SOFTWARE IS PROVIDED BY ESOTERIC SOFTWARE "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 BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
+ * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES, BUSINESS INTERRUPTION, OR LOSS OF
+ * USE, DATA, OR PROFITS) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER
+ * IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+ * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
+ * POSSIBILITY OF SUCH DAMAGE.
  *****************************************************************************/
 
-package com.esotericsoftware.spine;
-
-import com.badlogic.gdx.utils.Array;
-import com.badlogic.gdx.utils.ObjectMap;
-import com.badlogic.gdx.utils.ObjectMap.Entry;
-import com.badlogic.gdx.utils.Pool;
-import com.esotericsoftware.spine.attachments.Attachment;
-
-/** Stores attachments by slot index and attachment name. */
-public class Skin {
-	static private final Key lookup = new Key();
-
-	final String name;
-	final ObjectMap<Key, Attachment> attachments = new ObjectMap();
-	final Pool<Key> keyPool = new Pool(64) {
-		protected Object newObject () {
-			return new Key();
-		}
-	};
-
-	public Skin (String name) {
-		if (name == null) throw new IllegalArgumentException("name cannot be null.");
-		this.name = name;
-	}
-
-	public void addAttachment (int slotIndex, String name, Attachment attachment) {
-		if (attachment == null) throw new IllegalArgumentException("attachment cannot be null.");
-		if (slotIndex < 0) throw new IllegalArgumentException("slotIndex must be >= 0.");
-		Key key = keyPool.obtain();
-		key.set(slotIndex, name);
-		attachments.put(key, attachment);
-	}
-
-	/** @return May be null. */
-	public Attachment getAttachment (int slotIndex, String name) {
-		if (slotIndex < 0) throw new IllegalArgumentException("slotIndex must be >= 0.");
-		lookup.set(slotIndex, name);
-		return attachments.get(lookup);
-	}
-
-	public void findNamesForSlot (int slotIndex, Array<String> names) {
-		if (names == null) throw new IllegalArgumentException("names cannot be null.");
-		if (slotIndex < 0) throw new IllegalArgumentException("slotIndex must be >= 0.");
-		for (Key key : attachments.keys())
-			if (key.slotIndex == slotIndex) names.add(key.name);
-	}
-
-	public void findAttachmentsForSlot (int slotIndex, Array<Attachment> attachments) {
-		if (attachments == null) throw new IllegalArgumentException("attachments cannot be null.");
-		if (slotIndex < 0) throw new IllegalArgumentException("slotIndex must be >= 0.");
-		for (Entry<Key, Attachment> entry : this.attachments.entries())
-			if (entry.key.slotIndex == slotIndex) attachments.add(entry.value);
-	}
-
-	public void clear () {
-		for (Key key : attachments.keys())
-			keyPool.free(key);
-		attachments.clear();
-	}
-
-	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) {
-		for (Entry<Key, Attachment> entry : oldSkin.attachments.entries()) {
-			int slotIndex = entry.key.slotIndex;
-			Slot slot = skeleton.slots.get(slotIndex);
-			if (slot.getAttachment() == entry.value) {
-				Attachment attachment = getAttachment(slotIndex, entry.key.name);
-				if (attachment != null) slot.setAttachment(attachment);
-			}
-		}
-	}
-
-	static class Key {
-		int slotIndex;
-		String name;
-		int hashCode;
-
-		public void set (int slotIndex, String name) {
-			if (name == null) throw new IllegalArgumentException("name cannot be null.");
-			this.slotIndex = slotIndex;
-			this.name = name;
-			hashCode = 31 * (31 + name.hashCode()) + slotIndex;
-		}
-
-		public int hashCode () {
-			return hashCode;
-		}
-
-		public boolean equals (Object object) {
-			if (object == null) return false;
-			Key other = (Key)object;
-			if (slotIndex != other.slotIndex) return false;
-			if (!name.equals(other.name)) return false;
-			return true;
-		}
-
-		public String toString () {
-			return slotIndex + ":" + name;
-		}
-	}
+package com.esotericsoftware.spine;
+
+import com.badlogic.gdx.utils.Array;
+import com.badlogic.gdx.utils.ObjectMap;
+import com.badlogic.gdx.utils.ObjectMap.Entry;
+import com.badlogic.gdx.utils.Pool;
+import com.esotericsoftware.spine.attachments.Attachment;
+
+/** Stores attachments by slot index and attachment name. */
+public class Skin {
+	static private final Key lookup = new Key();
+
+	final String name;
+	final ObjectMap<Key, Attachment> attachments = new ObjectMap();
+	final Pool<Key> keyPool = new Pool(64) {
+		protected Object newObject () {
+			return new Key();
+		}
+	};
+
+	public Skin (String name) {
+		if (name == null) throw new IllegalArgumentException("name cannot be null.");
+		this.name = name;
+	}
+
+	public void addAttachment (int slotIndex, String name, Attachment attachment) {
+		if (attachment == null) throw new IllegalArgumentException("attachment cannot be null.");
+		if (slotIndex < 0) throw new IllegalArgumentException("slotIndex must be >= 0.");
+		Key key = keyPool.obtain();
+		key.set(slotIndex, name);
+		attachments.put(key, attachment);
+	}
+
+	/** @return May be null. */
+	public Attachment getAttachment (int slotIndex, String name) {
+		if (slotIndex < 0) throw new IllegalArgumentException("slotIndex must be >= 0.");
+		lookup.set(slotIndex, name);
+		return attachments.get(lookup);
+	}
+
+	public void findNamesForSlot (int slotIndex, Array<String> names) {
+		if (names == null) throw new IllegalArgumentException("names cannot be null.");
+		if (slotIndex < 0) throw new IllegalArgumentException("slotIndex must be >= 0.");
+		for (Key key : attachments.keys())
+			if (key.slotIndex == slotIndex) names.add(key.name);
+	}
+
+	public void findAttachmentsForSlot (int slotIndex, Array<Attachment> attachments) {
+		if (attachments == null) throw new IllegalArgumentException("attachments cannot be null.");
+		if (slotIndex < 0) throw new IllegalArgumentException("slotIndex must be >= 0.");
+		for (Entry<Key, Attachment> entry : this.attachments.entries())
+			if (entry.key.slotIndex == slotIndex) attachments.add(entry.value);
+	}
+
+	public void clear () {
+		for (Key key : attachments.keys())
+			keyPool.free(key);
+		attachments.clear();
+	}
+
+	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) {
+		for (Entry<Key, Attachment> entry : oldSkin.attachments.entries()) {
+			int slotIndex = entry.key.slotIndex;
+			Slot slot = skeleton.slots.get(slotIndex);
+			if (slot.getAttachment() == entry.value) {
+				Attachment attachment = getAttachment(slotIndex, entry.key.name);
+				if (attachment != null) slot.setAttachment(attachment);
+			}
+		}
+	}
+
+	static class Key {
+		int slotIndex;
+		String name;
+		int hashCode;
+
+		public void set (int slotIndex, String name) {
+			if (name == null) throw new IllegalArgumentException("name cannot be null.");
+			this.slotIndex = slotIndex;
+			this.name = name;
+			hashCode = 31 * (31 + name.hashCode()) + slotIndex;
+		}
+
+		public int hashCode () {
+			return hashCode;
+		}
+
+		public boolean equals (Object object) {
+			if (object == null) return false;
+			Key other = (Key)object;
+			if (slotIndex != other.slotIndex) return false;
+			if (!name.equals(other.name)) return false;
+			return true;
+		}
+
+		public String toString () {
+			return slotIndex + ":" + name;
+		}
+	}
 }

+ 196 - 167
spine-libgdx/spine-libgdx/src/com/esotericsoftware/spine/TransformConstraint.java

@@ -1,168 +1,197 @@
-
-package com.esotericsoftware.spine;
-
-import static com.badlogic.gdx.math.MathUtils.*;
-
-import com.badlogic.gdx.math.Vector2;
-import com.badlogic.gdx.utils.Array;
-
-public class TransformConstraint implements Constraint {
-	final TransformConstraintData data;
-	final Array<Bone> bones;
-	Bone target;
-	float rotateMix, translateMix, scaleMix, shearMix;
-	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;
-		rotateMix = data.rotateMix;
-		translateMix = data.translateMix;
-		scaleMix = data.scaleMix;
-		shearMix = data.shearMix;
-		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);
-		rotateMix = constraint.rotateMix;
-		translateMix = constraint.translateMix;
-		scaleMix = constraint.scaleMix;
-		shearMix = constraint.shearMix;
-	}
-
-	public void apply () {
-		update();
-	}
-
-	public void update () {
-		float rotateMix = this.rotateMix, translateMix = this.translateMix, scaleMix = this.scaleMix, shearMix = this.shearMix;
-		Bone target = this.target;
-		float ta = target.a, tb = target.b, tc = target.c, td = target.d;
-		Array<Bone> bones = this.bones;
-		for (int i = 0, n = bones.size; i < n; i++) {
-			Bone bone = bones.get(i);
-			boolean modified = false;
-
-			if (rotateMix != 0) {
-				float a = bone.a, b = bone.b, c = bone.c, d = bone.d;
-				float r = atan2(tc, ta) - atan2(c, a) + data.offsetRotation * degRad;
-				if (r > PI)
-					r -= PI2;
-				else if (r < -PI) r += PI2;
-				r *= rotateMix;
-				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;
-				modified = true;
-			}
-
-			if (translateMix != 0) {
-				Vector2 temp = this.temp;
-				target.localToWorld(temp.set(data.offsetX, data.offsetY));
-				bone.worldX += (temp.x - bone.worldX) * translateMix;
-				bone.worldY += (temp.y - bone.worldY) * translateMix;
-				modified = true;
-			}
-
-			if (scaleMix > 0) {
-				float s = (float)Math.sqrt(bone.a * bone.a + bone.c * bone.c);
-				float ts = (float)Math.sqrt(ta * ta + tc * tc);
-				if (s > 0.00001f) s = (s + (ts - s + data.offsetScaleX) * scaleMix) / s;
-				bone.a *= s;
-				bone.c *= s;
-				s = (float)Math.sqrt(bone.b * bone.b + bone.d * bone.d);
-				ts = (float)Math.sqrt(tb * tb + td * td);
-				if (s > 0.00001f) s = (s + (ts - s + data.offsetScaleY) * scaleMix) / s;
-				bone.b *= s;
-				bone.d *= s;
-				modified = true;
-			}
-
-			if (shearMix > 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 + data.offsetShearY * degRad) * shearMix;
-				float s = (float)Math.sqrt(b * b + d * d);
-				bone.b = cos(r) * s;
-				bone.d = sin(r) * s;
-				modified = true;
-			}
-
-			if (modified) bone.appliedValid = false;
-		}
-	}
-
-	public int getOrder () {
-		return data.order;
-	}
-
-	public Array<Bone> getBones () {
-		return bones;
-	}
-
-	public Bone getTarget () {
-		return target;
-	}
-
-	public void setTarget (Bone target) {
-		this.target = target;
-	}
-
-	public float getRotateMix () {
-		return rotateMix;
-	}
-
-	public void setRotateMix (float rotateMix) {
-		this.rotateMix = rotateMix;
-	}
-
-	public float getTranslateMix () {
-		return translateMix;
-	}
-
-	public void setTranslateMix (float translateMix) {
-		this.translateMix = translateMix;
-	}
-
-	public float getScaleMix () {
-		return scaleMix;
-	}
-
-	public void setScaleMix (float scaleMix) {
-		this.scaleMix = scaleMix;
-	}
-
-	public float getShearMix () {
-		return shearMix;
-	}
-
-	public void setShearMix (float shearMix) {
-		this.shearMix = shearMix;
-	}
-
-	public TransformConstraintData getData () {
-		return data;
-	}
-
-	public String toString () {
-		return data.name;
-	}
+/******************************************************************************
+ * Spine Runtimes Software License v2.5
+ * 
+ * Copyright (c) 2013-2016, Esoteric Software
+ * All rights reserved.
+ * 
+ * You are granted a perpetual, non-exclusive, non-sublicensable, and
+ * non-transferable license to use, install, execute, and perform the Spine
+ * Runtimes software and derivative works solely for personal or internal
+ * use. Without the written permission of Esoteric Software (see Section 2 of
+ * the Spine Software License Agreement), you may not (a) modify, translate,
+ * adapt, or develop new applications using the Spine Runtimes or otherwise
+ * create derivative works or improvements of the Spine Runtimes or (b) remove,
+ * delete, alter, or obscure any trademarks or any copyright, trademark, patent,
+ * or other intellectual property or proprietary rights notices on or in the
+ * Software, including any copy thereof. Redistributions in binary or source
+ * form must include this license and terms.
+ * 
+ * THIS SOFTWARE IS PROVIDED BY ESOTERIC SOFTWARE "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 BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
+ * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES, BUSINESS INTERRUPTION, OR LOSS OF
+ * USE, DATA, OR PROFITS) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER
+ * IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+ * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
+ * POSSIBILITY OF SUCH DAMAGE.
+ *****************************************************************************/
+
+package com.esotericsoftware.spine;
+
+import static com.badlogic.gdx.math.MathUtils.*;
+
+import com.badlogic.gdx.math.Vector2;
+import com.badlogic.gdx.utils.Array;
+
+public class TransformConstraint implements Constraint {
+	final TransformConstraintData data;
+	final Array<Bone> bones;
+	Bone target;
+	float rotateMix, translateMix, scaleMix, shearMix;
+	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;
+		rotateMix = data.rotateMix;
+		translateMix = data.translateMix;
+		scaleMix = data.scaleMix;
+		shearMix = data.shearMix;
+		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);
+		rotateMix = constraint.rotateMix;
+		translateMix = constraint.translateMix;
+		scaleMix = constraint.scaleMix;
+		shearMix = constraint.shearMix;
+	}
+
+	public void apply () {
+		update();
+	}
+
+	public void update () {
+		float rotateMix = this.rotateMix, translateMix = this.translateMix, scaleMix = this.scaleMix, shearMix = this.shearMix;
+		Bone target = this.target;
+		float ta = target.a, tb = target.b, tc = target.c, td = target.d;
+		Array<Bone> bones = this.bones;
+		for (int i = 0, n = bones.size; i < n; i++) {
+			Bone bone = bones.get(i);
+			boolean modified = false;
+
+			if (rotateMix != 0) {
+				float a = bone.a, b = bone.b, c = bone.c, d = bone.d;
+				float r = atan2(tc, ta) - atan2(c, a) + data.offsetRotation * degRad;
+				if (r > PI)
+					r -= PI2;
+				else if (r < -PI) r += PI2;
+				r *= rotateMix;
+				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;
+				modified = true;
+			}
+
+			if (translateMix != 0) {
+				Vector2 temp = this.temp;
+				target.localToWorld(temp.set(data.offsetX, data.offsetY));
+				bone.worldX += (temp.x - bone.worldX) * translateMix;
+				bone.worldY += (temp.y - bone.worldY) * translateMix;
+				modified = true;
+			}
+
+			if (scaleMix > 0) {
+				float s = (float)Math.sqrt(bone.a * bone.a + bone.c * bone.c);
+				float ts = (float)Math.sqrt(ta * ta + tc * tc);
+				if (s > 0.00001f) s = (s + (ts - s + data.offsetScaleX) * scaleMix) / s;
+				bone.a *= s;
+				bone.c *= s;
+				s = (float)Math.sqrt(bone.b * bone.b + bone.d * bone.d);
+				ts = (float)Math.sqrt(tb * tb + td * td);
+				if (s > 0.00001f) s = (s + (ts - s + data.offsetScaleY) * scaleMix) / s;
+				bone.b *= s;
+				bone.d *= s;
+				modified = true;
+			}
+
+			if (shearMix > 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 + data.offsetShearY * degRad) * shearMix;
+				float s = (float)Math.sqrt(b * b + d * d);
+				bone.b = cos(r) * s;
+				bone.d = sin(r) * s;
+				modified = true;
+			}
+
+			if (modified) bone.appliedValid = false;
+		}
+	}
+
+	public int getOrder () {
+		return data.order;
+	}
+
+	public Array<Bone> getBones () {
+		return bones;
+	}
+
+	public Bone getTarget () {
+		return target;
+	}
+
+	public void setTarget (Bone target) {
+		this.target = target;
+	}
+
+	public float getRotateMix () {
+		return rotateMix;
+	}
+
+	public void setRotateMix (float rotateMix) {
+		this.rotateMix = rotateMix;
+	}
+
+	public float getTranslateMix () {
+		return translateMix;
+	}
+
+	public void setTranslateMix (float translateMix) {
+		this.translateMix = translateMix;
+	}
+
+	public float getScaleMix () {
+		return scaleMix;
+	}
+
+	public void setScaleMix (float scaleMix) {
+		this.scaleMix = scaleMix;
+	}
+
+	public float getShearMix () {
+		return shearMix;
+	}
+
+	public void setShearMix (float shearMix) {
+		this.shearMix = shearMix;
+	}
+
+	public TransformConstraintData getData () {
+		return data;
+	}
+
+	public String toString () {
+		return data.name;
+	}
 }

+ 155 - 126
spine-libgdx/spine-libgdx/src/com/esotericsoftware/spine/TransformConstraintData.java

@@ -1,127 +1,156 @@
-
-package com.esotericsoftware.spine;
-
-import com.badlogic.gdx.utils.Array;
-
-public class TransformConstraintData {
-	final String name;
-	int order;
-	final Array<BoneData> bones = new Array();
-	BoneData target;
-	float rotateMix, translateMix, scaleMix, shearMix;
-	float offsetRotation, offsetX, offsetY, offsetScaleX, offsetScaleY, offsetShearY;
-
-	public TransformConstraintData (String name) {
-		if (name == null) throw new IllegalArgumentException("name cannot be null.");
-		this.name = name;
-	}
-
-	public String getName () {
-		return name;
-	}
-
-	public int getOrder () {
-		return order;
-	}
-
-	public void setOrder (int order) {
-		this.order = order;
-	}
-
-	public Array<BoneData> getBones () {
-		return bones;
-	}
-
-	public BoneData getTarget () {
-		return target;
-	}
-
-	public void setTarget (BoneData target) {
-		if (target == null) throw new IllegalArgumentException("target cannot be null.");
-		this.target = target;
-	}
-
-	public float getRotateMix () {
-		return rotateMix;
-	}
-
-	public void setRotateMix (float rotateMix) {
-		this.rotateMix = rotateMix;
-	}
-
-	public float getTranslateMix () {
-		return translateMix;
-	}
-
-	public void setTranslateMix (float translateMix) {
-		this.translateMix = translateMix;
-	}
-
-	public float getScaleMix () {
-		return scaleMix;
-	}
-
-	public void setScaleMix (float scaleMix) {
-		this.scaleMix = scaleMix;
-	}
-
-	public float getShearMix () {
-		return shearMix;
-	}
-
-	public void setShearMix (float shearMix) {
-		this.shearMix = shearMix;
-	}
-
-	public float getOffsetRotation () {
-		return offsetRotation;
-	}
-
-	public void setOffsetRotation (float offsetRotation) {
-		this.offsetRotation = offsetRotation;
-	}
-
-	public float getOffsetX () {
-		return offsetX;
-	}
-
-	public void setOffsetX (float offsetX) {
-		this.offsetX = offsetX;
-	}
-
-	public float getOffsetY () {
-		return offsetY;
-	}
-
-	public void setOffsetY (float offsetY) {
-		this.offsetY = offsetY;
-	}
-
-	public float getOffsetScaleX () {
-		return offsetScaleX;
-	}
-
-	public void setOffsetScaleX (float offsetScaleX) {
-		this.offsetScaleX = offsetScaleX;
-	}
-
-	public float getOffsetScaleY () {
-		return offsetScaleY;
-	}
-
-	public void setOffsetScaleY (float offsetScaleY) {
-		this.offsetScaleY = offsetScaleY;
-	}
-
-	public float getOffsetShearY () {
-		return offsetShearY;
-	}
-
-	public void setOffsetShearY (float offsetShearY) {
-		this.offsetShearY = offsetShearY;
-	}
-
-	public String toString () {
-		return name;
-	}
+/******************************************************************************
+ * Spine Runtimes Software License v2.5
+ * 
+ * Copyright (c) 2013-2016, Esoteric Software
+ * All rights reserved.
+ * 
+ * You are granted a perpetual, non-exclusive, non-sublicensable, and
+ * non-transferable license to use, install, execute, and perform the Spine
+ * Runtimes software and derivative works solely for personal or internal
+ * use. Without the written permission of Esoteric Software (see Section 2 of
+ * the Spine Software License Agreement), you may not (a) modify, translate,
+ * adapt, or develop new applications using the Spine Runtimes or otherwise
+ * create derivative works or improvements of the Spine Runtimes or (b) remove,
+ * delete, alter, or obscure any trademarks or any copyright, trademark, patent,
+ * or other intellectual property or proprietary rights notices on or in the
+ * Software, including any copy thereof. Redistributions in binary or source
+ * form must include this license and terms.
+ * 
+ * THIS SOFTWARE IS PROVIDED BY ESOTERIC SOFTWARE "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 BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
+ * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES, BUSINESS INTERRUPTION, OR LOSS OF
+ * USE, DATA, OR PROFITS) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER
+ * IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+ * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
+ * POSSIBILITY OF SUCH DAMAGE.
+ *****************************************************************************/
+
+package com.esotericsoftware.spine;
+
+import com.badlogic.gdx.utils.Array;
+
+public class TransformConstraintData {
+	final String name;
+	int order;
+	final Array<BoneData> bones = new Array();
+	BoneData target;
+	float rotateMix, translateMix, scaleMix, shearMix;
+	float offsetRotation, offsetX, offsetY, offsetScaleX, offsetScaleY, offsetShearY;
+
+	public TransformConstraintData (String name) {
+		if (name == null) throw new IllegalArgumentException("name cannot be null.");
+		this.name = name;
+	}
+
+	public String getName () {
+		return name;
+	}
+
+	public int getOrder () {
+		return order;
+	}
+
+	public void setOrder (int order) {
+		this.order = order;
+	}
+
+	public Array<BoneData> getBones () {
+		return bones;
+	}
+
+	public BoneData getTarget () {
+		return target;
+	}
+
+	public void setTarget (BoneData target) {
+		if (target == null) throw new IllegalArgumentException("target cannot be null.");
+		this.target = target;
+	}
+
+	public float getRotateMix () {
+		return rotateMix;
+	}
+
+	public void setRotateMix (float rotateMix) {
+		this.rotateMix = rotateMix;
+	}
+
+	public float getTranslateMix () {
+		return translateMix;
+	}
+
+	public void setTranslateMix (float translateMix) {
+		this.translateMix = translateMix;
+	}
+
+	public float getScaleMix () {
+		return scaleMix;
+	}
+
+	public void setScaleMix (float scaleMix) {
+		this.scaleMix = scaleMix;
+	}
+
+	public float getShearMix () {
+		return shearMix;
+	}
+
+	public void setShearMix (float shearMix) {
+		this.shearMix = shearMix;
+	}
+
+	public float getOffsetRotation () {
+		return offsetRotation;
+	}
+
+	public void setOffsetRotation (float offsetRotation) {
+		this.offsetRotation = offsetRotation;
+	}
+
+	public float getOffsetX () {
+		return offsetX;
+	}
+
+	public void setOffsetX (float offsetX) {
+		this.offsetX = offsetX;
+	}
+
+	public float getOffsetY () {
+		return offsetY;
+	}
+
+	public void setOffsetY (float offsetY) {
+		this.offsetY = offsetY;
+	}
+
+	public float getOffsetScaleX () {
+		return offsetScaleX;
+	}
+
+	public void setOffsetScaleX (float offsetScaleX) {
+		this.offsetScaleX = offsetScaleX;
+	}
+
+	public float getOffsetScaleY () {
+		return offsetScaleY;
+	}
+
+	public void setOffsetScaleY (float offsetScaleY) {
+		this.offsetScaleY = offsetScaleY;
+	}
+
+	public float getOffsetShearY () {
+		return offsetShearY;
+	}
+
+	public void setOffsetShearY (float offsetShearY) {
+		this.offsetShearY = offsetShearY;
+	}
+
+	public String toString () {
+		return name;
+	}
 }

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

@@ -1,261 +1,260 @@
-/******************************************************************************
- * Spine Runtimes Software License
- * Version 2.3
- * 
- * Copyright (c) 2013-2015, Esoteric Software
- * All rights reserved.
- * 
- * You are granted a perpetual, non-exclusive, non-sublicensable and
- * non-transferable license to use, install, execute and perform the Spine
- * Runtimes Software (the "Software") and derivative works solely for personal
- * or internal use. Without the written permission of Esoteric Software (see
- * Section 2 of the Spine Software License Agreement), you may not (a) modify,
- * translate, adapt or otherwise create derivative works, improvements of the
- * Software or develop new applications using the Software or (b) remove,
- * delete, alter or obscure any trademarks or any copyright, trademark, patent
- * or other intellectual property or proprietary rights notices on or in the
- * Software, including any copy thereof. Redistributions in binary or source
- * form must include this license and terms.
- * 
- * THIS SOFTWARE IS PROVIDED BY ESOTERIC SOFTWARE "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 BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
- * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
- * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS;
- * OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
- * WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR
- * OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
- * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+/******************************************************************************
+ * Spine Runtimes Software License v2.5
+ * 
+ * Copyright (c) 2013-2016, Esoteric Software
+ * All rights reserved.
+ * 
+ * You are granted a perpetual, non-exclusive, non-sublicensable, and
+ * non-transferable license to use, install, execute, and perform the Spine
+ * Runtimes software and derivative works solely for personal or internal
+ * use. Without the written permission of Esoteric Software (see Section 2 of
+ * the Spine Software License Agreement), you may not (a) modify, translate,
+ * adapt, or develop new applications using the Spine Runtimes or otherwise
+ * create derivative works or improvements of the Spine Runtimes or (b) remove,
+ * delete, alter, or obscure any trademarks or any copyright, trademark, patent,
+ * or other intellectual property or proprietary rights notices on or in the
+ * Software, including any copy thereof. Redistributions in binary or source
+ * form must include this license and terms.
+ * 
+ * THIS SOFTWARE IS PROVIDED BY ESOTERIC SOFTWARE "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 BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
+ * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES, BUSINESS INTERRUPTION, OR LOSS OF
+ * USE, DATA, OR PROFITS) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER
+ * IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+ * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
+ * POSSIBILITY OF SUCH DAMAGE.
  *****************************************************************************/
 
-package com.esotericsoftware.spine.attachments;
-
-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.FloatArray;
-import com.badlogic.gdx.utils.NumberUtils;
-import com.esotericsoftware.spine.Bone;
-import com.esotericsoftware.spine.Skeleton;
-import com.esotericsoftware.spine.Slot;
-
-/** Attachment that displays a texture region. */
-public class MeshAttachment extends VertexAttachment {
-	private TextureRegion region;
-	private String path;
-	private float[] regionUVs, worldVertices;
-	private short[] triangles;
-	private final Color color = new Color(1, 1, 1, 1);
-	private int hullLength;
-	private MeshAttachment parentMesh;
-	private boolean inheritDeform;
-
-	// Nonessential.
-	private 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;
-	}
-
-	public void updateUVs () {
-		float[] regionUVs = this.regionUVs;
-		int verticesLength = regionUVs.length;
-		int worldVerticesLength = (verticesLength >> 1) * 5;
-		if (worldVertices == null || worldVertices.length != worldVerticesLength) worldVertices = new float[worldVerticesLength];
-
-		float u, v, width, height;
-		if (region == null) {
-			u = v = 0;
-			width = height = 1;
-		} else {
-			u = region.getU();
-			v = region.getV();
-			width = region.getU2() - u;
-			height = region.getV2() - v;
-		}
-		if (region instanceof AtlasRegion && ((AtlasRegion)region).rotate) {
-			for (int i = 0, w = 3; i < verticesLength; i += 2, w += 5) {
-				worldVertices[w] = u + regionUVs[i + 1] * width;
-				worldVertices[w + 1] = v + height - regionUVs[i] * height;
-			}
-		} else {
-			for (int i = 0, w = 3; i < verticesLength; i += 2, w += 5) {
-				worldVertices[w] = u + regionUVs[i] * width;
-				worldVertices[w + 1] = v + regionUVs[i + 1] * height;
-			}
-		}
-	}
-
-	/** @return The updated world vertices. */
-	public float[] updateWorldVertices (Slot slot, boolean premultipliedAlpha) {
-		Skeleton skeleton = slot.getSkeleton();
-		Color skeletonColor = skeleton.getColor(), slotColor = slot.getColor(), meshColor = color;
-		float alpha = skeletonColor.a * slotColor.a * meshColor.a * 255;
-		float multiplier = premultipliedAlpha ? alpha : 255;
-		float color = NumberUtils.intToFloatColor( //
-			((int)alpha << 24) //
-				| ((int)(skeletonColor.b * slotColor.b * meshColor.b * multiplier) << 16) //
-				| ((int)(skeletonColor.g * slotColor.g * meshColor.g * multiplier) << 8) //
-				| (int)(skeletonColor.r * slotColor.r * meshColor.r * multiplier));
-
-		FloatArray deformArray = slot.getAttachmentVertices();
-		float[] vertices = this.vertices, worldVertices = this.worldVertices;
-		int[] bones = this.bones;
-		if (bones == null) {
-			int verticesLength = vertices.length;
-			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 = 0, w = 0; v < verticesLength; v += 2, w += 5) {
-				float vx = vertices[v], vy = vertices[v + 1];
-				worldVertices[w] = vx * a + vy * b + x;
-				worldVertices[w + 1] = vx * c + vy * d + y;
-				worldVertices[w + 2] = color;
-			}
-			return worldVertices;
-		}
-		Object[] skeletonBones = skeleton.getBones().items;
-		if (deformArray.size == 0) {
-			for (int w = 0, v = 0, b = 0, n = bones.length; v < n; w += 5) {
-				float wx = 0, wy = 0;
-				int nn = bones[v++] + v;
-				for (; v < nn; 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;
-				worldVertices[w + 2] = color;
-			}
-		} else {
-			float[] deform = deformArray.items;
-			for (int w = 0, v = 0, b = 0, f = 0, n = bones.length; v < n; w += 5) {
-				float wx = 0, wy = 0;
-				int nn = bones[v++] + v;
-				for (; v < nn; 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;
-				worldVertices[w + 2] = color;
-			}
-		}
-		return worldVertices;
-	}
-
-	public boolean applyDeform (VertexAttachment sourceAttachment) {
-		return this == sourceAttachment || (inheritDeform && parentMesh == sourceAttachment);
-	}
-
-	public float[] getWorldVertices () {
-		return worldVertices;
-	}
-
-	public short[] getTriangles () {
-		return triangles;
-	}
-
-	/** Vertex number triplets which describe the mesh's triangulation. */
-	public void setTriangles (short[] triangles) {
-		this.triangles = triangles;
-	}
-
-	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;
-	}
-
-	public Color getColor () {
-		return color;
-	}
-
-	public String getPath () {
-		return path;
-	}
-
-	public void setPath (String path) {
-		this.path = path;
-	}
-
-	public int getHullLength () {
-		return hullLength;
-	}
-
-	public void setHullLength (int hullLength) {
-		this.hullLength = hullLength;
-	}
-
-	public void setEdges (short[] edges) {
-		this.edges = edges;
-	}
-
-	public short[] getEdges () {
-		return edges;
-	}
-
-	public float getWidth () {
-		return width;
-	}
-
-	public void setWidth (float width) {
-		this.width = width;
-	}
-
-	public float getHeight () {
-		return height;
-	}
-
-	public void setHeight (float height) {
-		this.height = height;
-	}
-
-	/** Returns the source mesh if this is a linked mesh, else returns null. */
-	public MeshAttachment getParentMesh () {
-		return parentMesh;
-	}
-
-	/** @param parentMesh May be null. */
-	public void setParentMesh (MeshAttachment parentMesh) {
-		this.parentMesh = parentMesh;
-		if (parentMesh != null) {
-			bones = parentMesh.bones;
-			vertices = parentMesh.vertices;
-			regionUVs = parentMesh.regionUVs;
-			triangles = parentMesh.triangles;
-			hullLength = parentMesh.hullLength;
-			edges = parentMesh.edges;
-			width = parentMesh.width;
-			height = parentMesh.height;
-		}
-	}
-
-	public boolean getInheritDeform () {
-		return inheritDeform;
-	}
-
-	public void setInheritDeform (boolean inheritDeform) {
-		this.inheritDeform = inheritDeform;
-	}
+package com.esotericsoftware.spine.attachments;
+
+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.FloatArray;
+import com.badlogic.gdx.utils.NumberUtils;
+import com.esotericsoftware.spine.Bone;
+import com.esotericsoftware.spine.Skeleton;
+import com.esotericsoftware.spine.Slot;
+
+/** Attachment that displays a texture region. */
+public class MeshAttachment extends VertexAttachment {
+	private TextureRegion region;
+	private String path;
+	private float[] regionUVs, worldVertices;
+	private short[] triangles;
+	private final Color color = new Color(1, 1, 1, 1);
+	private int hullLength;
+	private MeshAttachment parentMesh;
+	private boolean inheritDeform;
+
+	// Nonessential.
+	private 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;
+	}
+
+	public void updateUVs () {
+		float[] regionUVs = this.regionUVs;
+		int verticesLength = regionUVs.length;
+		int worldVerticesLength = (verticesLength >> 1) * 5;
+		if (worldVertices == null || worldVertices.length != worldVerticesLength) worldVertices = new float[worldVerticesLength];
+
+		float u, v, width, height;
+		if (region == null) {
+			u = v = 0;
+			width = height = 1;
+		} else {
+			u = region.getU();
+			v = region.getV();
+			width = region.getU2() - u;
+			height = region.getV2() - v;
+		}
+		if (region instanceof AtlasRegion && ((AtlasRegion)region).rotate) {
+			for (int i = 0, w = 3; i < verticesLength; i += 2, w += 5) {
+				worldVertices[w] = u + regionUVs[i + 1] * width;
+				worldVertices[w + 1] = v + height - regionUVs[i] * height;
+			}
+		} else {
+			for (int i = 0, w = 3; i < verticesLength; i += 2, w += 5) {
+				worldVertices[w] = u + regionUVs[i] * width;
+				worldVertices[w + 1] = v + regionUVs[i + 1] * height;
+			}
+		}
+	}
+
+	/** @return The updated world vertices. */
+	public float[] updateWorldVertices (Slot slot, boolean premultipliedAlpha) {
+		Skeleton skeleton = slot.getSkeleton();
+		Color skeletonColor = skeleton.getColor(), slotColor = slot.getColor(), meshColor = color;
+		float alpha = skeletonColor.a * slotColor.a * meshColor.a * 255;
+		float multiplier = premultipliedAlpha ? alpha : 255;
+		float color = NumberUtils.intToFloatColor( //
+			((int)alpha << 24) //
+				| ((int)(skeletonColor.b * slotColor.b * meshColor.b * multiplier) << 16) //
+				| ((int)(skeletonColor.g * slotColor.g * meshColor.g * multiplier) << 8) //
+				| (int)(skeletonColor.r * slotColor.r * meshColor.r * multiplier));
+
+		FloatArray deformArray = slot.getAttachmentVertices();
+		float[] vertices = this.vertices, worldVertices = this.worldVertices;
+		int[] bones = this.bones;
+		if (bones == null) {
+			int verticesLength = vertices.length;
+			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 = 0, w = 0; v < verticesLength; v += 2, w += 5) {
+				float vx = vertices[v], vy = vertices[v + 1];
+				worldVertices[w] = vx * a + vy * b + x;
+				worldVertices[w + 1] = vx * c + vy * d + y;
+				worldVertices[w + 2] = color;
+			}
+			return worldVertices;
+		}
+		Object[] skeletonBones = skeleton.getBones().items;
+		if (deformArray.size == 0) {
+			for (int w = 0, v = 0, b = 0, n = bones.length; v < n; w += 5) {
+				float wx = 0, wy = 0;
+				int nn = bones[v++] + v;
+				for (; v < nn; 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;
+				worldVertices[w + 2] = color;
+			}
+		} else {
+			float[] deform = deformArray.items;
+			for (int w = 0, v = 0, b = 0, f = 0, n = bones.length; v < n; w += 5) {
+				float wx = 0, wy = 0;
+				int nn = bones[v++] + v;
+				for (; v < nn; 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;
+				worldVertices[w + 2] = color;
+			}
+		}
+		return worldVertices;
+	}
+
+	public boolean applyDeform (VertexAttachment sourceAttachment) {
+		return this == sourceAttachment || (inheritDeform && parentMesh == sourceAttachment);
+	}
+
+	public float[] getWorldVertices () {
+		return worldVertices;
+	}
+
+	public short[] getTriangles () {
+		return triangles;
+	}
+
+	/** Vertex number triplets which describe the mesh's triangulation. */
+	public void setTriangles (short[] triangles) {
+		this.triangles = triangles;
+	}
+
+	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;
+	}
+
+	public Color getColor () {
+		return color;
+	}
+
+	public String getPath () {
+		return path;
+	}
+
+	public void setPath (String path) {
+		this.path = path;
+	}
+
+	public int getHullLength () {
+		return hullLength;
+	}
+
+	public void setHullLength (int hullLength) {
+		this.hullLength = hullLength;
+	}
+
+	public void setEdges (short[] edges) {
+		this.edges = edges;
+	}
+
+	public short[] getEdges () {
+		return edges;
+	}
+
+	public float getWidth () {
+		return width;
+	}
+
+	public void setWidth (float width) {
+		this.width = width;
+	}
+
+	public float getHeight () {
+		return height;
+	}
+
+	public void setHeight (float height) {
+		this.height = height;
+	}
+
+	/** Returns the source mesh if this is a linked mesh, else returns null. */
+	public MeshAttachment getParentMesh () {
+		return parentMesh;
+	}
+
+	/** @param parentMesh May be null. */
+	public void setParentMesh (MeshAttachment parentMesh) {
+		this.parentMesh = parentMesh;
+		if (parentMesh != null) {
+			bones = parentMesh.bones;
+			vertices = parentMesh.vertices;
+			regionUVs = parentMesh.regionUVs;
+			triangles = parentMesh.triangles;
+			hullLength = parentMesh.hullLength;
+			edges = parentMesh.edges;
+			width = parentMesh.width;
+			height = parentMesh.height;
+		}
+	}
+
+	public boolean getInheritDeform () {
+		return inheritDeform;
+	}
+
+	public void setInheritDeform (boolean inheritDeform) {
+		this.inheritDeform = inheritDeform;
+	}
 }

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

@@ -1,271 +1,270 @@
-/******************************************************************************
- * Spine Runtimes Software License
- * Version 2.3
- * 
- * Copyright (c) 2013-2015, Esoteric Software
- * All rights reserved.
- * 
- * You are granted a perpetual, non-exclusive, non-sublicensable and
- * non-transferable license to use, install, execute and perform the Spine
- * Runtimes Software (the "Software") and derivative works solely for personal
- * or internal use. Without the written permission of Esoteric Software (see
- * Section 2 of the Spine Software License Agreement), you may not (a) modify,
- * translate, adapt or otherwise create derivative works, improvements of the
- * Software or develop new applications using the Software or (b) remove,
- * delete, alter or obscure any trademarks or any copyright, trademark, patent
- * or other intellectual property or proprietary rights notices on or in the
- * Software, including any copy thereof. Redistributions in binary or source
- * form must include this license and terms.
- * 
- * THIS SOFTWARE IS PROVIDED BY ESOTERIC SOFTWARE "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 BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
- * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
- * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS;
- * OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
- * WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR
- * OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
- * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+/******************************************************************************
+ * Spine Runtimes Software License v2.5
+ * 
+ * Copyright (c) 2013-2016, Esoteric Software
+ * All rights reserved.
+ * 
+ * You are granted a perpetual, non-exclusive, non-sublicensable, and
+ * non-transferable license to use, install, execute, and perform the Spine
+ * Runtimes software and derivative works solely for personal or internal
+ * use. Without the written permission of Esoteric Software (see Section 2 of
+ * the Spine Software License Agreement), you may not (a) modify, translate,
+ * adapt, or develop new applications using the Spine Runtimes or otherwise
+ * create derivative works or improvements of the Spine Runtimes or (b) remove,
+ * delete, alter, or obscure any trademarks or any copyright, trademark, patent,
+ * or other intellectual property or proprietary rights notices on or in the
+ * Software, including any copy thereof. Redistributions in binary or source
+ * form must include this license and terms.
+ * 
+ * THIS SOFTWARE IS PROVIDED BY ESOTERIC SOFTWARE "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 BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
+ * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES, BUSINESS INTERRUPTION, OR LOSS OF
+ * USE, DATA, OR PROFITS) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER
+ * IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+ * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
+ * POSSIBILITY OF SUCH DAMAGE.
  *****************************************************************************/
 
-package com.esotericsoftware.spine.attachments;
-
-import com.esotericsoftware.spine.Bone;
-import com.esotericsoftware.spine.Skeleton;
-import com.esotericsoftware.spine.Slot;
-
-import static com.badlogic.gdx.graphics.g2d.Batch.*;
-
-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.math.MathUtils;
-import com.badlogic.gdx.utils.NumberUtils;
-
-/** Attachment that displays a texture region. */
-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[] vertices = new float[20];
-	private final float[] offset = new float[8];
-	private final Color color = new Color(1, 1, 1, 1);
-
-	public RegionAttachment (String name) {
-		super(name);
-	}
-
-	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;
-			if (region.rotate) {
-				localX += region.offsetX / region.originalWidth * width;
-				localY += region.offsetY / region.originalHeight * height;
-				localX2 -= (region.originalWidth - region.offsetX - region.packedHeight) / region.originalWidth * width;
-				localY2 -= (region.originalHeight - region.offsetY - region.packedWidth) / region.originalHeight * height;
-			} else {
-				localX += region.offsetX / region.originalWidth * width;
-				localY += region.offsetY / region.originalHeight * height;
-				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 = MathUtils.cosDeg(rotation);
-		float sin = MathUtils.sinDeg(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[] vertices = this.vertices;
-		if (region instanceof AtlasRegion && ((AtlasRegion)region).rotate) {
-			vertices[U3] = region.getU();
-			vertices[V3] = region.getV2();
-			vertices[U4] = region.getU();
-			vertices[V4] = region.getV();
-			vertices[U1] = region.getU2();
-			vertices[V1] = region.getV();
-			vertices[U2] = region.getU2();
-			vertices[V2] = region.getV2();
-		} else {
-			vertices[U2] = region.getU();
-			vertices[V2] = region.getV2();
-			vertices[U3] = region.getU();
-			vertices[V3] = region.getV();
-			vertices[U4] = region.getU2();
-			vertices[V4] = region.getV();
-			vertices[U1] = region.getU2();
-			vertices[V1] = region.getV2();
-		}
-	}
-
-	public TextureRegion getRegion () {
-		if (region == null) throw new IllegalStateException("Region has not been set: " + this);
-		return region;
-	}
-
-	/** @return The updated world vertices. */
-	public float[] updateWorldVertices (Slot slot, boolean premultipliedAlpha) {
-		Skeleton skeleton = slot.getSkeleton();
-		Color skeletonColor = skeleton.getColor();
-		Color slotColor = slot.getColor();
-		Color regionColor = color;
-		float alpha = skeletonColor.a * slotColor.a * regionColor.a * 255;
-		float multiplier = premultipliedAlpha ? alpha : 255;
-		float color = NumberUtils.intToFloatColor( //
-			((int)alpha << 24) //
-				| ((int)(skeletonColor.b * slotColor.b * regionColor.b * multiplier) << 16) //
-				| ((int)(skeletonColor.g * slotColor.g * regionColor.g * multiplier) << 8) //
-				| (int)(skeletonColor.r * slotColor.r * regionColor.r * multiplier));
-
-		float[] vertices = this.vertices;
-		float[] offset = this.offset;
-		Bone bone = slot.getBone();
-		float x = bone.getWorldX(), y = bone.getWorldY();
-		float a = bone.getA(), b = bone.getB(), c = bone.getC(), d = bone.getD();
-		float offsetX, offsetY;
-
-		offsetX = offset[BRX];
-		offsetY = offset[BRY];
-		vertices[X1] = offsetX * a + offsetY * b + x; // br
-		vertices[Y1] = offsetX * c + offsetY * d + y;
-		vertices[C1] = color;
-
-		offsetX = offset[BLX];
-		offsetY = offset[BLY];
-		vertices[X2] = offsetX * a + offsetY * b + x; // bl
-		vertices[Y2] = offsetX * c + offsetY * d + y;
-		vertices[C2] = color;
-
-		offsetX = offset[ULX];
-		offsetY = offset[ULY];
-		vertices[X3] = offsetX * a + offsetY * b + x; // ul
-		vertices[Y3] = offsetX * c + offsetY * d + y;
-		vertices[C3] = color;
-
-		offsetX = offset[URX];
-		offsetY = offset[URY];
-		vertices[X4] = offsetX * a + offsetY * b + x; // ur
-		vertices[Y4] = offsetX * c + offsetY * d + y;
-		vertices[C4] = color;
-		return vertices;
-	}
-
-	public float[] getWorldVertices () {
-		return vertices;
-	}
-
-	public float[] getOffset () {
-		return offset;
-	}
-
-	public float getX () {
-		return x;
-	}
-
-	public void setX (float x) {
-		this.x = x;
-	}
-
-	public float getY () {
-		return y;
-	}
-
-	public void setY (float y) {
-		this.y = y;
-	}
-
-	public float getScaleX () {
-		return scaleX;
-	}
-
-	public void setScaleX (float scaleX) {
-		this.scaleX = scaleX;
-	}
-
-	public float getScaleY () {
-		return scaleY;
-	}
-
-	public void setScaleY (float scaleY) {
-		this.scaleY = scaleY;
-	}
-
-	public float getRotation () {
-		return rotation;
-	}
-
-	public void setRotation (float rotation) {
-		this.rotation = rotation;
-	}
-
-	public float getWidth () {
-		return width;
-	}
-
-	public void setWidth (float width) {
-		this.width = width;
-	}
-
-	public float getHeight () {
-		return height;
-	}
-
-	public void setHeight (float height) {
-		this.height = height;
-	}
-
-	public Color getColor () {
-		return color;
-	}
-
-	public String getPath () {
-		return path;
-	}
-
-	public void setPath (String path) {
-		this.path = path;
-	}
+package com.esotericsoftware.spine.attachments;
+
+import com.esotericsoftware.spine.Bone;
+import com.esotericsoftware.spine.Skeleton;
+import com.esotericsoftware.spine.Slot;
+
+import static com.badlogic.gdx.graphics.g2d.Batch.*;
+
+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.math.MathUtils;
+import com.badlogic.gdx.utils.NumberUtils;
+
+/** Attachment that displays a texture region. */
+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[] vertices = new float[20];
+	private final float[] offset = new float[8];
+	private final Color color = new Color(1, 1, 1, 1);
+
+	public RegionAttachment (String name) {
+		super(name);
+	}
+
+	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;
+			if (region.rotate) {
+				localX += region.offsetX / region.originalWidth * width;
+				localY += region.offsetY / region.originalHeight * height;
+				localX2 -= (region.originalWidth - region.offsetX - region.packedHeight) / region.originalWidth * width;
+				localY2 -= (region.originalHeight - region.offsetY - region.packedWidth) / region.originalHeight * height;
+			} else {
+				localX += region.offsetX / region.originalWidth * width;
+				localY += region.offsetY / region.originalHeight * height;
+				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 = MathUtils.cosDeg(rotation);
+		float sin = MathUtils.sinDeg(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[] vertices = this.vertices;
+		if (region instanceof AtlasRegion && ((AtlasRegion)region).rotate) {
+			vertices[U3] = region.getU();
+			vertices[V3] = region.getV2();
+			vertices[U4] = region.getU();
+			vertices[V4] = region.getV();
+			vertices[U1] = region.getU2();
+			vertices[V1] = region.getV();
+			vertices[U2] = region.getU2();
+			vertices[V2] = region.getV2();
+		} else {
+			vertices[U2] = region.getU();
+			vertices[V2] = region.getV2();
+			vertices[U3] = region.getU();
+			vertices[V3] = region.getV();
+			vertices[U4] = region.getU2();
+			vertices[V4] = region.getV();
+			vertices[U1] = region.getU2();
+			vertices[V1] = region.getV2();
+		}
+	}
+
+	public TextureRegion getRegion () {
+		if (region == null) throw new IllegalStateException("Region has not been set: " + this);
+		return region;
+	}
+
+	/** @return The updated world vertices. */
+	public float[] updateWorldVertices (Slot slot, boolean premultipliedAlpha) {
+		Skeleton skeleton = slot.getSkeleton();
+		Color skeletonColor = skeleton.getColor();
+		Color slotColor = slot.getColor();
+		Color regionColor = color;
+		float alpha = skeletonColor.a * slotColor.a * regionColor.a * 255;
+		float multiplier = premultipliedAlpha ? alpha : 255;
+		float color = NumberUtils.intToFloatColor( //
+			((int)alpha << 24) //
+				| ((int)(skeletonColor.b * slotColor.b * regionColor.b * multiplier) << 16) //
+				| ((int)(skeletonColor.g * slotColor.g * regionColor.g * multiplier) << 8) //
+				| (int)(skeletonColor.r * slotColor.r * regionColor.r * multiplier));
+
+		float[] vertices = this.vertices;
+		float[] offset = this.offset;
+		Bone bone = slot.getBone();
+		float x = bone.getWorldX(), y = bone.getWorldY();
+		float a = bone.getA(), b = bone.getB(), c = bone.getC(), d = bone.getD();
+		float offsetX, offsetY;
+
+		offsetX = offset[BRX];
+		offsetY = offset[BRY];
+		vertices[X1] = offsetX * a + offsetY * b + x; // br
+		vertices[Y1] = offsetX * c + offsetY * d + y;
+		vertices[C1] = color;
+
+		offsetX = offset[BLX];
+		offsetY = offset[BLY];
+		vertices[X2] = offsetX * a + offsetY * b + x; // bl
+		vertices[Y2] = offsetX * c + offsetY * d + y;
+		vertices[C2] = color;
+
+		offsetX = offset[ULX];
+		offsetY = offset[ULY];
+		vertices[X3] = offsetX * a + offsetY * b + x; // ul
+		vertices[Y3] = offsetX * c + offsetY * d + y;
+		vertices[C3] = color;
+
+		offsetX = offset[URX];
+		offsetY = offset[URY];
+		vertices[X4] = offsetX * a + offsetY * b + x; // ur
+		vertices[Y4] = offsetX * c + offsetY * d + y;
+		vertices[C4] = color;
+		return vertices;
+	}
+
+	public float[] getWorldVertices () {
+		return vertices;
+	}
+
+	public float[] getOffset () {
+		return offset;
+	}
+
+	public float getX () {
+		return x;
+	}
+
+	public void setX (float x) {
+		this.x = x;
+	}
+
+	public float getY () {
+		return y;
+	}
+
+	public void setY (float y) {
+		this.y = y;
+	}
+
+	public float getScaleX () {
+		return scaleX;
+	}
+
+	public void setScaleX (float scaleX) {
+		this.scaleX = scaleX;
+	}
+
+	public float getScaleY () {
+		return scaleY;
+	}
+
+	public void setScaleY (float scaleY) {
+		this.scaleY = scaleY;
+	}
+
+	public float getRotation () {
+		return rotation;
+	}
+
+	public void setRotation (float rotation) {
+		this.rotation = rotation;
+	}
+
+	public float getWidth () {
+		return width;
+	}
+
+	public void setWidth (float width) {
+		this.width = width;
+	}
+
+	public float getHeight () {
+		return height;
+	}
+
+	public void setHeight (float height) {
+		this.height = height;
+	}
+
+	public Color getColor () {
+		return color;
+	}
+
+	public String getPath () {
+		return path;
+	}
+
+	public void setPath (String path) {
+		this.path = path;
+	}
 }

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

@@ -1,148 +1,147 @@
-/******************************************************************************
- * Spine Runtimes Software License
- * Version 2.3
- * 
- * Copyright (c) 2013-2015, Esoteric Software
- * All rights reserved.
- * 
- * You are granted a perpetual, non-exclusive, non-sublicensable and
- * non-transferable license to use, install, execute and perform the Spine
- * Runtimes Software (the "Software") and derivative works solely for personal
- * or internal use. Without the written permission of Esoteric Software (see
- * Section 2 of the Spine Software License Agreement), you may not (a) modify,
- * translate, adapt or otherwise create derivative works, improvements of the
- * Software or develop new applications using the Software or (b) remove,
- * delete, alter or obscure any trademarks or any copyright, trademark, patent
- * or other intellectual property or proprietary rights notices on or in the
- * Software, including any copy thereof. Redistributions in binary or source
- * form must include this license and terms.
- * 
- * THIS SOFTWARE IS PROVIDED BY ESOTERIC SOFTWARE "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 BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
- * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
- * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS;
- * OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
- * WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR
- * OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
- * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+/******************************************************************************
+ * Spine Runtimes Software License v2.5
+ * 
+ * Copyright (c) 2013-2016, Esoteric Software
+ * All rights reserved.
+ * 
+ * You are granted a perpetual, non-exclusive, non-sublicensable, and
+ * non-transferable license to use, install, execute, and perform the Spine
+ * Runtimes software and derivative works solely for personal or internal
+ * use. Without the written permission of Esoteric Software (see Section 2 of
+ * the Spine Software License Agreement), you may not (a) modify, translate,
+ * adapt, or develop new applications using the Spine Runtimes or otherwise
+ * create derivative works or improvements of the Spine Runtimes or (b) remove,
+ * delete, alter, or obscure any trademarks or any copyright, trademark, patent,
+ * or other intellectual property or proprietary rights notices on or in the
+ * Software, including any copy thereof. Redistributions in binary or source
+ * form must include this license and terms.
+ * 
+ * THIS SOFTWARE IS PROVIDED BY ESOTERIC SOFTWARE "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 BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
+ * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES, BUSINESS INTERRUPTION, OR LOSS OF
+ * USE, DATA, OR PROFITS) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER
+ * IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+ * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
+ * POSSIBILITY OF SUCH DAMAGE.
  *****************************************************************************/
 
-package com.esotericsoftware.spine.attachments;
-
-import com.badlogic.gdx.utils.FloatArray;
-import com.esotericsoftware.spine.Bone;
-import com.esotericsoftware.spine.Skeleton;
-import com.esotericsoftware.spine.Slot;
-
-/** An attachment with vertices that are transformed by one or more bones and can be deformed by a slot's vertices. */
-public class VertexAttachment extends Attachment {
-	int[] bones;
-	float[] vertices;
-	int worldVerticesLength;
-
-	public VertexAttachment (String name) {
-		super(name);
-	}
-
-	protected void computeWorldVertices (Slot slot, float[] worldVertices) {
-		computeWorldVertices(slot, 0, worldVerticesLength, worldVertices, 0);
-	}
-
-	/** Transforms local vertices to world coordinates.
-	 * @param start The index of the first local vertex 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()} - start.
-	 * @param worldVertices The output world vertices. Must have a length >= offset + count.
-	 * @param offset The worldVertices index to begin writing values. */
-	protected void computeWorldVertices (Slot slot, int start, int count, float[] worldVertices, int offset) {
-		count += offset;
-		Skeleton skeleton = slot.getSkeleton();
-		FloatArray deformArray = slot.getAttachmentVertices();
-		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 += 2) {
-				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 = skeleton.getBones().items;
-		if (deformArray.size == 0) {
-			for (int w = offset, b = skip * 3; w < count; w += 2) {
-				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 += 2) {
-				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;
-			}
-		}
-	}
-
-	/** Returns true if a deform originally applied to the specified attachment should be applied to this attachment. */
-	public boolean applyDeform (VertexAttachment sourceAttachment) {
-		return this == sourceAttachment;
-	}
-
-	/** @return May be null if this attachment has no weights. */
-	public int[] getBones () {
-		return bones;
-	}
-
-	/** For each vertex, the number of bones affecting the vertex followed by that many bone indices. Ie: count, boneIndex, ...
-	 * @param bones May be null if this attachment has no weights. */
-	public void setBones (int[] bones) {
-		this.bones = bones;
-	}
-
-	public float[] getVertices () {
-		return vertices;
-	}
-
-	/** Sets the vertex position in the bone's coordinate system. For a non-weighted attachment, the values are x,y entries for
-	 * each vertex. For a weighted attachment, the values are x,y,weight entries for each bone affecting each vertex. */
-	public void setVertices (float[] vertices) {
-		this.vertices = vertices;
-	}
-
-	public int getWorldVerticesLength () {
-		return worldVerticesLength;
-	}
-
-	public void setWorldVerticesLength (int worldVerticesLength) {
-		this.worldVerticesLength = worldVerticesLength;
-	}
+package com.esotericsoftware.spine.attachments;
+
+import com.badlogic.gdx.utils.FloatArray;
+import com.esotericsoftware.spine.Bone;
+import com.esotericsoftware.spine.Skeleton;
+import com.esotericsoftware.spine.Slot;
+
+/** An attachment with vertices that are transformed by one or more bones and can be deformed by a slot's vertices. */
+public class VertexAttachment extends Attachment {
+	int[] bones;
+	float[] vertices;
+	int worldVerticesLength;
+
+	public VertexAttachment (String name) {
+		super(name);
+	}
+
+	protected void computeWorldVertices (Slot slot, float[] worldVertices) {
+		computeWorldVertices(slot, 0, worldVerticesLength, worldVertices, 0);
+	}
+
+	/** Transforms local vertices to world coordinates.
+	 * @param start The index of the first local vertex 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()} - start.
+	 * @param worldVertices The output world vertices. Must have a length >= offset + count.
+	 * @param offset The worldVertices index to begin writing values. */
+	protected void computeWorldVertices (Slot slot, int start, int count, float[] worldVertices, int offset) {
+		count += offset;
+		Skeleton skeleton = slot.getSkeleton();
+		FloatArray deformArray = slot.getAttachmentVertices();
+		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 += 2) {
+				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 = skeleton.getBones().items;
+		if (deformArray.size == 0) {
+			for (int w = offset, b = skip * 3; w < count; w += 2) {
+				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 += 2) {
+				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;
+			}
+		}
+	}
+
+	/** Returns true if a deform originally applied to the specified attachment should be applied to this attachment. */
+	public boolean applyDeform (VertexAttachment sourceAttachment) {
+		return this == sourceAttachment;
+	}
+
+	/** @return May be null if this attachment has no weights. */
+	public int[] getBones () {
+		return bones;
+	}
+
+	/** For each vertex, the number of bones affecting the vertex followed by that many bone indices. Ie: count, boneIndex, ...
+	 * @param bones May be null if this attachment has no weights. */
+	public void setBones (int[] bones) {
+		this.bones = bones;
+	}
+
+	public float[] getVertices () {
+		return vertices;
+	}
+
+	/** Sets the vertex position in the bone's coordinate system. For a non-weighted attachment, the values are x,y entries for
+	 * each vertex. For a weighted attachment, the values are x,y,weight entries for each bone affecting each vertex. */
+	public void setVertices (float[] vertices) {
+		this.vertices = vertices;
+	}
+
+	public int getWorldVerticesLength () {
+		return worldVerticesLength;
+	}
+
+	public void setWorldVerticesLength (int worldVerticesLength) {
+		this.worldVerticesLength = worldVerticesLength;
+	}
 }

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

@@ -1,657 +1,656 @@
-/******************************************************************************
- * Spine Runtimes Software License
- * Version 2.3
- * 
- * Copyright (c) 2013-2015, Esoteric Software
- * All rights reserved.
- * 
- * You are granted a perpetual, non-exclusive, non-sublicensable and
- * non-transferable license to use, install, execute and perform the Spine
- * Runtimes Software (the "Software") and derivative works solely for personal
- * or internal use. Without the written permission of Esoteric Software (see
- * Section 2 of the Spine Software License Agreement), you may not (a) modify,
- * translate, adapt or otherwise create derivative works, improvements of the
- * Software or develop new applications using the Software or (b) remove,
- * delete, alter or obscure any trademarks or any copyright, trademark, patent
- * or other intellectual property or proprietary rights notices on or in the
- * Software, including any copy thereof. Redistributions in binary or source
- * form must include this license and terms.
- * 
- * THIS SOFTWARE IS PROVIDED BY ESOTERIC SOFTWARE "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 BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
- * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
- * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS;
- * OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
- * WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR
- * OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
- * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+/******************************************************************************
+ * Spine Runtimes Software License v2.5
+ * 
+ * Copyright (c) 2013-2016, Esoteric Software
+ * All rights reserved.
+ * 
+ * You are granted a perpetual, non-exclusive, non-sublicensable, and
+ * non-transferable license to use, install, execute, and perform the Spine
+ * Runtimes software and derivative works solely for personal or internal
+ * use. Without the written permission of Esoteric Software (see Section 2 of
+ * the Spine Software License Agreement), you may not (a) modify, translate,
+ * adapt, or develop new applications using the Spine Runtimes or otherwise
+ * create derivative works or improvements of the Spine Runtimes or (b) remove,
+ * delete, alter, or obscure any trademarks or any copyright, trademark, patent,
+ * or other intellectual property or proprietary rights notices on or in the
+ * Software, including any copy thereof. Redistributions in binary or source
+ * form must include this license and terms.
+ * 
+ * THIS SOFTWARE IS PROVIDED BY ESOTERIC SOFTWARE "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 BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
+ * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES, BUSINESS INTERRUPTION, OR LOSS OF
+ * USE, DATA, OR PROFITS) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER
+ * IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+ * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
+ * POSSIBILITY OF SUCH DAMAGE.
  *****************************************************************************/
 
-package com.esotericsoftware.spine;
-
-import static com.badlogic.gdx.scenes.scene2d.actions.Actions.*;
-
-import java.awt.FileDialog;
-import java.awt.Frame;
-import java.io.File;
-import java.lang.Thread.UncaughtExceptionHandler;
-
-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.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.Pixmap;
-import com.badlogic.gdx.graphics.Pixmap.Format;
-import com.badlogic.gdx.graphics.Texture;
-import com.badlogic.gdx.graphics.Texture.TextureFilter;
-import com.badlogic.gdx.graphics.g2d.PolygonSpriteBatch;
-import com.badlogic.gdx.graphics.g2d.TextureAtlas;
-import com.badlogic.gdx.graphics.g2d.TextureAtlas.AtlasRegion;
-import com.badlogic.gdx.graphics.g2d.TextureAtlas.TextureAtlasData;
-import com.badlogic.gdx.graphics.glutils.ShapeRenderer;
-import com.badlogic.gdx.graphics.glutils.ShapeRenderer.ShapeType;
-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.Touchable;
-import com.badlogic.gdx.scenes.scene2d.ui.CheckBox;
-import com.badlogic.gdx.scenes.scene2d.ui.Label;
-import com.badlogic.gdx.scenes.scene2d.ui.List;
-import com.badlogic.gdx.scenes.scene2d.ui.ScrollPane;
-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.WidgetGroup;
-import com.badlogic.gdx.scenes.scene2d.ui.Window;
-import com.badlogic.gdx.scenes.scene2d.utils.ChangeListener;
-import com.badlogic.gdx.scenes.scene2d.utils.ClickListener;
-import com.badlogic.gdx.utils.Array;
-import com.badlogic.gdx.utils.viewport.ScreenViewport;
-import com.esotericsoftware.spine.AnimationState.AnimationStateAdapter;
-import com.esotericsoftware.spine.AnimationState.TrackEntry;
-
-public class SkeletonViewer extends ApplicationAdapter {
-	static final float checkModifiedInterval = 0.250f;
-	static final float reloadDelay = 1;
-
-	UI ui;
-
-	PolygonSpriteBatch batch;
-	SkeletonMeshRenderer renderer;
-	SkeletonRendererDebug debugRenderer;
-	SkeletonData skeletonData;
-	Skeleton skeleton;
-	AnimationState state;
-	int skeletonX, skeletonY;
-	FileHandle skeletonFile;
-	long lastModified;
-	float lastModifiedCheck, reloadTimer;
-	Preferences prefs;
-
-	public void create () {
-		Thread.setDefaultUncaughtExceptionHandler(new UncaughtExceptionHandler() {
-			public void uncaughtException (Thread thread, Throwable ex) {
-				ex.printStackTrace();
-				Runtime.getRuntime().halt(0); // Prevent Swing from keeping JVM alive.
-			}
-		});
-
-		prefs = Gdx.app.getPreferences("spine-skeletonviewer");
-		ui = new UI();
-		batch = new PolygonSpriteBatch();
-		renderer = new SkeletonMeshRenderer();
-		debugRenderer = new SkeletonRendererDebug();
-		skeletonX = (int)(ui.window.getWidth() + (Gdx.graphics.getWidth() - ui.window.getWidth()) / 2);
-		skeletonY = Gdx.graphics.getHeight() / 4;
-		ui.loadPrefs();
-
-		loadSkeleton(
-			Gdx.files.internal(Gdx.app.getPreferences("spine-skeletonviewer").getString("lastFile", "spineboy/spineboy.json")),
-			false);
-
-		ui.loadPrefs();
-	}
-
-	void loadSkeleton (final FileHandle skeletonFile, boolean reload) {
-		if (skeletonFile == null) return;
-
-		try {
-			// A regular texture atlas would normally usually be used. This returns a white image for images not found in the atlas.
-			Pixmap pixmap = new Pixmap(32, 32, Format.RGBA8888);
-			pixmap.setColor(new Color(1, 1, 1, 0.33f));
-			pixmap.fill();
-			final AtlasRegion fake = new AtlasRegion(new Texture(pixmap), 0, 0, 32, 32);
-			pixmap.dispose();
-
-			String atlasFileName = skeletonFile.nameWithoutExtension();
-			if (atlasFileName.endsWith(".json")) atlasFileName = new FileHandle(atlasFileName).nameWithoutExtension();
-			FileHandle atlasFile = skeletonFile.sibling(atlasFileName + ".atlas");
-			if (!atlasFile.exists()) atlasFile = skeletonFile.sibling(atlasFileName + ".atlas.txt");
-			TextureAtlasData data = !atlasFile.exists() ? null : new TextureAtlasData(atlasFile, atlasFile.parent(), false);
-			TextureAtlas atlas = new TextureAtlas(data) {
-				public AtlasRegion findRegion (String name) {
-					AtlasRegion region = super.findRegion(name);
-					if (region == null) {
-						// Look for separate image file.
-						FileHandle file = skeletonFile.sibling(name + ".png");
-						if (file.exists()) {
-							Texture texture = new Texture(file);
-							texture.setFilter(TextureFilter.Linear, TextureFilter.Linear);
-							region = new AtlasRegion(texture, 0, 0, texture.getWidth(), texture.getHeight());
-							region.name = name;
-						}
-					}
-					return region != null ? region : fake;
-				}
-			};
-
-			String extension = skeletonFile.extension();
-			if (extension.equalsIgnoreCase("json") || extension.equalsIgnoreCase("txt")) {
-				SkeletonJson json = new SkeletonJson(atlas);
-				json.setScale(ui.scaleSlider.getValue());
-				skeletonData = json.readSkeletonData(skeletonFile);
-			} else {
-				SkeletonBinary binary = new SkeletonBinary(atlas);
-				binary.setScale(ui.scaleSlider.getValue());
-				skeletonData = binary.readSkeletonData(skeletonFile);
-				if (skeletonData.getBones().size == 0) throw new Exception("No bones in skeleton data.");
-			}
-		} catch (Exception ex) {
-			ex.printStackTrace();
-			ui.toast("Error loading skeleton: " + skeletonFile.name());
-			lastModifiedCheck = 5;
-			return;
-		}
-
-		skeleton = new Skeleton(skeletonData);
-		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());
-			}
-		});
-
-		this.skeletonFile = skeletonFile;
-		prefs.putString("lastFile", skeletonFile.path());
-		prefs.flush();
-		lastModified = skeletonFile.lastModified();
-		lastModifiedCheck = checkModifiedInterval;
-
-		// 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);
-		}
-
-		// Configure skeleton from UI.
-
-		if (ui.skinList.getSelected() != null) skeleton.setSkin(ui.skinList.getSelected());
-		setAnimation();
-
-		if (reload) ui.toast("Reloaded.");
-	}
-
-	void setAnimation () {
-		if (ui.animationList.getSelected() == null) return;
-		TrackEntry current = state.getCurrent(0);
-		if (current == null) {
-			state.setEmptyAnimation(0, 0);
-			TrackEntry entry = state.addAnimation(0, ui.animationList.getSelected(), ui.loopCheckbox.isChecked(), 0);
-			entry.setMixDuration(ui.mixSlider.getValue());
-			entry.setTrackEnd(Integer.MAX_VALUE);
-		} else {
-			TrackEntry entry = state.setAnimation(0, ui.animationList.getSelected(), ui.loopCheckbox.isChecked());
-			entry.setTrackEnd(Integer.MAX_VALUE);
-		}
-	}
-
-	public void render () {
-		Gdx.gl.glClear(GL20.GL_COLOR_BUFFER_BIT);
-
-		float delta = Gdx.graphics.getDeltaTime();
-
-		// Draw skeleton origin lines.
-		ShapeRenderer shapes = debugRenderer.getShapeRenderer();
-		if (state != null) {
-			shapes.setColor(Color.DARK_GRAY);
-			shapes.begin(ShapeType.Line);
-			shapes.line(skeleton.x, -99999, skeleton.x, 99999);
-			shapes.line(-99999, skeleton.y, 99999, skeleton.y);
-			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 && lastModified != time) reloadTimer = reloadDelay;
-				}
-			} else {
-				reloadTimer -= delta;
-				if (reloadTimer <= 0) loadSkeleton(skeletonFile, true);
-			}
-
-			// Pose and render skeleton.
-			state.getData().setDefaultMix(ui.mixSlider.getValue());
-			renderer.setPremultipliedAlpha(ui.premultipliedCheckbox.isChecked());
-
-			delta = Math.min(delta, 0.032f) * ui.speedSlider.getValue();
-			skeleton.update(delta);
-			skeleton.setFlip(ui.flipXCheckbox.isChecked(), ui.flipYCheckbox.isChecked());
-			if (!ui.pauseButton.isChecked()) {
-				state.update(delta);
-				state.apply(skeleton);
-			}
-			skeleton.setPosition(skeletonX, skeletonY);
-			skeleton.updateWorldTransform();
-
-			batch.setColor(Color.WHITE);
-			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.draw(skeleton);
-		}
-
-		// Render UI.
-		ui.stage.act();
-		ui.stage.draw();
-
-		// Draw indicator lines for animation and mix times.
-		if (state != null) {
-			TrackEntry entry = state.getCurrent(0);
-			if (entry != null) {
-				shapes.begin(ShapeType.Line);
-
-				float percent = entry.getAnimationTime() / entry.getAnimationEnd();
-				float x = ui.window.getRight() + (Gdx.graphics.getWidth() - ui.window.getRight()) * percent;
-				shapes.setColor(Color.CYAN);
-				shapes.line(x, 0, x, 20);
-
-				percent = entry.getMixDuration() == 0 ? 1 : Math.min(1, entry.getMixTime() / entry.getMixDuration());
-				x = ui.window.getRight() + (Gdx.graphics.getWidth() - ui.window.getRight()) * percent;
-				shapes.setColor(Color.RED);
-				shapes.line(x, 0, x, 20);
-
-				shapes.end();
-			}
-		}
-	}
-
-	public void resize (int width, int height) {
-		batch.getProjectionMatrix().setToOrtho2D(0, 0, width, height);
-		debugRenderer.getShapeRenderer().setProjectionMatrix(batch.getProjectionMatrix());
-		ui.stage.getViewport().update(width, height, true);
-		if (!ui.minimizeButton.isChecked()) ui.window.setHeight(height + 8);
-	}
-
-	class UI {
-		Stage stage = new Stage(new ScreenViewport());
-		com.badlogic.gdx.scenes.scene2d.ui.Skin skin = new com.badlogic.gdx.scenes.scene2d.ui.Skin(
-			Gdx.files.internal("skin/skin.json"));
-
-		Window window = new Window("Skeleton", skin);
-		Table root = new Table(skin);
-		TextButton openButton = new TextButton("Open", skin);
-		List<String> animationList = new List(skin);
-		List<String> skinList = new List(skin);
-		CheckBox loopCheckbox = new CheckBox("Loop", skin);
-		CheckBox premultipliedCheckbox = new CheckBox("Premultiplied", skin);
-		Slider mixSlider = new Slider(0, 4, 0.01f, false, skin);
-		Label mixLabel = new Label("0.3", skin);
-		Slider speedSlider = new Slider(0, 3, 0.01f, false, skin);
-		Label speedLabel = new Label("1.0", skin);
-		CheckBox flipXCheckbox = new CheckBox("X", skin);
-		CheckBox flipYCheckbox = new CheckBox("Y", skin);
-		CheckBox debugBonesCheckbox = new CheckBox("Bones", skin);
-		CheckBox debugRegionsCheckbox = new CheckBox("Regions", skin);
-		CheckBox debugBoundingBoxesCheckbox = new CheckBox("Bounds", skin);
-		CheckBox debugMeshHullCheckbox = new CheckBox("Mesh hull", skin);
-		CheckBox debugMeshTrianglesCheckbox = new CheckBox("Triangles", skin);
-		CheckBox debugPathsCheckbox = new CheckBox("Paths", skin);
-		Slider scaleSlider = new Slider(0.1f, 3, 0.01f, false, skin);
-		Label scaleLabel = new Label("1.0", skin);
-		TextButton pauseButton = new TextButton("Pause", skin, "toggle");
-		TextButton minimizeButton = new TextButton("-", skin);
-		TextButton bonesSetupPoseButton = new TextButton("Bones", skin);
-		TextButton slotsSetupPoseButton = new TextButton("Slots", skin);
-		TextButton setupPoseButton = new TextButton("Both", skin);
-		WidgetGroup toasts = new WidgetGroup();
-		boolean prefsLoaded;
-
-		public UI () {
-			animationList.getSelection().setRequired(false);
-
-			premultipliedCheckbox.setChecked(true);
-
-			loopCheckbox.setChecked(true);
-
-			scaleSlider.setValue(1);
-			scaleSlider.setSnapToValues(new float[] {1, 1.5f, 2, 2.5f, 3, 3.5f}, 0.01f);
-
-			mixSlider.setValue(0.3f);
-			mixSlider.setSnapToValues(new float[] {1, 1.5f, 2, 2.5f, 3, 3.5f}, 0.1f);
-
-			speedSlider.setValue(1);
-			speedSlider.setSnapToValues(new float[] {0.5f, 0.75f, 1, 1.25f, 1.5f, 2, 2.5f}, 0.1f);
-
-			window.setMovable(false);
-			window.setResizable(false);
-			window.setKeepWithinStage(false);
-			window.setX(-3);
-			window.setY(-2);
-
-			window.getTitleLabel().setColor(new Color(0.76f, 1, 1, 1));
-			window.getTitleTable().add(openButton).space(3);
-			window.getTitleTable().add(minimizeButton).width(20);
-
-			ScrollPane skinScroll = new ScrollPane(skinList, skin, "bg");
-			skinScroll.setFadeScrollBars(false);
-
-			ScrollPane animationScroll = new ScrollPane(animationList, skin, "bg");
-			animationScroll.setFadeScrollBars(false);
-
-			// Layout.
-
-			root.defaults().space(6);
-			root.columnDefaults(0).top().right().padTop(3);
-			root.columnDefaults(1).left();
-			root.add("Scale:");
-			{
-				Table table = table();
-				table.add(scaleLabel).width(29);
-				table.add(scaleSlider).fillX().expandX();
-				root.add(table).fill().row();
-			}
-			root.add("Flip:");
-			root.add(table(flipXCheckbox, flipYCheckbox)).row();
-			root.add("Debug:");
-			root.add(table(debugBonesCheckbox, debugRegionsCheckbox, debugBoundingBoxesCheckbox)).row();
-			root.add();
-			root.add(table(debugMeshHullCheckbox, debugMeshTrianglesCheckbox, debugPathsCheckbox)).row();
-			root.add("Alpha:");
-			root.add(premultipliedCheckbox).row();
-			root.add("Skin:");
-			root.add(skinScroll).expand().fill().minHeight(75).row();
-			root.add("Setup pose:");
-			root.add(table(bonesSetupPoseButton, slotsSetupPoseButton, setupPoseButton)).row();
-			root.add("Animation:");
-			root.add(animationScroll).expand().fill().minHeight(75).row();
-			root.add("Mix:");
-			{
-				Table table = table();
-				table.add(mixLabel).width(29);
-				table.add(mixSlider).fillX().expandX();
-				root.add(table).fill().row();
-			}
-			root.add("Speed:");
-			{
-				Table table = table();
-				table.add(speedLabel).width(29);
-				table.add(speedSlider).fillX().expandX();
-				root.add(table).fill().row();
-			}
-			root.add("Playback:");
-			root.add(table(pauseButton, loopCheckbox)).row();
-
-			window.add(root).expand().fill();
-			window.pack();
-			stage.addActor(window);
-
-			{
-				Table table = new Table(skin);
-				table.setFillParent(true);
-				table.setTouchable(Touchable.disabled);
-				stage.addActor(table);
-				table.pad(10).bottom().right();
-				table.add(toasts);
-			}
-
-			window.addListener(new InputListener() {
-				public boolean touchDown (InputEvent event, float x, float y, int pointer, int button) {
-					event.cancel();
-					return true;
-				}
-			});
-
-			openButton.addListener(new ChangeListener() {
-				public void changed (ChangeEvent event, Actor actor) {
-					FileDialog fileDialog = new FileDialog((Frame)null, "Choose skeleton file");
-					fileDialog.setMode(FileDialog.LOAD);
-					fileDialog.setVisible(true);
-					String name = fileDialog.getFile();
-					String dir = fileDialog.getDirectory();
-					if (name == null || dir == null) return;
-					loadSkeleton(new FileHandle(new File(dir, name).getAbsolutePath()), false);
-				}
-			});
-
-			setupPoseButton.addListener(new ChangeListener() {
-				public void changed (ChangeEvent event, Actor actor) {
-					if (skeleton != null) skeleton.setToSetupPose();
-				}
-			});
-			bonesSetupPoseButton.addListener(new ChangeListener() {
-				public void changed (ChangeEvent event, Actor actor) {
-					if (skeleton != null) skeleton.setBonesToSetupPose();
-				}
-			});
-			slotsSetupPoseButton.addListener(new ChangeListener() {
-				public void changed (ChangeEvent event, Actor actor) {
-					if (skeleton != null) skeleton.setSlotsToSetupPose();
-				}
-			});
-
-			minimizeButton.addListener(new ClickListener() {
-				public boolean touchDown (InputEvent event, float x, float y, int pointer, int button) {
-					event.cancel();
-					return super.touchDown(event, x, y, pointer, button);
-				}
-
-				public void clicked (InputEvent event, float x, float y) {
-					if (minimizeButton.isChecked()) {
-						window.getCells().get(0).setActor(null);
-						window.setHeight(37);
-						minimizeButton.setText("+");
-					} else {
-						window.getCells().get(0).setActor(root);
-						ui.window.setHeight(Gdx.graphics.getHeight() + 8);
-						minimizeButton.setText("-");
-					}
-				}
-			});
-
-			scaleSlider.addListener(new ChangeListener() {
-				public void changed (ChangeEvent event, Actor actor) {
-					scaleLabel.setText(Float.toString((int)(scaleSlider.getValue() * 100) / 100f));
-					if (!scaleSlider.isDragging()) loadSkeleton(skeletonFile, false);
-				}
-			});
-
-			speedSlider.addListener(new ChangeListener() {
-				public void changed (ChangeEvent event, Actor actor) {
-					speedLabel.setText(Float.toString((int)(speedSlider.getValue() * 100) / 100f));
-				}
-			});
-
-			mixSlider.addListener(new ChangeListener() {
-				public void changed (ChangeEvent event, Actor actor) {
-					mixLabel.setText(Float.toString((int)(mixSlider.getValue() * 100) / 100f));
-					if (state != null) state.getData().setDefaultMix(mixSlider.getValue());
-				}
-			});
-
-			animationList.addListener(new ChangeListener() {
-				public void changed (ChangeEvent event, Actor actor) {
-					if (state != null) {
-						String name = animationList.getSelected();
-						if (name == null)
-							state.setEmptyAnimation(0, ui.mixSlider.getValue());
-						else
-							setAnimation();
-					}
-				}
-			});
-
-			loopCheckbox.addListener(new ChangeListener() {
-				public void changed (ChangeEvent event, Actor actor) {
-					setAnimation();
-				}
-			});
-
-			skinList.addListener(new ChangeListener() {
-				public void changed (ChangeEvent event, Actor actor) {
-					if (skeleton != null) {
-						String skinName = skinList.getSelected();
-						if (skinName == null)
-							skeleton.setSkin((Skin)null);
-						else
-							skeleton.setSkin(skinName);
-						skeleton.setSlotsToSetupPose();
-					}
-				}
-			});
-
-			Gdx.input.setInputProcessor(new InputMultiplexer(stage, new InputAdapter() {
-				public boolean touchDown (int screenX, int screenY, int pointer, int button) {
-					touchDragged(screenX, screenY, pointer);
-					return false;
-				}
-
-				public boolean touchDragged (int screenX, int screenY, int pointer) {
-					skeletonX = screenX;
-					skeletonY = Gdx.graphics.getHeight() - screenY;
-					return false;
-				}
-
-				public boolean touchUp (int screenX, int screenY, int pointer, int button) {
-					savePrefs();
-					return false;
-				}
-			}));
-
-			ChangeListener savePrefsListener = new ChangeListener() {
-				public void changed (ChangeEvent event, Actor actor) {
-					if (actor instanceof Slider && ((Slider)actor).isDragging()) return;
-					savePrefs();
-				}
-			};
-			debugBonesCheckbox.addListener(savePrefsListener);
-			debugRegionsCheckbox.addListener(savePrefsListener);
-			debugMeshHullCheckbox.addListener(savePrefsListener);
-			debugMeshTrianglesCheckbox.addListener(savePrefsListener);
-			debugPathsCheckbox.addListener(savePrefsListener);
-			premultipliedCheckbox.addListener(savePrefsListener);
-			loopCheckbox.addListener(savePrefsListener);
-			speedSlider.addListener(savePrefsListener);
-			mixSlider.addListener(savePrefsListener);
-			scaleSlider.addListener(savePrefsListener);
-			animationList.addListener(savePrefsListener);
-			skinList.addListener(savePrefsListener);
-		}
-
-		private Table table (Actor... actors) {
-			Table table = new Table();
-			table.defaults().space(6);
-			table.add(actors);
-			return table;
-		}
-
-		void toast (String text) {
-			Table table = new Table();
-			table.add(new Label(text, skin));
-			table.getColor().a = 0;
-			table.pack();
-			table.setPosition(-table.getWidth(), -3 - table.getHeight());
-			table.addAction(sequence( //
-				parallel(moveBy(0, table.getHeight(), 0.3f), fadeIn(0.3f)), //
-				delay(5f), //
-				parallel(moveBy(0, table.getHeight(), 0.3f), fadeOut(0.3f)), //
-				removeActor() //
-			));
-			for (Actor actor : toasts.getChildren())
-				actor.addAction(moveBy(0, table.getHeight(), 0.3f));
-			toasts.addActor(table);
-			toasts.getParent().toFront();
-		}
-
-		void savePrefs () {
-			if (!prefsLoaded) return;
-			prefs.putBoolean("debugBones", debugBonesCheckbox.isChecked());
-			prefs.putBoolean("debugRegions", debugRegionsCheckbox.isChecked());
-			prefs.putBoolean("debugMeshHull", debugMeshHullCheckbox.isChecked());
-			prefs.putBoolean("debugMeshTriangles", debugMeshTrianglesCheckbox.isChecked());
-			prefs.putBoolean("debugPaths", debugPathsCheckbox.isChecked());
-			prefs.putBoolean("premultiplied", premultipliedCheckbox.isChecked());
-			prefs.putBoolean("loop", loopCheckbox.isChecked());
-			prefs.putFloat("speed", speedSlider.getValue());
-			prefs.putFloat("mix", mixSlider.getValue());
-			prefs.putFloat("scale", scaleSlider.getValue());
-			prefs.putInteger("x", skeletonX);
-			prefs.putInteger("y", skeletonY);
-			if (animationList.getSelected() != null) prefs.putString("animationName", animationList.getSelected());
-			if (skinList.getSelected() != null) prefs.putString("skinName", skinList.getSelected());
-			prefs.flush();
-		}
-
-		void loadPrefs () {
-			debugBonesCheckbox.setChecked(prefs.getBoolean("debugBones", true));
-			debugRegionsCheckbox.setChecked(prefs.getBoolean("debugRegions", false));
-			debugMeshHullCheckbox.setChecked(prefs.getBoolean("debugMeshHull", false));
-			debugMeshTrianglesCheckbox.setChecked(prefs.getBoolean("debugMeshTriangles", false));
-			debugPathsCheckbox.setChecked(prefs.getBoolean("debugPaths", true));
-			premultipliedCheckbox.setChecked(prefs.getBoolean("premultiplied", true));
-			loopCheckbox.setChecked(prefs.getBoolean("loop", false));
-			speedSlider.setValue(prefs.getFloat("speed", 0.3f));
-			mixSlider.setValue(prefs.getFloat("mix", 0.3f));
-			scaleSlider.setValue(prefs.getFloat("scale", 1));
-			skeletonX = prefs.getInteger("x", 0);
-			skeletonY = prefs.getInteger("y", 0);
-			animationList.setSelected(prefs.getString("animationName", null));
-			skinList.setSelected(prefs.getString("skinName", null));
-			prefsLoaded = true;
-		}
-	}
-
-	static public void main (String[] args) throws Exception {
-		LwjglApplicationConfiguration.disableAudio = true;
-		LwjglApplicationConfiguration config = new LwjglApplicationConfiguration();
-		config.width = 800;
-		config.height = 600;
-		config.title = "Skeleton Viewer";
-		config.allowSoftwareMode = true;
-		new LwjglApplication(new SkeletonViewer(), config);
-	}
+package com.esotericsoftware.spine;
+
+import static com.badlogic.gdx.scenes.scene2d.actions.Actions.*;
+
+import java.awt.FileDialog;
+import java.awt.Frame;
+import java.io.File;
+import java.lang.Thread.UncaughtExceptionHandler;
+
+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.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.Pixmap;
+import com.badlogic.gdx.graphics.Pixmap.Format;
+import com.badlogic.gdx.graphics.Texture;
+import com.badlogic.gdx.graphics.Texture.TextureFilter;
+import com.badlogic.gdx.graphics.g2d.PolygonSpriteBatch;
+import com.badlogic.gdx.graphics.g2d.TextureAtlas;
+import com.badlogic.gdx.graphics.g2d.TextureAtlas.AtlasRegion;
+import com.badlogic.gdx.graphics.g2d.TextureAtlas.TextureAtlasData;
+import com.badlogic.gdx.graphics.glutils.ShapeRenderer;
+import com.badlogic.gdx.graphics.glutils.ShapeRenderer.ShapeType;
+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.Touchable;
+import com.badlogic.gdx.scenes.scene2d.ui.CheckBox;
+import com.badlogic.gdx.scenes.scene2d.ui.Label;
+import com.badlogic.gdx.scenes.scene2d.ui.List;
+import com.badlogic.gdx.scenes.scene2d.ui.ScrollPane;
+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.WidgetGroup;
+import com.badlogic.gdx.scenes.scene2d.ui.Window;
+import com.badlogic.gdx.scenes.scene2d.utils.ChangeListener;
+import com.badlogic.gdx.scenes.scene2d.utils.ClickListener;
+import com.badlogic.gdx.utils.Array;
+import com.badlogic.gdx.utils.viewport.ScreenViewport;
+import com.esotericsoftware.spine.AnimationState.AnimationStateAdapter;
+import com.esotericsoftware.spine.AnimationState.TrackEntry;
+
+public class SkeletonViewer extends ApplicationAdapter {
+	static final float checkModifiedInterval = 0.250f;
+	static final float reloadDelay = 1;
+
+	UI ui;
+
+	PolygonSpriteBatch batch;
+	SkeletonMeshRenderer renderer;
+	SkeletonRendererDebug debugRenderer;
+	SkeletonData skeletonData;
+	Skeleton skeleton;
+	AnimationState state;
+	int skeletonX, skeletonY;
+	FileHandle skeletonFile;
+	long lastModified;
+	float lastModifiedCheck, reloadTimer;
+	Preferences prefs;
+
+	public void create () {
+		Thread.setDefaultUncaughtExceptionHandler(new UncaughtExceptionHandler() {
+			public void uncaughtException (Thread thread, Throwable ex) {
+				ex.printStackTrace();
+				Runtime.getRuntime().halt(0); // Prevent Swing from keeping JVM alive.
+			}
+		});
+
+		prefs = Gdx.app.getPreferences("spine-skeletonviewer");
+		ui = new UI();
+		batch = new PolygonSpriteBatch();
+		renderer = new SkeletonMeshRenderer();
+		debugRenderer = new SkeletonRendererDebug();
+		skeletonX = (int)(ui.window.getWidth() + (Gdx.graphics.getWidth() - ui.window.getWidth()) / 2);
+		skeletonY = Gdx.graphics.getHeight() / 4;
+		ui.loadPrefs();
+
+		loadSkeleton(
+			Gdx.files.internal(Gdx.app.getPreferences("spine-skeletonviewer").getString("lastFile", "spineboy/spineboy.json")),
+			false);
+
+		ui.loadPrefs();
+	}
+
+	void loadSkeleton (final FileHandle skeletonFile, boolean reload) {
+		if (skeletonFile == null) return;
+
+		try {
+			// A regular texture atlas would normally usually be used. This returns a white image for images not found in the atlas.
+			Pixmap pixmap = new Pixmap(32, 32, Format.RGBA8888);
+			pixmap.setColor(new Color(1, 1, 1, 0.33f));
+			pixmap.fill();
+			final AtlasRegion fake = new AtlasRegion(new Texture(pixmap), 0, 0, 32, 32);
+			pixmap.dispose();
+
+			String atlasFileName = skeletonFile.nameWithoutExtension();
+			if (atlasFileName.endsWith(".json")) atlasFileName = new FileHandle(atlasFileName).nameWithoutExtension();
+			FileHandle atlasFile = skeletonFile.sibling(atlasFileName + ".atlas");
+			if (!atlasFile.exists()) atlasFile = skeletonFile.sibling(atlasFileName + ".atlas.txt");
+			TextureAtlasData data = !atlasFile.exists() ? null : new TextureAtlasData(atlasFile, atlasFile.parent(), false);
+			TextureAtlas atlas = new TextureAtlas(data) {
+				public AtlasRegion findRegion (String name) {
+					AtlasRegion region = super.findRegion(name);
+					if (region == null) {
+						// Look for separate image file.
+						FileHandle file = skeletonFile.sibling(name + ".png");
+						if (file.exists()) {
+							Texture texture = new Texture(file);
+							texture.setFilter(TextureFilter.Linear, TextureFilter.Linear);
+							region = new AtlasRegion(texture, 0, 0, texture.getWidth(), texture.getHeight());
+							region.name = name;
+						}
+					}
+					return region != null ? region : fake;
+				}
+			};
+
+			String extension = skeletonFile.extension();
+			if (extension.equalsIgnoreCase("json") || extension.equalsIgnoreCase("txt")) {
+				SkeletonJson json = new SkeletonJson(atlas);
+				json.setScale(ui.scaleSlider.getValue());
+				skeletonData = json.readSkeletonData(skeletonFile);
+			} else {
+				SkeletonBinary binary = new SkeletonBinary(atlas);
+				binary.setScale(ui.scaleSlider.getValue());
+				skeletonData = binary.readSkeletonData(skeletonFile);
+				if (skeletonData.getBones().size == 0) throw new Exception("No bones in skeleton data.");
+			}
+		} catch (Exception ex) {
+			ex.printStackTrace();
+			ui.toast("Error loading skeleton: " + skeletonFile.name());
+			lastModifiedCheck = 5;
+			return;
+		}
+
+		skeleton = new Skeleton(skeletonData);
+		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());
+			}
+		});
+
+		this.skeletonFile = skeletonFile;
+		prefs.putString("lastFile", skeletonFile.path());
+		prefs.flush();
+		lastModified = skeletonFile.lastModified();
+		lastModifiedCheck = checkModifiedInterval;
+
+		// 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);
+		}
+
+		// Configure skeleton from UI.
+
+		if (ui.skinList.getSelected() != null) skeleton.setSkin(ui.skinList.getSelected());
+		setAnimation();
+
+		if (reload) ui.toast("Reloaded.");
+	}
+
+	void setAnimation () {
+		if (ui.animationList.getSelected() == null) return;
+		TrackEntry current = state.getCurrent(0);
+		if (current == null) {
+			state.setEmptyAnimation(0, 0);
+			TrackEntry entry = state.addAnimation(0, ui.animationList.getSelected(), ui.loopCheckbox.isChecked(), 0);
+			entry.setMixDuration(ui.mixSlider.getValue());
+			entry.setTrackEnd(Integer.MAX_VALUE);
+		} else {
+			TrackEntry entry = state.setAnimation(0, ui.animationList.getSelected(), ui.loopCheckbox.isChecked());
+			entry.setTrackEnd(Integer.MAX_VALUE);
+		}
+	}
+
+	public void render () {
+		Gdx.gl.glClear(GL20.GL_COLOR_BUFFER_BIT);
+
+		float delta = Gdx.graphics.getDeltaTime();
+
+		// Draw skeleton origin lines.
+		ShapeRenderer shapes = debugRenderer.getShapeRenderer();
+		if (state != null) {
+			shapes.setColor(Color.DARK_GRAY);
+			shapes.begin(ShapeType.Line);
+			shapes.line(skeleton.x, -99999, skeleton.x, 99999);
+			shapes.line(-99999, skeleton.y, 99999, skeleton.y);
+			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 && lastModified != time) reloadTimer = reloadDelay;
+				}
+			} else {
+				reloadTimer -= delta;
+				if (reloadTimer <= 0) loadSkeleton(skeletonFile, true);
+			}
+
+			// Pose and render skeleton.
+			state.getData().setDefaultMix(ui.mixSlider.getValue());
+			renderer.setPremultipliedAlpha(ui.premultipliedCheckbox.isChecked());
+
+			delta = Math.min(delta, 0.032f) * ui.speedSlider.getValue();
+			skeleton.update(delta);
+			skeleton.setFlip(ui.flipXCheckbox.isChecked(), ui.flipYCheckbox.isChecked());
+			if (!ui.pauseButton.isChecked()) {
+				state.update(delta);
+				state.apply(skeleton);
+			}
+			skeleton.setPosition(skeletonX, skeletonY);
+			skeleton.updateWorldTransform();
+
+			batch.setColor(Color.WHITE);
+			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.draw(skeleton);
+		}
+
+		// Render UI.
+		ui.stage.act();
+		ui.stage.draw();
+
+		// Draw indicator lines for animation and mix times.
+		if (state != null) {
+			TrackEntry entry = state.getCurrent(0);
+			if (entry != null) {
+				shapes.begin(ShapeType.Line);
+
+				float percent = entry.getAnimationTime() / entry.getAnimationEnd();
+				float x = ui.window.getRight() + (Gdx.graphics.getWidth() - ui.window.getRight()) * percent;
+				shapes.setColor(Color.CYAN);
+				shapes.line(x, 0, x, 20);
+
+				percent = entry.getMixDuration() == 0 ? 1 : Math.min(1, entry.getMixTime() / entry.getMixDuration());
+				x = ui.window.getRight() + (Gdx.graphics.getWidth() - ui.window.getRight()) * percent;
+				shapes.setColor(Color.RED);
+				shapes.line(x, 0, x, 20);
+
+				shapes.end();
+			}
+		}
+	}
+
+	public void resize (int width, int height) {
+		batch.getProjectionMatrix().setToOrtho2D(0, 0, width, height);
+		debugRenderer.getShapeRenderer().setProjectionMatrix(batch.getProjectionMatrix());
+		ui.stage.getViewport().update(width, height, true);
+		if (!ui.minimizeButton.isChecked()) ui.window.setHeight(height + 8);
+	}
+
+	class UI {
+		Stage stage = new Stage(new ScreenViewport());
+		com.badlogic.gdx.scenes.scene2d.ui.Skin skin = new com.badlogic.gdx.scenes.scene2d.ui.Skin(
+			Gdx.files.internal("skin/skin.json"));
+
+		Window window = new Window("Skeleton", skin);
+		Table root = new Table(skin);
+		TextButton openButton = new TextButton("Open", skin);
+		List<String> animationList = new List(skin);
+		List<String> skinList = new List(skin);
+		CheckBox loopCheckbox = new CheckBox("Loop", skin);
+		CheckBox premultipliedCheckbox = new CheckBox("Premultiplied", skin);
+		Slider mixSlider = new Slider(0, 4, 0.01f, false, skin);
+		Label mixLabel = new Label("0.3", skin);
+		Slider speedSlider = new Slider(0, 3, 0.01f, false, skin);
+		Label speedLabel = new Label("1.0", skin);
+		CheckBox flipXCheckbox = new CheckBox("X", skin);
+		CheckBox flipYCheckbox = new CheckBox("Y", skin);
+		CheckBox debugBonesCheckbox = new CheckBox("Bones", skin);
+		CheckBox debugRegionsCheckbox = new CheckBox("Regions", skin);
+		CheckBox debugBoundingBoxesCheckbox = new CheckBox("Bounds", skin);
+		CheckBox debugMeshHullCheckbox = new CheckBox("Mesh hull", skin);
+		CheckBox debugMeshTrianglesCheckbox = new CheckBox("Triangles", skin);
+		CheckBox debugPathsCheckbox = new CheckBox("Paths", skin);
+		Slider scaleSlider = new Slider(0.1f, 3, 0.01f, false, skin);
+		Label scaleLabel = new Label("1.0", skin);
+		TextButton pauseButton = new TextButton("Pause", skin, "toggle");
+		TextButton minimizeButton = new TextButton("-", skin);
+		TextButton bonesSetupPoseButton = new TextButton("Bones", skin);
+		TextButton slotsSetupPoseButton = new TextButton("Slots", skin);
+		TextButton setupPoseButton = new TextButton("Both", skin);
+		WidgetGroup toasts = new WidgetGroup();
+		boolean prefsLoaded;
+
+		public UI () {
+			animationList.getSelection().setRequired(false);
+
+			premultipliedCheckbox.setChecked(true);
+
+			loopCheckbox.setChecked(true);
+
+			scaleSlider.setValue(1);
+			scaleSlider.setSnapToValues(new float[] {1, 1.5f, 2, 2.5f, 3, 3.5f}, 0.01f);
+
+			mixSlider.setValue(0.3f);
+			mixSlider.setSnapToValues(new float[] {1, 1.5f, 2, 2.5f, 3, 3.5f}, 0.1f);
+
+			speedSlider.setValue(1);
+			speedSlider.setSnapToValues(new float[] {0.5f, 0.75f, 1, 1.25f, 1.5f, 2, 2.5f}, 0.1f);
+
+			window.setMovable(false);
+			window.setResizable(false);
+			window.setKeepWithinStage(false);
+			window.setX(-3);
+			window.setY(-2);
+
+			window.getTitleLabel().setColor(new Color(0.76f, 1, 1, 1));
+			window.getTitleTable().add(openButton).space(3);
+			window.getTitleTable().add(minimizeButton).width(20);
+
+			ScrollPane skinScroll = new ScrollPane(skinList, skin, "bg");
+			skinScroll.setFadeScrollBars(false);
+
+			ScrollPane animationScroll = new ScrollPane(animationList, skin, "bg");
+			animationScroll.setFadeScrollBars(false);
+
+			// Layout.
+
+			root.defaults().space(6);
+			root.columnDefaults(0).top().right().padTop(3);
+			root.columnDefaults(1).left();
+			root.add("Scale:");
+			{
+				Table table = table();
+				table.add(scaleLabel).width(29);
+				table.add(scaleSlider).fillX().expandX();
+				root.add(table).fill().row();
+			}
+			root.add("Flip:");
+			root.add(table(flipXCheckbox, flipYCheckbox)).row();
+			root.add("Debug:");
+			root.add(table(debugBonesCheckbox, debugRegionsCheckbox, debugBoundingBoxesCheckbox)).row();
+			root.add();
+			root.add(table(debugMeshHullCheckbox, debugMeshTrianglesCheckbox, debugPathsCheckbox)).row();
+			root.add("Alpha:");
+			root.add(premultipliedCheckbox).row();
+			root.add("Skin:");
+			root.add(skinScroll).expand().fill().minHeight(75).row();
+			root.add("Setup pose:");
+			root.add(table(bonesSetupPoseButton, slotsSetupPoseButton, setupPoseButton)).row();
+			root.add("Animation:");
+			root.add(animationScroll).expand().fill().minHeight(75).row();
+			root.add("Mix:");
+			{
+				Table table = table();
+				table.add(mixLabel).width(29);
+				table.add(mixSlider).fillX().expandX();
+				root.add(table).fill().row();
+			}
+			root.add("Speed:");
+			{
+				Table table = table();
+				table.add(speedLabel).width(29);
+				table.add(speedSlider).fillX().expandX();
+				root.add(table).fill().row();
+			}
+			root.add("Playback:");
+			root.add(table(pauseButton, loopCheckbox)).row();
+
+			window.add(root).expand().fill();
+			window.pack();
+			stage.addActor(window);
+
+			{
+				Table table = new Table(skin);
+				table.setFillParent(true);
+				table.setTouchable(Touchable.disabled);
+				stage.addActor(table);
+				table.pad(10).bottom().right();
+				table.add(toasts);
+			}
+
+			window.addListener(new InputListener() {
+				public boolean touchDown (InputEvent event, float x, float y, int pointer, int button) {
+					event.cancel();
+					return true;
+				}
+			});
+
+			openButton.addListener(new ChangeListener() {
+				public void changed (ChangeEvent event, Actor actor) {
+					FileDialog fileDialog = new FileDialog((Frame)null, "Choose skeleton file");
+					fileDialog.setMode(FileDialog.LOAD);
+					fileDialog.setVisible(true);
+					String name = fileDialog.getFile();
+					String dir = fileDialog.getDirectory();
+					if (name == null || dir == null) return;
+					loadSkeleton(new FileHandle(new File(dir, name).getAbsolutePath()), false);
+				}
+			});
+
+			setupPoseButton.addListener(new ChangeListener() {
+				public void changed (ChangeEvent event, Actor actor) {
+					if (skeleton != null) skeleton.setToSetupPose();
+				}
+			});
+			bonesSetupPoseButton.addListener(new ChangeListener() {
+				public void changed (ChangeEvent event, Actor actor) {
+					if (skeleton != null) skeleton.setBonesToSetupPose();
+				}
+			});
+			slotsSetupPoseButton.addListener(new ChangeListener() {
+				public void changed (ChangeEvent event, Actor actor) {
+					if (skeleton != null) skeleton.setSlotsToSetupPose();
+				}
+			});
+
+			minimizeButton.addListener(new ClickListener() {
+				public boolean touchDown (InputEvent event, float x, float y, int pointer, int button) {
+					event.cancel();
+					return super.touchDown(event, x, y, pointer, button);
+				}
+
+				public void clicked (InputEvent event, float x, float y) {
+					if (minimizeButton.isChecked()) {
+						window.getCells().get(0).setActor(null);
+						window.setHeight(37);
+						minimizeButton.setText("+");
+					} else {
+						window.getCells().get(0).setActor(root);
+						ui.window.setHeight(Gdx.graphics.getHeight() + 8);
+						minimizeButton.setText("-");
+					}
+				}
+			});
+
+			scaleSlider.addListener(new ChangeListener() {
+				public void changed (ChangeEvent event, Actor actor) {
+					scaleLabel.setText(Float.toString((int)(scaleSlider.getValue() * 100) / 100f));
+					if (!scaleSlider.isDragging()) loadSkeleton(skeletonFile, false);
+				}
+			});
+
+			speedSlider.addListener(new ChangeListener() {
+				public void changed (ChangeEvent event, Actor actor) {
+					speedLabel.setText(Float.toString((int)(speedSlider.getValue() * 100) / 100f));
+				}
+			});
+
+			mixSlider.addListener(new ChangeListener() {
+				public void changed (ChangeEvent event, Actor actor) {
+					mixLabel.setText(Float.toString((int)(mixSlider.getValue() * 100) / 100f));
+					if (state != null) state.getData().setDefaultMix(mixSlider.getValue());
+				}
+			});
+
+			animationList.addListener(new ChangeListener() {
+				public void changed (ChangeEvent event, Actor actor) {
+					if (state != null) {
+						String name = animationList.getSelected();
+						if (name == null)
+							state.setEmptyAnimation(0, ui.mixSlider.getValue());
+						else
+							setAnimation();
+					}
+				}
+			});
+
+			loopCheckbox.addListener(new ChangeListener() {
+				public void changed (ChangeEvent event, Actor actor) {
+					setAnimation();
+				}
+			});
+
+			skinList.addListener(new ChangeListener() {
+				public void changed (ChangeEvent event, Actor actor) {
+					if (skeleton != null) {
+						String skinName = skinList.getSelected();
+						if (skinName == null)
+							skeleton.setSkin((Skin)null);
+						else
+							skeleton.setSkin(skinName);
+						skeleton.setSlotsToSetupPose();
+					}
+				}
+			});
+
+			Gdx.input.setInputProcessor(new InputMultiplexer(stage, new InputAdapter() {
+				public boolean touchDown (int screenX, int screenY, int pointer, int button) {
+					touchDragged(screenX, screenY, pointer);
+					return false;
+				}
+
+				public boolean touchDragged (int screenX, int screenY, int pointer) {
+					skeletonX = screenX;
+					skeletonY = Gdx.graphics.getHeight() - screenY;
+					return false;
+				}
+
+				public boolean touchUp (int screenX, int screenY, int pointer, int button) {
+					savePrefs();
+					return false;
+				}
+			}));
+
+			ChangeListener savePrefsListener = new ChangeListener() {
+				public void changed (ChangeEvent event, Actor actor) {
+					if (actor instanceof Slider && ((Slider)actor).isDragging()) return;
+					savePrefs();
+				}
+			};
+			debugBonesCheckbox.addListener(savePrefsListener);
+			debugRegionsCheckbox.addListener(savePrefsListener);
+			debugMeshHullCheckbox.addListener(savePrefsListener);
+			debugMeshTrianglesCheckbox.addListener(savePrefsListener);
+			debugPathsCheckbox.addListener(savePrefsListener);
+			premultipliedCheckbox.addListener(savePrefsListener);
+			loopCheckbox.addListener(savePrefsListener);
+			speedSlider.addListener(savePrefsListener);
+			mixSlider.addListener(savePrefsListener);
+			scaleSlider.addListener(savePrefsListener);
+			animationList.addListener(savePrefsListener);
+			skinList.addListener(savePrefsListener);
+		}
+
+		private Table table (Actor... actors) {
+			Table table = new Table();
+			table.defaults().space(6);
+			table.add(actors);
+			return table;
+		}
+
+		void toast (String text) {
+			Table table = new Table();
+			table.add(new Label(text, skin));
+			table.getColor().a = 0;
+			table.pack();
+			table.setPosition(-table.getWidth(), -3 - table.getHeight());
+			table.addAction(sequence( //
+				parallel(moveBy(0, table.getHeight(), 0.3f), fadeIn(0.3f)), //
+				delay(5f), //
+				parallel(moveBy(0, table.getHeight(), 0.3f), fadeOut(0.3f)), //
+				removeActor() //
+			));
+			for (Actor actor : toasts.getChildren())
+				actor.addAction(moveBy(0, table.getHeight(), 0.3f));
+			toasts.addActor(table);
+			toasts.getParent().toFront();
+		}
+
+		void savePrefs () {
+			if (!prefsLoaded) return;
+			prefs.putBoolean("debugBones", debugBonesCheckbox.isChecked());
+			prefs.putBoolean("debugRegions", debugRegionsCheckbox.isChecked());
+			prefs.putBoolean("debugMeshHull", debugMeshHullCheckbox.isChecked());
+			prefs.putBoolean("debugMeshTriangles", debugMeshTrianglesCheckbox.isChecked());
+			prefs.putBoolean("debugPaths", debugPathsCheckbox.isChecked());
+			prefs.putBoolean("premultiplied", premultipliedCheckbox.isChecked());
+			prefs.putBoolean("loop", loopCheckbox.isChecked());
+			prefs.putFloat("speed", speedSlider.getValue());
+			prefs.putFloat("mix", mixSlider.getValue());
+			prefs.putFloat("scale", scaleSlider.getValue());
+			prefs.putInteger("x", skeletonX);
+			prefs.putInteger("y", skeletonY);
+			if (animationList.getSelected() != null) prefs.putString("animationName", animationList.getSelected());
+			if (skinList.getSelected() != null) prefs.putString("skinName", skinList.getSelected());
+			prefs.flush();
+		}
+
+		void loadPrefs () {
+			debugBonesCheckbox.setChecked(prefs.getBoolean("debugBones", true));
+			debugRegionsCheckbox.setChecked(prefs.getBoolean("debugRegions", false));
+			debugMeshHullCheckbox.setChecked(prefs.getBoolean("debugMeshHull", false));
+			debugMeshTrianglesCheckbox.setChecked(prefs.getBoolean("debugMeshTriangles", false));
+			debugPathsCheckbox.setChecked(prefs.getBoolean("debugPaths", true));
+			premultipliedCheckbox.setChecked(prefs.getBoolean("premultiplied", true));
+			loopCheckbox.setChecked(prefs.getBoolean("loop", false));
+			speedSlider.setValue(prefs.getFloat("speed", 0.3f));
+			mixSlider.setValue(prefs.getFloat("mix", 0.3f));
+			scaleSlider.setValue(prefs.getFloat("scale", 1));
+			skeletonX = prefs.getInteger("x", 0);
+			skeletonY = prefs.getInteger("y", 0);
+			animationList.setSelected(prefs.getString("animationName", null));
+			skinList.setSelected(prefs.getString("skinName", null));
+			prefsLoaded = true;
+		}
+	}
+
+	static public void main (String[] args) throws Exception {
+		LwjglApplicationConfiguration.disableAudio = true;
+		LwjglApplicationConfiguration config = new LwjglApplicationConfiguration();
+		config.width = 800;
+		config.height = 600;
+		config.title = "Skeleton Viewer";
+		config.allowSoftwareMode = true;
+		new LwjglApplication(new SkeletonViewer(), config);
+	}
 }