Jelajahi Sumber

IK and flip timelines for spine-js.

NathanSweet 11 tahun lalu
induk
melakukan
11633a0b22
1 mengubah file dengan 397 tambahan dan 113 penghapusan
  1. 397 113
      spine-js/spine.js

+ 397 - 113
spine-js/spine.js

@@ -40,7 +40,8 @@ spine.BoneData.prototype = {
 	rotation: 0,
 	scaleX: 1, scaleY: 1,
 	inheritScale: true,
-	inheritRotation: true
+	inheritRotation: true,
+	flipX: false, flipY: false
 };
 
 spine.SlotData = function (name, boneData) {
@@ -53,21 +54,34 @@ spine.SlotData.prototype = {
 	additiveBlending: false
 };
 
-spine.Bone = function (boneData, parent) {
+spine.IkConstraintData = function (name) {
+	this.name = name;
+	this.bones = [];
+};
+spine.IkConstraintData.prototype = {
+	target: null,
+	bendDirection: 1,
+	mix: 1
+};
+
+spine.Bone = function (boneData, skeleton, parent) {
 	this.data = boneData;
+	this.skeleton = skeleton;
 	this.parent = parent;
 	this.setToSetupPose();
 };
 spine.Bone.yDown = false;
 spine.Bone.prototype = {
 	x: 0, y: 0,
-	rotation: 0,
+	rotation: 0, rotationIK: 0,
 	scaleX: 1, scaleY: 1,
+	flipX: false, flipY: false,
 	m00: 0, m01: 0, worldX: 0, // a b x
 	m10: 0, m11: 0, worldY: 0, // c d y
 	worldRotation: 0,
 	worldScaleX: 1, worldScaleY: 1,
-	updateWorldTransform: function (flipX, flipY) {
+	worldFlipX: false, worldFlipY: false,
+	updateWorldTransform: function () {
 		var parent = this.parent;
 		if (parent != null) {
 			this.worldX = this.x * parent.m00 + this.y * parent.m01 + parent.worldX;
@@ -79,28 +93,35 @@ spine.Bone.prototype = {
 				this.worldScaleX = this.scaleX;
 				this.worldScaleY = this.scaleY;
 			}
-			this.worldRotation = this.data.inheritRotation ? parent.worldRotation + this.rotation : this.rotation;
+			this.worldRotation = this.data.inheritRotation ? parent.worldRotation + this.rotationIK : this.rotationIK;
+			this.worldFlipX = parent.worldFlipX != this.flipX;
+			this.worldFlipY = parent.worldFlipY != this.flipY;
 		} else {
-			this.worldX = flipX ? -this.x : this.x;
-			this.worldY = flipY != spine.Bone.yDown ? -this.y : this.y;
+			var skeletonFlipX = skeleton.flipX, skeletonFlipY = skeleton.flipY;
+			this.worldX = skeletonFlipX ? -this.x : this.x;
+			this.worldY = skeletonFlipY != spine.Bone.yDown ? -this.y : this.y;
 			this.worldScaleX = this.scaleX;
 			this.worldScaleY = this.scaleY;
-			this.worldRotation = this.rotation;
+			this.worldRotation = this.rotationIK;
+			this.worldFlipX = skeletonFlipX != this.flipX;
+			this.worldFlipY = skeletonFlipY != this.flipY;
 		}
 		var radians = this.worldRotation * Math.PI / 180;
 		var cos = Math.cos(radians);
 		var sin = Math.sin(radians);
-		this.m00 = cos * this.worldScaleX;
-		this.m10 = sin * this.worldScaleX;
-		this.m01 = -sin * this.worldScaleY;
-		this.m11 = cos * this.worldScaleY;
-		if (flipX) {
-			this.m00 = -this.m00;
-			this.m01 = -this.m01;
+		if (this.worldFlipX) {
+			this.m00 = -cos * this.worldScaleX;
+			this.m10 = sin * this.worldScaleX;
+		} else {
+			this.m00 = cos * this.worldScaleX;
+			this.m10 = -sin * this.worldScaleX;
 		}
-		if (flipY != spine.Bone.yDown) {
-			this.m10 = -this.m10;
-			this.m11 = -this.m11;
+		if (this.worldFlipY != spine.Bone.yDown) {
+			this.m01 = -sin * this.worldScaleY;
+			this.m11 = -cos * this.worldScaleY;
+		} else {
+			this.m01 = sin * this.worldScaleY;
+			this.m11 = cos * this.worldScaleY;
 		}
 	},
 	setToSetupPose: function () {
@@ -108,14 +129,32 @@ spine.Bone.prototype = {
 		this.x = data.x;
 		this.y = data.y;
 		this.rotation = data.rotation;
+		this.rotationIK = this.rotation;
 		this.scaleX = data.scaleX;
 		this.scaleY = data.scaleY;
+		this.flipX = data.flipX;
+		this.flipY = data.flipY;
+	},
+	worldToLocal: function (world) void {
+		var dx = world[0] - this.worldX, dy = world[1] - this.worldY;
+		var m00 = this.m00, m10 = this.m10, m01 = this.m01, m11 = this.m11;
+		if (this.worldFlipX != (this.worldFlipY != spine.Bone.yDown)) {
+			m00 = -m00;
+			m11 = -m11;
+		}
+		var invDet = 1 / (m00 * m11 - m01 * m10);
+		world[0] = (dx * m00 * invDet - dy * m01 * invDet);
+		world[1] = (dy * m11 * invDet - dx * m10 * invDet);
+	},
+	localToWorld: function (local) {
+		var localX = local[0], localY = local[1];
+		local[0] = localX * this.m00 + localY * this.m01 + this.worldX;
+		local[1] = localX * this.m10 + localY * this.m11 + this.worldY;
 	}
 };
 
-spine.Slot = function (slotData, skeleton, bone) {
+spine.Slot = function (slotData, bone) {
 	this.data = slotData;
-	this.skeleton = skeleton;
 	this.bone = bone;
 	this.setToSetupPose();
 };
@@ -126,14 +165,14 @@ spine.Slot.prototype = {
 	attachmentVertices: [],
 	setAttachment: function (attachment) {
 		this.attachment = attachment;
-		this._attachmentTime = this.skeleton.time;
+		this._attachmentTime = this.bone.skeleton.time;
 		this.attachmentVertices.length = 0;
 	},
 	setAttachmentTime: function (time) {
-		this._attachmentTime = this.skeleton.time - time;
+		this._attachmentTime = this.bone.skeleton.time - time;
 	},
 	getAttachmentTime: function () {
-		return this.skeleton.time - this._attachmentTime;
+		return this.bone.skeleton.time - this._attachmentTime;
 	},
 	setToSetupPose: function () {
 		var data = this.data;
@@ -142,16 +181,28 @@ spine.Slot.prototype = {
 		this.b = data.b;
 		this.a = data.a;
 
-		var slotDatas = this.skeleton.data.slots;
+		var slotDatas = this.bone.skeleton.data.slots;
 		for (var i = 0, n = slotDatas.length; i < n; i++) {
 			if (slotDatas[i] == data) {
-				this.setAttachment(!data.attachmentName ? null : this.skeleton.getAttachmentBySlotIndex(i, data.attachmentName));
+				this.setAttachment(!data.attachmentName ? null : this.bone.skeleton.getAttachmentBySlotIndex(i, data.attachmentName));
 				break;
 			}
 		}
 	}
 };
 
+spine.IkConstraint = function (data, skeleton) {
+	this.data = data;
+	this.mix = data.mix;
+	this.bendDirection = data.bendDirection;
+
+	this.bones = [];
+	for (var i = 0, n = data.bones.length; i < n; i++)
+		this.bones.push(skeleton.findBone(data.bones[i].name));
+	this.target = skeleton.findBone(data.target.name);
+};
+spine.IkConstraint.prototype = {};
+
 spine.Skin = function (name) {
 	this.name = name;
 	this.attachments = {};
@@ -217,6 +268,20 @@ spine.binarySearch = function (values, target, step) {
 		current = (low + high) >>> 1;
 	}
 };
+spine.binarySearch1 = function (values, target) {
+	var low = 0;
+	var high = values.length - 2;
+	if (high == 0) return step;
+	var current = high >>> 1;
+	while (true) {
+		if (values[current + 1] <= target)
+			low = current + 1;
+		else
+			high = current;
+		if (low == high) return low + 1;
+		current = (low + high) >>> 1;
+	}
+};
 spine.linearSearch = function (values, target, step) {
 	for (var i = 0, last = values.length - step; i <= last; i += step)
 		if (values[i] > target) return i;
@@ -224,62 +289,35 @@ spine.linearSearch = function (values, target, step) {
 };
 
 spine.Curves = function (frameCount) {
-	this.curves = []; // dfx, dfy, ddfx, ddfy, dddfx, dddfy, ...
-	this.curves.length = (frameCount - 1) * 6;
+	this.curves = []; // type, x, y, ...
+	this.curves.length = (frameCount - 1) * 19/*BEZIER_SIZE*/;
 };
 spine.Curves.prototype = {
 	setLinear: function (frameIndex) {
-		this.curves[frameIndex * 6] = 0/*LINEAR*/;
+		this.curves[frameIndex * 19/*BEZIER_SIZE*/] = 0/*LINEAR*/;
 	},
 	setStepped: function (frameIndex) {
-		this.curves[frameIndex * 6] = -1/*STEPPED*/;
+		this.curves[frameIndex * 19/*BEZIER_SIZE*/] = 1/*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. */
 	setCurve: function (frameIndex, cx1, cy1, cx2, cy2) {
-		var subdiv_step = 1 / 10/*BEZIER_SEGMENTS*/;
-		var subdiv_step2 = subdiv_step * subdiv_step;
-		var subdiv_step3 = subdiv_step2 * subdiv_step;
-		var pre1 = 3 * subdiv_step;
-		var pre2 = 3 * subdiv_step2;
-		var pre4 = 6 * subdiv_step2;
-		var pre5 = 6 * subdiv_step3;
-		var tmp1x = -cx1 * 2 + cx2;
-		var tmp1y = -cy1 * 2 + cy2;
-		var tmp2x = (cx1 - cx2) * 3 + 1;
-		var tmp2y = (cy1 - cy2) * 3 + 1;
-		var i = frameIndex * 6;
+		var subdiv1 = 1f / 10/*BEZIER_SEGMENTS*/, subdiv2 = subdiv1 * subdiv1, subdiv3 = subdiv2 * subdiv1;
+		var pre1 = 3 * subdiv1, pre2 = 3 * subdiv2, pre4 = 6 * subdiv2, pre5 = 6 * subdiv3;
+		var tmp1x = -cx1 * 2 + cx2, tmp1y = -cy1 * 2 + cy2, tmp2x = (cx1 - cx2) * 3 + 1, tmp2y = (cy1 - cy2) * 3 + 1;
+		var dfx = cx1 * pre1 + tmp1x * pre2 + tmp2x * subdiv3, dfy = cy1 * pre1 + tmp1y * pre2 + tmp2y * subdiv3;
+		var ddfx = tmp1x * pre4 + tmp2x * pre5, ddfy = tmp1y * pre4 + tmp2y * pre5;
+		var dddfx = tmp2x * pre5, dddfy = tmp2y * pre5;
+
+		var i = frameIndex * 19/*BEZIER_SIZE*/;
 		var 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;
-	},
-	getCurvePercent: function (frameIndex, percent) {
-		percent = percent < 0 ? 0 : (percent > 1 ? 1 : percent);
-		var curveIndex = frameIndex * 6;
-		var curves = this.curves;
-		var dfx = curves[curveIndex];
-		if (!dfx/*LINEAR*/) return percent;
-		if (dfx == -1/*STEPPED*/) return 0;
-		var dfy = curves[curveIndex + 1];
-		var ddfx = curves[curveIndex + 2];
-		var ddfy = curves[curveIndex + 3];
-		var dddfx = curves[curveIndex + 4];
-		var dddfy = curves[curveIndex + 5];
+		curves[i++] = 2/*BEZIER*/;
+
 		var x = dfx, y = dfy;
-		var i = 10/*BEZIER_SEGMENTS*/ - 2;
-		while (true) {
-			if (x >= percent) {
-				var lastX = x - dfx;
-				var lastY = y - dfy;
-				return lastY + (y - lastY) * (percent - lastX) / (x - lastX);
-			}
-			if (i == 0) break;
-			i--;
+		for (var n = i + 19/*BEZIER_SIZE*/ - 1; i < n; i += 2) {
+			curves[i] = x;
+			curves[i + 1] = y;
 			dfx += ddfx;
 			dfy += ddfy;
 			ddfx += dddfx;
@@ -287,6 +325,31 @@ spine.Curves.prototype = {
 			x += dfx;
 			y += dfy;
 		}
+	},
+	getCurvePercent: function (frameIndex, percent) {
+		percent = percent < 0 ? 0 : (percent > 1 ? 1 : percent);
+		var curves = this.curves;
+		var i = frameIndex * 10/*BEZIER_SIZE*/;
+		var type = curves[i];
+		if (type == 0/*LINEAR*/) return percent;
+		if (type == 1/*STEPPED*/) return 0;
+		i++;
+		var x = 0;
+		for (var start = i, n = i + 10/*BEZIER_SIZE*/ - 1; i < n; i += 2) {
+			x = curves[i];
+			if (x >= percent) {
+				var prevX, prevY;
+				if (i == start) {
+					prevX = 0;
+					prevY = 0;
+				} else {
+					prevX = curves[i - 2];
+					prevY = curves[i - 1];
+				}
+				return prevY + (curves[i + 1] - prevY) * (percent - prevX) / (x - prevX);
+			}
+		}
+		var y = curves[i - 1];
 		return y + (1 - y) * (percent - x) / (1 - x); // Last point is 1,1.
 	}
 };
@@ -322,19 +385,19 @@ spine.RotateTimeline.prototype = {
 			return;
 		}
 
-		// Interpolate between the last frame and the current frame.
+		// Interpolate between the previous frame and the current frame.
 		var frameIndex = spine.binarySearch(frames, time, 2);
-		var lastFrameValue = frames[frameIndex - 1];
+		var prevFrameValue = frames[frameIndex - 1];
 		var frameTime = frames[frameIndex];
-		var percent = 1 - (time - frameTime) / (frames[frameIndex - 2/*LAST_FRAME_TIME*/] - frameTime);
+		var percent = 1 - (time - frameTime) / (frames[frameIndex - 2/*PREV_FRAME_TIME*/] - frameTime);
 		percent = this.curves.getCurvePercent(frameIndex / 2 - 1, percent);
 
-		var amount = frames[frameIndex + 1/*FRAME_VALUE*/] - lastFrameValue;
+		var amount = frames[frameIndex + 1/*FRAME_VALUE*/] - prevFrameValue;
 		while (amount > 180)
 			amount -= 360;
 		while (amount < -180)
 			amount += 360;
-		amount = bone.data.rotation + (lastFrameValue + amount * percent) - bone.rotation;
+		amount = bone.data.rotation + (prevFrameValue + amount * percent) - bone.rotation;
 		while (amount > 180)
 			amount -= 360;
 		while (amount < -180)
@@ -371,16 +434,16 @@ spine.TranslateTimeline.prototype = {
 			return;
 		}
 
-		// Interpolate between the last frame and the current frame.
+		// Interpolate between the previous frame and the current frame.
 		var frameIndex = spine.binarySearch(frames, time, 3);
-		var lastFrameX = frames[frameIndex - 2];
-		var lastFrameY = frames[frameIndex - 1];
+		var prevFrameX = frames[frameIndex - 2];
+		var prevFrameY = frames[frameIndex - 1];
 		var frameTime = frames[frameIndex];
-		var percent = 1 - (time - frameTime) / (frames[frameIndex + -3/*LAST_FRAME_TIME*/] - frameTime);
+		var percent = 1 - (time - frameTime) / (frames[frameIndex + -3/*PREV_FRAME_TIME*/] - frameTime);
 		percent = this.curves.getCurvePercent(frameIndex / 3 - 1, percent);
 
-		bone.x += (bone.data.x + lastFrameX + (frames[frameIndex + 1/*FRAME_X*/] - lastFrameX) * percent - bone.x) * alpha;
-		bone.y += (bone.data.y + lastFrameY + (frames[frameIndex + 2/*FRAME_Y*/] - lastFrameY) * percent - bone.y) * alpha;
+		bone.x += (bone.data.x + prevFrameX + (frames[frameIndex + 1/*FRAME_X*/] - prevFrameX) * percent - bone.x) * alpha;
+		bone.y += (bone.data.y + prevFrameY + (frames[frameIndex + 2/*FRAME_Y*/] - prevFrameY) * percent - bone.y) * alpha;
 	}
 };
 
@@ -412,16 +475,16 @@ spine.ScaleTimeline.prototype = {
 			return;
 		}
 
-		// Interpolate between the last frame and the current frame.
+		// Interpolate between the previous frame and the current frame.
 		var frameIndex = spine.binarySearch(frames, time, 3);
-		var lastFrameX = frames[frameIndex - 2];
-		var lastFrameY = frames[frameIndex - 1];
+		var prevFrameX = frames[frameIndex - 2];
+		var prevFrameY = frames[frameIndex - 1];
 		var frameTime = frames[frameIndex];
-		var percent = 1 - (time - frameTime) / (frames[frameIndex + -3/*LAST_FRAME_TIME*/] - frameTime);
+		var percent = 1 - (time - frameTime) / (frames[frameIndex + -3/*PREV_FRAME_TIME*/] - frameTime);
 		percent = this.curves.getCurvePercent(frameIndex / 3 - 1, percent);
 
-		bone.scaleX += (bone.data.scaleX * (lastFrameX + (frames[frameIndex + 1/*FRAME_X*/] - lastFrameX) * percent) - bone.scaleX) * alpha;
-		bone.scaleY += (bone.data.scaleY * (lastFrameY + (frames[frameIndex + 2/*FRAME_Y*/] - lastFrameY) * percent) - bone.scaleY) * alpha;
+		bone.scaleX += (bone.data.scaleX * (prevFrameX + (frames[frameIndex + 1/*FRAME_X*/] - prevFrameX) * percent) - bone.scaleX) * alpha;
+		bone.scaleY += (bone.data.scaleY * (prevFrameY + (frames[frameIndex + 2/*FRAME_Y*/] - prevFrameY) * percent) - bone.scaleY) * alpha;
 	}
 };
 
@@ -456,20 +519,20 @@ spine.ColorTimeline.prototype = {
 			b = frames[i - 1];
 			a = frames[i];
 		} else {
-			// Interpolate between the last frame and the current frame.
+			// Interpolate between the previous frame and the current frame.
 			var frameIndex = spine.binarySearch(frames, time, 5);
-			var lastFrameR = frames[frameIndex - 4];
-			var lastFrameG = frames[frameIndex - 3];
-			var lastFrameB = frames[frameIndex - 2];
-			var lastFrameA = frames[frameIndex - 1];
+			var prevFrameR = frames[frameIndex - 4];
+			var prevFrameG = frames[frameIndex - 3];
+			var prevFrameB = frames[frameIndex - 2];
+			var prevFrameA = frames[frameIndex - 1];
 			var frameTime = frames[frameIndex];
-			var percent = 1 - (time - frameTime) / (frames[frameIndex - 5/*LAST_FRAME_TIME*/] - frameTime);
+			var percent = 1 - (time - frameTime) / (frames[frameIndex - 5/*PREV_FRAME_TIME*/] - frameTime);
 			percent = this.curves.getCurvePercent(frameIndex / 5 - 1, percent);
 
-			r = lastFrameR + (frames[frameIndex + 1/*FRAME_R*/] - lastFrameR) * percent;
-			g = lastFrameG + (frames[frameIndex + 2/*FRAME_G*/] - lastFrameG) * percent;
-			b = lastFrameB + (frames[frameIndex + 3/*FRAME_B*/] - lastFrameB) * percent;
-			a = lastFrameA + (frames[frameIndex + 4/*FRAME_A*/] - lastFrameA) * percent;
+			r = prevFrameR + (frames[frameIndex + 1/*FRAME_R*/] - prevFrameR) * percent;
+			g = prevFrameG + (frames[frameIndex + 2/*FRAME_G*/] - prevFrameG) * percent;
+			b = prevFrameB + (frames[frameIndex + 3/*FRAME_B*/] - prevFrameB) * percent;
+			a = prevFrameA + (frames[frameIndex + 4/*FRAME_A*/] - prevFrameA) * percent;
 		}
 		var slot = skeleton.slots[this.slotIndex];
 		if (alpha < 1) {
@@ -504,16 +567,18 @@ spine.AttachmentTimeline.prototype = {
 	},
 	apply: function (skeleton, lastTime, time, firedEvents, alpha) {
 		var frames = this.frames;
-		if (time < frames[0]) return; // Time is before first frame.
+		if (time < frames[0]) {
+			if (lastTime > time) this.apply(skeleton, lastTime, Number.MAX_VALUE, null, 0);
+			return;
+		} else if (lastTime > time) //
+			lastTime = -1;
 
-		var frameIndex;
-		if (time >= frames[frames.length - 1]) // Time is after last frame.
-			frameIndex = frames.length - 1;
-		else
-			frameIndex = spine.binarySearch(frames, time, 1) - 1;
+		var frameIndex = time >= frames[frames.length - 1] ? frames.length - 1 : spine.binarySearch1(frames, time) - 1;
+		if (frames[frameIndex] < lastTime) return;
 
 		var attachmentName = this.attachmentNames[frameIndex];
-		skeleton.slots[this.slotIndex].setAttachment(!attachmentName ? null : skeleton.getAttachmentBySlotIndex(this.slotIndex, attachmentName));
+		skeleton.slots[this.slotIndex].setAttachment(
+			!attachmentName ? null : skeleton.getAttachmentBySlotIndex(this.slotIndex, attachmentName));
 	}
 };
 
@@ -549,7 +614,7 @@ spine.EventTimeline.prototype = {
 		if (lastTime < frames[0])
 			frameIndex = 0;
 		else {
-			frameIndex = spine.binarySearch(frames, lastTime, 1);
+			frameIndex = spine.binarySearch1(frames, lastTime);
 			var frame = frames[frameIndex];
 			while (frameIndex > 0) { // Fire multiple events with the same frame.
 				if (frames[frameIndex - 1] != frame) break;
@@ -584,7 +649,7 @@ spine.DrawOrderTimeline.prototype = {
 		if (time >= frames[frames.length - 1]) // Time is after last frame.
 			frameIndex = frames.length - 1;
 		else
-			frameIndex = spine.binarySearch(frames, time, 1) - 1;
+			frameIndex = spine.binarySearch1(frames, time) - 1;
 
 		var drawOrder = skeleton.drawOrder;
 		var slots = skeleton.slots;
@@ -647,7 +712,7 @@ spine.FfdTimeline.prototype = {
 		}
 
 		// Interpolate between the previous frame and the current frame.
-		var frameIndex = spine.binarySearch(frames, time, 1);
+		var frameIndex = spine.binarySearch1(frames, time);
 		var frameTime = frames[frameIndex];
 		var percent = 1 - (time - frameTime) / (frames[frameIndex - 1] - frameTime);
 		percent = this.curves.getCurvePercent(frameIndex - 1, percent < 0 ? 0 : (percent > 1 ? 1 : percent));
@@ -669,15 +734,115 @@ spine.FfdTimeline.prototype = {
 	}
 };
 
+spine.IkConstraintTimeline = function (frameCount) {
+	this.curves = new spine.Curves(frameCount);
+	this.frames = []; // time, mix, bendDirection, ...
+	this.frames.length = frameCount * 3;
+};
+spine.IkConstraintTimeline.prototype = {
+	ikConstraintIndex: 0,
+	getFrameCount: function () {
+		return this.frames.length / 3;
+	},
+	setFrame: function (frameIndex, time, mix, bendDirection) {
+		frameIndex *= 3;
+		this.frames[frameIndex] = time;
+		this.frames[frameIndex + 1] = mix;
+		this.frames[frameIndex + 2] = bendDirection;
+	},
+	apply: function (skeleton, lastTime, time, firedEvents, alpha) {
+		var frames = this.frames;
+		if (time < frames[0]) return; // Time is before first frame.
+
+		var ikConstraint = skeleton.ikConstraints[this.ikConstraintIndex];
+
+		if (time >= frames[frames.length - 3]) { // Time is after last frame.
+			ikConstraint.mix += (frames[frames.length - 2] - ikConstraint.mix) * alpha;
+			ikConstraint.bendDirection = frames[frames.length - 1];
+			return;
+		}
+
+		// Interpolate between the previous frame and the current frame.
+		var frameIndex = spine.binarySearch(frames, time, 3);
+		var prevFrameMix = frames[frameIndex + -2/*PREV_FRAME_MIX*/];
+		var frameTime = frames[frameIndex];
+		var percent = 1 - (time - frameTime) / (frames[frameIndex + -3/*PREV_FRAME_TIME*/] - frameTime);
+		percent = this.curves.getCurvePercent(frameIndex / 3 - 1, percent);
+
+		var mix = prevFrameMix + (frames[frameIndex + 1/*FRAME_MIX*/] - prevFrameMix) * percent;
+		ikConstraint.mix += (mix - ikConstraint.mix) * alpha;
+		ikConstraint.bendDirection = frames[frameIndex + -1/*PREV_FRAME_BEND_DIRECTION*/];
+	}
+};
+
+spine.FlipXTimeline = function (frameCount) {
+	this.curves = new spine.Curves(frameCount);
+	this.frames = []; // time, flip, ...
+	this.frames.length = frameCount * 2;
+};
+spine.FlipXTimeline.prototype = {
+	boneIndex: 0,
+	getFrameCount: function () {
+		return this.frames.length / 2;
+	},
+	setFrame: function (frameIndex, time, flip) {
+		frameIndex *= 2;
+		this.frames[frameIndex] = time;
+		this.frames[frameIndex + 1] = flip ? 1 : 0;
+	},
+	apply: function (skeleton, lastTime, time, firedEvents, alpha) {
+		var frames = this.frames;
+		if (time < frames[0]) {
+			if (lastTime > time) this.apply(skeleton, lastTime, Number.MAX_VALUE, null, 0);
+			return;
+		} else if (lastTime > time) //
+			lastTime = -1;
+		var frameIndex = (time >= frames[frames.length - 2] ? frames.length : spine.binarySearch(frames, time, 2)) - 2;
+		if (frames[frameIndex] < lastTime) return;
+		bone.flipX = skeleton.bones.get(boneIndex), frames[frameIndex + 1] != 0;
+	}
+};
+
+spine.FlipYTimeline = function (frameCount) {
+	this.curves = new spine.Curves(frameCount);
+	this.frames = []; // time, flip, ...
+	this.frames.length = frameCount * 2;
+};
+spine.FlipYTimeline.prototype = {
+	boneIndex: 0,
+	getFrameCount: function () {
+		return this.frames.length / 2;
+	},
+	setFrame: function (frameIndex, time, flip) {
+		frameIndex *= 2;
+		this.frames[frameIndex] = time;
+		this.frames[frameIndex + 1] = flip ? 1 : 0;
+	},
+	apply: function (skeleton, lastTime, time, firedEvents, alpha) {
+		var frames = this.frames;
+		if (time < frames[0]) {
+			if (lastTime > time) this.apply(skeleton, lastTime, Number.MAX_VALUE, null, 0);
+			return;
+		} else if (lastTime > time) //
+			lastTime = -1;
+		var frameIndex = (time >= frames[frames.length - 2] ? frames.length : spine.binarySearch(frames, time, 2)) - 2;
+		if (frames[frameIndex] < lastTime) return;
+		bone.flipY = skeleton.bones.get(boneIndex), frames[frameIndex + 1] != 0;
+	}
+};
+
 spine.SkeletonData = function () {
 	this.bones = [];
 	this.slots = [];
 	this.skins = [];
 	this.events = [];
 	this.animations = [];
+	this.ikConstraints = [];
 };
 spine.SkeletonData.prototype = {
 	defaultSkin: null,
+	width: 0, height: 0,
+	version: null, hash: null,
 	/** @return May be null. */
 	findBone: function (boneName) {
 		var bones = this.bones;
@@ -727,6 +892,13 @@ spine.SkeletonData.prototype = {
 		for (var i = 0, n = animations.length; i < n; i++)
 			if (animations[i].name == animationName) return animations[i];
 		return null;
+	},
+	/** @return May be null. */
+	findIkConstraint: function (ikConstraintName) {
+		var ikConstraints = this.ikConstraints;
+		for (var i = 0, n = ikConstraints.length; i < n; i++)
+			if (ikConstraints[i].name == ikConstraintName) return ikConstraints[i];
+		return null;
 	}
 };
 
@@ -737,7 +909,7 @@ spine.Skeleton = function (skeletonData) {
 	for (var i = 0, n = skeletonData.bones.length; i < n; i++) {
 		var boneData = skeletonData.bones[i];
 		var parent = !boneData.parent ? null : this.bones[skeletonData.bones.indexOf(boneData.parent)];
-		this.bones.push(new spine.Bone(boneData, parent));
+		this.bones.push(new spine.Bone(boneData, this, parent));
 	}
 
 	this.slots = [];
@@ -745,10 +917,17 @@ spine.Skeleton = function (skeletonData) {
 	for (var i = 0, n = skeletonData.slots.length; i < n; i++) {
 		var slotData = skeletonData.slots[i];
 		var bone = this.bones[skeletonData.bones.indexOf(slotData.boneData)];
-		var slot = new spine.Slot(slotData, this, bone);
+		var slot = new spine.Slot(slotData, bone);
 		this.slots.push(slot);
 		this.drawOrder.push(slot);
 	}
+	
+	this.ikConstraints = [];
+	for (var i = 0, n = skeletonData.ikConstraints.length; i < n; i++)
+		this.ikConstraints.push(new spine.IkConstraint(skeletonData.ikConstraints[i], this));
+
+	this.boneCache = [];
+	this.updateCache();
 };
 spine.Skeleton.prototype = {
 	x: 0, y: 0,
@@ -756,13 +935,63 @@ spine.Skeleton.prototype = {
 	r: 1, g: 1, b: 1, a: 1,
 	time: 0,
 	flipX: false, flipY: false,
+	updateCache: function () {
+		var ikConstraints = this.ikConstraints;
+		var ikConstraintsCount = ikConstraints.length;
+
+		var arrayCount = ikConstraintsCount + 1;
+		var boneCache = this.boneCache;
+		if (boneCache.length > arrayCount) boneCache.splice(arrayCount, boneCache.length - arrayCount);
+
+		for (var i = 0, n = boneCache.length; i < n; i++) 
+			boneCache[i].length = 0;
+		while (boneCache.length < arrayCount)
+			boneCache[boneCache.length] = [];
+
+		var nonIkBones = boneCache[0];
+		var bones = this.bones;
+
+		outer:
+		for (var i = 0, n = bones.length; i < n; i++) {
+			var bone = bones[i];
+			var current = bone;
+			do {
+				for (var ii = 0, nn = bones.length; ii < nn; ii++) {
+					var ikConstraint = ikConstraints[ii];
+					var parent = ikConstraint.bones[0];
+					var child= ikConstraint.bones[ikConstraint.bones.length - 1];
+					while (true) {
+						if (current == child) {
+							boneCache[ii].push(bone);
+							boneCache[ii + 1].push(bone);
+							continue outer;
+						}
+						if (child == parent) break;
+						child = child.parent;
+					}
+					ii++;
+				}
+				current = current.parent;
+			} while (current != null);
+			nonIkBones[nonIkBones.length] = bone;
+		}
+	},
 	/** Updates the world transform for each bone. */
 	updateWorldTransform: function () {
-		var flipX = this.flipX;
-		var flipY = this.flipY;
 		var bones = this.bones;
-		for (var i = 0, n = bones.length; i < n; i++)
-			bones[i].updateWorldTransform(flipX, flipY);
+		for (var i = 0, n = bones.length; i < n; i++) {
+			var bone = bones[i];
+			bone.rotationIK = bone.rotation;
+		}
+		var i = 0, last = this.boneCache.length - 1;
+		while (true) {
+			var cacheBones = this.boneCache[i];
+			for (var ii = 0, nn = cacheBones.length; ii < nn; ii++)
+				cacheBones[ii].updateWorldTransform();
+			if (i == last) break;
+			this.ikConstraints[i].apply();
+			i++;
+		}
 	},
 	/** Sets the bones and slots to their setup pose values. */
 	setToSetupPose: function () {
@@ -773,6 +1002,13 @@ spine.Skeleton.prototype = {
 		var bones = this.bones;
 		for (var i = 0, n = bones.length; i < n; i++)
 			bones[i].setToSetupPose();
+
+		var ikConstraints = this.ikConstraints;
+		for (var i = 0, n = ikConstraints.length; i < n; i++) {
+			var ikConstraint = ikConstraints[i];
+			ikConstraint.bendDirection = ikConstraint.data.bendDirection;
+			ikConstraint.mix = ikConstraint.data.mix;
+		}
 	},
 	setSlotsToSetupPose: function () {
 		var slots = this.slots;
@@ -871,6 +1107,12 @@ spine.Skeleton.prototype = {
 		}
 		throw "Slot not found: " + slotName;
 	},
+	findIkConstraint: function (ikConstraintName) {
+		var ikConstraints = this.ikConstraints;
+		for (var i = 0, n = ikConstraints.length; i < n; i++)
+			if (ikConstraints[i].data.name == ikConstraintName) return ikConstraints[i];
+		return null;
+	},
 	update: function (delta) {
 		this.time += delta;
 	}
@@ -1081,7 +1323,7 @@ spine.SkinnedMeshAttachment.prototype = {
 		}
 	},
 	computeWorldVertices: function (x, y, slot, worldVertices) {
-		var skeletonBones = slot.skeleton.bones;
+		var skeletonBones = slot.bone.skeleton.bones;
 		var weights = this.weights;
 		var bones = this.bones;
 
@@ -1386,6 +1628,28 @@ spine.SkeletonJson.prototype = {
 			skeletonData.bones.push(boneData);
 		}
 
+		// IK constraints.
+		var ik = root["ik"];
+		for (var i = 0, n = ik.length; i < n; i++) {
+			var ikMap = ik[i];
+			var ikConstraintData = new spine.IkConstraintData(ikMap["name"]);
+
+			var bones = ikMap["bones"];
+			for (var ii = 0, nn = bones.length; ii < nn; ii++) {
+				var bone = skeletonData.findBone(bones[ii]);
+				if (!bone) throw "IK bone not found: " + bones[ii];
+				ikConstraintData.bones.push(bone);
+			}
+
+			ikConstraintData.target = skeletonData.findBone(ikMap["target"]);
+			if (!ikConstraintData.target) throw "Target bone not found: " + ikMap["target"];
+
+			ikConstraintData.bendDirection = (!ikMap.hasOwnProperty("bendPositive") || ikMap["bendPositive"]) ? 1 : -1;
+			ikConstraintData.mix = ikMap.hasOwnProperty("mix") ? ikMap["mix"] : 1;
+
+			skeletonData.ikConstraints.push(ikConstraintData);
+		}
+
 		// Slots.
 		var slots = root["slots"];
 		for (var i = 0, n = slots.length; i < n; i++) {
@@ -1649,6 +1913,26 @@ spine.SkeletonJson.prototype = {
 			}
 		}
 
+		var ikMap = map["ik"];
+		for (var ikConstraintName in ikMap) {
+			if (!ikMap.hasOwnProperty(ikConstraintName)) continue;
+			var ikConstraint = skeletonData.findIkConstraint(ikConstraintName);
+			var values = ikMap[ikConstraintName];
+			var timeline = new spine.IkConstraintTimeline(values.length);
+			timeline.ikConstraintIndex = skeletonData.ikConstraints.indexOf(ikConstraint);
+			var frameIndex = 0;
+			for (var i = 0, n = values.length; i < n; i++) {
+				var valueMap = values[i];
+				var mix = valueMap.hasOwnProperty("mix") ? valueMap["mix"] : 1;
+				var bendDirection = (!valueMap.hasOwnProperty("bendPositive") || valueMap["bendPositive"]) ? 1 : -1;
+				timeline.setFrame(frameIndex, valueMap["time"], mix, bendDirection);
+				this.readCurve(timeline, frameIndex, valueMap);
+				frameIndex++;
+			}
+			timelines.push(timeline);
+			duration = Math.max(duration, timeline.frames[timeline.frameCount * 3 - 3]);
+		}
+
 		var ffd = map["ffd"];
 		for (var skinName in ffd) {
 			var skin = skeletonData.findSkin(skinName);