diff --git a/.gitignore b/.gitignore
index 4478e9e1f..d0064313e 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,2 +1,6 @@
spine-cpp/Debug/*
spine-libgdx/bin/*
+spine-libgdx/libs/gdx-backend-lwjgl-natives.jar
+spine-libgdx/libs/gdx-backend-lwjgl.jar
+spine-libgdx/libs/gdx-natives.jar
+spine-libgdx/libs/gdx.jar
diff --git a/spine-libgdx/.classpath b/spine-libgdx/.classpath
new file mode 100644
index 000000000..70260f375
--- /dev/null
+++ b/spine-libgdx/.classpath
@@ -0,0 +1,9 @@
+
+
+
+
+
+
+
+
+
diff --git a/spine-libgdx/.project b/spine-libgdx/.project
new file mode 100644
index 000000000..7d65046eb
--- /dev/null
+++ b/spine-libgdx/.project
@@ -0,0 +1,17 @@
+
+
+ spine-libgdx
+
+
+
+
+
+ org.eclipse.jdt.core.javabuilder
+
+
+
+
+
+ org.eclipse.jdt.core.javanature
+
+
diff --git a/spine-libgdx/.settings/org.eclipse.jdt.core.prefs b/spine-libgdx/.settings/org.eclipse.jdt.core.prefs
new file mode 100644
index 000000000..089aa9ace
--- /dev/null
+++ b/spine-libgdx/.settings/org.eclipse.jdt.core.prefs
@@ -0,0 +1,95 @@
+eclipse.preferences.version=1
+org.eclipse.jdt.core.compiler.annotation.missingNonNullByDefaultAnnotation=ignore
+org.eclipse.jdt.core.compiler.annotation.nonnull=org.eclipse.jdt.annotation.NonNull
+org.eclipse.jdt.core.compiler.annotation.nonnullbydefault=org.eclipse.jdt.annotation.NonNullByDefault
+org.eclipse.jdt.core.compiler.annotation.nullable=org.eclipse.jdt.annotation.Nullable
+org.eclipse.jdt.core.compiler.annotation.nullanalysis=disabled
+org.eclipse.jdt.core.compiler.codegen.inlineJsrBytecode=enabled
+org.eclipse.jdt.core.compiler.codegen.targetPlatform=1.5
+org.eclipse.jdt.core.compiler.codegen.unusedLocal=preserve
+org.eclipse.jdt.core.compiler.compliance=1.5
+org.eclipse.jdt.core.compiler.debug.lineNumber=generate
+org.eclipse.jdt.core.compiler.debug.localVariable=generate
+org.eclipse.jdt.core.compiler.debug.sourceFile=generate
+org.eclipse.jdt.core.compiler.problem.annotationSuperInterface=warning
+org.eclipse.jdt.core.compiler.problem.assertIdentifier=error
+org.eclipse.jdt.core.compiler.problem.autoboxing=ignore
+org.eclipse.jdt.core.compiler.problem.comparingIdentical=warning
+org.eclipse.jdt.core.compiler.problem.deadCode=ignore
+org.eclipse.jdt.core.compiler.problem.deprecation=ignore
+org.eclipse.jdt.core.compiler.problem.deprecationInDeprecatedCode=disabled
+org.eclipse.jdt.core.compiler.problem.deprecationWhenOverridingDeprecatedMethod=disabled
+org.eclipse.jdt.core.compiler.problem.discouragedReference=warning
+org.eclipse.jdt.core.compiler.problem.emptyStatement=ignore
+org.eclipse.jdt.core.compiler.problem.enumIdentifier=error
+org.eclipse.jdt.core.compiler.problem.explicitlyClosedAutoCloseable=ignore
+org.eclipse.jdt.core.compiler.problem.fallthroughCase=ignore
+org.eclipse.jdt.core.compiler.problem.fatalOptionalError=disabled
+org.eclipse.jdt.core.compiler.problem.fieldHiding=ignore
+org.eclipse.jdt.core.compiler.problem.finalParameterBound=warning
+org.eclipse.jdt.core.compiler.problem.finallyBlockNotCompletingNormally=warning
+org.eclipse.jdt.core.compiler.problem.forbiddenReference=warning
+org.eclipse.jdt.core.compiler.problem.hiddenCatchBlock=warning
+org.eclipse.jdt.core.compiler.problem.includeNullInfoFromAsserts=disabled
+org.eclipse.jdt.core.compiler.problem.incompatibleNonInheritedInterfaceMethod=warning
+org.eclipse.jdt.core.compiler.problem.incompleteEnumSwitch=warning
+org.eclipse.jdt.core.compiler.problem.indirectStaticAccess=ignore
+org.eclipse.jdt.core.compiler.problem.localVariableHiding=ignore
+org.eclipse.jdt.core.compiler.problem.methodWithConstructorName=warning
+org.eclipse.jdt.core.compiler.problem.missingDefaultCase=ignore
+org.eclipse.jdt.core.compiler.problem.missingDeprecatedAnnotation=ignore
+org.eclipse.jdt.core.compiler.problem.missingEnumCaseDespiteDefault=disabled
+org.eclipse.jdt.core.compiler.problem.missingHashCodeMethod=ignore
+org.eclipse.jdt.core.compiler.problem.missingOverrideAnnotation=ignore
+org.eclipse.jdt.core.compiler.problem.missingOverrideAnnotationForInterfaceMethodImplementation=enabled
+org.eclipse.jdt.core.compiler.problem.missingSerialVersion=ignore
+org.eclipse.jdt.core.compiler.problem.missingSynchronizedOnInheritedMethod=ignore
+org.eclipse.jdt.core.compiler.problem.noEffectAssignment=warning
+org.eclipse.jdt.core.compiler.problem.noImplicitStringConversion=warning
+org.eclipse.jdt.core.compiler.problem.nonExternalizedStringLiteral=ignore
+org.eclipse.jdt.core.compiler.problem.nullAnnotationInferenceConflict=error
+org.eclipse.jdt.core.compiler.problem.nullReference=warning
+org.eclipse.jdt.core.compiler.problem.nullSpecViolation=error
+org.eclipse.jdt.core.compiler.problem.nullUncheckedConversion=warning
+org.eclipse.jdt.core.compiler.problem.overridingPackageDefaultMethod=warning
+org.eclipse.jdt.core.compiler.problem.parameterAssignment=ignore
+org.eclipse.jdt.core.compiler.problem.possibleAccidentalBooleanAssignment=warning
+org.eclipse.jdt.core.compiler.problem.potentialNullReference=ignore
+org.eclipse.jdt.core.compiler.problem.potentiallyUnclosedCloseable=ignore
+org.eclipse.jdt.core.compiler.problem.rawTypeReference=ignore
+org.eclipse.jdt.core.compiler.problem.redundantNullAnnotation=warning
+org.eclipse.jdt.core.compiler.problem.redundantNullCheck=ignore
+org.eclipse.jdt.core.compiler.problem.redundantSpecificationOfTypeArguments=ignore
+org.eclipse.jdt.core.compiler.problem.redundantSuperinterface=ignore
+org.eclipse.jdt.core.compiler.problem.reportMethodCanBePotentiallyStatic=ignore
+org.eclipse.jdt.core.compiler.problem.reportMethodCanBeStatic=ignore
+org.eclipse.jdt.core.compiler.problem.specialParameterHidingField=disabled
+org.eclipse.jdt.core.compiler.problem.staticAccessReceiver=warning
+org.eclipse.jdt.core.compiler.problem.suppressOptionalErrors=disabled
+org.eclipse.jdt.core.compiler.problem.suppressWarnings=enabled
+org.eclipse.jdt.core.compiler.problem.syntheticAccessEmulation=warning
+org.eclipse.jdt.core.compiler.problem.typeParameterHiding=warning
+org.eclipse.jdt.core.compiler.problem.unavoidableGenericTypeProblems=enabled
+org.eclipse.jdt.core.compiler.problem.uncheckedTypeOperation=ignore
+org.eclipse.jdt.core.compiler.problem.unclosedCloseable=ignore
+org.eclipse.jdt.core.compiler.problem.undocumentedEmptyBlock=ignore
+org.eclipse.jdt.core.compiler.problem.unhandledWarningToken=warning
+org.eclipse.jdt.core.compiler.problem.unnecessaryElse=ignore
+org.eclipse.jdt.core.compiler.problem.unnecessaryTypeCheck=ignore
+org.eclipse.jdt.core.compiler.problem.unqualifiedFieldAccess=ignore
+org.eclipse.jdt.core.compiler.problem.unusedDeclaredThrownException=ignore
+org.eclipse.jdt.core.compiler.problem.unusedDeclaredThrownExceptionExemptExceptionAndThrowable=enabled
+org.eclipse.jdt.core.compiler.problem.unusedDeclaredThrownExceptionIncludeDocCommentReference=enabled
+org.eclipse.jdt.core.compiler.problem.unusedDeclaredThrownExceptionWhenOverriding=enabled
+org.eclipse.jdt.core.compiler.problem.unusedImport=ignore
+org.eclipse.jdt.core.compiler.problem.unusedLabel=warning
+org.eclipse.jdt.core.compiler.problem.unusedLocal=ignore
+org.eclipse.jdt.core.compiler.problem.unusedObjectAllocation=ignore
+org.eclipse.jdt.core.compiler.problem.unusedParameter=ignore
+org.eclipse.jdt.core.compiler.problem.unusedParameterIncludeDocCommentReference=enabled
+org.eclipse.jdt.core.compiler.problem.unusedParameterWhenImplementingAbstract=enabled
+org.eclipse.jdt.core.compiler.problem.unusedParameterWhenOverridingConcrete=enabled
+org.eclipse.jdt.core.compiler.problem.unusedPrivateMember=ignore
+org.eclipse.jdt.core.compiler.problem.unusedWarningToken=warning
+org.eclipse.jdt.core.compiler.problem.varargsArgumentNeedCast=warning
+org.eclipse.jdt.core.compiler.source=1.5
diff --git a/spine-libgdx/src/com/esotericsoftware/spine/Animation.java b/spine-libgdx/src/com/esotericsoftware/spine/Animation.java
new file mode 100644
index 000000000..3148feb2d
--- /dev/null
+++ b/spine-libgdx/src/com/esotericsoftware/spine/Animation.java
@@ -0,0 +1,477 @@
+
+package com.esotericsoftware.spine;
+
+import com.badlogic.gdx.graphics.Color;
+import com.badlogic.gdx.math.MathUtils;
+import com.badlogic.gdx.utils.Array;
+
+public class Animation {
+ private final Array timelines;
+ private float duration;
+
+ public Animation (Array timelines, float duration) {
+ if (timelines == null) throw new IllegalArgumentException("timelines cannot be null.");
+ this.timelines = timelines;
+ this.duration = duration;
+ }
+
+ public Array getTimelines () {
+ return timelines;
+ }
+
+ /** Returns the duration of the animation in seconds. Defaults to the max {@link Timeline#getDuration() duration} of the
+ * timelines. */
+ public float getDuration () {
+ return duration;
+ }
+
+ public void setDuration (float duration) {
+ this.duration = duration;
+ }
+
+ /** Poses the skeleton at the specified time for this animation. */
+ public void apply (Skeleton skeleton, float time, boolean loop) {
+ if (skeleton == null) throw new IllegalArgumentException("skeleton cannot be null.");
+
+ if (loop && duration != 0) time %= duration;
+
+ Array timeline = this.timelines;
+ for (int i = 0, n = timeline.size; i < n; i++)
+ timeline.get(i).apply(skeleton, time, 1);
+ }
+
+ /** Poses the skeleton at the specified time for this animation mixed with the current pose.
+ * @param alpha The amount of this animation that affects the current pose. */
+ public void mix (Skeleton skeleton, float time, boolean loop, float alpha) {
+ if (skeleton == null) throw new IllegalArgumentException("skeleton cannot be null.");
+
+ if (loop && duration != 0) time %= duration;
+
+ Array timeline = this.timelines;
+ for (int i = 0, n = timeline.size; i < n; i++)
+ timeline.get(i).apply(skeleton, time, alpha);
+ }
+
+ /** @param target After the first and before the last entry. */
+ 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;
+ }
+ }
+
+ 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) continue;
+ return i;
+ }
+ return -1;
+ }
+
+ /** The keyframes for a single animation timeline. */
+ static public interface Timeline {
+ /** Returns the time in seconds of the last keyframe. */
+ public float getDuration ();
+
+ public int getKeyframeCount ();
+
+ /** Sets the value(s) for the specified time. */
+ public void apply (Skeleton skeleton, float time, float alpha);
+ }
+
+ /** Base class for frames that use an interpolation bezier curve. */
+ static public abstract class CurveTimeline implements Timeline {
+ static private final float LINEAR = 0;
+ static private final float STEPPED = -1;
+ static private final int BEZIER_SEGMENTS = 10;
+
+ private final float[] curves; // dfx, dfy, ddfx, ddfy, dddfx, dddfy, ...
+
+ public CurveTimeline (int keyframeCount) {
+ curves = new float[(keyframeCount - 1) * 6];
+ }
+
+ public void setLinear (int keyframeIndex) {
+ curves[keyframeIndex * 6] = LINEAR;
+ }
+
+ public void setStepped (int keyframeIndex) {
+ curves[keyframeIndex * 6] = STEPPED;
+ }
+
+ /** 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 keyframeIndex, float cx1, float cy1, float cx2, float cy2) {
+ float subdiv_step = 1f / BEZIER_SEGMENTS;
+ float subdiv_step2 = subdiv_step * subdiv_step;
+ float subdiv_step3 = subdiv_step2 * subdiv_step;
+ float pre1 = 3 * subdiv_step;
+ float pre2 = 3 * subdiv_step2;
+ float pre4 = 6 * subdiv_step2;
+ float pre5 = 6 * subdiv_step3;
+ float tmp1x = -cx1 * 2 + cx2;
+ float tmp1y = -cy1 * 2 + cy2;
+ float tmp2x = (cx1 - cx2) * 3 + 1;
+ float tmp2y = (cy1 - cy2) * 3 + 1;
+ int i = keyframeIndex * 6;
+ float[] curves = this.curves;
+ curves[i] = cx1 * pre1 + tmp1x * pre2 + tmp2x * subdiv_step3;
+ curves[i + 1] = cy1 * pre1 + tmp1y * pre2 + tmp2y * subdiv_step3;
+ curves[i + 2] = tmp1x * pre4 + tmp2x * pre5;
+ curves[i + 3] = tmp1y * pre4 + tmp2y * pre5;
+ curves[i + 4] = tmp2x * pre5;
+ curves[i + 5] = tmp2y * pre5;
+ }
+
+ public float getCurvePercent (int keyframeIndex, float percent) {
+ int curveIndex = keyframeIndex * 6;
+ float[] curves = this.curves;
+ float dfx = curves[curveIndex];
+ if (dfx == LINEAR) return percent;
+ if (dfx == STEPPED) return 0;
+ float dfy = curves[curveIndex + 1];
+ float ddfx = curves[curveIndex + 2];
+ float ddfy = curves[curveIndex + 3];
+ float dddfx = curves[curveIndex + 4];
+ float dddfy = curves[curveIndex + 5];
+ float x = dfx, y = dfy;
+ int i = BEZIER_SEGMENTS - 2;
+ while (true) {
+ if (x >= percent) {
+ float lastX = x - dfx;
+ float lastY = y - dfy;
+ return lastY + (y - lastY) * (percent - lastX) / (x - lastX);
+ }
+ if (i == 0) break;
+ i--;
+ dfx += ddfx;
+ dfy += ddfy;
+ ddfx += dddfx;
+ ddfy += dddfy;
+ x += dfx;
+ y += dfy;
+ }
+ return y + (1 - y) * (percent - x) / (1 - x); // Last point is 1,1.
+ }
+ }
+
+ static public class RotateTimeline extends CurveTimeline {
+ static private final int LAST_FRAME_TIME = -2;
+ static private final int FRAME_VALUE = 1;
+
+ private int boneIndex;
+ private final float[] frames; // time, value, ...
+
+ public RotateTimeline (int keyframeCount) {
+ super(keyframeCount);
+ frames = new float[keyframeCount * 2];
+ }
+
+ public float getDuration () {
+ return frames[frames.length - 2];
+ }
+
+ public int getKeyframeCount () {
+ return frames.length / 2;
+ }
+
+ public void setBoneIndex (int boneIndex) {
+ this.boneIndex = boneIndex;
+ }
+
+ public int getBoneIndex () {
+ return boneIndex;
+ }
+
+ public float[] getKeyframes () {
+ return frames;
+ }
+
+ /** Sets the time and value of the specified keyframe. */
+ public void setKeyframe (int keyframeIndex, float time, float value) {
+ keyframeIndex *= 2;
+ frames[keyframeIndex] = time;
+ frames[keyframeIndex + 1] = value;
+ }
+
+ public void apply (Skeleton skeleton, float time, float alpha) {
+ 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 - 2]) { // Time is after last frame.
+ float amount = bone.data.rotation + frames[frames.length - 1] - bone.rotation;
+ while (amount > 180)
+ amount -= 360;
+ while (amount < -180)
+ amount += 360;
+ bone.rotation += amount * alpha;
+ return;
+ }
+
+ // Interpolate between the last frame and the current frame.
+ int frameIndex = binarySearch(frames, time, 2);
+ float lastFrameValue = frames[frameIndex - 1];
+ float frameTime = frames[frameIndex];
+ float percent = MathUtils.clamp(1 - (time - frameTime) / (frames[frameIndex + LAST_FRAME_TIME] - frameTime), 0, 1);
+ percent = getCurvePercent(frameIndex / 2 - 1, percent);
+
+ float amount = frames[frameIndex + FRAME_VALUE] - lastFrameValue;
+ while (amount > 180)
+ amount -= 360;
+ while (amount < -180)
+ amount += 360;
+ amount = bone.data.rotation + (lastFrameValue + amount * percent) - bone.rotation;
+ while (amount > 180)
+ amount -= 360;
+ while (amount < -180)
+ amount += 360;
+ bone.rotation += amount * alpha;
+ }
+ }
+
+ static public class TranslateTimeline extends CurveTimeline {
+ static final int LAST_FRAME_TIME = -3;
+ static final int FRAME_X = 1;
+ static final int FRAME_Y = 2;
+
+ int boneIndex;
+ final float[] frames; // time, value, value, ...
+
+ public TranslateTimeline (int keyframeCount) {
+ super(keyframeCount);
+ frames = new float[keyframeCount * 3];
+ }
+
+ public float getDuration () {
+ return frames[frames.length - 3];
+ }
+
+ public int getKeyframeCount () {
+ return frames.length / 3;
+ }
+
+ public void setBoneIndex (int boneIndex) {
+ this.boneIndex = boneIndex;
+ }
+
+ public int getBoneIndex () {
+ return boneIndex;
+ }
+
+ public float[] getKeyframes () {
+ return frames;
+ }
+
+ /** Sets the time and value of the specified keyframe. */
+ public void setKeyframe (int keyframeIndex, float time, float x, float y) {
+ keyframeIndex *= 3;
+ frames[keyframeIndex] = time;
+ frames[keyframeIndex + 1] = x;
+ frames[keyframeIndex + 2] = y;
+ }
+
+ public void apply (Skeleton skeleton, float time, float alpha) {
+ 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 - 3]) { // Time is after last frame.
+ bone.x += (bone.data.x + frames[frames.length - 2] - bone.x) * alpha;
+ bone.y += (bone.data.y + frames[frames.length - 1] - bone.y) * alpha;
+ return;
+ }
+
+ // Interpolate between the last frame and the current frame.
+ int frameIndex = binarySearch(frames, time, 3);
+ float lastFrameX = frames[frameIndex - 2];
+ float lastFrameY = frames[frameIndex - 1];
+ float frameTime = frames[frameIndex];
+ float percent = MathUtils.clamp(1 - (time - frameTime) / (frames[frameIndex + LAST_FRAME_TIME] - frameTime), 0, 1);
+ percent = getCurvePercent(frameIndex / 3 - 1, percent);
+
+ bone.x += (bone.data.x + lastFrameX + (frames[frameIndex + FRAME_X] - lastFrameX) * percent - bone.x) * alpha;
+ bone.y += (bone.data.y + lastFrameY + (frames[frameIndex + FRAME_Y] - lastFrameY) * percent - bone.y) * alpha;
+ }
+ }
+
+ static public class ScaleTimeline extends TranslateTimeline {
+ public ScaleTimeline (int keyframeCount) {
+ super(keyframeCount);
+ }
+
+ public void apply (Skeleton skeleton, float time, float alpha) {
+ 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 - 3]) { // Time is after last frame.
+ bone.scaleX += (bone.data.scaleX - 1 + frames[frames.length - 2] - bone.scaleX) * alpha;
+ bone.scaleY += (bone.data.scaleY - 1 + frames[frames.length - 1] - bone.scaleY) * alpha;
+ return;
+ }
+
+ // Interpolate between the last frame and the current frame.
+ int frameIndex = binarySearch(frames, time, 3);
+ float lastFrameX = frames[frameIndex - 2];
+ float lastFrameY = frames[frameIndex - 1];
+ float frameTime = frames[frameIndex];
+ float percent = MathUtils.clamp(1 - (time - frameTime) / (frames[frameIndex + LAST_FRAME_TIME] - frameTime), 0, 1);
+ percent = getCurvePercent(frameIndex / 3 - 1, percent);
+
+ bone.scaleX += (bone.data.scaleX - 1 + lastFrameX + (frames[frameIndex + FRAME_X] - lastFrameX) * percent - bone.scaleX)
+ * alpha;
+ bone.scaleY += (bone.data.scaleY - 1 + lastFrameY + (frames[frameIndex + FRAME_Y] - lastFrameY) * percent - bone.scaleY)
+ * alpha;
+ }
+ }
+
+ static public class ColorTimeline extends CurveTimeline {
+ static private final int LAST_FRAME_TIME = -5;
+ static private final int FRAME_R = 1;
+ static private final int FRAME_G = 2;
+ static private final int FRAME_B = 3;
+ static private final int FRAME_A = 4;
+
+ private int slotIndex;
+ private final float[] frames; // time, r, g, b, a, ...
+
+ public ColorTimeline (int keyframeCount) {
+ super(keyframeCount);
+ frames = new float[keyframeCount * 5];
+ }
+
+ public float getDuration () {
+ return frames[frames.length - 5];
+ }
+
+ public int getKeyframeCount () {
+ return frames.length / 5;
+ }
+
+ public void setSlotIndex (int slotIndex) {
+ this.slotIndex = slotIndex;
+ }
+
+ public int getSlotIndex () {
+ return slotIndex;
+ }
+
+ public float[] getKeyframes () {
+ return frames;
+ }
+
+ /** Sets the time and value of the specified keyframe. */
+ public void setKeyframe (int keyframeIndex, float time, float r, float g, float b, float a) {
+ keyframeIndex *= 5;
+ frames[keyframeIndex] = time;
+ frames[keyframeIndex + 1] = r;
+ frames[keyframeIndex + 2] = g;
+ frames[keyframeIndex + 3] = b;
+ frames[keyframeIndex + 4] = a;
+ }
+
+ public void apply (Skeleton skeleton, float time, float alpha) {
+ float[] frames = this.frames;
+ if (time < frames[0]) return; // Time is before first frame.
+
+ Color color = skeleton.slots.get(slotIndex).color;
+
+ if (time >= frames[frames.length - 5]) { // Time is after last frame.
+ int i = frames.length - 1;
+ float r = frames[i - 3];
+ float g = frames[i - 2];
+ float b = frames[i - 1];
+ float a = frames[i];
+ color.set(r, g, b, a);
+ return;
+ }
+
+ // Interpolate between the last frame and the current frame.
+ int frameIndex = binarySearch(frames, time, 5);
+ float lastFrameR = frames[frameIndex - 4];
+ float lastFrameG = frames[frameIndex - 3];
+ float lastFrameB = frames[frameIndex - 2];
+ float lastFrameA = frames[frameIndex - 1];
+ float frameTime = frames[frameIndex];
+ float percent = MathUtils.clamp(1 - (time - frameTime) / (frames[frameIndex + LAST_FRAME_TIME] - frameTime), 0, 1);
+ percent = getCurvePercent(frameIndex / 5 - 1, percent);
+
+ float r = lastFrameR + (frames[frameIndex + FRAME_R] - lastFrameR) * percent;
+ float g = lastFrameG + (frames[frameIndex + FRAME_G] - lastFrameG) * percent;
+ float b = lastFrameB + (frames[frameIndex + FRAME_B] - lastFrameB) * percent;
+ float a = lastFrameA + (frames[frameIndex + FRAME_A] - lastFrameA) * percent;
+ if (alpha < 1)
+ color.add((r - color.r) * alpha, (g - color.g) * alpha, (b - color.b) * alpha, (a - color.a) * alpha);
+ else
+ color.set(r, g, b, a);
+ }
+ }
+
+ static public class AttachmentTimeline implements Timeline {
+ private int slotIndex;
+ private final float[] frames; // time, ...
+ private final String[] attachmentNames;
+
+ public AttachmentTimeline (int keyframeCount) {
+ frames = new float[keyframeCount];
+ attachmentNames = new String[keyframeCount];
+ }
+
+ public float getDuration () {
+ return frames[frames.length - 1];
+ }
+
+ public int getKeyframeCount () {
+ return frames.length;
+ }
+
+ public int getSlotIndex () {
+ return slotIndex;
+ }
+
+ public void setSlotIndex (int slotIndex) {
+ this.slotIndex = slotIndex;
+ }
+
+ public float[] getKeyframes () {
+ return frames;
+ }
+
+ public String[] getAttachmentNames () {
+ return attachmentNames;
+ }
+
+ /** Sets the time and value of the specified keyframe. */
+ public void setKeyframe (int keyframeIndex, float time, String attachmentName) {
+ frames[keyframeIndex] = time;
+ attachmentNames[keyframeIndex] = attachmentName;
+ }
+
+ public void apply (Skeleton skeleton, float time, float alpha) {
+ 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));
+ }
+ }
+}
diff --git a/spine-libgdx/src/com/esotericsoftware/spine/Attachment.java b/spine-libgdx/src/com/esotericsoftware/spine/Attachment.java
new file mode 100644
index 000000000..aaa7528a2
--- /dev/null
+++ b/spine-libgdx/src/com/esotericsoftware/spine/Attachment.java
@@ -0,0 +1,91 @@
+
+package com.esotericsoftware.spine;
+
+import com.badlogic.gdx.graphics.g2d.SpriteBatch;
+
+abstract public class Attachment {
+ final String name;
+ boolean resolved;
+ private float x, y, scaleX, scaleY, rotation, width, height;
+
+ public Attachment (String name) {
+ if (name == null) throw new IllegalArgumentException("name cannot be null.");
+ this.name = name;
+ }
+
+ abstract public void updateOffset ();
+
+ abstract public void draw (SpriteBatch batch, Slot slot);
+
+ 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 boolean isResolved () {
+ return resolved;
+ }
+
+ public void setResolved (boolean resolved) {
+ this.resolved = resolved;
+ }
+
+ public String getName () {
+ return name;
+ }
+
+ public String toString () {
+ return name;
+ }
+}
diff --git a/spine-libgdx/src/com/esotericsoftware/spine/AttachmentResolver.java b/spine-libgdx/src/com/esotericsoftware/spine/AttachmentResolver.java
new file mode 100644
index 000000000..4d668b8d9
--- /dev/null
+++ b/spine-libgdx/src/com/esotericsoftware/spine/AttachmentResolver.java
@@ -0,0 +1,6 @@
+
+package com.esotericsoftware.spine;
+
+public interface AttachmentResolver {
+ public void resolve (Attachment attachment);
+}
diff --git a/spine-libgdx/src/com/esotericsoftware/spine/Bone.java b/spine-libgdx/src/com/esotericsoftware/spine/Bone.java
new file mode 100644
index 000000000..b0d4b7ec8
--- /dev/null
+++ b/spine-libgdx/src/com/esotericsoftware/spine/Bone.java
@@ -0,0 +1,185 @@
+
+package com.esotericsoftware.spine;
+
+import static com.badlogic.gdx.math.Matrix3.*;
+
+import com.badlogic.gdx.math.MathUtils;
+import com.badlogic.gdx.math.Matrix3;
+
+public class Bone {
+ final BoneData data;
+ final Bone parent;
+ float x, y;
+ float rotation;
+ float scaleX = 1, scaleY = 1;
+
+ float m00, m01, worldX; // a b x
+ float m10, m11, worldY; // c d y
+ float worldRotation;
+ float worldScaleX, worldScaleY;
+
+ /** @param parent May be null. */
+ public Bone (BoneData data, Bone parent) {
+ if (data == null) throw new IllegalArgumentException("data cannot be null.");
+ this.data = data;
+ this.parent = parent;
+ setToBindPose();
+ }
+
+ /** Copy constructor.
+ * @param parent May be null. */
+ public Bone (Bone bone, Bone parent) {
+ if (bone == null) throw new IllegalArgumentException("bone cannot be null.");
+ this.parent = parent;
+ data = bone.data;
+ x = bone.x;
+ y = bone.y;
+ rotation = bone.rotation;
+ scaleX = bone.scaleX;
+ scaleY = bone.scaleY;
+ }
+
+ /** Computes the world SRT using the parent bone and the local SRT. */
+ public void updateWorldTransform (boolean flipX, boolean flipY) {
+ Bone parent = this.parent;
+ if (parent != null) {
+ worldX = x * parent.m00 + y * parent.m01 + parent.worldX;
+ worldY = x * parent.m10 + y * parent.m11 + parent.worldY;
+ worldScaleX = parent.worldScaleX * scaleX;
+ worldScaleY = parent.worldScaleY * scaleY;
+ worldRotation = parent.worldRotation + rotation;
+ } else {
+ worldX = x;
+ worldY = y;
+ worldScaleX = scaleX;
+ worldScaleY = scaleY;
+ worldRotation = rotation;
+ }
+ float cos = MathUtils.cosDeg(worldRotation);
+ float sin = MathUtils.sinDeg(worldRotation);
+ m00 = cos * worldScaleX;
+ m10 = sin * worldScaleX;
+ m01 = -sin * worldScaleY;
+ m11 = cos * worldScaleY;
+ if (flipX) {
+ m00 = -m00;
+ m01 = -m01;
+ }
+ if (flipY) {
+ m10 = -m10;
+ m11 = -m11;
+ }
+ }
+
+ public void setToBindPose () {
+ BoneData data = this.data;
+ x = data.x;
+ y = data.y;
+ rotation = data.rotation;
+ scaleX = data.scaleX;
+ scaleY = data.scaleY;
+ }
+
+ public BoneData getData () {
+ return data;
+ }
+
+ public Bone getParent () {
+ return parent;
+ }
+
+ 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 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 float getM00 () {
+ return m00;
+ }
+
+ public float getM01 () {
+ return m01;
+ }
+
+ public float getM10 () {
+ return m10;
+ }
+
+ public float getM11 () {
+ return m11;
+ }
+
+ public float getWorldX () {
+ return worldX;
+ }
+
+ public float getWorldY () {
+ return worldY;
+ }
+
+ public float getWorldRotation () {
+ return worldRotation;
+ }
+
+ public float getWorldScaleX () {
+ return worldScaleX;
+ }
+
+ public float getWorldScaleY () {
+ return worldScaleY;
+ }
+
+ public Matrix3 getWorldTransform (Matrix3 worldTransform) {
+ if (worldTransform == null) throw new IllegalArgumentException("worldTransform cannot be null.");
+ float[] val = worldTransform.val;
+ val[M00] = m00;
+ val[M01] = m01;
+ val[M02] = worldX;
+ val[M10] = m10;
+ val[M11] = m11;
+ val[M12] = worldY;
+ val[M20] = 0;
+ val[M21] = 0;
+ val[M22] = 1;
+ return worldTransform;
+ }
+
+ public String toString () {
+ return data.name;
+ }
+}
diff --git a/spine-libgdx/src/com/esotericsoftware/spine/BoneData.java b/spine-libgdx/src/com/esotericsoftware/spine/BoneData.java
new file mode 100644
index 000000000..dbf1069f1
--- /dev/null
+++ b/spine-libgdx/src/com/esotericsoftware/spine/BoneData.java
@@ -0,0 +1,93 @@
+
+package com.esotericsoftware.spine;
+
+public class BoneData {
+ final BoneData parent;
+ final String name;
+ float length;
+ float x, y;
+ float rotation;
+ float scaleX = 1, scaleY = 1;
+
+ /** @param parent May be null. */
+ public BoneData (String name, BoneData parent) {
+ if (name == null) throw new IllegalArgumentException("name cannot be null.");
+ 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.");
+ this.parent = parent;
+ name = bone.name;
+ length = bone.length;
+ x = bone.x;
+ y = bone.y;
+ rotation = bone.rotation;
+ scaleX = bone.scaleX;
+ scaleY = bone.scaleY;
+ }
+
+ /** @return May be null. */
+ public BoneData getParent () {
+ return parent;
+ }
+
+ public String getName () {
+ return name;
+ }
+
+ 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 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 String toString () {
+ return name;
+ }
+}
diff --git a/spine-libgdx/src/com/esotericsoftware/spine/Skeleton.java b/spine-libgdx/src/com/esotericsoftware/spine/Skeleton.java
new file mode 100644
index 000000000..64cfe3ac9
--- /dev/null
+++ b/spine-libgdx/src/com/esotericsoftware/spine/Skeleton.java
@@ -0,0 +1,279 @@
+
+package com.esotericsoftware.spine;
+
+import com.esotericsoftware.spine.Skin.Key;
+
+import com.badlogic.gdx.graphics.Color;
+import com.badlogic.gdx.graphics.g2d.SpriteBatch;
+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.ObjectMap.Entry;
+
+public class Skeleton {
+ final SkeletonData data;
+ final Array bones;
+ final Array slots;
+ final Array drawOrder;
+ Skin skin;
+ final Color color;
+ float time;
+ boolean flipX, flipY;
+
+ 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 parent = boneData.parent == null ? null : bones.get(data.bones.indexOf(boneData.parent, true));
+ bones.add(new Bone(boneData, parent));
+ }
+
+ slots = new Array(data.slots.size);
+ drawOrder = new Array(data.slots.size);
+ for (SlotData slotData : data.slots) {
+ Bone bone = bones.get(data.bones.indexOf(slotData.boneData, true));
+ Slot slot = new Slot(slotData, this, bone);
+ slots.add(slot);
+ drawOrder.add(slot);
+ }
+
+ color = new Color(1, 1, 1, 1);
+ }
+
+ /** 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 parent = bones.get(skeleton.bones.indexOf(bone.parent, true));
+ bones.add(new Bone(bone, parent));
+ }
+
+ slots = new Array(skeleton.slots.size);
+ for (Slot slot : skeleton.slots) {
+ Bone bone = bones.get(skeleton.bones.indexOf(slot.bone, true));
+ Slot newSlot = new Slot(slot, this, bone);
+ slots.add(newSlot);
+ }
+
+ drawOrder = new Array(slots.size);
+ for (Slot slot : skeleton.drawOrder)
+ drawOrder.add(slots.get(skeleton.slots.indexOf(slot, true)));
+
+ skin = skeleton.skin;
+ color = new Color(skeleton.color);
+ time = skeleton.time;
+ }
+
+ /** Updates the world transform for each bone. */
+ public void updateWorldTransform () {
+ boolean flipX = this.flipX;
+ boolean flipY = this.flipY;
+ Array bones = this.bones;
+ for (int i = 0, n = bones.size; i < n; i++)
+ bones.get(i).updateWorldTransform(flipX, flipY);
+ }
+
+ /** Sets the bones and slots to their bind pose values. */
+ public void setToBindPose () {
+ setBonesToBindPose();
+ setSlotsToBindPose();
+ }
+
+ public void setBonesToBindPose () {
+ Array bones = this.bones;
+ for (int i = 0, n = bones.size; i < n; i++)
+ bones.get(i).setToBindPose();
+ }
+
+ public void setSlotsToBindPose () {
+ Array slots = this.slots;
+ for (int i = 0, n = slots.size; i < n; i++)
+ slots.get(i).setToBindPose(i);
+ }
+
+ public void draw (SpriteBatch batch) {
+ Array drawOrder = this.drawOrder;
+ for (int i = 0, n = drawOrder.size; i < n; i++) {
+ Slot slot = drawOrder.get(i);
+ Attachment attachment = slot.attachment;
+ if (attachment != null) {
+ if (!attachment.resolved) data.attachmentResolver.resolve(attachment);
+ attachment.updateOffset();
+ attachment.draw(batch, slot);
+ }
+ }
+ }
+
+ public void drawDebug (ShapeRenderer renderer) {
+ renderer.setColor(Color.RED);
+ renderer.begin(ShapeType.Line);
+ for (int i = 0, n = bones.size; i < n; i++) {
+ Bone bone = bones.get(i);
+ if (bone.parent == null) continue;
+ float x = bone.data.length * bone.m00 + bone.worldX;
+ float y = bone.data.length * bone.m10 + bone.worldY;
+ renderer.line(bone.worldX, bone.worldY, x, y);
+ }
+ renderer.end();
+
+ renderer.setColor(Color.GREEN);
+ renderer.begin(ShapeType.Filled);
+ for (int i = 0, n = bones.size; i < n; i++) {
+ Bone bone = bones.get(i);
+ renderer.setColor(Color.GREEN);
+ renderer.circle(bone.worldX, bone.worldY, 3);
+ }
+ renderer.end();
+ }
+
+ public SkeletonData getData () {
+ return data;
+ }
+
+ public Array getBones () {
+ return bones;
+ }
+
+ /** @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 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 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 getSlots () {
+ return slots;
+ }
+
+ /** @return May be null. */
+ public Slot findSlot (String slotName) {
+ if (slotName == null) throw new IllegalArgumentException("slotName cannot be null.");
+ Array 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 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 getDrawOrder () {
+ return 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 not found in the {@link SkeletonData#getDefaultSkin() default skin}. Attachments
+ * from the new skin are attached if the corresponding attachment from the old skin is currently attached.
+ * @param newSkin May be null. */
+ public void setSkin (Skin newSkin) {
+ if (skin != null && newSkin != null) newSkin.attachAll(this, skin);
+ 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 (data.defaultSkin != null) {
+ Attachment attachment = data.defaultSkin.getAttachment(slotIndex, attachmentName);
+ if (attachment != null) return attachment;
+ }
+ if (skin != null) return skin.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.");
+ if (attachmentName == null) throw new IllegalArgumentException("attachmentName cannot be null.");
+ for (int i = 0, n = slots.size; i < n; i++) {
+ Slot slot = slots.get(i);
+ if (slot.data.name.equals(slotName)) {
+ slot.setAttachment(getAttachment(i, attachmentName));
+ return;
+ }
+ }
+ throw new IllegalArgumentException("Slot not found: " + slotName);
+ }
+
+ public Color getColor () {
+ return 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 float getTime () {
+ return time;
+ }
+
+ public void setTime (float time) {
+ this.time = time;
+ }
+
+ public void update (float delta) {
+ time += delta;
+ }
+}
diff --git a/spine-libgdx/src/com/esotericsoftware/spine/SkeletonBinary.java b/spine-libgdx/src/com/esotericsoftware/spine/SkeletonBinary.java
new file mode 100644
index 000000000..5480b536b
--- /dev/null
+++ b/spine-libgdx/src/com/esotericsoftware/spine/SkeletonBinary.java
@@ -0,0 +1,270 @@
+
+package com.esotericsoftware.spine;
+
+import com.esotericsoftware.spine.Animation.AttachmentTimeline;
+import com.esotericsoftware.spine.Animation.ColorTimeline;
+import com.esotericsoftware.spine.Animation.CurveTimeline;
+import com.esotericsoftware.spine.Animation.RotateTimeline;
+import com.esotericsoftware.spine.Animation.ScaleTimeline;
+import com.esotericsoftware.spine.Animation.Timeline;
+import com.esotericsoftware.spine.Animation.TranslateTimeline;
+import com.esotericsoftware.spine.attachments.RegionAttachment;
+import com.esotericsoftware.spine.attachments.RegionSequenceAttachment;
+import com.esotericsoftware.spine.attachments.RegionSequenceAttachment.Mode;
+import com.esotericsoftware.spine.attachments.TextureAtlasAttachmentResolver;
+
+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.SerializationException;
+
+import java.io.IOException;
+
+public class SkeletonBinary {
+ static public final int TIMELINE_SCALE = 0;
+ static public final int TIMELINE_ROTATE = 1;
+ static public final int TIMELINE_TRANSLATE = 2;
+ static public final int TIMELINE_ATTACHMENT = 3;
+ static public final int TIMELINE_COLOR = 4;
+
+ static public final int ATTACHMENT_REGION = 0;
+ static public final int ATTACHMENT_REGION_SEQUENCE = 1;
+
+ static public final int CURVE_LINEAR = 0;
+ static public final int CURVE_STEPPED = 1;
+ static public final int CURVE_BEZIER = 2;
+
+ private final AttachmentResolver attachmentResolver;
+ private float scale = 1;
+
+ public SkeletonBinary (TextureAtlas atlas) {
+ attachmentResolver = new TextureAtlasAttachmentResolver(atlas);
+ }
+
+ public SkeletonBinary (AttachmentResolver attachmentResolver) {
+ this.attachmentResolver = attachmentResolver;
+ }
+
+ 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.");
+
+ SkeletonData skeletonData = new SkeletonData(attachmentResolver);
+ DataInput input = new DataInput(file.read(512));
+ try {
+ // Bones.
+ for (int i = 0, n = input.readInt(true); i < n; i++) {
+ String name = input.readString();
+ BoneData parent = null;
+ String parentName = input.readString();
+ if (parentName != null) {
+ parent = skeletonData.findBone(parentName);
+ if (parent == null) throw new SerializationException("Bone not found: " + parentName);
+ }
+ BoneData boneData = new BoneData(name, parent);
+ boneData.x = input.readFloat() * scale;
+ boneData.y = input.readFloat() * scale;
+ boneData.scaleX = input.readFloat();
+ boneData.scaleY = input.readFloat();
+ boneData.rotation = input.readFloat();
+ boneData.length = input.readFloat() * scale;
+ skeletonData.addBone(boneData);
+ }
+
+ // Slots.
+ for (int i = 0, n = input.readInt(true); i < n; i++) {
+ String slotName = input.readString();
+ String boneName = input.readString();
+ BoneData boneData = skeletonData.findBone(boneName);
+ if (boneData == null) throw new SerializationException("Bone not found: " + boneName);
+ SlotData slotData = new SlotData(slotName, boneData);
+ Color.rgba8888ToColor(slotData.getColor(), input.readInt());
+ slotData.setAttachmentName(input.readString());
+ skeletonData.addSlot(slotData);
+ }
+
+ // Default skin.
+ Skin defaultSkin = readSkin(input, "default");
+ if (defaultSkin != null) {
+ skeletonData.setDefaultSkin(defaultSkin);
+ skeletonData.addSkin(defaultSkin);
+ }
+
+ // Skins.
+ for (int i = 0, n = input.readInt(true); i < n; i++)
+ skeletonData.addSkin(readSkin(input, input.readString()));
+
+ input.close();
+ } catch (IOException ex) {
+ throw new SerializationException("Error reading skeleton file.", ex);
+ }
+
+ skeletonData.bones.shrink();
+ skeletonData.slots.shrink();
+ skeletonData.skins.shrink();
+ return skeletonData;
+ }
+
+ private Skin readSkin (DataInput input, String skinName) 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);
+ int attachmentCount = input.readInt(true);
+ for (int ii = 0; ii < attachmentCount; ii++) {
+ String name = input.readString();
+ skin.addAttachment(slotIndex, name, readAttachment(input, name));
+ }
+ }
+ return skin;
+ }
+
+ private Attachment readAttachment (DataInput input, String attachmentName) throws IOException {
+ String name = input.readString();
+ if (name == null) name = attachmentName;
+
+ Attachment attachment;
+ int type = input.readByte();
+ switch (type) {
+ case ATTACHMENT_REGION:
+ attachment = new RegionAttachment(name);
+ break;
+ case ATTACHMENT_REGION_SEQUENCE:
+ float fps = input.readFloat();
+ Mode mode = Mode.values()[input.readInt(true)];
+ attachment = new RegionSequenceAttachment(name, 1 / fps, mode);
+ break;
+ default:
+ throw new SerializationException("Unknown attachment type: " + type + " (" + name + ")");
+ }
+
+ attachment.setX(input.readFloat() * scale);
+ attachment.setY(input.readFloat() * scale);
+ attachment.setScaleX(input.readFloat());
+ attachment.setScaleY(input.readFloat());
+ attachment.setRotation(input.readFloat());
+ attachment.setWidth(input.readFloat() * scale);
+ attachment.setHeight(input.readFloat() * scale);
+ return attachment;
+ }
+
+ public Animation readAnimation (FileHandle file, SkeletonData skeleton) {
+ if (file == null) throw new IllegalArgumentException("file cannot be null.");
+ if (skeleton == null) throw new IllegalArgumentException("skeleton cannot be null.");
+
+ Array timelines = new Array();
+ float duration = 0;
+
+ DataInput input = new DataInput(file.read(512));
+ try {
+ int boneCount = input.readInt(true);
+ for (int i = 0; i < boneCount; i++) {
+ String boneName = input.readString();
+ int boneIndex = skeleton.findBoneIndex(boneName);
+ if (boneIndex == -1) throw new SerializationException("Bone not found: " + boneName);
+ int itemCount = input.readInt(true);
+ for (int ii = 0; ii < itemCount; ii++) {
+ int timelineType = input.readByte();
+ int keyCount = input.readInt(true);
+ switch (timelineType) {
+ case TIMELINE_ROTATE: {
+ RotateTimeline timeline = new RotateTimeline(keyCount);
+ timeline.setBoneIndex(boneIndex);
+ for (int keyframeIndex = 0; keyframeIndex < keyCount; keyframeIndex++) {
+ timeline.setKeyframe(keyframeIndex, input.readFloat(), input.readFloat());
+ if (keyframeIndex < keyCount - 1) readCurve(input, keyframeIndex, timeline);
+ }
+ timelines.add(timeline);
+ duration = Math.max(duration, timeline.getDuration());
+ break;
+ }
+ case TIMELINE_TRANSLATE:
+ case TIMELINE_SCALE:
+ TranslateTimeline timeline;
+ float timelineScale = 1;
+ if (timelineType == TIMELINE_SCALE)
+ timeline = new ScaleTimeline(keyCount);
+ else {
+ timeline = new TranslateTimeline(keyCount);
+ timelineScale = scale;
+ }
+ timeline.setBoneIndex(boneIndex);
+ for (int keyframeIndex = 0; keyframeIndex < keyCount; keyframeIndex++) {
+ timeline.setKeyframe(keyframeIndex, input.readFloat(), input.readFloat() * timelineScale, input.readFloat()
+ * timelineScale);
+ if (keyframeIndex < keyCount - 1) readCurve(input, keyframeIndex, timeline);
+ }
+ timelines.add(timeline);
+ duration = Math.max(duration, timeline.getDuration());
+ break;
+ default:
+ throw new RuntimeException("Invalid timeline type for a bone: " + timelineType + " (" + boneName + ")");
+ }
+ }
+ }
+
+ int slotCount = input.readInt(true);
+ for (int i = 0; i < slotCount; i++) {
+ String slotName = input.readString();
+ int slotIndex = skeleton.findSlotIndex(slotName);
+ int itemCount = input.readInt(true);
+ for (int ii = 0; ii < itemCount; ii++) {
+ int timelineType = input.readByte();
+ int keyCount = input.readInt(true);
+ switch (timelineType) {
+ case TIMELINE_COLOR: {
+ ColorTimeline timeline = new ColorTimeline(keyCount);
+ timeline.setSlotIndex(slotIndex);
+ for (int keyframeIndex = 0; keyframeIndex < keyCount; keyframeIndex++) {
+ float time = input.readFloat();
+ Color.rgba8888ToColor(Color.tmp, input.readInt());
+ timeline.setKeyframe(keyframeIndex, time, Color.tmp.r, Color.tmp.g, Color.tmp.b, Color.tmp.a);
+ if (keyframeIndex < keyCount - 1) readCurve(input, keyframeIndex, timeline);
+ }
+ timelines.add(timeline);
+ duration = Math.max(duration, timeline.getDuration());
+ break;
+ }
+ case TIMELINE_ATTACHMENT:
+ AttachmentTimeline timeline = new AttachmentTimeline(keyCount);
+ timeline.setSlotIndex(slotIndex);
+ for (int keyframeIndex = 0; keyframeIndex < keyCount; keyframeIndex++)
+ timeline.setKeyframe(keyframeIndex, input.readFloat(), input.readString());
+ timelines.add(timeline);
+ duration = Math.max(duration, timeline.getDuration());
+ break;
+ default:
+ throw new RuntimeException("Invalid timeline type for a slot: " + timelineType + " (" + slotName + ")");
+ }
+ }
+ }
+ } catch (IOException ex) {
+ throw new SerializationException("Error reading skeleton file.", ex);
+ }
+
+ timelines.shrink();
+ return new Animation(timelines, duration);
+ }
+
+ private void readCurve (DataInput input, int keyframeIndex, CurveTimeline timeline) throws IOException {
+ switch (input.readByte()) {
+ case CURVE_STEPPED:
+ timeline.setStepped(keyframeIndex);
+ break;
+ case CURVE_BEZIER:
+ timeline.setCurve(keyframeIndex, input.readFloat(), input.readFloat(), input.readFloat(), input.readFloat());
+ break;
+ }
+ }
+}
diff --git a/spine-libgdx/src/com/esotericsoftware/spine/SkeletonData.java b/spine-libgdx/src/com/esotericsoftware/spine/SkeletonData.java
new file mode 100644
index 000000000..a98d17a47
--- /dev/null
+++ b/spine-libgdx/src/com/esotericsoftware/spine/SkeletonData.java
@@ -0,0 +1,120 @@
+
+package com.esotericsoftware.spine;
+
+import com.badlogic.gdx.utils.Array;
+
+public class SkeletonData {
+ final Array bones = new Array(); // Ordered parents first.
+ final Array slots = new Array(); // Bind pose draw order.
+ final Array skins = new Array();
+ Skin defaultSkin;
+ final AttachmentResolver attachmentResolver;
+
+ public SkeletonData (AttachmentResolver attachmentResolver) {
+ if (attachmentResolver == null) throw new IllegalArgumentException("attachmentResolver cannot be null.");
+ this.attachmentResolver = attachmentResolver;
+ }
+
+ public void clear () {
+ bones.clear();
+ slots.clear();
+ skins.clear();
+ defaultSkin = null;
+ }
+
+ public AttachmentResolver getAttachmentResolver () {
+ return attachmentResolver;
+ }
+
+ // --- Bones.
+
+ public void addBone (BoneData bone) {
+ if (bone == null) throw new IllegalArgumentException("bone cannot be null.");
+ bones.add(bone);
+ }
+
+ public Array getBones () {
+ return bones;
+ }
+
+ /** @return May be null. */
+ public BoneData findBone (String boneName) {
+ if (boneName == null) throw new IllegalArgumentException("boneName cannot be null.");
+ Array 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 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 void addSlot (SlotData slot) {
+ if (slot == null) throw new IllegalArgumentException("slot cannot be null.");
+ slots.add(slot);
+ }
+
+ public Array getSlots () {
+ return slots;
+ }
+
+ /** @return May be null. */
+ public SlotData findSlot (String slotName) {
+ if (slotName == null) throw new IllegalArgumentException("slotName cannot be null.");
+ Array 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 bone was not found. */
+ public int findSlotIndex (String slotName) {
+ if (slotName == null) throw new IllegalArgumentException("slotName cannot be null.");
+ Array 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;
+ }
+
+ public void addSkin (Skin skin) {
+ if (skin == null) throw new IllegalArgumentException("skin cannot be null.");
+ skins.add(skin);
+ }
+
+ /** @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 getSkins () {
+ return skins;
+ }
+}
diff --git a/spine-libgdx/src/com/esotericsoftware/spine/SkeletonJson.java b/spine-libgdx/src/com/esotericsoftware/spine/SkeletonJson.java
new file mode 100644
index 000000000..0dcd5f04e
--- /dev/null
+++ b/spine-libgdx/src/com/esotericsoftware/spine/SkeletonJson.java
@@ -0,0 +1,277 @@
+
+package com.esotericsoftware.spine;
+
+import com.esotericsoftware.spine.Animation.AttachmentTimeline;
+import com.esotericsoftware.spine.Animation.ColorTimeline;
+import com.esotericsoftware.spine.Animation.CurveTimeline;
+import com.esotericsoftware.spine.Animation.RotateTimeline;
+import com.esotericsoftware.spine.Animation.ScaleTimeline;
+import com.esotericsoftware.spine.Animation.Timeline;
+import com.esotericsoftware.spine.Animation.TranslateTimeline;
+import com.esotericsoftware.spine.attachments.RegionSequenceAttachment;
+import com.esotericsoftware.spine.attachments.RegionSequenceAttachment.Mode;
+import com.esotericsoftware.spine.attachments.RegionAttachment;
+import com.esotericsoftware.spine.attachments.TextureAtlasAttachmentResolver;
+
+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.Json;
+import com.badlogic.gdx.utils.ObjectMap.Entry;
+import com.badlogic.gdx.utils.OrderedMap;
+import com.badlogic.gdx.utils.SerializationException;
+
+public class SkeletonJson {
+ static public final String TIMELINE_SCALE = "scale";
+ static public final String TIMELINE_ROTATE = "rotate";
+ static public final String TIMELINE_TRANSLATE = "translate";
+ static public final String TIMELINE_ATTACHMENT = "attachment";
+ static public final String TIMELINE_COLOR = "color";
+
+ static public final String ATTACHMENT_REGION = "region";
+ static public final String ATTACHMENT_REGION_SEQUENCE = "regionSequence";
+
+ private final Json json = new Json();
+ private final AttachmentResolver attachmentResolver;
+ private float scale = 1;
+
+ public SkeletonJson (TextureAtlas atlas) {
+ attachmentResolver = new TextureAtlasAttachmentResolver(atlas);
+ }
+
+ public SkeletonJson (AttachmentResolver attachmentResolver) {
+ this.attachmentResolver = attachmentResolver;
+ }
+
+ 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.");
+
+ SkeletonData skeletonData = new SkeletonData(attachmentResolver);
+
+ OrderedMap root = json.fromJson(OrderedMap.class, file);
+
+ // Bones.
+ for (OrderedMap boneMap : (Array)root.get("bones")) {
+ BoneData parent = null;
+ String parentName = (String)boneMap.get("parent");
+ if (parentName != null) {
+ parent = skeletonData.findBone(parentName);
+ if (parent == null) throw new SerializationException("Parent bone not found: " + parentName);
+ }
+ BoneData boneData = new BoneData((String)boneMap.get("name"), parent);
+ boneData.length = getFloat(boneMap, "length", 0) * scale;
+ boneData.x = getFloat(boneMap, "x", 0) * scale;
+ boneData.y = getFloat(boneMap, "y", 0) * scale;
+ boneData.rotation = getFloat(boneMap, "rotation", 0);
+ boneData.scaleX = getFloat(boneMap, "scaleX", 1);
+ boneData.scaleY = getFloat(boneMap, "scaleY", 1);
+ skeletonData.addBone(boneData);
+ }
+
+ // Slots.
+ Array slots = (Array)root.get("slots");
+ if (slots != null) {
+ for (OrderedMap slotMap : slots) {
+ String slotName = (String)slotMap.get("name");
+ String boneName = (String)slotMap.get("bone");
+ BoneData boneData = skeletonData.findBone(boneName);
+ if (boneData == null) throw new SerializationException("Slot bone not found: " + boneName);
+ SlotData slotData = new SlotData(slotName, boneData);
+
+ String color = (String)slotMap.get("color");
+ if (color != null) slotData.getColor().set(Color.valueOf(color));
+
+ slotData.setAttachmentName((String)slotMap.get("attachment"));
+
+ skeletonData.addSlot(slotData);
+ }
+ }
+
+ // Skins.
+ OrderedMap slotMap = (OrderedMap)root.get("skins");
+ if (slotMap != null) {
+ for (Entry entry : slotMap.entries()) {
+ Skin skin = new Skin(entry.key);
+ for (Entry slotEntry : ((OrderedMap)entry.value).entries()) {
+ int slotIndex = skeletonData.findSlotIndex(slotEntry.key);
+ for (Entry attachmentEntry : ((OrderedMap)slotEntry.value).entries()) {
+ Attachment attachment = readAttachment(attachmentEntry.key, attachmentEntry.value);
+ skin.addAttachment(slotIndex, attachmentEntry.key, attachment);
+ }
+ }
+ skeletonData.addSkin(skin);
+ if (skin.name.equals("default")) skeletonData.setDefaultSkin(skin);
+ }
+ }
+
+ skeletonData.bones.shrink();
+ skeletonData.slots.shrink();
+ skeletonData.skins.shrink();
+ return skeletonData;
+ }
+
+ private Attachment readAttachment (String name, OrderedMap map) {
+ name = (String)map.get("name", name);
+ Attachment attachment;
+ String type = (String)map.get("type");
+ if (type == null) type = ATTACHMENT_REGION;
+ if (type.equals(ATTACHMENT_REGION)) {
+ attachment = new RegionAttachment(name);
+
+ } else if (type.equals(ATTACHMENT_REGION_SEQUENCE)) {
+ Float fps = (Float)map.get("fps");
+ if (fps == null) throw new SerializationException("Region sequence attachment missing fps: " + name);
+
+ String modeString = (String)map.get("mode");
+ Mode mode = modeString == null ? Mode.forward : Mode.valueOf(modeString);
+
+ attachment = new RegionSequenceAttachment(name, 1 / fps, mode);
+
+ } else
+ throw new SerializationException("Unknown attachment type: " + type + " (" + name + ")");
+
+ attachment.setX(getFloat(map, "x", 0) * scale);
+ attachment.setY(getFloat(map, "y", 0) * scale);
+ attachment.setScaleX(getFloat(map, "scaleX", 1));
+ attachment.setScaleY(getFloat(map, "scaleY", 1));
+ attachment.setRotation(getFloat(map, "rotation", 0));
+ attachment.setWidth(getFloat(map, "width", 32) * scale);
+ attachment.setHeight(getFloat(map, "height", 32) * scale);
+ return attachment;
+ }
+
+ private float getFloat (OrderedMap map, String name, float defaultValue) {
+ Object value = map.get(name);
+ if (value == null) return defaultValue;
+ return (Float)value;
+ }
+
+ public Animation readAnimation (FileHandle file, SkeletonData skeletonData) {
+ if (file == null) throw new IllegalArgumentException("file cannot be null.");
+ if (skeletonData == null) throw new IllegalArgumentException("skeletonData cannot be null.");
+
+ OrderedMap map = json.fromJson(OrderedMap.class, file);
+
+ Array timelines = new Array();
+ float duration = 0;
+
+ OrderedMap bonesMap = (OrderedMap)map.get("bones");
+ for (Entry entry : bonesMap.entries()) {
+ String boneName = entry.key;
+ int boneIndex = skeletonData.findBoneIndex(boneName);
+ if (boneIndex == -1) throw new SerializationException("Bone not found: " + boneName);
+ OrderedMap, ?> propertyMap = (OrderedMap)entry.value;
+
+ for (Entry propertyEntry : propertyMap.entries()) {
+ Array values = (Array)propertyEntry.value;
+ String timelineType = (String)propertyEntry.key;
+ if (timelineType.equals(TIMELINE_ROTATE)) {
+ RotateTimeline timeline = new RotateTimeline(values.size);
+ timeline.setBoneIndex(boneIndex);
+
+ int keyframeIndex = 0;
+ for (OrderedMap valueMap : values) {
+ float time = (Float)valueMap.get("time");
+ timeline.setKeyframe(keyframeIndex, time, (Float)valueMap.get("angle"));
+ readCurve(timeline, keyframeIndex, valueMap);
+ keyframeIndex++;
+ }
+ timelines.add(timeline);
+ duration = Math.max(duration, timeline.getDuration());
+
+ } else if (timelineType.equals(TIMELINE_TRANSLATE) || timelineType.equals(TIMELINE_SCALE)) {
+ TranslateTimeline timeline;
+ float timelineScale = 1;
+ if (timelineType.equals(TIMELINE_SCALE))
+ timeline = new ScaleTimeline(values.size);
+ else {
+ timeline = new TranslateTimeline(values.size);
+ timelineScale = scale;
+ }
+ timeline.setBoneIndex(boneIndex);
+
+ int keyframeIndex = 0;
+ for (OrderedMap valueMap : values) {
+ float time = (Float)valueMap.get("time");
+ Float x = (Float)valueMap.get("x"), y = (Float)valueMap.get("y");
+ timeline.setKeyframe(keyframeIndex, time, x == null ? 0 : (x * timelineScale), y == null ? 0
+ : (y * timelineScale));
+ readCurve(timeline, keyframeIndex, valueMap);
+ keyframeIndex++;
+ }
+ timelines.add(timeline);
+ duration = Math.max(duration, timeline.getDuration());
+
+ } else
+ throw new RuntimeException("Invalid timeline type for a bone: " + timelineType + " (" + boneName + ")");
+ }
+ }
+
+ OrderedMap slotsMap = (OrderedMap)map.get("slots");
+ if (slotsMap != null) {
+ for (Entry entry : slotsMap.entries()) {
+ String slotName = entry.key;
+ int slotIndex = skeletonData.findSlotIndex(slotName);
+ OrderedMap, ?> propertyMap = (OrderedMap)entry.value;
+
+ for (Entry propertyEntry : propertyMap.entries()) {
+ Array values = (Array)propertyEntry.value;
+ String timelineType = (String)propertyEntry.key;
+ if (timelineType.equals(TIMELINE_COLOR)) {
+ ColorTimeline timeline = new ColorTimeline(values.size);
+ timeline.setSlotIndex(slotIndex);
+
+ int keyframeIndex = 0;
+ for (OrderedMap valueMap : values) {
+ float time = (Float)valueMap.get("time");
+ Color color = Color.valueOf((String)valueMap.get("color"));
+ timeline.setKeyframe(keyframeIndex, time, color.r, color.g, color.b, color.a);
+ readCurve(timeline, keyframeIndex, valueMap);
+ keyframeIndex++;
+ }
+ timelines.add(timeline);
+ duration = Math.max(duration, timeline.getDuration());
+
+ } else if (timelineType.equals(TIMELINE_ATTACHMENT)) {
+ AttachmentTimeline timeline = new AttachmentTimeline(values.size);
+ timeline.setSlotIndex(slotIndex);
+
+ int keyframeIndex = 0;
+ for (OrderedMap valueMap : values) {
+ float time = (Float)valueMap.get("time");
+ timeline.setKeyframe(keyframeIndex++, time, (String)valueMap.get("name"));
+ }
+ timelines.add(timeline);
+ duration = Math.max(duration, timeline.getDuration());
+
+ } else
+ throw new RuntimeException("Invalid timeline type for a slot: " + timelineType + " (" + slotName + ")");
+ }
+ }
+ }
+
+ timelines.shrink();
+ return new Animation(timelines, duration);
+ }
+
+ private void readCurve (CurveTimeline timeline, int keyframeIndex, OrderedMap valueMap) {
+ Object curveObject = valueMap.get("curve");
+ if (curveObject == null) return;
+ if (curveObject.equals("stepped"))
+ timeline.setStepped(keyframeIndex);
+ else if (curveObject instanceof Array) {
+ Array curve = (Array)curveObject;
+ timeline.setCurve(keyframeIndex, (Float)curve.get(0), (Float)curve.get(1), (Float)curve.get(2), (Float)curve.get(3));
+ }
+ }
+}
diff --git a/spine-libgdx/src/com/esotericsoftware/spine/Skin.java b/spine-libgdx/src/com/esotericsoftware/spine/Skin.java
new file mode 100644
index 000000000..d18b3cd6f
--- /dev/null
+++ b/spine-libgdx/src/com/esotericsoftware/spine/Skin.java
@@ -0,0 +1,97 @@
+
+package com.esotericsoftware.spine;
+
+import com.badlogic.gdx.utils.Array;
+import com.badlogic.gdx.utils.ObjectMap;
+import com.badlogic.gdx.utils.ObjectMap.Entry;
+
+/** Stores attachments by slot index and attachment name. */
+public class Skin {
+ static private final Key lookup = new Key();
+
+ final String name;
+ final ObjectMap attachments = new ObjectMap();
+
+ 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.");
+ Key key = new Key();
+ key.set(slotIndex, name);
+ attachments.put(key, attachment);
+ }
+
+ /** @return May be null. */
+ public Attachment getAttachment (int slotIndex, String name) {
+ lookup.set(slotIndex, name);
+ return attachments.get(lookup);
+ }
+
+ public void findNamesForSlot (int slotIndex, Array names) {
+ if (names == null) throw new IllegalArgumentException("names cannot be null.");
+ for (Key key : attachments.keys())
+ if (key.slotIndex == slotIndex) names.add(key.name);
+ }
+
+ public void findAttachmentsForSlot (int slotIndex, Array attachments) {
+ if (attachments == null) throw new IllegalArgumentException("attachments cannot be null.");
+ for (Entry entry : this.attachments.entries())
+ if (entry.key.slotIndex == slotIndex) attachments.add(entry.value);
+ }
+
+ public void clear () {
+ attachments.clear();
+ }
+
+ public String getName () {
+ return name;
+ }
+
+ public String toString () {
+ return name;
+ }
+
+ static class Key {
+ int slotIndex;
+ String name;
+ int hashCode;
+
+ public void set (int slotName, String name) {
+ if (name == null) throw new IllegalArgumentException("attachmentName cannot be null.");
+ this.slotIndex = slotName;
+ 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;
+ }
+ }
+
+ /** Attach all attachments from this skin if the corresponding attachment from the old skin is currently attached. */
+ void attachAll (Skeleton skeleton, Skin oldSkin) {
+ for (Entry entry : oldSkin.attachments.entries()) {
+ int slotIndex = entry.key.slotIndex;
+ Slot slot = skeleton.slots.get(slotIndex);
+ if (slot.attachment == entry.value) {
+ Attachment attachment = getAttachment(slotIndex, entry.key.name);
+ if (attachment != null) slot.setAttachment(attachment);
+ }
+ }
+ }
+}
diff --git a/spine-libgdx/src/com/esotericsoftware/spine/Slot.java b/spine-libgdx/src/com/esotericsoftware/spine/Slot.java
new file mode 100644
index 000000000..4d8b3eb73
--- /dev/null
+++ b/spine-libgdx/src/com/esotericsoftware/spine/Slot.java
@@ -0,0 +1,93 @@
+
+package com.esotericsoftware.spine;
+
+import com.badlogic.gdx.graphics.Color;
+
+public class Slot {
+ final SlotData data;
+ final Bone bone;
+ private final Skeleton skeleton;
+ final Color color;
+ Attachment attachment;
+ private float attachmentTime;
+
+ Slot () {
+ data = null;
+ bone = null;
+ skeleton = null;
+ color = new Color(1, 1, 1, 1);
+ }
+
+ public Slot (SlotData data, Skeleton skeleton, Bone bone) {
+ if (data == null) throw new IllegalArgumentException("data cannot be null.");
+ if (skeleton == null) throw new IllegalArgumentException("skeleton cannot be null.");
+ if (bone == null) throw new IllegalArgumentException("bone cannot be null.");
+ this.data = data;
+ this.skeleton = skeleton;
+ this.bone = bone;
+ color = new Color(1, 1, 1, 1);
+ }
+
+ /** Copy constructor. */
+ public Slot (Slot slot, Skeleton skeleton, Bone bone) {
+ if (slot == null) throw new IllegalArgumentException("slot cannot be null.");
+ if (skeleton == null) throw new IllegalArgumentException("skeleton cannot be null.");
+ if (bone == null) throw new IllegalArgumentException("bone cannot be null.");
+ data = slot.data;
+ this.skeleton = skeleton;
+ this.bone = bone;
+ color = new Color(slot.color);
+ attachment = slot.attachment;
+ attachmentTime = slot.attachmentTime;
+ }
+
+ public SlotData getData () {
+ return data;
+ }
+
+ public Skeleton getSkeleton () {
+ return skeleton;
+ }
+
+ public Bone getBone () {
+ return bone;
+ }
+
+ public Color getColor () {
+ return color;
+ }
+
+ /** @return May be null. */
+ public Attachment getAttachment () {
+ return attachment;
+ }
+
+ /** Sets the attachment and resets {@link #getAttachmentTime()}.
+ * @param attachment May be null. */
+ public void setAttachment (Attachment attachment) {
+ this.attachment = attachment;
+ attachmentTime = skeleton.time;
+ }
+
+ public void setAttachmentTime (float time) {
+ attachmentTime = skeleton.time - time;
+ }
+
+ /** Returns the time since the attachment was set. */
+ public float getAttachmentTime () {
+ return skeleton.time - attachmentTime;
+ }
+
+ void setToBindPose (int slotIndex) {
+ color.set(data.color);
+ setAttachment(data.attachmentName == null ? null : skeleton.getAttachment(slotIndex, data.attachmentName));
+ }
+
+ public void setToBindPose () {
+ setToBindPose(skeleton.slots.indexOf(this, true));
+ }
+
+ public String toString () {
+ return data.name;
+ }
+}
diff --git a/spine-libgdx/src/com/esotericsoftware/spine/SlotData.java b/spine-libgdx/src/com/esotericsoftware/spine/SlotData.java
new file mode 100644
index 000000000..f5c9ce203
--- /dev/null
+++ b/spine-libgdx/src/com/esotericsoftware/spine/SlotData.java
@@ -0,0 +1,49 @@
+
+package com.esotericsoftware.spine;
+
+import com.badlogic.gdx.graphics.Color;
+
+public class SlotData {
+ final String name;
+ final BoneData boneData;
+ final Color color = new Color(1, 1, 1, 1);
+ String attachmentName;
+
+ SlotData () {
+ name = null;
+ boneData = null;
+ }
+
+ public SlotData (String name, BoneData boneData) {
+ if (name == null) throw new IllegalArgumentException("name cannot be null.");
+ if (boneData == null) throw new IllegalArgumentException("boneData cannot be null.");
+ this.name = name;
+ this.boneData = boneData;
+ }
+
+ public String getName () {
+ return name;
+ }
+
+ public BoneData getBoneData () {
+ return boneData;
+ }
+
+ public Color getColor () {
+ return color;
+ }
+
+ /** @param attachmentName May be null. */
+ public void setAttachmentName (String attachmentName) {
+ this.attachmentName = attachmentName;
+ }
+
+ /** @return May be null. */
+ public String getAttachmentName () {
+ return attachmentName;
+ }
+
+ public String toString () {
+ return name;
+ }
+}
diff --git a/spine-libgdx/src/com/esotericsoftware/spine/attachments/RegionAttachment.java b/spine-libgdx/src/com/esotericsoftware/spine/attachments/RegionAttachment.java
new file mode 100644
index 000000000..a5cf355da
--- /dev/null
+++ b/spine-libgdx/src/com/esotericsoftware/spine/attachments/RegionAttachment.java
@@ -0,0 +1,153 @@
+
+package com.esotericsoftware.spine.attachments;
+
+import com.esotericsoftware.spine.Attachment;
+import com.esotericsoftware.spine.Bone;
+import com.esotericsoftware.spine.Slot;
+
+import static com.badlogic.gdx.graphics.g2d.SpriteBatch.*;
+
+import com.badlogic.gdx.graphics.Color;
+import com.badlogic.gdx.graphics.g2d.SpriteBatch;
+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 {
+ private TextureRegion region;
+ private final float[] vertices = new float[20];
+ private final float[] offset = new float[8];
+
+ 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 * height;
+ localY += region.offsetY / region.originalHeight * width;
+ 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[0] = localXCos - localYSin;
+ offset[1] = localYCos + localXSin;
+ offset[2] = localXCos - localY2Sin;
+ offset[3] = localY2Cos + localXSin;
+ offset[4] = localX2Cos - localY2Sin;
+ offset[5] = localY2Cos + localX2Sin;
+ offset[6] = localX2Cos - localYSin;
+ offset[7] = localYCos + localX2Sin;
+ }
+
+ public void setRegion (TextureRegion region) {
+ if (region == null) throw new IllegalArgumentException("region cannot be null.");
+ TextureRegion oldRegion = this.region;
+ this.region = region;
+ float[] vertices = this.vertices;
+ if (region instanceof AtlasRegion && ((AtlasRegion)region).rotate) {
+ 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();
+ } else {
+ vertices[U1] = region.getU();
+ vertices[V1] = region.getV2();
+ vertices[U2] = region.getU();
+ vertices[V2] = region.getV();
+ vertices[U3] = region.getU2();
+ vertices[V3] = region.getV();
+ vertices[U4] = region.getU2();
+ vertices[V4] = region.getV2();
+ }
+ updateOffset();
+ }
+
+ public TextureRegion getRegion () {
+ if (region == null) throw new IllegalStateException("RegionAttachment is not resolved: " + this);
+ return region;
+ }
+
+ public void draw (SpriteBatch batch, Slot slot) {
+ if (region == null) throw new IllegalStateException("RegionAttachment is not resolved: " + this);
+
+ Color skeletonColor = slot.getSkeleton().getColor();
+ Color slotColor = slot.getColor();
+ float color = NumberUtils.intToFloatColor( //
+ ((int)(255 * skeletonColor.a * slotColor.a) << 24) //
+ | ((int)(255 * skeletonColor.b * slotColor.b) << 16) //
+ | ((int)(255 * skeletonColor.g * slotColor.g) << 8) //
+ | ((int)(255 * skeletonColor.r * slotColor.r)));
+ float[] vertices = this.vertices;
+ vertices[C1] = color;
+ vertices[C2] = color;
+ vertices[C3] = color;
+ vertices[C4] = color;
+
+ updateWorldVertices(slot.getBone());
+
+ batch.draw(region.getTexture(), vertices, 0, vertices.length);
+ }
+
+ public void updateWorldVertices (Bone bone) {
+ float[] vertices = this.vertices;
+ float[] offset = this.offset;
+ float x = bone.getWorldX();
+ float y = bone.getWorldY();
+ float m00 = bone.getM00();
+ float m01 = bone.getM01();
+ float m10 = bone.getM10();
+ float m11 = bone.getM11();
+ vertices[X1] = offset[0] * m00 + offset[1] * m01 + x;
+ vertices[Y1] = offset[0] * m10 + offset[1] * m11 + y;
+ vertices[X2] = offset[2] * m00 + offset[3] * m01 + x;
+ vertices[Y2] = offset[2] * m10 + offset[3] * m11 + y;
+ vertices[X3] = offset[4] * m00 + offset[5] * m01 + x;
+ vertices[Y3] = offset[4] * m10 + offset[5] * m11 + y;
+ vertices[X4] = offset[6] * m00 + offset[7] * m01 + x;
+ vertices[Y4] = offset[6] * m10 + offset[7] * m11 + y;
+ }
+
+ public float[] getWorldVertices () {
+ return vertices;
+ }
+}
diff --git a/spine-libgdx/src/com/esotericsoftware/spine/attachments/RegionSequenceAttachment.java b/spine-libgdx/src/com/esotericsoftware/spine/attachments/RegionSequenceAttachment.java
new file mode 100644
index 000000000..80acea639
--- /dev/null
+++ b/spine-libgdx/src/com/esotericsoftware/spine/attachments/RegionSequenceAttachment.java
@@ -0,0 +1,68 @@
+
+package com.esotericsoftware.spine.attachments;
+
+import com.esotericsoftware.spine.Slot;
+
+import com.badlogic.gdx.graphics.g2d.SpriteBatch;
+import com.badlogic.gdx.graphics.g2d.TextureRegion;
+import com.badlogic.gdx.math.MathUtils;
+
+/** Attachment that displays various texture regions over time. */
+public class RegionSequenceAttachment extends RegionAttachment {
+ private final Mode mode;
+ private float frameTime;
+ private TextureRegion[] regions;
+
+ /** @param frameTime Time in seconds each frame is shown. */
+ public RegionSequenceAttachment (String name, float frameTime, Mode mode) {
+ super(name);
+ if (mode == null) throw new IllegalArgumentException("mode cannot be null.");
+
+ this.frameTime = frameTime;
+ this.mode = mode;
+ }
+
+ public void draw (SpriteBatch batch, Slot slot) {
+ if (regions == null) throw new IllegalStateException("RegionSequenceAttachment is not resolved: " + this);
+
+ int frameIndex = (int)(slot.getAttachmentTime() / frameTime);
+ switch (mode) {
+ case forward:
+ frameIndex = Math.min(regions.length - 1, frameIndex);
+ break;
+ case forwardLoop:
+ frameIndex = frameIndex % regions.length;
+ break;
+ case pingPong:
+ frameIndex = frameIndex % (regions.length * 2);
+ if (frameIndex >= regions.length) frameIndex = regions.length - 1 - (frameIndex - regions.length);
+ break;
+ case random:
+ frameIndex = MathUtils.random(regions.length - 1);
+ break;
+ case backward:
+ frameIndex = Math.max(regions.length - frameIndex - 1, 0);
+ break;
+ case backwardLoop:
+ frameIndex = frameIndex % regions.length;
+ frameIndex = regions.length - frameIndex - 1;
+ break;
+ }
+ setRegion(regions[frameIndex]);
+ super.draw(batch, slot);
+ }
+
+ /** May be null if the attachment is not resolved. */
+ public TextureRegion[] getRegions () {
+ if (regions == null) throw new IllegalStateException("RegionSequenceAttachment is not resolved: " + this);
+ return regions;
+ }
+
+ public void setRegions (TextureRegion[] regions) {
+ this.regions = regions;
+ }
+
+ static public enum Mode {
+ forward, backward, forwardLoop, backwardLoop, pingPong, random
+ }
+}
diff --git a/spine-libgdx/src/com/esotericsoftware/spine/attachments/TextureAtlasAttachmentResolver.java b/spine-libgdx/src/com/esotericsoftware/spine/attachments/TextureAtlasAttachmentResolver.java
new file mode 100644
index 000000000..ed5b70530
--- /dev/null
+++ b/spine-libgdx/src/com/esotericsoftware/spine/attachments/TextureAtlasAttachmentResolver.java
@@ -0,0 +1,29 @@
+
+package com.esotericsoftware.spine.attachments;
+
+import com.esotericsoftware.spine.Attachment;
+import com.esotericsoftware.spine.AttachmentResolver;
+
+import com.badlogic.gdx.graphics.g2d.TextureAtlas;
+import com.badlogic.gdx.graphics.g2d.TextureAtlas.AtlasRegion;
+
+public class TextureAtlasAttachmentResolver implements AttachmentResolver {
+ private TextureAtlas atlas;
+
+ public TextureAtlasAttachmentResolver (TextureAtlas atlas) {
+ if (atlas == null) throw new IllegalArgumentException("atlas cannot be null.");
+ this.atlas = atlas;
+ }
+
+ public void resolve (Attachment attachment) {
+ if (attachment instanceof RegionAttachment) {
+ AtlasRegion region = atlas.findRegion(attachment.getName());
+ if (region == null) throw new RuntimeException("Region not found in atlas: " + attachment);
+ ((RegionAttachment)attachment).setRegion(region);
+ attachment.setResolved(true);
+ return;
+ }
+
+ throw new IllegalArgumentException("Unable to resolve attachment of type: " + attachment.getClass().getName());
+ }
+}
diff --git a/spine-libgdx/test/com/esotericsoftware/spine/MixTest.java b/spine-libgdx/test/com/esotericsoftware/spine/MixTest.java
new file mode 100644
index 000000000..cb78495b0
--- /dev/null
+++ b/spine-libgdx/test/com/esotericsoftware/spine/MixTest.java
@@ -0,0 +1,124 @@
+
+package com.esotericsoftware.spine;
+
+import com.badlogic.gdx.ApplicationAdapter;
+import com.badlogic.gdx.Gdx;
+import com.badlogic.gdx.Input.Keys;
+import com.badlogic.gdx.InputAdapter;
+import com.badlogic.gdx.backends.lwjgl.LwjglApplication;
+import com.badlogic.gdx.backends.lwjgl.LwjglApplicationConfiguration;
+import com.badlogic.gdx.graphics.Color;
+import com.badlogic.gdx.graphics.GL10;
+import com.badlogic.gdx.graphics.g2d.SpriteBatch;
+import com.badlogic.gdx.graphics.g2d.TextureAtlas;
+import com.badlogic.gdx.graphics.glutils.ShapeRenderer;
+
+public class MixTest extends ApplicationAdapter {
+ SpriteBatch batch;
+ float time;
+ ShapeRenderer renderer;
+
+ SkeletonData skeletonData;
+ Skeleton skeleton;
+ Animation walkAnimation;
+ Animation jumpAnimation;
+
+ public void create () {
+ batch = new SpriteBatch();
+ renderer = new ShapeRenderer();
+
+ final String name = "spineboy";
+
+ TextureAtlas atlas = new TextureAtlas(Gdx.files.internal(name + ".atlas"));
+
+ if (true) {
+ SkeletonJson json = new SkeletonJson(atlas);
+ // json.setScale(2);
+ skeletonData = json.readSkeletonData(Gdx.files.internal(name + "-skeleton.json"));
+ walkAnimation = json.readAnimation(Gdx.files.internal(name + "-walk.json"), skeletonData);
+ jumpAnimation = json.readAnimation(Gdx.files.internal(name + "-jump.json"), skeletonData);
+ } else {
+ SkeletonBinary binary = new SkeletonBinary(atlas);
+ // binary.setScale(2);
+ skeletonData = binary.readSkeletonData(Gdx.files.internal(name + ".skel"));
+ walkAnimation = binary.readAnimation(Gdx.files.internal(name + "-walk.anim"), skeletonData);
+ jumpAnimation = binary.readAnimation(Gdx.files.internal(name + "-jump.anim"), skeletonData);
+ }
+
+ skeleton = new Skeleton(skeletonData);
+ skeleton.setToBindPose();
+
+ final Bone root = skeleton.getRootBone();
+ root.x = -50;
+ root.y = 20;
+ root.scaleX = 1f;
+ root.scaleY = 1f;
+ skeleton.updateWorldTransform();
+ }
+
+ 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.4f;
+ float blendOut = 0.4f;
+ float blendOutStart = beforeJump + jump - blendOut;
+ float total = 3.75f;
+
+ time += delta;
+
+ Bone root = skeleton.getRootBone();
+ float speed = 180;
+ if (time > beforeJump + blendIn && time < blendOutStart) speed = 360;
+ root.setX(root.getX() + speed * delta);
+
+ Gdx.gl.glClear(GL10.GL_COLOR_BUFFER_BIT);
+ batch.begin();
+ batch.setColor(Color.GRAY);
+
+ if (time > total) {
+ // restart
+ time = 0;
+ root.setX(-50);
+ } else if (time > beforeJump + jump) {
+ // just walk after jump
+ walkAnimation.apply(skeleton, time, true);
+ } else if (time > blendOutStart) {
+ // blend out jump
+ walkAnimation.apply(skeleton, time, true);
+ jumpAnimation.mix(skeleton, time - beforeJump, false, 1 - (time - blendOutStart) / blendOut);
+ } else if (time > beforeJump + blendIn) {
+ // just jump
+ jumpAnimation.apply(skeleton, time - beforeJump, false);
+ } else if (time > beforeJump) {
+ // blend in jump
+ walkAnimation.apply(skeleton, time, true);
+ jumpAnimation.mix(skeleton, time - beforeJump, false, (time - beforeJump) / blendIn);
+ } else {
+ // just walk before jump
+ walkAnimation.apply(skeleton, time, true);
+ }
+
+ skeleton.updateWorldTransform();
+ skeleton.update(Gdx.graphics.getDeltaTime());
+ skeleton.draw(batch);
+
+ batch.end();
+
+ // skeleton.drawDebug(renderer);
+ }
+
+ public void resize (int width, int height) {
+ batch.getProjectionMatrix().setToOrtho2D(0, 0, width, height);
+ renderer.setProjectionMatrix(batch.getProjectionMatrix());
+ }
+
+ public static void main (String[] args) throws Exception {
+ LwjglApplicationConfiguration config = new LwjglApplicationConfiguration();
+ config.title = "Mix Test";
+ config.width = 640;
+ config.height = 480;
+ new LwjglApplication(new MixTest(), config);
+ }
+}
diff --git a/spine-libgdx/test/com/esotericsoftware/spine/SkeletonTest.java b/spine-libgdx/test/com/esotericsoftware/spine/SkeletonTest.java
new file mode 100644
index 000000000..7367dabba
--- /dev/null
+++ b/spine-libgdx/test/com/esotericsoftware/spine/SkeletonTest.java
@@ -0,0 +1,123 @@
+
+package com.esotericsoftware.spine;
+
+import com.badlogic.gdx.ApplicationAdapter;
+import com.badlogic.gdx.Gdx;
+import com.badlogic.gdx.Input.Keys;
+import com.badlogic.gdx.InputAdapter;
+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.GL10;
+import com.badlogic.gdx.graphics.Pixmap;
+import com.badlogic.gdx.graphics.Pixmap.Format;
+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.g2d.TextureAtlas.AtlasRegion;
+import com.badlogic.gdx.graphics.g2d.TextureAtlas.TextureAtlasData;
+import com.badlogic.gdx.graphics.glutils.ShapeRenderer;
+
+public class SkeletonTest extends ApplicationAdapter {
+ SpriteBatch batch;
+ float time;
+ ShapeRenderer renderer;
+
+ SkeletonData skeletonData;
+ Skeleton skeleton;
+ Animation animation;
+
+ public void create () {
+ batch = new SpriteBatch();
+ renderer = new ShapeRenderer();
+
+ final String name = "goblins";
+
+ // 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(Color.WHITE);
+ pixmap.fill();
+ final AtlasRegion fake = new AtlasRegion(new Texture(pixmap), 0, 0, 32, 32);
+ pixmap.dispose();
+ FileHandle atlasFile = Gdx.files.internal(name + ".atlas");
+ 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);
+ return region != null ? region : fake;
+ }
+ };
+
+ if (true) {
+ SkeletonJson json = new SkeletonJson(atlas);
+ // json.setScale(2);
+ skeletonData = json.readSkeletonData(Gdx.files.internal(name + "-skeleton.json"));
+ animation = json.readAnimation(Gdx.files.internal(name + "-walk.json"), skeletonData);
+ } else {
+ SkeletonBinary binary = new SkeletonBinary(atlas);
+ // binary.setScale(2);
+ skeletonData = binary.readSkeletonData(Gdx.files.internal(name + ".skel"));
+ animation = binary.readAnimation(Gdx.files.internal(name + "-walk.anim"), skeletonData);
+ }
+
+ skeleton = new Skeleton(skeletonData);
+ if (name.equals("goblins")) skeleton.setSkin("goblin");
+ skeleton.setToBindPose();
+
+ Bone root = skeleton.getRootBone();
+ root.x = 50;
+ root.y = 20;
+ root.scaleX = 1f;
+ root.scaleY = 1f;
+ skeleton.updateWorldTransform();
+
+ Gdx.input.setInputProcessor(new InputAdapter() {
+ public boolean keyDown (int keycode) {
+ if (keycode == Keys.SPACE) {
+ if (name.equals("goblins")) {
+ skeleton.setSkin(skeleton.getSkin().getName().equals("goblin") ? "goblingirl" : "goblin");
+ skeleton.setSlotsToBindPose();
+ }
+ }
+ return true;
+ }
+ });
+ }
+
+ public void render () {
+ time += Gdx.graphics.getDeltaTime();
+
+ Bone root = skeleton.getRootBone();
+ float x = root.getX() + 160 * Gdx.graphics.getDeltaTime() * (skeleton.getFlipX() ? -1 : 1);
+ if (x > Gdx.graphics.getWidth()) skeleton.setFlipX(true);
+ if (x < 0) skeleton.setFlipX(false);
+ root.setX(x);
+
+ Gdx.gl.glClear(GL10.GL_COLOR_BUFFER_BIT);
+ batch.begin();
+ batch.setColor(Color.GRAY);
+
+ animation.apply(skeleton, time, true);
+ skeleton.updateWorldTransform();
+ skeleton.update(Gdx.graphics.getDeltaTime());
+ skeleton.draw(batch);
+
+ batch.end();
+
+ skeleton.drawDebug(renderer);
+ }
+
+ public void resize (int width, int height) {
+ batch.getProjectionMatrix().setToOrtho2D(0, 0, width, height);
+ renderer.setProjectionMatrix(batch.getProjectionMatrix());
+ }
+
+ public static void main (String[] args) throws Exception {
+ LwjglApplicationConfiguration config = new LwjglApplicationConfiguration();
+ config.title = "Skeleton Test";
+ config.width = 640;
+ config.height = 480;
+ new LwjglApplication(new SkeletonTest(), config);
+ }
+}
diff --git a/spine-libgdx/test/goblins-skeleton.json b/spine-libgdx/test/goblins-skeleton.json
new file mode 100644
index 000000000..be335a530
--- /dev/null
+++ b/spine-libgdx/test/goblins-skeleton.json
@@ -0,0 +1,202 @@
+{
+"bones": [
+ { "name": "root", "length": 0 },
+ { "name": "hip", "parent": "root", "length": 0, "x": 0.64, "y": 114.41 },
+ { "name": "left upper leg", "parent": "hip", "length": 50.39, "x": 14.45, "y": 2.81, "rotation": -89.09 },
+ { "name": "left lower leg", "parent": "left upper leg", "length": 49.89, "x": 56.34, "y": 0.98, "rotation": -16.65 },
+ { "name": "left foot", "parent": "left lower leg", "length": 46.5, "x": 58.94, "y": -7.61, "rotation": 102.43 },
+ { "name": "right upper leg", "parent": "hip", "length": 42.45, "x": -20.07, "y": -6.83, "rotation": -97.49 },
+ { "name": "right lower leg", "parent": "right upper leg", "length": 58.52, "x": 42.99, "y": -0.61, "rotation": -14.34 },
+ { "name": "right foot", "parent": "right lower leg", "length": 45.45, "x": 64.88, "y": 0.04, "rotation": 110.3 },
+ { "name": "torso", "parent": "hip", "length": 85.82, "x": -6.42, "y": 1.97, "rotation": 93.92 },
+ { "name": "neck", "parent": "torso", "length": 18.38, "x": 81.67, "y": -6.34, "rotation": -1.51 },
+ { "name": "head", "parent": "neck", "length": 68.28, "x": 20.93, "y": 11.59, "rotation": -13.92 },
+ { "name": "right shoulder", "parent": "torso", "length": 37.24, "x": 76.02, "y": 18.14, "rotation": 133.88 },
+ { "name": "right arm", "parent": "right shoulder", "length": 36.74, "x": 37.6, "y": 0.31, "rotation": 36.32 },
+ { "name": "right hand", "parent": "right arm", "length": 15.32, "x": 36.9, "y": 0.34, "rotation": 2.35 },
+ { "name": "left shoulder", "parent": "torso", "length": 35.43, "x": 74.04, "y": -20.38, "rotation": -156.96 },
+ { "name": "left arm", "parent": "left shoulder", "length": 35.62, "x": 37.85, "y": -2.34, "rotation": 28.16 },
+ { "name": "left hand", "parent": "left arm", "length": 11.52, "x": 35.62, "y": 0.07, "rotation": 2.7 },
+ { "name": "pelvis", "parent": "hip", "length": 0, "x": 1.41, "y": -6.57 }
+],
+"slots": [
+ { "name": "left shoulder", "bone": "left shoulder", "attachment": "left shoulder" },
+ { "name": "left arm", "bone": "left arm", "attachment": "left arm" },
+ { "name": "left hand item", "bone": "left hand", "attachment": "left hand item" },
+ { "name": "left hand", "bone": "left hand", "attachment": "left hand" },
+ { "name": "left foot", "bone": "left foot", "attachment": "left foot" },
+ { "name": "left lower leg", "bone": "left lower leg", "attachment": "left lower leg" },
+ { "name": "left upper leg", "bone": "left upper leg", "attachment": "left upper leg" },
+ { "name": "neck", "bone": "neck", "attachment": "neck" },
+ { "name": "torso", "bone": "torso", "attachment": "torso" },
+ { "name": "pelvis", "bone": "pelvis", "attachment": "pelvis" },
+ { "name": "right foot", "bone": "right foot", "attachment": "right foot" },
+ { "name": "right lower leg", "bone": "right lower leg", "attachment": "right lower leg" },
+ { "name": "undie straps", "bone": "pelvis", "attachment": "undie straps" },
+ { "name": "undies", "bone": "pelvis", "attachment": "undies" },
+ { "name": "right upper leg", "bone": "right upper leg", "attachment": "right upper leg" },
+ { "name": "head", "bone": "head", "attachment": "head" },
+ { "name": "eyes", "bone": "head", "attachment": "eyes open" },
+ { "name": "right shoulder", "bone": "right shoulder", "attachment": "right shoulder" },
+ { "name": "right arm", "bone": "right arm", "attachment": "right arm" },
+ { "name": "right hand item 2", "bone": "right hand", "attachment": "right hand item 2" },
+ { "name": "right hand", "bone": "right hand", "attachment": "right hand" },
+ { "name": "right hand item", "bone": "right hand", "attachment": "right hand item" }
+],
+"skins": {
+ "goblin": {
+ "neck": {
+ "neck": { "name": "goblin/neck", "x": 10.1, "y": 0.42, "rotation": -93.69, "width": 36, "height": 41 }
+ },
+ "undies": {
+ "undies": { "name": "goblin/undies", "x": 6.3, "y": 0.12, "rotation": 0.91, "width": 36, "height": 29 }
+ },
+ "right hand": {
+ "right hand": { "name": "goblin/right-hand", "x": 7.88, "y": 2.78, "rotation": 91.96, "width": 36, "height": 37 }
+ },
+ "right arm": {
+ "right arm": { "name": "goblin/right-arm", "x": 16.44, "y": -1.04, "rotation": 94.32, "width": 23, "height": 50 }
+ },
+ "head": {
+ "head": { "name": "goblin/head", "x": 25.73, "y": 2.33, "rotation": -92.29, "width": 103, "height": 66 }
+ },
+ "left shoulder": {
+ "left shoulder": { "name": "goblin/left-shoulder", "x": 15.56, "y": -2.26, "rotation": 62.01, "width": 29, "height": 44 }
+ },
+ "left arm": {
+ "left arm": {
+ "name": "goblin/left-arm",
+ "x": 16.7,
+ "y": -1.69,
+ "scaleX": 1.057,
+ "scaleY": 1.057,
+ "rotation": 33.84,
+ "width": 37,
+ "height": 35
+ }
+ },
+ "left hand": {
+ "left hand": {
+ "name": "goblin/left-hand",
+ "x": 3.47,
+ "y": 3.41,
+ "scaleX": 0.892,
+ "scaleY": 0.892,
+ "rotation": 31.14,
+ "width": 36,
+ "height": 41
+ }
+ },
+ "right lower leg": {
+ "right lower leg": { "name": "goblin/right-lower-leg", "x": 25.68, "y": -3.15, "rotation": 111.83, "width": 36, "height": 76 }
+ },
+ "right upper leg": {
+ "right upper leg": { "name": "goblin/right-upper-leg", "x": 20.35, "y": 1.47, "rotation": 97.49, "width": 34, "height": 63 }
+ },
+ "pelvis": {
+ "pelvis": { "name": "goblin/pelvis", "x": -5.61, "y": 0.76, "width": 62, "height": 43 }
+ },
+ "left lower leg": {
+ "left lower leg": { "name": "goblin/left-lower-leg", "x": 23.58, "y": -2.06, "rotation": 105.75, "width": 33, "height": 70 }
+ },
+ "left upper leg": {
+ "left upper leg": { "name": "goblin/left-upper-leg", "x": 29.68, "y": -3.87, "rotation": 89.09, "width": 33, "height": 73 }
+ },
+ "torso": {
+ "torso": { "name": "goblin/torso", "x": 38.09, "y": -3.87, "rotation": -94.95, "width": 68, "height": 96 }
+ },
+ "right shoulder": {
+ "right shoulder": { "name": "goblin/right-shoulder", "x": 15.68, "y": -1.03, "rotation": 130.65, "width": 39, "height": 45 }
+ },
+ "right foot": {
+ "right foot": { "name": "goblin/right-foot", "x": 23.56, "y": 9.8, "rotation": 1.52, "width": 63, "height": 33 }
+ },
+ "left foot": {
+ "left foot": { "name": "goblin/left-foot", "x": 24.85, "y": 8.74, "rotation": 3.32, "width": 65, "height": 31 }
+ },
+ "right hand item": {
+ "right hand item": { "name": "goblin/shield", "x": -0.47, "y": 1.1, "rotation": 91.16, "width": 70, "height": 72 }
+ },
+ "left hand item": {
+ "left hand item": { "name": "goblin/spear", "x": -4.55, "y": 39.2, "rotation": 13.04, "width": 22, "height": 368 }
+ },
+ "undie straps": {
+ "undie straps": { "name": "goblin/undie-straps", "x": -3.87, "y": 13.1, "scaleX": 1.089, "width": 55, "height": 19 }
+ }
+ },
+ "goblingirl": {
+ "left upper leg": {
+ "left upper leg": { "name": "goblingirl/left-upper-leg", "x": 30.21, "y": -2.95, "rotation": 89.09, "width": 33, "height": 70 }
+ },
+ "left lower leg": {
+ "left lower leg": { "name": "goblingirl/left-lower-leg", "x": 25.02, "y": -0.6, "rotation": 105.75, "width": 33, "height": 70 }
+ },
+ "left foot": {
+ "left foot": { "name": "goblingirl/left-foot", "x": 25.17, "y": 7.92, "rotation": 3.32, "width": 65, "height": 31 }
+ },
+ "right upper leg": {
+ "right upper leg": { "name": "goblingirl/right-upper-leg", "x": 19.69, "y": 2.13, "rotation": 97.49, "width": 34, "height": 63 }
+ },
+ "right lower leg": {
+ "right lower leg": { "name": "goblingirl/right-lower-leg", "x": 26.15, "y": -3.27, "rotation": 111.83, "width": 36, "height": 76 }
+ },
+ "right foot": {
+ "right foot": { "name": "goblingirl/right-foot", "x": 23.46, "y": 9.66, "rotation": 1.52, "width": 63, "height": 33 }
+ },
+ "torso": {
+ "torso": { "name": "goblingirl/torso", "x": 36.28, "y": -5.14, "rotation": -95.74, "width": 68, "height": 96 }
+ },
+ "left shoulder": {
+ "left shoulder": { "name": "goblingirl/left-shoulder", "x": 19.8, "y": -0.42, "rotation": 61.21, "width": 28, "height": 46 }
+ },
+ "left arm": {
+ "left arm": { "name": "goblingirl/left-arm", "x": 19.64, "y": -2.42, "rotation": 33.05, "width": 37, "height": 35 }
+ },
+ "left hand": {
+ "left hand": {
+ "name": "goblingirl/left-hand",
+ "x": 4.34,
+ "y": 2.39,
+ "scaleX": 0.896,
+ "scaleY": 0.896,
+ "rotation": 30.34,
+ "width": 35,
+ "height": 40
+ }
+ },
+ "neck": {
+ "neck": { "name": "goblingirl/neck", "x": 6.16, "y": -3.14, "rotation": -98.86, "width": 35, "height": 41 }
+ },
+ "head": {
+ "head": { "name": "goblingirl/head", "x": 27.71, "y": -4.32, "rotation": -85.58, "width": 103, "height": 81 }
+ },
+ "right shoulder": {
+ "right shoulder": { "name": "goblingirl/right-shoulder", "x": 14.46, "y": 0.45, "rotation": 129.85, "width": 39, "height": 45 }
+ },
+ "right arm": {
+ "right arm": { "name": "goblingirl/right-arm", "x": 16.85, "y": -0.66, "rotation": 93.52, "width": 28, "height": 50 }
+ },
+ "right hand": {
+ "right hand": { "name": "goblingirl/right-hand", "x": 7.21, "y": 3.43, "rotation": 91.16, "width": 36, "height": 37 }
+ },
+ "pelvis": {
+ "pelvis": { "name": "goblingirl/pelvis", "x": -3.87, "y": 3.18, "width": 62, "height": 43 }
+ },
+ "undie straps": {
+ "undie straps": { "name": "goblingirl/undie-straps", "x": -1.51, "y": 14.18, "width": 55, "height": 19 }
+ },
+ "undies": {
+ "undies": { "name": "goblingirl/undies", "x": 5.4, "y": 1.7, "width": 36, "height": 29 }
+ },
+ "left hand item": {
+ "left hand item": { "name": "goblingirl/dagger", "x": 7.88, "y": -23.45, "rotation": 10.47, "width": 26, "height": 108 }
+ },
+ "right hand item 2": {
+ "right hand item 2": { "name": "goblingirl/dagger", "x": 7.17, "y": -22.38, "rotation": -5.27, "width": 26, "height": 108 }
+ },
+ "right hand item": {
+ "right hand item": { "name": "goblingirl/dagger-tip", "x": 13.45, "y": 22.07, "rotation": -3.23, "width": 17, "height": 17 }
+ }
+ }
+}
+}
\ No newline at end of file
diff --git a/spine-libgdx/test/goblins-walk.anim b/spine-libgdx/test/goblins-walk.anim
new file mode 100644
index 000000000..f77b26afd
Binary files /dev/null and b/spine-libgdx/test/goblins-walk.anim differ
diff --git a/spine-libgdx/test/goblins-walk.json b/spine-libgdx/test/goblins-walk.json
new file mode 100644
index 000000000..5dc23cf44
--- /dev/null
+++ b/spine-libgdx/test/goblins-walk.json
@@ -0,0 +1,288 @@
+{
+"bones": {
+ "left upper leg": {
+ "rotate": [
+ { "time": 0, "angle": -26.55 },
+ { "time": 0.1333, "angle": -8.78 },
+ { "time": 0.2666, "angle": 9.51 },
+ { "time": 0.4, "angle": 30.74 },
+ { "time": 0.5333, "angle": 25.33 },
+ { "time": 0.6666, "angle": 26.11 },
+ { "time": 0.8, "angle": -7.7 },
+ { "time": 0.9333, "angle": -21.19 },
+ { "time": 1.0666, "angle": -26.55 }
+ ],
+ "translate": [
+ { "time": 0, "x": -1.32, "y": 1.7 },
+ { "time": 0.4, "x": -0.06, "y": 2.42 },
+ { "time": 1.0666, "x": -1.32, "y": 1.7 }
+ ]
+ },
+ "right upper leg": {
+ "rotate": [
+ { "time": 0, "angle": 42.45 },
+ { "time": 0.1333, "angle": 52.1 },
+ { "time": 0.2666, "angle": 8.53 },
+ { "time": 0.5333, "angle": -16.93 },
+ { "time": 0.6666, "angle": 1.89 },
+ {
+ "time": 0.8,
+ "angle": 28.06,
+ "curve": [ 0.462, 0.11, 1, 1 ]
+ },
+ {
+ "time": 0.9333,
+ "angle": 58.68,
+ "curve": [ 0.5, 0.02, 1, 1 ]
+ },
+ { "time": 1.0666, "angle": 42.45 }
+ ],
+ "translate": [
+ { "time": 0, "x": 6.23, "y": 0 },
+ { "time": 0.2666, "x": 2.14, "y": 2.4 },
+ { "time": 0.5333, "x": 2.44, "y": 4.8 },
+ { "time": 1.0666, "x": 6.23, "y": 0 }
+ ]
+ },
+ "left lower leg": {
+ "rotate": [
+ { "time": 0, "angle": -22.98 },
+ { "time": 0.1333, "angle": -63.5 },
+ { "time": 0.2666, "angle": -73.76 },
+ { "time": 0.5333, "angle": 5.11 },
+ { "time": 0.6666, "angle": -28.29 },
+ { "time": 0.8, "angle": 4.08 },
+ { "time": 0.9333, "angle": 3.53 },
+ { "time": 1.0666, "angle": -22.98 }
+ ],
+ "translate": [
+ { "time": 0, "x": 0, "y": 0 },
+ { "time": 0.2666, "x": 2.55, "y": -0.47 },
+ { "time": 0.5333, "x": 0, "y": 0, "curve": "stepped" },
+ { "time": 1.0666, "x": 0, "y": 0 }
+ ]
+ },
+ "left foot": {
+ "rotate": [
+ { "time": 0, "angle": -3.69 },
+ { "time": 0.1333, "angle": -10.42 },
+ { "time": 0.2666, "angle": -5.01 },
+ { "time": 0.4, "angle": 3.87 },
+ { "time": 0.5333, "angle": -3.87 },
+ { "time": 0.6666, "angle": 2.78 },
+ { "time": 0.8, "angle": 1.68 },
+ { "time": 0.9333, "angle": -8.54 },
+ { "time": 1.0666, "angle": -3.69 }
+ ]
+ },
+ "right shoulder": {
+ "rotate": [
+ {
+ "time": 0,
+ "angle": 5.29,
+ "curve": [ 0.264, 0, 0.75, 1 ]
+ },
+ { "time": 0.6666, "angle": 6.65 },
+ { "time": 1.0666, "angle": 5.29 }
+ ]
+ },
+ "right arm": {
+ "rotate": [
+ {
+ "time": 0,
+ "angle": -4.02,
+ "curve": [ 0.267, 0, 0.804, 0.99 ]
+ },
+ {
+ "time": 0.6666,
+ "angle": 19.78,
+ "curve": [ 0.307, 0, 0.787, 0.99 ]
+ },
+ { "time": 1.0666, "angle": -4.02 }
+ ]
+ },
+ "right hand": {
+ "rotate": [
+ { "time": 0, "angle": 8.98 },
+ { "time": 0.6666, "angle": 0.51 },
+ { "time": 1.0666, "angle": 8.98 }
+ ]
+ },
+ "left shoulder": {
+ "rotate": [
+ {
+ "time": 0,
+ "angle": 6.25,
+ "curve": [ 0.339, 0, 0.683, 1 ]
+ },
+ {
+ "time": 0.5333,
+ "angle": -11.78,
+ "curve": [ 0.281, 0, 0.686, 0.99 ]
+ },
+ { "time": 1.0666, "angle": 6.25 }
+ ],
+ "translate": [
+ { "time": 0, "x": 1.15, "y": 0.23 }
+ ]
+ },
+ "left hand": {
+ "rotate": [
+ {
+ "time": 0,
+ "angle": -21.23,
+ "curve": [ 0.295, 0, 0.755, 0.98 ]
+ },
+ {
+ "time": 0.5333,
+ "angle": -27.28,
+ "curve": [ 0.241, 0, 0.75, 0.97 ]
+ },
+ { "time": 1.0666, "angle": -21.23 }
+ ]
+ },
+ "left arm": {
+ "rotate": [
+ {
+ "time": 0,
+ "angle": 28.37,
+ "curve": [ 0.339, 0, 0.683, 1 ]
+ },
+ {
+ "time": 0.5333,
+ "angle": 60.09,
+ "curve": [ 0.281, 0, 0.686, 0.99 ]
+ },
+ { "time": 1.0666, "angle": 28.37 }
+ ]
+ },
+ "torso": {
+ "rotate": [
+ { "time": 0, "angle": -10.28 },
+ {
+ "time": 0.1333,
+ "angle": -15.38,
+ "curve": [ 0.545, 0, 0.818, 1 ]
+ },
+ {
+ "time": 0.4,
+ "angle": -9.78,
+ "curve": [ 0.58, 0.17, 0.669, 0.99 ]
+ },
+ {
+ "time": 0.6666,
+ "angle": -15.75,
+ "curve": [ 0.235, 0.01, 0.795, 1 ]
+ },
+ {
+ "time": 0.9333,
+ "angle": -7.06,
+ "curve": [ 0.209, 0, 0.816, 0.98 ]
+ },
+ { "time": 1.0666, "angle": -10.28 }
+ ],
+ "translate": [
+ { "time": 0, "x": -1.29, "y": 1.68 }
+ ]
+ },
+ "right foot": {
+ "rotate": [
+ { "time": 0, "angle": -5.25 },
+ { "time": 0.2666, "angle": -1.91 },
+ { "time": 0.4, "angle": -6.45 },
+ { "time": 0.5333, "angle": -5.39 },
+ { "time": 0.8, "angle": -11.68 },
+ { "time": 0.9333, "angle": 0.46 },
+ { "time": 1.0666, "angle": -5.25 }
+ ]
+ },
+ "right lower leg": {
+ "rotate": [
+ {
+ "time": 0,
+ "angle": -3.39,
+ "curve": [ 0.316, 0.01, 0.741, 0.98 ]
+ },
+ {
+ "time": 0.1333,
+ "angle": -45.53,
+ "curve": [ 0.229, 0, 0.738, 0.97 ]
+ },
+ { "time": 0.2666, "angle": -4.83 },
+ { "time": 0.5333, "angle": -19.53 },
+ { "time": 0.6666, "angle": -64.8 },
+ {
+ "time": 0.8,
+ "angle": -82.56,
+ "curve": [ 0.557, 0.18, 1, 1 ]
+ },
+ { "time": 1.0666, "angle": -3.39 }
+ ],
+ "translate": [
+ { "time": 0, "x": 0, "y": 0, "curve": "stepped" },
+ { "time": 0.5333, "x": 0, "y": 0 },
+ { "time": 0.6666, "x": 2.18, "y": 0.21 },
+ { "time": 1.0666, "x": 0, "y": 0 }
+ ]
+ },
+ "hip": {
+ "rotate": [
+ { "time": 0, "angle": 0, "curve": "stepped" },
+ { "time": 1.0666, "angle": 0 }
+ ],
+ "translate": [
+ { "time": 0, "x": 0, "y": -4.16 },
+ {
+ "time": 0.1333,
+ "x": 0,
+ "y": -7.05,
+ "curve": [ 0.359, 0.47, 0.646, 0.74 ]
+ },
+ { "time": 0.4, "x": 0, "y": 6.78 },
+ { "time": 0.5333, "x": 0, "y": -6.13 },
+ {
+ "time": 0.6666,
+ "x": 0,
+ "y": -7.05,
+ "curve": [ 0.359, 0.47, 0.646, 0.74 ]
+ },
+ { "time": 0.9333, "x": 0, "y": 6.78 },
+ { "time": 1.0666, "x": 0, "y": -4.16 }
+ ]
+ },
+ "neck": {
+ "rotate": [
+ { "time": 0, "angle": 3.6 },
+ { "time": 0.1333, "angle": 17.49 },
+ { "time": 0.2666, "angle": 6.1 },
+ { "time": 0.4, "angle": 3.45 },
+ { "time": 0.5333, "angle": 5.17 },
+ { "time": 0.6666, "angle": 18.36 },
+ { "time": 0.8, "angle": 6.09 },
+ { "time": 0.9333, "angle": 2.28 },
+ { "time": 1.0666, "angle": 3.6 }
+ ]
+ },
+ "head": {
+ "rotate": [
+ {
+ "time": 0,
+ "angle": 3.6,
+ "curve": [ 0, 0, 0.704, 1.17 ]
+ },
+ { "time": 0.1333, "angle": -0.2 },
+ { "time": 0.2666, "angle": 6.1 },
+ { "time": 0.4, "angle": 3.45 },
+ {
+ "time": 0.5333,
+ "angle": 5.17,
+ "curve": [ 0, 0, 0.704, 1.61 ]
+ },
+ { "time": 0.7, "angle": 1.1 },
+ { "time": 0.8, "angle": 6.09 },
+ { "time": 0.9333, "angle": 2.28 },
+ { "time": 1.0666, "angle": 3.6 }
+ ]
+ }
+}
+}
\ No newline at end of file
diff --git a/spine-libgdx/test/goblins.atlas b/spine-libgdx/test/goblins.atlas
new file mode 100644
index 000000000..4638120bf
--- /dev/null
+++ b/spine-libgdx/test/goblins.atlas
@@ -0,0 +1,297 @@
+
+goblins.png
+format: RGBA8888
+filter: Nearest,Nearest
+repeat: none
+goblin/spear
+ rotate: false
+ xy: 1, 123
+ size: 22, 368
+ orig: 22, 368
+ offset: 0, 0
+ index: -1
+goblin/head
+ rotate: false
+ xy: 25, 425
+ size: 103, 66
+ orig: 103, 66
+ offset: 0, 0
+ index: -1
+goblin/torso
+ rotate: false
+ xy: 25, 327
+ size: 68, 96
+ orig: 68, 96
+ offset: 0, 0
+ index: -1
+goblin/shield
+ rotate: false
+ xy: 25, 253
+ size: 70, 72
+ orig: 70, 72
+ offset: 0, 0
+ index: -1
+goblin/right-lower-leg
+ rotate: false
+ xy: 25, 175
+ size: 36, 76
+ orig: 36, 76
+ offset: 0, 0
+ index: -1
+goblin/left-upper-leg
+ rotate: false
+ xy: 95, 350
+ size: 33, 73
+ orig: 33, 73
+ offset: 0, 0
+ index: -1
+goblin/pelvis
+ rotate: false
+ xy: 130, 448
+ size: 62, 43
+ orig: 62, 43
+ offset: 0, 0
+ index: -1
+goblin/left-lower-leg
+ rotate: false
+ xy: 130, 376
+ size: 33, 70
+ orig: 33, 70
+ offset: 0, 0
+ index: -1
+goblin/right-upper-leg
+ rotate: false
+ xy: 63, 188
+ size: 34, 63
+ orig: 34, 63
+ offset: 0, 0
+ index: -1
+goblin/right-shoulder
+ rotate: false
+ xy: 194, 446
+ size: 39, 45
+ orig: 39, 45
+ offset: 0, 0
+ index: -1
+goblin/left-hand
+ rotate: false
+ xy: 25, 132
+ size: 36, 41
+ orig: 36, 41
+ offset: 0, 0
+ index: -1
+goblin/neck
+ rotate: false
+ xy: 63, 145
+ size: 36, 41
+ orig: 36, 41
+ offset: 0, 0
+ index: -1
+goblin/right-arm
+ rotate: false
+ xy: 165, 396
+ size: 23, 50
+ orig: 23, 50
+ offset: 0, 0
+ index: -1
+goblin/left-foot
+ rotate: false
+ xy: 190, 413
+ size: 65, 31
+ orig: 65, 31
+ offset: 0, 0
+ index: -1
+goblin/right-foot
+ rotate: false
+ xy: 190, 378
+ size: 63, 33
+ orig: 63, 33
+ offset: 0, 0
+ index: -1
+goblin/left-shoulder
+ rotate: false
+ xy: 1, 77
+ size: 29, 44
+ orig: 29, 44
+ offset: 0, 0
+ index: -1
+goblin/right-hand
+ rotate: false
+ xy: 1, 38
+ size: 36, 37
+ orig: 36, 37
+ offset: 0, 0
+ index: -1
+goblin/left-arm
+ rotate: false
+ xy: 1, 1
+ size: 37, 35
+ orig: 37, 35
+ offset: 0, 0
+ index: -1
+goblin/undies
+ rotate: false
+ xy: 130, 345
+ size: 36, 29
+ orig: 36, 29
+ offset: 0, 0
+ index: -1
+goblin/undie-straps
+ rotate: false
+ xy: 168, 357
+ size: 55, 19
+ orig: 55, 19
+ offset: 0, 0
+ index: -1
+
+goblins2.png
+format: RGBA8888
+filter: Nearest,Nearest
+repeat: none
+goblingirl/goblingirl
+ rotate: false
+ xy: 1, 217
+ size: 148, 294
+ orig: 148, 294
+ offset: 0, 0
+ index: -1
+goblingirl/head
+ rotate: false
+ xy: 1, 134
+ size: 103, 81
+ orig: 103, 81
+ offset: 0, 0
+ index: -1
+goblingirl/torso
+ rotate: false
+ xy: 151, 415
+ size: 68, 96
+ orig: 68, 96
+ offset: 0, 0
+ index: -1
+goblingirl/right-upper-leg
+ rotate: false
+ xy: 221, 448
+ size: 34, 63
+ orig: 34, 63
+ offset: 0, 0
+ index: -1
+goblingirl/dagger
+ rotate: false
+ xy: 1, 24
+ size: 26, 108
+ orig: 26, 108
+ offset: 0, 0
+ index: -1
+goblingirl/right-lower-leg
+ rotate: false
+ xy: 106, 139
+ size: 36, 76
+ orig: 36, 76
+ offset: 0, 0
+ index: -1
+goblingirl/pelvis
+ rotate: false
+ xy: 151, 370
+ size: 62, 43
+ orig: 62, 43
+ offset: 0, 0
+ index: -1
+goblingirl/left-lower-leg
+ rotate: false
+ xy: 29, 62
+ size: 33, 70
+ orig: 33, 70
+ offset: 0, 0
+ index: -1
+goblingirl/left-upper-leg
+ rotate: false
+ xy: 151, 298
+ size: 33, 70
+ orig: 33, 70
+ offset: 0, 0
+ index: -1
+goblingirl/right-shoulder
+ rotate: false
+ xy: 64, 87
+ size: 39, 45
+ orig: 39, 45
+ offset: 0, 0
+ index: -1
+goblingirl/right-arm
+ rotate: false
+ xy: 151, 246
+ size: 28, 50
+ orig: 28, 50
+ offset: 0, 0
+ index: -1
+goblingirl/left-shoulder
+ rotate: false
+ xy: 186, 322
+ size: 28, 46
+ orig: 28, 46
+ offset: 0, 0
+ index: -1
+goblingirl/left-hand
+ rotate: false
+ xy: 29, 20
+ size: 35, 40
+ orig: 35, 40
+ offset: 0, 0
+ index: -1
+goblingirl/undie-straps
+ rotate: false
+ xy: 64, 66
+ size: 55, 19
+ orig: 55, 19
+ offset: 0, 0
+ index: -1
+goblingirl/right-foot
+ rotate: false
+ xy: 66, 31
+ size: 63, 33
+ orig: 63, 33
+ offset: 0, 0
+ index: -1
+goblingirl/left-arm
+ rotate: false
+ xy: 151, 209
+ size: 37, 35
+ orig: 37, 35
+ offset: 0, 0
+ index: -1
+goblingirl/neck
+ rotate: false
+ xy: 144, 166
+ size: 35, 41
+ orig: 35, 41
+ offset: 0, 0
+ index: -1
+goblingirl/left-foot
+ rotate: false
+ xy: 144, 133
+ size: 65, 31
+ orig: 65, 31
+ offset: 0, 0
+ index: -1
+goblingirl/right-hand
+ rotate: false
+ xy: 186, 283
+ size: 36, 37
+ orig: 36, 37
+ offset: 0, 0
+ index: -1
+goblingirl/undies
+ rotate: false
+ xy: 181, 252
+ size: 36, 29
+ orig: 36, 29
+ offset: 0, 0
+ index: -1
+goblingirl/dagger-tip
+ rotate: false
+ xy: 1, 1
+ size: 17, 17
+ orig: 17, 17
+ offset: 0, 0
+ index: -1
diff --git a/spine-libgdx/test/goblins.png b/spine-libgdx/test/goblins.png
new file mode 100644
index 000000000..05a15f299
Binary files /dev/null and b/spine-libgdx/test/goblins.png differ
diff --git a/spine-libgdx/test/goblins.skel b/spine-libgdx/test/goblins.skel
new file mode 100644
index 000000000..6b9e70b10
Binary files /dev/null and b/spine-libgdx/test/goblins.skel differ
diff --git a/spine-libgdx/test/goblins2.png b/spine-libgdx/test/goblins2.png
new file mode 100644
index 000000000..f90ce4753
Binary files /dev/null and b/spine-libgdx/test/goblins2.png differ
diff --git a/spine-libgdx/test/spineboy-jump.anim b/spine-libgdx/test/spineboy-jump.anim
new file mode 100644
index 000000000..966f8a03c
Binary files /dev/null and b/spine-libgdx/test/spineboy-jump.anim differ
diff --git a/spine-libgdx/test/spineboy-jump.json b/spine-libgdx/test/spineboy-jump.json
new file mode 100644
index 000000000..4ec271f64
--- /dev/null
+++ b/spine-libgdx/test/spineboy-jump.json
@@ -0,0 +1,410 @@
+{
+"bones": {
+ "hip": {
+ "rotate": [
+ { "time": 0, "angle": 0, "curve": "stepped" },
+ { "time": 0.9333, "angle": 0, "curve": "stepped" },
+ { "time": 1.3666, "angle": 0 }
+ ],
+ "translate": [
+ { "time": 0, "x": -11.57, "y": -3 },
+ { "time": 0.2333, "x": -16.2, "y": -19.43 },
+ {
+ "time": 0.3333,
+ "x": 7.66,
+ "y": -8.48,
+ "curve": [ 0.057, 0.06, 0.712, 1 ]
+ },
+ { "time": 0.3666, "x": 15.38, "y": 5.01 },
+ { "time": 0.4666, "x": -7.84, "y": 57.22 },
+ {
+ "time": 0.6,
+ "x": -10.81,
+ "y": 96.34,
+ "curve": [ 0.241, 0, 1, 1 ]
+ },
+ { "time": 0.7333, "x": -7.01, "y": 54.7 },
+ { "time": 0.8, "x": -10.58, "y": 32.2 },
+ { "time": 0.9333, "x": -31.99, "y": 0.45 },
+ { "time": 1.0666, "x": -12.48, "y": -29.47 },
+ { "time": 1.3666, "x": -11.57, "y": -3 }
+ ],
+ "scale": [
+ { "time": 0, "x": 1, "y": 1, "curve": "stepped" },
+ { "time": 0.9333, "x": 1, "y": 1, "curve": "stepped" },
+ { "time": 1.3666, "x": 1, "y": 1 }
+ ]
+ },
+ "left upper leg": {
+ "rotate": [
+ { "time": 0, "angle": 17.13 },
+ { "time": 0.2333, "angle": 44.35 },
+ { "time": 0.3333, "angle": 16.46 },
+ { "time": 0.4, "angle": -9.88 },
+ { "time": 0.4666, "angle": -11.42 },
+ { "time": 0.5666, "angle": 23.46 },
+ { "time": 0.7666, "angle": 71.82 },
+ { "time": 0.9333, "angle": 65.53 },
+ { "time": 1.0666, "angle": 51.01 },
+ { "time": 1.3666, "angle": 17.13 }
+ ],
+ "translate": [
+ { "time": 0, "x": -3, "y": -2.25, "curve": "stepped" },
+ { "time": 0.9333, "x": -3, "y": -2.25, "curve": "stepped" },
+ { "time": 1.3666, "x": -3, "y": -2.25 }
+ ],
+ "scale": [
+ { "time": 0, "x": 1, "y": 1, "curve": "stepped" },
+ { "time": 0.9333, "x": 1, "y": 1, "curve": "stepped" },
+ { "time": 1.3666, "x": 1, "y": 1 }
+ ]
+ },
+ "left lower leg": {
+ "rotate": [
+ { "time": 0, "angle": -16.25 },
+ { "time": 0.2333, "angle": -52.21 },
+ { "time": 0.4, "angle": 15.04 },
+ { "time": 0.4666, "angle": -8.95 },
+ { "time": 0.5666, "angle": -39.53 },
+ { "time": 0.7666, "angle": -27.27 },
+ { "time": 0.9333, "angle": -3.52 },
+ { "time": 1.0666, "angle": -61.92 },
+ { "time": 1.3666, "angle": -16.25 }
+ ],
+ "translate": [
+ { "time": 0, "x": 0, "y": 0, "curve": "stepped" },
+ { "time": 0.9333, "x": 0, "y": 0, "curve": "stepped" },
+ { "time": 1.3666, "x": 0, "y": 0 }
+ ],
+ "scale": [
+ { "time": 0, "x": 1, "y": 1, "curve": "stepped" },
+ { "time": 0.9333, "x": 1, "y": 1, "curve": "stepped" },
+ { "time": 1.3666, "x": 1, "y": 1 }
+ ]
+ },
+ "left foot": {
+ "rotate": [
+ { "time": 0, "angle": 0.33 },
+ { "time": 0.2333, "angle": 6.2 },
+ { "time": 0.3333, "angle": 14.73 },
+ { "time": 0.4, "angle": -15.54 },
+ { "time": 0.4333, "angle": -21.2 },
+ { "time": 0.5666, "angle": -7.55 },
+ { "time": 0.7666, "angle": -0.67 },
+ { "time": 0.9333, "angle": -0.58 },
+ { "time": 1.0666, "angle": 14.64 },
+ { "time": 1.3666, "angle": 0.33 }
+ ],
+ "translate": [
+ { "time": 0, "x": 0, "y": 0, "curve": "stepped" },
+ { "time": 0.9333, "x": 0, "y": 0, "curve": "stepped" },
+ { "time": 1.3666, "x": 0, "y": 0 }
+ ],
+ "scale": [
+ { "time": 0, "x": 1, "y": 1, "curve": "stepped" },
+ { "time": 0.9333, "x": 1, "y": 1, "curve": "stepped" },
+ { "time": 1.3666, "x": 1, "y": 1 }
+ ]
+ },
+ "right upper leg": {
+ "rotate": [
+ { "time": 0, "angle": 25.97 },
+ { "time": 0.2333, "angle": 46.43 },
+ { "time": 0.3333, "angle": 22.61 },
+ { "time": 0.4, "angle": 2.13 },
+ {
+ "time": 0.4666,
+ "angle": 0.04,
+ "curve": [ 0, 0, 0.637, 0.98 ]
+ },
+ { "time": 0.6, "angle": 65.55 },
+ { "time": 0.7666, "angle": 64.93 },
+ { "time": 0.9333, "angle": 41.08 },
+ { "time": 1.0666, "angle": 66.25 },
+ { "time": 1.3666, "angle": 25.97 }
+ ],
+ "translate": [
+ { "time": 0, "x": 5.74, "y": 0.61 },
+ { "time": 0.2333, "x": 4.79, "y": 1.79 },
+ { "time": 0.3333, "x": 6.05, "y": -4.55 },
+ { "time": 0.9333, "x": 4.79, "y": 1.79, "curve": "stepped" },
+ { "time": 1.0666, "x": 4.79, "y": 1.79 },
+ { "time": 1.3666, "x": 5.74, "y": 0.61 }
+ ],
+ "scale": [
+ { "time": 0, "x": 1, "y": 1, "curve": "stepped" },
+ { "time": 0.9333, "x": 1, "y": 1, "curve": "stepped" },
+ { "time": 1.3666, "x": 1, "y": 1 }
+ ]
+ },
+ "right lower leg": {
+ "rotate": [
+ { "time": 0, "angle": -27.46 },
+ { "time": 0.2333, "angle": -64.03 },
+ { "time": 0.4, "angle": -48.36 },
+ { "time": 0.5666, "angle": -76.86 },
+ { "time": 0.7666, "angle": -26.89 },
+ { "time": 0.9, "angle": -18.97 },
+ { "time": 0.9333, "angle": -14.18 },
+ { "time": 1.0666, "angle": -80.45 },
+ { "time": 1.3666, "angle": -27.46 }
+ ],
+ "translate": [
+ { "time": 0, "x": 0, "y": 0, "curve": "stepped" },
+ { "time": 0.9333, "x": 0, "y": 0, "curve": "stepped" },
+ { "time": 1.3666, "x": 0, "y": 0 }
+ ],
+ "scale": [
+ { "time": 0, "x": 1, "y": 1, "curve": "stepped" },
+ { "time": 0.9333, "x": 1, "y": 1, "curve": "stepped" },
+ { "time": 1.3666, "x": 1, "y": 1 }
+ ]
+ },
+ "right foot": {
+ "rotate": [
+ { "time": 0, "angle": 1.08 },
+ { "time": 0.2333, "angle": 16.02 },
+ { "time": 0.3, "angle": 12.94 },
+ { "time": 0.3333, "angle": 15.16 },
+ { "time": 0.4, "angle": -14.7 },
+ { "time": 0.4333, "angle": -12.85 },
+ { "time": 0.4666, "angle": -19.18 },
+ { "time": 0.5666, "angle": -15.82 },
+ { "time": 0.6, "angle": -3.59 },
+ { "time": 0.7666, "angle": -3.56 },
+ { "time": 0.9333, "angle": 1.86 },
+ { "time": 1.0666, "angle": 16.02 },
+ { "time": 1.3666, "angle": 1.08 }
+ ],
+ "translate": [
+ { "time": 0, "x": 0, "y": 0, "curve": "stepped" },
+ { "time": 0.9333, "x": 0, "y": 0, "curve": "stepped" },
+ { "time": 1.3666, "x": 0, "y": 0 }
+ ],
+ "scale": [
+ { "time": 0, "x": 1, "y": 1, "curve": "stepped" },
+ { "time": 0.9333, "x": 1, "y": 1, "curve": "stepped" },
+ { "time": 1.3666, "x": 1, "y": 1 }
+ ]
+ },
+ "torso": {
+ "rotate": [
+ { "time": 0, "angle": -13.35 },
+ { "time": 0.2333, "angle": -48.95 },
+ { "time": 0.4333, "angle": -35.77 },
+ { "time": 0.6, "angle": -4.59 },
+ { "time": 0.7666, "angle": 14.61 },
+ { "time": 0.9333, "angle": 15.74 },
+ { "time": 1.0666, "angle": -32.44 },
+ { "time": 1.3666, "angle": -13.35 }
+ ],
+ "translate": [
+ { "time": 0, "x": -3.67, "y": 1.68, "curve": "stepped" },
+ { "time": 0.9333, "x": -3.67, "y": 1.68, "curve": "stepped" },
+ { "time": 1.3666, "x": -3.67, "y": 1.68 }
+ ],
+ "scale": [
+ { "time": 0, "x": 1, "y": 1, "curve": "stepped" },
+ { "time": 0.9333, "x": 1, "y": 1, "curve": "stepped" },
+ { "time": 1.3666, "x": 1, "y": 1 }
+ ]
+ },
+ "neck": {
+ "rotate": [
+ { "time": 0, "angle": 12.78 },
+ { "time": 0.2333, "angle": 16.46 },
+ { "time": 0.4, "angle": 26.49 },
+ { "time": 0.6, "angle": 15.51 },
+ { "time": 0.7666, "angle": 1.34 },
+ { "time": 0.9333, "angle": 2.35 },
+ { "time": 1.0666, "angle": 6.08 },
+ { "time": 1.3, "angle": 21.23 },
+ { "time": 1.3666, "angle": 12.78 }
+ ],
+ "translate": [
+ { "time": 0, "x": 0, "y": 0, "curve": "stepped" },
+ { "time": 0.9333, "x": 0, "y": 0, "curve": "stepped" },
+ { "time": 1.3666, "x": 0, "y": 0 }
+ ],
+ "scale": [
+ { "time": 0, "x": 1, "y": 1, "curve": "stepped" },
+ { "time": 0.9333, "x": 1, "y": 1, "curve": "stepped" },
+ { "time": 1.3666, "x": 1, "y": 1 }
+ ]
+ },
+ "head": {
+ "rotate": [
+ { "time": 0, "angle": 5.19 },
+ { "time": 0.2333, "angle": 20.27 },
+ { "time": 0.4, "angle": 15.27 },
+ { "time": 0.6, "angle": -24.69 },
+ { "time": 0.7666, "angle": -11.02 },
+ { "time": 0.9333, "angle": -24.38 },
+ { "time": 1.0666, "angle": 11.99 },
+ { "time": 1.3, "angle": 4.86 },
+ { "time": 1.3666, "angle": 5.19 }
+ ],
+ "translate": [
+ { "time": 0, "x": 0, "y": 0, "curve": "stepped" },
+ { "time": 0.9333, "x": 0, "y": 0, "curve": "stepped" },
+ { "time": 1.3666, "x": 0, "y": 0 }
+ ],
+ "scale": [
+ { "time": 0, "x": 1, "y": 1, "curve": "stepped" },
+ { "time": 0.9333, "x": 1, "y": 1, "curve": "stepped" },
+ { "time": 1.3666, "x": 1, "y": 1 }
+ ]
+ },
+ "left shoulder": {
+ "rotate": [
+ {
+ "time": 0,
+ "angle": 0.05,
+ "curve": [ 0, 0, 0.62, 1 ]
+ },
+ {
+ "time": 0.2333,
+ "angle": 279.66,
+ "curve": [ 0.218, 0.67, 0.66, 0.99 ]
+ },
+ {
+ "time": 0.5,
+ "angle": 62.27,
+ "curve": [ 0.462, 0, 0.764, 0.58 ]
+ },
+ { "time": 0.9333, "angle": 28.91 },
+ { "time": 1.0666, "angle": -8.62 },
+ { "time": 1.1666, "angle": -18.43 },
+ { "time": 1.3666, "angle": 0.05 }
+ ],
+ "translate": [
+ { "time": 0, "x": -1.76, "y": 0.56, "curve": "stepped" },
+ { "time": 0.9333, "x": -1.76, "y": 0.56, "curve": "stepped" },
+ { "time": 1.3666, "x": -1.76, "y": 0.56 }
+ ],
+ "scale": [
+ { "time": 0, "x": 1, "y": 1, "curve": "stepped" },
+ { "time": 0.9333, "x": 1, "y": 1, "curve": "stepped" },
+ { "time": 1.3666, "x": 1, "y": 1 }
+ ]
+ },
+ "left hand": {
+ "rotate": [
+ { "time": 0, "angle": 11.58, "curve": "stepped" },
+ { "time": 0.9333, "angle": 11.58, "curve": "stepped" },
+ { "time": 1.3666, "angle": 11.58 }
+ ],
+ "translate": [
+ { "time": 0, "x": 0, "y": 0, "curve": "stepped" },
+ { "time": 0.9333, "x": 0, "y": 0, "curve": "stepped" },
+ { "time": 1.3666, "x": 0, "y": 0 }
+ ],
+ "scale": [
+ { "time": 0, "x": 1, "y": 1, "curve": "stepped" },
+ { "time": 0.9333, "x": 1, "y": 1, "curve": "stepped" },
+ { "time": 1.3666, "x": 1, "y": 1 }
+ ]
+ },
+ "left arm": {
+ "rotate": [
+ { "time": 0, "angle": 0.51 },
+ { "time": 0.4333, "angle": 12.82 },
+ { "time": 0.6, "angle": 47.55 },
+ { "time": 0.9333, "angle": 12.82 },
+ { "time": 1.1666, "angle": -6.5 },
+ { "time": 1.3666, "angle": 0.51 }
+ ],
+ "translate": [
+ { "time": 0, "x": 0, "y": 0, "curve": "stepped" },
+ { "time": 0.9333, "x": 0, "y": 0, "curve": "stepped" },
+ { "time": 1.3666, "x": 0, "y": 0 }
+ ],
+ "scale": [
+ { "time": 0, "x": 1, "y": 1, "curve": "stepped" },
+ { "time": 0.9333, "x": 1, "y": 1, "curve": "stepped" },
+ { "time": 1.3666, "x": 1, "y": 1 }
+ ]
+ },
+ "right shoulder": {
+ "rotate": [
+ {
+ "time": 0,
+ "angle": 43.82,
+ "curve": [ 0, 0, 0.62, 1 ]
+ },
+ {
+ "time": 0.2333,
+ "angle": -8.74,
+ "curve": [ 0.304, 0.58, 0.709, 0.97 ]
+ },
+ {
+ "time": 0.5333,
+ "angle": -208.02,
+ "curve": [ 0.462, 0, 0.764, 0.58 ]
+ },
+ { "time": 0.9333, "angle": -246.72 },
+ { "time": 1.0666, "angle": -307.13 },
+ { "time": 1.1666, "angle": 37.15 },
+ { "time": 1.3666, "angle": 43.82 }
+ ],
+ "translate": [
+ { "time": 0, "x": -7.84, "y": 7.19, "curve": "stepped" },
+ { "time": 0.9333, "x": -7.84, "y": 7.19, "curve": "stepped" },
+ { "time": 1.3666, "x": -7.84, "y": 7.19 }
+ ],
+ "scale": [
+ { "time": 0, "x": 1, "y": 1, "curve": "stepped" },
+ { "time": 0.9333, "x": 1, "y": 1, "curve": "stepped" },
+ { "time": 1.3666, "x": 1, "y": 1 }
+ ]
+ },
+ "right arm": {
+ "rotate": [
+ { "time": 0, "angle": -4.02 },
+ { "time": 0.6, "angle": 17.5 },
+ { "time": 0.9333, "angle": -4.02 },
+ { "time": 1.1666, "angle": -16.72 },
+ { "time": 1.3666, "angle": -4.02 }
+ ],
+ "translate": [
+ { "time": 0, "x": 0, "y": 0, "curve": "stepped" },
+ { "time": 0.9333, "x": 0, "y": 0, "curve": "stepped" },
+ { "time": 1.3666, "x": 0, "y": 0 }
+ ],
+ "scale": [
+ { "time": 0, "x": 1, "y": 1, "curve": "stepped" },
+ { "time": 0.9333, "x": 1, "y": 1, "curve": "stepped" },
+ { "time": 1.3666, "x": 1, "y": 1 }
+ ]
+ },
+ "right hand": {
+ "rotate": [
+ { "time": 0, "angle": 22.92, "curve": "stepped" },
+ { "time": 0.9333, "angle": 22.92, "curve": "stepped" },
+ { "time": 1.3666, "angle": 22.92 }
+ ],
+ "translate": [
+ { "time": 0, "x": 0, "y": 0, "curve": "stepped" },
+ { "time": 0.9333, "x": 0, "y": 0, "curve": "stepped" },
+ { "time": 1.3666, "x": 0, "y": 0 }
+ ],
+ "scale": [
+ { "time": 0, "x": 1, "y": 1, "curve": "stepped" },
+ { "time": 0.9333, "x": 1, "y": 1, "curve": "stepped" },
+ { "time": 1.3666, "x": 1, "y": 1 }
+ ]
+ },
+ "root": {
+ "rotate": [
+ { "time": 0, "angle": 0 },
+ { "time": 0.4333, "angle": -14.52 },
+ { "time": 0.8, "angle": 9.86 },
+ { "time": 1.3666, "angle": 0 }
+ ],
+ "scale": [
+ { "time": 0, "x": 1, "y": 1, "curve": "stepped" },
+ { "time": 1.3666, "x": 1, "y": 1 }
+ ]
+ }
+}
+}
\ No newline at end of file
diff --git a/spine-libgdx/test/spineboy-skeleton.json b/spine-libgdx/test/spineboy-skeleton.json
new file mode 100644
index 000000000..a51a9feeb
--- /dev/null
+++ b/spine-libgdx/test/spineboy-skeleton.json
@@ -0,0 +1,101 @@
+{
+"bones": [
+ { "name": "root", "length": 0 },
+ { "name": "hip", "parent": "root", "length": 0, "x": 0.64, "y": 114.41 },
+ { "name": "left upper leg", "parent": "hip", "length": 50.39, "x": 14.45, "y": 2.81, "rotation": -89.09 },
+ { "name": "left lower leg", "parent": "left upper leg", "length": 56.45, "x": 51.78, "y": 3.46, "rotation": -16.65 },
+ { "name": "left foot", "parent": "left lower leg", "length": 46.5, "x": 64.02, "y": -8.67, "rotation": 102.43 },
+ { "name": "right upper leg", "parent": "hip", "length": 45.76, "x": -18.27, "rotation": -101.13 },
+ { "name": "right lower leg", "parent": "right upper leg", "length": 58.52, "x": 50.21, "y": 0.6, "rotation": -10.7 },
+ { "name": "right foot", "parent": "right lower leg", "length": 45.45, "x": 64.88, "y": 0.04, "rotation": 110.3 },
+ { "name": "torso", "parent": "hip", "length": 85.82, "x": -6.42, "y": 1.97, "rotation": 94.95 },
+ { "name": "neck", "parent": "torso", "length": 18.38, "x": 83.64, "y": -1.78, "rotation": 0.9 },
+ { "name": "head", "parent": "neck", "length": 68.28, "x": 19.09, "y": 6.97, "rotation": -8.94 },
+ { "name": "right shoulder", "parent": "torso", "length": 49.95, "x": 81.9, "y": 6.79, "rotation": 130.6 },
+ { "name": "right arm", "parent": "right shoulder", "length": 36.74, "x": 49.95, "y": -0.12, "rotation": 40.12 },
+ { "name": "right hand", "parent": "right arm", "length": 15.32, "x": 36.9, "y": 0.34, "rotation": 2.35 },
+ { "name": "left shoulder", "parent": "torso", "length": 44.19, "x": 78.96, "y": -15.75, "rotation": -156.96 },
+ { "name": "left arm", "parent": "left shoulder", "length": 35.62, "x": 44.19, "y": -0.01, "rotation": 28.16 },
+ { "name": "left hand", "parent": "left arm", "length": 11.52, "x": 35.62, "y": 0.07, "rotation": 2.7 },
+ { "name": "pelvis", "parent": "hip", "length": 0, "x": 1.41, "y": -6.57 }
+],
+"slots": [
+ { "name": "template", "bone": "root", "color": "ff898c86" },
+ { "name": "left shoulder", "bone": "left shoulder", "attachment": "left-shoulder" },
+ { "name": "left arm", "bone": "left arm", "attachment": "left-arm" },
+ { "name": "left hand", "bone": "left hand", "attachment": "left-hand" },
+ { "name": "left foot", "bone": "left foot", "attachment": "left-foot" },
+ { "name": "left lower leg", "bone": "left lower leg", "attachment": "left-lower-leg" },
+ { "name": "left upper leg", "bone": "left upper leg", "attachment": "left-upper-leg" },
+ { "name": "pelvis", "bone": "pelvis", "attachment": "pelvis" },
+ { "name": "right foot", "bone": "right foot", "attachment": "right-foot" },
+ { "name": "right lower leg", "bone": "right lower leg", "attachment": "right-lower-leg" },
+ { "name": "right upper leg", "bone": "right upper leg", "attachment": "right-upper-leg" },
+ { "name": "torso", "bone": "torso", "attachment": "torso" },
+ { "name": "neck", "bone": "neck", "attachment": "neck" },
+ { "name": "head", "bone": "head", "attachment": "head" },
+ { "name": "eyes", "bone": "head", "attachment": "eyes" },
+ { "name": "right shoulder", "bone": "right shoulder", "attachment": "right-shoulder" },
+ { "name": "right arm", "bone": "right arm", "attachment": "right-arm" },
+ { "name": "right hand", "bone": "right hand", "attachment": "right-hand" }
+],
+"skins": {
+ "default": {
+ "template": {
+ "spineboy": { "y": 167.82, "width": 145, "height": 341 }
+ },
+ "left shoulder": {
+ "left-shoulder": { "x": 23.74, "y": 0.11, "rotation": 62.01, "width": 34, "height": 53 }
+ },
+ "left arm": {
+ "left-arm": { "x": 15.11, "y": -0.44, "rotation": 33.84, "width": 35, "height": 29 }
+ },
+ "left hand": {
+ "left-hand": { "x": 0.75, "y": 1.86, "rotation": 31.14, "width": 35, "height": 38 }
+ },
+ "left foot": {
+ "left-foot": { "x": 24.35, "y": 8.88, "rotation": 3.32, "width": 65, "height": 30 }
+ },
+ "left lower leg": {
+ "left-lower-leg": { "x": 24.55, "y": -1.92, "rotation": 105.75, "width": 49, "height": 64 }
+ },
+ "left upper leg": {
+ "left-upper-leg": { "x": 26.12, "y": -1.85, "rotation": 89.09, "width": 33, "height": 67 }
+ },
+ "pelvis": {
+ "pelvis": { "x": -4.83, "y": 10.62, "width": 63, "height": 47 }
+ },
+ "right foot": {
+ "right-foot": { "x": 19.02, "y": 8.47, "rotation": 1.52, "width": 67, "height": 30 }
+ },
+ "right lower leg": {
+ "right-lower-leg": { "x": 23.28, "y": -2.59, "rotation": 111.83, "width": 51, "height": 64 }
+ },
+ "right upper leg": {
+ "right-upper-leg": { "x": 23.03, "y": 0.25, "rotation": 101.13, "width": 44, "height": 70 }
+ },
+ "torso": {
+ "torso": { "x": 44.57, "y": -7.08, "rotation": -94.95, "width": 68, "height": 92 }
+ },
+ "neck": {
+ "neck": { "x": 9.42, "y": -3.66, "rotation": -100.15, "width": 34, "height": 28 }
+ },
+ "head": {
+ "head": { "x": 53.94, "y": -5.75, "rotation": -86.9, "width": 121, "height": 132 }
+ },
+ "eyes": {
+ "eyes": { "x": 28.94, "y": -32.92, "rotation": -86.9, "width": 34, "height": 27 },
+ "eyes-closed": { "x": 28.77, "y": -32.86, "rotation": -86.9, "width": 34, "height": 27 }
+ },
+ "right shoulder": {
+ "right-shoulder": { "x": 25.86, "y": 0.03, "rotation": 134.44, "width": 52, "height": 51 }
+ },
+ "right arm": {
+ "right-arm": { "x": 18.34, "y": -2.64, "rotation": 94.32, "width": 21, "height": 45 }
+ },
+ "right hand": {
+ "right-hand": { "x": 6.82, "y": 1.25, "rotation": 91.96, "width": 32, "height": 32 }
+ }
+ }
+}
+}
\ No newline at end of file
diff --git a/spine-libgdx/test/spineboy-walk.anim b/spine-libgdx/test/spineboy-walk.anim
new file mode 100644
index 000000000..077f20f5a
Binary files /dev/null and b/spine-libgdx/test/spineboy-walk.anim differ
diff --git a/spine-libgdx/test/spineboy-walk.json b/spine-libgdx/test/spineboy-walk.json
new file mode 100644
index 000000000..b40e53a95
--- /dev/null
+++ b/spine-libgdx/test/spineboy-walk.json
@@ -0,0 +1,278 @@
+{
+"bones": {
+ "left upper leg": {
+ "rotate": [
+ { "time": 0, "angle": -26.55 },
+ { "time": 0.1333, "angle": -8.78 },
+ { "time": 0.2666, "angle": 9.51 },
+ { "time": 0.4, "angle": 30.74 },
+ { "time": 0.5333, "angle": 25.33 },
+ { "time": 0.6666, "angle": 26.11 },
+ { "time": 0.8, "angle": -7.7 },
+ { "time": 0.9333, "angle": -21.19 },
+ { "time": 1.0666, "angle": -26.55 }
+ ],
+ "translate": [
+ { "time": 0, "x": -3, "y": -2.25 },
+ { "time": 0.4, "x": -2.18, "y": -2.25 },
+ { "time": 1.0666, "x": -3, "y": -2.25 }
+ ]
+ },
+ "right upper leg": {
+ "rotate": [
+ { "time": 0, "angle": 42.45 },
+ { "time": 0.1333, "angle": 52.1 },
+ { "time": 0.2666, "angle": 5.96 },
+ { "time": 0.5333, "angle": -16.93 },
+ { "time": 0.6666, "angle": 1.89 },
+ {
+ "time": 0.8,
+ "angle": 28.06,
+ "curve": [ 0.462, 0.11, 1, 1 ]
+ },
+ {
+ "time": 0.9333,
+ "angle": 58.68,
+ "curve": [ 0.5, 0.02, 1, 1 ]
+ },
+ { "time": 1.0666, "angle": 42.45 }
+ ],
+ "translate": [
+ { "time": 0, "x": 8.11, "y": -2.36 },
+ { "time": 0.1333, "x": 10.03, "y": -2.56 },
+ { "time": 0.4, "x": 2.76, "y": -2.97 },
+ { "time": 0.5333, "x": 2.76, "y": -2.81 },
+ { "time": 0.9333, "x": 8.67, "y": -2.54 },
+ { "time": 1.0666, "x": 8.11, "y": -2.36 }
+ ]
+ },
+ "left lower leg": {
+ "rotate": [
+ { "time": 0, "angle": -10.21 },
+ { "time": 0.1333, "angle": -55.64 },
+ { "time": 0.2666, "angle": -68.12 },
+ { "time": 0.5333, "angle": 5.11 },
+ { "time": 0.6666, "angle": -28.29 },
+ { "time": 0.8, "angle": 4.08 },
+ { "time": 0.9333, "angle": 3.53 },
+ { "time": 1.0666, "angle": -10.21 }
+ ]
+ },
+ "left foot": {
+ "rotate": [
+ { "time": 0, "angle": -3.69 },
+ { "time": 0.1333, "angle": -10.42 },
+ { "time": 0.2666, "angle": -17.14 },
+ { "time": 0.4, "angle": -2.83 },
+ { "time": 0.5333, "angle": -3.87 },
+ { "time": 0.6666, "angle": 2.78 },
+ { "time": 0.8, "angle": 1.68 },
+ { "time": 0.9333, "angle": -8.54 },
+ { "time": 1.0666, "angle": -3.69 }
+ ]
+ },
+ "right shoulder": {
+ "rotate": [
+ {
+ "time": 0,
+ "angle": 20.89,
+ "curve": [ 0.264, 0, 0.75, 1 ]
+ },
+ {
+ "time": 0.1333,
+ "angle": 3.72,
+ "curve": [ 0.272, 0, 0.841, 1 ]
+ },
+ { "time": 0.6666, "angle": -278.28 },
+ { "time": 1.0666, "angle": 20.89 }
+ ],
+ "translate": [
+ { "time": 0, "x": -7.84, "y": 7.19 },
+ { "time": 0.1333, "x": -6.36, "y": 6.42 },
+ { "time": 0.6666, "x": -11.07, "y": 5.25 },
+ { "time": 1.0666, "x": -7.84, "y": 7.19 }
+ ]
+ },
+ "right arm": {
+ "rotate": [
+ {
+ "time": 0,
+ "angle": -4.02,
+ "curve": [ 0.267, 0, 0.804, 0.99 ]
+ },
+ {
+ "time": 0.1333,
+ "angle": -13.99,
+ "curve": [ 0.341, 0, 1, 1 ]
+ },
+ {
+ "time": 0.6666,
+ "angle": 36.54,
+ "curve": [ 0.307, 0, 0.787, 0.99 ]
+ },
+ { "time": 1.0666, "angle": -4.02 }
+ ]
+ },
+ "right hand": {
+ "rotate": [
+ { "time": 0, "angle": 22.92 },
+ { "time": 0.4, "angle": -8.97 },
+ { "time": 0.6666, "angle": 0.51 },
+ { "time": 1.0666, "angle": 22.92 }
+ ]
+ },
+ "left shoulder": {
+ "rotate": [
+ { "time": 0, "angle": -1.47 },
+ { "time": 0.1333, "angle": 13.6 },
+ { "time": 0.6666, "angle": 280.74 },
+ { "time": 1.0666, "angle": -1.47 }
+ ],
+ "translate": [
+ { "time": 0, "x": -1.76, "y": 0.56 },
+ { "time": 0.6666, "x": -2.47, "y": 8.14 },
+ { "time": 1.0666, "x": -1.76, "y": 0.56 }
+ ]
+ },
+ "left hand": {
+ "rotate": [
+ {
+ "time": 0,
+ "angle": 11.58,
+ "curve": [ 0.169, 0.37, 0.632, 1.55 ]
+ },
+ {
+ "time": 0.1333,
+ "angle": 28.13,
+ "curve": [ 0.692, 0, 0.692, 0.99 ]
+ },
+ {
+ "time": 0.6666,
+ "angle": -27.42,
+ "curve": [ 0.117, 0.41, 0.738, 1.76 ]
+ },
+ { "time": 0.8, "angle": -36.32 },
+ { "time": 1.0666, "angle": 11.58 }
+ ]
+ },
+ "left arm": {
+ "rotate": [
+ { "time": 0, "angle": -8.27 },
+ { "time": 0.1333, "angle": 18.43 },
+ { "time": 0.6666, "angle": 0.88 },
+ { "time": 1.0666, "angle": -8.27 }
+ ]
+ },
+ "torso": {
+ "rotate": [
+ { "time": 0, "angle": -10.28 },
+ {
+ "time": 0.1333,
+ "angle": -15.38,
+ "curve": [ 0.545, 0, 1, 1 ]
+ },
+ {
+ "time": 0.4,
+ "angle": -9.78,
+ "curve": [ 0.58, 0.17, 1, 1 ]
+ },
+ { "time": 0.6666, "angle": -15.75 },
+ { "time": 0.9333, "angle": -7.06 },
+ { "time": 1.0666, "angle": -10.28 }
+ ],
+ "translate": [
+ { "time": 0, "x": -3.67, "y": 1.68 },
+ { "time": 0.1333, "x": -3.67, "y": 0.68 },
+ { "time": 0.4, "x": -3.67, "y": 1.97 },
+ { "time": 0.6666, "x": -3.67, "y": -0.14 },
+ { "time": 1.0666, "x": -3.67, "y": 1.68 }
+ ]
+ },
+ "right foot": {
+ "rotate": [
+ { "time": 0, "angle": -5.25 },
+ { "time": 0.2666, "angle": -4.08 },
+ { "time": 0.4, "angle": -6.45 },
+ { "time": 0.5333, "angle": -5.39 },
+ { "time": 0.8, "angle": -11.68 },
+ { "time": 0.9333, "angle": 0.46 },
+ { "time": 1.0666, "angle": -5.25 }
+ ]
+ },
+ "right lower leg": {
+ "rotate": [
+ { "time": 0, "angle": -3.39 },
+ { "time": 0.1333, "angle": -45.53 },
+ { "time": 0.2666, "angle": -2.59 },
+ { "time": 0.5333, "angle": -19.53 },
+ { "time": 0.6666, "angle": -64.8 },
+ {
+ "time": 0.8,
+ "angle": -82.56,
+ "curve": [ 0.557, 0.18, 1, 1 ]
+ },
+ { "time": 1.0666, "angle": -3.39 }
+ ]
+ },
+ "hip": {
+ "rotate": [
+ { "time": 0, "angle": 0, "curve": "stepped" },
+ { "time": 1.0666, "angle": 0 }
+ ],
+ "translate": [
+ { "time": 0, "x": 0, "y": 0 },
+ {
+ "time": 0.1333,
+ "x": 0,
+ "y": -7.61,
+ "curve": [ 0.272, 0.86, 1, 1 ]
+ },
+ { "time": 0.4, "x": 0, "y": 8.7 },
+ { "time": 0.5333, "x": 0, "y": -0.41 },
+ {
+ "time": 0.6666,
+ "x": 0,
+ "y": -7.05,
+ "curve": [ 0.235, 0.89, 1, 1 ]
+ },
+ { "time": 0.8, "x": 0, "y": 2.92 },
+ { "time": 0.9333, "x": 0, "y": 6.78 },
+ { "time": 1.0666, "x": 0, "y": 0 }
+ ]
+ },
+ "neck": {
+ "rotate": [
+ { "time": 0, "angle": 3.6 },
+ { "time": 0.1333, "angle": 17.49 },
+ { "time": 0.2666, "angle": 6.1 },
+ { "time": 0.4, "angle": 3.45 },
+ { "time": 0.5333, "angle": 5.17 },
+ { "time": 0.6666, "angle": 18.36 },
+ { "time": 0.8, "angle": 6.09 },
+ { "time": 0.9333, "angle": 2.28 },
+ { "time": 1.0666, "angle": 3.6 }
+ ]
+ },
+ "head": {
+ "rotate": [
+ {
+ "time": 0,
+ "angle": 3.6,
+ "curve": [ 0, 0, 0.704, 1.61 ]
+ },
+ { "time": 0.1666, "angle": -0.2 },
+ { "time": 0.2666, "angle": 6.1 },
+ { "time": 0.4, "angle": 3.45 },
+ {
+ "time": 0.5333,
+ "angle": 5.17,
+ "curve": [ 0, 0, 0.704, 1.61 ]
+ },
+ { "time": 0.7, "angle": 1.1 },
+ { "time": 0.8, "angle": 6.09 },
+ { "time": 0.9333, "angle": 2.28 },
+ { "time": 1.0666, "angle": 3.6 }
+ ]
+ }
+}
+}
\ No newline at end of file
diff --git a/spine-libgdx/test/spineboy.atlas b/spine-libgdx/test/spineboy.atlas
new file mode 100644
index 000000000..88fb3e0b5
--- /dev/null
+++ b/spine-libgdx/test/spineboy.atlas
@@ -0,0 +1,166 @@
+
+spineboy.png
+format: RGBA8888
+filter: Nearest,Nearest
+repeat: none
+head
+ rotate: false
+ xy: 1, 122
+ size: 121, 132
+ orig: 121, 132
+ offset: 0, 0
+ index: -1
+torso
+ rotate: false
+ xy: 1, 28
+ size: 68, 92
+ orig: 68, 92
+ offset: 0, 0
+ index: -1
+left-pant-bottom
+ rotate: false
+ xy: 1, 4
+ size: 44, 22
+ orig: 44, 22
+ offset: 0, 0
+ index: -1
+right-pant-bottom
+ rotate: false
+ xy: 47, 8
+ size: 46, 18
+ orig: 46, 18
+ offset: 0, 0
+ index: -1
+right-upper-leg
+ rotate: false
+ xy: 71, 50
+ size: 44, 70
+ orig: 44, 70
+ offset: 0, 0
+ index: -1
+pelvis
+ rotate: false
+ xy: 95, 1
+ size: 63, 47
+ orig: 63, 47
+ offset: 0, 0
+ index: -1
+left-upper-leg
+ rotate: false
+ xy: 117, 53
+ size: 33, 67
+ orig: 33, 67
+ offset: 0, 0
+ index: -1
+right-foot
+ rotate: false
+ xy: 160, 224
+ size: 67, 30
+ orig: 67, 30
+ offset: 0, 0
+ index: -1
+left-shoulder
+ rotate: false
+ xy: 124, 201
+ size: 34, 53
+ orig: 34, 53
+ offset: 0, 0
+ index: -1
+left-ankle
+ rotate: false
+ xy: 229, 222
+ size: 25, 32
+ orig: 25, 32
+ offset: 0, 0
+ index: -1
+left-foot
+ rotate: false
+ xy: 160, 192
+ size: 65, 30
+ orig: 65, 30
+ offset: 0, 0
+ index: -1
+neck
+ rotate: false
+ xy: 124, 171
+ size: 34, 28
+ orig: 34, 28
+ offset: 0, 0
+ index: -1
+right-arm
+ rotate: false
+ xy: 124, 124
+ size: 21, 45
+ orig: 21, 45
+ offset: 0, 0
+ index: -1
+right-ankle
+ rotate: false
+ xy: 227, 190
+ size: 25, 30
+ orig: 25, 30
+ offset: 0, 0
+ index: -1
+left-hand
+ rotate: false
+ xy: 147, 131
+ size: 35, 38
+ orig: 35, 38
+ offset: 0, 0
+ index: -1
+left-arm
+ rotate: false
+ xy: 184, 161
+ size: 35, 29
+ orig: 35, 29
+ offset: 0, 0
+ index: -1
+eyes-closed
+ rotate: false
+ xy: 221, 161
+ size: 34, 27
+ orig: 34, 27
+ offset: 0, 0
+ index: -1
+right-lower-leg
+ rotate: false
+ xy: 152, 65
+ size: 51, 64
+ orig: 51, 64
+ offset: 0, 0
+ index: -1
+right-foot-idle
+ rotate: false
+ xy: 184, 131
+ size: 53, 28
+ orig: 53, 28
+ offset: 0, 0
+ index: -1
+left-lower-leg
+ rotate: false
+ xy: 205, 65
+ size: 49, 64
+ orig: 49, 64
+ offset: 0, 0
+ index: -1
+right-shoulder
+ rotate: false
+ xy: 160, 12
+ size: 52, 51
+ orig: 52, 51
+ offset: 0, 0
+ index: -1
+eyes
+ rotate: false
+ xy: 214, 36
+ size: 34, 27
+ orig: 34, 27
+ offset: 0, 0
+ index: -1
+right-hand
+ rotate: false
+ xy: 214, 2
+ size: 32, 32
+ orig: 32, 32
+ offset: 0, 0
+ index: -1
diff --git a/spine-libgdx/test/spineboy.png b/spine-libgdx/test/spineboy.png
new file mode 100644
index 000000000..b8b493dfd
Binary files /dev/null and b/spine-libgdx/test/spineboy.png differ
diff --git a/spine-libgdx/test/spineboy.skel b/spine-libgdx/test/spineboy.skel
new file mode 100644
index 000000000..71f85c395
Binary files /dev/null and b/spine-libgdx/test/spineboy.skel differ