瀏覽代碼

Merge branch '3.7-beta' into 3.7-beta-cpp

badlogic 7 年之前
父節點
當前提交
38e5d2f35a
共有 24 個文件被更改,包括 346 次插入190 次删除
  1. 0 0
      examples/stretchyman/export/stretchyman-pro.json
  2. 二進制
      examples/stretchyman/export/stretchyman-pro.skel
  3. 二進制
      examples/stretchyman/stretchyman-pro.spine
  4. 18 7
      spine-csharp/src/Animation.cs
  5. 1 1
      spine-csharp/src/AnimationState.cs
  6. 55 17
      spine-csharp/src/IkConstraint.cs
  7. 9 0
      spine-csharp/src/IkConstraintData.cs
  8. 1 0
      spine-csharp/src/Skeleton.cs
  9. 2 1
      spine-csharp/src/SkeletonBinary.cs
  10. 3 1
      spine-csharp/src/SkeletonJson.cs
  11. 11 1
      spine-libgdx/spine-libgdx-tests/src/com/esotericsoftware/spine/AnimationStateTests.java
  12. 18 9
      spine-libgdx/spine-libgdx/src/com/esotericsoftware/spine/Animation.java
  13. 112 80
      spine-libgdx/spine-libgdx/src/com/esotericsoftware/spine/AnimationState.java
  14. 27 11
      spine-libgdx/spine-libgdx/src/com/esotericsoftware/spine/IkConstraint.java
  15. 27 8
      spine-libgdx/spine-libgdx/src/com/esotericsoftware/spine/IkConstraintData.java
  16. 2 1
      spine-libgdx/spine-libgdx/src/com/esotericsoftware/spine/Skeleton.java
  17. 5 2
      spine-libgdx/spine-libgdx/src/com/esotericsoftware/spine/SkeletonBinary.java
  18. 5 2
      spine-libgdx/spine-libgdx/src/com/esotericsoftware/spine/SkeletonJson.java
  19. 1 0
      spine-unity/Assets/Spine/Editor/spine-unity/Editor/SkeletonDebugWindow.cs
  20. 1 1
      spine-unity/Assets/Spine/Editor/spine-unity/Editor/SpineEditorUtilities.cs
  21. 1 2
      spine-unity/Assets/Spine/Runtime/spine-unity/Modules/Ghost/SkeletonGhost.cs
  22. 37 41
      spine-unity/Assets/Spine/Runtime/spine-unity/Modules/Ghost/SkeletonGhostRenderer.cs
  23. 9 5
      spine-unity/Assets/Spine/Runtime/spine-unity/Modules/SkeletonGraphic/SkeletonGraphicMirror.cs
  24. 1 0
      spine-unity/Assets/Spine/Runtime/spine-unity/SkeletonExtensions.cs

File diff suppressed because it is too large
+ 0 - 0
examples/stretchyman/export/stretchyman-pro.json


二進制
examples/stretchyman/export/stretchyman-pro.skel


二進制
examples/stretchyman/stretchyman-pro.spine


+ 18 - 7
spine-csharp/src/Animation.cs

@@ -1210,9 +1210,9 @@ namespace Spine {
 	}
 
 	public class IkConstraintTimeline : CurveTimeline {
-		public const int ENTRIES = 3;
-		private const int PREV_TIME = -3, PREV_MIX = -2, PREV_BEND_DIRECTION = -1;
-		private const int MIX = 1, BEND_DIRECTION = 2;
+		public const int ENTRIES = 4;
+		private const int PREV_TIME = -4, PREV_MIX = -3, PREV_BEND_DIRECTION = -2, PREV_STRETCH = -1;
+		private const int MIX = 1, BEND_DIRECTION = 2, STRETCH = 3;
 
 		internal int ikConstraintIndex;
 		internal float[] frames;
@@ -1230,11 +1230,12 @@ namespace Spine {
 		}
 			
 		/// <summary>Sets the time, mix and bend direction of the specified keyframe.</summary>
-		public void SetFrame (int frameIndex, float time, float mix, int bendDirection) {
+		public void SetFrame (int frameIndex, float time, float mix, int bendDirection, bool stretch) {
 			frameIndex *= ENTRIES;
 			frames[frameIndex] = time;
 			frames[frameIndex + MIX] = mix;
 			frames[frameIndex + BEND_DIRECTION] = bendDirection;
+			frames[frameIndex + STRETCH] = stretch ? 1 : 0;
 		}
 
 		override public void Apply (Skeleton skeleton, float lastTime, float time, ExposedList<Event> firedEvents, float alpha, MixBlend blend, MixDirection direction) {
@@ -1245,10 +1246,12 @@ namespace Spine {
 				case MixBlend.Setup:
 					constraint.mix = constraint.data.mix;
 					constraint.bendDirection = constraint.data.bendDirection;
+					constraint.stretch = constraint.data.stretch;
 					return;
 				case MixBlend.First:
 					constraint.mix += (constraint.data.mix - constraint.mix) * alpha;
 					constraint.bendDirection = constraint.data.bendDirection;
+					constraint.stretch = constraint.data.stretch;
 					return;
 				}
 				return;
@@ -1257,11 +1260,19 @@ namespace Spine {
 			if (time >= frames[frames.Length - ENTRIES]) { // Time is after last frame.
 				if (blend == MixBlend.Setup) {
 					constraint.mix = constraint.data.mix + (frames[frames.Length + PREV_MIX] - constraint.data.mix) * alpha;
-					constraint.bendDirection = direction == MixDirection.Out ? constraint.data.bendDirection
-						: (int)frames[frames.Length + PREV_BEND_DIRECTION];
+					if (direction == MixDirection.Out) {
+						constraint.bendDirection = constraint.data.bendDirection;
+						constraint.stretch = constraint.data.stretch;
+					} else {
+						constraint.bendDirection = (int)frames[frames.Length + PREV_BEND_DIRECTION];
+						constraint.stretch = frames[frames.Length + PREV_STRETCH] != 0;
+					}
 				} else {
 					constraint.mix += (frames[frames.Length + PREV_MIX] - constraint.mix) * alpha;
-					if (direction == MixDirection.In) constraint.bendDirection = (int)frames[frames.Length + PREV_BEND_DIRECTION];
+					if (direction == MixDirection.In) {
+						constraint.bendDirection = (int)frames[frames.Length + PREV_BEND_DIRECTION];
+						constraint.stretch = frames[frames.Length + PREV_STRETCH] != 0;
+					}
 				}
 				return;
 			}

+ 1 - 1
spine-csharp/src/AnimationState.cs

@@ -334,10 +334,10 @@ namespace Spine {
 			// Mix between rotations using the direction of the shortest route on the first frame while detecting crosses.
 			float r1 = pose == MixBlend.Setup ? bone.data.rotation : bone.rotation;
 			float total, diff = r2 - r1;
+			diff -= (16384 - (int)(16384.499999999996 - diff / 360)) * 360;
 			if (diff == 0) {
 				total = timelinesRotation[i];
 			} else {
-				diff -= (16384 - (int)(16384.499999999996 - diff / 360)) * 360;
 				float lastTotal, lastDiff;
 				if (firstFrame) {
 					lastTotal = 0;

+ 55 - 17
spine-csharp/src/IkConstraint.cs

@@ -35,15 +35,43 @@ namespace Spine {
 		internal IkConstraintData data;
 		internal ExposedList<Bone> bones = new ExposedList<Bone>();
 		internal Bone target;
-		internal float mix;
 		internal int bendDirection;
+		internal bool stretch;
+		internal float mix;
 
 		public IkConstraintData Data { get { return data; } }
 		public int Order { get { return data.order; } }
-		public ExposedList<Bone> Bones { get { return bones; } }
-		public Bone Target { get { return target; } set { target = value; } }
-		public int BendDirection { get { return bendDirection; } set { bendDirection = value; } }
-		public float Mix { get { return mix; } set { mix = value; } }
+
+		/// <summary>The bones that will be modified by this IK constraint.</summary>
+		public ExposedList<Bone> Bones {
+			get { return bones; }
+		}
+
+		/// <summary>The bone that is the IK target.</summary>
+		public Bone Target {
+			get { return target; }
+			set { target = value; }
+		}
+
+		/// <summary>Controls the bend direction of the IK bones, either 1 or -1.</summary>
+		public int BendDirection {
+			get { return bendDirection; }
+			set { bendDirection = value; }
+		}
+
+		/// <summary>
+		/// When true, if the target is out of range, the parent bone is scaled on the X axis to reach it.
+		/// IF the parent bone has nonuniform scale, stretching is not applied.</summary>
+		public bool Stretch {
+			get { return stretch; }
+			set { stretch = value; }
+		}
+
+		/// <summary>A percentage (0-1) that controls the mix between the constrained and unconstrained rotations.</summary>
+		public float Mix {
+			get { return mix; }
+			set { mix = value; }
+		}
 
 		public IkConstraint (IkConstraintData data, Skeleton skeleton) {
 			if (data == null) throw new ArgumentNullException("data", "data cannot be null.");
@@ -51,6 +79,7 @@ namespace Spine {
 			this.data = data;
 			mix = data.mix;
 			bendDirection = data.bendDirection;
+			stretch = data.stretch;
 
 			bones = new ExposedList<Bone>(data.bones.Count);
 			foreach (BoneData boneData in data.bones)
@@ -68,10 +97,10 @@ namespace Spine {
 			ExposedList<Bone> bones = this.bones;
 			switch (bones.Count) {
 			case 1:
-				Apply(bones.Items[0], target.worldX, target.worldY, mix);
+				Apply(bones.Items[0], target.worldX, target.worldY, stretch, mix);
 				break;
 			case 2:
-				Apply(bones.Items[0], bones.Items[1], target.worldX, target.worldY, bendDirection, mix);
+				Apply(bones.Items[0], bones.Items[1], target.worldX, target.worldY, bendDirection, stretch, mix);
 				break;
 			}
 		}
@@ -82,7 +111,7 @@ namespace Spine {
 
 		/// <summary>Adjusts the bone rotation so the tip is as close to the target position as possible. The target is specified
 		/// in the world coordinate system.</summary>
-		static public void Apply (Bone bone, float targetX, float targetY, float alpha) {
+		static public void Apply (Bone bone, float targetX, float targetY, bool stretch, float alpha) {
 			if (!bone.appliedValid) bone.UpdateAppliedTransform();
 			Bone p = bone.parent;
 			float id = 1 / (p.a * p.d - p.b * p.c);
@@ -92,15 +121,21 @@ namespace Spine {
 			if (bone.ascaleX < 0) rotationIK += 180;
 			if (rotationIK > 180)
 				rotationIK -= 360;
-			else if (rotationIK < -180) rotationIK += 360;
-			bone.UpdateWorldTransform(bone.ax, bone.ay, bone.arotation + rotationIK * alpha, bone.ascaleX, bone.ascaleY, bone.ashearX,
+			else if (rotationIK < -180)
+				rotationIK += 360;
+			float sx = bone.ascaleX;
+			if (stretch) {
+				float b = bone.data.length * sx, dd = (float)Math.Sqrt(tx * tx + ty * ty);
+				if (dd > b && b > 0.0001f) sx *= (dd / b - 1) * alpha + 1;
+			}
+			bone.UpdateWorldTransform(bone.ax, bone.ay, bone.arotation + rotationIK * alpha, sx, bone.ascaleY, bone.ashearX,
 				bone.ashearY);
 		}
 
 		/// <summary>Adjusts the parent and child bone rotations so the tip of the child is as close to the target position as
 		/// possible. The target is specified in the world coordinate system.</summary>
 		/// <param name="child">A direct descendant of the parent bone.</param>
-		static public void Apply (Bone parent, Bone child, float targetX, float targetY, int bendDir, float alpha) {
+		static public void Apply (Bone parent, Bone child, float targetX, float targetY, int bendDir, bool stretch, float alpha) {
 			if (alpha == 0) {
 				child.UpdateWorldTransform ();
 				return;
@@ -108,7 +143,7 @@ namespace Spine {
 			//float px = parent.x, py = parent.y, psx = parent.scaleX, psy = parent.scaleY, csx = child.scaleX;
 			if (!parent.appliedValid) parent.UpdateAppliedTransform();
 			if (!child.appliedValid) child.UpdateAppliedTransform();
-			float px = parent.ax, py = parent.ay, psx = parent.ascaleX, psy = parent.ascaleY, csx = child.ascaleX;
+			float px = parent.ax, py = parent.ay, psx = parent.ascaleX, sx = psx, psy = parent.ascaleY, csx = child.ascaleX;
 			int os1, os2, s2;
 			if (psx < 0) {
 				psx = -psx;
@@ -144,17 +179,20 @@ namespace Spine {
 			c = pp.c;
 			d = pp.d;
 			float id = 1 / (a * d - b * c), x = targetX - pp.worldX, y = targetY - pp.worldY;
-			float tx = (x * d - y * b) * id - px, ty = (y * a - x * c) * id - py;
+			float tx = (x * d - y * b) * id - px, ty = (y * a - x * c) * id - py, dd = tx * tx + ty * ty;
 			x = cwx - pp.worldX;
 			y = cwy - pp.worldY;
 			float dx = (x * d - y * b) * id - px, dy = (y * a - x * c) * id - py;
 			float l1 = (float)Math.Sqrt(dx * dx + dy * dy), l2 = child.data.length * csx, a1, a2;
 			if (u) {
 				l2 *= psx;
-				float cos = (tx * tx + ty * ty - l1 * l1 - l2 * l2) / (2 * l1 * l2);
+				float cos = (dd - l1 * l1 - l2 * l2) / (2 * l1 * l2);
 				if (cos < -1)
 					cos = -1;
-				else if (cos > 1) cos = 1;
+				else if (cos > 1) {
+					cos = 1;
+					if (stretch && l1 + l2 > 0.0001f) sx *= ((float)Math.Sqrt(dd) / (l1 + l2) - 1) * alpha + 1;
+				}
 				a2 = (float)Math.Acos(cos) * bendDir;
 				a = l1 + l2 * cos;
 				b = l2 * (float)Math.Sin(a2);
@@ -162,7 +200,7 @@ namespace Spine {
 			} else {
 				a = psx * l2;
 				b = psy * l2;
-				float aa = a * a, bb = b * b, dd = tx * tx + ty * ty, ta = (float)Math.Atan2(ty, tx);
+				float aa = a * a, bb = b * b, ta = (float)Math.Atan2(ty, tx);
 				c = bb * l1 * l1 + aa * dd - aa * bb;
 				float c1 = -2 * bb * l1, c2 = bb - aa;
 				d = c1 * c1 - 4 * c2 * c;
@@ -215,7 +253,7 @@ namespace Spine {
 			if (a1 > 180)
 				a1 -= 360;
 			else if (a1 < -180) a1 += 360;
-			parent.UpdateWorldTransform(px, py, rotation + a1 * alpha, parent.scaleX, parent.ascaleY, 0, 0);
+			parent.UpdateWorldTransform(px, py, rotation + a1 * alpha, sx, parent.ascaleY, 0, 0);
 			rotation = child.arotation;
 			a2 = ((a2 + os) * MathUtils.RadDeg - child.ashearX) * s2 + os2 - rotation;
 			if (a2 > 180)

+ 9 - 0
spine-csharp/src/IkConstraintData.cs

@@ -39,6 +39,7 @@ namespace Spine {
 		internal List<BoneData> bones = new List<BoneData>();
 		internal BoneData target;
 		internal int bendDirection = 1;
+		internal bool stretch;
 		internal float mix = 1;
 
 		/// <summary>The IK constraint's name, which is unique within the skeleton.</summary>
@@ -68,6 +69,14 @@ namespace Spine {
 			set { bendDirection = value; }
 		}
 
+		/// <summary>
+		/// When true, if the target is out of range, the parent bone is scaled on the X axis to reach it. 
+		/// If the bone has local nonuniform scale, stretching is not applied.</summary>
+		public bool Stretch {
+			get { return stretch; }
+			set { stretch = value; }
+		}
+
 		public float Mix { get { return mix; } set { mix = value; } }
 
 		public IkConstraintData (string name) {

+ 1 - 0
spine-csharp/src/Skeleton.cs

@@ -309,6 +309,7 @@ namespace Spine {
 			for (int i = 0, n = ikConstraints.Count; i < n; i++) {
 				IkConstraint constraint = ikConstraintsItems[i];
 				constraint.bendDirection = constraint.data.bendDirection;
+				constraint.stretch = constraint.data.stretch;
 				constraint.mix = constraint.data.mix;
 			}
 

+ 2 - 1
spine-csharp/src/SkeletonBinary.cs

@@ -210,6 +210,7 @@ namespace Spine {
 				data.target = skeletonData.bones.Items[ReadVarint(input, true)];
 				data.mix = ReadFloat(input);
 				data.bendDirection = ReadSByte(input);
+				data.stretch = ReadBoolean(input);
 				skeletonData.ikConstraints.Add(data);
 			}
 
@@ -648,7 +649,7 @@ namespace Spine {
 				IkConstraintTimeline timeline = new IkConstraintTimeline(frameCount);
 				timeline.ikConstraintIndex = index;
 				for (int frameIndex = 0; frameIndex < frameCount; frameIndex++) {
-					timeline.SetFrame(frameIndex, ReadFloat(input), ReadFloat(input), ReadSByte(input));
+					timeline.SetFrame(frameIndex, ReadFloat(input), ReadFloat(input), ReadSByte(input), ReadBoolean(input));
 					if (frameIndex < frameCount - 1) ReadCurve(input, frameIndex, timeline);
 				}
 				timelines.Add(timeline);

+ 3 - 1
spine-csharp/src/SkeletonJson.cs

@@ -182,6 +182,7 @@ namespace Spine {
 					if (data.target == null) throw new Exception("Target bone not found: " + targetName);
 
 					data.bendDirection = GetBoolean(constraintMap, "bendPositive", true) ? 1 : -1;
+					data.stretch = GetBoolean(constraintMap, "stretch", false);
 					data.mix = GetFloat(constraintMap, "mix", 1);
 
 					skeletonData.ikConstraints.Add(data);
@@ -597,7 +598,8 @@ namespace Spine {
 						float time = (float)valueMap["time"];
 						float mix = GetFloat(valueMap, "mix", 1);
 						bool bendPositive = GetBoolean(valueMap, "bendPositive", true);
-						timeline.SetFrame(frameIndex, time, mix, bendPositive ? 1 : -1);
+						bool stretch = GetBoolean(valueMap, "stretch", false);
+						timeline.SetFrame(frameIndex, time, mix, bendPositive ? 1 : -1, stretch);
 						ReadCurve(valueMap, timeline, frameIndex);
 						frameIndex++;
 					}

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

@@ -30,6 +30,7 @@
 
 package com.esotericsoftware.spine;
 
+import java.lang.reflect.Field;
 import java.util.concurrent.atomic.AtomicInteger;
 
 import com.badlogic.gdx.Files.FileType;
@@ -803,7 +804,8 @@ public class AnimationStateTests {
 		expected.addAll(expectedArray);
 		stateData = new AnimationStateData(skeletonData);
 		state = new AnimationState(stateData);
-		state.trackEntryPool = new Pool<TrackEntry>() {
+
+		Pool trackEntryPool = new Pool<TrackEntry>() {
 			public TrackEntry obtain () {
 				TrackEntry entry = super.obtain();
 				entryCount++;
@@ -821,6 +823,14 @@ public class AnimationStateTests {
 				super.free(entry);
 			}
 		};
+		try {
+			Field field = state.getClass().getDeclaredField("trackEntryPool");
+			field.setAccessible(true);
+			field.set(state, trackEntryPool);
+		} catch (Exception ex) {
+			throw new RuntimeException(ex);
+		}
+
 		time = 0;
 		fail = false;
 		log(test + ": " + description);

+ 18 - 9
spine-libgdx/spine-libgdx/src/com/esotericsoftware/spine/Animation.java

@@ -1295,15 +1295,15 @@ public class Animation {
 		}
 	}
 
-	/** Changes an IK constraint's {@link IkConstraint#getMix()}, {@link IkConstraint#getBendDirection()}, and
-	 * {@link IkConstraint#getStretch()}. */
+	/** Changes an IK constraint's {@link IkConstraint#getMix()}, {@link IkConstraint#getBendDirection()},
+	 * {@link IkConstraint#getStretch()}, and {@link IkConstraint#getCompress()}. */
 	static public class IkConstraintTimeline extends CurveTimeline {
-		static public final int ENTRIES = 4;
-		static private final int PREV_TIME = -4, PREV_MIX = -3, PREV_BEND_DIRECTION = -2, PREV_STRETCH = -1;
-		static private final int MIX = 1, BEND_DIRECTION = 2, STRETCH = 3;
+		static public final int ENTRIES = 5;
+		static private final int PREV_TIME = -5, PREV_MIX = -4, PREV_BEND_DIRECTION = -3, PREV_COMPRESS = -2, PREV_STRETCH = -1;
+		static private final int MIX = 1, BEND_DIRECTION = 2, COMPRESS = 3, STRETCH = 4;
 
 		int ikConstraintIndex;
-		private final float[] frames; // time, mix, bendDirection, ...
+		private final float[] frames; // time, mix, bendDirection, compress, stretch, ...
 
 		public IkConstraintTimeline (int frameCount) {
 			super(frameCount);
@@ -1324,17 +1324,18 @@ public class Animation {
 			return ikConstraintIndex;
 		}
 
-		/** The time in seconds, mix, and bend direction for each key frame. */
+		/** The time in seconds, mix, bend direction, compress, and stretch for each key frame. */
 		public float[] getFrames () {
 			return frames;
 		}
 
-		/** Sets the time in seconds, mix, and bend direction for the specified key frame. */
-		public void setFrame (int frameIndex, float time, float mix, int bendDirection, boolean stretch) {
+		/** Sets the time in seconds, mix, bend direction, compress, and stretch for the specified key frame. */
+		public void setFrame (int frameIndex, float time, float mix, int bendDirection, boolean compress, boolean stretch) {
 			frameIndex *= ENTRIES;
 			frames[frameIndex] = time;
 			frames[frameIndex + MIX] = mix;
 			frames[frameIndex + BEND_DIRECTION] = bendDirection;
+			frames[frameIndex + COMPRESS] = compress ? 1 : 0;
 			frames[frameIndex + STRETCH] = stretch ? 1 : 0;
 		}
 
@@ -1348,11 +1349,13 @@ public class Animation {
 				case setup:
 					constraint.mix = constraint.data.mix;
 					constraint.bendDirection = constraint.data.bendDirection;
+					constraint.compress = constraint.data.compress;
 					constraint.stretch = constraint.data.stretch;
 					return;
 				case first:
 					constraint.mix += (constraint.data.mix - constraint.mix) * alpha;
 					constraint.bendDirection = constraint.data.bendDirection;
+					constraint.compress = constraint.data.compress;
 					constraint.stretch = constraint.data.stretch;
 				}
 				return;
@@ -1363,15 +1366,18 @@ public class Animation {
 					constraint.mix = constraint.data.mix + (frames[frames.length + PREV_MIX] - constraint.data.mix) * alpha;
 					if (direction == out) {
 						constraint.bendDirection = constraint.data.bendDirection;
+						constraint.compress = constraint.data.compress;
 						constraint.stretch = constraint.data.stretch;
 					} else {
 						constraint.bendDirection = (int)frames[frames.length + PREV_BEND_DIRECTION];
+						constraint.compress = frames[frames.length + PREV_COMPRESS] != 0;
 						constraint.stretch = frames[frames.length + PREV_STRETCH] != 0;
 					}
 				} else {
 					constraint.mix += (frames[frames.length + PREV_MIX] - constraint.mix) * alpha;
 					if (direction == in) {
 						constraint.bendDirection = (int)frames[frames.length + PREV_BEND_DIRECTION];
+						constraint.compress = frames[frames.length + PREV_COMPRESS] != 0;
 						constraint.stretch = frames[frames.length + PREV_STRETCH] != 0;
 					}
 				}
@@ -1388,15 +1394,18 @@ public class Animation {
 				constraint.mix = constraint.data.mix + (mix + (frames[frame + MIX] - mix) * percent - constraint.data.mix) * alpha;
 				if (direction == out) {
 					constraint.bendDirection = constraint.data.bendDirection;
+					constraint.compress = constraint.data.compress;
 					constraint.stretch = constraint.data.stretch;
 				} else {
 					constraint.bendDirection = (int)frames[frame + PREV_BEND_DIRECTION];
+					constraint.compress = frames[frame + PREV_COMPRESS] != 0;
 					constraint.stretch = frames[frame + PREV_STRETCH] != 0;
 				}
 			} else {
 				constraint.mix += (mix + (frames[frame + MIX] - mix) * percent - constraint.mix) * alpha;
 				if (direction == in) {
 					constraint.bendDirection = (int)frames[frame + PREV_BEND_DIRECTION];
+					constraint.compress = frames[frame + PREV_COMPRESS] != 0;
 					constraint.stretch = frames[frame + PREV_STRETCH] != 0;
 				}
 			}

+ 112 - 80
spine-libgdx/spine-libgdx/src/com/esotericsoftware/spine/AnimationState.java

@@ -38,6 +38,7 @@ import com.badlogic.gdx.utils.IntArray;
 import com.badlogic.gdx.utils.IntSet;
 import com.badlogic.gdx.utils.Pool;
 import com.badlogic.gdx.utils.Pool.Poolable;
+
 import com.esotericsoftware.spine.Animation.AttachmentTimeline;
 import com.esotericsoftware.spine.Animation.DrawOrderTimeline;
 import com.esotericsoftware.spine.Animation.MixBlend;
@@ -62,20 +63,20 @@ public class AnimationState {
 	/** 1) This is the first timeline to set this property.<br>
 	 * 2) The next track entry to be applied does have a timeline to set this property.<br>
 	 * 3) The next track entry after that one does not have a timeline to set this property.<br>
-	 * Result: Mix from the setup pose to the timeline pose, but avoid the "dipping" problem by not using the mix percentage. This
-	 * means the timeline pose won't mix out toward the setup pose. A subsequent timeline will set this property using a mix. */
-	static private final int DIP = 2;
+	 * Result: Mix from the setup pose to the timeline pose, but do not mix out. This avoids "dipping" when crossfading animations
+	 * that key the same property. A subsequent timeline will set this property using a mix. */
+	static private final int HOLD = 2;
 	/** 1) This is the first timeline to set this property.<br>
 	 * 2) The next track entry to be applied does have a timeline to set this property.<br>
 	 * 3) The next track entry after that one does have a timeline to set this property.<br>
-	 * 4) timelineDipMix stores the first subsequent track entry that does not have a timeline to set this property.<br>
-	 * Result: This is the same as DIP except the mix percentage from the timelineDipMix track entry is used. This handles when
-	 * more than 2 track entries in a row have a timeline which sets the same property.<br>
-	 * Eg, A -> B -> C -> D where A, B, and C have a timeline to set the same property, but D does not. When A is applied, A's mix
-	 * percentage is not used to avoid dipping, however a later track entry (D, the first entry without a timeline which sets the
-	 * property) is actually mixing out A (which affects B and C). Without using D's mix percentage, A would be applied fully until
-	 * mixed out, causing snapping. */
-	static private final int DIP_MIX = 3;
+	 * 4) timelineHoldMix stores the first subsequent track entry that does not have a timeline to set this property.<br>
+	 * Result: The same as HOLD except the mix percentage from the timelineHoldMix track entry is used. This handles when more than
+	 * 2 track entries in a row have a timeline that sets the same property.<br>
+	 * Eg, A -> B -> C -> D where A, B, and C have a timeline setting same property, but D does not. When A is applied, to avoid
+	 * "dipping" A is not mixed out, however D (the first entry that doesn't set the property) mixing in is used to mix out A
+	 * (which affects B and C). Without using D to mix out, A would be applied fully until mixing completes, then snap into
+	 * place. */
+	static private final int HOLD_MIX = 3;
 
 	private AnimationStateData data;
 	final Array<TrackEntry> tracks = new Array();
@@ -83,11 +84,10 @@ public class AnimationState {
 	final Array<AnimationStateListener> listeners = new Array();
 	private final EventQueue queue = new EventQueue();
 	private final IntSet propertyIDs = new IntSet();
-	private final Array<TrackEntry> mixingTo = new Array();
 	boolean animationsChanged;
 	private float timeScale = 1;
 
-	Pool<TrackEntry> trackEntryPool = new Pool() {
+	final Pool<TrackEntry> trackEntryPool = new Pool() {
 		protected Object newObject () {
 			return new TrackEntry();
 		}
@@ -147,6 +147,7 @@ public class AnimationState {
 				// End mixing from entries once all have completed.
 				TrackEntry from = current.mixingFrom;
 				current.mixingFrom = null;
+				if (from != null) from.mixingTo = null;
 				while (from != null) {
 					queue.end(from);
 					from = from.mixingFrom;
@@ -174,6 +175,7 @@ public class AnimationState {
 			// Require totalAlpha == 0 to ensure mixing is complete, unless mixDuration == 0 (the transition is a single frame).
 			if (from.totalAlpha == 0 || to.mixDuration == 0) {
 				to.mixingFrom = from.mixingFrom;
+				if (from.mixingFrom != null) from.mixingFrom.mixingTo = to;
 				to.interruptAlpha = from.interruptAlpha;
 				queue.end(from);
 			}
@@ -213,11 +215,11 @@ public class AnimationState {
 			float animationLast = current.animationLast, animationTime = current.getAnimationTime();
 			int timelineCount = current.animation.timelines.size;
 			Object[] timelines = current.animation.timelines.items;
-			if (mix == 1 || blend == MixBlend.add) {
+			if (i == 0 && (mix == 1 || blend == MixBlend.add)) {
 				for (int ii = 0; ii < timelineCount; ii++)
 					((Timeline)timelines[ii]).apply(skeleton, animationLast, animationTime, events, mix, blend, MixDirection.in);
 			} else {
-				int[] timelineData = current.timelineData.items;
+				int[] timelineMode = current.timelineMode.items;
 
 				boolean firstFrame = current.timelinesRotation.size == 0;
 				if (firstFrame) current.timelinesRotation.setSize(timelineCount << 1);
@@ -225,7 +227,7 @@ public class AnimationState {
 
 				for (int ii = 0; ii < timelineCount; ii++) {
 					Timeline timeline = (Timeline)timelines[ii];
-					MixBlend timelineBlend = timelineData[ii] == SUBSEQUENT ? blend : MixBlend.setup;
+					MixBlend timelineBlend = timelineMode[ii] == SUBSEQUENT ? blend : MixBlend.setup;
 					if (timeline instanceof RotateTimeline) {
 						applyRotateTimeline(timeline, skeleton, animationTime, mix, timelineBlend, timelinesRotation, ii << 1,
 							firstFrame);
@@ -262,14 +264,14 @@ public class AnimationState {
 		float animationLast = from.animationLast, animationTime = from.getAnimationTime();
 		int timelineCount = from.animation.timelines.size;
 		Object[] timelines = from.animation.timelines.items;
-		float alphaDip = from.alpha * to.interruptAlpha, alphaMix = alphaDip * (1 - mix);
+		float alphaHold = from.alpha * to.interruptAlpha, alphaMix = alphaHold * (1 - mix);
 
 		if (blend == MixBlend.add) {
 			for (int i = 0; i < timelineCount; i++)
 				((Timeline)timelines[i]).apply(skeleton, animationLast, animationTime, events, alphaMix, blend, MixDirection.out);
 		} else {
-			int[] timelineData = from.timelineData.items;
-			Object[] timelineDipMix = from.timelineDipMix.items;
+			int[] timelineMode = from.timelineMode.items;
+			Object[] timelineHoldMix = from.timelineHoldMix.items;
 
 			boolean firstFrame = from.timelinesRotation.size == 0;
 			if (firstFrame) from.timelinesRotation.setSize(timelineCount << 1);
@@ -280,7 +282,7 @@ public class AnimationState {
 				Timeline timeline = (Timeline)timelines[i];
 				MixBlend timelineBlend;
 				float alpha;
-				switch (timelineData[i]) {
+				switch (timelineMode[i]) {
 				case SUBSEQUENT:
 					if (!attachments && timeline instanceof AttachmentTimeline) continue;
 					if (!drawOrder && timeline instanceof DrawOrderTimeline) continue;
@@ -291,14 +293,14 @@ public class AnimationState {
 					timelineBlend = MixBlend.setup;
 					alpha = alphaMix;
 					break;
-				case DIP:
+				case HOLD:
 					timelineBlend = MixBlend.setup;
-					alpha = alphaDip;
+					alpha = alphaHold;
 					break;
 				default:
 					timelineBlend = MixBlend.setup;
-					TrackEntry dipMix = (TrackEntry)timelineDipMix[i];
-					alpha = alphaDip * Math.max(0, 1 - dipMix.mixTime / dipMix.mixDuration);
+					TrackEntry holdMix = (TrackEntry)timelineHoldMix[i];
+					alpha = alphaHold * Math.max(0, 1 - holdMix.mixTime / holdMix.mixDuration);
 					break;
 				}
 				from.totalAlpha += alpha;
@@ -356,10 +358,10 @@ public class AnimationState {
 		// Mix between rotations using the direction of the shortest route on the first frame.
 		float r1 = blend == MixBlend.setup ? bone.data.rotation : bone.rotation;
 		float total, diff = r2 - r1;
+		diff -= (16384 - (int)(16384.499999999996 - diff / 360)) * 360;
 		if (diff == 0)
 			total = timelinesRotation[i];
 		else {
-			diff -= (16384 - (int)(16384.499999999996 - diff / 360)) * 360;
 			float lastTotal, lastDiff;
 			if (firstFrame) {
 				lastTotal = 0;
@@ -448,6 +450,7 @@ public class AnimationState {
 			if (from == null) break;
 			queue.end(from);
 			entry.mixingFrom = null;
+			entry.mixingTo = null;
 			entry = from;
 		}
 
@@ -463,6 +466,7 @@ public class AnimationState {
 		if (from != null) {
 			if (interrupt) queue.interrupt(from);
 			current.mixingFrom = from;
+			from.mixingTo = current;
 			current.mixTime = 0;
 
 			// Store the interrupted mix percentage.
@@ -625,6 +629,7 @@ public class AnimationState {
 		entry.trackIndex = trackIndex;
 		entry.animation = animation;
 		entry.loop = loop;
+		entry.holdPrevious = false;
 
 		entry.eventThreshold = 0;
 		entry.attachmentThreshold = 0;
@@ -662,15 +667,67 @@ public class AnimationState {
 		animationsChanged = false;
 
 		IntSet propertyIDs = this.propertyIDs;
-		propertyIDs.clear();
-		Array<TrackEntry> mixingTo = this.mixingTo;
+		propertyIDs.clear(2048);
 
 		for (int i = 0, n = tracks.size; i < n; i++) {
 			TrackEntry entry = tracks.get(i);
-			if (entry != null && (i == 0 || entry.mixBlend != MixBlend.add)) entry.setTimelineData(null, mixingTo, propertyIDs);
+			if (entry == null) continue;
+			// Move to last entry, then iterate in reverse (the order animations are applied).
+			while (entry.mixingFrom != null)
+				entry = entry.mixingFrom;
+			do {
+				if (entry.mixingTo == null || entry.mixBlend != MixBlend.add) setTimelineModes(entry);
+				entry = entry.mixingTo;
+			} while (entry != null);
 		}
 	}
 
+	private void setTimelineModes (TrackEntry entry) {
+		TrackEntry to = entry.mixingTo;
+		Object[] timelines = entry.animation.timelines.items;
+		int timelinesCount = entry.animation.timelines.size;
+		int[] timelineMode = entry.timelineMode.setSize(timelinesCount);
+		entry.timelineHoldMix.clear();
+		Object[] timelineHoldMix = entry.timelineHoldMix.setSize(timelinesCount);
+		IntSet propertyIDs = this.propertyIDs;
+
+		if (to != null && to.holdPrevious) {
+			for (int i = 0; i < timelinesCount; i++) {
+				propertyIDs.add(((Timeline)timelines[i]).getPropertyId());
+				timelineMode[i] = HOLD;
+			}
+			return;
+		}
+
+		outer:
+		for (int i = 0; i < timelinesCount; i++) {
+			int id = ((Timeline)timelines[i]).getPropertyId();
+			if (!propertyIDs.add(id))
+				timelineMode[i] = SUBSEQUENT;
+			else if (to == null || !hasTimeline(to, id))
+				timelineMode[i] = FIRST;
+			else {
+				for (TrackEntry next = to.mixingTo; next != null; next = next.mixingTo) {
+					if (hasTimeline(next, id)) continue;
+					if (next.mixDuration > 0) {
+						timelineMode[i] = HOLD_MIX;
+						timelineHoldMix[i] = next;
+						continue outer;
+					}
+					break;
+				}
+				timelineMode[i] = HOLD;
+			}
+		}
+	}
+
+	private boolean hasTimeline (TrackEntry entry, int id) {
+		Object[] timelines = entry.animation.timelines.items;
+		for (int i = 0, n = entry.animation.timelines.size; i < n; i++)
+			if (((Timeline)timelines[i]).getPropertyId() == id) return true;
+		return false;
+	}
+
 	/** Returns the track entry for the animation currently playing on the track, or null if no animation is currently playing. */
 	public TrackEntry getCurrent (int trackIndex) {
 		if (trackIndex >= tracks.size) return null;
@@ -744,74 +801,30 @@ public class AnimationState {
 	 * References to a track entry must not be kept after the {@link AnimationStateListener#dispose(TrackEntry)} event occurs. */
 	static public class TrackEntry implements Poolable {
 		Animation animation;
-		TrackEntry next, mixingFrom;
+		TrackEntry next, mixingFrom, mixingTo;
 		AnimationStateListener listener;
 		int trackIndex;
-		boolean loop;
+		boolean loop, holdPrevious;
 		float eventThreshold, attachmentThreshold, drawOrderThreshold;
 		float animationStart, animationEnd, animationLast, nextAnimationLast;
 		float delay, trackTime, trackLast, nextTrackLast, trackEnd, timeScale;
 		float alpha, mixTime, mixDuration, interruptAlpha, totalAlpha;
 		MixBlend mixBlend = MixBlend.replace;
-		final IntArray timelineData = new IntArray();
-		final Array<TrackEntry> timelineDipMix = new Array();
+		final IntArray timelineMode = new IntArray();
+		final Array<TrackEntry> timelineHoldMix = new Array();
 		final FloatArray timelinesRotation = new FloatArray();
 
 		public void reset () {
 			next = null;
 			mixingFrom = null;
+			mixingTo = null;
 			animation = null;
 			listener = null;
-			timelineData.clear();
-			timelineDipMix.clear();
+			timelineMode.clear();
+			timelineHoldMix.clear();
 			timelinesRotation.clear();
 		}
 
-		/** @param to May be null. */
-		TrackEntry setTimelineData (TrackEntry to, Array<TrackEntry> mixingToArray, IntSet propertyIDs) {
-			if (to != null) mixingToArray.add(to);
-			TrackEntry lastEntry = mixingFrom != null ? mixingFrom.setTimelineData(this, mixingToArray, propertyIDs) : this;
-			if (to != null) mixingToArray.pop();
-
-			Object[] mixingTo = mixingToArray.items;
-			int mixingToLast = mixingToArray.size - 1;
-			Object[] timelines = animation.timelines.items;
-			int timelinesCount = animation.timelines.size;
-			int[] timelineData = this.timelineData.setSize(timelinesCount);
-			timelineDipMix.clear();
-			Object[] timelineDipMix = this.timelineDipMix.setSize(timelinesCount);
-			outer:
-			for (int i = 0; i < timelinesCount; i++) {
-				int id = ((Timeline)timelines[i]).getPropertyId();
-				if (!propertyIDs.add(id))
-					timelineData[i] = SUBSEQUENT;
-				else if (to == null || !to.hasTimeline(id))
-					timelineData[i] = FIRST;
-				else {
-					for (int ii = mixingToLast; ii >= 0; ii--) {
-						TrackEntry entry = (TrackEntry)mixingTo[ii];
-						if (!entry.hasTimeline(id)) {
-							if (entry.mixDuration > 0) {
-								timelineData[i] = DIP_MIX;
-								timelineDipMix[i] = entry;
-								continue outer;
-							}
-							break;
-						}
-					}
-					timelineData[i] = DIP;
-				}
-			}
-			return lastEntry;
-		}
-
-		private boolean hasTimeline (int id) {
-			Object[] timelines = animation.timelines.items;
-			for (int i = 0, n = animation.timelines.size; i < n; i++)
-				if (((Timeline)timelines[i]).getPropertyId() == id) return true;
-			return false;
-		}
-
 		/** The index of the track where this track entry is either current or queued.
 		 * <p>
 		 * See {@link AnimationState#getCurrent(int)}. */
@@ -1054,6 +1067,25 @@ public class AnimationState {
 			return mixingFrom;
 		}
 
+		public void setHoldPrevious (boolean holdPrevious) {
+			this.holdPrevious = holdPrevious;
+		}
+
+		/** If true, when mixing from the previous animation to this animation, the previous animation is applied as normal instead
+		 * of being mixed out.
+		 * <p>
+		 * When mixing between animations that key the same property, if a lower track also keys that property then the value will
+		 * briefly dip toward the lower track value during the mix. This happens because the first animation mixes from 100% to 0%
+		 * while the second animation mixes from 0% to 100%. Setting <code>holdPrevious</code> to true applies the first animation
+		 * at 100% during the mix so the lower track value is overwritten. Such dipping does not occur on the lowest track which
+		 * keys the property, only when a higher track also keys the property.
+		 * <p>
+		 * Snapping will occur if <code>holdPrevious</code> is true and this animation does not key all the same properties as the
+		 * previous animation. */
+		public boolean getHoldPrevious () {
+			return holdPrevious;
+		}
+
 		/** Resets the rotation directions for mixing this entry's rotate timelines. This can be useful to avoid bones rotating the
 		 * long way around when using {@link #alpha} and starting animations on other tracks.
 		 * <p>
@@ -1165,7 +1197,7 @@ public class AnimationState {
 		start, interrupt, end, dispose, complete, event
 	}
 
-	/** The interface which can be implemented to receive TrackEntry events.
+	/** The interface to implement for receiving TrackEntry events.
 	 * <p>
 	 * See TrackEntry {@link TrackEntry#setListener(AnimationStateListener)} and AnimationState
 	 * {@link AnimationState#addListener(AnimationStateListener)}. */

+ 27 - 11
spine-libgdx/spine-libgdx/src/com/esotericsoftware/spine/IkConstraint.java

@@ -43,7 +43,7 @@ public class IkConstraint implements Constraint {
 	final Array<Bone> bones;
 	Bone target;
 	int bendDirection;
-	boolean stretch;
+	boolean compress, stretch;
 	float mix = 1;
 
 	public IkConstraint (IkConstraintData data, Skeleton skeleton) {
@@ -52,6 +52,7 @@ public class IkConstraint implements Constraint {
 		this.data = data;
 		mix = data.mix;
 		bendDirection = data.bendDirection;
+		compress = data.compress;
 		stretch = data.stretch;
 
 		bones = new Array(data.bones.size);
@@ -71,6 +72,7 @@ public class IkConstraint implements Constraint {
 		target = skeleton.bones.get(constraint.target.data.index);
 		mix = constraint.mix;
 		bendDirection = constraint.bendDirection;
+		compress = constraint.compress;
 		stretch = constraint.stretch;
 	}
 
@@ -84,7 +86,7 @@ public class IkConstraint implements Constraint {
 		Array<Bone> bones = this.bones;
 		switch (bones.size) {
 		case 1:
-			apply(bones.first(), target.worldX, target.worldY, stretch, mix);
+			apply(bones.first(), target.worldX, target.worldY, compress, stretch, data.uniform, mix);
 			break;
 		case 2:
 			apply(bones.first(), bones.get(1), target.worldX, target.worldY, bendDirection, stretch, mix);
@@ -128,8 +130,17 @@ public class IkConstraint implements Constraint {
 		this.bendDirection = bendDirection;
 	}
 
-	/** When true, if the target is out of range, the parent bone is scaled on the X axis to reach it. If the parent bone has local
-	 * nonuniform scale, stretching is not applied. */
+	/** When true and only a single bone is being constrained, if the target is too close, the bone is scaled to reach it. */
+	public boolean getCompress () {
+		return compress;
+	}
+
+	public void setCompress (boolean compress) {
+		this.compress = compress;
+	}
+
+	/** When true, if the target is out of range, the parent bone is scaled to reach it. If more than one bone is being constrained
+	 * and the parent bone has local nonuniform scale, stretch is not applied. */
 	public boolean getStretch () {
 		return stretch;
 	}
@@ -148,7 +159,8 @@ public class IkConstraint implements Constraint {
 	}
 
 	/** Applies 1 bone IK. The target is specified in the world coordinate system. */
-	static public void apply (Bone bone, float targetX, float targetY, boolean stretch, float alpha) {
+	static public void apply (Bone bone, float targetX, float targetY, boolean compress, boolean stretch, boolean uniform,
+		float alpha) {
 		if (!bone.appliedValid) bone.updateAppliedTransform();
 		Bone p = bone.parent;
 		float id = 1 / (p.a * p.d - p.b * p.c);
@@ -158,14 +170,18 @@ public class IkConstraint implements Constraint {
 		if (bone.ascaleX < 0) rotationIK += 180;
 		if (rotationIK > 180)
 			rotationIK -= 360;
-		else if (rotationIK < -180) rotationIK += 360;
-		float sx = bone.ascaleX;
-		if (stretch) {
+		else if (rotationIK < -180) //
+			rotationIK += 360;
+		float sx = bone.ascaleX, sy = bone.ascaleY;
+		if (compress || stretch) {
 			float b = bone.data.length * sx, dd = (float)Math.sqrt(tx * tx + ty * ty);
-			if (dd > b && b > 0.0001f) sx *= (dd / b - 1) * alpha + 1;
+			if ((compress && dd < b) || (stretch && dd > b) && b > 0.0001f) {
+				float s = (dd / b - 1) * alpha + 1;
+				sx *= s;
+				if (uniform) sy *= s;
+			}
 		}
-		bone.updateWorldTransform(bone.ax, bone.ay, bone.arotation + rotationIK * alpha, sx, bone.ascaleY, bone.ashearX,
-			bone.ashearY);
+		bone.updateWorldTransform(bone.ax, bone.ay, bone.arotation + rotationIK * alpha, sx, sy, bone.ashearX, bone.ashearY);
 	}
 
 	/** Applies 2 bone IK. The target is specified in the world coordinate system.

+ 27 - 8
spine-libgdx/spine-libgdx/src/com/esotericsoftware/spine/IkConstraintData.java

@@ -41,7 +41,7 @@ public class IkConstraintData {
 	final Array<BoneData> bones = new Array();
 	BoneData target;
 	int bendDirection = 1;
-	boolean stretch;
+	boolean compress, stretch, uniform;
 	float mix = 1;
 
 	public IkConstraintData (String name) {
@@ -78,6 +78,15 @@ public class IkConstraintData {
 		this.target = target;
 	}
 
+	/** A percentage (0-1) that controls the mix between the constrained and unconstrained rotations. */
+	public float getMix () {
+		return mix;
+	}
+
+	public void setMix (float mix) {
+		this.mix = mix;
+	}
+
 	/** Controls the bend direction of the IK bones, either 1 or -1. */
 	public int getBendDirection () {
 		return bendDirection;
@@ -87,8 +96,17 @@ public class IkConstraintData {
 		this.bendDirection = bendDirection;
 	}
 
-	/** When true, if the target is out of range, the parent bone is scaled on the X axis to reach it. If the parent bone has local
-	 * nonuniform scale, stretching is not applied. */
+	/** When true and only a single bone is being constrained, if the target is too close, the bone is scaled to reach it. */
+	public boolean getCompress () {
+		return compress;
+	}
+
+	public void setCompress (boolean compress) {
+		this.compress = compress;
+	}
+
+	/** When true, if the target is out of range, the parent bone is scaled to reach it. If more than one bone is being constrained
+	 * and the parent bone has local nonuniform scale, stretch is not applied. */
 	public boolean getStretch () {
 		return stretch;
 	}
@@ -97,13 +115,14 @@ public class IkConstraintData {
 		this.stretch = stretch;
 	}
 
-	/** A percentage (0-1) that controls the mix between the constrained and unconstrained rotations. */
-	public float getMix () {
-		return mix;
+	/** When true, only a single bone is being constrained, and {@link #getCompress()} or {@link #getStretch()} is used, the bone
+	 * is scaled on both the X and Y axes. */
+	public boolean getUniform () {
+		return uniform;
 	}
 
-	public void setMix (float mix) {
-		this.mix = mix;
+	public void setUniform (boolean uniform) {
+		this.uniform = uniform;
 	}
 
 	public String toString () {

+ 2 - 1
spine-libgdx/spine-libgdx/src/com/esotericsoftware/spine/Skeleton.java

@@ -394,9 +394,10 @@ public class Skeleton {
 		Array<IkConstraint> ikConstraints = this.ikConstraints;
 		for (int i = 0, n = ikConstraints.size; i < n; i++) {
 			IkConstraint constraint = ikConstraints.get(i);
+			constraint.mix = constraint.data.mix;
 			constraint.bendDirection = constraint.data.bendDirection;
+			constraint.compress = constraint.data.compress;
 			constraint.stretch = constraint.data.stretch;
-			constraint.mix = constraint.data.mix;
 		}
 
 		Array<TransformConstraint> transformConstraints = this.transformConstraints;

+ 5 - 2
spine-libgdx/spine-libgdx/src/com/esotericsoftware/spine/SkeletonBinary.java

@@ -232,7 +232,9 @@ public class SkeletonBinary {
 				data.target = skeletonData.bones.get(input.readInt(true));
 				data.mix = input.readFloat();
 				data.bendDirection = input.readByte();
+				data.compress = input.readBoolean();
 				data.stretch = input.readBoolean();
+				data.uniform = input.readBoolean();
 				skeletonData.ikConstraints.add(data);
 			}
 
@@ -307,7 +309,7 @@ public class SkeletonBinary {
 				data.intValue = input.readInt(false);
 				data.floatValue = input.readFloat();
 				data.stringValue = input.readString();
-				data.audioPath = input.readString(); 
+				data.audioPath = input.readString();
 				skeletonData.events.add(data);
 			}
 
@@ -661,7 +663,8 @@ public class SkeletonBinary {
 				IkConstraintTimeline timeline = new IkConstraintTimeline(frameCount);
 				timeline.ikConstraintIndex = index;
 				for (int frameIndex = 0; frameIndex < frameCount; frameIndex++) {
-					timeline.setFrame(frameIndex, input.readFloat(), input.readFloat(), input.readByte(), input.readBoolean());
+					timeline.setFrame(frameIndex, input.readFloat(), input.readFloat(), input.readByte(), input.readBoolean(),
+						input.readBoolean());
 					if (frameIndex < frameCount - 1) readCurve(input, frameIndex, timeline);
 				}
 				timelines.add(timeline);

+ 5 - 2
spine-libgdx/spine-libgdx/src/com/esotericsoftware/spine/SkeletonJson.java

@@ -185,9 +185,11 @@ public class SkeletonJson {
 			data.target = skeletonData.findBone(targetName);
 			if (data.target == null) throw new SerializationException("IK target bone not found: " + targetName);
 
+			data.mix = constraintMap.getFloat("mix", 1);
 			data.bendDirection = constraintMap.getBoolean("bendPositive", true) ? 1 : -1;
+			data.compress = constraintMap.getBoolean("compress", false);
 			data.stretch = constraintMap.getBoolean("stretch", false);
-			data.mix = constraintMap.getFloat("mix", 1);
+			data.uniform = constraintMap.getBoolean("uniform", false);
 
 			skeletonData.ikConstraints.add(data);
 		}
@@ -569,7 +571,8 @@ public class SkeletonJson {
 			int frameIndex = 0;
 			for (JsonValue valueMap = constraintMap.child; valueMap != null; valueMap = valueMap.next) {
 				timeline.setFrame(frameIndex, valueMap.getFloat("time"), valueMap.getFloat("mix", 1),
-					valueMap.getBoolean("bendPositive", true) ? 1 : -1, valueMap.getBoolean("stretch", false));
+					valueMap.getBoolean("bendPositive", true) ? 1 : -1, valueMap.getBoolean("compress", false),
+					valueMap.getBoolean("stretch", false));
 				readCurve(valueMap, timeline, frameIndex);
 				frameIndex++;
 			}

+ 1 - 0
spine-unity/Assets/Spine/Editor/spine-unity/Editor/SkeletonDebugWindow.cs

@@ -375,6 +375,7 @@ namespace Spine.Unity.Editor {
 										EditorGUI.BeginChangeCheck();
 										c.Mix = EditorGUILayout.Slider("Mix", c.Mix, MixMin, MixMax);
 										c.BendDirection = EditorGUILayout.Toggle(SpineInspectorUtility.TempContent("Bend Clockwise", tooltip: "IkConstraint.BendDirection == 1 if clockwise; -1 if counterclockwise."), c.BendDirection > 0) ? 1 : -1;
+										c.Stretch = EditorGUILayout.Toggle(SpineInspectorUtility.TempContent("Stretch", tooltip: "Stretch the parent bone when the target is out of range. Not applied when parent bone has nonuniform scale."), c.Stretch);
 										if (EditorGUI.EndChangeCheck())	requireRepaint = true;
 
 										EditorGUILayout.Space();

+ 1 - 1
spine-unity/Assets/Spine/Editor/spine-unity/Editor/SpineEditorUtilities.cs

@@ -905,7 +905,7 @@ namespace Spine.Unity.Editor {
 						}
 
 						if (!match)
-							Debug.LogWarningFormat("Skeleton '{0}' (exported with Spine {1}) may be incompatible with your runtime version: spine-unity v{2}", asset.name, rawVersion, primaryRuntimeVersionDebugString);
+							Debug.LogWarningFormat("Skeleton '{0}' (exported with Spine {1}) may be incompatible with your runtime version: spine-csharp v{2}", asset.name, rawVersion, primaryRuntimeVersionDebugString);
 					}
 				}
 

+ 1 - 2
spine-unity/Assets/Spine/Runtime/spine-unity/Modules/Ghost/SkeletonGhost.cs

@@ -45,10 +45,9 @@ namespace Spine.Unity.Modules {
 		public bool ghostingEnabled = true;
 		[Tooltip("The time between invididual ghost pieces being spawned.")]
 		[UnityEngine.Serialization.FormerlySerializedAs("spawnRate")]
-		public float spawnInterval = 0.05f;
+		public float spawnInterval = 1f/30f;
 		[Tooltip("Maximum number of ghosts that can exist at a time. If the fade speed is not fast enough, the oldest ghost will immediately disappear to enforce the maximum number.")]
 		public int maximumGhosts = 10;
-		[Tooltip("Fadespeed 1 means it will take 1 second for a piece to fade out. 2 means it will take 1/2 second. 10 means it will take 1/10 of a second.")]
 		public float fadeSpeed = 10;
 
 		[Header("Rendering")]

+ 37 - 41
spine-unity/Assets/Spine/Runtime/spine-unity/Modules/Ghost/SkeletonGhostRenderer.cs

@@ -35,17 +35,23 @@ using System.Collections;
 
 namespace Spine.Unity.Modules {
 	public class SkeletonGhostRenderer : MonoBehaviour {
+		static readonly Color32 TransparentBlack = new Color32(0, 0, 0, 0);
+		const string colorPropertyName = "_Color";
 
-		public float fadeSpeed = 10;
-
-		Color32[] colors;
-		Color32 black = new Color32(0, 0, 0, 0);
+		float fadeSpeed = 10;
+		Color32 startColor;
 		MeshFilter meshFilter;
 		MeshRenderer meshRenderer;
 
+		MaterialPropertyBlock mpb;
+		int colorId;
+
 		void Awake () {
 			meshRenderer = gameObject.AddComponent<MeshRenderer>();
 			meshFilter = gameObject.AddComponent<MeshFilter>();
+
+			colorId = Shader.PropertyToID(colorPropertyName);
+			mpb = new MaterialPropertyBlock();
 		}
 
 		public void Initialize (Mesh mesh, Material[] materials, Color32 color, bool additive, float speed, int sortingLayerID, int sortingOrder) {
@@ -56,12 +62,9 @@ namespace Spine.Unity.Modules {
 			meshRenderer.sortingLayerID = sortingLayerID;
 			meshRenderer.sortingOrder = sortingOrder;
 			meshFilter.sharedMesh = Instantiate(mesh);
-			colors = meshFilter.sharedMesh.colors32;
-
-			if ((color.a + color.r + color.g + color.b) > 0) {
-				for (int i = 0; i < colors.Length; i++)
-					colors[i] = color;
-			}
+			startColor = color;
+			mpb.SetColor(colorId, color);
+			meshRenderer.SetPropertyBlock(mpb);
 
 			fadeSpeed = speed;
 
@@ -72,19 +75,17 @@ namespace Spine.Unity.Modules {
 		}
 
 		IEnumerator Fade () {
-			Color32 c;
-			for (int t = 0; t < 500; t++) {
-				bool breakout = true;
-				for (int i = 0; i < colors.Length; i++) {
-					c = colors[i];
-					if (c.a > 0)
-						breakout = false;
-
-					colors[i] = Color32.Lerp(c, black, Time.deltaTime * fadeSpeed);
-				}
-				meshFilter.sharedMesh.colors32 = colors;
-
-				if (breakout)
+			Color32 c = startColor;
+			Color32 black = SkeletonGhostRenderer.TransparentBlack;
+
+			float t = 1f;
+			for (float hardTimeLimit = 5f; hardTimeLimit > 0; hardTimeLimit -= Time.deltaTime) {
+				c = Color32.Lerp(black, startColor, t);
+				mpb.SetColor(colorId, c);
+				meshRenderer.SetPropertyBlock(mpb);
+
+				t = Mathf.Lerp(t, 0, Time.deltaTime * fadeSpeed);
+				if (t <= 0)
 					break;
 				
 				yield return null;
@@ -95,25 +96,20 @@ namespace Spine.Unity.Modules {
 		}
 
 		IEnumerator FadeAdditive () {
-			Color32 c;
-			Color32 black = this.black;
-
-			for (int t = 0; t < 500; t++) {
-
-				bool breakout = true;
-				for (int i = 0; i < colors.Length; i++) {
-					c = colors[i];
-					black.a = c.a;
-					if (c.r > 0 || c.g > 0 || c.b > 0)
-						breakout = false;
-
-					colors[i] = Color32.Lerp(c, black, Time.deltaTime * fadeSpeed);
-				}
-
-				meshFilter.sharedMesh.colors32 = colors;
-
-				if (breakout)
+			Color32 c = startColor;
+			Color32 black = SkeletonGhostRenderer.TransparentBlack;
+
+			float t = 1f;
+			
+			for (float hardTimeLimit = 5f; hardTimeLimit > 0; hardTimeLimit -= Time.deltaTime) {
+				c = Color32.Lerp(black, startColor, t);
+				mpb.SetColor(colorId, c);
+				meshRenderer.SetPropertyBlock(mpb);
+
+				t = Mathf.Lerp(t, 0, Time.deltaTime * fadeSpeed);
+				if (t <= 0)
 					break;
+
 				yield return null;
 			}
 

+ 9 - 5
spine-unity/Assets/Spine/Runtime/spine-unity/Modules/SkeletonGraphic/SkeletonGraphicMirror.cs

@@ -1,4 +1,4 @@
-/******************************************************************************
+/******************************************************************************
  * Spine Runtimes Software License v2.5
  *
  * Copyright (c) 2013-2016, Esoteric Software
@@ -49,7 +49,8 @@ namespace Spine.Unity.Modules {
 		}
 
 		void Start () {
-			if (mirrorOnStart) StartMirroring();
+			if (mirrorOnStart)
+				StartMirroring();
 		}
 
 		void LateUpdate () {
@@ -57,13 +58,16 @@ namespace Spine.Unity.Modules {
 		}
 
 		void OnDisable () {
-			if (restoreOnDisable) RestoreIndependentSkeleton();
+			if (restoreOnDisable)
+				RestoreIndependentSkeleton();
 		}
 
 		/// <summary>Freeze the SkeletonGraphic on this GameObject, and use the source as the Skeleton to be rendered by the SkeletonGraphic.</summary>
 		public void StartMirroring () {
-			if (source == null) return;
-			if (skeletonGraphic == null) return;
+			if (source == null)
+				return;
+			if (skeletonGraphic == null)
+				return;
 
 			skeletonGraphic.startingAnimation = string.Empty;
 

+ 1 - 0
spine-unity/Assets/Spine/Runtime/spine-unity/SkeletonExtensions.cs

@@ -503,6 +503,7 @@ namespace Spine {
 				ikc = skeleton.ikConstraints.Items[i];
 				ikc.mix = ikc.data.mix;
 				ikc.bendDirection = ikc.data.bendDirection;
+				ikc.stretch = ikc.data.stretch;
 				break;
 
 			// TransformConstraint

Some files were not shown because too many files changed in this diff